knife-xapi 0.5.4 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -18,7 +18,6 @@
18
18
  # limitations under the License.
19
19
  #
20
20
 
21
-
22
21
  require 'chef/knife/xapi_base'
23
22
 
24
23
  class Chef
@@ -27,136 +26,142 @@ class Chef
27
26
  require 'timeout'
28
27
  include Chef::Knife::XapiBase
29
28
 
30
- Chef::Knife::XapiBase.set_defaults( {
31
- :domain => "",
32
- :ssh_user => "root",
33
- :ssh_port => "22",
34
- :ping_timeout => 600,
35
- :install_repo => "http://isoredirect.centos.org/centos/6/os/x86_64/",
36
- :kernel_params => "graphical utf8",
37
- :xapi_disk_size => "8g",
38
- :xapi_cpus => "1",
39
- :xapi_mem => "1g",
40
- :bootstrap_template => "chef-full",
41
- :template_file => false,
42
- :run_list => [],
43
- :json_attributes => {}
44
- })
29
+ Chef::Knife::XapiBase.set_defaults(
30
+ domain: '',
31
+ ssh_user: 'root',
32
+ ssh_port: '22',
33
+ ping_timeout: 600,
34
+ install_repo: 'http://isoredirect.centos.org/centos/6/os/x86_64/',
35
+ kernel_params: 'graphical utf8',
36
+ xapi_disk_size: '8g',
37
+ xapi_cpus: '1',
38
+ xapi_mem: '1g',
39
+ bootstrap_template: 'chef-full',
40
+ template_file: false,
41
+ run_list: [],
42
+ json_attributes: {}
43
+ )
45
44
 
46
45
  deps do
47
46
  require 'chef/knife/bootstrap'
48
47
  Chef::Knife::Bootstrap.load_deps
49
48
  end
50
49
 
51
- banner "knife xapi guest create NAME [NETWORKS] (options)"
50
+ banner 'knife xapi guest create NAME [NETWORKS] (options)'
52
51
 
53
52
  option :xapi_vm_template,
54
- :short => "-T Template Name Label",
55
- :long => "--xapi-vm-template",
56
- :proc => Proc.new { |key| Chef::Config[:knife][:xapi_vm_template] = key },
57
- :description => "xapi template name to create from. accepts an string or regex"
53
+ short: '-T Template Name Label',
54
+ long: '--xapi-vm-template',
55
+ proc: proc { |key| Chef::Config[:knife][:xapi_vm_template] = key },
56
+ description: 'xapi template name to create from. accepts an string or regex'
58
57
 
59
58
  option :install_repo,
60
- :short => "-R If you're using a builtin template you will need to specify a repo url",
61
- :long => "--install-repo",
62
- :description => "Install repo for this template (if needed)",
63
- :proc => Proc.new { |key| Chef::Config[:knife][:install_repo] = key }
59
+ short: "-R If you're using a builtin template you will need to specify a repo url",
60
+ long: '--install-repo',
61
+ description: 'Install repo for this template (if needed)',
62
+ proc: proc { |key| Chef::Config[:knife][:install_repo] = key }
63
+
64
+ option :macaddress,
65
+ short: '-m MAC Address',
66
+ long: '--mac-address',
67
+ description: 'Use a pre-generated MAC address (optional)',
68
+ proc: proc { |key| Chef::Config[:knife][:macaddress] = key }
64
69
 
65
70
  option :xapi_sr,
66
- :short => "-S Storage repo to provision VM from",
67
- :long => "--xapi-sr",
68
- :proc => Proc.new { |key| Chef::Config[:knife][:xapi_sr] = key },
69
- :description => "The Xen SR to use, If blank will use pool/hypervisor default"
71
+ short: '-S Storage repo to provision VM from',
72
+ long: '--xapi-sr',
73
+ proc: proc { |key| Chef::Config[:knife][:xapi_sr] = key },
74
+ description: 'The Xen SR to use, If blank will use pool/hypervisor default'
70
75
 
71
76
  option :kernel_params,
72
- :short => "-B Set of kernel boot params to pass to the vm",
73
- :long => "--kernel-params",
74
- :description => "You can add more boot options to the vm e.g.: \"ks='http://foo.local/ks'\"",
75
- :proc => Proc.new { |key| Chef::Config[:knife][:kernel_params] = key }
77
+ short: '-B Set of kernel boot params to pass to the vm',
78
+ long: '--kernel-params',
79
+ description: "You can add more boot options to the vm e.g.: \"ks='http://foo.local/ks'\"",
80
+ proc: proc { |key| Chef::Config[:knife][:kernel_params] = key }
76
81
 
