vcloud-rest 0.3.0 → 1.0.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.
@@ -20,6 +20,15 @@ require 'rest-client'
20
20
  require 'nokogiri'
21
21
  require 'httpclient'
22
22
  require 'ruby-progressbar'
23
+ require 'logger'
24
+
25
+ require 'vcloud-rest/vcloud/vapp'
26
+ require 'vcloud-rest/vcloud/org'
27
+ require 'vcloud-rest/vcloud/catalog'
28
+ require 'vcloud-rest/vcloud/vdc'
29
+ require 'vcloud-rest/vcloud/vm'
30
+ require 'vcloud-rest/vcloud/ovf'
31
+ require 'vcloud-rest/vcloud/network'
23
32
 
24
33
  module VCloudClient
25
34
  class UnauthorizedAccess < StandardError; end
@@ -27,9 +36,10 @@ module VCloudClient
27
36
  class WrongItemIDError < StandardError; end
28
37
  class InvalidStateError < StandardError; end
29
38
  class InternalServerError < StandardError; end
39
+ class OVFError < StandardError; end
40
+ class MethodNotAllowed < StandardError; end
30
41
  class UnhandledError < StandardError; end
31
42
 
32
-
33
43
  # Main class to access vCloud rest APIs
34
44
  class Connection
35
45
  attr_reader :api_url, :auth_key
@@ -42,6 +52,8 @@ module VCloudClient
42
52
  @password = password
43
53
  @org_name = org_name
44
54
  @api_version = (api_version || "5.1")
55
+
56
+ init_logger
45
57
  end
46
58
 
47
59
  ##
@@ -70,664 +82,8 @@ module VCloudClient
70
82
  }
71
83
 
72
84
  response, headers = send_request(params)
