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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +33 -2
- data/examples/echo_read.rb +1 -1
- data/examples/features_read.rb +1 -1
- data/examples/flow_mod_new.rb +13 -0
- data/examples/flow_mod_read.rb +6 -0
- data/features/echo_read.feature +27 -3
- data/features/features_read.feature +46 -2
- data/features/flow_mod_read.feature +186 -0
- data/features/hello_read.feature +9 -0
- data/features/packet_data/flow_mod_add.raw +0 -0
- data/features/packet_data/flow_mod_delete.raw +0 -0
- data/features/packet_data/flow_mod_delete_strict.raw +0 -0
- data/features/packet_data/flow_mod_modify.raw +0 -0
- data/features/packet_data/flow_mod_modify_strict.raw +0 -0
- data/features/packet_in_read.feature +13 -0
- data/features/packet_out_read.feature +16 -0
- data/features/step_definitions/packet_data_steps.rb +10 -1
- data/lib/pio.rb +1 -0
- data/lib/pio/echo.rb +10 -8
- data/lib/pio/enqueue.rb +1 -1
- data/lib/pio/features.rb +64 -7
- data/lib/pio/flow_mod.rb +86 -0
- data/lib/pio/hello.rb +4 -74
- data/lib/pio/ipv4_address.rb +1 -1
- data/lib/pio/match.rb +167 -0
- data/lib/pio/open_flow.rb +1 -0
- data/lib/pio/open_flow/actions.rb +65 -0
- data/lib/pio/open_flow/flags.rb +12 -9
- data/lib/pio/open_flow/message.rb +105 -21
- data/lib/pio/open_flow/phy_port.rb +31 -27
- data/lib/pio/open_flow/type.rb +1 -0
- data/lib/pio/packet_in.rb +2 -12
- data/lib/pio/packet_out.rb +4 -74
- data/lib/pio/send_out_port.rb +1 -0
- data/lib/pio/type/ip_address.rb +2 -7
- data/lib/pio/version.rb +1 -1
- data/pio.gemspec +8 -8
- data/spec/pio/echo/reply_spec.rb +61 -6
- data/spec/pio/echo/request_spec.rb +61 -6
- data/spec/pio/features/reply_spec.rb +81 -4
- data/spec/pio/features/request_spec.rb +88 -41
- data/spec/pio/flow_mod_spec.rb +151 -0
- data/spec/pio/hello_spec.rb +73 -56
- data/spec/pio/match_spec.rb +194 -0
- data/spec/pio/packet_in_spec.rb +35 -38
- data/spec/pio/packet_out_spec.rb +243 -182
- data/spec/pio/wildcards_spec.rb +115 -0
- metadata +37 -30
- data/features/packet_data/echo.raw +0 -0
- data/lib/pio/echo/message.rb +0 -49
- data/lib/pio/echo/reply.rb +0 -41
- data/lib/pio/echo/request.rb +0 -43
- data/lib/pio/features/reply.rb +0 -88
- data/lib/pio/features/request.rb +0 -68
- data/lib/pio/open_flow/parser.rb +0 -22
- data/spec/pio/echo_spec.rb +0 -49
- 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
|
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
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
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
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
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
|
4
|
+
# OpenFlow 1.0 Features Request and Reply message.
|
8
5
|
class Features
|
9
|
-
|
10
|
-
|
6
|
+
# OpenFlow 1.0 Features Request message.
|
7
|
+
class Request < OpenFlow::Message.factory(OpenFlow::Type::FEATURES_REQUEST)
|
8
|
+
end
|
11
9
|
|
12
|
-
|
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
|
data/lib/pio/flow_mod.rb
ADDED
@@ -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
|
-
#
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
data/lib/pio/ipv4_address.rb
CHANGED
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
|