77
82
  option :xapi_skip_disk,
78
- :long => "--xapi-skip-disk",
79
- :proc => Proc.new { |key| Chef::Config[:knife][:xapi_skip_disk] = key },
80
- :description => "Don't try to add disks to the new VM"
83
+ long: '--xapi-skip-disk',
84
+ proc: proc { |key| Chef::Config[:knife][:xapi_skip_disk] = key },
85
+ description: "Don't try to add disks to the new VM"
81
86
 
82
87
  option :xapi_disk_size,
83
- :short => "-D Size of disk. 1g 512m etc",
84
- :long => "--xapi-disk-size",
85
- :description => "The size of the root disk, use 'm' 'g' 't' if no unit specified assumes g",
86
- :proc => Proc.new { |key| Chef::Config[:knife][:xapi_disk_size] = key.to_s }
88
+ short: '-D Size of disk. 1g 512m etc',
89
+ long: '--xapi-disk-size',
90
+ description: "The size of the root disk, use 'm' 'g' 't' if no unit specified assumes g",
91
+ proc: proc { |key| Chef::Config[:knife][:xapi_disk_size] = key.to_s }
87
92
 
88
93
  option :xapi_cpus,
89
- :short => "-C Number of VCPUs to provision",
90
- :long => "--xapi-cpus",
91
- :description => "Number of VCPUS this vm should have 1 4 8 etc",
92
- :proc => Proc.new { |key| Chef::Config[:knife][:xapi_cpus] = key.to_s }
94
+ short: '-C Number of VCPUs to provision',
95
+ long: '--xapi-cpus',
96
+ description: 'Number of VCPUS this vm should have 1 4 8 etc',
97
+ proc: proc { |key| Chef::Config[:knife][:xapi_cpus] = key.to_s }
93
98
 
94
99
  option :xapi_mem,
95
- :short => "-M Ammount of memory to provision",
96
- :long => "--xapi-mem",
97
- :description => "Ammount of memory the VM should have specify with m g etc 512m, 2g if no unit spcified it assumes gigabytes",
98
- :proc => Proc.new { |key| Chef::Config[:knife][:xapi_mem] = key.to_s }
100
+ short: '-M Ammount of memory to provision',
101
+ long: '--xapi-mem',
102
+ description: 'Ammount of memory the VM should have specify with m g etc 512m, 2g if no unit spcified it assumes gigabytes',
103
+ proc: proc { |key| Chef::Config[:knife][:xapi_mem] = key.to_s }
99
104
 
100
105
  option :chef_node_name,
101
- :short => "-N NAME",
102
- :long => "--node-name NAME",
103
- :description => "The Chef node name for your new node"
106
+ short: '-N NAME',
107
+ long: '--node-name NAME',
108
+ description: 'The Chef node name for your new node'
104
109
 
105
110
  option :ssh_key_name,
106
- :short => "-S KEY",
107
- :long => "--ssh-key KEY",
108
- :proc => Proc.new { |key| Chef::Config[:knife][:ssh_key_name] = key },
109
- :description => "The SSH key id"
111
+ short: '-S KEY',
112
+ long: '--ssh-key KEY',
113
+ proc: proc { |key| Chef::Config[:knife][:ssh_key_name] = key },
114
+ description: 'The SSH key id'
110
115
 
111
116
  option :ssh_user,
112
- :short => "-x USERNAME",
113
- :long => "--ssh-user USERNAME",
114
- :description => "The ssh username",
115
- :proc => Proc.new { |key| Chef::Config[:knife][:ssh_user] = key }
117
+ short: '-x USERNAME',
118
+ long: '--ssh-user USERNAME',
119
+ description: 'The ssh username',
120
+ proc: proc { |key| Chef::Config[:knife][:ssh_user] = key }
116
121
 
117
122
  option :ssh_password,
118
- :short => "-P PASSWORD",
119
- :long => "--ssh-password PASSWORD",
120
- :proc => Proc.new { |key| Chef::Config[:knife][:ssh_password] = key },
121
- :description => "The ssh password"
123
+ short: '-P PASSWORD',
124
+ long: '--ssh-password PASSWORD',
125
+ proc: proc { |key| Chef::Config[:knife][:ssh_password] = key },
126
+ description: 'The ssh password'
122
127
 