73
- end
74
-
75
- ##
76
- # Fetch existing organizations and their IDs
77
- def get_organizations
78
- params = {
79
- 'method' => :get,
80
- 'command' => '/org'
81
- }
82
-
83
- response, headers = send_request(params)
84
- orgs = response.css('OrgList Org')
85
-
86
- results = {}
87
- orgs.each do |org|
88
- results[org['name']] = org['href'].gsub("#{@api_url}/org/", "")
89
- end
90
- results
91
- end
92
-
93
- ##
94
- # Fetch details about an organization:
95
- # - catalogs
96
- # - vdcs
97
- # - networks
98
- def get_organization(orgId)
99
- params = {
100
- 'method' => :get,
101
- 'command' => "/org/#{orgId}"
102
- }
103
-
104
- response, headers = send_request(params)
105
- catalogs = {}
106
- response.css("Link[type='application/vnd.vmware.vcloud.catalog+xml']").each do |item|
107
- catalogs[item['name']] = item['href'].gsub("#{@api_url}/catalog/", "")
108
- end
109
-
110
- vdcs = {}
111
- response.css("Link[type='application/vnd.vmware.vcloud.vdc+xml']").each do |item|
112
- vdcs[item['name']] = item['href'].gsub("#{@api_url}/vdc/", "")
113
- end
114
-
115
- networks = {}
116
- response.css("Link[type='application/vnd.vmware.vcloud.orgNetwork+xml']").each do |item|
117
- networks[item['name']] = item['href'].gsub("#{@api_url}/network/", "")
118
- end
119
-
120
- tasklists = {}
121
- response.css("Link[type='application/vnd.vmware.vcloud.tasksList+xml']").each do |item|
122
- tasklists[item['name']] = item['href'].gsub("#{@api_url}/tasksList/", "")
123
- end
124
-
125
- { :catalogs => catalogs, :vdcs => vdcs, :networks => networks, :tasklists => tasklists }
126
- end
127
-
128
- ##
129
- # Fetch details about a given catalog
130
- def get_catalog(catalogId)
131
- params = {
132
- 'method' => :get,
133
- 'command' => "/catalog/#{catalogId}"
134
- }
135
-
136
- response, headers = send_request(params)
137
- description = response.css("Description").first
138
- description = description.text unless description.nil?
139
-
140
- items = {}
141
- response.css("CatalogItem[type='application/vnd.vmware.vcloud.catalogItem+xml']").each do |item|
142
- items[item['name']] = item['href'].gsub("#{@api_url}/catalogItem/", "")
143
- end
144
- { :description => description, :items => items }
145
- end
146
-
147
- ##
148
- # Fetch details about a given vdc:
149
- # - description
150
- # - vapps
151
- # - networks
152
- def get_vdc(vdcId)
153
- params = {
154
- 'method' => :get,
155
- 'command' => "/vdc/#{vdcId}"
156
- }
157
-
158
- response, headers = send_request(params)
159
- description = response.css("Description").first
160
- description = description.text unless description.nil?
161
-
162
- vapps = {}
163
- response.css("ResourceEntity[type='application/vnd.vmware.vcloud.vApp+xml']").each do |item|
164
- vapps[item['name']] = item['href'].gsub("#{@api_url}/vApp/vapp-", "")
165
- end
166
-
167
- networks = {}
168
- response.css("Network[type='application/vnd.vmware.vcloud.network+xml']").each do |item|
169
- networks[item['name']] = item['href'].gsub("#{@api_url}/network/", "")
170
- end
171
- { :description => description, :vapps => vapps, :networks => networks }
172
- end
173
-
174
- ##
175
- # Fetch details about a given catalog item:
176
- # - description
177
- # - vApp templates
178
- def get_catalog_item(catalogItemId)
179
- params = {
180
- 'method' => :get,
181
- 'command' => "/catalogItem/#{catalogItemId}"
182
- }
183
-
184
- response, headers = send_request(params)
185
- description = response.css("Description").first
186
- description = description.text unless description.nil?
187
-
188
- items = {}
189
- response.css("Entity[type='application/vnd.vmware.vcloud.vAppTemplate+xml']").each do |item|
190
- items[item['name']] = item['href'].gsub("#{@api_url}/vAppTemplate/vappTemplate-", "")
191
- end
192
- { :description => description, :items => items }
193
- end
194
-
195
- ##
196
- # Fetch details about a given vapp:
197
- # - name
198
- # - description
199
- # - status
200
- # - IP
201
- # - Children VMs:
202
- # -- IP addresses
203
- # -- status
204
- # -- ID
205
- def get_vapp(vAppId)
206
- params = {
207
- 'method' => :get,
208
- 'command' => "/vApp/vapp-#{vAppId}"
209
- }
210
-
211
- response, headers = send_request(params)
212
-
213
- vapp_node = response.css('VApp').first
214
- if vapp_node
215
- name = vapp_node['name']
216
- status = convert_vapp_status(vapp_node['status'])
217
- end
218
-
219
- description = response.css("Description").first
220
- description = description.text unless description.nil?
221
-
222
- ip = response.css('IpAddress').first
223
- ip = ip.text unless ip.nil?
224
-
225
- vms = response.css('Children Vm')
226
- vms_hash = {}
227
-
228
- # ipAddress could be namespaced or not: see https://github.com/astratto/vcloud-rest/issues/3
229
- vms.each do |vm|
230
- vapp_local_id = vm.css('VAppScopedLocalId')
231
- addresses = vm.css('rasd|Connection').collect{|n| n['vcloud:ipAddress'] || n['ipAddress'] }
232
- vms_hash[vm['name']] = {
233
- :addresses => addresses,
234
- :status => convert_vapp_status(vm['status']),
235
- :id => vm['href'].gsub("#{@api_url}/vApp/vm-", ''),
236
- :vapp_scoped_local_id => vapp_local_id.text
237
- }
238
- end
239
-
240
- # TODO: EXPAND INFO FROM RESPONSE
241
- { :name => name, :description => description, :status => status, :ip => ip, :vms_hash => vms_hash }
242
- end
243
-
244
- ##
245
- # Delete a given vapp
246
- # NOTE: It doesn't verify that the vapp is shutdown
247
- def delete_vapp(vAppId)
248
- params = {
249
- 'method' => :delete,
250
- 'command' => "/vApp/vapp-#{vAppId}"
251
- }
252
-
253
- response, headers = send_request(params)
254
- task_id = headers[:location].gsub("#{@api_url}/task/", "")
255
- task_id
256
- end
257
-
258
- ##
259
- # Shutdown a given vapp
260
- def poweroff_vapp(vAppId)
261
- builder = Nokogiri::XML::Builder.new do |xml|
262
- xml.UndeployVAppParams(
263
- "xmlns" => "http://www.vmware.com/vcloud/v1.5") {
264
- xml.UndeployPowerAction 'powerOff'
265
- }
266
- end
267
-
268
- params = {
269
- 'method' => :post,
270
- 'command' => "/vApp/vapp-#{vAppId}/action/undeploy"
271
- }
272
-
273
- response, headers = send_request(params, builder.to_xml,
274
- "application/vnd.vmware.vcloud.undeployVAppParams+xml")
275
- task_id = headers[:location].gsub("#{@api_url}/task/", "")
276
- task_id
277
- end
278
-
279
- ##
280
- # Suspend a given vapp
281
- def suspend_vapp(vAppId)
282
- params = {
283
- 'method' => :post,
284
- 'command' => "/vApp/vapp-#{vAppId}/power/action/suspend"
285
- }
286
-
287
- response, headers = send_request(params)
288
- task_id = headers[:location].gsub("#{@api_url}/task/", "")
289
- task_id
290
- end
291
-
292
- ##
293
- # reboot a given vapp
294
- # This will basically initial a guest OS reboot, and will only work if
295
- # VMware-tools are installed on the underlying VMs.
296
- # vShield Edge devices are not affected
297
- def reboot_vapp(vAppId)
298
- params = {
299
- 'method' => :post,
300
- 'command' => "/vApp/vapp-#{vAppId}/power/action/reboot"
301
- }
302
-
303
- response, headers = send_request(params)
304
- task_id = headers[:location].gsub("#{@api_url}/task/", "")
305
- task_id
306
- end
307
-
308
- ##
309
- # reset a given vapp
310
- # This will basically reset the VMs within the vApp
311
- # vShield Edge devices are not affected.
312
- def reset_vapp(vAppId)
313
- params = {
314
- 'method' => :post,
315
- 'command' => "/vApp/vapp-#{vAppId}/power/action/reset"
316
- }
317
-
318
- response, headers = send_request(params)
319
- task_id = headers[:location].gsub("#{@api_url}/task/", "")
320
- task_id
321
- end
322
-
323
- ##
324
- # Boot a given vapp
325
- def poweron_vapp(vAppId)
326
- params = {
327
- 'method' => :post,
328
- 'command' => "/vApp/vapp-#{vAppId}/power/action/powerOn"
329
- }
330
-
331
- response, headers = send_request(params)
332
- task_id = headers[:location].gsub("#{@api_url}/task/", "")
333
- task_id
334
- end
335
-
336
- ##
337
- # Create a vapp starting from a template
338
- #
339
- # Params:
340
- # - vdc: the associated VDC
341
- # - vapp_name: name of the target vapp
342
- # - vapp_description: description of the target vapp
343
- # - vapp_templateid: ID of the vapp template
344
- def create_vapp_from_template(vdc, vapp_name, vapp_description, vapp_templateid, poweron=false)
345
- builder = Nokogiri::XML::Builder.new do |xml|
346
- xml.InstantiateVAppTemplateParams(
347
- "xmlns" => "http://www.vmware.com/vcloud/v1.5",
348
- "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance",
349
- "xmlns:ovf" => "http://schemas.dmtf.org/ovf/envelope/1",
350
- "name" => vapp_name,
351
- "deploy" => "true",
352
- "powerOn" => poweron) {
353
- xml.Description vapp_description
354
- xml.Source("href" => "#{@api_url}/vAppTemplate/#{vapp_templateid}")
355
- }
356
- end
357
-
358
- params = {
359
- "method" => :post,
360
- "command" => "/vdc/#{vdc}/action/instantiateVAppTemplate"
361
- }
362
-
363
- response, headers = send_request(params, builder.to_xml, "application/vnd.vmware.vcloud.instantiateVAppTemplateParams+xml")
364
-
365
- vapp_id = headers[:location].gsub("#{@api_url}/vApp/vapp-", "")
366
-
367
- task = response.css("VApp Task[operationName='vdcInstantiateVapp']").first
368
- task_id = task["href"].gsub("#{@api_url}/task/", "")
369
-
370
- { :vapp_id => vapp_id, :task_id => task_id }
371
- end
372
-
373
- ##
374
- # Compose a vapp using existing virtual machines
375
- #
376
- # Params:
377
- # - vdc: the associated VDC
378
- # - vapp_name: name of the target vapp
379
- # - vapp_description: description of the target vapp
380
- # - vm_list: hash with IDs of the VMs to be used in the composing process
381
- # - network_config: hash of the network configuration for the vapp
382
- def compose_vapp_from_vm(vdc, vapp_name, vapp_description, vm_list={}, network_config={})
383
- builder = Nokogiri::XML::Builder.new do |xml|
384
- xml.ComposeVAppParams(
385
- "xmlns" => "http://www.vmware.com/vcloud/v1.5",
386
- "xmlns:ovf" => "http://schemas.dmtf.org/ovf/envelope/1",
387
- "name" => vapp_name) {
388
- xml.Description vapp_description
389
- xml.InstantiationParams {
390
- xml.NetworkConfigSection {
391
- xml['ovf'].Info "Configuration parameters for logical networks"
392
- xml.NetworkConfig("networkName" => network_config[:name]) {
393
- xml.Configuration {
394
- xml.IpScopes {
395
- xml.IpScope {
396
- xml.IsInherited(network_config[:is_inherited] || "false")
397
- xml.Gateway network_config[:gateway]
398
- xml.Netmask network_config[:netmask]
399
- xml.Dns1 network_config[:dns1] if network_config[:dns1]
400
- xml.Dns2 network_config[:dns2] if network_config[:dns2]
401
- xml.DnsSuffix network_config[:dns_suffix] if network_config[:dns_suffix]
402
- xml.IpRanges {
403
- xml.IpRange {
404
- xml.StartAddress network_config[:start_address]
405
- xml.EndAddress network_config[:end_address]
406
- }
407
- }
408
- }
409
- }
410
- xml.ParentNetwork("href" => "#{@api_url}/network/#{network_config[:parent_network]}")
411
- xml.FenceMode network_config[:fence_mode]
412
-
413
- xml.Features {
414
- xml.FirewallService {
415
- xml.IsEnabled(network_config[:enable_firewall] || "false")
416
- }
417
- }
418
- }
419
- }
420
- }
421
- }
422
- vm_list.each do |vm_name, vm_id|
423
- xml.SourcedItem {
424
- xml.Source("href" => "#{@api_url}/vAppTemplate/vm-#{vm_id}", "name" => vm_name)
425
- xml.InstantiationParams {
426
- xml.NetworkConnectionSection(
427
- "xmlns:ovf" => "http://schemas.dmtf.org/ovf/envelope/1",
428
- "type" => "application/vnd.vmware.vcloud.networkConnectionSection+xml",
429
- "href" => "#{@api_url}/vAppTemplate/vm-#{vm_id}/networkConnectionSection/") {
430
- xml['ovf'].Info "Network config for sourced item"
431
- xml.PrimaryNetworkConnectionIndex "0"
432
- xml.NetworkConnection("network" => network_config[:name]) {
433
- xml.NetworkConnectionIndex "0"
434
- xml.IsConnected "true"
435
- xml.IpAddressAllocationMode(network_config[:ip_allocation_mode] || "POOL")
436
- }
437
- }
438
- }
439
- xml.NetworkAssignment("containerNetwork" => network_config[:name], "innerNetwork" => network_config[:name])
440
- }
441
- end
442
- xml.AllEULAsAccepted "true"
443
- }
444
- end
445
-
446
- params = {
447
- "method" => :post,
448
- "command" => "/vdc/#{vdc}/action/composeVApp"
449
- }
450
-
451
- response, headers = send_request(params, builder.to_xml, "application/vnd.vmware.vcloud.composeVAppParams+xml")
452
-
453
- vapp_id = headers[:location].gsub("#{@api_url}/vApp/vapp-", "")
454
-
455
- task = response.css("VApp Task[operationName='vdcComposeVapp']").first
456
- task_id = task["href"].gsub("#{@api_url}/task/", "")
457
-
458
- { :vapp_id => vapp_id, :task_id => task_id }
459
- end
460
-
461
- # Fetch details about a given vapp template:
462
- # - name
463
- # - description
464
- # - Children VMs:
465
- # -- ID
466
- def get_vapp_template(vAppId)
467
- params = {
468
- 'method' => :get,
469
- 'command' => "/vAppTemplate/vappTemplate-#{vAppId}"
470
- }
471
-
472
- response, headers = send_request(params)
473
-
474
- vapp_node = response.css('VAppTemplate').first
475
- if vapp_node
476
- name = vapp_node['name']
477
- status = convert_vapp_status(vapp_node['status'])
478
- end
479
-
480
- description = response.css("Description").first
481
- description = description.text unless description.nil?
482
-
483
- ip = response.css('IpAddress').first
484
- ip = ip.text unless ip.nil?
485
-
486
- vms = response.css('Children Vm')
487
- vms_hash = {}
488
-
489
- vms.each do |vm|
490
- vms_hash[vm['name']] = {
491
- :id => vm['href'].gsub("#{@api_url}/vAppTemplate/vm-", '')
492
- }
493
- end
494
-
495
- # TODO: EXPAND INFO FROM RESPONSE
496
- { :name => name, :description => description, :vms_hash => vms_hash }
497
- end
498
-
499
- ##
500
- # Set vApp port forwarding rules
501
- #
502
- # - vappid: id of the vapp to be modified
503
- # - network_name: name of the vapp network to be modified
504
- # - config: hash with network configuration specifications, must contain an array inside :nat_rules with the nat rules to be applied.
505
- def set_vapp_port_forwarding_rules(vappid, network_name, config={})
506
- builder = Nokogiri::XML::Builder.new do |xml|
507
- xml.NetworkConfigSection(
508
- "xmlns" => "http://www.vmware.com/vcloud/v1.5",
509
- "xmlns:ovf" => "http://schemas.dmtf.org/ovf/envelope/1") {
510
- xml['ovf'].Info "Network configuration"
511
- xml.NetworkConfig("networkName" => network_name) {
512
- xml.Configuration {
513
- xml.ParentNetwork("href" => "#{@api_url}/network/#{config[:parent_network]}")
514
- xml.FenceMode(config[:fence_mode] || 'isolated')
515
- xml.Features {
516
- xml.NatService {
517
- xml.IsEnabled "true"
518
- xml.NatType "portForwarding"
519
- xml.Policy(config[:nat_policy_type] || "allowTraffic")
520
- config[:nat_rules].each do |nat_rule|
521
- xml.NatRule {
522
- xml.VmRule {
523
- xml.ExternalPort nat_rule[:nat_external_port]
524
- xml.VAppScopedVmId nat_rule[:vm_scoped_local_id]
525
- xml.VmNicId(nat_rule[:nat_vmnic_id] || "0")
526
- xml.InternalPort nat_rule[:nat_internal_port]
527
- xml.Protocol(nat_rule[:nat_protocol] || "TCP")
528
- }
529
- }
530
- end
531
- }
532
- }
533
- }
534
- }
535
- }
536
- end
537
-
538
- params = {
539
- 'method' => :put,
540
- 'command' => "/vApp/vapp-#{vappid}/networkConfigSection"
541
- }
542
-
543
- response, headers = send_request(params, builder.to_xml, "application/vnd.vmware.vcloud.networkConfigSection+xml")
544
-
545
- task_id = headers[:location].gsub("#{@api_url}/task/", "")
546
- task_id
547
- end
548
-
549
- ##
550
- # Get vApp port forwarding rules
551
- #
552
- # - vappid: id of the vApp
553
- def get_vapp_port_forwarding_rules(vAppId)
554
- params = {
555
- 'method' => :get,
556
- 'command' => "/vApp/vapp-#{vAppId}/networkConfigSection"
557
- }
558
-
559
- response, headers = send_request(params)
560
-
561
- # FIXME: this will return nil if the vApp uses multiple vApp Networks
562
- # with Edge devices in natRouted/portForwarding mode.
563
- config = response.css('NetworkConfigSection/NetworkConfig/Configuration')
564
- fenceMode = config.css('/FenceMode').text
565
- natType = config.css('/Features/NatService/NatType').text
566
-
567
- raise InvalidStateError, "Invalid request because FenceMode must be set to natRouted." unless fenceMode == "natRouted"
568
- raise InvalidStateError, "Invalid request because NatType must be set to portForwarding." unless natType == "portForwarding"
569
-
570
- nat_rules = {}
571
- config.css('/Features/NatService/NatRule').each do |rule|
572
- # portforwarding rules information
573
- ruleId = rule.css('Id').text
574
- vmRule = rule.css('VmRule')
575
-
576
- nat_rules[rule.css('Id').text] = {
577
- :ExternalIpAddress => vmRule.css('ExternalIpAddress').text,
578
- :ExternalPort => vmRule.css('ExternalPort').text,
579
- :VAppScopedVmId => vmRule.css('VAppScopedVmId').text,
580
- :VmNicId => vmRule.css('VmNicId').text,
581
- :InternalPort => vmRule.css('InternalPort').text,
582
- :Protocol => vmRule.css('Protocol').text
583
- }
584
- end
585
- nat_rules
586
- end
587
- ##
588
- # get vApp edge public IP from the vApp ID
589
- # Only works when:
590
- # - vApp needs to be poweredOn
591
- # - FenceMode is set to "natRouted"
592
- # - NatType" is set to "portForwarding
593
- # This will be required to know how to connect to VMs behind the Edge device.
594
- def get_vapp_edge_public_ip(vAppId)
595
- # Check the network configuration section
596
- params = {
597
- 'method' => :get,
598
- 'command' => "/vApp/vapp-#{vAppId}/networkConfigSection"
599
- }
600
-
601
- response, headers = send_request(params)
602
-
603
- # FIXME: this will return nil if the vApp uses multiple vApp Networks
604
- # with Edge devices in natRouted/portForwarding mode.
605
- config = response.css('NetworkConfigSection/NetworkConfig/Configuration')
606
-
607
- fenceMode = config.css('/FenceMode').text
608
- natType = config.css('/Features/NatService/NatType').text
609
-
610
- raise InvalidStateError, "Invalid request because FenceMode must be set to natRouted." unless fenceMode == "natRouted"
611
- raise InvalidStateError, "Invalid request because NatType must be set to portForwarding." unless natType == "portForwarding"
612
-
613
- # Check the routerInfo configuration where the global external IP is defined
614
- edgeIp = config.css('/RouterInfo/ExternalIp')
615
- edgeIp = edgeIp.text unless edgeIp.nil?
616
- end
617
-
618
- ##
619
- # Upload an OVF package
620
- # - vdcId
621
- # - vappName
622
- # - vappDescription
623
- # - ovfFile
624
- # - catalogId
625
- # - uploadOptions {}
626
- def upload_ovf(vdcId, vappName, vappDescription, ovfFile, catalogId, uploadOptions={})
627
- builder = Nokogiri::XML::Builder.new do |xml|
628
- xml.UploadVAppTemplateParams(
629
- "xmlns" => "http://www.vmware.com/vcloud/v1.5",
630
- "xmlns:ovf" => "http://schemas.dmtf.org/ovf/envelope/1",
631
- "manifestRequired" => "true",
632
- "name" => vappName) {
633
- xml.Description vappDescription
634
- }
635
- end
636
-
637
- params = {
638
- 'method' => :post,
639
- 'command' => "/vdc/#{vdcId}/action/uploadVAppTemplate"
640
- }
641
-
642
- response, headers = send_request(params, builder.to_xml,
643
- "application/vnd.vmware.vcloud.uploadVAppTemplateParams+xml")
644
-
645
- # Get vAppTemplate Link from location
646
- vAppTemplate = headers[:location].gsub("#{@api_url}/vAppTemplate/vappTemplate-", "")
647
- descriptorUpload = response.css("Files Link [rel='upload:default']").first[:href].gsub("#{@host_url}/transfer/", "")
648
- transferGUID = descriptorUpload.gsub("/descriptor.ovf", "")
649
-
650
- ovfFileBasename = File.basename(ovfFile, ".ovf")
651
- ovfDir = File.dirname(ovfFile)
652
-
653
- # Send OVF Descriptor
654
- uploadURL = "/transfer/#{descriptorUpload}"
655
- uploadFile = "#{ovfDir}/#{ovfFileBasename}.ovf"
656
- upload_file(uploadURL, uploadFile, vAppTemplate, uploadOptions)
657
-
658
- # Begin the catch for upload interruption
659
- begin
660
- params = {
661
- 'method' => :get,
662
- 'command' => "/vAppTemplate/vappTemplate-#{vAppTemplate}"
663
- }
664
-
665
- # Loop to wait for the upload links to show up in the vAppTemplate we just created
666
- while true
667
- response, headers = send_request(params)
668
- break unless response.css("Files Link [rel='upload:default']").count == 1
669
- sleep 1
670
- end
671
-
672
- # Send Manifest
673
- uploadURL = "/transfer/#{transferGUID}/descriptor.mf"
674
- uploadFile = "#{ovfDir}/#{ovfFileBasename}.mf"
675
- upload_file(uploadURL, uploadFile, vAppTemplate, uploadOptions)
676
-
677
- # Start uploading OVF VMDK files
678
- params = {
679
- 'method' => :get,
680
- 'command' => "/vAppTemplate/vappTemplate-#{vAppTemplate}"
681
- }
682
- response, headers = send_request(params)
683
- response.css("Files File [bytesTransferred='0'] Link [rel='upload:default']").each do |file|
684
- fileName = file[:href].gsub("#{@host_url}/transfer/#{transferGUID}/","")
685
- uploadFile = "#{ovfDir}/#{fileName}"
686
- uploadURL = "/transfer/#{transferGUID}/#{fileName}"
687
- upload_file(uploadURL, uploadFile, vAppTemplate, uploadOptions)
688
- end
689
-
690
- # Add item to the catalog catalogId
691
- builder = Nokogiri::XML::Builder.new do |xml|
692
- xml.CatalogItem(
693
- "xmlns" => "http://www.vmware.com/vcloud/v1.5",
694
- "type" => "application/vnd.vmware.vcloud.catalogItem+xml",
695
- "name" => vappName) {
696
- xml.Description vappDescription
697
- xml.Entity(
698
- "href" => "#{@api_url}/vAppTemplate/vappTemplate-#{vAppTemplate}"
699
- )
700
- }
701
- end
702
-
703
- params = {
704
- 'method' => :post,
705
- 'command' => "/catalog/#{catalogId}/catalogItems"
706
- }
707
-
708
- response, headers = send_request(params, builder.to_xml,
709
- "application/vnd.vmware.vcloud.catalogItem+xml")
710
-
711
- rescue Exception => e
712
- puts "Exception detected: #{e.message}."
713
- puts "Aborting task..."
714
-
715
- # Get vAppTemplate Task
716
- params = {
717
- 'method' => :get,
718
- 'command' => "/vAppTemplate/vappTemplate-#{vAppTemplate}"
719
- }
720
- response, headers = send_request(params)
721
-
722
- # Cancel Task
723
- cancelHook = response.css("Tasks Task Link [rel='task:cancel']").first[:href].gsub("#{@api_url}","")
724
- params = {
725
- 'method' => :post,
726
- 'command' => cancelHook
727
- }
728
- response, headers = send_request(params)
729
- raise
730
- end
85
+ # reset auth key to nil
86
+ @auth_key = nil
731
87
  end
