vcloud-rest 1.2.0 → 1.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: f5423c402bd67bd07893da3ed80210af1ae63b0c
4
- data.tar.gz: 389acab1aaec5d6cbf9733dc02d5143f8fcdaabf
3
+ metadata.gz: 470e97ca321d742340571fff5cbbaa6e5a8fbe2b
4
+ data.tar.gz: 0460be22738dca2e1b05668a861777d6380dd3d4
5
5
  SHA512:
6
- metadata.gz: 66eaeeec2baa31c8903f80ef68fd9315b260bcf72ea4180f014c118fa8cdaf1cff64f21f91af8aecb7b234534ac0f4bdc9a4726fa27feedd2e9f6e066bf76c47
7
- data.tar.gz: a45669ed115205cd51fda43166b99d64892834f9128c04a78e2bba7ca0fcfb31b08a902d10f12eb2f08b75f3d5d272ca2fef65e766b9f340d30bc26926911925
6
+ metadata.gz: acbace49f0444b3bff99e21ef01c9947597cba62adedf0b4385967627d4898cee59463cfbe1216f9abfdc75312344cd43146dd5608b48cc33eb894313f1e749c
7
+ data.tar.gz: 813bdcc2737e13da061b0b76d659fe0147275e8d61c80220106dca631dfa5688ae48bbc35e6510cc5016ae3c686b87afd9cccec543f0d62d7def955c88523596
data/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  Changes
2
2
  ==
3
+ 2014-06-03 (1.3.0)
4
+
5
+ Note that starting from this release Ruby 1.8.7+ and 1.9.2 are not explicitly tested against anymore.
6
+
7
+ FEATURES:
8
+
9
+ * Add "media" item type management to `get_catalog_item`
10
+ * Add `discard_suspend_state_[vapp|vm]` to discard suspended state of a vApp or VM
11
+ * Add independent disk capabilities
12
+ * `get_vapp` returns also network IDs
13
+ * Add `discard_[vm|vapp]_snapshot` to discard snapshots
14
+ * Add `acquire_ticket_vm` that retrieves a screen ticket (VMRC) for a given VM
15
+
16
+ FIXES:
17
+
18
+ * Fix add network to VM for API v.5.5
19
+ * Fix VM IP address retrieval with different interfaces on same network
20
+ * Don't use hardcoded rasd:Parent in `add_disk`
21
+
22
+ Note that now `get_vm` appends NetworkConnectionIndex to network name to generate a unique hash key.
23
+
3
24
  2014-02-06 (1.2.0)
4
25
 
5
26
  FEATURES:
data/README.md CHANGED
@@ -17,7 +17,7 @@ This plugin is distributed as a Ruby Gem. To install it, run:
17
17
 
18
18
  Depending on your system's configuration, you may need to run this command with root privileges.
19
19
 
20
- vcloud-rest is tested against ruby 2.1.0, 2.0.0, 1.9.x and 1.8.7+.
20
+ vcloud-rest is tested against ruby 2.1.2, 2.0.0, 1.9.3 and ruby-head.
21
21
 
22
22
  FEATURES
23
23
  --
@@ -49,10 +49,12 @@ TODO
49
49
 
50
50
  PREREQUISITES
51
51
  --
52
- - nokogiri ~> 1.6.0
53
- - rest-client ~> 1.6.7
54
- - httpclient ~> 2.3.3
55
- - ruby-progressbar ~> 1.1.1
52
+ - nokogiri
53
+ - rest-client
54
+ - httpclient
55
+ - ruby-progressbar
56
+
57
+ (see *vcloud_rest.gemspec* for details)
56
58
 
57
59
  For testing purpose:
58
60
  - minitest (included in ruby 1.9)
@@ -88,14 +90,55 @@ Or:
88
90
 
89
91
  ruby spec/connection_spec.rb
90
92
 
91
- Note: in order to run tests with ruby 1.8.7+ you need to export RUBYOPT="rubygems"
93
+ Note: in order to run tests with ruby 1.8.x you need to export RUBYOPT="rubygems"
94
+
95
+ ### Write new tests
96
+
97
+ Tests are now managed using VCR and thus real interactions are recorded and replayed.
98
+
99
+ In order to write new tests the following steps are required:
100
+
101
+ 1. create a file *test_credentials.yml*
102
+ 1. create a new test entry specifying a new VCR cassette *my_test_case.yml*
103
+ 1. review and anonymize data if necessary under *spec/fixtures/vcr_cassettes/my_test_case.yml*
104
+
105
+ **Note:** values in *test_credentials.yml* are automatically anonymized.
106
+
107
+ Examples:
108
+
109
+ => test_credentials.yml
110
+ :host: https://vcloud_instance_url
111
+ :username: test_username
112
+ :password: test_password
113
+ :org: test_organization
114
+
115
+
116
+ => Test entry in connection_spec.rb
117
+ it "should power off a given vapp" do
118
+ VCR.use_cassette('vapps/poweroff_vapp') do
119
+ connection.login
120
+ task_id = connection.poweroff_vapp("65b4dbc9-b0b1-46e4-a420-8f8147369f8b")
121
+ expect(task_id).to eq "ae791b59-4c9f-4fe2-9916-703f1fc3cbd5"
122
+ end
123
+ end
124
+
125
+ => Recorded fixture (credentials auto-anonymized)
126
+ ---
127
+ http_interactions:
128
+ - request:
129
+ method: post
130
+ uri: https://testuser%40testorg:testpass@testurl.local/api/sessions
131
+ body:
132
+ encoding: UTF-8
133
+ ...
134
+
92
135
 
93
136
  LICENSE
94
137
  --
95
138
 
96
139
  Author:: Stefano Tortarolo <stefano.tortarolo@gmail.com>
97
140
 
98
- Copyright:: Copyright (c) 2012-2013
141
+ Copyright:: Copyright (c) 2012-2014
99
142
  License:: Apache License, Version 2.0
100
143
 
101
144
  Licensed under the Apache License, Version 2.0 (the "License");
@@ -21,6 +21,7 @@ require 'nokogiri'
21
21
  require 'httpclient'
22
22
  require 'ruby-progressbar'
23
23
  require 'logger'
24
+ require 'uri'
24
25
 
25
26
  require 'vcloud-rest/vcloud/vapp'