123
128
  option :ssh_port,
124
- :short => "-p PORT",
125
- :long => "--ssh-port PORT",
126
- :description => "The ssh port"
129
+ short: '-p PORT',
130
+ long: '--ssh-port PORT',
131
+ description: 'The ssh port'
127
132
 
128
133
  option :ping_timeout,
129
- :long => "--ping-timeout",
130
- :description => "Seconds to timeout waiting for ip from guest"
134
+ long: '--ping-timeout',
135
+ description: 'Seconds to timeout waiting for ip from guest'
131
136
 
132
137
  option :bootstrap_version,
133
- :long => "--bootstrap-version VERSION",
134
- :description => "The version of Chef to install"
138
+ long: '--bootstrap-version VERSION',
139
+ description: 'The version of Chef to install'
135
140
 
136
141
  option :bootstrap_template,
137
- :short => "-d Template Name",
138
- :long => "--bootstrap-template Template Name",
139
- :description => "Bootstrap using a specific template"
142
+ short: '-d Template Name',
143
+ long: '--bootstrap-template Template Name',
144
+ description: 'Bootstrap using a specific template'
140
145
 
141
146
  option :template_file,
142
- :short => "-F FILEPATH",
143
- :long => "--template-file TEMPLATE",
144
- :description => "Full path to location of template to use"
147
+ short: '-F FILEPATH',
148
+ long: '--template-file TEMPLATE',
149
+ description: 'Full path to location of template to use'
145
150
 
146
151
  option :json_attributes,
147
- :short => "-j JSON_ATTRIBS",
148
- :long => "--json-attributes",
149
- :description => "A JSON string to be added to the first run of chef-client",
150
- :proc => lambda { |o| JSON.parse(o) }
152
+ short: '-j JSON_ATTRIBS',
153
+ long: '--json-attributes',
154
+ description: 'A JSON string to be added to the first run of chef-client',
155
+ proc: lambda { |o| JSON.parse(o) }
151
156
 
152
157
  option :run_list,
153
- :short => "-r RUN_LIST",
154
- :long => "--run-list RUN_LIST",
155
- :description => "Comma separated list of roles/recipes to apply",
156
- :proc => lambda { |o| o.split(/[\s,]+/) }
158
+ short: '-r RUN_LIST',
159
+ long: '--run-list RUN_LIST',
160
+ description: 'Comma separated list of roles/recipes to apply',
161
+ proc: lambda { |o| o.split(/[\s,]+/) }
157
162
 
158
163
  def tcp_test_ssh(hostname)
159
- tcp_socket = TCPSocket.new(hostname, locate_config_value(:ssh_port) )
164
+ tcp_socket = TCPSocket.new(hostname, locate_config_value(:ssh_port))
160
165
  readable = IO.select([tcp_socket], nil, nil, 5)
161
166
  if readable
162
167
  Chef::Log.debug("sshd accepting connections on #{hostname}, banner is #{tcp_socket.gets}")
@@ -195,35 +200,53 @@ class Chef
195
200
  end
196
201
  end
197
202
 
198
- def pingable?(guest_ip, timeout=5)
203
+ def pingable?(guest_ip, timeout = 5)
199
204
  sleep(20)
200
205
  system "ping -c 1 -t #{timeout} #{guest_ip} >/dev/null"
201
206
  end
202
207
 
203
208
  def get_guest_ip(vm_ref)
204
- begin
205
- timeout( locate_config_value(:ping_timeout).to_i ) do
206
- ui.msg "Waiting for guest ip address"
207
- guest_ip = ""
208
- while guest_ip.empty?
209
- print(".")
210
- sleep @initial_sleep_delay ||= 10
211
- vgm = xapi.VM.get_guest_metrics(vm_ref)
212
- next if "OpaqueRef:NULL" == vgm
213
- networks = xapi.VM_guest_metrics.get_networks(vgm)
214
- if networks.has_key?("0/ip")
215
- guest_ip = networks["0/ip"]
216
- end
209
+ timeout(locate_config_value(:ping_timeout).to_i) do
210
+ ui.msg 'Waiting for guest ip address'
211
+ guest_ip = ''
212
+ while guest_ip.empty?
213
+ print('.')
214
+ sleep @initial_sleep_delay ||= 10
215
+ vgm = xapi.VM.get_guest_metrics(vm_ref)
216
+ next if 'OpaqueRef:NULL' == vgm
217
+ networks = xapi.VM_guest_metrics.get_networks(vgm)
218
+ if networks.key?('0/ip')
219
+ guest_ip = networks['0/ip']
217
220
  end
