knife-xapi 0.2.2 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/chef/knife/xapi_base.rb +16 -14
- data/lib/chef/knife/xapi_guest_create.rb +65 -51
- data/lib/knife-xapi/version.rb +1 -1
- metadata +2 -4
- data/README.rdoc +0 -58
data/lib/chef/knife/xapi_base.rb
CHANGED
@@ -42,24 +42,25 @@ class Chef::Knife
|
|
42
42
|
require 'readline'
|
43
43
|
end
|
44
44
|
|
45
|
-
option :
|
45
|
+
option :xapi_host,
|
46
46
|
:short => "-h SERVER_URL",
|
47
47
|
:long => "--host SERVER_URL",
|
48
48
|
:description => "The url to the xenserver, http://somehost.local.lan/",
|
49
|
-
:proc => Proc.new { |host| Chef::Config[:knife][:
|
49
|
+
:proc => Proc.new { |host| Chef::Config[:knife][:xapi_host] = host }
|
50
50
|
|
51
|
-
option :
|
51
|
+
option :xapi_password,
|
52
52
|
:short => "-K PASSWORD",
|
53
|
-
:long => "--
|
53
|
+
:long => "--xapi-password PASSWORD",
|
54
54
|
:description => "Your xenserver password",
|
55
|
-
:proc => Proc.new { |key| Chef::Config[:knife][:
|
55
|
+
:proc => Proc.new { |key| Chef::Config[:knife][:xapi_password] = key }
|
56
56
|
|
57
|
-
option :
|
57
|
+
option :xapi_username,
|
58
58
|
:short => "-A USERNAME",
|
59
|
-
:long => "--
|
59
|
+
:long => "--xapi-username USERNAME",
|
60
60
|
:description => "Your xenserver username",
|
61
|
-
:proc => Proc.new { |username| Chef::Config[:knife][:
|
61
|
+
:proc => Proc.new { |username| Chef::Config[:knife][:xapi_username] = username }
|
62
62
|
end
|
63
|
+
|
63
64
|
end
|
64
65
|
|
65
66
|
# highline setup
|
@@ -71,11 +72,12 @@ class Chef::Knife
|
|
71
72
|
def xapi
|
72
73
|
@xapi ||= begin
|
73
74
|
|
74
|
-
|
75
|
+
ui.fatal "Must provide a xapi host with --host "unless locate_config_value(:xapi_host)
|
76
|
+
session = XenApi::Client.new( locate_config_value(:xapi_host) )
|
75
77
|
|
76
78
|
# get the password from the user
|
77
|
-
password =
|
78
|
-
username =
|
79
|
+
password = locate_config_value(:xapi_password) || nil
|
80
|
+
username = locate_config_value(:xapi_username) || "root"
|
79
81
|
if password.nil? or password.empty?
|
80
82
|
password = h.ask("Enter password for user #{username}: " ) { |input| input.echo = "*" }
|
81
83
|
end
|
@@ -132,7 +134,7 @@ class Chef::Knife
|
|
132
134
|
|
133
135
|
# ensure return values
|
134
136
|
if found
|
135
|
-
|
137
|
+
ui.msg "Using Template: #{h.color(found["name_label"], :cyan)}"
|
136
138
|
return get_template(found["name_label"]) # get the ref to this one
|
137
139
|
end
|
138
140
|
return nil
|
@@ -162,7 +164,7 @@ class Chef::Knife
|
|
162
164
|
|
163
165
|
# add a new vif
|
164
166
|
def add_vif_by_name(vm_ref, dev_num, net_name)
|
165
|
-
|
167
|
+
Chef::Log.debug "Looking up vif for: #{h.color(net_name, :cyan)}"
|
166
168
|
network_ref = xapi.network.get_by_name_label(net_name).first
|
167
169
|
if network_ref.nil?
|
168
170
|
ui.warn "#{h.color(net_name,:red)} not found, moving on"
|
@@ -170,7 +172,7 @@ class Chef::Knife
|
|
170
172
|
end
|
171
173
|
|
172
174
|
mac = generate_mac
|
173
|
-
|
175
|
+
Chef::Log.debug "Provisioning: #{h.color(net_name, :cyan)}, #{h.color(mac,:green)}, #{h.color(network_ref, :yellow)}"
|
174
176
|
|
175
177
|
vif = {
|
176
178
|
'device' => dev_num.to_s,
|
@@ -38,21 +38,23 @@ class Chef
|
|
38
38
|
:short => "-T Template Name Label",
|
39
39
|
:long => "--xapi-vm-template",
|
40
40
|
:description => "xapi template name to create from. accepts an string or regex",
|
41
|
-
:proc => Proc.new { |template| Chef::Config[:knife][:
|
41
|
+
:proc => Proc.new { |template| Chef::Config[:knife][:vm_template] = template }
|
42
42
|
|
43
43
|
option :domain,
|
44
44
|
:short => "-f Name",
|
45
45
|
:long => "--domain Name",
|
46
46
|
:description => "the domain name for the guest",
|
47
|
-
:proc => Proc.new { |domain| Chef::Config[:knife][:
|
47
|
+
:proc => Proc.new { |domain| Chef::Config[:knife][:domain] = domain },
|
48
|
+
:default => ""
|
48
49
|
|
49
50
|
option :install_repo,
|
50
51
|
:short => "-R If you're using a builtin template you will need to specify a repo url",
|
51
52
|
:long => "--xapi-install-repo",
|
52
53
|
:description => "Install repo for this template (if needed)",
|
53
|
-
:proc => Proc.new { |repo| Chef::Config[:knife][:
|
54
|
+
:proc => Proc.new { |repo| Chef::Config[:knife][:install_repo] = repo },
|
55
|
+
:default => "http://isoredirect.centos.org/centos/6/os/x86_64/"
|
54
56
|
|
55
|
-
option :
|
57
|
+
option :xapi_sr,
|
56
58
|
:short => "-S Storage repo to provision VM from",
|
57
59
|
:long => "--xapi-sr",
|
58
60
|
:description => "The Xen SR to use, If blank will use pool/hypervisor default",
|
@@ -62,25 +64,29 @@ class Chef
|
|
62
64
|
:short => "-B Set of kernel boot params to pass to the vm",
|
63
65
|
:long => "--xapi-kernel-params",
|
64
66
|
:description => "You can add more boot options to the vm e.g.: \"ks='http://foo.local/ks'\"",
|
65
|
-
:proc => Proc.new {|kernel| Chef::Config[:knife][:xapi_kernel_params] = kernel }
|
67
|
+
:proc => Proc.new {|kernel| Chef::Config[:knife][:xapi_kernel_params] = kernel },
|
68
|
+
:default => "graphical utf8"
|
66
69
|
|
67
|
-
option :
|
70
|
+
option :xapi_disk_size,
|
68
71
|
:short => "-D Size of disk. 1g 512m etc",
|
69
72
|
:long => "--xapi-disk-size",
|
70
73
|
:description => "The size of the root disk, use 'm' 'g' 't' if no unit specified assumes g",
|
71
|
-
:proc => Proc.new {|disk| Chef::Config[:knife][:xapi_disk_size] = disk }
|
74
|
+
:proc => Proc.new {|disk| Chef::Config[:knife][:xapi_disk_size] = disk },
|
75
|
+
:default => "8g"
|
72
76
|
|
73
|
-
option :
|
77
|
+
option :xapi_cpus,
|
74
78
|
:short => "-C Number of VCPUs to provision",
|
75
79
|
:long => "--xapi-cpus",
|
76
80
|
:description => "Number of VCPUS this vm should have 1 4 8 etc",
|
81
|
+
:default => 2,
|
77
82
|
:proc => Proc.new {|cpu| Chef::Config[:knife][:xapi_cpus] = cpu }
|
78
83
|
|
79
|
-
option :
|
84
|
+
option :xapi_mem,
|
80
85
|
:short => "-M Ammount of memory to provision",
|
81
86
|
:long => "--xapi-mem",
|
82
87
|
:description => "Ammount of memory the VM should have specify with m g etc 512m, 2g if no unit spcified it assumes gigabytes",
|
83
|
-
:proc => Proc.new {|mem| Chef::Config[:knife][:xapi_mem] = mem }
|
88
|
+
:proc => Proc.new {|mem| Chef::Config[:knife][:xapi_mem] = mem },
|
89
|
+
:default => "1g"
|
84
90
|
|
85
91
|
option :chef_node_name,
|
86
92
|
:short => "-N NAME",
|
@@ -91,7 +97,8 @@ class Chef
|
|
91
97
|
:short => "-S KEY",
|
92
98
|
:long => "--ssh-key KEY",
|
93
99
|
:description => "The SSH key id",
|
94
|
-
:proc => Proc.new { |key| Chef::Config[:knife][:
|
100
|
+
:proc => Proc.new { |key| Chef::Config[:knife][:ssh_key_name] = key }
|
101
|
+
|
95
102
|
|
96
103
|
option :ssh_user,
|
97
104
|
:short => "-x USERNAME",
|
@@ -108,19 +115,19 @@ class Chef
|
|
108
115
|
:short => "-p PORT",
|
109
116
|
:long => "--ssh-port PORT",
|
110
117
|
:description => "The ssh port",
|
111
|
-
:
|
112
|
-
:
|
118
|
+
:proc => Proc.new { |key| Chef::Config[:knife][:ssh_port] = key },
|
119
|
+
:default => "22"
|
113
120
|
|
114
121
|
option :bootstrap_version,
|
115
122
|
:long => "--bootstrap-version VERSION",
|
116
123
|
:description => "The version of Chef to install",
|
117
124
|
:proc => Proc.new { |v| Chef::Config[:knife][:bootstrap_version] = v }
|
118
125
|
|
119
|
-
option :
|
120
|
-
:short => "-d
|
121
|
-
:long => "--
|
122
|
-
:description => "Bootstrap
|
123
|
-
:proc => Proc.new { |d| Chef::Config[:knife][:
|
126
|
+
option :bootstrap_template,
|
127
|
+
:short => "-d Template Name",
|
128
|
+
:long => "--bootstrap-template Template Name",
|
129
|
+
:description => "Bootstrap using a specific template",
|
130
|
+
:proc => Proc.new { |d| Chef::Config[:knife][:bootstrap_template] = d },
|
124
131
|
:default => "ubuntu10.04-gems"
|
125
132
|
|
126
133
|
option :template_file,
|
@@ -212,41 +219,40 @@ class Chef
|
|
212
219
|
$stdout.sync = true
|
213
220
|
|
214
221
|
# get the template vm we are going to build from
|
215
|
-
template_ref = find_template(
|
222
|
+
template_ref = find_template( locate_config_value(:vm_template) )
|
216
223
|
|
217
|
-
|
224
|
+
Chef::Log.debug "Cloning Guest from Template: #{h.color(template_ref, :bold, :cyan )}"
|
218
225
|
vm_ref = xapi.VM.clone(template_ref, server_name)
|
219
226
|
|
227
|
+
# TODO: lift alot of this
|
220
228
|
begin
|
221
|
-
xapi.VM.set_name_description(vm_ref, "VM
|
229
|
+
xapi.VM.set_name_description(vm_ref, "VM from knife-xapi as #{server_name}")
|
222
230
|
|
223
231
|
# configure the install repo
|
224
|
-
repo =
|
225
|
-
ui.msg "Setting Install Repo: #{h.color(repo,:bold, :cyan)}"
|
232
|
+
repo = locate_config_value(:install_repo)
|
226
233
|
xapi.VM.set_other_config(vm_ref, { "install-repository" => repo } )
|
227
|
-
|
228
|
-
|
234
|
+
|
235
|
+
|
236
|
+
cpus = locate_config_value( :xapi_cpus ).to_s
|
237
|
+
|
229
238
|
xapi.VM.set_VCPUs_max( vm_ref, cpus )
|
230
239
|
xapi.VM.set_VCPUs_at_startup( vm_ref, cpus )
|
231
240
|
|
232
|
-
memory_size = input_to_bytes(
|
233
|
-
ui.msg "Mem size: #{ h.color( memory_size, :cyan)}"
|
234
|
-
|
241
|
+
memory_size = input_to_bytes( locate_config_value(:xapi_mem) ).to_s
|
235
242
|
# static-min <= dynamic-min = dynamic-max = static-max
|
236
243
|
xapi.VM.set_memory_limits(vm_ref, memory_size, memory_size, memory_size, memory_size)
|
237
244
|
|
238
245
|
#
|
239
246
|
# setup the Boot args
|
240
247
|
#
|
241
|
-
boot_args =
|
242
|
-
domainname =
|
248
|
+
boot_args = locate_config_value(:kernel_params)
|
249
|
+
domainname = locate_config_value(:domain)
|
243
250
|
|
244
251
|
# if no hostname param set hostname to given vm name
|
245
252
|
boot_args << " hostname=#{server_name}" unless boot_args.match(/hostname=.+\s?/)
|
246
253
|
# if domainname is supplied we put that in there as well
|
247
254
|
boot_args << " domainname=#{domainname}" unless boot_args.match(/domainname=.+\s?/)
|
248
255
|
|
249
|
-
ui.msg "Setting Boot Args: #{h.color boot_args, :cyan}"
|
250
256
|
xapi.VM.set_PV_args( vm_ref, boot_args )
|
251
257
|
|
252
258
|
# TODO: validate that the vm gets a network here
|
@@ -259,23 +265,23 @@ class Chef
|
|
259
265
|
end
|
260
266
|
end
|
261
267
|
|
262
|
-
|
263
|
-
|
264
|
-
|
268
|
+
if locate_config_value(:xapi_sr)
|
269
|
+
sr_ref = get_sr_by_name( locate_config_value(:xapi_sr) )
|
270
|
+
else
|
271
|
+
sr_ref = find_default_sr
|
265
272
|
end
|
266
273
|
|
267
274
|
if sr_ref.nil?
|
268
275
|
ui.error "SR specified not found or can't be used Aborting"
|
269
276
|
cleanup(vm_ref)
|
270
277
|
end
|
271
|
-
|
278
|
+
Chef::Log.debug "SR: #{h.color sr_ref, :cyan}"
|
272
279
|
|
273
280
|
# Create the VDI
|
274
|
-
|
275
|
-
vdi_ref = create_vdi("#{server_name}-root", sr_ref, disk_size )
|
281
|
+
vdi_ref = create_vdi("#{server_name}-root", sr_ref, locate_config_value(:xapi_disk_size) )
|
276
282
|
# if vdi_ref is nill we need to bail/cleanup
|
277
283
|
cleanup(vm_ref) unless vdi_ref
|
278
|
-
ui.msg( "#{ h.color "OK", :green}"
|
284
|
+
ui.msg( "#{ h.color "OK", :green} ")
|
279
285
|
|
280
286
|
# Attach the VDI to the VM
|
281
287
|
vbd_ref = create_vbd(vm_ref, vdi_ref, 0)
|
@@ -283,13 +289,17 @@ class Chef
|
|
283
289
|
ui.msg( "#{ h.color "OK", :green}" )
|
284
290
|
|
285
291
|
ui.msg "Provisioning new Guest: #{h.color(vm_ref, :bold, :cyan )}"
|
292
|
+
ui.msg "Boot Args: #{h.color boot_args,:bold, :cyan}"
|
293
|
+
ui.msg "Install Repo: #{ h.color(repo,:bold, :cyan)}"
|
294
|
+
ui.msg "Memory: #{ h.color( locate_config_value(:xapi_mem).to_s, :bold, :cyan)}"
|
295
|
+
ui.msg "CPUs: #{ h.color( locate_config_value(:xapi_cpus).to_s, :bold, :cyan)}"
|
296
|
+
ui.msg "Disk: #{ h.color( locate_config_value(:xapi_disk_size).to_s, :bold, :cyan)}"
|
286
297
|
provisioned = xapi.VM.provision(vm_ref)
|
287
298
|
|
288
299
|
ui.msg "Starting new Guest #{h.color( provisioned, :cyan)} "
|
289
|
-
|
290
300
|
task = xapi.Async.VM.start(vm_ref, false, true)
|
291
301
|
wait_on_task(task)
|
292
|
-
ui.msg( "#{ h.color "
|
302
|
+
ui.msg( "#{ h.color "OK!", :green}" )
|
293
303
|
|
294
304
|
exit 0 unless locate_config_value(:run_list)
|
295
305
|
rescue Exception => e
|
@@ -312,7 +322,7 @@ class Chef
|
|
312
322
|
timeout(480) do
|
313
323
|
print(".") until tcp_test_ssh(guest_addr) {
|
314
324
|
sleep @initial_sleep_delay ||= 10
|
315
|
-
|
325
|
+
ui.msg( "#{ h.color "OK!", :green}" )
|
316
326
|
}
|
317
327
|
end
|
318
328
|
rescue Timeout::Error
|
@@ -321,8 +331,12 @@ class Chef
|
|
321
331
|
end
|
322
332
|
|
323
333
|
|
324
|
-
|
325
|
-
|
334
|
+
# begin
|
335
|
+
if domainname.empty?
|
336
|
+
server = server_name
|
337
|
+
else
|
338
|
+
server = "#{server_name}.#{domainname}"
|
339
|
+
end
|
326
340
|
bootstrap = Chef::Knife::Bootstrap.new
|
327
341
|
bootstrap.name_args = [ guest_addr ]
|
328
342
|
bootstrap.config[:run_list] = config[:run_list]
|
@@ -330,9 +344,9 @@ class Chef
|
|
330
344
|
bootstrap.config[:ssh_port] = config[:ssh_port]
|
331
345
|
bootstrap.config[:ssh_password] = config[:ssh_password]
|
332
346
|
bootstrap.config[:identity_file] = config[:identity_file]
|
333
|
-
bootstrap.config[:chef_node_name] = config[:chef_node_name] ||
|
347
|
+
bootstrap.config[:chef_node_name] = config[:chef_node_name] || server
|
334
348
|
bootstrap.config[:bootstrap_version] = locate_config_value(:bootstrap_version)
|
335
|
-
bootstrap.config[:distro] = locate_config_value(:
|
349
|
+
bootstrap.config[:distro] = locate_config_value(:bootstrap_template)
|
336
350
|
bootstrap.config[:use_sudo] = true unless config[:ssh_user] == 'root'
|
337
351
|
bootstrap.config[:template_file] = locate_config_value(:template_file)
|
338
352
|
bootstrap.config[:environment] = config[:environment]
|
@@ -340,12 +354,12 @@ class Chef
|
|
340
354
|
bootstrap.config[:run_list] = config[:run_list]
|
341
355
|
|
342
356
|
bootstrap.run
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
357
|
+
# rescue Exception => e
|
358
|
+
# ui.msg "#{h.color 'ERROR:'} #{h.color( e.message, :red )}"
|
359
|
+
# puts "Nested backtrace:"
|
360
|
+
# ui.msg "#{h.color( e.backtrace.join("\n"), :yellow)}"
|
361
|
+
# cleanup(vm_ref)
|
362
|
+
# end
|
349
363
|
|
350
364
|
end
|
351
365
|
|
data/lib/knife-xapi/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: knife-xapi
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-05
|
12
|
+
date: 2012-06-05 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: chef
|
@@ -81,10 +81,8 @@ executables: []
|
|
81
81
|
extensions: []
|
82
82
|
extra_rdoc_files:
|
83
83
|
- LICENSE
|
84
|
-
- README.rdoc
|
85
84
|
files:
|
86
85
|
- LICENSE
|
87
|
-
- README.rdoc
|
88
86
|
- lib/chef/knife/xapi_base.rb
|
89
87
|
- lib/chef/knife/xapi_guest_create.rb
|
90
88
|
- lib/chef/knife/xapi_guest_delete.rb
|
data/README.rdoc
DELETED
@@ -1,58 +0,0 @@
|
|
1
|
-
=Knife Xapi
|
2
|
-
This plugin gives knife the ability to create guests on a XAPI compatable hyper visor
|
3
|
-
|
4
|
-
==Installation
|
5
|
-
This plugin is distributed as a Ruby Gem. To install it, run:
|
6
|
-
gem install knife-xapi
|
7
|
-
|
8
|
-
==Configuration
|
9
|
-
Config options are extenable in the knife.rb the folowing k/v pairs are implemented
|
10
|
-
|
11
|
-
Chef::Config[:knife][:xenserver_host] :: The API Host to connect to
|
12
|
-
Chef::Config[:knife][:xenserver_username] :: The User name to connect to the api with
|
13
|
-
Chef::Config[:knife][:xenserver_password] :: The Password (if not set will prompt on commandline)
|
14
|
-
Chef::Config[:knife][:xapi_vm_template] :: Set a default template to be used when creating Guests
|
15
|
-
Chef::Config[:knife][:xapi_install_repo] :: The install repo config option to set when using Xen builtin templates
|
16
|
-
Chef::Config[:knife][:xapi_sr] :: The Storage Repository to provision from, uses pool/hypervisor default when not set
|
17
|
-
Chef::Config[:knife][:xapi_disk_size] :: Default VM disk size (8g if not specified)
|
18
|
-
Chef::Config[:knife][:xapi_cpus] :: The Default CPUs to provision for guests (2 if not specified)
|
19
|
-
Chef::Config[:knife][:xapi_mem] = mem :: The Defaul ammount of Memory for guests (1g if not specified)
|
20
|
-
Chef::Config[:knife][:xapi_kernel_params] :: Optional Boot paramaters to pass to the guest
|
21
|
-
Chef::Config[:knife][:xapi_bootstrap] :: Not implemented yet, but will be the bootstrap script to execute after guest create
|
22
|
-
|
23
|
-
==Usage
|
24
|
-
=Create
|
25
|
-
Basic usage to create a VM from existing VM template:
|
26
|
-
knife xapi guest create "NewBox" "public" --xapi-vm-template "MyBaseBox" --host http://sandbox/
|
27
|
-
|
28
|
-
|
29
|
-
More verbose example using a kickstart file and booting the Centos 5 default template:
|
30
|
-
knife xapi guest create "MySpiffyBox" "pub_network" --host http://sandbox/ \
|
31
|
-
-B "dns=8.8.8.8 ks=http://192.168.6.4/repo/ks/default.ks ip=192.168.6.7 netmask=255.255.255.0 gateway=192.168.6.1" \
|
32
|
-
-R http://192.168.6.5/repo/centos/5/os/x86_64 -C 4 -M 4g -D 5g
|
33
|
-
*-B Boot args where i am assigning all the centos/rhel boot args for Ip setup and kickstart file
|
34
|
-
*-R Repo URL used by xenserver to start the net install
|
35
|
-
*-C Number of cpus for this guest
|
36
|
-
*-M Memory size
|
37
|
-
*-D Disk size
|
38
|
-
|
39
|
-
Use Knife builtin help schematic for more info
|
40
|
-
knife xapi guest create --help
|
41
|
-
|
42
|
-
=Delete
|
43
|
-
Delete is pretty simple. When there are multiple vms with a name label you should be prompted to select one
|
44
|
-
knife xapi guest delete testing
|
45
|
-
|
46
|
-
If you know the UUID of the VM you can specify --uuid
|
47
|
-
knife xapi guest delete b461c0d2-d24d-bc02-3231-711101f57b8e --uuid
|
48
|
-
|
49
|
-
=List
|
50
|
-
List shows the vm's on the pool/host Ignoring Controll domains and templates. VM OpaqueRef and UUID are displayed which can be
|
51
|
-
knife xapi guest list
|
52
|
-
Name Label Ref UUID
|
53
|
-
test-server OpaqueRef:82065b80-55ff-63ce-ef89-6b33fb5fd272 9b0a0afa-5573-7875-b787-47fbfa2548a4
|
54
|
-
tester OpaqueRef:2d239fbd-bff6-4e60-f675-e1d2530199d2 de760651-2db8-6f81-0783-7b8364f591fd
|
55
|
-
test-client OpaqueRef:e4bbd801-c9be-e355-2a22-2ca468a90a81 35156957-45f4-02f8-6de9-6adbcd5e0c6d
|
56
|
-
test-client OpaqueRef:f5b562f8-a493-f535-335e-ae70b3177869 f46e4d6b-bd9e-e47b-5f0d-b849ff75c5ef
|
57
|
-
|
58
|
-
|