26
27
  require 'vcloud-rest/vcloud/org'
@@ -28,7 +29,9 @@ require 'vcloud-rest/vcloud/catalog'
28
29
  require 'vcloud-rest/vcloud/vdc'
29
30
  require 'vcloud-rest/vcloud/vm'
30
31
  require 'vcloud-rest/vcloud/ovf'
32
+ require 'vcloud-rest/vcloud/media'
31
33
  require 'vcloud-rest/vcloud/network'
34
+ require 'vcloud-rest/vcloud/disk'
32
35
 
33
36
  module VCloudClient
34
37
  class UnauthorizedAccess < StandardError; end
@@ -82,6 +85,7 @@ module VCloudClient
82
85
  }
83
86
 
84
87
  response, headers = send_request(params)
88
+ ensure
85
89
  # reset auth key to nil
86
90
  @auth_key = nil
87
91
  end
@@ -130,20 +134,21 @@ module VCloudClient
130
134
  # Sends a synchronous request to the vCloud API and returns the response as parsed XML + headers.
131
135
  def send_request(params, payload=nil, content_type=nil)
132
136
  headers = {:accept => "application/*+xml;version=#{@api_version}"}
137
+ invocation_params = {:method => params['method'],
138
+ :headers => headers,
139
+ :url => "#{@api_url}#{params['command']}",
140
+ :payload => payload}
141
+
133
142
  if @auth_key
134
143
  headers.merge!({:x_vcloud_authorization => @auth_key})
144
+ else
145
+ invocation_params.merge!({:user => "#{@username}@#{@org_name}",
146
+ :password => @password })
135
147
  end
136
148
 
137
- if content_type
138
- headers.merge!({:content_type => content_type})
139
- end
149
+ headers.merge!({:content_type => content_type}) if content_type
140
150
 
141
- request = RestClient::Request.new(:method => params['method'],
142
- :user => "#{@username}@#{@org_name}",
143
- :password => @password,
144
- :headers => headers,
145
- :url => "#{@api_url}#{params['command']}",
146
- :payload => payload)
151
+ request = RestClient::Request.new(invocation_params)
147
152
 
148
153
  begin
149
154
  response = request.execute
@@ -236,6 +241,18 @@ module VCloudClient
236
241
  task_id
237
242
  end
238
243
 
