network-infra-utility 0.6.0 → 0.8.3
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/.gitignore +6 -0
- data/CHANGELOG.md +31 -0
- data/GUIDE.md +201 -212
- data/README.md +1 -6
- data/Rakefile +1 -1
- data/bin/gen-get +144 -0
- data/bin/geo-api +40 -1
- data/bin/geo-get +18 -36
- data/bin/ngeo-get +253 -0
- data/bin/packbit +227 -0
- data/bin/probe +134 -0
- data/document/reference/DNS/346/212/200/346/234/257/346/226/207/346/241/243.md +912 -0
- data/network-infra-utility.gemspec +6 -4
- data/network.rb +19 -1
- data/service/geodb/GeoAPI.md +64 -0
- data/service/geodb/api.rb +77 -19
- data/service/geoquery/GeoQuery.md +309 -0
- data/service/geoquery/cache.rb +78 -0
- data/service/geoquery/geo_cache.rb +233 -0
- data/service/geoquery/geoquery.rb +327 -0
- data/service/geoquery/local.rb +151 -0
- data/service/geoquery/merge.rb +52 -0
- data/service/geoquery/normalize.rb +102 -0
- data/service/geoquery/online.rb +136 -0
- data/service/simlab/SimLab.md +140 -0
- data/service/simlab/bin/simctl +152 -0
- data/service/simlab/lib/simlab.rb +124 -0
- data/service/simlab/lib/simlab_engine.rb +146 -0
- data/service/simlab/lib/simlab_observer.rb +176 -0
- data/service/simlab/lib/simlab_router.rb +330 -0
- data/service/simlab/lib/simlab_scenario.rb +200 -0
- data/service/simlab/lib/simlab_switch.rb +182 -0
- data/service/simlab/lib/simlab_topology.rb +196 -0
- data/service/simlab/lib/simlab_traffic.rb +167 -0
- data/support/basic/packet.rb +167 -0
- data/support/routing/bgp.rb +279 -0
- data/support/routing/lpm_trie.rb +145 -0
- data/support/routing/ospf.rb +243 -0
- data/support/routing/prefix.rb +155 -0
- data/support/routing/rib.rb +167 -0
- data/support/routing/rip.rb +223 -0
- data/support/routing/static_route.rb +102 -0
- data/support/switching/frame_forward.rb +87 -0
- data/support/switching/mac_table.rb +127 -0
- data/support/switching/stp.rb +230 -0
- data/support/switching/vlan.rb +155 -0
- data/tool/packbit/README.md +136 -0
- data/tool/packbit/config/five-tuple.yml +25 -0
- data/tool/packbit/config/flow.yml +17 -0
- data/tool/packbit/config/packbit.yml +41 -0
- data/tool/packbit/erl/packbit.erl +588 -0
- data/tool/probe/README.md +88 -0
- data/tool/probe/lib/probe.rb +54 -0
- data/tool/probe/lib/probe_base.rb +98 -0
- data/tool/probe/lib/probe_dns.rb +64 -0
- data/tool/probe/lib/probe_icmp.rb +71 -0
- data/tool/probe/lib/probe_ntp.rb +51 -0
- data/tool/probe/lib/probe_radius.rb +58 -0
- data/tool/probe/lib/probe_registry.rb +44 -0
- data/tool/probe/lib/probe_result.rb +75 -0
- data/tool/probe/lib/probe_snmp.rb +114 -0
- data/tool/probe/lib/probe_tcp.rb +82 -0
- data/tool/probe/lib/probe_version.rb +7 -0
- data/version.rb +1 -1
- metadata +74 -2
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# coding: utf-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# simctl - 路由交换仿真 CLI 工具。
|
|
6
|
+
#
|
|
7
|
+
# 用法:
|
|
8
|
+
# simctl simulate SCENARIO.yml # 运行场景
|
|
9
|
+
# simctl simulate SCENARIO.yml --until 100 # 运行到指定时间
|
|
10
|
+
# simctl dump --rib R1 SCENARIO.yml # 导出 RIB
|
|
11
|
+
# simctl dump --mac S1 SCENARIO.yml # 导出 MAC 表
|
|
12
|
+
# simctl trace --from H1 --to 10.0.0.2 SCENARIO.yml # 路径追踪
|
|
13
|
+
|
|
14
|
+
require "thor"
|
|
15
|
+
require_relative "../lib/simlab"
|
|
16
|
+
|
|
17
|
+
module NetworkInfraUtility
|
|
18
|
+
module SimLab
|
|
19
|
+
class CLI < Thor
|
|
20
|
+
desc "simulate SCENARIO", "运行仿真场景"
|
|
21
|
+
option :until, type: :numeric, default: 100, desc: "运行到指定虚拟时间"
|
|
22
|
+
option :steps, type: :numeric, desc: "执行指定步数"
|
|
23
|
+
option :verbose, type: :boolean, default: false, desc: "详细输出"
|
|
24
|
+
|
|
25
|
+
def simulate(scenario_file)
|
|
26
|
+
sim = Simulation.new
|
|
27
|
+
sim.load(scenario_file)
|
|
28
|
+
sim.build
|
|
29
|
+
|
|
30
|
+
puts "Loading scenario: #{scenario_file}"
|
|
31
|
+
puts "Devices: #{sim.topology.device_names.join(', ')}"
|
|
32
|
+
puts "Links: #{sim.topology.links.size}"
|
|
33
|
+
puts "Running simulation..."
|
|
34
|
+
|
|
35
|
+
if options[:steps]
|
|
36
|
+
sim.run(steps: options[:steps])
|
|
37
|
+
else
|
|
38
|
+
sim.run(final_time: options[:until])
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
puts "Simulation completed at t=#{sim.time}"
|
|
42
|
+
|
|
43
|
+
if options[:verbose]
|
|
44
|
+
sim.topology.devices.each_value do |device|
|
|
45
|
+
if device.is_a?(Device::Router)
|
|
46
|
+
puts sim.observer.format_rib(device)
|
|
47
|
+
elsif device.is_a?(Device::Switch)
|
|
48
|
+
puts sim.observer.format_mac(device)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
desc "dump SCENARIO", "导出设备表信息"
|
|
55
|
+
option :rib, type: :string, desc: "导出指定路由器的 RIB"
|
|
56
|
+
option :mac, type: :string, desc: "导出指定交换机的 MAC 表"
|
|
57
|
+
option :stp, type: :string, desc: "导出指定交换机的 STP 状态"
|
|
58
|
+
option :all, type: :boolean, default: false, desc: "导出所有设备"
|
|
59
|
+
|
|
60
|
+
def dump(scenario_file)
|
|
61
|
+
sim = Simulation.new
|
|
62
|
+
sim.load(scenario_file)
|
|
63
|
+
sim.build
|
|
64
|
+
sim.run(final_time: 1) # 跑一个 tick 让协议初始化
|
|
65
|
+
|
|
66
|
+
if options[:rib]
|
|
67
|
+
device = sim.topology.devices[options[:rib]]
|
|
68
|
+
if device && device.is_a?(Device::Router)
|
|
69
|
+
puts sim.observer.format_rib(device)
|
|
70
|
+
else
|
|
71
|
+
puts "Device #{options[:rib]} not found or not a router"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
if options[:mac]
|
|
76
|
+
device = sim.topology.devices[options[:mac]]
|
|
77
|
+
if device && device.is_a?(Device::Switch)
|
|
78
|
+
puts sim.observer.format_mac(device)
|
|
79
|
+
else
|
|
80
|
+
puts "Device #{options[:mac]} not found or not a switch"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
if options[:stp]
|
|
85
|
+
device = sim.topology.devices[options[:stp]]
|
|
86
|
+
if device && device.is_a?(Device::Switch) && device.stp
|
|
87
|
+
stp_info = device.stp_dump
|
|
88
|
+
puts "=== STP: #{options[:stp]} ==="
|
|
89
|
+
puts " Bridge ID: #{stp_info[:bridge_id]}"
|
|
90
|
+
puts " Root ID: #{stp_info[:root_id]}"
|
|
91
|
+
puts " Root Bridge: #{stp_info[:root_bridge]}"
|
|
92
|
+
stp_info[:ports].each { |port, info| puts " #{port}: role=#{info[:role]} state=#{info[:state]}" }
|
|
93
|
+
else
|
|
94
|
+
puts "Device #{options[:stp]} not found, not a switch, or STP not enabled"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
if options[:all]
|
|
99
|
+
sim.topology.devices.each_value do |device|
|
|
100
|
+
if device.is_a?(Device::Router)
|
|
101
|
+
puts sim.observer.format_rib(device)
|
|
102
|
+
elsif device.is_a?(Device::Switch)
|
|
103
|
+
puts sim.observer.format_mac(device)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
desc "trace SCENARIO", "追踪转发路径"
|
|
110
|
+
option :from, type: :string, required: true, desc: "起始设备"
|
|
111
|
+
option :to, type: :string, required: true, desc: "目的 IP"
|
|
112
|
+
|
|
113
|
+
def trace(scenario_file)
|
|
114
|
+
sim = Simulation.new
|
|
115
|
+
sim.load(scenario_file)
|
|
116
|
+
sim.build
|
|
117
|
+
sim.run(final_time: 10)
|
|
118
|
+
|
|
119
|
+
result = sim.trace(from: options[:from], to: options[:to])
|
|
120
|
+
puts sim.observer.format_trace(result)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
desc "list SCENARIO", "列出场景中的设备和链路"
|
|
124
|
+
|
|
125
|
+
def list(scenario_file)
|
|
126
|
+
sim = Simulation.new
|
|
127
|
+
sim.load(scenario_file)
|
|
128
|
+
|
|
129
|
+
puts "=== Devices ==="
|
|
130
|
+
sim.topology.devices.each_value do |d|
|
|
131
|
+
type = d.is_a?(Device::Router) ? "Router" : "Switch"
|
|
132
|
+
puts " #{d.name} (#{type})"
|
|
133
|
+
if d.is_a?(Device::Router)
|
|
134
|
+
d.interfaces.each { |port, info| puts " #{port}: #{info[:cidr]} (#{info[:status]})" }
|
|
135
|
+
else
|
|
136
|
+
puts " Ports: #{d.ports.keys.join(', ')}"
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
puts "\n=== Links ==="
|
|
141
|
+
sim.topology.links.each do |link|
|
|
142
|
+
puts " #{link.endpoint_a} <-> #{link.endpoint_b} (#{link.status}, bw=#{link.bandwidth}, delay=#{link.delay})"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
puts "\n=== Hosts ==="
|
|
146
|
+
sim.topology.hosts.each { |name, info| puts " #{name}: #{info[:ip]} gw=#{info[:gateway]}" }
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
NetworkInfraUtility::SimLab::CLI.start(ARGV)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# SimLab 统一入口模块。
|
|
5
|
+
#
|
|
6
|
+
# 负责加载仿真服务的所有子模块,提供统一接口。
|
|
7
|
+
#
|
|
8
|
+
# 用法:
|
|
9
|
+
# require_relative "lib/simlab"
|
|
10
|
+
# simlab = NetworkInfraUtility::SimLab::Simulation.new
|
|
11
|
+
# simlab.load("scenario.yml")
|
|
12
|
+
# simlab.run(final_time: 100)
|
|
13
|
+
# simlab.dump_rib("R1")
|
|
14
|
+
|
|
15
|
+
require_relative "simlab_topology"
|
|
16
|
+
require_relative "simlab_engine"
|
|
17
|
+
require_relative "simlab_router"
|
|
18
|
+
require_relative "simlab_switch"
|
|
19
|
+
require_relative "simlab_traffic"
|
|
20
|
+
require_relative "simlab_observer"
|
|
21
|
+
require_relative "simlab_scenario"
|
|
22
|
+
|
|
23
|
+
module NetworkInfraUtility
|
|
24
|
+
module SimLab
|
|
25
|
+
class Error < StandardError; end
|
|
26
|
+
|
|
27
|
+
class Simulation
|
|
28
|
+
attr_reader :topology, :engine, :observer, :time
|
|
29
|
+
|
|
30
|
+
def initialize
|
|
31
|
+
@topology = Topology.new
|
|
32
|
+
@engine = Engine.new
|
|
33
|
+
@observer = Observer.new
|
|
34
|
+
@traffic = Traffic.new
|
|
35
|
+
@time = 0
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# 加载场景文件(YAML 或 Ruby DSL)。
|
|
39
|
+
def load(file_path)
|
|
40
|
+
scenario = Scenario.load(file_path)
|
|
41
|
+
scenario.apply_to(self)
|
|
42
|
+
self
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# 从拓扑构建仿真环境。
|
|
46
|
+
def build
|
|
47
|
+
@topology.devices.each_value(&:build)
|
|
48
|
+
@topology.links.each { |link| link.activate }
|
|
49
|
+
self
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# 运行仿真直到指定时间。
|
|
53
|
+
def run(final_time: nil, steps: nil)
|
|
54
|
+
if final_time
|
|
55
|
+
@engine.run_until(final_time) do |time, event|
|
|
56
|
+
@time = time
|
|
57
|
+
tick_devices(time)
|
|
58
|
+
event.call
|
|
59
|
+
end
|
|
60
|
+
elsif steps
|
|
61
|
+
@engine.run_steps(steps) do |time, event|
|
|
62
|
+
@time = time
|
|
63
|
+
tick_devices(time)
|
|
64
|
+
event.call
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
self
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# 单步执行。
|
|
71
|
+
def step
|
|
72
|
+
@engine.step do |time, event|
|
|
73
|
+
@time = time
|
|
74
|
+
tick_devices(time)
|
|
75
|
+
event.call
|
|
76
|
+
end
|
|
77
|
+
self
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# 注入流量/事件。
|
|
81
|
+
def inject(event)
|
|
82
|
+
@traffic.inject(@engine, event)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# 导出 RIB 快照。
|
|
86
|
+
def dump_rib(device_name)
|
|
87
|
+
device = @topology.devices[device_name]
|
|
88
|
+
raise Error, "Device #{device_name} not found" unless device
|
|
89
|
+
|
|
90
|
+
@observer.rib_dump(device)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# 导出 MAC 表快照。
|
|
94
|
+
def dump_mac(device_name)
|
|
95
|
+
device = @topology.devices[device_name]
|
|
96
|
+
raise Error, "Device #{device_name} not found" unless device
|
|
97
|
+
|
|
98
|
+
@observer.mac_dump(device)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# 转发路径追踪。
|
|
102
|
+
def trace(from:, to:)
|
|
103
|
+
@observer.trace(topology: @topology, from: from, to: to)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# 收敛检测。
|
|
107
|
+
def converged?(ticks: 5)
|
|
108
|
+
@observer.converged?(topology: @topology, ticks: ticks, engine: @engine)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# 快照导出。
|
|
112
|
+
def snapshot
|
|
113
|
+
@observer.snapshot(@topology)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
# 全局时钟推进时,所有设备同步 tick。
|
|
119
|
+
def tick_devices(now)
|
|
120
|
+
@topology.devices.each_value { |device| device.tick(now: now) }
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# 离散事件仿真核心:全局虚拟时钟 + 事件队列。
|
|
5
|
+
#
|
|
6
|
+
# 设计要点:
|
|
7
|
+
# - 全局虚拟时钟
|
|
8
|
+
# - 事件队列(按时间排序)
|
|
9
|
+
# - 支持延迟事件(链路延迟、计时器、老化)
|
|
10
|
+
# - 收敛检测辅助(统计 N 个 tick 内 RIB/MAC 无变化)
|
|
11
|
+
#
|
|
12
|
+
# support 组件通过 tick(now) 被动推进,不自己计时。
|
|
13
|
+
# device 之间不直接互调方法,由 engine 按链路投递帧/报文(可带延迟)。
|
|
14
|
+
#
|
|
15
|
+
# 用法:
|
|
16
|
+
# engine = Engine.new
|
|
17
|
+
# engine.schedule(at: 10) { puts "tick at 10" }
|
|
18
|
+
# engine.schedule(at: 20, delay: 5) { puts "delayed event" }
|
|
19
|
+
# engine.run_until(30) { |time, event| event.call }
|
|
20
|
+
|
|
21
|
+
module NetworkInfraUtility
|
|
22
|
+
module SimLab
|
|
23
|
+
# 仿真事件。
|
|
24
|
+
Event = Data.define(:at, :priority, :id, :block) do
|
|
25
|
+
include Comparable
|
|
26
|
+
|
|
27
|
+
def <=>(other)
|
|
28
|
+
[at, priority] <=> [other.at, other.priority]
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# 投递信封:封装设备间传递的报文。
|
|
33
|
+
Envelope = Data.define(:at, :src_node, :src_if, :dst_node, :dst_if, :payload)
|
|
34
|
+
|
|
35
|
+
class Engine
|
|
36
|
+
attr_reader :clock, :event_count
|
|
37
|
+
|
|
38
|
+
def initialize
|
|
39
|
+
@clock = 0 # 虚拟时钟(秒)
|
|
40
|
+
@queue = [] # 事件队列(最小堆模拟)
|
|
41
|
+
@event_count = 0
|
|
42
|
+
@seq = 0 # 事件序号(用于稳定排序)
|
|
43
|
+
@stopped = false
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# 调度一个事件在指定时间执行。
|
|
47
|
+
# at: 执行时间
|
|
48
|
+
# priority: 优先级(数字小的先执行,默认 0)
|
|
49
|
+
# 返回事件 ID(可用于取消)。
|
|
50
|
+
def schedule(at:, priority: 0, &block)
|
|
51
|
+
@seq += 1
|
|
52
|
+
event = Event.new(at: at, priority: priority, id: @seq, block: block)
|
|
53
|
+
@queue << event
|
|
54
|
+
@queue.sort_by! { |e| [e.at, e.priority, e.id] }
|
|
55
|
+
@event_count += 1
|
|
56
|
+
@seq
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# 调度一个延迟事件。
|
|
60
|
+
# delay: 从当前时钟起的延迟
|
|
61
|
+
def schedule_delayed(delay:, priority: 0, &block)
|
|
62
|
+
schedule(at: @clock + delay, priority: priority, &block)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# 投递信封(带链路延迟)。
|
|
66
|
+
def deliver(envelope, delay: 0)
|
|
67
|
+
schedule(at: envelope.at + delay) do
|
|
68
|
+
yield envelope if block_given?
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# 取消指定 ID 的事件。
|
|
73
|
+
def cancel(event_id)
|
|
74
|
+
@queue.reject! { |e| e.id == event_id }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# 运行到指定时间。
|
|
78
|
+
def run_until(final_time)
|
|
79
|
+
while !@queue.empty? && !@stopped
|
|
80
|
+
event = @queue.first
|
|
81
|
+
break if event.at > final_time
|
|
82
|
+
|
|
83
|
+
@queue.shift
|
|
84
|
+
@clock = event.at
|
|
85
|
+
yield event.at, event.block if block_given?
|
|
86
|
+
end
|
|
87
|
+
@clock = final_time if @clock < final_time
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# 执行指定步数的事件。
|
|
91
|
+
def run_steps(steps)
|
|
92
|
+
steps.times do
|
|
93
|
+
break if @queue.empty? || @stopped
|
|
94
|
+
|
|
95
|
+
event = @queue.shift
|
|
96
|
+
@clock = event.at
|
|
97
|
+
yield event.at, event.block if block_given?
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# 执行单步。
|
|
102
|
+
def step
|
|
103
|
+
return nil if @queue.empty? || @stopped
|
|
104
|
+
|
|
105
|
+
event = @queue.shift
|
|
106
|
+
@clock = event.at
|
|
107
|
+
yield event.at, event.block if block_given?
|
|
108
|
+
event
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# 停止仿真。
|
|
112
|
+
def stop
|
|
113
|
+
@stopped = true
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# 重置。
|
|
117
|
+
def reset
|
|
118
|
+
@clock = 0
|
|
119
|
+
@queue.clear
|
|
120
|
+
@event_count = 0
|
|
121
|
+
@seq = 0
|
|
122
|
+
@stopped = false
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# 队列中剩余事件数。
|
|
126
|
+
def pending
|
|
127
|
+
@queue.size
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# 是否有空闲(队列空)。
|
|
131
|
+
def idle?
|
|
132
|
+
@queue.empty?
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# 当前时钟。
|
|
136
|
+
def now
|
|
137
|
+
@clock
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# 查看下一个事件时间。
|
|
141
|
+
def next_event_time
|
|
142
|
+
@queue.first&.at
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# 观测导出:RIB/MAC 快照、转发路径 trace、收敛时间统计。
|
|
5
|
+
#
|
|
6
|
+
# 输出 JSON/文本快照,便于断言和教学演示。
|
|
7
|
+
#
|
|
8
|
+
# 用法:
|
|
9
|
+
# observer = Observer.new
|
|
10
|
+
# observer.rib_dump(router)
|
|
11
|
+
# observer.mac_dump(switch)
|
|
12
|
+
# observer.trace(topology: topo, from: "H1", to: "10.0.0.2")
|
|
13
|
+
|
|
14
|
+
require 'json'
|
|
15
|
+
require 'set'
|
|
16
|
+
|
|
17
|
+
module NetworkInfraUtility
|
|
18
|
+
module SimLab
|
|
19
|
+
class Observer
|
|
20
|
+
# RIB 快照。
|
|
21
|
+
def rib_dump(device)
|
|
22
|
+
device.rib_dump
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# MAC 表快照。
|
|
26
|
+
def mac_dump(device)
|
|
27
|
+
device.mac_dump
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# 转发路径追踪。
|
|
31
|
+
# 返回 [{device:, port:, decision:}] 的跳转路径。
|
|
32
|
+
def trace(topology:, from:, to:)
|
|
33
|
+
path = []
|
|
34
|
+
visited = Set.new
|
|
35
|
+
current_device = topology.devices[from]
|
|
36
|
+
|
|
37
|
+
return path unless current_device
|
|
38
|
+
|
|
39
|
+
while current_device && !visited.include?(current_device.name)
|
|
40
|
+
visited.add(current_device.name)
|
|
41
|
+
|
|
42
|
+
if current_device.is_a?(Device::Router)
|
|
43
|
+
# 路由器:查 RIB 确定下一跳
|
|
44
|
+
route = current_device.rib.lookup(parse_ip(to))
|
|
45
|
+
break unless route
|
|
46
|
+
|
|
47
|
+
hop = {
|
|
48
|
+
device: current_device.name,
|
|
49
|
+
type: :router,
|
|
50
|
+
port: route.ifindex,
|
|
51
|
+
next_hop: route.next_hop,
|
|
52
|
+
protocol: route.protocol
|
|
53
|
+
}
|
|
54
|
+
path << hop
|
|
55
|
+
|
|
56
|
+
# 找对端设备
|
|
57
|
+
adj = topology.peer_of(current_device.name, route.ifindex)
|
|
58
|
+
break unless adj
|
|
59
|
+
|
|
60
|
+
current_device = topology.devices[adj[:peer_device]]
|
|
61
|
+
|
|
62
|
+
elsif current_device.is_a?(Device::Switch)
|
|
63
|
+
# 交换机:查 MAC 表确定出端口
|
|
64
|
+
mac_entry = current_device.mac_table.lookup(to)
|
|
65
|
+
port = mac_entry ? mac_entry.port : nil
|
|
66
|
+
|
|
67
|
+
hop = {
|
|
68
|
+
device: current_device.name,
|
|
69
|
+
type: :switch,
|
|
70
|
+
port: port || 'flood',
|
|
71
|
+
decision: mac_entry ? :forward : :flood
|
|
72
|
+
}
|
|
73
|
+
path << hop
|
|
74
|
+
|
|
75
|
+
if port
|
|
76
|
+
adj = topology.peer_of(current_device.name, port)
|
|
77
|
+
break unless adj
|
|
78
|
+
current_device = topology.devices[adj[:peer_device]]
|
|
79
|
+
else
|
|
80
|
+
break
|
|
81
|
+
end
|
|
82
|
+
else
|
|
83
|
+
break
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
path
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# 收敛检测:统计 N 个 tick 内 RIB/MAC 是否无变化。
|
|
91
|
+
def converged?(topology:, ticks:, engine:)
|
|
92
|
+
# 简化实现:检查是否还有 pending 事件
|
|
93
|
+
# 真实实现应比较 RIB/MAC 快照
|
|
94
|
+
snapshots_before = capture_snapshots(topology)
|
|
95
|
+
|
|
96
|
+
ticks.times do
|
|
97
|
+
engine.step do |time, event|
|
|
98
|
+
topology.devices.each_value { |d| d.tick(now: time) }
|
|
99
|
+
event.call
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
snapshots_after = capture_snapshots(topology)
|
|
104
|
+
snapshots_before == snapshots_after
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# 全局快照。
|
|
108
|
+
def snapshot(topology)
|
|
109
|
+
{
|
|
110
|
+
devices: topology.devices.transform_values { |d| d.to_h },
|
|
111
|
+
links: topology.links.map { |l| { a: l.endpoint_a.to_s, b: l.endpoint_b.to_s, status: l.status } },
|
|
112
|
+
timestamp: engine_now
|
|
113
|
+
}
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# 导出为 JSON。
|
|
117
|
+
def to_json(topology)
|
|
118
|
+
JSON.pretty_generate(snapshot(topology))
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# RIB 快照格式化输出。
|
|
122
|
+
def format_rib(device)
|
|
123
|
+
rib_data = device.rib_dump
|
|
124
|
+
lines = ["=== RIB: #{device.name} ==="]
|
|
125
|
+
|
|
126
|
+
rib_data.each do |prefix, routes|
|
|
127
|
+
routes.each do |r|
|
|
128
|
+
lines << " #{prefix} via #{r[:next_hop]} (#{r[:protocol]} metric=#{r[:metric]})"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
lines.join("\n")
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# MAC 表快照格式化输出。
|
|
136
|
+
def format_mac(device)
|
|
137
|
+
mac_data = device.mac_dump
|
|
138
|
+
lines = ["=== MAC Table: #{device.name} ==="]
|
|
139
|
+
|
|
140
|
+
mac_data.each do |mac, info|
|
|
141
|
+
lines << " #{mac} -> #{info[:port]}" + (info[:static] ? ' (static)' : '')
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
lines.join("\n")
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# 路径追踪格式化输出。
|
|
148
|
+
def format_trace(trace_result)
|
|
149
|
+
lines = ["=== Trace ==="]
|
|
150
|
+
trace_result.each do |hop|
|
|
151
|
+
lines << " #{hop[:device]} (#{hop[:type]}) -> port #{hop[:port]}" +
|
|
152
|
+
(hop[:next_hop] ? " next_hop=#{hop[:next_hop]} proto=#{hop[:protocol]}" : '') +
|
|
153
|
+
(hop[:decision] ? " decision=#{hop[:decision]}" : '')
|
|
154
|
+
end
|
|
155
|
+
lines << " (reached destination)" if trace_result.any?
|
|
156
|
+
lines.join("\n")
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
private
|
|
160
|
+
|
|
161
|
+
def capture_snapshots(topology)
|
|
162
|
+
topology.devices.transform_values do |d|
|
|
163
|
+
d.is_a?(Device::Router) ? d.rib_dump : d.mac_dump
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def parse_ip(ip_str)
|
|
168
|
+
ip_str.include?(':') ? IPv6.new(ip_str) : IPv4.new(ip_str)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def engine_now
|
|
172
|
+
Time.now.to_i # 简化
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|