218
- puts "\n"
219
- return guest_ip
220
221
  end
221
- rescue Timeout::Error
222
- ui.msg "Timeout waiting for XAPI to report IP address "
222
+ puts "\n"
223
+ return guest_ip
223
224
  end
225
+ rescue Timeout::Error
226
+ ui.msg 'Timeout waiting for XAPI to report IP address '
224
227
  end
225
228
 
226
229
 
230
+ def resolve_sr_ref
231
+ ui_sr = locate_config_value(:xapi_sr)
232
+ sr_ref = nil
233
+ if locate_config_value(:xapi_sr)
234
+ if is_uuid?(ui_sr)
235
+ sr_ref = get_sr_by_uuid(ui_sr)
236
+ else
237
+ sr_ref = get_sr_by_name(ui_sr)
238
+ end
239
+ else
240
+ sr_ref = find_default_sr
241
+ end
242
+
243
+ if sr_ref.nil?
244
+ ui.error "SR specified not found or can't be used Aborting"
245
+ fail(vm_ref) if sr_ref.nil?
246
+ end
247
+ sr_ref
248
+ end
249
+
227
250
  def run
228
251
  server_name = @name_args[0]
229
252
  domainname = locate_config_value(:domain)
@@ -234,11 +257,18 @@ class Chef
234
257
  end
235
258
 
236
259
  # get the template vm we are going to build from
237
- template_ref = find_template( locate_config_value(:xapi_vm_template) )
238
-
239
- Chef::Log.debug "Cloning Guest from Template: #{h.color(template_ref, :bold, :cyan )}"
240
- task = xapi.Async.VM.clone(template_ref, fqdn)
241
- ui.msg "Waiting on Template Clone"
260
+ template_ref = find_template(locate_config_value(:xapi_vm_template))
261
+ sr_ref = resolve_sr_ref
262
+
263
+ if locate_config_value(:xapi_sr)
264
+ Chef::Log.debug "Copying Guest from Template: #{h.color(template_ref, :bold, :cyan)}"
265
+ task = xapi.Async.VM.copy(template_ref, fqdn, sr_ref)
266
+ else
267
+ Chef::Log.debug "Cloning Guest from Template: #{h.color(template_ref, :bold, :cyan)}"
268
+ task = xapi.Async.VM.clone(template_ref, fqdn)
269
+ end
270
+
271
+ ui.msg 'Waiting on Template Clone'
242
272
  vm_ref = get_task_ref(task)
243
273
 
244
274
  Chef::Log.debug "New VM ref: #{vm_ref}"
@@ -251,23 +281,25 @@ class Chef
251
281
  repo = locate_config_value(:install_repo)
252
282
 
253
283
  # make sure we don't clobber existing params
254
- other_config = Hash.new
284
+ other_config = {}
255
285
  record = xapi.VM.get_record(vm_ref)
256
- if record.has_key? "other_config"
257
- other_config = record["other_config"]
286
+ if record.key? 'other_config'
287
+ other_config = record['other_config']
258
288
  end
259
- other_config["install-repository"] = repo
260
- # for some reason the deb disks template is wonkey and has weird entry here
289
+ other_config['install-repository'] = repo
290
+
291
+ # remove any disk config/xml template might be trying to do (ubuntu)
261
292
  other_config.delete_if {|k,v| k=="disks"}
293
+
262
294
  Chef::Log.debug "Other_config: #{other_config.inspect}"
263
295
  xapi.VM.set_other_config(vm_ref, other_config)
264
296
 
265
- cpus = locate_config_value( :xapi_cpus ).to_s
297
+ cpus = locate_config_value(:xapi_cpus).to_s
266
298
 
267
- xapi.VM.set_VCPUs_max( vm_ref, cpus )
268
- xapi.VM.set_VCPUs_at_startup( vm_ref, cpus )
299
+ xapi.VM.set_VCPUs_max(vm_ref, cpus)
300
+ xapi.VM.set_VCPUs_at_startup(vm_ref, cpus)
269
301
 
270
- memory_size = input_to_bytes( locate_config_value(:xapi_mem) ).to_s
302
+ memory_size = input_to_bytes(locate_config_value(:xapi_mem)).to_s
271
303
  # static-min <= dynamic-min = dynamic-max = static-max
272
304
  xapi.VM.set_memory_limits(vm_ref, memory_size, memory_size, memory_size, memory_size)
273
305
 
@@ -283,100 +315,89 @@ class Chef
283
315
  boot_args << " domain=#{domainname}" unless boot_args.match(/domain=.+\s?/)
284
316
  boot_args << " dnsdomain=#{domainname}" unless boot_args.match(/dnsdomain=.+\s?/)
285
317
 
286
- xapi.VM.set_PV_args( vm_ref, boot_args )
318
+ xapi.VM.set_PV_args(vm_ref, boot_args)
287
319
 
288
320
  # TODO: validate that the vm gets a network here
289
321
  networks = @name_args[1..-1]
290
322
  # if the user has provided networks
291
323
  if networks.length >= 1
292
- clear_vm_vifs( xapi.VM.get_record( vm_ref ) )
324
+ clear_vm_vifs(xapi.VM.get_record(vm_ref))
293
325
  networks.each_with_index do |net, index|
294
326
  add_vif_by_name(vm_ref, index, net)
295
327
  end
296
328
  end
297
329
 
298
- unless locate_config_value(:xapi_skip_disk)
299
- sr_ref = nil
300
- if locate_config_value(:xapi_sr)
301
- sr_ref = get_sr_by_name( locate_config_value(:xapi_sr) )
302
- else
303
- sr_ref = find_default_sr
304
- end
305
-
306
- if sr_ref.nil?
307
- ui.error "SR specified not found or can't be used Aborting"
308
- fail(vm_ref) if sr_ref.nil?
309
- end
310
- Chef::Log.debug "SR: #{h.color sr_ref, :cyan}"
311
-
330
+ Chef::Log.debug "SR: #{h.color sr_ref, :cyan}"
312
331
 
332
+ unless locate_config_value(:xapi_skip_disk)
313
333
  disk_size = locate_config_value(:xapi_disk_size)
314
- # setup disks
315
- if disk_size != nil and disk_size.to_i > 0
316
- # when a template already has disks, we decide the position number based on it.
317
- position = xapi.VM.get_VBDs(vm_ref).length
334
+ # setup disks
335
+ if !disk_size.nil? && disk_size.to_i > 0
336
+ # when a template already has disks, we decide the position number based on it.
337
+ position = xapi.VM.get_VBDs(vm_ref).length
318
338
 
319
339
  # Create the VDI
320
- vdi_ref = create_vdi("#{server_name}-root", sr_ref, locate_config_value(:xapi_disk_size) )
340
+ vdi_ref = create_vdi("#{server_name}-root", sr_ref, locate_config_value(:xapi_disk_size))
321
341
  fail(vm_ref) unless vdi_ref
322
342
 
323
343
  # Attach the VDI to the VM
324
344
  # if its position is 0 set it bootable
325
- position == 0 ? bootable=true : bootable=false
345
+ position == 0 ? bootable = true : bootable = false
326
346
 
327
347
  vbd_ref = create_vbd(vm_ref, vdi_ref, position, bootable)
328
348
  fail(vm_ref) unless vbd_ref
329
349
  end
330
350
  end
331
351
 
332
- ui.msg "Provisioning new Guest: #{h.color(fqdn, :bold, :cyan )}"
333
- ui.msg "Boot Args: #{h.color boot_args,:bold, :cyan}"
334
- ui.msg "Install Repo: #{ h.color(repo,:bold, :cyan)}"
335
- ui.msg "Memory: #{ h.color( locate_config_value(:xapi_mem).to_s, :bold, :cyan)}"
336
- ui.msg "CPUs: #{ h.color( locate_config_value(:xapi_cpus).to_s, :bold, :cyan)}"
337
- ui.msg "Disk: #{ h.color( disk_size.to_s, :bold, :cyan)}"
352
+ ui.msg "Provisioning new Guest: #{h.color(fqdn, :bold, :cyan)}"
353
+ ui.msg "Boot Args: #{h.color boot_args, :bold, :cyan}"
354
+ ui.msg "Install Repo: #{ h.color(repo, :bold, :cyan)}"
355
+ ui.msg "Memory: #{ h.color(locate_config_value(:xapi_mem).to_s, :bold, :cyan)}"
356
+ ui.msg "CPUs: #{ h.color(locate_config_value(:xapi_cpus).to_s, :bold, :cyan)}"
357
+ ui.msg "Disk: #{ h.color(disk_size.to_s, :bold, :cyan)}"
338
358
  provisioned = xapi.VM.provision(vm_ref)
