knife-azure 1.8.7 → 1.9.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/lib/azure/azure_interface.rb +79 -81
- data/lib/azure/custom_errors.rb +34 -35
- data/lib/azure/helpers.rb +43 -44
- data/lib/azure/resource_management/ARM_deployment_template.rb +679 -678
- data/lib/azure/resource_management/ARM_interface.rb +513 -515
- data/lib/azure/resource_management/vnet_config.rb +43 -43
- data/lib/azure/resource_management/windows_credentials.rb +181 -184
- data/lib/azure/service_management/ASM_interface.rb +309 -317
- data/lib/azure/service_management/ag.rb +16 -16
- data/lib/azure/service_management/certificate.rb +30 -31
- data/lib/azure/service_management/connection.rb +31 -31
- data/lib/azure/service_management/deploy.rb +40 -38
- data/lib/azure/service_management/disk.rb +14 -10
- data/lib/azure/service_management/host.rb +28 -24
- data/lib/azure/service_management/image.rb +23 -22
- data/lib/azure/service_management/loadbalancer.rb +12 -12
- data/lib/azure/service_management/rest.rb +20 -19
- data/lib/azure/service_management/role.rb +274 -273
- data/lib/azure/service_management/storageaccount.rb +29 -25
- data/lib/azure/service_management/utility.rb +6 -7
- data/lib/azure/service_management/vnet.rb +44 -44
- data/lib/chef/knife/azure_ag_create.rb +18 -18
- data/lib/chef/knife/azure_ag_list.rb +3 -3
- data/lib/chef/knife/azure_base.rb +56 -56
- data/lib/chef/knife/azure_image_list.rb +8 -10
- data/lib/chef/knife/azure_internal-lb_create.rb +15 -15
- data/lib/chef/knife/azure_internal-lb_list.rb +3 -3
- data/lib/chef/knife/azure_server_create.rb +49 -50
- data/lib/chef/knife/azure_server_delete.rb +22 -24
- data/lib/chef/knife/azure_server_list.rb +4 -4
- data/lib/chef/knife/azure_server_show.rb +5 -5
- data/lib/chef/knife/azure_vnet_create.rb +17 -17
- data/lib/chef/knife/azure_vnet_list.rb +3 -3
- data/lib/chef/knife/azurerm_base.rb +58 -60
- data/lib/chef/knife/azurerm_server_create.rb +23 -22
- data/lib/chef/knife/azurerm_server_delete.rb +30 -34
- data/lib/chef/knife/azurerm_server_list.rb +42 -42
- data/lib/chef/knife/azurerm_server_show.rb +1 -1
- data/lib/chef/knife/bootstrap/bootstrap_options.rb +7 -8
- data/lib/chef/knife/bootstrap/bootstrapper.rb +65 -65
- data/lib/chef/knife/bootstrap/common_bootstrap_options.rb +3 -4
- data/lib/chef/knife/bootstrap_azure.rb +13 -13
- data/lib/chef/knife/bootstrap_azurerm.rb +106 -106
- data/lib/knife-azure/version.rb +2 -2
- metadata +43 -76
- data/lib/azure/resource_management/ARM_base.rb +0 -29
@@ -1,317 +1,309 @@
|
|
1
|
-
#
|
2
|
-
#
|
3
|
-
#
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
|
18
|
-
|
19
|
-
require
|
20
|
-
require
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
@
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
rows << server.
|
46
|
-
rows <<
|
47
|
-
|
48
|
-
state
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
rows << server.
|
59
|
-
rows << server.
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
msg_pair(ui,
|
96
|
-
msg_pair(ui,
|
97
|
-
msg_pair(ui,
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
details <<
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
end
|
194
|
-
|
195
|
-
def
|
196
|
-
connection.
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
end
|
208
|
-
|
209
|
-
def
|
210
|
-
connection.
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
end
|
222
|
-
|
223
|
-
def
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
if
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
params[:azure_storage_account] =
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
if
|
267
|
-
ret_val = connection.
|
268
|
-
ret_val.content.empty? ? Chef::Log.warn("Deleted created
|
269
|
-
end
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
connection.
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
backtrace_message = "#{e.class}: #{e}\n#{e.backtrace.join("\n")}"
|
311
|
-
Chef::Log.debug("#{backtrace_message}")
|
312
|
-
end
|
313
|
-
end
|
314
|
-
end
|
315
|
-
end
|
316
|
-
end
|
317
|
-
|
1
|
+
#
|
2
|
+
# Copyright:: Copyright 2016-2018 Chef Software, Inc.
|
3
|
+
# License:: Apache License, Version 2.0
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
#
|
17
|
+
|
18
|
+
require "azure/azure_interface"
|
19
|
+
require "azure/service_management/rest"
|
20
|
+
require "azure/service_management/connection"
|
21
|
+
|
22
|
+
module Azure
|
23
|
+
class ServiceManagement
|
24
|
+
class ASMInterface < AzureInterface
|
25
|
+
include AzureAPI
|
26
|
+
|
27
|
+
attr_accessor :connection
|
28
|
+
|
29
|
+
def initialize(params = {})
|
30
|
+
@rest = Rest.new(params)
|
31
|
+
@connection = Azure::ServiceManagement::Connection.new(@rest)
|
32
|
+
super
|
33
|
+
end
|
34
|
+
|
35
|
+
def list_images
|
36
|
+
connection.images.all
|
37
|
+
end
|
38
|
+
|
39
|
+
def list_servers
|
40
|
+
servers = connection.roles.all
|
41
|
+
cols = ["DNS Name", "VM Name", "Status", "IP Address", "SSH Port", "WinRM Port", "RDP Port"]
|
42
|
+
rows = []
|
43
|
+
servers.each do |server|
|
44
|
+
rows << server.hostedservicename.to_s + ".cloudapp.net" # Info about the DNS name at http://msdn.microsoft.com/en-us/library/ee460806.aspx
|
45
|
+
rows << server.name.to_s
|
46
|
+
rows << begin
|
47
|
+
state = server.status.to_s.downcase
|
48
|
+
case state
|
49
|
+
when "shutting-down", "terminated", "stopping", "stopped"
|
50
|
+
ui.color(state, :red)
|
51
|
+
when "pending"
|
52
|
+
ui.color(state, :yellow)
|
53
|
+
else
|
54
|
+
ui.color("ready", :green)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
rows << server.publicipaddress.to_s
|
58
|
+
rows << server.sshport.to_s
|
59
|
+
rows << server.winrmport.to_s
|
60
|
+
ports = server.tcpports
|
61
|
+
rows << rdp_port(ports)
|
62
|
+
end
|
63
|
+
display_list(ui, cols, rows)
|
64
|
+
end
|
65
|
+
|
66
|
+
def rdp_port(arr_ports)
|
67
|
+
if !arr_ports
|
68
|
+
return ""
|
69
|
+
end
|
70
|
+
if arr_ports.length > 0
|
71
|
+
arr_ports.each do |port|
|
72
|
+
if port["Name"] == "Remote Desktop"
|
73
|
+
return port["PublicPort"]
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
""
|
78
|
+
end
|
79
|
+
|
80
|
+
def find_server(params = {})
|
81
|
+
server = connection.roles.find(params[:name], params = { :azure_dns_name => params[:azure_dns_name] })
|
82
|
+
end
|
83
|
+
|
84
|
+
def delete_server(params = {})
|
85
|
+
server = find_server({ name: params[:name], azure_dns_name: params[:azure_dns_name] })
|
86
|
+
|
87
|
+
unless server
|
88
|
+
puts "\n"
|
89
|
+
ui.error("Server #{params[:name]} does not exist")
|
90
|
+
exit!
|
91
|
+
end
|
92
|
+
|
93
|
+
puts "\n"
|
94
|
+
msg_pair(ui, "DNS Name", server.hostedservicename + ".cloudapp.net")
|
95
|
+
msg_pair(ui, "VM Name", server.name)
|
96
|
+
msg_pair(ui, "Size", server.size)
|
97
|
+
msg_pair(ui, "Public Ip Address", server.publicipaddress)
|
98
|
+
puts "\n"
|
99
|
+
|
100
|
+
begin
|
101
|
+
ui.confirm("Do you really want to delete this server")
|
102
|
+
rescue SystemExit # Need to handle this as confirming with N/n raises SystemExit exception
|
103
|
+
server = nil # Cleanup is implicitly performed in other cloud plugins
|
104
|
+
exit!
|
105
|
+
end
|
106
|
+
|
107
|
+
params[:azure_dns_name] = server.hostedservicename
|
108
|
+
|
109
|
+
connection.roles.delete(params)
|
110
|
+
|
111
|
+
puts "\n"
|
112
|
+
ui.warn("Deleted server #{server.name}")
|
113
|
+
end
|
114
|
+
|
115
|
+
def show_server(name)
|
116
|
+
role = connection.roles.find name
|
117
|
+
|
118
|
+
puts ""
|
119
|
+
if role
|
120
|
+
details = Array.new
|
121
|
+
details << ui.color("Role name", :bold, :cyan)
|
122
|
+
details << role.name
|
123
|
+
details << ui.color("Status", :bold, :cyan)
|
124
|
+
details << role.status
|
125
|
+
details << ui.color("Size", :bold, :cyan)
|
126
|
+
details << role.size
|
127
|
+
details << ui.color("Hosted service name", :bold, :cyan)
|
128
|
+
details << role.hostedservicename
|
129
|
+
details << ui.color("Deployment name", :bold, :cyan)
|
130
|
+
details << role.deployname
|
131
|
+
details << ui.color("Host name", :bold, :cyan)
|
132
|
+
details << role.hostname
|
133
|
+
unless role.sshport.nil?
|
134
|
+
details << ui.color("SSH port", :bold, :cyan)
|
135
|
+
details << role.sshport
|
136
|
+
end
|
137
|
+
unless role.winrmport.nil?
|
138
|
+
details << ui.color("WinRM port", :bold, :cyan)
|
139
|
+
details << role.winrmport
|
140
|
+
end
|
141
|
+
details << ui.color("Public IP", :bold, :cyan)
|
142
|
+
details << role.publicipaddress.to_s
|
143
|
+
|
144
|
+
unless role.thumbprint.empty?
|
145
|
+
details << ui.color("Thumbprint", :bold, :cyan)
|
146
|
+
details << role.thumbprint
|
147
|
+
end
|
148
|
+
puts ui.list(details, :columns_across, 2)
|
149
|
+
if role.tcpports.length > 0 || role.udpports.length > 0
|
150
|
+
details.clear
|
151
|
+
details << ui.color("Ports open", :bold, :cyan)
|
152
|
+
details << ui.color("Local port", :bold, :cyan)
|
153
|
+
details << ui.color("IP", :bold, :cyan)
|
154
|
+
details << ui.color("Public port", :bold, :cyan)
|
155
|
+
if role.tcpports.length > 0
|
156
|
+
role.tcpports.each do |port|
|
157
|
+
details << "tcp"
|
158
|
+
details << port["LocalPort"]
|
159
|
+
details << port["Vip"]
|
160
|
+
details << port["PublicPort"]
|
161
|
+
end
|
162
|
+
end
|
163
|
+
if role.udpports.length > 0
|
164
|
+
role.udpports.each do |port|
|
165
|
+
details << "udp"
|
166
|
+
details << port["LocalPort"]
|
167
|
+
details << port["Vip"]
|
168
|
+
details << port["PublicPort"]
|
169
|
+
end
|
170
|
+
end
|
171
|
+
puts ui.list(details, :columns_across, 4)
|
172
|
+
end
|
173
|
+
else
|
174
|
+
puts "No VM found"
|
175
|
+
end
|
176
|
+
|
177
|
+
rescue => error
|
178
|
+
puts "#{error.class} and #{error.message}"
|
179
|
+
end
|
180
|
+
|
181
|
+
def list_internal_lb
|
182
|
+
lbs = connection.lbs.all
|
183
|
+
cols = %w{Name Service Subnet VIP}
|
184
|
+
rows = []
|
185
|
+
lbs.each do |lb|
|
186
|
+
cols.each { |col| rows << lb.send(col.downcase).to_s }
|
187
|
+
end
|
188
|
+
display_list(ui, cols, rows)
|
189
|
+
end
|
190
|
+
|
191
|
+
def create_internal_lb(params = {})
|
192
|
+
connection.lbs.create(params)
|
193
|
+
end
|
194
|
+
|
195
|
+
def list_vnets
|
196
|
+
vnets = connection.vnets.all
|
197
|
+
cols = ["Name", "Affinity Group", "State"]
|
198
|
+
rows = []
|
199
|
+
vnets.each do |vnet|
|
200
|
+
%w{name affinity_group state}.each { |col| rows << vnet.send(col).to_s }
|
201
|
+
end
|
202
|
+
display_list(ui, cols, rows)
|
203
|
+
end
|
204
|
+
|
205
|
+
def create_vnet(params = {})
|
206
|
+
connection.vnets.create(params)
|
207
|
+
end
|
208
|
+
|
209
|
+
def list_affinity_groups
|
210
|
+
affinity_groups = connection.ags.all
|
211
|
+
cols = %w{Name Location Description}
|
212
|
+
rows = []
|
213
|
+
affinity_groups.each do |affinity_group|
|
214
|
+
cols.each { |col| rows << affinity_group.send(col.downcase).to_s }
|
215
|
+
end
|
216
|
+
display_list(ui, cols, rows)
|
217
|
+
end
|
218
|
+
|
219
|
+
def create_affinity_group(params = {})
|
220
|
+
connection.ags.create(params)
|
221
|
+
end
|
222
|
+
|
223
|
+
def create_server(params = {})
|
224
|
+
remove_hosted_service_on_failure = params[:azure_dns_name]
|
225
|
+
if connection.hosts.exists?(params[:azure_dns_name])
|
226
|
+
remove_hosted_service_on_failure = nil
|
227
|
+
end
|
228
|
+
|
229
|
+
#If Storage Account is not specified, check if the geographic location has one to re-use
|
230
|
+
if not params[:azure_storage_account]
|
231
|
+
storage_accts = connection.storageaccounts.all
|
232
|
+
storage = storage_accts.find { |storage_acct| storage_acct.location.to_s == params[:azure_service_location] }
|
233
|
+
unless storage
|
234
|
+
params[:azure_storage_account] = [strip_non_ascii(params[:azure_vm_name]), random_string].join.downcase
|
235
|
+
remove_storage_service_on_failure = params[:azure_storage_account]
|
236
|
+
else
|
237
|
+
remove_storage_service_on_failure = nil
|
238
|
+
params[:azure_storage_account] = storage.name.to_s
|
239
|
+
end
|
240
|
+
else
|
241
|
+
if connection.storageaccounts.exists?(params[:azure_storage_account])
|
242
|
+
remove_storage_service_on_failure = nil
|
243
|
+
else
|
244
|
+
remove_storage_service_on_failure = params[:azure_storage_account]
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
begin
|
249
|
+
connection.deploys.create(params)
|
250
|
+
rescue Exception => e
|
251
|
+
Chef::Log.error("Failed to create the server -- exception being rescued: #{e}")
|
252
|
+
backtrace_message = "#{e.class}: #{e}\n#{e.backtrace.join("\n")}"
|
253
|
+
Chef::Log.debug("#{backtrace_message}")
|
254
|
+
cleanup_and_exit(remove_hosted_service_on_failure, remove_storage_service_on_failure)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
def cleanup_and_exit(remove_hosted_service_on_failure, remove_storage_service_on_failure)
|
259
|
+
Chef::Log.warn("Cleaning up resources...")
|
260
|
+
|
261
|
+
if remove_hosted_service_on_failure
|
262
|
+
ret_val = connection.hosts.delete(remove_hosted_service_on_failure)
|
263
|
+
ret_val.content.empty? ? Chef::Log.warn("Deleted created DNS: #{remove_hosted_service_on_failure}.") : Chef::Log.warn("Deletion failed for created DNS:#{remove_hosted_service_on_failure}. " + ret_val.text)
|
264
|
+
end
|
265
|
+
|
266
|
+
if remove_storage_service_on_failure
|
267
|
+
ret_val = connection.storageaccounts.delete(remove_storage_service_on_failure)
|
268
|
+
ret_val.content.empty? ? Chef::Log.warn("Deleted created Storage Account: #{remove_storage_service_on_failure}.") : Chef::Log.warn("Deletion failed for created Storage Account: #{remove_storage_service_on_failure}. " + ret_val.text)
|
269
|
+
end
|
270
|
+
exit 1
|
271
|
+
end
|
272
|
+
|
273
|
+
def get_role_server(dns_name, vm_name)
|
274
|
+
deploy = connection.deploys.queryDeploy(dns_name)
|
275
|
+
deploy.find_role(vm_name)
|
276
|
+
end
|
277
|
+
|
278
|
+
def get_extension(name, publisher)
|
279
|
+
connection.query_azure("resourceextensions/#{publisher}/#{name}")
|
280
|
+
end
|
281
|
+
|
282
|
+
def deployment_name(dns_name)
|
283
|
+
connection.deploys.get_deploy_name_for_hostedservice(dns_name)
|
284
|
+
end
|
285
|
+
|
286
|
+
def deployment(path)
|
287
|
+
connection.query_azure(path)
|
288
|
+
end
|
289
|
+
|
290
|
+
def valid_image?(name)
|
291
|
+
connection.images.exists?(name)
|
292
|
+
end
|
293
|
+
|
294
|
+
def vm_image?(name)
|
295
|
+
connection.images.is_vm_image(name)
|
296
|
+
end
|
297
|
+
|
298
|
+
def add_extension(name, params = {})
|
299
|
+
ui.info "Started with Chef Extension deployment on the server #{name}..."
|
300
|
+
connection.roles.update(name, params)
|
301
|
+
ui.info "\nSuccessfully deployed Chef Extension on the server #{name}."
|
302
|
+
rescue Exception => e
|
303
|
+
Chef::Log.error("Failed to add extension to the server -- exception being rescued: #{e}")
|
304
|
+
backtrace_message = "#{e.class}: #{e}\n#{e.backtrace.join("\n")}"
|
305
|
+
Chef::Log.debug("#{backtrace_message}")
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|