construqt 0.0.1

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 (48) hide show
  1. checksums.yaml +7 -0
  2. data/lib/construqt/addresses.rb +204 -0
  3. data/lib/construqt/bgps.rb +164 -0
  4. data/lib/construqt/cables.rb +47 -0
  5. data/lib/construqt/firewalls.rb +247 -0
  6. data/lib/construqt/flavour/ciscian/ciscian.rb +687 -0
  7. data/lib/construqt/flavour/ciscian/dialect_dlink-dgs15xx.rb +235 -0
  8. data/lib/construqt/flavour/ciscian/dialect_hp-2510g.rb +114 -0
  9. data/lib/construqt/flavour/delegates.rb +448 -0
  10. data/lib/construqt/flavour/flavour.rb +97 -0
  11. data/lib/construqt/flavour/mikrotik/flavour_mikrotik.rb +417 -0
  12. data/lib/construqt/flavour/mikrotik/flavour_mikrotik_bgp.rb +134 -0
  13. data/lib/construqt/flavour/mikrotik/flavour_mikrotik_interface.rb +79 -0
  14. data/lib/construqt/flavour/mikrotik/flavour_mikrotik_ipsec.rb +65 -0
  15. data/lib/construqt/flavour/mikrotik/flavour_mikrotik_result.rb +182 -0
  16. data/lib/construqt/flavour/mikrotik/flavour_mikrotik_schema.rb +355 -0
  17. data/lib/construqt/flavour/plantuml/plantuml.rb +462 -0
  18. data/lib/construqt/flavour/ubuntu/flavour_ubuntu.rb +381 -0
  19. data/lib/construqt/flavour/ubuntu/flavour_ubuntu_bgp.rb +117 -0
  20. data/lib/construqt/flavour/ubuntu/flavour_ubuntu_dns.rb +97 -0
  21. data/lib/construqt/flavour/ubuntu/flavour_ubuntu_firewall.rb +300 -0
  22. data/lib/construqt/flavour/ubuntu/flavour_ubuntu_ipsec.rb +144 -0
  23. data/lib/construqt/flavour/ubuntu/flavour_ubuntu_opvn.rb +60 -0
  24. data/lib/construqt/flavour/ubuntu/flavour_ubuntu_result.rb +537 -0
  25. data/lib/construqt/flavour/ubuntu/flavour_ubuntu_services.rb +115 -0
  26. data/lib/construqt/flavour/ubuntu/flavour_ubuntu_vrrp.rb +52 -0
  27. data/lib/construqt/flavour/unknown/unknown.rb +175 -0
  28. data/lib/construqt/hostid.rb +42 -0
  29. data/lib/construqt/hosts.rb +98 -0
  30. data/lib/construqt/interfaces.rb +166 -0
  31. data/lib/construqt/ipsecs.rb +64 -0
  32. data/lib/construqt/networks.rb +81 -0
  33. data/lib/construqt/regions.rb +32 -0
  34. data/lib/construqt/resource.rb +42 -0
  35. data/lib/construqt/services.rb +53 -0
  36. data/lib/construqt/tags.rb +61 -0
  37. data/lib/construqt/templates.rb +37 -0
  38. data/lib/construqt/tests/test_addresses.rb +50 -0
  39. data/lib/construqt/tests/test_bgps.rb +24 -0
  40. data/lib/construqt/tests/test_hostid.rb +32 -0
  41. data/lib/construqt/tests/test_hosts.rb +23 -0
  42. data/lib/construqt/tests/test_utils.rb +76 -0
  43. data/lib/construqt/users.rb +19 -0
  44. data/lib/construqt/util.rb +163 -0
  45. data/lib/construqt/version.rb +3 -0
  46. data/lib/construqt/vlans.rb +51 -0
  47. data/lib/construqt.rb +92 -0
  48. metadata +105 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 2ec8fbada65d807c3b03a0659270b3574e274fce
