vcloud-rest 0.2.2 → 0.3.0

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