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,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Netstack
|
|
6
|
+
# 按来源和范围组织观察结果与变更建议;持久化由宿主负责。
|
|
7
|
+
module Inventory
|
|
8
|
+
# 管理代码块内的设备连接,只读取已请求且受支持的维度。
|
|
9
|
+
# 单个维度失败不会使其他已完成的观察失效。
|
|
10
|
+
class Collector
|
|
11
|
+
DIMENSIONS = %i[device interfaces transceivers neighbors].freeze
|
|
12
|
+
|
|
13
|
+
def initialize(connector: Connector, parser: CliParser.new, clock: -> { Time.now.utc })
|
|
14
|
+
@connector = connector
|
|
15
|
+
@parser = parser
|
|
16
|
+
@clock = clock
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def collect(target:, dimensions: %i[device interfaces neighbors], connection_options: {}, scope: {})
|
|
20
|
+
raise InputError, "collection target must be a Target" unless target.is_a?(Target)
|
|
21
|
+
unless dimensions.is_a?(Array) && dimensions.all? { |name| DIMENSIONS.include?(name) } && dimensions.uniq == dimensions
|
|
22
|
+
raise InputError, "unknown or duplicate inventory dimensions"
|
|
23
|
+
end
|
|
24
|
+
unless connection_options.is_a?(Hash) && !connection_options.key?(:host) && !connection_options.key?("host")
|
|
25
|
+
raise InputError, "connection options cannot override the target host"
|
|
26
|
+
end
|
|
27
|
+
result = DIMENSIONS.to_h { |name| [name, Dimension.new(name: name, status: :not_requested)] }
|
|
28
|
+
scope = Snapshot.new(target: target, source: "cli", dimensions: result, scope: scope).scope
|
|
29
|
+
dimensions.each { |name| result[name] = Dimension.new(name: name, status: :unsupported) }
|
|
30
|
+
supported = dimensions.select { |name| name == :neighbors || (name != :transceivers && @parser.supports?(target.platform)) }
|
|
31
|
+
return Snapshot.new(target: target, source: "cli", dimensions: result, scope: scope) if supported.empty?
|
|
32
|
+
|
|
33
|
+
begin
|
|
34
|
+
@connector.open(target.platform, host: target.host, **connection_options) do |device|
|
|
35
|
+
configuration = nil
|
|
36
|
+
configuration_error = nil
|
|
37
|
+
configuration_started = @clock.call
|
|
38
|
+
if (supported & %i[device interfaces]).any?
|
|
39
|
+
begin
|
|
40
|
+
configuration = device.running_config.value!
|
|
41
|
+
rescue StandardError => error
|
|
42
|
+
configuration_error = error
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
supported.each do |name|
|
|
46
|
+
next if name == :neighbors && !device.supports?(:neighbors)
|
|
47
|
+
|
|
48
|
+
started = name == :neighbors ? @clock.call : configuration_started
|
|
49
|
+
begin
|
|
50
|
+
raise configuration_error if configuration_error && name != :neighbors
|
|
51
|
+
|
|
52
|
+
records = collect_dimension(name, device, target, configuration)
|
|
53
|
+
if scope.key?(name)
|
|
54
|
+
keys = scope.fetch(name)
|
|
55
|
+
records = records.select { |record| keys.include?(record.key) }
|
|
56
|
+
end
|
|
57
|
+
evidence = name == :neighbors ? [] : [{ kind: :configuration, sha256: Digest::SHA256.hexdigest(configuration) }]
|
|
58
|
+
result[name] = Dimension.new(name: name, status: :complete, records: records, started_at: started,
|
|
59
|
+
completed_at: @clock.call, evidence: evidence)
|
|
60
|
+
rescue StandardError => error
|
|
61
|
+
result[name] = failure(name, error, started)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
rescue StandardError => error
|
|
66
|
+
supported.each do |name|
|
|
67
|
+
result[name] = failure(name, error, @clock.call) unless result[name].complete?
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
Snapshot.new(target: target, source: "cli", dimensions: result, scope: scope)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def collect_dimension(name, device, target, configuration)
|
|
76
|
+
case name
|
|
77
|
+
when :device
|
|
78
|
+
command = target.platform.start_with?("cisco") ? "show version" : "display version"
|
|
79
|
+
version = device.execute_command(command).value!
|
|
80
|
+
[@parser.device(configuration: configuration, version: version, target: target)]
|
|
81
|
+
when :interfaces
|
|
82
|
+
@parser.interfaces(configuration: configuration, platform: target.platform)
|
|
83
|
+
when :neighbors
|
|
84
|
+
device.neighbors.map do |neighbor|
|
|
85
|
+
Neighbor.new(local_interface: neighbor.local_interface, remote_name: neighbor.neighbor_name,
|
|
86
|
+
remote_interface: neighbor.neighbor_interface, chassis_id: neighbor.chassis_id,
|
|
87
|
+
attributes: { protocol: neighbor.protocol })
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def failure(name, error, started)
|
|
93
|
+
code = error.respond_to?(:code) ? error.code : :collection_failed
|
|
94
|
+
Dimension.new(name: name, status: :failed, started_at: started, completed_at: @clock.call,
|
|
95
|
+
diagnostics: [{ code: code }])
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Inventory
|
|
5
|
+
class Device < Data.define(:key, :name, :serial, :model, :vendor, :os_version, :attributes)
|
|
6
|
+
def initialize(key:, name: nil, serial: nil, model: nil, vendor: nil, os_version: nil, attributes: {})
|
|
7
|
+
super(key: Values.text(key, name: "device key"), name: Values.text(name, name: "device name", optional: true),
|
|
8
|
+
serial: Values.text(serial, name: "serial", optional: true), model: Values.text(model, name: "model", optional: true),
|
|
9
|
+
vendor: Values.text(vendor, name: "vendor", optional: true), os_version: Values.text(os_version, name: "OS version", optional: true),
|
|
10
|
+
attributes: Values.copy(attributes))
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Inventory
|
|
5
|
+
class Interface < Data.define(:name, :description, :admin_status, :oper_status, :addresses, :attributes)
|
|
6
|
+
def initialize(name:, description: nil, admin_status: :unknown, oper_status: :unknown, addresses: [], attributes: {})
|
|
7
|
+
statuses = [admin_status, oper_status].map do |status|
|
|
8
|
+
status = status.to_sym if status.is_a?(String)
|
|
9
|
+
raise InputError, "invalid interface status" unless %i[up down unknown].include?(status)
|
|
10
|
+
|
|
11
|
+
status
|
|
12
|
+
end
|
|
13
|
+
unless addresses.is_a?(Array) && addresses.all? { |address| address.is_a?(Address) }
|
|
14
|
+
raise InputError, "interface addresses must be Address values"
|
|
15
|
+
end
|
|
16
|
+
raise InputError, "description must be text or nil" unless description.nil? || description.is_a?(String)
|
|
17
|
+
|
|
18
|
+
super(name: Values.text(name, name: "interface name"), description: Values.copy(description),
|
|
19
|
+
admin_status: statuses[0], oper_status: statuses[1], addresses: addresses.dup.freeze, attributes: Values.copy(attributes))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def key = name.downcase
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Inventory
|
|
5
|
+
class Neighbor < Data.define(:local_interface, :remote_name, :remote_interface, :chassis_id, :attributes)
|
|
6
|
+
def initialize(local_interface:, remote_name:, remote_interface:, chassis_id: nil, attributes: {})
|
|
7
|
+
super(local_interface: Values.text(local_interface, name: "local interface"), remote_name: Values.text(remote_name, name: "remote name"),
|
|
8
|
+
remote_interface: Values.text(remote_interface, name: "remote interface"),
|
|
9
|
+
chassis_id: Values.text(chassis_id, name: "chassis id", optional: true), attributes: Values.copy(attributes))
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def key = [local_interface.downcase, chassis_id || remote_name.downcase, remote_interface.downcase].freeze
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Inventory
|
|
5
|
+
# 只计算变更,不写入宿主记录;仅完整维度可以提出删除,
|
|
6
|
+
# 并且删除必须限定在采集来源与明确的身份范围内。
|
|
7
|
+
class Reconciler
|
|
8
|
+
# current contains host records keyed by dimension. Fields owned by people stay with the host.
|
|
9
|
+
# A row has :key, :attributes, :managed_fields and :source; collection never guesses ownership.
|
|
10
|
+
def call(current:, snapshot:, target:)
|
|
11
|
+
unless snapshot.is_a?(Snapshot) && target.is_a?(Target) &&
|
|
12
|
+
target == snapshot.target
|
|
13
|
+
raise InputError, "snapshot target has changed"
|
|
14
|
+
end
|
|
15
|
+
raise InputError, "current inventory must be a Hash" unless current.is_a?(Hash)
|
|
16
|
+
|
|
17
|
+
changes = {}
|
|
18
|
+
skipped = {}
|
|
19
|
+
snapshot.dimensions.each do |name, dimension|
|
|
20
|
+
unless dimension.complete?
|
|
21
|
+
skipped[name] = dimension.status
|
|
22
|
+
next
|
|
23
|
+
end
|
|
24
|
+
rows = current.fetch(name, [])
|
|
25
|
+
validate_rows!(rows)
|
|
26
|
+
indexed = rows.to_h { |row| [identity(name, row.fetch(:key)), row] }
|
|
27
|
+
raise InputError, "duplicate current inventory identity" unless indexed.size == rows.size
|
|
28
|
+
|
|
29
|
+
updates = []
|
|
30
|
+
creates = []
|
|
31
|
+
observed = {}
|
|
32
|
+
dimension.records.each do |record|
|
|
33
|
+
key = identity(name, record.key)
|
|
34
|
+
existing = indexed[key]
|
|
35
|
+
attributes = record.to_h.reject { |field, _| field == :key }
|
|
36
|
+
if existing && existing[:source] == snapshot.source
|
|
37
|
+
fields = existing.fetch(:managed_fields)
|
|
38
|
+
desired = existing.fetch(:attributes).merge(attributes.select { |field, _| fields.include?(field) })
|
|
39
|
+
# Host record identity and display spelling remain stable for interface references.
|
|
40
|
+
desired[:name] = existing[:attributes][:name] if name == :interfaces && existing[:attributes].key?(:name)
|
|
41
|
+
updates << { key: existing[:key], attributes: desired } if desired != existing[:attributes]
|
|
42
|
+
elsif !existing
|
|
43
|
+
creates << { key: record.key, attributes: attributes, source: snapshot.source }
|
|
44
|
+
end
|
|
45
|
+
observed[key] = true
|
|
46
|
+
end
|
|
47
|
+
scope = snapshot.scope[name]&.to_h { |key| [key, true] }
|
|
48
|
+
removals = rows.filter_map do |row|
|
|
49
|
+
key = identity(name, row[:key])
|
|
50
|
+
in_scope = scope.nil? || scope.key?(key)
|
|
51
|
+
row[:key] if in_scope && row[:source] == snapshot.source && !observed.key?(key)
|
|
52
|
+
end
|
|
53
|
+
changes[name] = { create: creates, update: updates, remove: removals }
|
|
54
|
+
end
|
|
55
|
+
Changes.new(target: target, source: snapshot.source, dimensions: changes, skipped: skipped)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def identity(name, key)
|
|
61
|
+
Snapshot.identity(name, key)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def validate_rows!(rows)
|
|
65
|
+
unless rows.is_a?(Array) && rows.all? do |row|
|
|
66
|
+
row.is_a?(Hash) && row.key?(:key) && row[:attributes].is_a?(Hash) &&
|
|
67
|
+
row[:managed_fields].is_a?(Array) && row[:managed_fields].all? { |field| field.is_a?(Symbol) } && row[:source].is_a?(String)
|
|
68
|
+
end
|
|
69
|
+
raise InputError, "invalid current inventory records"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Inventory
|
|
5
|
+
# 同时校验记录身份、目标归属和采集范围。
|
|
6
|
+
# 未提供范围表示整个维度;空范围表示不包含任何身份。
|
|
7
|
+
class Snapshot < Data.define(:target, :source, :dimensions, :scope)
|
|
8
|
+
RECORD_TYPES = { device: Device, interfaces: Interface, transceivers: Transceiver, neighbors: Neighbor }.freeze
|
|
9
|
+
|
|
10
|
+
def initialize(target:, source:, dimensions:, scope: {})
|
|
11
|
+
raise InputError, "snapshot target must be a Target" unless target.is_a?(Target)
|
|
12
|
+
raise InputError, "snapshot dimensions must be a Hash" unless dimensions.is_a?(Hash)
|
|
13
|
+
raise InputError, "snapshot scope must be a Hash" unless scope.is_a?(Hash)
|
|
14
|
+
|
|
15
|
+
normalized_scope = scope.each_with_object({}) do |(name, keys), result|
|
|
16
|
+
name = name.to_sym if name.is_a?(String)
|
|
17
|
+
unless RECORD_TYPES.key?(name) && keys.is_a?(Array) && !result.key?(name)
|
|
18
|
+
raise InputError, "scope must contain dimension identity arrays"
|
|
19
|
+
end
|
|
20
|
+
result[name] = keys.map { |key| self.class.identity(name, key) }
|
|
21
|
+
raise InputError, "duplicate inventory scope identity" unless result[name].uniq.size == result[name].size
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
normalized = {}
|
|
25
|
+
dimensions.each do |name, dimension|
|
|
26
|
+
name = name.to_sym if name.is_a?(String)
|
|
27
|
+
unless RECORD_TYPES.key?(name) && dimension.is_a?(Dimension) && dimension.name == name && !normalized.key?(name)
|
|
28
|
+
raise InputError, "invalid snapshot dimension"
|
|
29
|
+
end
|
|
30
|
+
unless dimension.records.all? { |record| record.is_a?(RECORD_TYPES.fetch(name)) }
|
|
31
|
+
raise InputError, "invalid inventory record type"
|
|
32
|
+
end
|
|
33
|
+
identities = dimension.records.map(&:key)
|
|
34
|
+
raise InputError, "duplicate inventory identity" unless identities.uniq.size == identities.size
|
|
35
|
+
if name == :device && dimension.records.any? { |record| record.key != target.key }
|
|
36
|
+
raise InputError, "device record does not belong to target"
|
|
37
|
+
end
|
|
38
|
+
if name == :device && dimension.complete? && dimension.records.size != 1
|
|
39
|
+
raise InputError, "a complete device dimension must identify the target"
|
|
40
|
+
end
|
|
41
|
+
if normalized_scope.key?(name) && (identities - normalized_scope[name]).any?
|
|
42
|
+
raise InputError, "inventory contains records outside the collected scope"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
normalized[name] = dimension
|
|
46
|
+
end
|
|
47
|
+
super(target: target, source: Values.text(source, name: "snapshot source"), dimensions: normalized.freeze, scope: Values.copy(normalized_scope))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.identity(name, key)
|
|
51
|
+
case name
|
|
52
|
+
when :device then Values.text(key, name: "device identity")
|
|
53
|
+
when :interfaces then Values.text(key, name: "interface identity").downcase.freeze
|
|
54
|
+
when :transceivers then Values.integer(key, name: "transceiver identity", min: 1)
|
|
55
|
+
when :neighbors
|
|
56
|
+
raise InputError, "neighbor identity must contain three text components" unless key.is_a?(Array) && key.size == 3
|
|
57
|
+
key.each { |part| Values.text(part, name: "neighbor identity") }
|
|
58
|
+
[key[0].strip.downcase, key[1].strip, key[2].strip.downcase].map(&:freeze).freeze
|
|
59
|
+
else raise InputError, "unknown inventory identity dimension"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def complete?
|
|
64
|
+
requested = dimensions.values.reject { |dimension| dimension.status == :not_requested }
|
|
65
|
+
!requested.empty? && requested.all?(&:complete?)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Inventory
|
|
5
|
+
class Transceiver < Data.define(:interface_index, :interface_name, :vendor, :model, :serial, :measurements)
|
|
6
|
+
def initialize(interface_index:, interface_name:, vendor: nil, model: nil, serial: nil, measurements: {})
|
|
7
|
+
raise InputError, "transceiver measurements must be a Hash" unless measurements.is_a?(Hash)
|
|
8
|
+
|
|
9
|
+
measurements.each_value do |value|
|
|
10
|
+
unless value.nil? || (value.is_a?(Numeric) && value.real? && value.finite?)
|
|
11
|
+
raise InputError, "measurements must be finite numbers or nil"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
super(interface_index: Values.integer(interface_index, name: "interface index", min: 1),
|
|
15
|
+
interface_name: Values.text(interface_name, name: "interface name"), vendor: Values.text(vendor, name: "vendor", optional: true),
|
|
16
|
+
model: Values.text(model, name: "model", optional: true), serial: Values.text(serial, name: "serial", optional: true),
|
|
17
|
+
measurements: Values.copy(measurements))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def key = interface_index
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ipaddr"
|
|
4
|
+
require_relative "range_set"
|
|
5
|
+
|
|
6
|
+
module Netstack
|
|
7
|
+
# 在标准 IPAddr 能力上补充防火墙所需的 IPv4 和服务范围运算。
|
|
8
|
+
class IPv4 < ::IPAddr
|
|
9
|
+
IPV4_MAX = ::IPAddr::IN4MASK
|
|
10
|
+
PORT_MAX = (1 << 16) - 1
|
|
11
|
+
SERVICE_MAX = (1 << 24) - 1
|
|
12
|
+
# 服务编码中反复使用的协议号集中命名,避免上层散落裸数字。
|
|
13
|
+
ANY_PROTOCOL = 0
|
|
14
|
+
ICMP = 1
|
|
15
|
+
TCP = 6
|
|
16
|
+
UDP = 17
|
|
17
|
+
PROTOCOL_NUMBERS = { "tcp" => TCP, "udp" => UDP, "icmp" => ICMP,
|
|
18
|
+
"ms-rpc-tcp" => TCP, "ms-rpc-udp" => UDP }.freeze
|
|
19
|
+
|
|
20
|
+
# 规范化 IPv4 输入,并拒绝 IPv6、非法前缀及非十进制设备写法。
|
|
21
|
+
def initialize(address = "0.0.0.0", family = Socket::AF_INET)
|
|
22
|
+
unless [ Socket::AF_INET, Socket::AF_UNSPEC ].include?(family)
|
|
23
|
+
raise ArgumentError, "IPv4 helpers require IPv4"
|
|
24
|
+
end
|
|
25
|
+
if address.is_a?(String)
|
|
26
|
+
address = address.sub(/\Aany\z/i, "0.0.0.0/0").sub(/\Aany(?=\/)/i, "0.0.0.0")
|
|
27
|
+
unless /\A[0-9]+(?:\.[0-9]+){3}(?:\/(?:[0-9]+|[0-9]+(?:\.[0-9]+){3}))?\z/.match?(address)
|
|
28
|
+
raise ArgumentError, "Invalid IPv4 expression: #{address.inspect}"
|
|
29
|
+
end
|
|
30
|
+
# 设备格式把带前导零的八位组和前缀都按十进制解释。
|
|
31
|
+
address = address.gsub(/\b0+(?=[0-9])/, "")
|
|
32
|
+
elsif !address.is_a?(Integer)
|
|
33
|
+
raise ArgumentError, "IPv4 input must be a string or integer"
|
|
34
|
+
end
|
|
35
|
+
super(address, Socket::AF_INET)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# 把地址转换成包含首尾整数的标准集合,供范围运算复用。
|
|
39
|
+
def to_set
|
|
40
|
+
raise ArgumentError, "IPv4 helpers require IPv4" unless ipv4?
|
|
41
|
+
range = to_range
|
|
42
|
+
RangeSet.new(range.begin.to_i..range.end.to_i)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class << self
|
|
46
|
+
# 接口地址保留主机位;不能直接用 IPAddr.new("host/prefix").to_s。
|
|
47
|
+
def host_cidr(expression, mask = nil)
|
|
48
|
+
host, prefix = string(expression, label: "Interface address").split("/", 2)
|
|
49
|
+
"#{from_i(to_i(host))}/#{prefix_length(mask || prefix || 32)}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# 路由目的统一归一化为网络地址和前缀。
|
|
53
|
+
def network_cidr(expression, mask = nil)
|
|
54
|
+
address = new(host_cidr(expression, mask))
|
|
55
|
+
"#{address}/#{address.prefix}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# 取接口 CIDR 中的主机地址,供 NAT 出口使用。
|
|
59
|
+
def host(expression) = host_cidr(expression).split("/", 2).first
|
|
60
|
+
|
|
61
|
+
# 把不带前缀的 IPv4 文本转换为整数。
|
|
62
|
+
def to_i(address)
|
|
63
|
+
unless address.is_a?(String) && !address.include?("/")
|
|
64
|
+
raise ArgumentError, "Invalid IPv4 address: #{address.inspect}"
|
|
65
|
+
end
|
|
66
|
+
new(address).to_i
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# 把 IPv4 整数转换为点分十进制文本。
|
|
70
|
+
def from_i(number)
|
|
71
|
+
value = integer(number, max: IPV4_MAX, label: "IPv4 integer")
|
|
72
|
+
new(value).to_s
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# 把前缀长度或掩码转换为规范前缀长度。
|
|
76
|
+
def prefix_length(mask)
|
|
77
|
+
mask = integer(mask, max: 32, label: "Prefix length") unless mask.is_a?(String)
|
|
78
|
+
new("0.0.0.0/#{mask}").prefix
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# 根据前缀长度生成点分十进制网络掩码。
|
|
82
|
+
def netmask(mask) = new(IPV4_MAX).mask(prefix_length(mask)).to_s
|
|
83
|
+
|
|
84
|
+
# 把通配符掩码转换为网络掩码。
|
|
85
|
+
def wildcard_mask(wildcard)
|
|
86
|
+
unless wildcard.is_a?(String) && /\A[0-9]+(?:\.[0-9]+){3}\z/.match?(wildcard)
|
|
87
|
+
raise ArgumentError, "Invalid IPv4 wildcard"
|
|
88
|
+
end
|
|
89
|
+
(~new(wildcard)).to_s
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# 根据地址和掩码计算覆盖的整数地址集合。
|
|
93
|
+
def subnet(address, mask = 32)
|
|
94
|
+
new(to_i(address)).mask(prefix_length(mask.nil? ? 32 : mask)).to_set
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# 返回子网的网络地址文本。
|
|
98
|
+
def network(address, mask = 32) = from_i(subnet(address, mask).min)
|
|
99
|
+
|
|
100
|
+
# 解析主机、CIDR 或网络掩码表达式,也支持两个 IPv4 端点。
|
|
101
|
+
def parse(expression)
|
|
102
|
+
text = string(expression, label: "IPv4 expression")
|
|
103
|
+
parts = text.split("-", -1)
|
|
104
|
+
return between(*parts) if parts.size == 2
|
|
105
|
+
raise ArgumentError, "Invalid IPv4 expression: #{text}" unless parts.size == 1
|
|
106
|
+
new(text).to_set
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# 用两个端点创建有序的闭区间。
|
|
110
|
+
def between(first, last)
|
|
111
|
+
low, high = [ to_i(first), to_i(last) ].minmax
|
|
112
|
+
RangeSet.new(low..high)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# 将地址范围精确拆成 CIDR,供持久化与命令生成共用。
|
|
116
|
+
def cidrs(set)
|
|
117
|
+
unless set.is_a?(RangeSet) && (set.empty? || set.max <= IPV4_MAX)
|
|
118
|
+
raise ArgumentError, "Expected IPv4 integer ranges"
|
|
119
|
+
end
|
|
120
|
+
set.flat_map do |range|
|
|
121
|
+
cursor, last = range.begin, range.end
|
|
122
|
+
blocks = []
|
|
123
|
+
while cursor <= last
|
|
124
|
+
size = 1
|
|
125
|
+
size *= 2 while (cursor % (size * 2)).zero? && (cursor + (size * 2) - 1) <= last
|
|
126
|
+
blocks << [ IPv4.from_i(cursor), 32 - (size.bit_length - 1) ]
|
|
127
|
+
cursor += size
|
|
128
|
+
end
|
|
129
|
+
blocks
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# 把整数区间压缩成最短的单个网段或端点表达式。
|
|
134
|
+
def format_range(range)
|
|
135
|
+
unless range.is_a?(Range) && !range.exclude_end? && range.begin.is_a?(Integer) && range.end.is_a?(Integer)
|
|
136
|
+
raise ArgumentError, "IPv4 notation requires a closed integer range"
|
|
137
|
+
end
|
|
138
|
+
integer(range.begin, max: IPV4_MAX, label: "IPv4 lower bound")
|
|
139
|
+
integer(range.end, max: IPV4_MAX, label: "IPv4 upper bound")
|
|
140
|
+
raise ArgumentError, "IPv4 range is reversed" if range.begin > range.end
|
|
141
|
+
size = range.end - range.begin + 1
|
|
142
|
+
first = from_i(range.begin)
|
|
143
|
+
if (size & (size - 1)).zero? && (range.begin % size).zero?
|
|
144
|
+
"#{first}/#{32 - (size.bit_length - 1)}"
|
|
145
|
+
else
|
|
146
|
+
"#{first}-#{from_i(range.end)}"
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# 解析单个端口或端口范围,并限制在 16 位端口空间内。
|
|
151
|
+
def ports(expression)
|
|
152
|
+
if expression.is_a?(Range)
|
|
153
|
+
result = RangeSet.new(expression)
|
|
154
|
+
raise ArgumentError, "Port must be in 0..#{PORT_MAX}" if result.max > PORT_MAX
|
|
155
|
+
return result
|
|
156
|
+
end
|
|
157
|
+
text = expression.is_a?(Integer) ? expression.to_s : string(expression, label: "Port")
|
|
158
|
+
match = /\A\s*([0-9]+)(?:(?:\s*-\s*|\s+)([0-9]+))?\s*\z/.match(text)
|
|
159
|
+
raise ArgumentError, "Invalid port expression: #{text}" unless match
|
|
160
|
+
first, last = [ match[1], match[2] || match[1] ].map do |endpoint|
|
|
161
|
+
integer(endpoint, max: PORT_MAX, label: "Port")
|
|
162
|
+
end.minmax
|
|
163
|
+
RangeSet.new(first..last)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# 把协议名称或编号转换成服务编码使用的协议编号。
|
|
167
|
+
def protocol_number(protocol)
|
|
168
|
+
protocol = protocol.to_s if protocol.is_a?(Integer) || protocol.is_a?(Symbol)
|
|
169
|
+
name = string(protocol, label: "Protocol").downcase
|
|
170
|
+
return ANY_PROTOCOL if name == "any"
|
|
171
|
+
PROTOCOL_NUMBERS.fetch(name) { integer(name, max: 255, label: "Protocol number") }
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# 服务编码的高八位保存协议,低十六位保存目标端口;省略端口时使用零。
|
|
175
|
+
def service(expression)
|
|
176
|
+
text = string(expression, label: "Service expression")
|
|
177
|
+
protocol, port, *unexpected_parts = text.split("/", -1)
|
|
178
|
+
raise ArgumentError, "Invalid service expression: #{text}" unless protocol && unexpected_parts.empty?
|
|
179
|
+
ranges = ports(port || 0)
|
|
180
|
+
return RangeSet.new(0..SERVICE_MAX) if protocol.casecmp?(ANY_PROTOCOL.to_s) || protocol.casecmp?("any")
|
|
181
|
+
base = protocol_number(protocol) << 16
|
|
182
|
+
RangeSet.new(*ranges.map { |range| (base + range.begin)..(base + range.end) })
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
private
|
|
186
|
+
|
|
187
|
+
# 校验并转换整数输入,同时限制其数值范围。
|
|
188
|
+
def integer(value, max:, label:)
|
|
189
|
+
unless value.is_a?(Integer) || (value.is_a?(String) && /\A[0-9]+\z/.match?(value))
|
|
190
|
+
raise ArgumentError, "#{label} must be an integer in 0..#{max}"
|
|
191
|
+
end
|
|
192
|
+
number = value.is_a?(Integer) ? value : Integer(value, 10)
|
|
193
|
+
raise ArgumentError, "#{label} must be in 0..#{max}" unless number.between?(0, max)
|
|
194
|
+
number
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# 校验文本输入并保留原字符串,便于上层继续解析原始表达式。
|
|
198
|
+
def string(value, label:)
|
|
199
|
+
raise ArgumentError, "#{label} must be a string" unless value.is_a?(String)
|
|
200
|
+
value
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ipaddr"
|
|
4
|
+
|
|
5
|
+
module Netstack
|
|
6
|
+
# IPv6 ranges remain separate from IPv4. Serialize endpoints as address text.
|
|
7
|
+
module IPv6
|
|
8
|
+
MAX = (1 << 128) - 1
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def address(value)
|
|
12
|
+
unless value.is_a?(String) && value == value.strip && !value.include?("%")
|
|
13
|
+
raise ArgumentError, "Invalid IPv6 address"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
result = ::IPAddr.new(value)
|
|
17
|
+
raise ArgumentError, "Expected IPv6" unless result.ipv6?
|
|
18
|
+
|
|
19
|
+
result
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def canonical(value, host: false)
|
|
23
|
+
ip = address(value)
|
|
24
|
+
return "#{address(value.split("/").first)}/#{ip.prefix}" if host
|
|
25
|
+
|
|
26
|
+
value.include?("/") ? "#{ip}/#{ip.prefix}" : ip.to_s
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def parse(value)
|
|
30
|
+
raise ArgumentError, "IPv6 expression must be text" unless value.is_a?(String)
|
|
31
|
+
|
|
32
|
+
if value.include?("-")
|
|
33
|
+
parts = value.split("-", -1)
|
|
34
|
+
unless parts.size == 2 && parts.none? { |part| part.include?("/") }
|
|
35
|
+
raise ArgumentError, "IPv6 range needs two hosts"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
first, last = parts.map { |part| address(part).to_i }
|
|
39
|
+
RangeSet.new(first..last)
|
|
40
|
+
else
|
|
41
|
+
ip = address(value)
|
|
42
|
+
RangeSet.new(ip.to_range.begin.to_i..ip.to_range.end.to_i)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def normalize(value)
|
|
47
|
+
raise ArgumentError, "IPv6 expression must be text" unless value.is_a?(String)
|
|
48
|
+
return canonical(value) unless value.include?("-")
|
|
49
|
+
|
|
50
|
+
range = parse(value).ranges.fetch(0)
|
|
51
|
+
"#{from_i(range.begin)}-#{from_i(range.end)}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def from_i(value)
|
|
55
|
+
raise ArgumentError, "IPv6 integer is out of range" unless value.is_a?(Integer) && value.between?(0, MAX)
|
|
56
|
+
|
|
57
|
+
::IPAddr.new(value, Socket::AF_INET6).to_s
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def ranges(set)
|
|
61
|
+
validate_set!(set)
|
|
62
|
+
set.map { |range| [from_i(range.begin), from_i(range.end)] }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def cidrs(set)
|
|
66
|
+
validate_set!(set)
|
|
67
|
+
set.flat_map do |range|
|
|
68
|
+
cursor = range.begin
|
|
69
|
+
blocks = []
|
|
70
|
+
while cursor <= range.end
|
|
71
|
+
remaining_bits = (range.end - cursor + 1).bit_length - 1
|
|
72
|
+
aligned_bits = cursor.zero? ? 128 : (cursor & -cursor).bit_length - 1
|
|
73
|
+
bits = [remaining_bits, aligned_bits].min
|
|
74
|
+
blocks << [from_i(cursor), 128 - bits]
|
|
75
|
+
cursor += 1 << bits
|
|
76
|
+
end
|
|
77
|
+
blocks
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def validate_set!(set)
|
|
82
|
+
return if set.is_a?(RangeSet) && (set.empty? || set.max <= MAX)
|
|
83
|
+
|
|
84
|
+
raise ArgumentError, "Expected IPv6 integer ranges"
|
|
85
|
+
end
|
|
86
|
+
private_class_method :validate_set!
|
|
87
|
+
end
|
|
88
|
+
end
|