4
+ data.tar.gz: 021691f9b160814aa6cd08580b0888337f1d8160
5
+ SHA512:
6
+ metadata.gz: cd0256802fca445d8739ba16c613a936664144cb19162ad0c09bc4b91b0e8b27b3522d304647d2b23283812c3b2843db1ee22dd376175c48310bc57eea78c1f2
7
+ data.tar.gz: e46c4967d8df48ed987ccf52b553ba2ecfb1ba4ff2c988947798f44a8ae2603c747c99bcc764776950dbefaa695034671994dbe0b1c3652b66800664986c77e9
@@ -0,0 +1,204 @@
1
+
2
+ module Construqt
3
+ class Addresses
4
+
5
+ UNREACHABLE = :unreachable
6
+ LOOOPBACK = :looopback
7
+ DHCPV4 = :dhcpv4
8
+ DHCPV6 = :dhcpv6
9
+ IPV4 = :ipv4
10
+ IPV6 = :ipv6
11
+
12
+ def initialize(network)
13
+ @network = network
14
+ @Addresses = []
15
+ end
16
+
17
+ def network
18
+ @network
19
+ end
20
+
21
+ class Address
22
+ attr_accessor :host
23
+ attr_accessor :interface
24
+ attr_accessor :ips
25
+ attr_accessor :tags
26
+ def dhcpv4?
27
+ @dhcpv4
28
+ end
29
+
30
+ def dhcpv6?
31
+ @dhcpv6
32
+ end
33
+
34
+ def loopback?
35
+ @loopback
36
+ end
37
+
38
+ def initialize()
39
+ self.ips = []
40
+ self.host = nil
41
+ self.interface = nil
42
+ self.routes = []
43
+ self.tags = []
44
+ @loopback = @dhcpv4 = @dhcpv6 = false
45
+ @name = nil
46
+ end
47
+
48
+ def match_network(ip)
49
+ if ip.ipv4?
50
+ self.v4s.find{|nip| nip.include?(ip) }
51
+ else
52
+ self.v6s.find{|nip| nip.include?(ip) }
53
+ end
54
+ end
55
+
56
+ def v6s
57
+ self.ips.select{|ip| ip.ipv6? }
58
+ end
59
+
60
+ def v4s
61
+ self.ips.select{|ip| ip.ipv4? }
62
+ end
63
+
64
+ def first_ipv4
65
+ v4s.first
66
+ end
67
+
68
+ def first_ipv6
69
+ v6s.first
70
+ end
71
+
72
+ def merge_tag(name, &block)
73
+ Construqt::Tags.add(([name]+self.tags).join("#")) { |name| block.call(name) }
74
+ end
75
+
76
+ def tag(tag)
77
+ self.tags << tag
78
+ self
79
+ end
80
+
81
+ def set_name(xname)
82
+ (@name, obj) = self.merge_tag(xname) { |xname| self }
83
+ self
84
+ end
85
+
86
+ def name=(name)
87
+ set_name(name)
88
+ end
89
+
90
+ def name
91
+ ret = self.name!
92
+ throw "unreferenced address [#{self.ips.map{|i| i.to_string }}]" unless ret
93
+ ret
94
+ end
95
+
96
+ def name!
97
+ return @name if @name
98
+ return "#{self.interface.name}-#{self.interface.host.name}" if self.interface
99
+ return self.host.name if self.host
100
+ nil
101
+ end
102
+
103
+ def add_ip(ip, region = "")
104
+ throw "please give a ip #{ip}" unless ip
105
+ if ip
106
+ #puts ">>>>> #{ip} #{ip.class.name}"
107
+ if DHCPV4 == ip
108
+ @dhcpv4 = true
109
+ elsif DHCPV6 == ip
110
+ @dhcpv6 = true
111
+ elsif LOOOPBACK == ip
112
+ @loopback = true
113
+ else
114
+ (unused, ip) = self.merge_tag(ip) { |ip| IPAddress.parse(ip) }
115
+ self.ips << ip
116
+ end
117
+ end
118
+
119
+ self
120
+ end
121
+
122
+ # @nameservers = []
123
+ # def add_nameserver(ip)
124
+ # @nameservers << IPAddress.parse(ip)
125
+ # self
126
+ # end
127
+
128
+ attr_accessor :routes
129
+ def add_routes(addr_s, via, options = {})
130
+ addrs = addr_s.kind_of?(Array) ? addr_s : [addr_s]
131
+ addrs.each{|addr| addr.ips.each {|i| add_route(i.to_string, via, options) } }
132
+ self
133
+ end
134
+
135
+ def add_route(dst, via, option = {})
136
+ #puts "DST => "+dst.class.name+":"+dst.to_s
137
+ (unused, dst) = self.merge_tag(dst) { |dst| IPAddress.parse(dst) }
138
+ metric = option['metric']
139
+ if via == UNREACHABLE
140
+ via = nil
141
+ type = 'unreachable'
142
+ else
143
+ if via.nil?
144
+ via = nil
145
+ else
146
+ via = IPAddress.parse(via)
147
+ throw "different type #{dst} #{via}" unless dst.ipv4? == via.ipv4? && dst.ipv6? == via.ipv6?
148
+ end
149
+
150
+ type = nil
151
+ end
152
+
153
+ self.routes << OpenStruct.new("dst" => dst, "via" => via, "type" => type, "metric" => metric)
154
+ self
155
+ end
156
+
157
+ def to_s
158
+ "<Address:Address #{@name}=>#{self.ips.map{|i| i.to_s}.join(":")}>"
159
+ end
160
+ end
161
+
162
+ def create
163
+ ret = Address.new()
164
+ @Addresses << ret
165
+ ret
166
+ end
167
+
168
+ def tag(tag)
169
+ create.tag(tag)
170
+ end
171
+
172
+ def add_ip(ip, region = "")
173
+ create.add_ip(ip, region)
174
+ end
175
+
176
+ def add_route(dest, via = nil)
177
+ create.add_route(dest, via)
178
+ end
179
+
180
+ def set_name(name)
181
+ create.set_name(name)
182
+ end
183
+
184
+ def all
185
+ @Addresses
186
+ end
187
+
188
+ def v4_default_route(tag = "")
189
+ nets = [(1..9),(11..126),(128..168),(170..171),(173..191),(193..223)].map do |range|
190
+ range.to_a.map{|i| "#{i}.0.0.0/8"}
191
+ end.flatten
192
+ nets += (0..255).to_a.select{|i| i!=254}.map{|i| "169.#{i}.0.0/16" }
193
+ nets += (0..255).to_a.select{|i| !(16<=i&&i<31)}.map{|i| "172.#{i}.0.0/16" }
194
+ nets += (0..255).to_a.select{|i| i!=168}.map{|i| "192.#{i}.0.0/16" }
195
+
196
+ v4_default_route = self.create
197
+ v4_default_route.set_name(tag).tag(tag) if tag && !tag.empty?
198
+ IPAddress::IPv4::summarize(*(nets.map{|i| IPAddress::IPv4.new(i) })).each do |i|
199
+ v4_default_route.add_ip(i.to_string)
200
+ end
201
+ v4_default_route
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,164 @@
1
+ module Construqt
2
+ module Bgps
3
+ class Bgp < OpenStruct
4
+ def initialize(cfg)
5
+ super(cfg)
6
+ end
7
+
8
+ def build_config()
9
+ self.left.build_config(nil, nil)
10
+ self.right.build_config(nil, nil)
11
+ end
12
+
13
+ def ident
14
+ self.left.ident
15
+ end
16
+ end
17
+
18
+ @bgps = {}
19
+ def self.connections
20
+ @bgps.values
21
+ end
22
+
23
+ def self.add_connection(cfg, id)
24
+ throw "my not found #{cfg[id]['my'].inspect}" unless cfg[id]['my']
25
+ throw "as not found #{cfg[id]['as'].inspect}" unless cfg[id]['as']
26
+ throw "as not a as #{cfg[id]['as'].inspect}" unless cfg[id]['as'].kind_of?(As)
27
+ #throw "filter not found #{cfg.inspect}" unless cfg[id]['filter']
28
+ cfg[id]['filter'] ||= {}
29
+ cfg[id]['other'] = nil
30
+ cfg[id]['cfg'] = nil
31
+ cfg[id]['host'] = cfg[id]['my'].host
32
+ cfg[id] = cfg[id]['host'].flavour.create_bgp(cfg[id])
33
+ end
34
+
35
+ def self.connection(name, cfg)
36
+ throw "filter not allowed" if cfg['filter']
37
+ throw "duplicated name #{name}" if @bgps[name]
38
+ add_connection(cfg, 'left')
39
+ add_connection(cfg, 'right')
40
+ cfg['name'] = name
41
+
42
+ cfg = @bgps[name] = Bgp.new(cfg)
43
+ cfg.left.other = cfg.right
44
+ cfg.left.cfg = cfg
45
+ cfg.right.other = cfg.left
46
+ cfg.right.cfg = cfg
47
+ cfg
48
+ end
49
+
50
+ def self.build_config()
51
+ #binding.pry
52
+ hosts = {}
53
+ @bgps.each do |name, bgp|
54
+ bgp.build_config()
55
+ hosts[bgp.left.host.name] = bgp.left
56
+ hosts[bgp.right.host.name] = bgp.right
57
+ end
58
+
59
+ #hosts.values.each do |flavour_bgp|
60
+ # flavour_bgp.header(flavour_bgp.host)
61
+ # flavour_bgp.footer(flavour_bgp.host)
62
+ #end
63
+ end
64
+
65
+ @filters = {}
66
+
67
+ class Filter
68
+ def initialize(name)
69
+ @name = name
70
+ @list = []
71
+ end
72
+
73
+ def list
74
+ @list
75
+ end
76
+
77
+ def name
78
+ @name
79
+ end
80
+
81
+ def addr_v_(cfg)
82
+ [OpenStruct.new({:code=>4, :is? => lambda {|i| i.ipv4? }, :max_prefix=>32}),
83
+ OpenStruct.new({:code=>6, :is? => lambda {|i| i.ipv6? }, :max_prefix=>128})].each do |family|
84
+ addr = cfg["addr_v#{family.code}"]
85
+ next unless addr
86
+ cfg.delete("addr_v#{family.code}")
87
+ addr_sub_prefix = cfg['addr_sub_prefix']
88
+ cfg.delete('addr_sub_prefix')
89
+ #puts addr.inspect
90
+ (addr.kind_of?(Construqt::Addresses::Address) ? [addr] : addr).each do |addr|
91
+ addr.ips.each do |net|
92
+ next unless family.is?.call(net)
93
+ network = Construqt::Addresses::Address.new
94
+ network.add_ip(net.to_string)
95
+ cfg = { 'network' => network }.merge(cfg)
96
+ cfg['prefix_length'] = [net.prefix,family.max_prefix] if addr_sub_prefix
97
+ @list << cfg
98
+ end
99
+ end
100
+
101
+ nil
102
+ end
103
+ end
104
+
105
+ def accept(cfg)
106
+ cfg = {}.merge(cfg)
107
+ cfg['rule'] = 'accept'
108
+ addr_v_(cfg)
109
+ @list << cfg if cfg['network']
110
+ end
111
+
112
+ def reject(cfg)
113
+ cfg = {}.merge(cfg)
114
+ cfg['rule'] = 'reject'
115
+ addr_v_(cfg)
116
+ @list << cfg if cfg['network']
117
+ end
118
+ end
119
+
120
+ class As < OpenStruct
121
+ def initialize(cfg)
122
+ super(cfg)
123
+ end
124
+
125
+ def name
126
+ (self.prefix || "AS") + self.as.to_s
127
+ end
128
+
129
+ def num
130
+ self.as
131
+ end
132
+ end
133
+
134
+ @as = {}
135
+ def self.add_as(as, config)
136
+ throw "as must be a number #{as}" unless as.kind_of?(Fixnum)
137
+ throw "as defined before #{as}" if @as[as]
138
+ config['as'] = as
139
+ @as[as] = As.new(config)
140
+ end
141
+
142
+ def self.find_as(as)
143
+ ret = @as[as]
144
+ throw "as not found #{as}" unless ret
145
+ ret
146
+ end
147
+
148
+ def self.add_filter(name, &block)
149
+ @filters[name] = Filter.new(name)
150
+ block.call(@filters[name])
151
+ @filters[name]
152
+ end
153
+
154
+ def self.filters
155
+ @filters.values
156
+ end
157
+
158
+ def self.find_filter(name)
159
+ ret = @filters[name]
160
+ throw "bgp not filter with name #{name}" unless ret
161
+ ret
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,47 @@
1
+
2
+ module Construqt
3
+
4
+ class Cables
5
+
6
+ def initialize(region)
7
+ @region = region
8
+ @cables = {}
9
+ end
10
+
11
+ def region
12
+ @region
13
+ end
14
+
15
+ class Cable
16
+ attr_reader :left, :right
17
+ def initialize(left, right)
18
+ @left = left
19
+ @right = right
20
+ end
21
+
22
+ def key
23
+ [left.name, right.name].sort.join("<=>")
24
+ end
25
+ end
26
+
27
+ class DirectedCable
28
+ attr_accessor :cable, :other
29
+ def initialize(cable, other)
30
+ self.cable = cable
31
+ self.other = other
32
+ end
33
+ end
34
+
35
+ def add(iface_left, iface_right)
36
+ # throw "left should be a iface #{iface_left.class.name}" unless iface_left.kind_of?(Construqt::Flavour::InterfaceDelegate)
37
+ # throw "right should be a iface #{iface_right.class.name}" unless iface_right.kind_of?(Construqt::Flavour::InterfaceDelegate)
38
+ throw "left has a cable #{iface_left.cable}" if iface_left.cable
39
+ throw "right has a cable #{iface_right.cable}" if iface_right.cable
40
+ cable = Cable.new(iface_left, iface_right)
41
+ throw "cable exists #{iface_left.cable}=#{iface_right.cable}" if @cables[cable.key]
42
+ iface_left.cable = DirectedCable.new(cable, iface_right)
43
+ iface_right.cable = DirectedCable.new(cable, iface_left)
44
+ cable
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,247 @@
1
+ module Construqt
2
+ module Firewalls
3
+
4
+ @firewalls = {}
5
+ module Actions
6
+ NOTRACK = :NOTRACK
7
+ SNAT = :SNAT
8
+ ACCEPT = :ACCEPT
9
+ DROP = :DROP
10
+ end
11
+
12
+ class Firewall
13
+ def initialize(name)
14
+ @name = name
15
+ @raw = Raw.new(self)
16
+ @nat = Nat.new(self)
17
+ @forward = Forward.new(self)
18
+ @host = Host.new(self)
19
+ end
20
+
21
+ def name
22
+ @name
23
+ end
24
+
25
+ class Raw
26
+ attr_reader :firewall
27
+ def initialize(firewall)
28
+ @firewall = firewall
29
+ @rules = []
30
+ end
31
+
32
+ class RawEntry
33
+ include Util::Chainable
34
+ chainable_attr :prerouting, true, false, lambda{|i| @output = false; input_only(true); output_only(false) }
35
+ chainable_attr :input_only, true
36
+ chainable_attr :output, true, false, lambda {|i| @prerouting = false; input_only(false); output_only(true) }
37
+ chainable_attr :output_only, true
38
+ chainable_attr :interface
39
+ chainable_attr :from_interface, true, false
40
+ chainable_attr_value :from_net, nil
41
+ chainable_attr_value :to, nil
42
+ chainable_attr_value :to_net, nil
43
+ chainable_attr_value :action, nil
44
+
45
+ def initialize
46
+ @from_is = nil
47
+ end
48
+
49
+ def from_is_inbound?
50
+ @from_is == :inbound
51
+ end
52
+ def from_is_outbound?
53
+ @from_is == :outbound
54
+ end
55
+ def from_is(direction)
56
+ @from_is = direction
57
+ end
58
+ end
59
+
60
+ def add
61
+ entry = RawEntry.new
62
+ @rules << entry
63
+ entry
64
+ end
65
+
66
+
67
+ def rules
68
+ @rules
69
+ end
70
+ end
71
+
72
+ def get_raw
73
+ @raw
74
+ end
75
+
76
+ def raw(&block)
77
+ block.call(@raw)
78
+ end
79
+
80
+ class Nat
81
+ attr_reader :firewall, :rules
82
+ def initialize(firewall)
83
+ @firewall = firewall
84
+ @rules = []
85
+ end
86
+
87
+ class NatEntry
88
+ include Util::Chainable
89
+ chainable_attr :prerouting, true, false, lambda{|i| @postrouting = false; input_only(true); output_only(false) }
90
+ chainable_attr :input_only
91
+ chainable_attr :postrouting, true, false, lambda{|i| @prerouting = false; input_only(false); output_only(true) }
92
+ chainable_attr :output_only
93
+ chainable_attr :to_source
94
+ chainable_attr :interface
95
+ chainable_attr :from_interface, true, false
96
+ chainable_attr_value :from_net, nil
97
+ chainable_attr_value :to_net, nil
98
+ chainable_attr_value :action, nil
99
+ end
100
+
101
+ def add
102
+ entry = NatEntry.new
103
+ @rules << entry
104
+ entry
105
+ end
106
+ end
107
+
108
+ def get_nat
109
+ @nat
110
+ end
111
+
112
+ def nat(&block)
113
+ block.call(@nat)
114
+ end
115
+
116
+ class Mangle
117
+ @rules = []
118
+ class Tcpmss
119
+ end
120
+
121
+ def tcpmss
122
+ @rules << Tcpmss.new
123
+ end
124
+ end
125
+
126
+ def mangle(&block)
127
+ block.call(@mangle)
128
+ end
129
+
130
+ class Forward
131
+ attr_reader :firewall, :rules
132
+ def initialize(firewall)
133
+ @firewall = firewall
134
+ @rules = []
135
+ end
136
+
137
+ class ForwardEntry
138
+ include Util::Chainable
139
+ chainable_attr :interface
140
+ chainable_attr :connection
141
+ chainable_attr :input_only, true, true
142
+ chainable_attr :output_only, true, true
143
+ chainable_attr :from_interface, true, false
144
+ chainable_attr :connection
145
+ chainable_attr :tcp
146
+ chainable_attr :udp
147
+ chainable_attr_value :log, nil
148
+ chainable_attr_value :from_net, nil
149
+ chainable_attr_value :to_net, nil
150
+ chainable_attr_value :action, nil
151
+
152
+ def initialize
153
+ @from_is = nil
154
+ end
155
+
156
+ def from_is_inbound?
157
+ @from_is == :inbound
158
+ end
159
+ def from_is_outbound?
160
+ @from_is == :outbound
161
+ end
162
+ def from_is(direction)
163
+ @from_is = direction
164
+ end
165
+
166
+ def port(port)
167
+ @ports ||= []
168
+ @ports << port
169
+ self
170
+ end
171
+
172
+ def get_ports
173
+ @ports ||= []
174
+ end
175
+ end
176
+
177
+ def add
178
+ entry = ForwardEntry.new
179
+ #puts "ForwardEntry: #{@firewall.name} #{entry.input_only?} #{entry.output_only?}"
180
+ @rules << entry
181
+ entry
182
+ end
183
+ end
184
+
185
+ def get_forward
186
+ @forward
187
+ end
188
+
189
+ def forward(&block)
190
+ block.call(@forward)
191
+ end
192
+
193
+ class Host
194
+ attr_reader :firewall, :rules
195
+ def initialize(firewall)
196
+ @firewall = firewall
197
+ @rules = []
198
+ end
199
+
200
+ class HostEntry < Forward::ForwardEntry
201
+ include Util::Chainable
202
+ chainable_attr :from_host
203
+ chainable_attr :to_host
204
+ end
205
+
206
+ def add
207
+ entry = HostEntry.new
208
+ #puts "ForwardEntry: #{@firewall.name} #{entry.input_only?} #{entry.output_only?}"
209
+ @rules << entry
210
+ entry
211
+ end
212
+ end
213
+
214
+ def get_host
215
+ @host
216
+ end
217
+
218
+ def host(&block)
219
+ block.call(@host)
220
+ end
221
+
222
+ # class Input
223
+ # class All
224
+ # end
225
+
226
+ # @rules = []
227
+ # def all(cfg)
228
+ # @rules << All.new(cfg)
229
+ # end
230
+
231
+ # end
232
+ end
233
+
234
+ def self.add(name, &block)
235
+ throw "firewall with this name exists #{name}" if @firewalls[name]
236
+ fw = @firewalls[name] = Firewall.new(name)
237
+ block.call(fw)
238
+ fw
239
+ end
240
+
241
+ def self.find(name)
242
+ ret = @firewalls[name]
243
+ throw "firewall with this name #{name} not found" unless @firewalls[name]
244
+ ret
245
+ end
246
+ end
247
+ end