pio 0.10.1 → 0.11.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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +6 -0
  3. data/README.md +33 -2
  4. data/examples/echo_read.rb +1 -1
  5. data/examples/features_read.rb +1 -1
  6. data/examples/flow_mod_new.rb +13 -0
  7. data/examples/flow_mod_read.rb +6 -0
  8. data/features/echo_read.feature +27 -3
  9. data/features/features_read.feature +46 -2
  10. data/features/flow_mod_read.feature +186 -0
  11. data/features/hello_read.feature +9 -0
  12. data/features/packet_data/flow_mod_add.raw +0 -0
  13. data/features/packet_data/flow_mod_delete.raw +0 -0
  14. data/features/packet_data/flow_mod_delete_strict.raw +0 -0
  15. data/features/packet_data/flow_mod_modify.raw +0 -0
  16. data/features/packet_data/flow_mod_modify_strict.raw +0 -0
  17. data/features/packet_in_read.feature +13 -0
  18. data/features/packet_out_read.feature +16 -0
  19. data/features/step_definitions/packet_data_steps.rb +10 -1
  20. data/lib/pio.rb +1 -0
  21. data/lib/pio/echo.rb +10 -8
  22. data/lib/pio/enqueue.rb +1 -1
  23. data/lib/pio/features.rb +64 -7
  24. data/lib/pio/flow_mod.rb +86 -0
  25. data/lib/pio/hello.rb +4 -74
  26. data/lib/pio/ipv4_address.rb +1 -1
  27. data/lib/pio/match.rb +167 -0
  28. data/lib/pio/open_flow.rb +1 -0
  29. data/lib/pio/open_flow/actions.rb +65 -0
  30. data/lib/pio/open_flow/flags.rb +12 -9
  31. data/lib/pio/open_flow/message.rb +105 -21
  32. data/lib/pio/open_flow/phy_port.rb +31 -27
  33. data/lib/pio/open_flow/type.rb +1 -0
  34. data/lib/pio/packet_in.rb +2 -12
  35. data/lib/pio/packet_out.rb +4 -74
  36. data/lib/pio/send_out_port.rb +1 -0
  37. data/lib/pio/type/ip_address.rb +2 -7
  38. data/lib/pio/version.rb +1 -1
  39. data/pio.gemspec +8 -8
  40. data/spec/pio/echo/reply_spec.rb +61 -6
  41. data/spec/pio/echo/request_spec.rb +61 -6
  42. data/spec/pio/features/reply_spec.rb +81 -4
  43. data/spec/pio/features/request_spec.rb +88 -41
  44. data/spec/pio/flow_mod_spec.rb +151 -0
  45. data/spec/pio/hello_spec.rb +73 -56
  46. data/spec/pio/match_spec.rb +194 -0
  47. data/spec/pio/packet_in_spec.rb +35 -38
  48. data/spec/pio/packet_out_spec.rb +243 -182
  49. data/spec/pio/wildcards_spec.rb +115 -0
  50. metadata +37 -30
  51. data/features/packet_data/echo.raw +0 -0
  52. data/lib/pio/echo/message.rb +0 -49
  53. data/lib/pio/echo/reply.rb +0 -41
  54. data/lib/pio/echo/request.rb +0 -43
  55. data/lib/pio/features/reply.rb +0 -88
  56. data/lib/pio/features/request.rb +0 -68
  57. data/lib/pio/open_flow/parser.rb +0 -22
  58. data/spec/pio/echo_spec.rb +0 -49
  59. data/spec/pio/features_spec.rb +0 -96
@@ -3,3 +3,19 @@ Feature: Pio::PacketOut.read
3
3
  Given a packet data file "packet_out.raw"
4
4
  When I try to parse the file with "PacketOut" class
5
5
  Then it should finish successfully
6
+ And the parsed data have the following field and value:
7
+ | field | value |
8
+ | class | Pio::PacketOut |
9
+ | ofp_version | 1 |
10
+ | message_type | 13 |
11
+ | message_length | 88 |
12
+ | transaction_id | 22 |
13
+ | xid | 22 |
14
+ | buffer_id | 4294967295 |
15
+ | in_port | 65535 |
16
+ | actions.length | 1 |
17
+ | actions.first.class | Pio::SendOutPort |
18
+ | actions.first.port_number | 2 |
19
+ | actions.first.max_len | 65535 |
20
+ | data.length | 64 |
21
+
@@ -14,7 +14,7 @@ end
14
14
  When(/^I try to parse the file with "(.*?)" class$/) do |parser|
15
15
  parser_klass = Pio.const_get(parser)
16
16
  if @raw
17
- parser_klass.read IO.read(@raw)
17
+ @result = parser_klass.read(IO.read(@raw))
18
18
  elsif @pcap
19
19
  File.open(@pcap) do |file|
20
20
  pcap = Pio::Pcap::Frame.read(file)
@@ -28,3 +28,12 @@ end
28
28
  Then(/^it should finish successfully$/) do
29
29
  # Noop.
30
30
  end
31
+
32
+ Then(/^the parsed data have the following field and value:$/) do |table|
33
+ table.hashes.each do |each|
34
+ output = each['field'].split('.').inject(@result) do |memo, method|
35
+ memo.__send__(method)
36
+ end
37
+ expect(output.to_s).to eq(each['value'])
38
+ end
39
+ end
data/lib/pio.rb CHANGED
@@ -4,6 +4,7 @@ require 'pio/arp'
4
4
  require 'pio/dhcp'
5
5
  require 'pio/echo'
6
6
  require 'pio/features'
7
+ require 'pio/flow_mod'
7
8
  require 'pio/hello'
8
9
  require 'pio/icmp'
9
10
  require 'pio/lldp'
data/lib/pio/echo.rb CHANGED
@@ -1,14 +1,16 @@
1
- require 'pio/echo/reply'
2
- require 'pio/echo/request'
3
1
  require 'pio/open_flow'
4
- require 'pio/open_flow/parser'
5
2
 
6
3
  module Pio
7
- # OpenFlow 1.0 Echo Request and Reply message parser.
8
- class Echo
9
- KLASS = { Pio::OpenFlow::Type::ECHO_REQUEST => Pio::Echo::Request,
10
- Pio::OpenFlow::Type::ECHO_REPLY => Pio::Echo::Reply }
4
+ # OpenFlow 1.0 Echo Request and Reply message.
5
+ module Echo
6
+ # @!parse
7
+ # # OpenFlow 1.0 Echo Request message.
8
+ # class Request < OpenFlow::Message; end
9
+ class Request < OpenFlow::Message.factory(OpenFlow::Type::ECHO_REQUEST); end
11
10
 
12
- extend Pio::OpenFlow::Parser
11
+ # @!parse
12
+ # # OpenFlow 1.0 Echo Reply message.
13
+ # class Reply < OpenFlow::Message; end
14
+ class Reply < OpenFlow::Message.factory(OpenFlow::Type::ECHO_REPLY); end
13
15
  end
14
16
  end
data/lib/pio/enqueue.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  require 'bindata'
2
2
  require 'forwardable'
3
3
  require 'pio/monkey_patch/integer'
4
- require 'pio/open_flow'
4
+ require 'pio/open_flow/port_number'
5
5
 
6
6
  module Pio
7
7
  # An action to enqueue the packet on the specified queue attached to
data/lib/pio/features.rb CHANGED
@@ -1,14 +1,71 @@
1
- require 'pio/features/reply'
2
- require 'pio/features/request'
3
1
  require 'pio/open_flow'
4
- require 'pio/open_flow/parser'
5
2
 
6
3
  module Pio
7
- # OpenFlow 1.0 Features Request and Reply message parser.
4
+ # OpenFlow 1.0 Features Request and Reply message.
8
5
  class Features
9
- KLASS = { Pio::OpenFlow::Type::FEATURES_REQUEST => Pio::Features::Request,
10
- Pio::OpenFlow::Type::FEATURES_REPLY => Pio::Features::Reply }
6
+ # OpenFlow 1.0 Features Request message.
7
+ class Request < OpenFlow::Message.factory(OpenFlow::Type::FEATURES_REQUEST)
8
+ end
11
9
 
12
- extend Pio::OpenFlow::Parser
10
+ # OpenFlow 1.0 Features Reply message
11
+ class Reply < OpenFlow::Message.factory(OpenFlow::Type::FEATURES_REPLY)
12
+ # Message body of features reply.
13
+ class ReplyBody < BinData::Record
14
+ extend Flags
15
+
16
+ # enum ofp_capabilities
17
+ flags_32bit :capabilities,
18
+ [:flow_stats,
19
+ :table_stats,
20
+ :port_stats,
21
+ :stp,
22
+ :reserved,
23
+ :ip_reasm,
24
+ :queue_stats,
25
+ :arp_match_ip]
26
+
27
+ # enum ofp_action_type
28
+ flags_32bit :actions_flag,
29
+ [:output,
30
+ :set_vlan_vid,
31
+ :set_vlan_pcp,
32
+ :strip_vlan,
33
+ :set_dl_src,
34
+ :set_dl_dst,
35
+ :set_nw_src,
36
+ :set_nw_dst,
37
+ :set_nw_tos,
38
+ :set_tp_src,
39
+ :set_tp_dst,
40
+ :enqueue]
41
+
42
+ endian :big
43
+
44
+ uint64 :datapath_id
45
+ uint32 :n_buffers
46
+ uint8 :n_tables
47
+ uint24 :padding
48
+ hide :padding
49
+ capabilities :capabilities
50
+ actions_flag :actions
51
+ array :ports, type: :phy_port, read_until: :eof
52
+
53
+ def empty?
54
+ false
55
+ end
56
+
57
+ def length
58
+ 24 + ports.to_binary_s.length
59
+ end
60
+ end
61
+
62
+ def_delegators :body, :datapath_id
63
+ def_delegator :body, :datapath_id, :dpid
64
+ def_delegators :body, :n_buffers
65
+ def_delegators :body, :n_tables
66
+ def_delegators :body, :capabilities
67
+ def_delegators :body, :actions
68
+ def_delegators :body, :ports
69
+ end
13
70
  end
14
71
  end
@@ -0,0 +1,86 @@
1
+ require 'pio/match'
2
+ require 'pio/open_flow'
3
+
4
+ module Pio
5
+ # OpenFlow 1.0 flow setup and teardown message.
6
+ class FlowMod < OpenFlow::Message.factory(OpenFlow::Type::FLOW_MOD)
7
+ # enum ofp_flow_mod_command
8
+ class Command < BinData::Primitive
9
+ COMMANDS = {
10
+ add: 0,
11
+ modify: 1,
12
+ modify_strict: 2,
13
+ delete: 3,
14
+ delete_strict: 4
15
+ }
16
+
17
+ endian :big
18
+ uint16 :command
19
+
20
+ def get
21
+ COMMANDS.invert.fetch(command)
22
+ end
23
+
24
+ def set(value)
25
+ self.command = COMMANDS.fetch(value)
26
+ end
27
+ end
28
+
29
+ # Message body of FlowMod.
30
+ class FlowModBody < BinData::Record
31
+ # Pio::MatchFormat wrapper.
32
+ class Match < BinData::Primitive
33
+ endian :big
34
+
35
+ string :match, read_length: 40
36
+
37
+ def set(object)
38
+ self.match = object.to_binary_s
39
+ end
40
+
41
+ def get
42
+ Pio::Match.read match
43
+ end
44
+ end
45
+
46
+ extend Flags
47
+
48
+ flags_16bit :flags,
49
+ [:send_flow_rem,
50
+ :check_overwrap,
51
+ :emerg]
52
+
53
+ endian :big
54
+
55
+ match :match
56
+ uint64 :cookie
57
+ command :command
58
+ uint16 :idle_timeout
59
+ uint16 :hard_timeout
60
+ uint16 :priority
61
+ uint32 :buffer_id
62
+ uint16 :out_port
63
+ flags :flags
64
+ actions :actions, length: -> { open_flow_header.message_length - 72 }
65
+
66
+ def empty?
67
+ false
68
+ end
69
+
70
+ def length
71
+ 64 + actions.binary.length
72
+ end
73
+ end
74
+
75
+ def_delegators :body, :match
76
+ def_delegators :body, :cookie
77
+ def_delegators :body, :command
78
+ def_delegators :body, :idle_timeout
79
+ def_delegators :body, :hard_timeout
80
+ def_delegators :body, :priority
81
+ def_delegators :body, :buffer_id
82
+ def_delegators :body, :out_port
83
+ def_delegators :body, :flags
84
+ def_delegators :body, :actions
85
+ end
86
+ end
data/lib/pio/hello.rb CHANGED
@@ -1,78 +1,8 @@
1
- require 'English'
2
- require 'bindata'
3
1
  require 'pio/open_flow'
4
- require 'pio/parse_error'
5
2
 
6
3
  module Pio
7
- # OpenFlow 1.0 Hello message
8
- class Hello < Pio::OpenFlow::Message
9
- # Message body of Hello
10
- class Body < BinData::Record
11
- endian :big
12
-
13
- string :body # ignored
14
-
15
- def length
16
- 0
17
- end
18
-
19
- def empty?
20
- true
21
- end
22
- end
23
-
24
- def_format Pio::OpenFlow::Type::HELLO
25
-
26
- # Parses +raw_data+ binary string into a Hello message object.
27
- #
28
- # @example
29
- # Pio::Hello.read("\x01\x00\x00\b\x00\x00\x00\x00")
30
- # @return [Pio::Hello]
31
- def self.read(raw_data)
32
- hello = allocate
33
- hello.instance_variable_set :@format, Format.read(raw_data)
34
- hello
35
- rescue BinData::ValidityError
36
- raise Pio::ParseError, $ERROR_INFO.message
37
- end
38
-
39
- # Creates a Hello OpenFlow message.
40
- #
41
- # @overload initialize()
42
- # @example
43
- # Pio::Hello.new
44
- #
45
- # @overload initialize(transaction_id)
46
- # @example
47
- # Pio::Hello.new(123)
48
- # @param [Number] transaction_id
49
- # An unsigned 32-bit integer number associated with this
50
- # message.
51
- #
52
- # @overload initialize(user_options)
53
- # @example
54
- # Pio::Hello.new(transaction_id: 123)
55
- # Pio::Hello.new(xid: 123)
56
- # @param [Hash] user_options The options to create a message with.
57
- # @option user_options [Number] :transaction_id
58
- # @option user_options [Number] :xid An alias to transaction_id.
59
- #
60
- # @reek This method smells of :reek:FeatureEnvy
61
- # rubocop:disable MethodLength
62
- def initialize(user_options = {})
63
- options = if user_options.respond_to?(:to_i)
64
- { transaction_id: user_options.to_i }
65
- elsif user_options.respond_to?(:fetch)
66
- { transaction_id: user_options[:transaction_id] ||
67
- user_options[:xid] || 0 }
68
- else
69
- fail TypeError
70
- end
71
- if options[:transaction_id] >= 2**32
72
- fail ArgumentError, 'Transaction ID >= 2**32'
73
- end
74
- @format = Format.new(open_flow_header: options)
75
- end
76
- # rubocop:enable MethodLength
77
- end
4
+ # @!parse
5
+ # # OpenFlow 1.0 Hello message
6
+ # class Hello < OpenFlow::Message; end
7
+ class Hello < OpenFlow::Message.factory(Pio::OpenFlow::Type::HELLO); end
78
8
  end
@@ -29,7 +29,7 @@ module Pio
29
29
  when IPv4Address
30
30
  @value = addr.value
31
31
  else
32
- fail TypeError, "Invalid IPv4 address: #{ addr.inspect }"
32
+ fail TypeError, "Invalid IPv4 address: #{addr.inspect}"
33
33
  end
34
34
  end
35
35
 
data/lib/pio/match.rb ADDED
@@ -0,0 +1,167 @@
1
+ require 'English'
2
+ require 'bindata'
3
+ require 'pio/open_flow'
4
+ require 'pio/type/ip_address'
5
+ require 'pio/type/mac_address'
6
+ require 'forwardable'
7
+
8
+ module Pio
9
+ # Fields to match against flows
10
+ class Match
11
+ # Flow wildcards
12
+ class Wildcards < BinData::Primitive
13
+ BITS = {
14
+ in_port: 1 << 0,
15
+ dl_vlan: 1 << 1,
16
+ dl_src: 1 << 2,
17
+ dl_dst: 1 << 3,
18
+ dl_type: 1 << 4,
19
+ nw_proto: 1 << 5,
20
+ tp_src: 1 << 6,
21
+ tp_dst: 1 << 7,
22
+ nw_src: 0,
23
+ nw_src0: 1 << 8,
24
+ nw_src1: 1 << 9,
25
+ nw_src2: 1 << 10,
26
+ nw_src3: 1 << 11,
27
+ nw_src4: 1 << 12,
28
+ nw_src_all: 1 << 13,
29
+ nw_dst: 0,
30
+ nw_dst0: 1 << 14,
31
+ nw_dst1: 1 << 15,
32
+ nw_dst2: 1 << 16,
33
+ nw_dst3: 1 << 17,
34
+ nw_dst4: 1 << 18,
35
+ nw_dst_all: 1 << 19,
36
+ dl_vlan_pcp: 1 << 20,
37
+ nw_tos: 1 << 21
38
+ }
39
+ NW_FLAGS = [:nw_src, :nw_dst]
40
+ FLAGS = BITS.keys.select { |each| !(/^nw_(src|dst)/=~ each) }
41
+
42
+ endian :big
43
+
44
+ uint32 :flags
45
+
46
+ # This method smells of :reek:FeatureEnvy
47
+ def get
48
+ BITS.each_with_object(Hash.new(0)) do |(key, bit), memo|
49
+ next if flags & bit == 0
50
+ if /(nw_src|nw_dst)(\d)/=~ key
51
+ memo[$LAST_MATCH_INFO[1].intern] |= 1 << $LAST_MATCH_INFO[2].to_i
52
+ else
53
+ memo[key] = true
54
+ end
55
+ end
56
+ end
57
+
58
+ def set(params)
59
+ self.flags = params.inject(0) do |memo, (key, val)|
60
+ memo | case key
61
+ when :nw_src, :nw_dst
62
+ (params.fetch(key) & 31) << (key == :nw_src ? 8 : 14)
63
+ else
64
+ val ? BITS.fetch(key) : 0
65
+ end
66
+ end
67
+ end
68
+
69
+ def nw_src
70
+ get.fetch(:nw_src)
71
+ rescue KeyError
72
+ 0
73
+ end
74
+
75
+ def nw_dst
76
+ get.fetch(:nw_dst)
77
+ rescue KeyError
78
+ 0
79
+ end
80
+ end
81
+
82
+ # IP address
83
+ class MatchIpAddress < BinData::Primitive
84
+ default_parameter bitcount: 0
85
+
86
+ array :octets, type: :uint8, initial_length: 4
87
+
88
+ def set(value)
89
+ self.octets = IPv4Address.new(value).to_a
90
+ end
91
+
92
+ def get
93
+ ipaddr = octets.map { |each| format('%d', each) }.join('.')
94
+ prefixlen = 32 - eval_parameter(:bitcount)
95
+ IPv4Address.new(ipaddr + "/#{prefixlen}")
96
+ end
97
+
98
+ def ==(other)
99
+ get == other
100
+ end
101
+ end
102
+
103
+ # ofp_match format
104
+ class MatchFormat < BinData::Record
105
+ endian :big
106
+
107
+ wildcards :wildcards
108
+ uint16 :in_port
109
+ mac_address :dl_src
110
+ mac_address :dl_dst
111
+ uint16 :dl_vlan
112
+ uint8 :dl_vlan_pcp
113
+ uint8 :padding1
114
+ hide :padding1
115
+ uint16 :dl_type
116
+ uint8 :nw_tos
117
+ uint8 :nw_proto
118
+ uint16 :padding2
119
+ hide :padding2
120
+ match_ip_address :nw_src, bitcount: -> { wildcards.nw_src }
121
+ match_ip_address :nw_dst, bitcount: -> { wildcards.nw_dst }
122
+ uint16 :tp_src
123
+ uint16 :tp_dst
124
+ end
125
+
126
+ def self.read(binary)
127
+ MatchFormat.read binary
128
+ end
129
+
130
+ extend Forwardable
131
+
132
+ def_delegators :@format, :wildcards
133
+ def_delegators :@format, :in_port
134
+ def_delegators :@format, :dl_vlan
135
+ def_delegators :@format, :dl_src
136
+ def_delegators :@format, :dl_dst
137
+ def_delegators :@format, :dl_type
138
+ def_delegators :@format, :nw_proto
139
+ def_delegators :@format, :tp_src
140
+ def_delegators :@format, :tp_dst
141
+ def_delegators :@format, :nw_src
142
+ def_delegators :@format, :nw_src_all
143
+ def_delegators :@format, :nw_dst
144
+ def_delegators :@format, :nw_dst_all
145
+ def_delegators :@format, :dl_vlan_pcp
146
+ def_delegators :@format, :nw_tos
147
+ def_delegators :@format, :to_binary_s
148
+
149
+ # rubocop:disable MethodLength
150
+ # This method smells of :reek:FeatureEnvy
151
+ # This method smells of :reek:DuplicateMethodCall
152
+ def initialize(user_options)
153
+ flags = Wildcards::FLAGS.each_with_object({}) do |each, memo|
154
+ memo[each] = true unless user_options.key?(each)
155
+ end
156
+ Wildcards::NW_FLAGS.each_with_object(flags) do |each, memo|
157
+ if user_options.key?(each)
158
+ memo[each] = 32 - IPv4Address.new(user_options[each]).prefixlen
159
+ else
160
+ memo["#{each}_all".intern] = true
161
+ end
162
+ end
163
+ @format = MatchFormat.new({ wildcards: flags }.merge user_options)
164
+ end
165
+ # rubocop:enable MethodLength
166
+ end
167
+ end