732
88
 
733
89
  ##
@@ -751,145 +107,22 @@ module VCloudClient
751
107
  ##
752
108
  # Poll a given task until completion
753
109
  def wait_task_completion(taskid)
754
- status, errormsg, start_time, end_time, response = nil
110
+ errormsg = nil
111
+ task = {}
112
+
755
113
  loop do
756
114
  task = get_task(taskid)
757
115
  break if task[:status] != 'running'
758
116
  sleep 1
759
117
  end
760
118
 
761
- if status == 'error'
762
- errormsg = response.css("Error").first
119
+ if task[:status] == 'error'
120
+ errormsg = task[:response].css("Error").first
763
121
  errormsg = "Error code #{errormsg['majorErrorCode']} - #{errormsg['message']}"
764
122
  end
765
123
 
766
- { :status => status, :errormsg => errormsg, :start_time => start_time, :end_time => end_time }
767
- end
768
-
769
- ##
770
- # Set vApp Network Config
771
- def set_vapp_network_config(vappid, network_name, config={})
772
- builder = Nokogiri::XML::Builder.new do |xml|
773
- xml.NetworkConfigSection(
774
- "xmlns" => "http://www.vmware.com/vcloud/v1.5",
775
- "xmlns:ovf" => "http://schemas.dmtf.org/ovf/envelope/1") {
776
- xml['ovf'].Info "Network configuration"
777
- xml.NetworkConfig("networkName" => network_name) {
778
- xml.Configuration {
779
- xml.FenceMode(config[:fence_mode] || 'isolated')
780
- xml.RetainNetInfoAcrossDeployments(config[:retain_net] || false)
781
- xml.ParentNetwork("href" => config[:parent_network])
782
- }
783
- }
784
- }
785
- end
786
-
787
- params = {
788
- 'method' => :put,
789
- 'command' => "/vApp/vapp-#{vappid}/networkConfigSection"
790
- }
791
-
792
- response, headers = send_request(params, builder.to_xml, "application/vnd.vmware.vcloud.networkConfigSection+xml")
793
-
794
- task_id = headers[:location].gsub("#{@api_url}/task/", "")
795
- task_id
796
- end
797
-
798
- ##
799
- # Set VM Network Config
800
- def set_vm_network_config(vmid, network_name, config={})
801
- builder = Nokogiri::XML::Builder.new do |xml|
802
- xml.NetworkConnectionSection(
803
- "xmlns" => "http://www.vmware.com/vcloud/v1.5",
804
- "xmlns:ovf" => "http://schemas.dmtf.org/ovf/envelope/1") {
805
- xml['ovf'].Info "VM Network configuration"
806
- xml.PrimaryNetworkConnectionIndex(config[:primary_index] || 0)
807
- xml.NetworkConnection("network" => network_name, "needsCustomization" => true) {
808
- xml.NetworkConnectionIndex(config[:network_index] || 0)
809
- xml.IpAddress config[:ip] if config[:ip]
810
- xml.IsConnected(config[:is_connected] || true)
811
- xml.IpAddressAllocationMode config[:ip_allocation_mode] if config[:ip_allocation_mode]
812
- }
813
- }
814
- end
815
-
816
- params = {
817
- 'method' => :put,
818
- 'command' => "/vApp/vm-#{vmid}/networkConnectionSection"
819
- }
820
-
821
- response, headers = send_request(params, builder.to_xml, "application/vnd.vmware.vcloud.networkConnectionSection+xml")
822
-
823
- task_id = headers[:location].gsub("#{@api_url}/task/", "")
824
- task_id
825
- end
826
-
827
-
828
- ##
829
- # Set VM Guest Customization Config
830
- def set_vm_guest_customization(vmid, computer_name, config={})
831
- builder = Nokogiri::XML::Builder.new do |xml|
832
- xml.GuestCustomizationSection(
833
- "xmlns" => "http://www.vmware.com/vcloud/v1.5",
834
- "xmlns:ovf" => "http://schemas.dmtf.org/ovf/envelope/1") {
835
- xml['ovf'].Info "VM Guest Customization configuration"
836
- xml.Enabled config[:enabled] if config[:enabled]
837
- xml.AdminPasswordEnabled config[:admin_passwd_enabled] if config[:admin_passwd_enabled]
838
- xml.AdminPassword config[:admin_passwd] if config[:admin_passwd]
839
- xml.ComputerName computer_name
840
- }
841
- end
842
-
843
- params = {
844
- 'method' => :put,
845
- 'command' => "/vApp/vm-#{vmid}/guestCustomizationSection"
846
- }
847
-
848
- response, headers = send_request(params, builder.to_xml, "application/vnd.vmware.vcloud.guestCustomizationSection+xml")
849
-
850
- task_id = headers[:location].gsub("#{@api_url}/task/", "")
851
- task_id
852
- end
853
-
854
- ##
855
- # Fetch details about a given VM
856
- def get_vm(vmId)
857
- params = {
858
- 'method' => :get,
859
- 'command' => "/vApp/vm-#{vmId}"
860
- }
861
-
862
- response, headers = send_request(params)
863
-
864
- os_desc = response.css('ovf|OperatingSystemSection ovf|Description').first.text
865
-
866
- networks = {}
867
- response.css('NetworkConnection').each do |network|
868
- ip = network.css('IpAddress').first
869
- ip = ip.text if ip
870
-
871
- networks[network['network']] = {
872
- :index => network.css('NetworkConnectionIndex').first.text,
873
- :ip => ip,
874
- :is_connected => network.css('IsConnected').first.text,
875
- :mac_address => network.css('MACAddress').first.text,
876
- :ip_allocation_mode => network.css('IpAddressAllocationMode').first.text
877
- }
878
- end
879
-
880
- admin_password = response.css('GuestCustomizationSection AdminPassword').first
881
- admin_password = admin_password.text if admin_password
882
-
883
- guest_customizations = {
884
- :enabled => response.css('GuestCustomizationSection Enabled').first.text,
885
- :admin_passwd_enabled => response.css('GuestCustomizationSection AdminPasswordEnabled').first.text,
886
- :admin_passwd_auto => response.css('GuestCustomizationSection AdminPasswordAuto').first.text,
887
- :admin_passwd => admin_password,
888
- :reset_passwd_required => response.css('GuestCustomizationSection ResetPasswordRequired').first.text,
889
- :computer_name => response.css('GuestCustomizationSection ComputerName').first.text
890
- }
891
-
892
- { :os_desc => os_desc, :networks => networks, :guest_customizations => guest_customizations }
124
+ { :status => task[:status], :errormsg => errormsg,
125
+ :start_time => task[:start_time], :end_time => task[:end_time] }
893
126
  end
