vcloud-rest 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: dbde7288973c310f0a12f5b1bbd8980a25af4397
4
- data.tar.gz: 75c6edc2047b5ce235e9951bf6f723aabd0f928f
3
+ metadata.gz: 2c88e00c344ec71b95abf4340a2a1401d6617747
4
+ data.tar.gz: 07d79063ef3ab8906b30196c47758471c1473d88
5
5
  SHA512:
6
- metadata.gz: ce2ca0f2908846add8a5beaf12c23e0722d9cd4d3fd8433225e0247249a6f232d887c441df21064566abe1bcc08bb6006a403f1982fc19e9ccee19f30f5cc063
7
- data.tar.gz: e898766c0f2d3f0b85b0f2b343401cfa18c925983a753124e1a0c878c204322cb6dc802ca63c49c0a05188c58e2f819f83d3d7a1eca82568491fe24e6c700528
6
+ metadata.gz: bdd41a731e6204c0b1279904412dc5546bb57540e8c43b646274e44c3f17f84359d90665d122bb3f8c43c154f5459cbb7e3b4d5006917b0bc0830c4f0d3ba7e9
7
+ data.tar.gz: 144ce4ac54ef6b04b072306a86e9ea724091f419be29c2f8b6c681248c1ee264f214641decf658200c3d71ced465d5a37dbfe929c407cd019a269e954fb871c9
data/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  Changes
2
2
  ==
3
+ 2013-07-25 (0.3.0)
4
+
5
+ FEATURES:
6
+
7
+ * Add ```compose_vapp_from_vm``` to compose a vapp using VMs in a catalog
8
+ * Add ```get_vapp_template``` to get information on VMs inside a vapp template
9
+ * Add ```set_vapp_port_forwarding_rules``` to set NAT port forwarding rules in an existing vapp
10
+ * ```set_vapp_network_config```: add parent network specification
11
+ * ```get_vapp```: ```vms_hash``` now contains also ```vapp_scoped_local_id```
12
+ * Add ```get_vapp_edge_public_ip``` to fetch the public IP of a vApp (vShield Edge)
13
+ * Add ```get_vapp_port_forwarding_rules``` to return vApp portforwarding rules
14
+ * Add ``reboot_vapp/suspend_vapp/reset_vapp``
15
+ * Add ```upload_ovf``` to upload an OVF Package
16
+
17
+ CHANGES:
18
+
19
+ * ```RetainNetInfoAcrossDeployments``` now defaults to false (fenced deployments)
20
+
21
+ FIXES:
22
+
23
+ * Better handling of 500 errors
24
+
25
+ REMARKS:
26
+ A big thanks to Fabio Rapposelli and Timo Sugliani for the great work done!
27
+
3
28
  2013-05-13 (0.2.2)
4
29
  --
5
30
 
data/README.md CHANGED
@@ -19,7 +19,7 @@ This plugin is distributed as a Ruby Gem. To install it, run:
19
19
 
20
20
  Depending on your system's configuration, you may need to run this command with root privileges.
21
21
 
22
- vcloud-rest is tested against ruby 1.9.x and 1.8.7+.
22
+ vcloud-rest is tested against ruby 2.0.0, 1.9.x and 1.8.7+.
23
23
 
24
24
  FEATURES
25
25
  --
@@ -33,6 +33,9 @@ FEATURES
33
33
  - basic vApp network configuration
34
34
  - basic VM network configuration
35
35
  - basic VM Guest Customization configuration
36
+ - basic vApp compose capabilities
37
+ - basic vApp NAT port forwarding creation
38
+ - Catalog item upload with byterange upload and retry capabilities
36
39
 
37
40
  TODO
38
41
  --
@@ -41,8 +44,10 @@ TODO
41
44
 
42
45
  PREREQUISITES
43
46
  --
44
- - nokogiri ~> 1.5.5
47
+ - nokogiri ~> 1.6.0
45
48
  - rest-client ~> 1.6.7
49
+ - httpclient ~> 2.3.3
50
+ - ruby-progressbar ~> 1.1.1
46
51
 
47
52
  For testing purpose:
48
53
  - minitest (included in ruby 1.9)
@@ -56,6 +61,12 @@ USAGE
56
61
  conn.login
57
62
  conn.list_organizations
58
63
 
64
+ EXAMPLE
65
+ --
66
+ A (mostly complete) example can be found in
67
+
68
+ examples/example.rb
69
+
59
70
  TESTING
