phut 0.1.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.
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