339
359
 
340
- ui.msg "Starting new Guest #{h.color( provisioned, :cyan)} "
360
+ ui.msg "Starting new Guest #{h.color(provisioned, :cyan)} "
341
361
  start(vm_ref)
342
362
 
343
363
  exit 0 unless locate_config_value(:run_list)
344
- rescue Exception => e
345
- ui.msg "#{h.color 'ERROR:'} #{h.color( e.message, :red )}"
364
+ rescue => e
365
+ ui.msg "#{h.color 'ERROR:'} #{h.color(e.message, :red)}"
346
366
  # have to use join here to pass a string to highline
347
- puts "Nested backtrace:"
348
- ui.msg "#{h.color( e.backtrace.join("\n"), :yellow)}"
349
- fail(vm_ref)
367
+ puts 'Nested backtrace:'
368
+ ui.msg "#{h.color(e.backtrace.join("\n"), :yellow)}"
369
+ raise(vm_ref)
350
370
  end
351
371
 
352
372
  if locate_config_value(:run_list).empty?
353
- unless ( locate_config_value(:template_file) or locate_config_value(:bootstrap_template) )
373
+ unless locate_config_value(:template_file) or
374
+ locate_config_value(:bootstrap_template) != 'chef-full'
354
375
  exit 0
355
376
  end
356
377
  end
357
378
 
358
379
  guest_addr = wait_for_guest_ip(vm_ref)
359
- if guest_addr.nil? or guest_addr.empty?
360
- ui.msg("ip seems wrong using host+domain name instead")
380
+ if guest_addr.nil? || guest_addr.empty?
381
+ ui.msg('ip seems wrong using host+domain name instead')
361
382
  guest_addr = "#{server_name}.#{domainname}"
362
383
  end
363
384
  ui.msg "Trying to connect to guest @ #{guest_addr} "
364
385
 
365
386
  begin
366
387
  timeout(480) do
367
- print(".") until tcp_test_ssh(guest_addr) {
388
+ print('.') until tcp_test_ssh(guest_addr) do
368
389
  sleep @initial_sleep_delay ||= 10
369
- ui.msg( "#{ h.color "OK!", :green}" )
370
- }
390
+ ui.msg("#{ h.color 'OK!', :green}")
391
+ end
371
392
  end
372
393
  rescue Timeout::Error
373
- ui.msg "Timeout trying to login Wont bootstrap"
374
- fail
394
+ ui.msg 'Timeout trying to login Wont bootstrap'
395
+ raise
375
396
  end
376
397
 
377
398
  begin
378
399
  bootstrap = Chef::Knife::Bootstrap.new
379
- bootstrap.name_args = [ guest_addr ]
400
+ bootstrap.name_args = [guest_addr]
380
401
  bootstrap.config[:run_list] = locate_config_value(:run_list)
381
402
  bootstrap.config[:ssh_user] = locate_config_value(:ssh_user)
382
403
  bootstrap.config[:ssh_port] = locate_config_value(:ssh_port)
@@ -393,15 +414,13 @@ class Chef
393
414
  bootstrap.config[:run_list] = locate_config_value(:run_list)
394
415
 
395
416
  bootstrap.run
396
- rescue Exception => e
397
- ui.msg "#{h.color 'ERROR:'} #{h.color( e.message, :red )}"
398
- puts "Nested backtrace:"
399
- ui.msg "#{h.color( e.backtrace.join("\n"), :yellow)}"
400
- fail
417
+ rescue => e
418
+ ui.msg "#{h.color 'ERROR:'} #{h.color(e.message, :red)}"
419
+ puts 'Nested backtrace:'
420
+ ui.msg "#{h.color(e.backtrace.join("\n"), :yellow)}"
421
+ raise
401
422
  end
402
423
  end
403
-
404
424
  end
405
425
  end
406
426
  end
407
-