894
127
 
895
128
  private
@@ -912,33 +145,21 @@ module VCloudClient
912
145
  :url => "#{@api_url}#{params['command']}",
913
146
  :payload => payload)
914
147
 
915
-
916
148
  begin
917
149
  response = request.execute
918
150
  if ![200, 201, 202, 204].include?(response.code)
919
- puts "Warning: unattended code #{response.code}"
151
+ @logger.warn "Warning: unattended code #{response.code}"
920
152
  end
921
153
 
922
- # TODO: handle asynch properly, see TasksList
154
+ @logger.debug "Send request result: #{Nokogiri.parse(response)}"
155
+
923
156
  [Nokogiri.parse(response), response.headers]
924
157
  rescue RestClient::Unauthorized => e
925
158
  raise UnauthorizedAccess, "Client not authorized. Please check your credentials."
926
159
  rescue RestClient::BadRequest => e
927
160
  body = Nokogiri.parse(e.http_body)
928
161
  message = body.css("Error").first["message"]
929
-
930
- case message
931
- when /The request has invalid accept header/
932
- raise WrongAPIVersion, "Invalid accept header. Please verify that the server supports v.#{@api_version} or specify a different API Version."
933
- when /validation error on field 'id': String value has invalid format or length/
934
- raise WrongItemIDError, "Invalid ID specified. Please verify that the item exists and correctly typed."
935
- when /The requested operation could not be executed on vApp "(.*)". Stop the vApp and try again/
936
- raise InvalidStateError, "Invalid request because vApp is running. Stop vApp '#{$1}' and try again."
937
- when /The requested operation could not be executed since vApp "(.*)" is not running/
938
- raise InvalidStateError, "Invalid request because vApp is stopped. Start vApp '#{$1}' and try again."
939
- else
940
- raise UnhandledError, "BadRequest - unhandled error: #{message}.\nPlease report this issue."
941
- end
162
+ humanize_badrequest(message)
942
163
  rescue RestClient::Forbidden => e