60
71
  --
61
72
  Simply run:
@@ -72,7 +83,7 @@ LICENSE
72
83
 
73
84
  Author:: Stefano Tortarolo <stefano.tortarolo@gmail.com>
74
85
 
75
- Copyright:: Copyright (c) 2012
86
+ Copyright:: Copyright (c) 2012-2013
76
87
  License:: Apache License, Version 2.0
77
88
 
78
89
  Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,14 +18,18 @@
18
18
 
19
19
  require 'rest-client'
20
20
  require 'nokogiri'
21
+ require 'httpclient'
22
+ require 'ruby-progressbar'
21
23
 
22
24
  module VCloudClient
23
25
  class UnauthorizedAccess < StandardError; end
24
26
  class WrongAPIVersion < StandardError; end
25
27
  class WrongItemIDError < StandardError; end
26
28
  class InvalidStateError < StandardError; end
29
+ class InternalServerError < StandardError; end
27
30
  class UnhandledError < StandardError; end
28
31
 
32
+
29
33
  # Main class to access vCloud rest APIs
30
34
  class Connection
31
35
  attr_reader :api_url, :auth_key
@@ -33,6 +37,7 @@ module VCloudClient
33
37
  def initialize(host, username, password, org_name, api_version)
34
38
  @host = host
35
39
  @api_url = "#{host}/api"
40
+ @host_url = "#{host}"
36
41
  @username = username
37
42
  @password = password
38
43
  @org_name = org_name
@@ -68,8 +73,8 @@ module VCloudClient
68
73
  end
69
74
 
70
75
  ##
