vcloud-rest 0.3.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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