phut 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +11 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +34 -0
  7. data/CHANGELOG.md +8 -0
  8. data/CONTRIBUTING.md +46 -0
  9. data/Gemfile +4 -0
  10. data/Guardfile +29 -0
  11. data/LICENSE +339 -0
  12. data/README.md +49 -0
  13. data/Rakefile +10 -0
  14. data/bin/phut +111 -0
  15. data/features/dsl.feature +26 -0
  16. data/features/dsl_link.feature +22 -0
  17. data/features/dsl_vhost.feature +37 -0
  18. data/features/dsl_vswitch.feature +45 -0
  19. data/features/phut.feature +5 -0
  20. data/features/shell.feature +40 -0
  21. data/features/step_definitions/phut_steps.rb +30 -0
  22. data/features/support/env.rb +4 -0
  23. data/features/support/hooks.rb +34 -0
  24. data/lib/phut.rb +5 -0
  25. data/lib/phut/cli.rb +61 -0
  26. data/lib/phut/configuration.rb +64 -0
  27. data/lib/phut/null_logger.rb +14 -0
  28. data/lib/phut/open_vswitch.rb +116 -0
  29. data/lib/phut/parser.rb +51 -0
  30. data/lib/phut/phost.rb +92 -0
  31. data/lib/phut/setting.rb +54 -0
  32. data/lib/phut/shell_runner.rb +9 -0
  33. data/lib/phut/syntax.rb +123 -0
  34. data/lib/phut/version.rb +4 -0
  35. data/lib/phut/virtual_link.rb +56 -0
  36. data/phut.gemspec +55 -0
  37. data/spec/phut/open_vswitch_spec.rb +21 -0
  38. data/spec/phut/parser_spec.rb +69 -0
  39. data/spec/phut_spec.rb +43 -0
  40. data/spec/spec_helper.rb +14 -0
  41. data/tasks/cucumber.rake +18 -0
  42. data/tasks/flog.rake +25 -0
  43. data/tasks/gem.rake +13 -0
  44. data/tasks/openvswitch.rake +26 -0
  45. data/tasks/reek.rake +11 -0
  46. data/tasks/relish.rake +4 -0
  47. data/tasks/rspec.rake +9 -0
  48. data/tasks/rubocop.rake +8 -0
  49. data/tasks/vhost.rake +32 -0
  50. 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,14 @@
1
+ require 'logger'
2
+
3
+ module Phut
4
+ # Null logger
5
+ class NullLogger < Logger
6
+ def initialize(*_args)
7
+ # noop
8
+ end
9
+
10
+ def add(*_args, &_block)
11
+ # noop
12
+ end
13
+ end
14
+ 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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,9 @@
1
+ module Phut
2
+ # Provides sh method.
3
+ module ShellRunner
4
+ def sh(command)
5
+ system(command) || fail("#{command} failed.")
6
+ @logger.debug(command) if @logger
7
+ end
8
+ end
9
+ end
@@ -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