71
- # List existing organizations and their IDs
72
- def list_organizations
76
+ # Fetch existing organizations and their IDs
77
+ def get_organizations
73
78
  params = {
74
79
  'method' => :get,
75
80
  'command' => '/org'
@@ -86,11 +91,11 @@ module VCloudClient
86
91
  end
87
92
 
88
93
  ##
89
- # Show details about an organization:
94
+ # Fetch details about an organization:
90
95
  # - catalogs
91
96
  # - vdcs
92
97
  # - networks
93
- def show_organization(orgId)
98
+ def get_organization(orgId)
94
99
  params = {
95
100
  'method' => :get,
96
101
  'command' => "/org/#{orgId}"
@@ -117,12 +122,12 @@ module VCloudClient
117
122
  tasklists[item['name']] = item['href'].gsub("#{@api_url}/tasksList/", "")
118
123
  end
119
124
 
120
- [catalogs, vdcs, networks, tasklists]
125
+ { :catalogs => catalogs, :vdcs => vdcs, :networks => networks, :tasklists => tasklists }
121
126
  end
122
127
 
123
128
  ##
124
- # Show details about a given catalog
125
- def show_catalog(catalogId)
129
+ # Fetch details about a given catalog
130
+ def get_catalog(catalogId)
126
131
  params = {
127
132
  'method' => :get,
128
133
  'command' => "/catalog/#{catalogId}"
@@ -136,16 +141,15 @@ module VCloudClient
136
141
  response.css("CatalogItem[type='application/vnd.vmware.vcloud.catalogItem+xml']").each do |item|
137
142
  items[item['name']] = item['href'].gsub("#{@api_url}/catalogItem/", "")
138
143
  end
139
-
140
- [description, items]
144
+ { :description => description, :items => items }
141
145
  end
142
146
 
143
147
  ##
144
- # Show details about a given vdc:
148
+ # Fetch details about a given vdc:
145
149
  # - description
146
150
  # - vapps
147
151
  # - networks
148
- def show_vdc(vdcId)
152
+ def get_vdc(vdcId)
149
153
  params = {
150
154
  'method' => :get,
151
155
  'command' => "/vdc/#{vdcId}"
@@ -164,15 +168,14 @@ module VCloudClient
164
168
  response.css("Network[type='application/vnd.vmware.vcloud.network+xml']").each do |item|
165
169
  networks[item['name']] = item['href'].gsub("#{@api_url}/network/", "")
166
170
  end
167
-
168
- [description, vapps, networks]
171
+ { :description => description, :vapps => vapps, :networks => networks }
169
172
  end
170
173
 
171
174
  ##
172
- # Show details about a given catalog item:
175
+ # Fetch details about a given catalog item:
173
176
  # - description
174
177
  # - vApp templates
175
- def show_catalog_item(catalogItemId)
178
+ def get_catalog_item(catalogItemId)
176
179
  params = {
177
180
  'method' => :get,
178
181
  'command' => "/catalogItem/#{catalogItemId}"
@@ -186,12 +189,11 @@ module VCloudClient
186
189
  response.css("Entity[type='application/vnd.vmware.vcloud.vAppTemplate+xml']").each do |item|
187
190
  items[item['name']] = item['href'].gsub("#{@api_url}/vAppTemplate/vappTemplate-", "")
188
191
  end
189
-
190
- [description, items]
192
+ { :description => description, :items => items }
191
193
  end
192
194
 
193
195
  ##
194
- # Show details about a given vapp:
196
+ # Fetch details about a given vapp:
195
197
  # - name
196
198
  # - description
197
199
  # - status
@@ -200,7 +202,7 @@ module VCloudClient
200
202
  # -- IP addresses
201
203
  # -- status
202
204
  # -- ID
203
- def show_vapp(vAppId)
205
+ def get_vapp(vAppId)
204
206
  params = {
205
207
  'method' => :get,
206
208
  'command' => "/vApp/vapp-#{vAppId}"
@@ -225,15 +227,18 @@ module VCloudClient
225
227
 
226
228
  # ipAddress could be namespaced or not: see https://github.com/astratto/vcloud-rest/issues/3
227
229
  vms.each do |vm|
230
+ vapp_local_id = vm.css('VAppScopedLocalId')
228
231
  addresses = vm.css('rasd|Connection').collect{|n| n['vcloud:ipAddress'] || n['ipAddress'] }
229
- vms_hash[vm['name']] = {:addresses => addresses,
232
+ vms_hash[vm['name']] = {
233
+ :addresses => addresses,
230
234
  :status => convert_vapp_status(vm['status']),
231
- :id => vm['href'].gsub("#{@api_url}/vApp/vm-", '')
235
+ :id => vm['href'].gsub("#{@api_url}/vApp/vm-", ''),
236
+ :vapp_scoped_local_id => vapp_local_id.text
232
237
  }
233
238
  end
234
239
 
235
240
  # TODO: EXPAND INFO FROM RESPONSE
236
- [name, description, status, ip, vms_hash]
241
+ { :name => name, :description => description, :status => status, :ip => ip, :vms_hash => vms_hash }
237
242
  end
238
243
 
239
244
  ##
@@ -271,6 +276,50 @@ module VCloudClient
271
276
  task_id
272
277
  end
273
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
+
274
323
  ##
275
324
  # Boot a given vapp
276
325
  def poweron_vapp(vAppId)
@@ -318,12 +367,372 @@ module VCloudClient
318
367
  task = response.css("VApp Task[operationName='vdcInstantiateVapp']").first
319
368
  task_id = task["href"].gsub("#{@api_url}/task/", "")
320
369
 
321
- [vapp_id, task_id]
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
322
731
  end
323
732
 
324
733
  ##
325
- # Show a given task
326
- def show_task(taskid)
734
+ # Fetch information for a given task
735
+ def get_task(taskid)
327
736
  params = {
328
737
  'method' => :get,
329
738
  'command' => "/task/#{taskid}"
@@ -336,7 +745,7 @@ module VCloudClient
336
745
  start_time = task['startTime']
337
746
  end_time = task['endTime']
338
747
 
339
- [status, start_time, end_time, response]
748
+ { :status => status, :start_time => start_time, :end_time => end_time, :response => response }
340
749
  end
341
750
 
342
751
  ##
@@ -344,8 +753,8 @@ module VCloudClient
344
753
  def wait_task_completion(taskid)
345
754
  status, errormsg, start_time, end_time, response = nil
346
755
  loop do
347
- status, start_time, end_time, response = show_task(taskid)
348
- break if status != 'running'
756
+ task = get_task(taskid)
757
+ break if task[:status] != 'running'
349
758
  sleep 1
350
759
  end
351
760
 
@@ -354,7 +763,7 @@ module VCloudClient
354
763
  errormsg = "Error code #{errormsg['majorErrorCode']} - #{errormsg['message']}"
355
764
  end
356
765
 
357
- [status, errormsg, start_time, end_time]
766
+ { :status => status, :errormsg => errormsg, :start_time => start_time, :end_time => end_time }
358
767
  end
359
768
 
360
769
  ##
@@ -367,8 +776,9 @@ module VCloudClient
367
776
  xml['ovf'].Info "Network configuration"
368
777
  xml.NetworkConfig("networkName" => network_name) {
369
778
  xml.Configuration {
370
- xml.FenceMode (config[:fence_mode] || 'isolated')
371
- xml.RetainNetInfoAcrossDeployments (config[:retain_net] || true)
779
+ xml.FenceMode(config[:fence_mode] || 'isolated')
780
+ xml.RetainNetInfoAcrossDeployments(config[:retain_net] || false)
781
+ xml.ParentNetwork("href" => config[:parent_network])
372
782
  }
373
783
  }
374
784
  }
@@ -393,11 +803,11 @@ module VCloudClient
393
803
  "xmlns" => "http://www.vmware.com/vcloud/v1.5",
394
804
  "xmlns:ovf" => "http://schemas.dmtf.org/ovf/envelope/1") {
395
805
  xml['ovf'].Info "VM Network configuration"
396
- xml.PrimaryNetworkConnectionIndex (config[:primary_index] || 0)
806
+ xml.PrimaryNetworkConnectionIndex(config[:primary_index] || 0)
397
807
  xml.NetworkConnection("network" => network_name, "needsCustomization" => true) {
398
- xml.NetworkConnectionIndex (config[:network_index] || 0)
808
+ xml.NetworkConnectionIndex(config[:network_index] || 0)
399
809
  xml.IpAddress config[:ip] if config[:ip]
400
- xml.IsConnected (config[:is_connected] || true)
810
+ xml.IsConnected(config[:is_connected] || true)
401
811
  xml.IpAddressAllocationMode config[:ip_allocation_mode] if config[:ip_allocation_mode]
402
812
  }
403
813
  }
@@ -442,8 +852,8 @@ module VCloudClient
442
852
  end
443
853
 
444
854
  ##
445
- # Show details about a given VM
446
- def show_vm(vmId)
855
+ # Fetch details about a given VM
856
+ def get_vm(vmId)
447
857
  params = {
448
858
  'method' => :get,
449
859
  'command' => "/vApp/vm-#{vmId}"
@@ -479,7 +889,7 @@ module VCloudClient
479
889
  :computer_name => response.css('GuestCustomizationSection ComputerName').first.text
480
890
  }
481
891
 
482
- [os_desc, networks, guest_customizations]
892
+ { :os_desc => os_desc, :networks => networks, :guest_customizations => guest_customizations }
483
893
  end
484
894
 
485
895
  private
@@ -501,6 +911,8 @@ module VCloudClient
501
911
  :headers => headers,
502
912
  :url => "#{@api_url}#{params['command']}",
503
913
  :payload => payload)
914
+
915
+
504
916
  begin
505
917
  response = request.execute
506
918
  if ![200, 201, 202, 204].include?(response.code)
@@ -531,9 +943,98 @@ module VCloudClient
531
943
  body = Nokogiri.parse(e.http_body)
532
944
  message = body.css("Error").first["message"]
533
945
  raise UnauthorizedAccess, "Operation not permitted: #{message}."
946
+ rescue RestClient::InternalServerError => e
947
+ body = Nokogiri.parse(e.http_body)
948
+ message = body.css("Error").first["message"]
949
+ raise InternalServerError, "Internal Server Error: #{message}."
534
950
  end
535
951
  end
536
952
 
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
+
537
1038
  ##
538
1039
  # Convert vApp status codes into human readable description
539
1040
  def convert_vapp_status(status_code)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vcloud-rest
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefano Tortarolo
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-05-13 00:00:00.000000000 Z
11
+ date: 2013-07-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ~>
18
18
  - !ruby/object:Gem::Version
19
- version: 1.5.9
19
+ version: 1.6.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ~>
25
25
  - !ruby/object:Gem::Version
26
- version: 1.5.9
26
+ version: 1.6.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rest-client
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -38,6 +38,34 @@ dependencies:
38
38
  - - ~>
39
39
  - !ruby/object:Gem::Version
40
40
  version: 1.6.7
41
+ - !ruby/object:Gem::Dependency
42
+ name: httpclient
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: 2.3.3
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 2.3.3
55
+ - !ruby/object:Gem::Dependency
56
+ name: ruby-progressbar
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: 1.1.1
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: 1.1.1
41
69
  description: Ruby bindings to create, list and manage vCloud servers
42
70
  email:
43
71
  - stefano.tortarolo@gmail.com