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 +4 -4
- data/CHANGELOG.md +21 -0
- data/README.md +50 -7
- data/lib/vcloud-rest/connection.rb +129 -9
- data/lib/vcloud-rest/vcloud/catalog.rb +32 -22
- data/lib/vcloud-rest/vcloud/disk.rb +113 -0
- data/lib/vcloud-rest/vcloud/media.rb +100 -0
- data/lib/vcloud-rest/vcloud/network.rb +1 -1
- data/lib/vcloud-rest/vcloud/ovf.rb +5 -97
- data/lib/vcloud-rest/vcloud/vapp.rb +61 -0
- data/lib/vcloud-rest/vcloud/vdc.rb +15 -1
- data/lib/vcloud-rest/vcloud/vm.rb +56 -4
- data/lib/vcloud-rest/version.rb +1 -1
- metadata +126 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 470e97ca321d742340571fff5cbbaa6e5a8fbe2b
|
4
|
+
data.tar.gz: 0460be22738dca2e1b05668a861777d6380dd3d4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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
|
53
|
-
- rest-client
|
54
|
-
- httpclient
|
55
|
-
- ruby-progressbar
|
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.
|
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-
|
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(
|
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
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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
|
90
|
-
|
91
|
-
|
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
|
@@ -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, :
|
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'}
|
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
|
-
|
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
|
533
|
+
<rasd:Parent>#{disk_parent_ids.first}</rasd:Parent>
|
482
534
|
<rasd:ResourceType>17</rasd:ResourceType>
|
483
535
|
</Item>""")
|
484
536
|
end
|
data/lib/vcloud-rest/version.rb
CHANGED
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.
|
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-
|
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.
|
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:
|
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.
|
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:
|