phut 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/.rspec +3 -0
- data/.rubocop.yml +11 -0
- data/.ruby-version +1 -0
- data/.travis.yml +34 -0
- data/CHANGELOG.md +8 -0
- data/CONTRIBUTING.md +46 -0
- data/Gemfile +4 -0
- data/Guardfile +29 -0
- data/LICENSE +339 -0
- data/README.md +49 -0
- data/Rakefile +10 -0
- data/bin/phut +111 -0
- data/features/dsl.feature +26 -0
- data/features/dsl_link.feature +22 -0
- data/features/dsl_vhost.feature +37 -0
- data/features/dsl_vswitch.feature +45 -0
- data/features/phut.feature +5 -0
- data/features/shell.feature +40 -0
- data/features/step_definitions/phut_steps.rb +30 -0
- data/features/support/env.rb +4 -0
- data/features/support/hooks.rb +34 -0
- data/lib/phut.rb +5 -0
- data/lib/phut/cli.rb +61 -0
- data/lib/phut/configuration.rb +64 -0
- data/lib/phut/null_logger.rb +14 -0
- data/lib/phut/open_vswitch.rb +116 -0
- data/lib/phut/parser.rb +51 -0
- data/lib/phut/phost.rb +92 -0
- data/lib/phut/setting.rb +54 -0
- data/lib/phut/shell_runner.rb +9 -0
- data/lib/phut/syntax.rb +123 -0
- data/lib/phut/version.rb +4 -0
- data/lib/phut/virtual_link.rb +56 -0
- data/phut.gemspec +55 -0
- data/spec/phut/open_vswitch_spec.rb +21 -0
- data/spec/phut/parser_spec.rb +69 -0
- data/spec/phut_spec.rb +43 -0
- data/spec/spec_helper.rb +14 -0
- data/tasks/cucumber.rake +18 -0
- data/tasks/flog.rake +25 -0
- data/tasks/gem.rake +13 -0
- data/tasks/openvswitch.rake +26 -0
- data/tasks/reek.rake +11 -0
- data/tasks/relish.rake +4 -0
- data/tasks/rspec.rake +9 -0
- data/tasks/rubocop.rake +8 -0
- data/tasks/vhost.rake +32 -0
- metadata +426 -0
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'phut/null_logger'
|
3
|
+
require 'phut/open_vswitch'
|
4
|
+
|
5
|
+
module Phut
|
6
|
+
# Parsed DSL data.
|
7
|
+
class Configuration
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
def_delegators :@all, :fetch
|
11
|
+
|
12
|
+
def initialize(logger = NullLogger.new)
|
13
|
+
@all = {}
|
14
|
+
@logger = logger
|
15
|
+
end
|
16
|
+
|
17
|
+
def vswitches
|
18
|
+
@all.values.select { |each| each.is_a? OpenVswitch }
|
19
|
+
end
|
20
|
+
|
21
|
+
def vhosts
|
22
|
+
@all.values.select { |each| each.is_a? Phost }
|
23
|
+
end
|
24
|
+
|
25
|
+
def links
|
26
|
+
@all.values.select { |each| each.is_a? VirtualLink }
|
27
|
+
end
|
28
|
+
|
29
|
+
def run
|
30
|
+
links.each(&:run)
|
31
|
+
vswitches.each(&:run)
|
32
|
+
vhosts.each { |each| each.run vhosts }
|
33
|
+
end
|
34
|
+
|
35
|
+
def stop
|
36
|
+
vswitches.each(&:maybe_stop)
|
37
|
+
vhosts.each(&:maybe_stop)
|
38
|
+
links.each(&:maybe_stop)
|
39
|
+
end
|
40
|
+
|
41
|
+
def add_vswitch(name, attrs)
|
42
|
+
check_name_conflict name
|
43
|
+
@all[name] = OpenVswitch.new(attrs[:dpid], name, @logger)
|
44
|
+
end
|
45
|
+
|
46
|
+
def add_vhost(name, attrs)
|
47
|
+
check_name_conflict name
|
48
|
+
@all[name] = Phost.new(attrs[:ip], attrs[:promisc], name, @logger)
|
49
|
+
end
|
50
|
+
|
51
|
+
# This method smells of :reek:LongParameterList
|
52
|
+
def add_link(name_a, device_a, name_b, device_b)
|
53
|
+
@all[[name_a, name_b]] =
|
54
|
+
VirtualLink.new(name_a, device_a, name_b, device_b, @logger)
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def check_name_conflict(name)
|
60
|
+
conflict = @all[name]
|
61
|
+
fail "The name #{name} conflicts with #{conflict}." if conflict
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'phut/null_logger'
|
2
|
+
require 'phut/setting'
|
3
|
+
require 'phut/shell_runner'
|
4
|
+
|
5
|
+
module Phut
|
6
|
+
# Open vSwitch controller.
|
7
|
+
class OpenVswitch
|
8
|
+
include ShellRunner
|
9
|
+
|
10
|
+
OPENFLOWD =
|
11
|
+
"#{Phut.root}/vendor/openvswitch-1.2.2.trema1/tests/test-openflowd"
|
12
|
+
OFCTL =
|
13
|
+
"#{Phut.root}/vendor/openvswitch-1.2.2.trema1/utilities/ovs-ofctl"
|
14
|
+
|
15
|
+
attr_reader :dpid
|
16
|
+
alias_method :datapath_id, :dpid
|
17
|
+
attr_writer :interfaces
|
18
|
+
|
19
|
+
def initialize(dpid, name = nil, logger = NullLogger.new)
|
20
|
+
@dpid = dpid
|
21
|
+
@name = name
|
22
|
+
@interfaces = []
|
23
|
+
@logger = logger
|
24
|
+
end
|
25
|
+
|
26
|
+
def name
|
27
|
+
@name || format('%#x', @dpid)
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_s
|
31
|
+
"vswitch (name = #{name}, dpid = #{format('%#x', @dpid)})"
|
32
|
+
end
|
33
|
+
|
34
|
+
def run
|
35
|
+
sh "sudo #{OPENFLOWD} #{options.join ' '}"
|
36
|
+
rescue
|
37
|
+
raise "Open vSwitch (dpid = #{@dpid}) is already running!"
|
38
|
+
end
|
39
|
+
alias_method :start, :run
|
40
|
+
|
41
|
+
def stop
|
42
|
+
fail "Open vSwitch (dpid = #{@dpid}) is not running!" unless running?
|
43
|
+
pid = IO.read(pid_file).chomp
|
44
|
+
sh "sudo kill #{pid}"
|
45
|
+
end
|
46
|
+
alias_method :shutdown, :stop
|
47
|
+
|
48
|
+
def maybe_stop
|
49
|
+
return unless running?
|
50
|
+
stop
|
51
|
+
end
|
52
|
+
|
53
|
+
def bring_port_up(port_number)
|
54
|
+
sh "sudo #{OFCTL} mod-port #{network_device} #{port_number} up"
|
55
|
+
end
|
56
|
+
|
57
|
+
def bring_port_down(port_number)
|
58
|
+
sh "sudo #{OFCTL} mod-port #{network_device} #{port_number} down"
|
59
|
+
end
|
60
|
+
|
61
|
+
def dump_flows
|
62
|
+
sh "sudo #{OFCTL} dump-flows #{network_device}"
|
63
|
+
end
|
64
|
+
|
65
|
+
def running?
|
66
|
+
FileTest.exists?(pid_file)
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def restart
|
72
|
+
stop
|
73
|
+
start
|
74
|
+
end
|
75
|
+
|
76
|
+
def pid_file
|
77
|
+
"#{Phut.pid_dir}/open_vswitch.#{name}.pid"
|
78
|
+
end
|
79
|
+
|
80
|
+
def network_device
|
81
|
+
"vsw_#{name}"
|
82
|
+
end
|
83
|
+
|
84
|
+
# rubocop:disable MethodLength
|
85
|
+
def options
|
86
|
+
%W(--detach
|
87
|
+
--out-of-band
|
88
|
+
--fail=closed
|
89
|
+
--inactivity-probe=180
|
90
|
+
--rate-limit=40000
|
91
|
+
--burst-limit=20000
|
92
|
+
--pidfile=#{pid_file}
|
93
|
+
--verbose=ANY:file:#{logging_level}
|
94
|
+
--verbose=ANY:console:err
|
95
|
+
--log-file=#{Phut.log_dir}/open_vswitch.#{name}.log
|
96
|
+
--datapath-id=#{dpid_zero_filled}
|
97
|
+
--unixctl=#{Phut.socket_dir}/open_vswitch.#{name}.ctl
|
98
|
+
netdev@#{network_device} tcp:127.0.0.1:6633) +
|
99
|
+
ports_option
|
100
|
+
end
|
101
|
+
# rubocop:enable MethodLength
|
102
|
+
|
103
|
+
def dpid_zero_filled
|
104
|
+
hex = format('%x', @dpid)
|
105
|
+
'0' * (16 - hex.length) + hex
|
106
|
+
end
|
107
|
+
|
108
|
+
def logging_level
|
109
|
+
@logger.level == Logger::DEBUG ? 'dbg' : 'info'
|
110
|
+
end
|
111
|
+
|
112
|
+
def ports_option
|
113
|
+
@interfaces.empty? ? [] : ["--ports=#{@interfaces.join(',')}"]
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
data/lib/phut/parser.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'phut/configuration'
|
2
|
+
require 'phut/null_logger'
|
3
|
+
require 'phut/syntax'
|
4
|
+
|
5
|
+
module Phut
|
6
|
+
# Configuration DSL parser.
|
7
|
+
class Parser
|
8
|
+
def initialize(logger = NullLogger.new)
|
9
|
+
@config = Configuration.new(logger)
|
10
|
+
end
|
11
|
+
|
12
|
+
def parse(file)
|
13
|
+
Syntax.new(@config).instance_eval IO.read(file), file
|
14
|
+
assign_vswitch_interfaces
|
15
|
+
assign_vhost_interface
|
16
|
+
@config
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def assign_vswitch_interfaces
|
22
|
+
@config.vswitches.each do |each|
|
23
|
+
each.interfaces = find_interfaces_by_name(each.name)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def assign_vhost_interface
|
28
|
+
@config.vhosts.each do |each|
|
29
|
+
each.interface = find_host_interface_by_name(each.name)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def find_interfaces_by_name(name)
|
34
|
+
find_device_by_name(name, :name_a, :device_a) +
|
35
|
+
find_device_by_name(name, :name_b, :device_b)
|
36
|
+
end
|
37
|
+
|
38
|
+
def find_host_interface_by_name(name)
|
39
|
+
find_interfaces_by_name(name).tap do |interface|
|
40
|
+
fail "No link found for host #{name}" if interface.empty?
|
41
|
+
fail "Multiple links connect to host #{name}" if interface.size > 1
|
42
|
+
end.first
|
43
|
+
end
|
44
|
+
|
45
|
+
def find_device_by_name(name, name_type, device_type)
|
46
|
+
@config.links.select do |each|
|
47
|
+
each.__send__(name_type) == name
|
48
|
+
end.map(&device_type)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/phut/phost.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'phut/cli'
|
2
|
+
require 'phut/null_logger'
|
3
|
+
require 'phut/setting'
|
4
|
+
require 'phut/shell_runner'
|
5
|
+
require 'pio/mac'
|
6
|
+
|
7
|
+
module Phut
|
8
|
+
# An interface class to phost emulation utility program.
|
9
|
+
class Phost
|
10
|
+
include ShellRunner
|
11
|
+
|
12
|
+
attr_reader :ip
|
13
|
+
attr_reader :mac
|
14
|
+
attr_accessor :interface
|
15
|
+
|
16
|
+
def initialize(ip_address, promisc, name = nil, logger = NullLogger.new)
|
17
|
+
@ip = ip_address
|
18
|
+
@promisc = promisc
|
19
|
+
@name = name
|
20
|
+
@mac = Pio::Mac.new(rand(0xffffffffffff + 1))
|
21
|
+
@logger = logger
|
22
|
+
end
|
23
|
+
|
24
|
+
def name
|
25
|
+
@name || @ip
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_s
|
29
|
+
"vhost (name = #{name}, ip = #{@ip})"
|
30
|
+
end
|
31
|
+
|
32
|
+
def run(hosts = [])
|
33
|
+
sh "sudo #{executable} #{options.join ' '}"
|
34
|
+
sleep 1
|
35
|
+
set_ip_and_mac_address
|
36
|
+
maybe_enable_promisc
|
37
|
+
add_arp_entries hosts
|
38
|
+
end
|
39
|
+
|
40
|
+
def stop
|
41
|
+
fail "phost (name = #{name}) is not running!" unless running?
|
42
|
+
pid = IO.read(pid_file)
|
43
|
+
sh "sudo kill #{pid}"
|
44
|
+
end
|
45
|
+
|
46
|
+
def maybe_stop
|
47
|
+
return unless running?
|
48
|
+
stop
|
49
|
+
end
|
50
|
+
|
51
|
+
def netmask
|
52
|
+
'255.255.255.255'
|
53
|
+
end
|
54
|
+
|
55
|
+
def running?
|
56
|
+
FileTest.exists?(pid_file)
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def set_ip_and_mac_address
|
62
|
+
Phut::Cli.new(self, @logger).set_ip_and_mac_address
|
63
|
+
end
|
64
|
+
|
65
|
+
def maybe_enable_promisc
|
66
|
+
return unless @promisc
|
67
|
+
Phut::Cli.new(self).enable_promisc
|
68
|
+
end
|
69
|
+
|
70
|
+
def add_arp_entries(hosts)
|
71
|
+
hosts.each do |each|
|
72
|
+
Phut::Cli.new(self).add_arp_entry each
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def pid_file
|
77
|
+
"#{Phut.pid_dir}/phost.#{name}.pid"
|
78
|
+
end
|
79
|
+
|
80
|
+
def executable
|
81
|
+
"#{Phut.root}/vendor/phost/src/phost"
|
82
|
+
end
|
83
|
+
|
84
|
+
def options
|
85
|
+
%W(-p #{Phut.pid_dir}
|
86
|
+
-l #{Phut.log_dir}
|
87
|
+
-n #{name}
|
88
|
+
-i #{interface}
|
89
|
+
-D)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
data/lib/phut/setting.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'tmpdir'
|
2
|
+
|
3
|
+
# Base module.
|
4
|
+
module Phut
|
5
|
+
# Central configuration repository.
|
6
|
+
class Setting
|
7
|
+
DEFAULTS = {
|
8
|
+
root: File.expand_path(File.join(__dir__, '..', '..')),
|
9
|
+
pid_dir: Dir.tmpdir,
|
10
|
+
log_dir: Dir.tmpdir,
|
11
|
+
socket_dir: Dir.tmpdir
|
12
|
+
}
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@options = DEFAULTS.dup
|
16
|
+
end
|
17
|
+
|
18
|
+
def root
|
19
|
+
@options.fetch :root
|
20
|
+
end
|
21
|
+
|
22
|
+
def pid_dir
|
23
|
+
@options.fetch :pid_dir
|
24
|
+
end
|
25
|
+
|
26
|
+
def pid_dir=(path)
|
27
|
+
@options[:pid_dir] = File.expand_path(path)
|
28
|
+
end
|
29
|
+
|
30
|
+
def log_dir
|
31
|
+
@options.fetch :log_dir
|
32
|
+
end
|
33
|
+
|
34
|
+
def log_dir=(path)
|
35
|
+
@options[:log_dir] = File.expand_path(path)
|
36
|
+
end
|
37
|
+
|
38
|
+
def socket_dir
|
39
|
+
@options.fetch :socket_dir
|
40
|
+
end
|
41
|
+
|
42
|
+
def socket_dir=(path)
|
43
|
+
@options[:socket_dir] = File.expand_path(path)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
SettingSingleton = Setting.new
|
48
|
+
|
49
|
+
class << self
|
50
|
+
def method_missing(method, *args)
|
51
|
+
SettingSingleton.__send__ method, *args
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/phut/syntax.rb
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'phut/phost'
|
2
|
+
require 'phut/virtual_link'
|
3
|
+
|
4
|
+
module Phut
|
5
|
+
# DSL syntax definitions.
|
6
|
+
class Syntax
|
7
|
+
# The 'vswitch(name) { ...attributes... }' directive.
|
8
|
+
class VswitchDirective
|
9
|
+
def initialize(alias_name, &block)
|
10
|
+
@attributes = { name: alias_name }
|
11
|
+
instance_eval(&block)
|
12
|
+
end
|
13
|
+
|
14
|
+
def dpid(value)
|
15
|
+
dpid = if value.is_a?(String) && /^0x/=~ value
|
16
|
+
value.hex
|
17
|
+
else
|
18
|
+
value.to_i
|
19
|
+
end
|
20
|
+
@attributes[:dpid] = dpid
|
21
|
+
@attributes[:name] ||= format('%#x', @attributes[:dpid])
|
22
|
+
end
|
23
|
+
alias_method :datapath_id, :dpid
|
24
|
+
|
25
|
+
def [](key)
|
26
|
+
@attributes[key]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# The 'vhost(name) { ...attributes... }' directive.
|
31
|
+
class VhostDirective
|
32
|
+
# Generates an unused IP address
|
33
|
+
class UnusedIpAddress
|
34
|
+
def initialize
|
35
|
+
@index = 0
|
36
|
+
end
|
37
|
+
|
38
|
+
def generate
|
39
|
+
@index += 1
|
40
|
+
"192.168.0.#{@index}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
UnusedIpAddressSingleton = UnusedIpAddress.new
|
45
|
+
|
46
|
+
def initialize(alias_name, &block)
|
47
|
+
@attributes = { name: alias_name }
|
48
|
+
if block
|
49
|
+
instance_eval(&block) if block
|
50
|
+
else
|
51
|
+
@attributes[:ip] = UnusedIpAddressSingleton.generate
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def ip(value)
|
56
|
+
@attributes[:ip] = value
|
57
|
+
@attributes[:name] ||= value
|
58
|
+
end
|
59
|
+
|
60
|
+
def promisc(on_off)
|
61
|
+
@attributes[:promisc] = on_off
|
62
|
+
end
|
63
|
+
|
64
|
+
def [](key)
|
65
|
+
@attributes[key]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# The 'link name_a, name_b' directive.
|
70
|
+
class LinkDirective
|
71
|
+
# Generates an unique Link ID
|
72
|
+
class LinkId
|
73
|
+
def initialize
|
74
|
+
init
|
75
|
+
end
|
76
|
+
|
77
|
+
def init
|
78
|
+
@index = 0
|
79
|
+
end
|
80
|
+
|
81
|
+
def generate
|
82
|
+
@index += 1
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
LinkIdSingleton = LinkId.new
|
87
|
+
|
88
|
+
def initialize(name_a, name_b, link_id)
|
89
|
+
@attributes = {}
|
90
|
+
@attributes[:name_a] = name_a
|
91
|
+
@attributes[:name_b] = name_b
|
92
|
+
link_id = LinkIdSingleton.generate
|
93
|
+
@attributes[:device_a] = "link#{link_id}-0"
|
94
|
+
@attributes[:device_b] = "link#{link_id}-1"
|
95
|
+
end
|
96
|
+
|
97
|
+
def [](key)
|
98
|
+
@attributes[key]
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def initialize(config)
|
103
|
+
@config = config
|
104
|
+
LinkDirective::LinkIdSingleton.init
|
105
|
+
end
|
106
|
+
|
107
|
+
def vswitch(alias_name = nil, &block)
|
108
|
+
attrs = VswitchDirective.new(alias_name, &block)
|
109
|
+
@config.add_vswitch attrs[:name], attrs
|
110
|
+
end
|
111
|
+
|
112
|
+
def vhost(alias_name = nil, &block)
|
113
|
+
attrs = VhostDirective.new(alias_name, &block)
|
114
|
+
@config.add_vhost attrs[:name], attrs
|
115
|
+
end
|
116
|
+
|
117
|
+
def link(name_a, name_b)
|
118
|
+
link_id = @config.links.size
|
119
|
+
attrs = LinkDirective.new(name_a, name_b, link_id)
|
120
|
+
@config.add_link name_a, attrs[:device_a], name_b, attrs[:device_b]
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|