943
164
  body = Nokogiri.parse(e.http_body)
944
165
  message = body.css("Error").first["message"]
@@ -947,94 +168,13 @@ module VCloudClient
947
168
  body = Nokogiri.parse(e.http_body)
948
169
  message = body.css("Error").first["message"]
949
170
  raise InternalServerError, "Internal Server Error: #{message}."
171
+ rescue RestClient::MethodNotAllowed => e
172
+ body = Nokogiri.parse(e.http_body)
173
+ message = body.css("Error").first["message"]
174
+ raise MethodNotAllowed, "#{params['method']} to #{params['command']} not allowed: #{message}."
950
175
  end
951
176
  end
952
177
 
953
- ##
954
- # Upload a large file in configurable chunks, output an optional progressbar
955
- def upload_file(uploadURL, uploadFile, vAppTemplate, config={})
956
-
957
- # Set chunksize to 10M if not specified otherwise
958
- chunkSize = (config[:chunksize] || 10485760)
959
-
960
- # Set progress bar to default format if not specified otherwise
961
- progressBarFormat = (config[:progressbar_format] || "%e <%B> %p%% %t")
962
-
963
- # Set progress bar length to 120 if not specified otherwise
964
- progressBarLength = (config[:progressbar_length] || 120)
965
-
966
- # Open our file for upload
967
- uploadFileHandle = File.new(uploadFile, "rb" )
968
- fileName = File.basename(uploadFileHandle)
969
-
970
- progressBarTitle = "Uploading: " + uploadFile.to_s
971
-
972
- # Create a progressbar object if progress bar is enabled
973
- if config[:progressbar_enable] == true && uploadFileHandle.size.to_i > chunkSize
974
- progressbar = ProgressBar.create(:title => progressBarTitle, :starting_at => 0, :total => uploadFileHandle.size.to_i, :length => progressBarLength, :format => progressBarFormat)
975
- else
976
- puts progressBarTitle
977
- end
978
- # Create a new HTTP client
979
- clnt = HTTPClient.new
980
-
981
- # Disable SSL cert verification
982
- clnt.ssl_config.verify_mode=(OpenSSL::SSL::VERIFY_NONE)
983
-
984
- # Suppress SSL depth message
985
- clnt.ssl_config.verify_callback=proc{ |ok, ctx|; true };
986
-
987
- # Perform ranged upload until the file reaches its end
988
- until uploadFileHandle.eof?
989
-
990
- # Create ranges for this chunk upload
991
- rangeStart = uploadFileHandle.pos
992
- rangeStop = uploadFileHandle.pos.to_i + chunkSize
993
-
994
- # Read current chunk
995
- fileContent = uploadFileHandle.read(chunkSize)
996
-
997
- # If statement to handle last chunk transfer if is > than filesize
998
- if rangeStop.to_i > uploadFileHandle.size.to_i
999
- contentRange = "bytes " + rangeStart.to_s + "-" + uploadFileHandle.size.to_s + "/" + uploadFileHandle.size.to_s
1000
- rangeLen = uploadFileHandle.size.to_i - rangeStart.to_i
1001
- else
1002
- contentRange = "bytes " + rangeStart.to_s + "-" + rangeStop.to_s + "/" + uploadFileHandle.size.to_s
1003
- rangeLen = rangeStop.to_i - rangeStart.to_i
1004
- end
1005
-
1006
- # Build headers
1007
- extheader = {
1008
- 'x-vcloud-authorization' => @auth_key,
1009
- 'Content-Range' => contentRange,
1010
- 'Content-Length' => rangeLen.to_s
1011
- }
1012
-
1013
- begin
1014
- uploadRequest = "#{@host_url}#{uploadURL}"
1015
- connection = clnt.request('PUT', uploadRequest, nil, fileContent, extheader)
1016
- if config[:progressbar_enable] == true && uploadFileHandle.size.to_i > chunkSize
1017
- params = {
1018
- 'method' => :get,
1019
- 'command' => "/vAppTemplate/vappTemplate-#{vAppTemplate}"
1020
- }
1021
- response, headers = send_request(params)
1022
-
1023
- response.css("Files File [name='#{fileName}']").each do |file|
1024
- progressbar.progress=file[:bytesTransferred].to_i
1025
- end
1026
- end
1027
- rescue
1028
- retryTime = (config[:retry_time] || 5)
1029
- puts "Range #{contentRange} failed to upload, retrying the chunk in #{retryTime.to_s} seconds, to stop the action press CTRL+C."
1030
- sleep retryTime.to_i
1031
- retry
1032
- end
1033
- end
1034
- uploadFileHandle.close
1035
- end
1036
-
1037
-
1038
178
  ##
1039
179
  # Convert vApp status codes into human readable description
1040
180
  def convert_vapp_status(status_code)
@@ -1053,5 +193,47 @@ module VCloudClient
1053
193
  "Unknown #{status_code}"
1054
194
  end
1055
195
  end
196
+
197
+ def init_logger
198
+ level = if ENV["VCLOUD_REST_DEBUG_LEVEL"]
199
+ Logger::Severity.constants.find_index ENV["VCLOUD_REST_DEBUG_LEVEL"].upcase.to_sym
200
+ else
201
+ Logger::WARN
202
+ end
203
+ @logger = Logger.new(ENV["VCLOUD_REST_LOG_FILE"] || STDOUT)
204
+ @logger.level = level
205
+ end
206
+
207
+ def humanize_badrequest(message)
208
+ case message
209
+ when /The request has invalid accept header/
210
+ raise WrongAPIVersion, "Invalid accept header. Please verify that the server supports v.#{@api_version} or specify a different API Version."
211
+ when /validation error on field 'id': String value has invalid format or length/
212
+ raise WrongItemIDError, "Invalid ID specified. Please verify that the item exists and correctly typed."
213
+ when /The requested operation could not be executed on vApp "(.*)". Stop the vApp and try again/
214
+ raise InvalidStateError, "Invalid request because vApp is running. Stop vApp '#{$1}' and try again."
215
+ when /The requested operation could not be executed since vApp "(.*)" is not running/
216
+ raise InvalidStateError, "Invalid request because vApp is stopped. Start vApp '#{$1}' and try again."
217
+ else
218
+ raise UnhandledError, "BadRequest - unhandled error: #{message}.\nPlease report this issue."
219
+ end
220
+ end
221
+
222
+ ##
223
+ # Generic method to send power actions to vApp/VM
224
+ #
225
+ # i.e., 'suspend', 'powerOn'
226
+ def power_action(id, action, type=:vapp)
227
+ target = "#{type}-#{id}"
228
+
229
+ params = {
230
+ 'method' => :post,
231
+ 'command' => "/vApp/#{target}/power/action/#{action}"
232
+ }
233
+
234
+ response, headers = send_request(params)
235
+ task_id = headers[:location].gsub(/.*\/task\//, "")
236
+ task_id
237
+ end
1056
238
  end # class
1057
239
  end