244
+ ##
245
+ # Discard suspended state of a vApp/VM
246
+ def discard_suspended_state_action(id, type=:vapp)
247
+ params = {
248
+ "method" => :post,
249
+ "command" => "/vApp/#{type}-#{id}/action/discardSuspendedState"
250
+ }
251
+ response, headers = send_request(params)
252
+ task_id = headers[:location].gsub(/.*\/task\//, "")
253
+ task_id
254
+ end
255
+
239
256
  ##
240
257
  # Create a new vapp/vm snapshot (overwrites any existing)
241
258
  def create_snapshot_action(id, description="New Snapshot", type=:vapp)
@@ -266,6 +283,109 @@ module VCloudClient
266
283
  task_id
267
284
  end
268
285
 
286
+ ##
287
+ # Discard all existing snapshots (vapp/vm)
288
+ def discard_snapshot_action(id, type=:vapp)
289
+ params = {
290
+ "method" => :post,
291
+ "command" => "/vApp/#{type}-#{id}/action/removeAllSnapshots"
292
+ }
293
+ response, headers = send_request(params)
294
+ task_id = headers[:location].gsub(/.*\/task\//, "")
295
+ task_id
296
+ end
297
+
298
+ ##
299
+ # Upload a large file in configurable chunks, output an optional progressbar
300
+ def upload_file(uploadURL, uploadFile, progressUrl, config={})
301
+ raise ::IOError, "#{uploadFile} not found." unless File.exists?(uploadFile)
302
+
303
+ # Set chunksize to 10M if not specified otherwise
304
+ chunkSize = (config[:chunksize] || 10485760)
305
+
306
+ # Set progress bar to default format if not specified otherwise
307
+ progressBarFormat = (config[:progressbar_format] || "%e <%B> %p%% %t")
308
+
309
+ # Set progress bar length to 120 if not specified otherwise
310
+ progressBarLength = (config[:progressbar_length] || 120)
311
+
312
+ # Open our file for upload
313
+ uploadFileHandle = File.new(uploadFile, "rb" )
314
+ fileName = File.basename(uploadFileHandle)
315
+
316
+ progressBarTitle = "Uploading: " + uploadFile.to_s
317
+
318
+ # Create a progressbar object if progress bar is enabled
319
+ if config[:progressbar_enable] == true && uploadFileHandle.size.to_i > chunkSize
320
+ progressbar = ProgressBar.create(
321
+ :title => progressBarTitle,
322
+ :starting_at => 0,
323
+ :total => uploadFileHandle.size.to_i,
324
+ :length => progressBarLength,
325
+ :format => progressBarFormat
326
+ )
327
+ else
328
+ @logger.info progressBarTitle
329
+ end
330
+ # Create a new HTTP client
331
+ clnt = HTTPClient.new
332
+
333
+ # Disable SSL cert verification
334
+ clnt.ssl_config.verify_mode=(OpenSSL::SSL::VERIFY_NONE)
335
+
336
+ # Suppress SSL depth message
337
+ clnt.ssl_config.verify_callback=proc{ |ok, ctx|; true };
338
+
339
+ # Perform ranged upload until the file reaches its end
340
+ until uploadFileHandle.eof?
341
+
342
+ # Create ranges for this chunk upload
343
+ rangeStart = uploadFileHandle.pos
344
+ rangeStop = uploadFileHandle.pos.to_i + chunkSize
345
+
346
+ # Read current chunk
347
+ fileContent = uploadFileHandle.read(chunkSize)
348
+
349
+ # If statement to handle last chunk transfer if is > than filesize
350
+ if rangeStop.to_i > uploadFileHandle.size.to_i
351
+ contentRange = "bytes #{rangeStart.to_s}-#{uploadFileHandle.size.to_s}/#{uploadFileHandle.size.to_s}"
352
+ rangeLen = uploadFileHandle.size.to_i - rangeStart.to_i
353
+ else
354
+ contentRange = "bytes #{rangeStart.to_s}-#{rangeStop.to_s}/#{uploadFileHandle.size.to_s}"
355
+ rangeLen = rangeStop.to_i - rangeStart.to_i
356
+ end
357
+
358
+ # Build headers
359
+ extheader = {
360
+ 'x-vcloud-authorization' => @auth_key,
361
+ 'Content-Range' => contentRange,
362
+ 'Content-Length' => rangeLen.to_s
363
+ }
364
+
365
+ begin
366
+ uploadRequest = "#{@host_url}#{uploadURL}"
367
+ connection = clnt.request('PUT', uploadRequest, nil, fileContent, extheader)
368
+
369
+ if config[:progressbar_enable] == true && uploadFileHandle.size.to_i > chunkSize
370
+ params = {
371
+ 'method' => :get,
372
+ 'command' => progressUrl
373
+ }
374
+ response, headers = send_request(params)
375
+
376
+ response.css("Files File [name='#{fileName}']").each do |file|
377
+ progressbar.progress=file[:bytesTransferred].to_i
378
+ end
379
+ end
380
+ rescue
381
+ retryTime = (config[:retry_time] || 5)
382
+ @logger.warn "Range #{contentRange} failed to upload, retrying the chunk in #{retryTime.to_s} seconds, to stop the action press CTRL+C."
383
+ sleep retryTime.to_i
384
+ retry
385
+ end
386
+ end
387
+ uploadFileHandle.close
388
+ end
269
389
 
270
390
  end # class
271
391
  end
@@ -66,31 +66,41 @@ module VCloudClient
66
66
  description = description.text unless description.nil?
67
67
 
68
68
  items = []
69
- response.css("Entity[type='application/vnd.vmware.vcloud.vAppTemplate+xml']").each do |item|
70
- itemId = item['href'].gsub(/.*\/vAppTemplate\/vappTemplate\-/, "")
71
-
72
- # Fetch the catalogItemId information
73
- params = {
74
- 'method' => :get,
75
- 'command' => "/vAppTemplate/vappTemplate-#{itemId}"
76
- }
77
- response, headers = send_request(params)
78
-
79
- # VMs Hash for all the vApp VM entities
80
- vms_hash = {}
81
- response.css("/VAppTemplate/Children/Vm").each do |vmElem|
82
- vmName = vmElem["name"]
83
- vmId = vmElem["href"].gsub(/.*\/vAppTemplate\/vm\-/, "")
84
-
85
- # Add the VM name/id to the VMs Hash
86
- vms_hash[vmName] = { :id => vmId }
69
+ # manage two different types of catalog items: vAppTemplate and media
70
+ if response.css("Entity[type='application/vnd.vmware.vcloud.vAppTemplate+xml']").size > 0
71
+ response.css("Entity[type='application/vnd.vmware.vcloud.vAppTemplate+xml']").each do |item|
72
+ itemId = item['href'].gsub(/.*\/vAppTemplate\/vappTemplate\-/, "")
73
+
74
+ # Fetch the catalogItemId information
75
+ params = {
76
+ 'method' => :get,
77
+ 'command' => "/vAppTemplate/vappTemplate-#{itemId}"
78
+ }
79
+ response, headers = send_request(params)
80
+
81
+ # VMs Hash for all the vApp VM entities
82
+ vms_hash = {}
83
+ response.css("/VAppTemplate/Children/Vm").each do |vmElem|
84
+ vmName = vmElem["name"]
85
+ vmId = vmElem["href"].gsub(/.*\/vAppTemplate\/vm\-/, "")
86
+
87
+ # Add the VM name/id to the VMs Hash
88
+ vms_hash[vmName] = { :id => vmId }
89
+ end
90
+
91
+ items << { :id => itemId,
92
+ :name => item['name'],
93
+ :vms_hash => vms_hash }
87
94
  end
88
95
 
89
- items << { :id => itemId,
90
- :name => item['name'],
91
- :vms_hash => vms_hash }
96
+ { :id => catalogItemId, :description => description, :items => items, :type => 'vAppTemplate' }
97
+ elsif response.css("Entity[type='application/vnd.vmware.vcloud.media+xml']").size > 0
98
+ name = response.css("Entity[type='application/vnd.vmware.vcloud.media+xml']").first['name']
99
+ { :id => catalogItemId, :description => description, :name => name, :type => 'media' }
100
+ else
101
+ @logger.warn 'WARNING: either this catalog item is empty or contains something not managed by vcloud-rest'
102
+ { :id => catalogItemId, :description => description, :type => 'unknown' }
92
103
  end
93
- { :id => catalogItemId, :description => description, :items => items }
94
104
  end
95
105
 
96
106
  ##
@@ -0,0 +1,113 @@
1
+ ##
2
+ # Manage independent disks
3
+ # Independent disks are stand-alone virtual disks created in organization vDCs.
4
+ # Every disk is associated with an organization vDC but not with a VM and
5
+ # can be later attached to any VM deployed in that vDC.
6
+ #
7
+ # VERY IMPORTANT NOTE: independent disks size, unlike set_vm_disk_info, is in Kb
8
+ ##
9
+ module VCloudClient
10
+ class Connection
11
+ def create_disk(name, size, vdc_id, description="")
12
+ builder = Nokogiri::XML::Builder.new do |xml|
13
+ xml.DiskCreateParams(
14
+ "xmlns" => "http://www.vmware.com/vcloud/v1.5") {
15
+ xml.Disk("name" => name, "size" => size) {
16
+ xml.Description description
17
+ }
18
+ }
19
+ end
20
+
21
+ params = {
22
+ 'method' => :post,
23
+ 'command' => "/vdc/#{vdc_id}/disk"
24
+ }
25
+
26
+ @logger.debug "Creating independent disk #{name} in VDC #{vdc_id}"
27
+ response, headers = send_request(params, builder.to_xml, "application/vnd.vmware.vcloud.diskCreateParams+xml")
28
+
29
+ # Get the id of the new disk
30
+ disk_url = response.css("Disk").first[:href]
31
+ disk_id = disk_url.gsub(/.*\/disk\//, "")
32
+ @logger.debug "Independent disk created = #{disk_id}"
33
+
34
+ task = response.css("Task[operationName='vdcCreateDisk']").first
35
+ task_id = task["href"].gsub(/.*\/task\//, "")
36
+
37
+ { :disk_id => disk_id, :task_id => task_id }
38
+ end
39
+
40
+ def get_disk(disk_id)
41
+ params = {
42
+ 'method' => :get,
43
+ 'command' => "/disk/#{disk_id}"
44
+ }
45
+
46
+ @logger.debug "Fetching independent disk #{disk_id}"
47
+ response, headers = send_request(params)
48
+
49
+ name = response.css("Disk").attribute("name").text
50
+ size = response.css("Disk").attribute("size").text
51
+ description = response.css("Description").first
52
+ description = description.text unless description.nil?
53
+ storage_profile = response.css("StorageProfile").first[:name]
54
+ owner = response.css("User").first[:name]
55
+ { :id => disk_id, :name => name, :size => size, :description => description, :storage_profile => storage_profile, :owner => owner }
56
+ end
57
+
58
+ def get_disk_by_name(organization, vdcName, diskName)
59
+ result = nil
60
+
61
+ get_vdc_by_name(organization, vdcName)[:disks].each do |disk|
62
+ if disk[0].downcase == diskName.downcase
63
+ result = get_disk(disk[1])
64
+ end
65
+ end
66
+
67
+ result
68
+ end
69
+
70
+ def attach_disk_to_vm(disk_id, vm_id)
71
+ disk_attach_action(disk_id, vm_id, 'attach')
72
+ end
73
+
74
+ def detach_disk_from_vm(disk_id, vm_id)
75
+ disk_attach_action(disk_id, vm_id, 'detach')
76
+ end
77
+
78
+ def delete_disk(disk_id)
79
+ params = {
80
+ 'method' => :delete,
81
+ 'command' => "/disk/#{disk_id}"
82
+ }
83
+
84
+ @logger.debug "Deleting independent disk #{disk_id}"
85
+ response, headers = send_request(params)
86
+
87
+ task = response.css("Task").first
88
+ task_id = task["href"].gsub(/.*\/task\//, "")
89
+ end
90
+
91
+ private
92
+
93
+ def disk_attach_action(disk_id, vm_id, action)
94
+ builder = Nokogiri::XML::Builder.new do |xml|
95
+ xml.DiskAttachOrDetachParams("xmlns" => "http://www.vmware.com/vcloud/v1.5") {
96
+ xml.Disk(
97
+ "type" => "application/vnd.vmware.vcloud.disk+xml",
98
+ "href" => "#{@api_url}/disk/#{disk_id}")
99
+ }
100
+ end
101
+
102
+ params = {
103
+ 'method' => :post,
104
+ 'command' => "/vApp/vm-#{vm_id}/disk/action/#{action}"
105
+ }
106
+
107
+ response, headers = send_request(params, builder.to_xml, "application/vnd.vmware.vcloud.diskAttachOrDetachParams+xml")
108
+
109
+ task = response.css("Task").first
110
+ task_id = task["href"].gsub(/.*\/task\//, "")
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,100 @@
1
+ module VCloudClient
2
+ class Connection
3
+
4
+ def upload_media(vdcId, mediaName, mediaDescription, mediaFile, type, catalogId, uploadOptions={})
5
+ raise ::IOError, "File #{mediaFile} is missing." unless File.exists?(mediaFile)
6
+
7
+ fileName = File.basename(mediaFile)
8
+ mediaName = File.basename(fileName, ".*") if mediaName.nil? || mediaName.empty?
9
+ type = File.extname(fileName).delete(".") if type.nil? || type.empty?
10
+ size = File.size(mediaFile)
11
+
12
+ builder = Nokogiri::XML::Builder.new do |xml|
13
+ xml.Media(
14
+ "xmlns" => "http://www.vmware.com/vcloud/v1.5",
15
+ "xmlns:ovf" => "http://schemas.dmtf.org/ovf/envelope/1",
16
+ "size" => size,
17
+ "imageType" => type,
18
+ "name" => mediaName) {
19
+ xml.Description mediaDescription
20
+ }
21
+ end
22
+
23
+ params = {
24
+ 'method' => :post,
25
+ 'command' => "/vdc/#{vdcId}/media"
26
+ }
27
+
28
+ @logger.debug "Creating Media item"
29
+ response, headers = send_request(params, builder.to_xml, "application/vnd.vmware.vcloud.media+xml")
30
+
31
+ # Get the new media id from response
32
+ mediaUrl = response.css("Media").first[:href]
33
+ mediaId = mediaUrl.gsub(/.*\/media\//, "")
34
+ @logger.debug "Media item created - #{mediaId}"
35
+
36
+ # Get File upload:default link from response
37
+ uploadHref = response.css("Files Link [rel='upload:default']").first[:href]
38
+ fileUpload = uploadHref.gsub(/(.*)(\/transfer\/.*)/, "\\2")
39
+
40
+ begin
41
+ @logger.debug "Uploading #{mediaFile}"
42
+ upload_file(fileUpload, mediaFile, "/media/#{mediaId}", uploadOptions)
43
+
44
+ # Add item to the catalog catalogId
45
+ builder = Nokogiri::XML::Builder.new do |xml|
46
+ xml.CatalogItem(
47
+ "xmlns" => "http://www.vmware.com/vcloud/v1.5",
48
+ "type" => "application/vnd.vmware.vcloud.catalogItem+xml",
49
+ "name" => mediaName) {
50
+ xml.Description mediaDescription
51
+ xml.Entity("href" => "#{@api_url}/media/#{mediaId}")
52
+ }
53
+ end
54
+
55
+ params = {
56
+ 'method' => :post,
57
+ 'command' => "/catalog/#{catalogId}/catalogItems"
58
+ }
59
+
60
+ @logger.debug "Adding media item #{mediaName} to catalog."
61
+ response, headers = send_request(params, builder.to_xml, "application/vnd.vmware.vcloud.catalogItem+xml")
62
+
63
+ # TODO: the best thing would detect the real importing status.
64
+ entity = response.css("Entity").first
65
+ result = {}
66
+ if entity
67
+ result[:id] = entity['href'].gsub(/.*\/media\//, "")
68
+ result[:name] = entity['name']
69
+ end
70
+ result
71
+ rescue Exception => e
72
+ @logger.error "Exception detected: #{e.message}."
73
+
74
+ # Get Media Task
75
+ params = {
76
+ 'method' => :get,
77
+ 'command' => "/media/#{mediaId}"
78
+ }
79
+ response, headers = send_request(params)
80
+
81
+ # Cancel Task
82
+ # Note that it might not exist (i.e., error for existing vdc entity)
83
+ tasks = response.css("Tasks")
84
+ unless tasks.empty?
85
+ tasks.css("Task").each do |task|
86
+ if task['status'] == 'error'
87
+ @logger.error task.css('Error').first['message']
88
+ else
89
+ id = task['href'].gsub(/.*\/task\//, "")
90
+ @logger.error "Aborting task #{id}..."
91
+ cancel_task(id)
92
+ end
93
+ end
94
+ end
95
+
96
+ raise e
97
+ end
98
+ end
99
+ end
100
+ end
@@ -1,7 +1,7 @@
1
1
  module VCloudClient
2
2
  class Connection
3
3
  ##
4
- # Fetch details about a given network
4
+ # Fetch details about a given Org VDC network
5
5
  def get_network(networkId)
6
6
  response = get_base_network(networkId)
7
7
 
@@ -10,6 +10,7 @@ module VCloudClient
10
10
  # - uploadOptions {}
11
11
  def upload_ovf(vdcId, vappName, vappDescription, ovfFile, catalogId, uploadOptions={})
12
12
  raise ::IOError, "OVF #{ovfFile} is missing." unless File.exists?(ovfFile)
13
+ raise ::IOError, "Only .ovf files are supported" unless File.extname(ovfFile) =~ /\.ovf/
13
14
 
14
15
  # if send_manifest is not set, setting it true
15
16
  if uploadOptions[:send_manifest].nil? || uploadOptions[:send_manifest]
@@ -52,7 +53,7 @@ module VCloudClient
52
53
  # Send OVF Descriptor
53
54
  uploadURL = "/transfer/#{descriptorUpload}"
54
55
  uploadFile = "#{ovfDir}/#{ovfFileBasename}.ovf"
55
- upload_file(uploadURL, uploadFile, vAppTemplate, uploadOptions)
56
+ upload_file(uploadURL, uploadFile, "/vAppTemplate/vappTemplate-#{vAppTemplate}", uploadOptions)
56
57
 
57
58
  @logger.debug "OVF Descriptor uploaded."
58
59
 
@@ -80,7 +81,7 @@ module VCloudClient
80
81
  if uploadManifest == "true"
81
82
  uploadURL = "/transfer/#{transferGUID}/descriptor.mf"
82
83
  uploadFile = "#{ovfDir}/#{ovfFileBasename}.mf"
83
- upload_file(uploadURL, uploadFile, vAppTemplate, uploadOptions)
84
+ upload_file(uploadURL, uploadFile, "/vAppTemplate/vappTemplate-#{vAppTemplate}", uploadOptions)
84
85
  @logger.debug "OVF Manifest uploaded."
85
86
  end
86
87
 
@@ -94,7 +95,7 @@ module VCloudClient
94
95
  fileName = file[:href].gsub(/.*\/transfer\/#{transferGUID}\//, "")
95
96
  uploadFile = "#{ovfDir}/#{fileName}"
96
97
  uploadURL = "/transfer/#{transferGUID}/#{fileName}"
97
- upload_file(uploadURL, uploadFile, vAppTemplate, uploadOptions)
98
+ upload_file(uploadURL, uploadFile, "/vAppTemplate/vappTemplate-#{vAppTemplate}", uploadOptions)
98
99
  end
99
100
 
100
101
  # Add item to the catalog catalogId
@@ -156,98 +157,5 @@ module VCloudClient
156
157
  raise e
157
158
  end
158
159
  end
159
-
160
- private
161
- ##
162
- # Upload a large file in configurable chunks, output an optional progressbar
163
- def upload_file(uploadURL, uploadFile, vAppTemplate, config={})
164
- raise ::IOError, "#{uploadFile} not found." unless File.exists?(uploadFile)
165
-
166
- # Set chunksize to 10M if not specified otherwise
167
- chunkSize = (config[:chunksize] || 10485760)
168
-
169
- # Set progress bar to default format if not specified otherwise
170
- progressBarFormat = (config[:progressbar_format] || "%e <%B> %p%% %t")
171
-
172
- # Set progress bar length to 120 if not specified otherwise
173
- progressBarLength = (config[:progressbar_length] || 120)
174
-
175
- # Open our file for upload
176
- uploadFileHandle = File.new(uploadFile, "rb" )
177
- fileName = File.basename(uploadFileHandle)
178
-
179
- progressBarTitle = "Uploading: " + uploadFile.to_s
180
-
181
- # Create a progressbar object if progress bar is enabled
182
- if config[:progressbar_enable] == true && uploadFileHandle.size.to_i > chunkSize
183
- progressbar = ProgressBar.create(
184
- :title => progressBarTitle,
185
- :starting_at => 0,
186
- :total => uploadFileHandle.size.to_i,
187
- :length => progressBarLength,
188
- :format => progressBarFormat
189
- )
190
- else
191
- @logger.info progressBarTitle
192
- end
193
- # Create a new HTTP client
194
- clnt = HTTPClient.new
195
-
196
- # Disable SSL cert verification
197
- clnt.ssl_config.verify_mode=(OpenSSL::SSL::VERIFY_NONE)
198
-
199
- # Suppress SSL depth message
200
- clnt.ssl_config.verify_callback=proc{ |ok, ctx|; true };
201
-
202
- # Perform ranged upload until the file reaches its end
203
- until uploadFileHandle.eof?
204
-
205
- # Create ranges for this chunk upload
206
- rangeStart = uploadFileHandle.pos
207
- rangeStop = uploadFileHandle.pos.to_i + chunkSize
208
-
209
- # Read current chunk
210
- fileContent = uploadFileHandle.read(chunkSize)
211
-
212
- # If statement to handle last chunk transfer if is > than filesize
213
- if rangeStop.to_i > uploadFileHandle.size.to_i
214
- contentRange = "bytes #{rangeStart.to_s}-#{uploadFileHandle.size.to_s}/#{uploadFileHandle.size.to_s}"
215
- rangeLen = uploadFileHandle.size.to_i - rangeStart.to_i
216
- else
217
- contentRange = "bytes #{rangeStart.to_s}-#{rangeStop.to_s}/#{uploadFileHandle.size.to_s}"
218
- rangeLen = rangeStop.to_i - rangeStart.to_i
219
- end
220
-
221
- # Build headers
222
- extheader = {
223
- 'x-vcloud-authorization' => @auth_key,
224
- 'Content-Range' => contentRange,
225
- 'Content-Length' => rangeLen.to_s
226
- }
227
-
228
- begin
229
- uploadRequest = "#{@host_url}#{uploadURL}"
230
- connection = clnt.request('PUT', uploadRequest, nil, fileContent, extheader)
231
-
232
- if config[:progressbar_enable] == true && uploadFileHandle.size.to_i > chunkSize
233
- params = {
234
- 'method' => :get,
235
- 'command' => "/vAppTemplate/vappTemplate-#{vAppTemplate}"
236
- }
237
- response, headers = send_request(params)
238
-
239
- response.css("Files File [name='#{fileName}']").each do |file|
240
- progressbar.progress=file[:bytesTransferred].to_i
241
- end
242
- end
243
- rescue
244
- retryTime = (config[:retry_time] || 5)
245
- @logger.warn "Range #{contentRange} failed to upload, retrying the chunk in #{retryTime.to_s} seconds, to stop the action press CTRL+C."
246
- sleep retryTime.to_i
247
- retry
248
- end
249
- end
250
- uploadFileHandle.close
251
- end
252
- end
160
+ end # class
253
161
  end
@@ -34,6 +34,9 @@ module VCloudClient
34
34
 
35
35
  networks = response.css('NetworkConfig').reject{|n| n.attribute('networkName').text == 'none'}.
36
36
  collect do |network|
37
+ net_id = network.css('Link [rel=repair]')
38
+ net_id = net_id.attribute('href').text.gsub(/.*\/network\/(.*)\/action.*/, '\1') unless net_id.nil?
39
+
37
40
  net_name = network.attribute('networkName').text
38
41
 
39
42
  gateway = network.css('Gateway')
@@ -62,6 +65,7 @@ module VCloudClient
62
65
  }
63
66
 
64
67
  {
68
+ :id => net_id,
65
69
  :name => net_name,
66
70
  :scope => ipscope
67
71
  }
@@ -163,6 +167,12 @@ module VCloudClient
163
167
  power_action(vAppId, 'suspend')
164
168
  end
165
169
 
170
+ ##
171
+ # Discard suspended state of a given vapp
172
+ def discard_suspend_state_vapp(vAppId)
173
+ discard_suspended_state_action(vAppId, :vapp)
174
+ end
175
+
166
176
  ##
167
177
  # reboot a given vapp
168
178
  # This will basically initial a guest OS reboot, and will only work if
@@ -310,7 +320,52 @@ module VCloudClient
310
320
  { :vapp_id => vapp_id, :task_id => task_id }
311
321
  end
312
322
 
323
+ ##
324
+ # Create a new virtual machine from a template in an existing vApp.
325
+ #
326
+ # Params:
327
+ # - vapp: the target vapp
328
+ # - vm: hash with template ID and new VM name
329
+ # - network_config: hash of the network configuration for the VM
330
+ def add_vm_to_vapp(vapp, vm, network_config={})
331
+ builder = Nokogiri::XML::Builder.new do |xml|
332
+ xml.RecomposeVAppParams(
333
+ "xmlns" => "http://www.vmware.com/vcloud/v1.5",
334
+ "xmlns:ovf" => "http://schemas.dmtf.org/ovf/envelope/1",
335
+ "name" => vapp[:name]) {
336
+ xml.SourcedItem {
337
+ xml.Source("href" => "#{@api_url}/vAppTemplate/vm-#{vm[:template_id]}", "name" => vm[:vm_name])
338
+ xml.InstantiationParams {
339
+ xml.NetworkConnectionSection(
340
+ "xmlns:ovf" => "http://schemas.dmtf.org/ovf/envelope/1",
341
+ "type" => "application/vnd.vmware.vcloud.networkConnectionSection+xml",
342
+ "href" => "#{@api_url}/vAppTemplate/vm-#{vm[:template_id]}/networkConnectionSection/") {
343
+ xml['ovf'].Info "Network config for sourced item"
344
+ xml.PrimaryNetworkConnectionIndex "0"
345
+ xml.NetworkConnection("network" => network_config[:name]) {
346
+ xml.NetworkConnectionIndex "0"
347
+ xml.IsConnected "true"
348
+ xml.IpAddressAllocationMode(network_config[:ip_allocation_mode] || "POOL")
349
+ }
350
+ }
351
+ }
352
+ xml.NetworkAssignment("containerNetwork" => network_config[:name], "innerNetwork" => network_config[:name])
353
+ }
354
+ xml.AllEULAsAccepted "true"
355
+ }
356
+ end
357
+
358
+ params = {
359
+ "method" => :post,
360
+ "command" => "/vApp/vapp-#{vapp[:id]}/action/recomposeVApp"
361
+ }
313
362
 
363
+ response, headers = send_request(params, builder.to_xml, "application/vnd.vmware.vcloud.recomposeVAppParams+xml")
364
+
365
+ task = response.css("Task[operationName='vdcRecomposeVapp']").first
366
+ task_id = task["href"].gsub(/.*\/task\//, "")
367
+ task_id
368
+ end
314
369
 
315
370
  ##
316
371
  # Create a new vm snapshot (overwrites any existing)
@@ -340,6 +395,12 @@ module VCloudClient
340
395
  revert_snapshot_action(vmId, :vapp)
341
396
  end
342
397
 
398
+ ##
399
+ # Discard all existing snapshots
400
+ def discard_vapp_snapshot(vmId)
401
+ discard_snapshot_action(vmId, :vapp)
402
+ end
403
+
343
404
  ##
344
405
  # Clone a vapp in a given VDC to a new Vapp
345
406
  def clone_vapp(vdc_id, source_vapp_id, name, deploy="true", poweron="false", linked="false", delete_source="false")
@@ -4,6 +4,8 @@ module VCloudClient
4
4
  # Fetch details about a given vdc:
5
5
  # - description
6
6
  # - vapps
7
+ # - templates
8
+ # - disks
7
9
  # - networks
8
10
  def get_vdc(vdcId)
9
11
  params = {
@@ -24,12 +26,24 @@ module VCloudClient
24
26
  vapps[item['name']] = item['href'].gsub(/.*\/vApp\/vapp\-/, "")
25
27
  end
26
28
 
29
+ templates = {}
30
+ response.css("ResourceEntity[type='application/vnd.vmware.vcloud.vAppTemplate+xml']").each do |item|
31
+ templates[item['name']] = item['href'].gsub(/.*\/vAppTemplate\/vappTemplate\-/, "")
32
+ end
33
+
34
+ disks = {}
35
+ response.css("ResourceEntity[type='application/vnd.vmware.vcloud.disk+xml']").each do |item|
36
+ disks[item['name']] = item['href'].gsub(/.*\/disk\//, "")
37
+ end
38
+
27
39
  networks = {}
28
40
  response.css("Network[type='application/vnd.vmware.vcloud.network+xml']").each do |item|
29
41
  networks[item['name']] = item['href'].gsub(/.*\/network\//, "")
30
42
  end
43
+
31
44
  { :id => vdcId, :name => name, :description => description,
32
- :vapps => vapps, :networks => networks }
45
+ :vapps => vapps, :templates => templates, :disks => disks,
46
+ :networks => networks }
33
47
  end
34
48
 
35
49
  ##
@@ -186,8 +186,9 @@ module VCloudClient
186
186
  # For some reasons these elements must be removed
187
187
  netconfig_response.css("Link").each {|n| n.remove}
188
188
 
189
- # Delete placeholder network
190
- netconfig_response.css('NetworkConnection').find{|n| n.attribute('network').text == 'none'}.remove
189
+ # Delete placeholder network if present (since vcloud 5.5 has been removed)
190
+ none_network = netconfig_response.css('NetworkConnection').find{|n| n.attribute('network').text == 'none'}
191
+ none_network.remove if none_network
191
192
 
192
193
  networks_count = netconfig_response.css('NetworkConnection').count
193
194
 
@@ -349,7 +350,11 @@ module VCloudClient
349
350
  external_ip = network.css('ExternalIpAddress').first
350
351
  external_ip = external_ip.text if external_ip
351
352
 
352
- networks[network['network']] = {
353
+ # Append NetworkConnectionIndex to network name to generate a unique hash key,
354
+ # otherwise different interfaces on the same network would use the same hash key
355
+ key = "#{network['network']}_#{network.css('NetworkConnectionIndex').first.text}"
356
+
357
+ networks[key] = {
353
358
  :index => network.css('NetworkConnectionIndex').first.text,
354
359
  :ip => ip,
355
360
  :external_ip => external_ip,
@@ -422,6 +427,12 @@ module VCloudClient
422
427
  power_action(vmId, 'suspend', :vm)
423
428
  end
424
429
 
430
+ ##
431
+ # Discard suspended state of a given vm
432
+ def discard_suspend_state_vm(vmId)
433
+ discard_suspended_state_action(vmId, :vm)
434
+ end
435
+
425
436
  ##
426
437
  # reboot a given vm
427
438
  # This will basically initial a guest OS reboot, and will only work if
@@ -455,11 +466,52 @@ module VCloudClient
455
466
  revert_snapshot_action(vmId, :vm)
456
467
  end
457
468
 
469
+ ##
470
+ # Discard all existing snapshots
471
+ def discard_vm_snapshot(vmId)
472
+ discard_snapshot_action(vmId, :vm)
473
+ end
474
+
475
+ ##
476
+ # Retrieve a screen ticket that you can use with the VMRC browser plug-in
477
+ # to gain access to the console of a running VM.
478
+ def acquire_ticket_vm(vmId)
479
+
480
+ params = {
481
+ 'method' => :post,
482
+ 'command' => "/vApp/vm-#{vmId}/screen/action/acquireTicket"
483
+ }
484
+
485
+ response, headers = send_request(params)
486
+
487
+ screen_ticket = response.css("ScreenTicket").text
488
+
489
+ result = {}
490
+
491
+ if screen_ticket =~ /mks:\/\/([^\/]*)\/([^\?]*)\?ticket=(.*)/
492
+ result = { host: $1, moid: $2, token: $3 }
493
+ result[:token] = URI.unescape result[:token]
494
+ end
495
+ result
496
+ end
458
497
 
459
498
  private
460
499
  def add_disk(source_xml, disk_info)
500
+
461
501
  disks_count = source_xml.css("Item").css("rasd|HostResource").count
462
502
 
503
+ disk_parent_ids = Set.new
504
+
505
+ source_xml.css("Item").each do |entry|
506
+ parent_id = entry.css('rasd|Parent').first.text unless entry.css('rasd|Parent').empty?
507
+ resource_type = entry.css('rasd|ResourceType').first.text unless entry.css('rasd|ResourceType').empty?
508
+ disk_parent_ids << parent_id if resource_type == '17'
509
+ end
510
+
511
+ raise InvalidStateError, "Could not handle this request because multiple nodes with ResourceType 17 and different rasd:Parent exist" if disk_parent_ids.size > 1
512
+
513
+ raise InvalidStateError, "Could not handle this request because I could not find a node with ResourceType 17" if disk_parent_ids.size == 0
514
+
463
515
  # FIXME: This is a hack, but dealing with nokogiri APIs can be quite
464
516
  # frustrating sometimes...
465
517
  sibling = source_xml.css("Item").first
@@ -478,7 +530,7 @@ module VCloudClient
478
530
  ns12:busSubType=\"lsilogic\"
479
531
  ns12:busType=\"6\"/>
480
532
  <rasd:InstanceID>200#{disks_count}</rasd:InstanceID>
481
- <rasd:Parent>1</rasd:Parent>
533
+ <rasd:Parent>#{disk_parent_ids.first}</rasd:Parent>
482
534
  <rasd:ResourceType>17</rasd:ResourceType>
483
535
  </Item>""")
484
536
  end
@@ -1,3 +1,3 @@
1
1
  module VCloudClient
2
- VERSION = "1.2.0"
2
+ VERSION = "1.3.0"
3
3
  end
metadata CHANGED
@@ -1,71 +1,179 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vcloud-rest
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.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: 2014-02-06 00:00:00.000000000 Z
11
+ date: 2014-06-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - '>='
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: 1.5.10
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
26
  version: 1.5.10
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rest-client
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ~>
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.6'
34
+ - - ">="
32
35
  - !ruby/object:Gem::Version
33
36
  version: 1.6.7
34
37
  type: :runtime
35
38
  prerelease: false
36
39
  version_requirements: !ruby/object:Gem::Requirement
37
40
  requirements:
38
- - - ~>
41
+ - - "~>"
42
+ - !ruby/object:Gem::Version
43
+ version: '1.6'
44
+ - - ">="
39
45
  - !ruby/object:Gem::Version
40
46
  version: 1.6.7
41
47
  - !ruby/object:Gem::Dependency
42
48
  name: httpclient
43
49
  requirement: !ruby/object:Gem::Requirement
44
50
  requirements:
45
- - - ~>
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '2.3'
54
+ - - ">="
46
55
  - !ruby/object:Gem::Version
47
56
  version: 2.3.3
48
57
  type: :runtime
49
58
  prerelease: false
50
59
  version_requirements: !ruby/object:Gem::Requirement
51
60
  requirements:
52
- - - ~>
61
+ - - "~>"
62
+ - !ruby/object:Gem::Version
63
+ version: '2.3'
64
+ - - ">="
53
65
  - !ruby/object:Gem::Version
54
66
  version: 2.3.3
55
67
  - !ruby/object:Gem::Dependency
56
68
  name: ruby-progressbar
57
69
  requirement: !ruby/object:Gem::Requirement
58
70
  requirements:
59
- - - ~>
71
+ - - "~>"
60
72
  - !ruby/object:Gem::Version
61
- version: 1.4.0
73
+ version: '1.5'
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: 1.5.1
62
77
  type: :runtime
63
78
  prerelease: false
64
79
  version_requirements: !ruby/object:Gem::Requirement
65
80
  requirements:
66
- - - ~>
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '1.5'
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: 1.5.1
87
+ - !ruby/object:Gem::Dependency
88
+ name: rake
89
+ requirement: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - "~>"
92
+ - !ruby/object:Gem::Version
93
+ version: '10.1'
94
+ type: :development
95
+ prerelease: false
96
+ version_requirements: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - "~>"
99
+ - !ruby/object:Gem::Version
100
+ version: '10.1'
101
+ - !ruby/object:Gem::Dependency
102
+ name: rspec
103
+ requirement: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - "~>"
106
+ - !ruby/object:Gem::Version
107
+ version: '2.14'
108
+ type: :development
109
+ prerelease: false
110
+ version_requirements: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - "~>"
113
+ - !ruby/object:Gem::Version
114
+ version: '2.14'
115
+ - !ruby/object:Gem::Dependency
116
+ name: webmock
117
+ requirement: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - "~>"
120
+ - !ruby/object:Gem::Version
121
+ version: '1.13'
122
+ type: :development
123
+ prerelease: false
124
+ version_requirements: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - "~>"
127
+ - !ruby/object:Gem::Version
128
+ version: '1.13'
129
+ - !ruby/object:Gem::Dependency
130
+ name: vcr
131
+ requirement: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - "~>"
67
134
  - !ruby/object:Gem::Version
68
- version: 1.4.0
135
+ version: '2.9'
136
+ type: :development
137
+ prerelease: false
138
+ version_requirements: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - "~>"
141
+ - !ruby/object:Gem::Version
142
+ version: '2.9'
143
+ - !ruby/object:Gem::Dependency
144
+ name: simplecov
145
+ requirement: !ruby/object:Gem::Requirement
146
+ requirements:
147
+ - - "~>"
148
+ - !ruby/object:Gem::Version
149
+ version: '0.8'
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: 0.8.2
153
+ type: :development
154
+ prerelease: false
155
+ version_requirements: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '0.8'
160
+ - - ">="
161
+ - !ruby/object:Gem::Version
162
+ version: 0.8.2
163
+ - !ruby/object:Gem::Dependency
164
+ name: coveralls
165
+ requirement: !ruby/object:Gem::Requirement
166
+ requirements:
167
+ - - ">="
168
+ - !ruby/object:Gem::Version
169
+ version: '0'
170
+ type: :development
171
+ prerelease: false
172
+ version_requirements: !ruby/object:Gem::Requirement
173
+ requirements:
174
+ - - ">="
175
+ - !ruby/object:Gem::Version
176
+ version: '0'
69
177
  description: Ruby bindings to create, list and manage vCloud servers
70
178
  email:
71
179
  - stefano.tortarolo@gmail.com
@@ -74,10 +182,12 @@ extensions: []
74
182
  extra_rdoc_files: []
75
183
  files:
76
184
  - CHANGELOG.md
77
- - README.md
78
185
  - LICENSE
186
+ - README.md
79
187
  - lib/vcloud-rest/connection.rb
80
188
  - lib/vcloud-rest/vcloud/catalog.rb
189
+ - lib/vcloud-rest/vcloud/disk.rb
190
+ - lib/vcloud-rest/vcloud/media.rb
81
191
  - lib/vcloud-rest/vcloud/network.rb
82
192
  - lib/vcloud-rest/vcloud/org.rb
83
193
  - lib/vcloud-rest/vcloud/ovf.rb
@@ -96,19 +206,18 @@ require_paths:
96
206
  - lib
97
207
  required_ruby_version: !ruby/object:Gem::Requirement
98
208
  requirements:
99
- - - '>='
209
+ - - ">="
100
210
  - !ruby/object:Gem::Version
101
211
  version: '0'
102
212
  required_rubygems_version: !ruby/object:Gem::Requirement
103
213
  requirements:
104
- - - '>='
214
+ - - ">="
105
215
  - !ruby/object:Gem::Version
106
216
  version: '0'
107
217
  requirements: []
108
218
  rubyforge_project:
109
- rubygems_version: 2.1.11
219
+ rubygems_version: 2.2.2
110
220
  signing_key:
111
221
  specification_version: 4
112
222
  summary: Unofficial ruby bindings for VMWare vCloud's API
113
223
  test_files: []
114
- has_rdoc: