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,327 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ipaddr"
|
|
4
|
+
require "digest"
|
|
5
|
+
|
|
6
|
+
module Netstack
|
|
7
|
+
module Vxlan
|
|
8
|
+
module CiscoNxos
|
|
9
|
+
# Parses NX-OS configuration contexts, keeping ACL order and routing policy.
|
|
10
|
+
# The caller supplies transport completeness; parsing cannot prove a file is whole.
|
|
11
|
+
class ConfigParser
|
|
12
|
+
def parse(text, target:, complete: false)
|
|
13
|
+
Validation.invalid!("configuration must be a String") unless text.is_a?(String)
|
|
14
|
+
Validation.invalid!("complete must be boolean") unless [true, false].include?(complete)
|
|
15
|
+
issues = []
|
|
16
|
+
sections = tree(text)
|
|
17
|
+
interfaces = parse_interfaces(sections, issues)
|
|
18
|
+
vlan_vnis = parse_vlans(sections, issues)
|
|
19
|
+
nve = parse_nve(sections, issues)
|
|
20
|
+
bgp = parse_bgp(sections)
|
|
21
|
+
role = target.attributes[:role] || target.attributes["role"]
|
|
22
|
+
role = if role
|
|
23
|
+
role.to_sym
|
|
24
|
+
else
|
|
25
|
+
(if nve.empty?
|
|
26
|
+
reflector?(bgp) ? :spine : :unknown
|
|
27
|
+
else
|
|
28
|
+
:leaf
|
|
29
|
+
end)
|
|
30
|
+
end
|
|
31
|
+
hostname = sections.filter_map { |entry| entry[:text][/\Ahostname\s+(\S+)\z/, 1] }.last
|
|
32
|
+
issues << { code: :hostname_missing } unless hostname
|
|
33
|
+
segments = build_segments(interfaces, vlan_vnis, issues)
|
|
34
|
+
management_ip = target.attributes[:management_ip] || target.attributes["management_ip"] || ip_address(target.host)
|
|
35
|
+
Node.new(key: target.key, hostname: hostname, management_ip: management_ip, role: role,
|
|
36
|
+
features: sections.filter_map { |entry| entry[:text][/\Afeature\s+(.+)\z/, 1] },
|
|
37
|
+
interfaces: interfaces, vlan_vnis: vlan_vnis, vrfs: parse_vrfs(sections), nve: nve,
|
|
38
|
+
evpn: parse_evpn(sections), bgp: bgp, vpc_domain: parse_vpc(sections, interfaces),
|
|
39
|
+
acls: parse_acls(sections), prefix_lists: parse_prefix_lists(sections),
|
|
40
|
+
route_maps: parse_route_maps(sections), segments: segments,
|
|
41
|
+
anycast_mac: sections.filter_map { |entry| entry[:text][/\Afabric forwarding anycast-gateway-mac (\S+)\z/, 1] }.last,
|
|
42
|
+
complete: complete && issues.empty?, issues: issues, config_digest: config_digest(text))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def tree(text)
|
|
48
|
+
roots = []
|
|
49
|
+
stack = []
|
|
50
|
+
text.gsub("\r", "").each_line.with_index(1) do |line, number|
|
|
51
|
+
value = line.strip
|
|
52
|
+
next if value.empty? || value.start_with?("!", "#")
|
|
53
|
+
|
|
54
|
+
indentation = line[/\A\s*/].size
|
|
55
|
+
entry = { text: value, line: number, children: [] }
|
|
56
|
+
stack.pop while stack.any? && stack.last[0] >= indentation
|
|
57
|
+
(stack.empty? ? roots : stack.last[1][:children]) << entry
|
|
58
|
+
stack << [indentation, entry]
|
|
59
|
+
end
|
|
60
|
+
roots
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def direct(entry) = entry[:children].map { |child| child[:text] }
|
|
64
|
+
def value(lines, pattern) = lines.filter_map { |line| line[pattern, 1] }.last
|
|
65
|
+
|
|
66
|
+
def config_digest(text)
|
|
67
|
+
commands = text.gsub("\r", "").lines.reject { |line| line.strip.empty? || line.lstrip.start_with?("!") }
|
|
68
|
+
Digest::SHA256.hexdigest(commands.map(&:rstrip).join("\n"))
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def parse_interfaces(sections, issues)
|
|
72
|
+
sections.each_with_object({}) do |entry, result|
|
|
73
|
+
name = entry[:text][/\Ainterface (\S+)\z/, 1]
|
|
74
|
+
next unless name
|
|
75
|
+
|
|
76
|
+
name = name.sub(/\Avlan/i, "Vlan")
|
|
77
|
+
lines = direct(entry)
|
|
78
|
+
addresses = lines.filter_map { |line| line[/\Aip address (\S+)(?: secondary)?\z/, 1] }
|
|
79
|
+
issues << { code: :unsupported_interface_address, interface: name, line: entry[:line] } if lines.grep(/\Aip address /).size != addresses.size
|
|
80
|
+
valid_addresses = addresses.filter_map do |address|
|
|
81
|
+
unless %r{\A\d+(?:\.\d+){3}/\d+\z}.match?(address) && ip_prefix?(address)
|
|
82
|
+
issues << { code: :invalid_interface_address, interface: name, line: entry[:line] }
|
|
83
|
+
next
|
|
84
|
+
end
|
|
85
|
+
address
|
|
86
|
+
end
|
|
87
|
+
result[name] = {
|
|
88
|
+
addresses: valid_addresses, vrf: value(lines, /\Avrf member (\S+)\z/),
|
|
89
|
+
anycast: lines.include?("fabric forwarding mode anycast-gateway"),
|
|
90
|
+
shutdown: if lines.include?("shutdown")
|
|
91
|
+
true
|
|
92
|
+
else
|
|
93
|
+
lines.include?("no shutdown") ? false : nil
|
|
94
|
+
end,
|
|
95
|
+
acl_bindings: lines.each_with_object({}) do |line, acls|
|
|
96
|
+
acls[Regexp.last_match(2).to_sym] = Regexp.last_match(1) if /\Aip access-group (\S+) (in|out)\z/.match(line)
|
|
97
|
+
end,
|
|
98
|
+
vpc_id: value(lines, /\Avpc (\d+)\z/)&.to_i,
|
|
99
|
+
peer_link: lines.include?("vpc peer-link"),
|
|
100
|
+
channel_group: value(lines, /\Achannel-group (\d+)(?: mode \S+)?\z/)&.to_i,
|
|
101
|
+
ospf: value(lines, /\Aip router ospf (.+)\z/),
|
|
102
|
+
description: value(lines, /\Adescription (.+)\z/)
|
|
103
|
+
}
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def parse_vlans(sections, issues)
|
|
108
|
+
sections.each_with_object({}) do |entry, result|
|
|
109
|
+
match = /\Avlan ([\d,-]+)\z/.match(entry[:text])
|
|
110
|
+
next unless match
|
|
111
|
+
|
|
112
|
+
ids = expand_ids(match[1], 1..4094)
|
|
113
|
+
vni = value(direct(entry), /\Avn-segment (\d+)\z/)&.to_i
|
|
114
|
+
issues << { code: :invalid_vlan_mapping, line: entry[:line] } if direct(entry).any? { |line| line.start_with?("vn-segment ") } && vni.nil?
|
|
115
|
+
if !ids || (vni && (ids.size != 1 || !vni.between?(1, 16_777_215)))
|
|
116
|
+
issues << { code: :invalid_vlan_mapping, line: entry[:line] }
|
|
117
|
+
next
|
|
118
|
+
end
|
|
119
|
+
ids.each { |id| result[id] = vni }
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def expand_ids(text, range)
|
|
124
|
+
entries = text.split(",").flat_map do |part|
|
|
125
|
+
first, last = part.split("-").map(&:to_i)
|
|
126
|
+
return nil unless range.cover?(first) && (!last || (range.cover?(last) && last >= first))
|
|
127
|
+
|
|
128
|
+
last ? (first..last).to_a : [first]
|
|
129
|
+
end
|
|
130
|
+
entries.uniq
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def parse_nve(sections, issues)
|
|
134
|
+
entries = sections.select { |entry| /\Ainterface nve\d+\z/i.match?(entry[:text]) }
|
|
135
|
+
issues << { code: :multiple_nve_interfaces } if entries.size > 1
|
|
136
|
+
return {} if entries.empty?
|
|
137
|
+
|
|
138
|
+
entry = entries.first
|
|
139
|
+
lines = direct(entry)
|
|
140
|
+
members = {}
|
|
141
|
+
entry[:children].each do |child|
|
|
142
|
+
match = /\Amember vni (\d+)(?: (associate-vrf))?\z/.match(child[:text])
|
|
143
|
+
unless match
|
|
144
|
+
issues << { code: :unsupported_nve_member, line: child[:line] } if child[:text].start_with?("member vni ")
|
|
145
|
+
next
|
|
146
|
+
end
|
|
147
|
+
settings = direct(child)
|
|
148
|
+
members[match[1].to_i] = {
|
|
149
|
+
associate_vrf: !match[2].nil?, replication: settings.include?("ingress-replication protocol bgp") ? :bgp : nil,
|
|
150
|
+
multicast_group: value(settings, /\Amcast-group (\S+)\z/)
|
|
151
|
+
}
|
|
152
|
+
end
|
|
153
|
+
{ interface: entry[:text].split.last, source_interface: value(lines, /\Asource-interface (\S+)\z/),
|
|
154
|
+
host_reachability: value(lines, /\Ahost-reachability protocol (\S+)\z/),
|
|
155
|
+
shutdown: if lines.include?("shutdown")
|
|
156
|
+
true
|
|
157
|
+
else
|
|
158
|
+
lines.include?("no shutdown") ? false : nil
|
|
159
|
+
end, members: members }
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def route_targets(lines)
|
|
163
|
+
lines.filter_map do |line|
|
|
164
|
+
match = /\Aroute-target (import|export|both) (\S+)(?: (evpn))?\z/.match(line)
|
|
165
|
+
{ direction: match[1].to_sym, target: match[2], evpn: !match[3].nil? } if match
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def parse_vrfs(sections)
|
|
170
|
+
sections.each_with_object({}) do |entry, vrfs|
|
|
171
|
+
name = entry[:text][/\Avrf context (\S+)\z/, 1]
|
|
172
|
+
next unless name
|
|
173
|
+
|
|
174
|
+
lines = direct(entry)
|
|
175
|
+
families = entry[:children].select { |child| child[:text].start_with?("address-family ") }
|
|
176
|
+
vrfs[name] = { vni: value(lines, /\Avni (\d+)\z/)&.to_i, rd: value(lines, /\Ard (\S+)\z/),
|
|
177
|
+
route_targets: route_targets(lines + families.flat_map { |family| direct(family) }),
|
|
178
|
+
routes: lines.filter_map do |line|
|
|
179
|
+
match = /\Aip route (\S+) (\S+)(.*)\z/.match(line)
|
|
180
|
+
{ prefix: match[1], next_hop: match[2], options: match[3].strip } if match
|
|
181
|
+
end }
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def parse_evpn(sections)
|
|
186
|
+
sections.select { |entry| entry[:text] == "evpn" }.flat_map { |entry| entry[:children] }.each_with_object({}) do |entry, vnis|
|
|
187
|
+
match = /\Avni (\d+) (L[23])/i.match(entry[:text])
|
|
188
|
+
next unless match
|
|
189
|
+
|
|
190
|
+
lines = direct(entry)
|
|
191
|
+
vnis[match[1].to_i] = { type: match[2].downcase.to_sym, rd: value(lines, /\Ard (\S+)\z/), route_targets: route_targets(lines) }
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def parse_bgp(sections)
|
|
196
|
+
entry = sections.find { |section| /\Arouter bgp (\d+)\z/.match?(section[:text]) }
|
|
197
|
+
return {} unless entry
|
|
198
|
+
|
|
199
|
+
lines = direct(entry)
|
|
200
|
+
result = { asn: entry[:text].split.last.to_i, router_id: value(lines, /\Arouter-id (\S+)\z/), neighbors: {}, templates: {}, vrfs: {} }
|
|
201
|
+
entry[:children].each do |child|
|
|
202
|
+
match = /\A(neighbor|template peer|vrf) (\S+)\z/.match(child[:text])
|
|
203
|
+
next unless match
|
|
204
|
+
|
|
205
|
+
settings = direct(child)
|
|
206
|
+
families = child[:children].select { |sub| sub[:text].start_with?("address-family ") }.to_h do |family|
|
|
207
|
+
members = direct(family)
|
|
208
|
+
[family[:text].delete_prefix("address-family "), {
|
|
209
|
+
route_reflector_client: if members.include?("route-reflector-client")
|
|
210
|
+
true
|
|
211
|
+
else
|
|
212
|
+
members.include?("no route-reflector-client") ? false : nil
|
|
213
|
+
end,
|
|
214
|
+
extended_community: if members.include?("send-community extended") || members.include?("send-community both")
|
|
215
|
+
true
|
|
216
|
+
else
|
|
217
|
+
members.include?("no send-community extended") ? false : nil
|
|
218
|
+
end,
|
|
219
|
+
advertise_evpn: members.include?("advertise l2vpn evpn"),
|
|
220
|
+
redistribute: members.filter_map do |line|
|
|
221
|
+
m = /\Aredistribute (\S+)(?: route-map (\S+))?\z/.match(line)
|
|
222
|
+
{ protocol: m[1], route_map: m[2] } if m
|
|
223
|
+
end
|
|
224
|
+
}]
|
|
225
|
+
end
|
|
226
|
+
kind = { "neighbor" => :neighbors, "template peer" => :templates, "vrf" => :vrfs }.fetch(match[1])
|
|
227
|
+
result[kind][match[2]] = { remote_as: value(settings, /\Aremote-as (\S+)\z/), inherit_peer: value(settings, /\Ainherit peer (\S+)\z/),
|
|
228
|
+
update_source: value(settings, /\Aupdate-source (\S+)\z/), address_families: families }
|
|
229
|
+
end
|
|
230
|
+
result
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def reflector?(bgp)
|
|
234
|
+
%i[neighbors templates].any? do |kind|
|
|
235
|
+
bgp.fetch(kind, {}).values.any? do |peer|
|
|
236
|
+
peer[:address_families].values.any? do |family|
|
|
237
|
+
family[:route_reflector_client]
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def parse_vpc(sections, interfaces)
|
|
244
|
+
entry = sections.find { |section| /\Avpc domain \d+\z/.match?(section[:text]) }
|
|
245
|
+
return unless entry
|
|
246
|
+
|
|
247
|
+
lines = direct(entry)
|
|
248
|
+
keepalive = value(lines, /\Apeer-keepalive (.+)\z/)
|
|
249
|
+
keepalive = keepalive ? keepalive.scan(/(destination|source|vrf) (\S+)/).to_h.transform_keys(&:to_sym) : {}
|
|
250
|
+
VpcDomain.new(id: entry[:text].split.last.to_i, peer_keepalive: keepalive,
|
|
251
|
+
peer_links: interfaces.select { |_name, settings| settings[:peer_link] }.keys,
|
|
252
|
+
member_ports: interfaces.filter_map { |name, settings| [settings[:vpc_id], name] if settings[:vpc_id] }.to_h,
|
|
253
|
+
peer_gateway: lines.include?("peer-gateway"))
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def parse_acls(sections)
|
|
257
|
+
sections.each_with_object({}) do |entry, acls|
|
|
258
|
+
name = entry[:text][/\Aip access-list (\S+)\z/, 1]
|
|
259
|
+
next unless name
|
|
260
|
+
|
|
261
|
+
acls[name] = direct(entry).filter_map do |line|
|
|
262
|
+
next if /\A(?:\d+ )?remark\b/.match?(line) || line == "statistics per-entry"
|
|
263
|
+
|
|
264
|
+
match = /\A(\d+) (permit|deny) (.+)\z/.match(line)
|
|
265
|
+
match ? { sequence: match[1].to_i, action: match[2].to_sym, expression: match[3] } : { unsupported: true }
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def parse_prefix_lists(sections)
|
|
271
|
+
sections.each_with_object({}) do |entry, lists|
|
|
272
|
+
match = /\Aip prefix-list (\S+) seq (\d+) (permit|deny) (\S+)(.*)\z/.match(entry[:text])
|
|
273
|
+
next unless match
|
|
274
|
+
|
|
275
|
+
(lists[match[1]] ||= []) << { sequence: match[2].to_i, action: match[3].to_sym, prefix: match[4],
|
|
276
|
+
ge: match[5][/\bge (\d+)\b/, 1]&.to_i, le: match[5][/\ble (\d+)\b/, 1]&.to_i }
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def parse_route_maps(sections)
|
|
281
|
+
sections.each_with_object({}) do |entry, maps|
|
|
282
|
+
match = /\Aroute-map (\S+) (permit|deny) (\d+)\z/.match(entry[:text])
|
|
283
|
+
next unless match
|
|
284
|
+
|
|
285
|
+
lines = direct(entry)
|
|
286
|
+
(maps[match[1]] ||= []) << { action: match[2].to_sym, sequence: match[3].to_i,
|
|
287
|
+
prefix_lists: lines.filter_map { |line| line[/\Amatch ip address prefix-list (.+)\z/, 1] }.flat_map(&:split),
|
|
288
|
+
additional_matches: lines.grep(/\Amatch /).reject { |line| line.start_with?("match ip address prefix-list ") },
|
|
289
|
+
sets: lines.grep(/\Aset /) }
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def build_segments(interfaces, vlan_vnis, issues)
|
|
294
|
+
interfaces.filter_map do |name, settings|
|
|
295
|
+
match = /\AVlan(\d+)\z/i.match(name)
|
|
296
|
+
next unless match && vlan_vnis[match[1].to_i] && settings[:addresses].any?
|
|
297
|
+
|
|
298
|
+
if settings[:addresses].size > 1 || !settings[:vrf]
|
|
299
|
+
issues << { code: :unsupported_svi, interface: name }
|
|
300
|
+
next
|
|
301
|
+
end
|
|
302
|
+
address = settings[:addresses].first
|
|
303
|
+
network = IPAddr.new(address)
|
|
304
|
+
Segment.new(vlan_id: match[1].to_i, vni: vlan_vnis.fetch(match[1].to_i), vrf: settings[:vrf],
|
|
305
|
+
prefix: "#{network}/#{address.split("/").last}", gateway: address.split("/").first,
|
|
306
|
+
anycast: settings[:anycast], acl_bindings: settings[:acl_bindings])
|
|
307
|
+
rescue InputError
|
|
308
|
+
issues << { code: :invalid_segment, interface: name }
|
|
309
|
+
nil
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def ip_address(value)
|
|
314
|
+
IPAddr.new(value).to_s
|
|
315
|
+
rescue IPAddr::InvalidAddressError, IPAddr::AddressFamilyError
|
|
316
|
+
nil
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def ip_prefix?(value)
|
|
320
|
+
IPAddr.new(value).ipv4?
|
|
321
|
+
rescue IPAddr::InvalidAddressError
|
|
322
|
+
false
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Vxlan
|
|
5
|
+
module CiscoNxos
|
|
6
|
+
# No retry of configuration writes. Recovery verifies desired state by readback.
|
|
7
|
+
class DeploymentAdapter
|
|
8
|
+
def initialize(connector: Connector)
|
|
9
|
+
@connector = connector
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def preflight(plan:, previous:, credentials:) # rubocop:disable Naming/PredicateMethod -- Deployment adapter protocol requires this name.
|
|
13
|
+
validate_target!(plan.target)
|
|
14
|
+
plan.steps.each { |step| validate_step!(step) }
|
|
15
|
+
node = read_node(plan.target, credentials)
|
|
16
|
+
unless node.complete? && node.hostname == plan.preconditions[:hostname]
|
|
17
|
+
raise Error.new("VXLAN target identity or configuration evidence changed", code: :target_changed, stage: :preflight)
|
|
18
|
+
end
|
|
19
|
+
if !previous && node.config_digest != plan.preconditions[:config_digest]
|
|
20
|
+
raise Error.new("VXLAN configuration changed since planning", code: :stale_plan, stage: :preflight)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
true
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def apply(step:, target:, credentials:, idempotency_key:)
|
|
27
|
+
validate_target!(target)
|
|
28
|
+
validate_step!(step)
|
|
29
|
+
result = @connector.open(:cisco_nxos, **credentials, host: target.host) { |session| session.execute_script(step.parameters.fetch(:commands)) }
|
|
30
|
+
{ status: result.success? ? :applied : :unknown, evidence: { completed_commands: result.steps.size, operation_key: idempotency_key } }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def verify(step:, target:, credentials:, receipt:)
|
|
34
|
+
Validation.invalid!("verification receipt must be a Hash") unless receipt.is_a?(Hash)
|
|
35
|
+
validate_target!(target)
|
|
36
|
+
validate_step!(step)
|
|
37
|
+
Verifier.new.verify(node: read_node(target, credentials), expected: step.expected)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def read_node(target, credentials)
|
|
43
|
+
result = @connector.open(:cisco_nxos, **credentials, host: target.host) { |session| session.execute_script(["show running-config"]) }
|
|
44
|
+
raise Error.new("configuration readback failed", code: :readback_failed, stage: :verify) unless result.success? && result.steps.size == 1
|
|
45
|
+
|
|
46
|
+
ConfigParser.new.parse(result.steps.first.output, target: target, complete: true)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def validate_target!(target)
|
|
50
|
+
Validation.invalid!("Cisco NX-OS deployment requires cisco_nxos target") unless target.platform.to_s == "cisco_nxos"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def validate_step!(step)
|
|
54
|
+
Validation.invalid!("unsupported VXLAN deployment action") unless step.action == "vxlan_segment"
|
|
55
|
+
commands = step.parameters[:commands]
|
|
56
|
+
Validation.invalid!("VXLAN commands must be nonempty single lines") unless commands.is_a?(Array) && !commands.empty? && commands.all? do |command|
|
|
57
|
+
command.is_a?(String) && !command.empty? && !/[\r\n\x00]/.match?(command)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Vxlan
|
|
5
|
+
module CiscoNxos
|
|
6
|
+
class RuntimeParser
|
|
7
|
+
COMMANDS = {
|
|
8
|
+
nve_peers: "show nve peers", nve_vnis: "show nve vni",
|
|
9
|
+
bgp_neighbors: "show bgp l2vpn evpn summary", ospf_neighbors: "show ip ospf neighbors",
|
|
10
|
+
vpc: "show vpc brief", interfaces: "show interface brief",
|
|
11
|
+
port_channels: "show port-channel summary", mac_addresses: "show mac address-table"
|
|
12
|
+
}.freeze
|
|
13
|
+
BGP_FRAMING = [
|
|
14
|
+
/\ABGP summary information for VRF \S+, address family .+\z/i,
|
|
15
|
+
/\ABGP router identifier [\da-f:.]+, local AS number [\d.]+\z/i,
|
|
16
|
+
/\ABGP table version is \d+(?:, L2VPN EVPN config peers \d+, capable peers \d+)?\z/i,
|
|
17
|
+
/\A\d+ network entries and \d+ paths using \d+ bytes of memory\z/i,
|
|
18
|
+
/\ABGP attribute entries \[\d+\/\d+\], BGP AS path entries \[\d+\/\d+\]\z/i,
|
|
19
|
+
/\ABGP community entries \[\d+\/\d+\], BGP clusterlist entries \[\d+\/\d+\]\z/i
|
|
20
|
+
].freeze
|
|
21
|
+
PORT_CHANNEL_FRAMING = [
|
|
22
|
+
/\AFlags:\s+D - Down\s+P - Up in port-channel \(members\)\z/i,
|
|
23
|
+
/\AI - Individual\s+H - Hot-standby \(LACP only\)\z/i,
|
|
24
|
+
/\As - Suspended\s+r - Module-removed\z/i,
|
|
25
|
+
/\AS - Switched\s+R - Routed\z/i,
|
|
26
|
+
/\AU - Up \(port-channel\)\z/i,
|
|
27
|
+
/\AM - Not in use\. Min-links not met\z/i
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
30
|
+
def parse(dimension, text, complete: false)
|
|
31
|
+
Validation.invalid!("dimension must be a Symbol or String") unless dimension.is_a?(Symbol) || dimension.is_a?(String)
|
|
32
|
+
dimension = dimension.to_sym
|
|
33
|
+
Validation.invalid!("unknown VXLAN runtime dimension") unless COMMANDS.key?(dimension)
|
|
34
|
+
Validation.invalid!("runtime output must be a String") unless text.is_a?(String)
|
|
35
|
+
Validation.invalid!("complete must be boolean") unless [true, false].include?(complete)
|
|
36
|
+
if /(?:invalid command|invalid input|not supported|feature.*not enabled)/i.match?(text)
|
|
37
|
+
return RuntimeResult.new(dimension: dimension, status: :unsupported, issues: [{ code: :command_unsupported }])
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
rows, recognized, rejected, explicit_empty = send("parse_#{dimension}", text.gsub("\r", ""))
|
|
41
|
+
issues = []
|
|
42
|
+
issues << { code: :output_incomplete } unless complete
|
|
43
|
+
issues << { code: :format_unrecognized } unless recognized
|
|
44
|
+
issues << { code: :unparsed_rows, count: rejected } if rejected.positive?
|
|
45
|
+
issues << { code: :missing_completeness_evidence } if rows.empty? && !explicit_empty
|
|
46
|
+
RuntimeResult.new(dimension: dimension, status: issues.empty? ? :complete : :partial, records: rows, issues: issues)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def table(text, header:, empty:, framing: [])
|
|
52
|
+
rows = []
|
|
53
|
+
rejected = 0
|
|
54
|
+
explicit_empty = false
|
|
55
|
+
recognized_header = false
|
|
56
|
+
text.each_line do |line|
|
|
57
|
+
line = line.strip
|
|
58
|
+
next if line.empty? || /\A[-=]+\z/.match?(line) || framing.any? { |pattern| pattern.match?(line) }
|
|
59
|
+
if header.match?(line)
|
|
60
|
+
recognized_header = true
|
|
61
|
+
next
|
|
62
|
+
end
|
|
63
|
+
if empty.match?(line)
|
|
64
|
+
explicit_empty = true
|
|
65
|
+
next
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
row = yield(line)
|
|
69
|
+
row ? rows << row : rejected += 1
|
|
70
|
+
end
|
|
71
|
+
rejected += 1 if explicit_empty && rows.any?
|
|
72
|
+
[rows, recognized_header || explicit_empty || rows.any?, rejected, explicit_empty]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def parse_nve_peers(text)
|
|
76
|
+
table(text, header: /\AInterface\s+Peer-IP\s+State\s+LearnType\s+Uptime\s+Router-Mac\z/i,
|
|
77
|
+
empty: /\A(?:No peers(?: found)?|0 peers)\.?\z/i) do |line|
|
|
78
|
+
match = /\A(nve\d+)\s+(\d+(?:\.\d+){3})\s+(Up|Down)\s+(\S+)\s+(\S+)\s+(\S+)\z/i.match(line)
|
|
79
|
+
{ interface: match[1], peer_ip: match[2], state: match[3].downcase.to_sym, learn_type: match[4], uptime: match[5], router_mac: match[6] } if match
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def parse_nve_vnis(text)
|
|
84
|
+
table(text, header: /\AInterface\s+VNI\s+Multicast-group\s+State\s+Mode\s+Type\s+\[BD\/VRF\]\z/i,
|
|
85
|
+
empty: /\A(?:No VNIs(?: found)?|0 VNIs)\.?\z/i) do |line|
|
|
86
|
+
match = /\A(nve\d+)\s+(\d+)\s+(\S+)\s+(Up|Down)\s+(\S+)\s+(L[23])\s+\[([^\]]+)\](?:\s+.*)?\z/i.match(line)
|
|
87
|
+
if match
|
|
88
|
+
{ interface: match[1], vni: match[2].to_i, multicast_group: match[3], state: match[4].downcase.to_sym,
|
|
89
|
+
mode: match[5], type: match[6].downcase.to_sym, association: match[7] }
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def parse_bgp_neighbors(text)
|
|
95
|
+
table(text, header: /\ANeighbor\s+V\s+AS\s+MsgRcvd\s+MsgSent\s+TblVer\s+InQ\s+OutQ\s+Up\/Down\s+State\/PfxRcd\z/i,
|
|
96
|
+
empty: /\A(?:No (?:BGP )?neighbors(?: found)?|0 neighbors)\.?\z/i, framing: BGP_FRAMING) do |line|
|
|
97
|
+
match = /\A(\d+(?:\.\d+){3})\s+\d+\s+(\d+)\s+\d+\s+\d+\s+\d+\s+(\d+)\s+(\d+)\s+(\S+)\s+(.+)\z/.match(line)
|
|
98
|
+
next unless match
|
|
99
|
+
|
|
100
|
+
state = match[6].strip
|
|
101
|
+
{ peer_ip: match[1], remote_as: match[2].to_i, input_queue: match[3].to_i, output_queue: match[4].to_i,
|
|
102
|
+
uptime: match[5], state: /\A\d+\z/.match?(state) ? :established : state.downcase.tr(" ", "_").to_sym,
|
|
103
|
+
prefixes_received: /\A\d+\z/.match?(state) ? state.to_i : nil }
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def parse_ospf_neighbors(text)
|
|
108
|
+
table(text, header: /\ANeighbor ID\s+Pri\s+State\s+Up Time\s+Address\s+Interface\z/i,
|
|
109
|
+
empty: /\A(?:No (?:OSPF )?neighbors(?: found)?|0 neighbors)\.?\z/i,
|
|
110
|
+
framing: [/\AOSPF Process ID \S+ VRF \S+\z/i]) do |line|
|
|
111
|
+
match = /\A(\d+(?:\.\d+){3})\s+(\d+)\s+(\S+)\s+(\S+)\s+(\d+(?:\.\d+){3})\s+(\S+)\z/.match(line)
|
|
112
|
+
{ neighbor_id: match[1], priority: match[2].to_i, state: match[3].downcase, uptime: match[4], address: match[5], interface: match[6] } if match
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def parse_vpc(text)
|
|
117
|
+
row = {}
|
|
118
|
+
rejected = 0
|
|
119
|
+
fields = { domain_id: /\AvPC domain id\s*:\s*(\d+)\z/i, peer_status: /\APeer status\s*:\s*(.+)\z/i,
|
|
120
|
+
keepalive_status: /\AvPC keep-alive status\s*:\s*(.+)\z/i,
|
|
121
|
+
consistency_status: /\AConfiguration consistency status\s*:\s*(.+)\z/i,
|
|
122
|
+
role: /\AvPC role\s*:\s*(.+)\z/i }
|
|
123
|
+
text.each_line do |line|
|
|
124
|
+
line = line.strip
|
|
125
|
+
next if line.empty? || /\A[-=]+\z/.match?(line)
|
|
126
|
+
matched = fields.find { |_, pattern| pattern.match?(line) }
|
|
127
|
+
unless matched
|
|
128
|
+
rejected += 1
|
|
129
|
+
next
|
|
130
|
+
end
|
|
131
|
+
key, pattern = matched
|
|
132
|
+
value = line[pattern, 1]
|
|
133
|
+
if row.key?(key)
|
|
134
|
+
rejected += 1
|
|
135
|
+
else
|
|
136
|
+
row[key] = key == :domain_id ? value.to_i : value
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
return [[], false, rejected] if row.empty?
|
|
140
|
+
|
|
141
|
+
[[row], true, rejected + (%i[domain_id peer_status keepalive_status consistency_status] - row.keys).size]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def parse_interfaces(text)
|
|
145
|
+
table(text, header: /\A(?:Port\s+VRF\s+Status\s+IP Address|(?:Port|Interface)\s+Name\s+Status\s+Vlan\s+Duplex\s+Speed\s+Type)\z/i,
|
|
146
|
+
empty: /\A(?:No interfaces(?: found)?|0 interfaces)\.?\z/i) do |line|
|
|
147
|
+
next unless /\A(?:Eth(?:ernet)?\d|Po\d|port-channel\d|Vlan\d|loopback\d|nve\d)/i.match?(line)
|
|
148
|
+
tokens = line.split
|
|
149
|
+
state_index = tokens.index { |token| %w[up down connected notconnect disabled routed].include?(token.downcase) }
|
|
150
|
+
next unless state_index
|
|
151
|
+
|
|
152
|
+
{ interface: tokens.first, state: tokens[state_index].downcase.to_sym, detail: tokens[(state_index + 1)..].join(" ") }
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def parse_port_channels(text)
|
|
157
|
+
table(text, header: /\AGroup\s+Port-Channel\s+Type\s+Protocol\s+Member Ports\z/i,
|
|
158
|
+
empty: /\A(?:No port-channels(?: found)?|0 port-channels)\.?\z/i, framing: PORT_CHANNEL_FRAMING) do |line|
|
|
159
|
+
match = /\A(\d+)\s+(Po\d+)\(([^)]+)\)\s+(\S+)\s+(\S+)\s*(.*)\z/.match(line)
|
|
160
|
+
if match
|
|
161
|
+
members = match[6].split.map { |token| /\A([^\s()]+)\(([^\s()]+)\)\z/.match(token) }
|
|
162
|
+
next if members.any?(&:nil?)
|
|
163
|
+
{ group: match[1].to_i, interface: match[2], flags: match[3], type: match[4], protocol: match[5],
|
|
164
|
+
members: members.map { |member| { interface: member[1], flags: member[2] } } }
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def parse_mac_addresses(text)
|
|
170
|
+
table(text, header: /\AVLAN\s+MAC Address\s+Type\s+age\s+Secure\s+NTFY\s+Ports\z/i,
|
|
171
|
+
empty: /\A(?:No (?:MAC addresses|entries)(?: found)?|0 (?:MAC addresses|entries))\.?\z/i) do |line|
|
|
172
|
+
match = /\A(?:[*+GOCR]\s+)?(\d+)\s+([\da-f]{4}(?:\.[\da-f]{4}){2})\s+(dynamic|static)\s+(.+)\z/i.match(line)
|
|
173
|
+
if match
|
|
174
|
+
{ vlan_id: match[1].to_i, mac: match[2].delete(".").scan(/../).join(":"), type: match[3].downcase.to_sym,
|
|
175
|
+
destination: match[4].split.last }
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Vxlan
|
|
5
|
+
module CiscoNxos
|
|
6
|
+
# Renders only known missing objects; it never deletes or replaces existing policy.
|
|
7
|
+
class SegmentRenderer
|
|
8
|
+
def render(segment:, components:, nve_interface:, replication: :bgp, multicast_group: nil,
|
|
9
|
+
prefix_list: nil, prefix_sequence: nil)
|
|
10
|
+
Validation.name(nve_interface, "NVE interface")
|
|
11
|
+
commands = []
|
|
12
|
+
commands.push("vlan #{segment.vlan_id}", "vn-segment #{segment.vni}", "exit") if components.include?(:vlan)
|
|
13
|
+
if components.any? { |part| part.to_s.start_with?("svi") }
|
|
14
|
+
commands << "interface Vlan#{segment.vlan_id}"
|
|
15
|
+
commands << "no shutdown" if components.include?(:svi_enable)
|
|
16
|
+
commands << "vrf member #{segment.vrf}" if components.include?(:svi_vrf)
|
|
17
|
+
commands << "ip address #{segment.interface_address}" if components.include?(:svi_address)
|
|
18
|
+
commands << "fabric forwarding mode anycast-gateway" if components.include?(:svi_anycast)
|
|
19
|
+
segment.acl_bindings.each do |direction, name|
|
|
20
|
+
commands << "ip access-group #{name} #{direction}" if components.include?(:"svi_acl_#{direction}")
|
|
21
|
+
end
|
|
22
|
+
commands << "exit"
|
|
23
|
+
end
|
|
24
|
+
if components.include?(:nve)
|
|
25
|
+
commands.push("interface #{nve_interface}", "member vni #{segment.vni}")
|
|
26
|
+
commands << (replication == :bgp ? "ingress-replication protocol bgp" : "mcast-group #{multicast_group}")
|
|
27
|
+
commands.push("exit", "exit")
|
|
28
|
+
end
|
|
29
|
+
if components.include?(:evpn)
|
|
30
|
+
commands.push("evpn", "vni #{segment.vni} l2", "rd auto", "route-target import auto", "route-target export auto", "exit", "exit")
|
|
31
|
+
end
|
|
32
|
+
if components.include?(:advertisement)
|
|
33
|
+
Validation.name(prefix_list, "prefix-list")
|
|
34
|
+
Validation.integer(prefix_sequence, "prefix-list sequence", 1..4_294_967_294)
|
|
35
|
+
commands << "ip prefix-list #{prefix_list} seq #{prefix_sequence} permit #{segment.prefix}"
|
|
36
|
+
end
|
|
37
|
+
commands.empty? ? [].freeze : ["configure terminal", *commands, "end"].freeze
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|