vcloud-rest 1.2.0 → 1.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: 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: