vagrant-vcloudair 0.5.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.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +34 -0
  3. data/.rubocop.yml +34 -0
  4. data/Gemfile +7 -0
  5. data/LICENSE +21 -0
  6. data/README.md +109 -0
  7. data/lib/vagrant-vcloudair.rb +63 -0
  8. data/lib/vagrant-vcloudair/action.rb +298 -0
  9. data/lib/vagrant-vcloudair/action/announce_ssh_exec.rb +22 -0
  10. data/lib/vagrant-vcloudair/action/build_vapp.rb +235 -0
  11. data/lib/vagrant-vcloudair/action/connect_vcloud.rb +54 -0
  12. data/lib/vagrant-vcloudair/action/destroy_vapp.rb +54 -0
  13. data/lib/vagrant-vcloudair/action/destroy_vm.rb +37 -0
  14. data/lib/vagrant-vcloudair/action/disconnect_vcloud.rb +31 -0
  15. data/lib/vagrant-vcloudair/action/forward_ports.rb +132 -0
  16. data/lib/vagrant-vcloudair/action/handle_nat_port_collisions.rb +153 -0
  17. data/lib/vagrant-vcloudair/action/inventory_check.rb +210 -0
  18. data/lib/vagrant-vcloudair/action/is_bridged.rb +29 -0
  19. data/lib/vagrant-vcloudair/action/is_created.rb +35 -0
  20. data/lib/vagrant-vcloudair/action/is_last_vm.rb +31 -0
  21. data/lib/vagrant-vcloudair/action/is_paused.rb +20 -0
  22. data/lib/vagrant-vcloudair/action/is_running.rb +20 -0
  23. data/lib/vagrant-vcloudair/action/message_already_running.rb +16 -0
  24. data/lib/vagrant-vcloudair/action/message_cannot_suspend.rb +16 -0
  25. data/lib/vagrant-vcloudair/action/message_not_created.rb +16 -0
  26. data/lib/vagrant-vcloudair/action/message_not_running.rb +16 -0
  27. data/lib/vagrant-vcloudair/action/message_will_not_destroy.rb +21 -0
  28. data/lib/vagrant-vcloudair/action/power_off.rb +33 -0
  29. data/lib/vagrant-vcloudair/action/power_off_vapp.rb +40 -0
  30. data/lib/vagrant-vcloudair/action/power_on.rb +39 -0
  31. data/lib/vagrant-vcloudair/action/read_ssh_info.rb +153 -0
  32. data/lib/vagrant-vcloudair/action/read_state.rb +51 -0
  33. data/lib/vagrant-vcloudair/action/resume.rb +25 -0
  34. data/lib/vagrant-vcloudair/action/suspend.rb +25 -0
  35. data/lib/vagrant-vcloudair/action/unmap_port_forwardings.rb +74 -0
  36. data/lib/vagrant-vcloudair/cap/forwarded_ports.rb +38 -0
  37. data/lib/vagrant-vcloudair/cap/public_address.rb +18 -0
  38. data/lib/vagrant-vcloudair/cap/rdp_info.rb +18 -0
  39. data/lib/vagrant-vcloudair/cap/winrm_info.rb +15 -0
  40. data/lib/vagrant-vcloudair/command.rb +285 -0
  41. data/lib/vagrant-vcloudair/config.rb +205 -0
  42. data/lib/vagrant-vcloudair/driver/base.rb +643 -0
  43. data/lib/vagrant-vcloudair/driver/meta.rb +202 -0
  44. data/lib/vagrant-vcloudair/driver/version_5_1.rb +2019 -0
  45. data/lib/vagrant-vcloudair/errors.rb +77 -0
  46. data/lib/vagrant-vcloudair/model/forwarded_port.rb +66 -0
  47. data/lib/vagrant-vcloudair/plugin.rb +111 -0
  48. data/lib/vagrant-vcloudair/provider.rb +41 -0
  49. data/lib/vagrant-vcloudair/util/compile_forwarded_ports.rb +34 -0
  50. data/lib/vagrant-vcloudair/version.rb +5 -0
  51. data/locales/en.yml +169 -0
  52. data/vagrant-vcloudair.gemspec +33 -0
  53. metadata +266 -0
@@ -0,0 +1,202 @@
1
+ #
2
+ # Copyright 2012 Stefano Tortarolo
3
+ # Copyright 2013 Fabio Rapposelli and Timo Sugliani
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require 'forwardable'
19
+ require 'log4r'
20
+ require 'nokogiri'
21
+ require 'httpclient'
22
+
23
+ require File.expand_path('../base', __FILE__)
24
+
25
+ module VagrantPlugins
26
+ module VCloudAir
27
+ module Driver
28
+ class Meta < Base
29
+ # We use forwardable to do all our driver forwarding
30
+ extend Forwardable
31
+ attr_reader :driver
32
+
33
+ def initialize(cloud_id, username, password, vdc_name)
34
+ # Setup the base
35
+ super()
36
+
37
+ @username = username
38
+ @password = password
39
+
40
+ @logger = Log4r::Logger.new('vagrant::provider::vcloudair::meta')
41
+
42
+ # Logging into vCloud Air
43
+ params = {
44
+ 'method' => :post,
45
+ 'command' => '/vchs/sessions'
46
+ }
47
+
48
+ _response, headers = send_vcloudair_request(params)
49
+
50
+ unless headers.key?('x-vchs-authorization')
51
+ fail Errors::InvalidRequestError,
52
+ :message => 'Failed to authenticate: ' \
53
+ 'missing x-vchs-authorization header'
54
+ end
55
+
56
+ @vcloudair_auth_key = headers['x-vchs-authorization']
57
+
58
+ # Get Services available
59
+ params = {
60
+ 'method' => :get,
61
+ 'command' => '/vchs/services'
62
+ }
63
+
64
+ response, _headers = send_vcloudair_request(params)
65
+ services = response.css('Services Service')
66
+
67
+ service_id = cloud_id || vdc_name
68
+ services.each do |service|
69
+ if service['serviceId'] == service_id
70
+ @compute_id = URI(service['href']).path.gsub('/api', '')
71
+ end
72
+ end
73
+
74
+ fail Errors::ServiceNotFound if @compute_id.nil?
75
+
76
+ # Get Service Link to vCloud Director
77
+ params = {
78
+ 'method' => :get,
79
+ 'command' => @compute_id
80
+ }
81
+
82
+ response, _headers = send_vcloudair_request(params)
83
+
84
+ vdcs = response.css('Compute VdcRef')
85
+
86
+ vdcs.each do |vdc|
87
+ if vdc['name'] == vdc_name
88
+ @vdc_id = URI(vdc['href']).path.gsub('/api', '')
89
+ end
90
+ end
91
+
92
+ fail Errors::VdcNotFound, :message => vdc_name if @vdc_id.nil?
93
+
94
+ # Authenticate to vCloud Director
95
+ params = {
96
+ 'method' => :post,
97
+ 'command' => "#{@vdc_id}/vcloudsession"
98
+ }
99
+
100
+ response, _headers = send_vcloudair_request(params)
101
+
102
+ vdclinks = response.css('VCloudSession VdcLink')
103
+
104
+ vdclinks.each do |vdclink|
105
+ if vdclink['name'] == service_id
106
+ uri = URI(vdclink['href'])
107
+ @api_url = "#{uri.scheme}://#{uri.host}:#{uri.port}/api"
108
+ @host_url = "#{uri.scheme}://#{uri.host}:#{uri.port}"
109
+ @auth_key = vdclink['authorizationToken']
110
+ end
111
+ end
112
+
113
+ fail Errors::ObjectNotFound,
114
+ :message => 'Cannot find link to backend \
115
+ vCloud Director Instance' if @api_url.nil?
116
+
117
+ @org_name = vdc_name
118
+
119
+ # Read and assign the version of vCloud Air we know which
120
+ # specific driver to instantiate.
121
+ @logger.debug("Asking API Version with host_url: #{@host_url}")
122
+ @version = get_api_version(@host_url) || ''
123
+
124
+ # Instantiate the proper version driver for vCloud Air
125
+ @logger.debug("Finding driver for vCloud Air version: #{@version}")
126
+ driver_map = {
127
+ '5.1' => Version_5_1,
128
+ '5.5' => Version_5_1,
129
+ '5.6' => Version_5_1,
130
+ '5.7' => Version_5_1
131
+ }
132
+
133
+ if @version.start_with?('0.9') ||
134
+ @version.start_with?('1.0') ||
135
+ @version.start_with?('1.5')
136
+ # We only support vCloud Air 5.1 or higher.
137
+ fail Errors::VCloudAirOldVersion, :version => @version
138
+ end
139
+
140
+ driver_klass = nil
141
+ driver_map.each do |key, klass|
142
+ if @version.start_with?(key)
143
+ driver_klass = klass
144
+ break
145
+ end
146
+ end
147
+
148
+ unless driver_klass
149
+ supported_versions = driver_map.keys.sort.join(', ')
150
+ fail Errors::VCloudAirInvalidVersion,
151
+ :supported_versions => supported_versions
152
+ end
153
+
154
+ @logger.info("Using vCloud Air driver: #{driver_klass}")
155
+ @driver = driver_klass.new(@api_url, @host_url, @auth_key, @org_name)
156
+ end
157
+
158
+ def_delegators :@driver,
159
+ :login,
160
+ :logout,
161
+ :get_organizations,
162
+ :get_organization_id_by_name,
163
+ :get_organization_by_name,
164
+ :get_organization,
165
+ :get_catalog,
166
+ :get_catalog_id_by_name,
167
+ :get_catalog_by_name,
168
+ :get_vdc,
169
+ :get_vdc_id_by_name,
170
+ :get_vdc_by_name,
171
+ :get_catalog_item,
172
+ :get_catalog_item_by_name,
173
+ :get_vapp,
174
+ :delete_vapp,
175
+ :poweroff_vapp,
176
+ :suspend_vapp,
177
+ :reboot_vapp,
178
+ :reset_vapp,
179
+ :poweron_vapp,
180
+ :create_vapp_from_template,
181
+ :compose_vapp_from_vm,
182
+ :get_vapp_template,
183
+ :set_vapp_port_forwarding_rules,
184
+ :get_vapp_port_forwarding_rules,
185
+ :get_vapp_edge_public_ip,
186
+ :upload_ovf,
187
+ :get_task,
188
+ :wait_task_completion,
189
+ :set_vapp_network_config,
190
+ :set_vm_network_config,
191
+ :set_vm_guest_customization,
192
+ :get_vm,
193
+ :send_request,
194
+ :upload_file,
195
+ :convert_vapp_status
196
+
197
+ protected
198
+
199
+ end
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,2019 @@
1
+ #
2
+ # Copyright 2012 Stefano Tortarolo
3
+ # Copyright 2013 Fabio Rapposelli and Timo Sugliani
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require 'ruby-progressbar'
19
+ require 'set'
20
+ require 'netaddr'
21
+ require 'uri'
22
+
23
+ module VagrantPlugins
24
+ module VCloudAir
25
+ module Driver
26
+ # Main class to access vCloud Air rest APIs
27
+ class Version_5_1 < Base
28
+ attr_reader :auth_key, :id
29
+
30
+ ##
31
+ # Init the driver with the Vagrantfile information
32
+ def initialize(api_url, host_url, auth_key, org_name)
33
+ @logger = Log4r::Logger.new('vagrant::provider::vcloudair::driver_5_1')
34
+ @api_url = api_url
35
+ @host_url = host_url
36
+ @auth_key = auth_key
37
+ @org_name = org_name
38
+ @api_version = '5.1'
39
+ @id = nil
40
+
41
+ @cached_vapp_edge_public_ips = {}
42
+ end
43
+
44
+ ##
45
+ # Authenticate against the specified server
46
+ def login
47
+ end
48
+
49
+ ##
50
+ # Destroy the current session
51
+ def logout
52
+ params = {
53
+ 'method' => :delete,
54
+ 'command' => '/session'
55
+ }
56
+
57
+ _response, _headers = send_request(params)
58
+ # reset auth key to nil
59
+ @auth_key = nil
60
+ end
61
+
62
+ ##
63
+ # Fetch existing organizations and their IDs
64
+ def get_organizations
65
+ params = {
66
+ 'method' => :get,
67
+ 'command' => '/org'
68
+ }
69
+
70
+ response, _headers = send_request(params)
71
+ orgs = response.css('OrgList Org')
72
+
73
+ results = {}
74
+ orgs.each do |org|
75
+ results[org['name']] = URI(org['href']).path.gsub('/api/org/', '')
76
+ end
77
+ results
78
+ end
79
+
80
+ ##
81
+ # friendly helper method to fetch an Organization Id by name
82
+ # - name (this isn't case sensitive)
83
+ def get_organization_id_by_name(name)
84
+ result = nil
85
+
86
+ # Fetch all organizations
87
+ organizations = get_organizations
88
+
89
+ organizations.each do |organization|
90
+ if organization[0].downcase == name.downcase
91
+ result = organization[1]
92
+ end
93
+ end
94
+ result
95
+ end
96
+
97
+ ##
98
+ # friendly helper method to fetch an Organization by name
99
+ # - name (this isn't case sensitive)
100
+ def get_organization_by_name(name)
101
+ result = nil
102
+
103
+ # Fetch all organizations
104
+ organizations = get_organizations
105
+
106
+ organizations.each do |organization|
107
+ if organization[0].downcase == name.downcase
108
+ result = get_organization(organization[1])
109
+ end
110
+ end
111
+ result
112
+ end
113
+
114
+ ##
115
+ # Fetch details about an organization:
116
+ # - catalogs
117
+ # - vdcs
118
+ # - networks
119
+ def get_organization(org_id)
120
+ params = {
121
+ 'method' => :get,
122
+ 'command' => "/org/#{org_id}"
123
+ }
124
+
125
+ response, _headers = send_request(params)
126
+
127
+ catalogs = {}
128
+ response.css(
129
+ "Link[type='application/vnd.vmware.vcloud.catalog+xml']"
130
+ ).each do |item|
131
+ catalogs[item['name']] = URI(item['href']).path.gsub(
132
+ '/api/catalog/', ''
133
+ )
134
+ end
135
+
136
+ vdcs = {}
137
+ response.css(
138
+ "Link[type='application/vnd.vmware.vcloud.vdc+xml']"
139
+ ).each do |item|
140
+ vdcs[item['name']] = URI(item['href']).path.gsub(
141
+ '/api/vdc/', ''
142
+ )
143
+ end
144
+
145
+ networks = {}
146
+ response.css(
147
+ "Link[type='application/vnd.vmware.vcloud.orgNetwork+xml']"
148
+ ).each do |item|
149
+ networks[item['name']] = URI(item['href']).path.gsub(
150
+ '/api/network/', ''
151
+ )
152
+ end
153
+
154
+ tasklists = {}
155
+ response.css(
156
+ "Link[type='application/vnd.vmware.vcloud.tasksList+xml']"
157
+ ).each do |item|
158
+ tasklists[item['name']] = URI(item['href']).path.gsub(
159
+ '/api/tasksList/', ''
160
+ )
161
+ end
162
+
163
+ {
164
+ :catalogs => catalogs,
165
+ :vdcs => vdcs,
166
+ :networks => networks,
167
+ :tasklists => tasklists
168
+ }
169
+ end
170
+
171
+ ##
172
+ # Fetch details about a given catalog
173
+ def get_catalog(catalog_id)
174
+ params = {
175
+ 'method' => :get,
176
+ 'command' => "/catalog/#{catalog_id}"
177
+ }
178
+
179
+ response, _headers = send_request(params)
180
+ description = response.css('Description').first
181
+ description = description.text unless description.nil?
182
+
183
+ items = {}
184
+ response.css(
185
+ "CatalogItem[type='application/vnd.vmware.vcloud.catalogItem+xml']"
186
+ ).each do |item|
187
+ items[item['name']] = URI(item['href']).path.gsub(
188
+ '/api/catalogItem/', ''
189
+ )
190
+ end
191
+ { :description => description, :items => items }
192
+ end
193
+
194
+ ##
195
+ # Friendly helper method to fetch an catalog id by name
196
+ # - organization hash (from get_organization/get_organization_by_name)
197
+ # - catalog name
198
+ def get_catalog_id_by_name(organization, catalog_name)
199
+ result = nil
200
+
201
+ organization[:catalogs].each do |catalog|
202
+ if catalog[0].downcase == catalog_name.downcase
203
+ result = catalog[1]
204
+ end
205
+ end
206
+
207
+ if result.nil?
208
+ # catalog not found, search in global catalogs as well
209
+ # that are not listed in organization directly
210
+ params = {
211
+ 'method' => :get,
212
+ 'command' => '/catalogs/query/',
213
+ 'cacheable' => true
214
+ }
215
+
216
+ response, _headers = send_request(params)
217
+
218
+ catalogs = {}
219
+ response.css(
220
+ 'CatalogRecord'
221
+ ).each do |item|
222
+ catalogs[item['name']] = URI(item['href']).path.gsub(
223
+ '/api/catalog/', ''
224
+ )
225
+ end
226
+
227
+ catalogs.each do |catalog|
228
+ if catalog[0].downcase == catalog_name.downcase
229
+ result = catalog[1]
230
+ end
231
+ end
232
+
233
+ end
234
+
235
+ result
236
+ end
237
+
238
+ ##
239
+ # Friendly helper method to fetch an catalog by name
240
+ # - organization hash (from get_organization/get_organization_by_name)
241
+ # - catalog name
242
+ def get_catalog_by_name(organization, catalog_name)
243
+ result = nil
244
+
245
+ organization[:catalogs].each do |catalog|
246
+ if catalog[0].downcase == catalog_name.downcase
247
+ result = get_catalog(catalog[1])
248
+ end
249
+ end
250
+
251
+ result
252
+ end
253
+
254
+ ##
255
+ # Fetch details about a given vdc:
256
+ # - description
257
+ # - vapps
258
+ # - networks
259
+ def get_vdc(vdc_id)
260
+ params = {
261
+ 'method' => :get,
262
+ 'command' => "/vdc/#{vdc_id}"
263
+ }
264
+
265
+ response, _headers = send_request(params)
266
+ description = response.css('Description').first
267
+ description = description.text unless description.nil?
268
+
269
+ vapps = {}
270
+ response.css(
271
+ "ResourceEntity[type='application/vnd.vmware.vcloud.vApp+xml']"
272
+ ).each do |item|
273
+ vapps[item['name']] = URI(item['href']).path.gsub(
274
+ '/api/vApp/vapp-', ''
275
+ )
276
+ end
277
+
278
+ networks = {}
279
+ response.css(
280
+ "Network[type='application/vnd.vmware.vcloud.network+xml']"
281
+ ).each do |item|
282
+ networks[item['name']] = URI(item['href']).path.gsub(
283
+ '/api/network/', ''
284
+ )
285
+ end
286
+ {
287
+ :description => description, :vapps => vapps, :networks => networks
288
+ }
289
+ end
290
+
291
+ ##
292
+ # Friendly helper method to fetch a Organization VDC Id by name
293
+ # - Organization object
294
+ # - Organization VDC Name
295
+ def get_vdc_id_by_name(organization, vdc_name)
296
+ result = nil
297
+
298
+ organization[:vdcs].each do |vdc|
299
+ if vdc[0].downcase == vdc_name.downcase
300
+ result = vdc[1]
301
+ end
302
+ end
303
+
304
+ result
305
+ end
306
+
307
+ ##
308
+ # Friendly helper method to fetch a Organization VDC by name
309
+ # - Organization object
310
+ # - Organization VDC Name
311
+ def get_vdc_by_name(organization, vdc_name)
312
+ result = nil
313
+
314
+ organization[:vdcs].each do |vdc|
315
+ if vdc[0].downcase == vdc_name.downcase
316
+ result = get_vdc(vdc[1])
317
+ end
318
+ end
319
+
320
+ result
321
+ end
322
+
323
+ ##
324
+ # Fetch details about a given catalog item:
325
+ # - description
326
+ # - vApp templates
327
+ def get_catalog_item(catalog_item_id)
328
+ params = {
329
+ 'method' => :get,
330
+ 'command' => "/catalogItem/#{catalog_item_id}"
331
+ }
332
+
333
+ response, _headers = send_request(params)
334
+ description = response.css('Description').first
335
+ description = description.text unless description.nil?
336
+
337
+ items = {}
338
+ response.css(
339
+ "Entity[type='application/vnd.vmware.vcloud.vAppTemplate+xml']"
340
+ ).each do |item|
341
+ items[item['name']] = URI(item['href']).path.gsub(
342
+ '/api/vAppTemplate/vappTemplate-', ''
343
+ )
344
+ end
345
+ { :description => description, :items => items }
346
+ end
347
+
348
+ ##
349
+ # friendly helper method to fetch an catalogItem by name
350
+ # - catalogId (use get_catalog_name(org, name))
351
+ # - catalagItemName
352
+ def get_catalog_item_by_name(catalog_id, catalog_item_name)
353
+ result = nil
354
+ catalog_elems = get_catalog(catalog_id)
355
+
356
+ catalog_elems[:items].each do |catalog_elem|
357
+
358
+ catalog_item = get_catalog_item(catalog_elem[1])
359
+ if catalog_item[:items][catalog_item_name]
360
+ # This is a vApp Catalog Item
361
+
362
+ # fetch CatalogItemId
363
+ catalog_item_id = catalog_item[:items][catalog_item_name]
364
+
365
+ # Fetch the catalogItemId information
366
+ params = {
367
+ 'method' => :get,
368
+ 'command' => "/vAppTemplate/vappTemplate-#{catalog_item_id}"
369
+ }
370
+ response, _headers = send_request(params)
371
+
372
+ # VMs Hash for all the vApp VM entities
373
+ vms_hash = {}
374
+ response.css('/VAppTemplate/Children/Vm').each do |vm_elem|
375
+ vm_name = vm_elem['name']
376
+ vm_id = URI(vm_elem['href']).path.gsub(
377
+ '/api/vAppTemplate/vm-', ''
378
+ )
379
+
380
+ # Add the VM name/id to the VMs Hash
381
+ vms_hash[vm_name] = { :id => vm_id }
382
+ end
383
+ result = {
384
+ catalog_item_name => catalog_item_id, :vms_hash => vms_hash
385
+ }
386
+ end
387
+ end
388
+ result
389
+ end
390
+
391
+ ##
392
+ # Fetch details about a given vapp:
393
+ # - name
394
+ # - description
395
+ # - status
396
+ # - IP
397
+ # - Children VMs:
398
+ # -- IP addresses
399
+ # -- status
400
+ # -- ID
401
+ def get_vapp(vapp_id)
402
+ params = {
403
+ 'method' => :get,
404
+ 'command' => "/vApp/vapp-#{vapp_id}"
405
+ }
406
+
407
+ response, _headers = send_request(params)
408
+
409
+ vapp_node = response.css('VApp').first
410
+ if vapp_node
411
+ name = vapp_node['name']
412
+ status = convert_vapp_status(vapp_node['status'])
413
+ end
414
+
415
+ description = response.css('Description').first
416
+ description = description.text unless description.nil?
417
+
418
+ ip = response.css('IpAddress').first
419
+ ip = ip.text unless ip.nil?
420
+
421
+ vms = response.css('Children Vm')
422
+ vms_hash = {}
423
+
424
+ # ipAddress could be namespaced or not:
425
+ # see https://github.com/astratto/vcloud-rest/issues/3
426
+ vms.each do |vm|
427
+ vapp_local_id = vm.css('VAppScopedLocalId')
428
+ addresses = vm.css('rasd|Connection').collect {
429
+ |n| n['vcloud:ipAddress'] || n['ipAddress']
430
+ }
431
+ vms_hash[vm['name'].to_sym] = {
432
+ :addresses => addresses,
433
+ :status => convert_vapp_status(vm['status']),
434
+ :id => URI(vm['href']).path.gsub('/api/vApp/vm-', ''),
435
+ :vapp_scoped_local_id => vapp_local_id.text
436
+ }
437
+ end
438
+
439
+ # TODO: EXPAND INFO FROM RESPONSE
440
+ {
441
+ :name => name,
442
+ :description => description,
443
+ :status => status,
444
+ :ip => ip,
445
+ :vms_hash => vms_hash
446
+ }
447
+ end
448
+
449
+ ##
450
+ # Delete a given vapp
451
+ # NOTE: It doesn't verify that the vapp is shutdown
452
+ def delete_vapp(vapp_id)
453
+ params = {
454
+ 'method' => :delete,
455
+ 'command' => "/vApp/vapp-#{vapp_id}"
456
+ }
457
+
458
+ _response, headers = send_request(params)
459
+ task_id = URI(headers['Location']).path.gsub('/api/task/', '')
460
+ task_id
461
+ end
462
+
463
+ ##
464
+ # Shutdown a given vapp
465
+ def poweroff_vapp(vapp_id)
466
+ builder = Nokogiri::XML::Builder.new do |xml|
467
+ xml.UndeployVAppParams(
468
+ 'xmlns' => 'http://www.vmware.com/vcloud/v1.5'
469
+ ) { xml.UndeployPowerAction 'powerOff' }
470
+ end
471
+
472
+ params = {
473
+ 'method' => :post,
474
+ 'command' => "/vApp/vapp-#{vapp_id}/action/undeploy"
475
+ }
476
+
477
+ _response, headers = send_request(
478
+ params,
479
+ builder.to_xml,
480
+ 'application/vnd.vmware.vcloud.undeployVAppParams+xml'
481
+ )
482
+ task_id = URI(headers['Location']).path.gsub('/api/task/', '')
483
+ task_id
484
+ end
485
+
486
+ ##
487
+ # Suspend a given vapp
488
+ def suspend_vapp(vapp_id)
489
+ params = {
490
+ 'method' => :post,
491
+ 'command' => "/vApp/vapp-#{vapp_id}/power/action/suspend"
492
+ }
493
+
494
+ _response, headers = send_request(params)
495
+ task_id = URI(headers['Location']).path.gsub('/api/task/', '')
496
+ task_id
497
+ end
498
+
499
+ ##
500
+ # reboot a given vapp
501
+ # This will basically initial a guest OS reboot, and will only work if
502
+ # VMware-tools are installed on the underlying VMs.
503
+ # vShield Edge devices are not affected
504
+ def reboot_vapp(vapp_id)
505
+ params = {
506
+ 'method' => :post,
507
+ 'command' => "/vApp/vapp-#{vapp_id}/power/action/reboot"
508
+ }
509
+
510
+ _response, headers = send_request(params)
511
+ task_id = URI(headers['Location']).path.gsub('/api/task/', '')
512
+ task_id
513
+ end
514
+
515
+ ##
516
+ # reset a given vapp
517
+ # This will basically reset the VMs within the vApp
518
+ # vShield Edge devices are not affected.
519
+ def reset_vapp(vapp_id)
520
+ params = {
521
+ 'method' => :post,
522
+ 'command' => "/vApp/vapp-#{vapp_id}/power/action/reset"
523
+ }
524
+
525
+ _response, headers = send_request(params)
526
+ task_id = URI(headers['Location']).path.gsub('/api/task/', '')
527
+ task_id
528
+ end
529
+
530
+ ##
531
+ # Boot a given vapp
532
+ def poweron_vapp(vapp_id)
533
+ params = {
534
+ 'method' => :post,
535
+ 'command' => "/vApp/vapp-#{vapp_id}/power/action/powerOn"
536
+ }
537
+
538
+ _response, headers = send_request(params)
539
+ task_id = URI(headers['Location']).path.gsub('/api/task/', '')
540
+ task_id
541
+ end
542
+
543
+ #### VM operations ####
544
+ ##
545
+ # Delete a given vm
546
+ # NOTE: It doesn't verify that the vm is shutdown
547
+ def delete_vm(vm_id)
548
+ params = {
549
+ 'method' => :delete,
550
+ 'command' => "/vApp/vm-#{vm_id}"
551
+ }
552
+
553
+ _response, headers = send_request(params)
554
+ task_id = URI(headers['Location']).path.gsub('/api/task/', '')
555
+ task_id
556
+ end
557
+
558
+ ##
559
+ # Shutdown a given VM
560
+ # Using undeploy as a REAL powerOff
561
+ # Only poweroff will put the VM into a partially powered off state.
562
+ def poweroff_vm(vm_id)
563
+ builder = Nokogiri::XML::Builder.new do |xml|
564
+ xml.UndeployVAppParams(
565
+ 'xmlns' => 'http://www.vmware.com/vcloud/v1.5'
566
+ ) { xml.UndeployPowerAction 'powerOff' }
567
+ end
568
+
569
+ params = {
570
+ 'method' => :post,
571
+ 'command' => "/vApp/vm-#{vm_id}/action/undeploy"
572
+ }
573
+
574
+ _response, headers = send_request(
575
+ params,
576
+ builder.to_xml,
577
+ 'application/vnd.vmware.vcloud.undeployVAppParams+xml'
578
+ )
579
+ task_id = URI(headers['Location']).path.gsub('/api/task/', '')
580
+ task_id
581
+ end
582
+
583
+ ##
584
+ # Suspend a given VM
585
+ def suspend_vm(vm_id)
586
+ builder = Nokogiri::XML::Builder.new do |xml|
587
+ xml.UndeployVAppParams(
588
+ 'xmlns' => 'http://www.vmware.com/vcloud/v1.5'
589
+ ) { xml.UndeployPowerAction 'suspend' }
590
+ end
591
+
592
+ params = {
593
+ 'method' => :post,
594
+ 'command' => "/vApp/vm-#{vm_id}/action/undeploy"
595
+ }
596
+
597
+ _response, headers = send_request(
598
+ params,
599
+ builder.to_xml,
600
+ 'application/vnd.vmware.vcloud.undeployVAppParams+xml'
601
+ )
602
+ task_id = URI(headers['Location']).path.gsub('/api/task/', '')
603
+ task_id
604
+ end
605
+
606
+ ##
607
+ # reboot a given VM
608
+ # This will basically initial a guest OS reboot, and will only work if
609
+ # VMware-tools are installed on the underlying VMs.
610
+ # vShield Edge devices are not affected
611
+ def reboot_vm(vm_id)
612
+ params = {
613
+ 'method' => :post,
614
+ 'command' => "/vApp/vm-#{vm_id}/power/action/reboot"
615
+ }
616
+
617
+ _response, headers = send_request(params)
618
+ task_id = URI(headers['Location']).path.gsub('/api/task/', '')
619
+ task_id
620
+ end
621
+
622
+ ##
623
+ # reset a given VM
624
+ # This will basically reset the VMs within the vApp
625
+ # vShield Edge devices are not affected.
626
+ def reset_vm(vm_id)
627
+ params = {
628
+ 'method' => :post,
629
+ 'command' => "/vApp/vm-#{vm_id}/power/action/reset"
630
+ }
631
+
632
+ _response, headers = send_request(params)
633
+ task_id = URI(headers['Location']).path.gsub('/api/task/', '')
634
+ task_id
635
+ end
636
+
637
+ ##
638
+ # Boot a given VM
639
+ def poweron_vm(vm_id)
640
+ params = {
641
+ 'method' => :post,
642
+ 'command' => "/vApp/vm-#{vm_id}/power/action/powerOn"
643
+ }
644
+
645
+ _response, headers = send_request(params)
646
+ task_id = URI(headers['Location']).path.gsub('/api/task/', '')
647
+ task_id
648
+ end
649
+
650
+ ##
651
+ # Create a catalog in an organization
652
+ def create_catalog(org_id, catalog_name, catalog_description)
653
+ builder = Nokogiri::XML::Builder.new do |xml|
654
+ xml.AdminCatalog(
655
+ 'xmlns' => 'http://www.vmware.com/vcloud/v1.5',
656
+ 'name' => catalog_name
657
+ ) { xml.Description catalog_description }
658
+
659
+ end
660
+
661
+ params = {
662
+ 'method' => :post,
663
+ 'command' => "/admin/org/#{org_id}/catalogs"
664
+ }
665
+
666
+ response, _headers = send_request(
667
+ params,
668
+ builder.to_xml,
669
+ 'application/vnd.vmware.admin.catalog+xml'
670
+ )
671
+ task_id = URI(response.css(
672
+ "AdminCatalog Tasks Task[operationName='catalogCreateCatalog']"
673
+ ).first[:href]).path.gsub('/api/task/', '')
674
+
675
+ catalog_id = URI(response.css(
676
+ "AdminCatalog Link [type='application/vnd.vmware.vcloud.catalog+xml']"
677
+ ).first[:href]).path.gsub('/api/catalog/', '')
678
+
679
+ { :task_id => task_id, :catalog_id => catalog_id }
680
+ end
681
+
682
+ ##
683
+ # Create a vapp starting from a template
684
+ #
685
+ # Params:
686
+ # - vdc: the associated VDC
687
+ # - vapp_name: name of the target vapp
688
+ # - vapp_description: description of the target vapp
689
+ # - vapp_templateid: ID of the vapp template
690
+ def create_vapp_from_template(vdc, vapp_name, vapp_description, vapp_template_id, poweron = false)
691
+ builder = Nokogiri::XML::Builder.new do |xml|
692
+ xml.InstantiateVAppTemplateParams(
693
+ 'xmlns' => 'http://www.vmware.com/vcloud/v1.5',
694
+ 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
695
+ 'xmlns:ovf' => 'http://schemas.dmtf.org/ovf/envelope/1',
696
+ 'name' => vapp_name,
697
+ 'deploy' => 'true',
698
+ 'powerOn' => poweron
699
+ ) { xml.Description vapp_description xml.Source(
700
+ 'href' => "#{@api_url}/vAppTemplate/#{vapp_template_id}"
701
+ )
702
+ }
703
+ end
704
+
705
+ params = {
706
+ 'method' => :post,
707
+ 'command' => "/vdc/#{vdc}/action/instantiateVAppTemplate"
708
+ }
709
+
710
+ response, headers = send_request(
711
+ params,
712
+ builder.to_xml,
713
+ 'application/vnd.vmware.vcloud.instantiateVAppTemplateParams+xml'
714
+ )
715
+
716
+ vapp_id = URI(headers['Location']).path.gsub('/api/vApp/vapp-', '')
717
+
718
+ task = response.css(
719
+ "VApp Task[operationName='vdcInstantiateVapp']"
720
+ ).first
721
+
722
+ task_id = URI(task['href']).path.gsub("/api/task/", '')
723
+
724
+ { :vapp_id => vapp_id, :task_id => task_id }
725
+ end
726
+
727
+ ##
728
+ # Compose a vapp using existing virtual machines
729
+ #
730
+ # Params:
731
+ # - vdc: the associated VDC
732
+ # - vapp_name: name of the target vapp
733
+ # - vapp_description: description of the target vapp
734
+ # - vm_list: hash with IDs of the VMs used in the composing process
735
+ # - network_config: hash of the network configuration for the vapp
736
+ def compose_vapp_from_vm(vdc, vapp_name, vapp_description, vm_list = {}, network_config = {})
737
+ builder = Nokogiri::XML::Builder.new do |xml|
738
+ xml.ComposeVAppParams(
739
+ 'xmlns' => 'http://www.vmware.com/vcloud/v1.5',
740
+ 'xmlns:ovf' => 'http://schemas.dmtf.org/ovf/envelope/1',
741
+ 'name' => vapp_name,
742
+ 'deploy' => 'false',
743
+ 'powerOn' => 'false') {
744
+ xml.Description vapp_description
745
+ xml.InstantiationParams {
746
+ xml.NetworkConfigSection {
747
+ xml['ovf'].Info 'Configuration parameters for logical networks'
748
+ xml.NetworkConfig('networkName' => network_config[:name]) {
749
+ xml.Configuration {
750
+ if network_config[:fence_mode] != 'bridged'
751
+ xml.IpScopes {
752
+ xml.IpScope {
753
+ xml.IsInherited(network_config[:is_inherited] || 'false')
754
+ xml.Gateway network_config[:gateway]
755
+ xml.Netmask network_config[:netmask]
756
+ xml.Dns1 network_config[:dns1] if network_config[:dns1]
757
+ xml.Dns2 network_config[:dns2] if network_config[:dns2]
758
+ xml.DnsSuffix network_config[:dns_suffix] if network_config[:dns_suffix]
759
+ xml.IpRanges {
760
+ xml.IpRange {
761
+ xml.StartAddress network_config[:start_address]
762
+ xml.EndAddress network_config[:end_address]
763
+ }
764
+ }
765
+ }
766
+ }
767
+ end
768
+ xml.ParentNetwork("href" => "#{@api_url}/network/#{network_config[:parent_network]}")
769
+ xml.FenceMode network_config[:fence_mode]
770
+ if network_config[:fence_mode] != 'bridged'
771
+ xml.Features {
772
+ xml.FirewallService {
773
+ xml.IsEnabled(network_config[:enable_firewall] || "false")
774
+ }
775
+ xml.NatService {
776
+ xml.IsEnabled "true"
777
+ xml.NatType "portForwarding"
778
+ xml.Policy(network_config[:nat_policy_type] || "allowTraffic")
779
+ }
780
+ }
781
+ end
782
+ }
783
+ }
784
+ }
785
+ }
786
+ vm_list.each do |vm_name, vm_id|
787
+ xml.SourcedItem {
788
+ xml.Source('href' => "#{@api_url}/vAppTemplate/vm-#{vm_id}", 'name' => vm_name)
789
+ xml.InstantiationParams {
790
+ xml.NetworkConnectionSection(
791
+ 'xmlns:ovf' => 'http://schemas.dmtf.org/ovf/envelope/1',
792
+ 'type' => 'application/vnd.vmware.vcloud.networkConnectionSection+xml',
793
+ 'href' => "#{@api_url}/vAppTemplate/vm-#{vm_id}/networkConnectionSection/") {
794
+ xml['ovf'].Info 'Network config for sourced item'
795
+ xml.PrimaryNetworkConnectionIndex '0'
796
+ xml.NetworkConnection('network' => network_config[:name]) {
797
+ xml.NetworkConnectionIndex '0'
798
+ xml.IsConnected 'true'
799
+ xml.IpAddressAllocationMode(network_config[:ip_allocation_mode] || 'POOL')
800
+ }
801
+ }
802
+ }
803
+ xml.NetworkAssignment('containerNetwork' => network_config[:name], 'innerNetwork' => network_config[:name])
804
+ }
805
+ end
806
+ xml.AllEULAsAccepted 'true'
807
+ }
808
+ end
809
+
810
+ params = {
811
+ 'method' => :post,
812
+ 'command' => "/vdc/#{vdc}/action/composeVApp"
813
+ }
814
+
815
+ response, headers = send_request(
816
+ params,
817
+ builder.to_xml,
818
+ 'application/vnd.vmware.vcloud.composeVAppParams+xml'
819
+ )
820
+
821
+ vapp_id = URI(headers['Location']).path.gsub("/api/vApp/vapp-", '')
822
+
823
+ task = response.css("VApp Task[operationName='vdcComposeVapp']").first
824
+ task_id = URI(task['href']).path.gsub('/api/task/', '')
825
+
826
+ { :vapp_id => vapp_id, :task_id => task_id }
827
+ end
828
+
829
+ ##
830
+ # Recompose an existing vapp using existing virtual machines
831
+ #
832
+ # Params:
833
+ # - vdc: the associated VDC
834
+ # - vapp_name: name of the target vapp
835
+ # - vapp_description: description of the target vapp
836
+ # - vm_list: hash with IDs of the VMs to be used in the composing process
837
+ # - network_config: hash of the network configuration for the vapp
838
+
839
+ def recompose_vapp_from_vm(vapp_id, vm_list = {}, network_config = {})
840
+ original_vapp = get_vapp(vapp_id)
841
+
842
+ builder = Nokogiri::XML::Builder.new do |xml|
843
+ xml.RecomposeVAppParams(
844
+ 'xmlns' => 'http://www.vmware.com/vcloud/v1.5',
845
+ 'xmlns:ovf' => 'http://schemas.dmtf.org/ovf/envelope/1',
846
+ 'name' => original_vapp[:name]) {
847
+ xml.Description original_vapp[:description]
848
+ xml.InstantiationParams {}
849
+ vm_list.each do |vm_name, vm_id|
850
+ xml.SourcedItem {
851
+ xml.Source('href' => "#{@api_url}/vAppTemplate/vm-#{vm_id}", 'name' => vm_name)
852
+ xml.InstantiationParams {
853
+ xml.NetworkConnectionSection(
854
+ 'xmlns:ovf' => 'http://schemas.dmtf.org/ovf/envelope/1',
855
+ 'type' => 'application/vnd.vmware.vcloud.networkConnectionSection+xml',
856
+ 'href' => "#{@api_url}/vAppTemplate/vm-#{vm_id}/networkConnectionSection/") {
857
+ xml['ovf'].Info 'Network config for sourced item'
858
+ xml.PrimaryNetworkConnectionIndex '0'
859
+ xml.NetworkConnection('network' => network_config[:name]) {
860
+ xml.NetworkConnectionIndex '0'
861
+ xml.IsConnected 'true'
862
+ xml.IpAddressAllocationMode(network_config[:ip_allocation_mode] || 'POOL')
863
+ }
864
+ }
865
+ }
866
+ xml.NetworkAssignment('containerNetwork' => network_config[:name], 'innerNetwork' => network_config[:name])
867
+ }
868
+ end
869
+ xml.AllEULAsAccepted 'true'
870
+ }
871
+ end
872
+
873
+ params = {
874
+ 'method' => :post,
875
+ 'command' => "/vApp/vapp-#{vapp_id}/action/recomposeVApp"
876
+ }
877
+
878
+ response, headers = send_request(
879
+ params,
880
+ builder.to_xml,
881
+ 'application/vnd.vmware.vcloud.recomposeVAppParams+xml'
882
+ )
883
+
884
+ vapp_id = URI(headers['Location']).path.gsub('/api/vApp/vapp-', '')
885
+
886
+ task = response.css("Task [operationName='vdcRecomposeVapp']").first
887
+ task_id = URI(task['href']).path.gsub('/api/task/', '')
888
+
889
+ { :vapp_id => vapp_id, :task_id => task_id }
890
+ end
891
+
892
+ # Fetch details about a given vapp template:
893
+ # - name
894
+ # - description
895
+ # - Children VMs:
896
+ # -- ID
897
+ def get_vapp_template(vapp_id)
898
+ params = {
899
+ 'method' => :get,
900
+ 'command' => "/vAppTemplate/vappTemplate-#{vapp_id}"
901
+ }
902
+
903
+ response, _headers = send_request(params)
904
+
905
+ vapp_node = response.css('VAppTemplate').first
906
+ if vapp_node
907
+ name = vapp_node['name']
908
+ convert_vapp_status(vapp_node['status'])
909
+ end
910
+
911
+ description = response.css('Description').first
912
+ description = description.text unless description.nil?
913
+
914
+ vms = response.css('Children Vm')
915
+ vms_hash = {}
916
+
917
+ vms.each do |vm|
918
+ vms_hash[vm['name']] = {
919
+ :id => URI(vm['href']).path.gsub('/api/vAppTemplate/vm-', '')
920
+ }
921
+ end
922
+
923
+ { :name => name, :description => description, :vms_hash => vms_hash }
924
+ end
925
+
926
+ ##
927
+ # Set vApp port forwarding rules
928
+ #
929
+ # - vapp_id: id of the vapp to be modified
930
+ # - network_name: name of the vapp network to be modified
931
+ # - config: hash with network configuration specifications, must
932
+ # contain an array inside :nat_rules with the nat rules to be
933
+ # applied.
934
+ def set_vapp_port_forwarding_rules(vapp_id, network_name, config = {})
935
+ builder = Nokogiri::XML::Builder.new do |xml|
936
+ xml.NetworkConfigSection(
937
+ 'xmlns' => 'http://www.vmware.com/vcloud/v1.5',
938
+ 'xmlns:ovf' => 'http://schemas.dmtf.org/ovf/envelope/1') {
939
+ xml['ovf'].Info 'Network configuration'
940
+ xml.NetworkConfig('networkName' => network_name) {
941
+ xml.Configuration {
942
+ xml.ParentNetwork('href' => "#{@api_url}/network/#{config[:parent_network]}")
943
+ xml.FenceMode(config[:fence_mode] || 'isolated')
944
+ xml.Features {
945
+ xml.NatService {
946
+ xml.IsEnabled 'true'
947
+ xml.NatType 'portForwarding'
948
+ xml.Policy(config[:nat_policy_type] || 'allowTraffic')
949
+ config[:nat_rules].each do |nat_rule|
950
+ xml.NatRule {
951
+ xml.VmRule {
952
+ xml.ExternalPort nat_rule[:nat_external_port]
953
+ xml.VAppScopedVmId nat_rule[:vapp_scoped_local_id]
954
+ xml.VmNicId(nat_rule[:nat_vmnic_id] || '0')
955
+ xml.InternalPort nat_rule[:nat_internal_port]
956
+ xml.Protocol(nat_rule[:nat_protocol] || 'TCP')
957
+ }
958
+ }
959
+ end
960
+ }
961
+ }
962
+ }
963
+ }
964
+ }
965
+ end
966
+
967
+ params = {
968
+ 'method' => :put,
969
+ 'command' => "/vApp/vapp-#{vapp_id}/networkConfigSection"
970
+ }
971
+
972
+ _response, headers = send_request(
973
+ params,
974
+ builder.to_xml,
975
+ 'application/vnd.vmware.vcloud.networkConfigSection+xml'
976
+ )
977
+
978
+ task_id = URI(headers['Location']).path.gsub("/api/task/", '')
979
+ task_id
980
+ end
981
+
982
+ ##
983
+ # Add vApp port forwarding rules
984
+ #
985
+ # - vapp_id: id of the vapp to be modified
986
+ # - network_name: name of the vapp network to be modified
987
+ # - config: hash with network configuration specifications,
988
+ # must contain an array inside :nat_rules with the nat rules to add.
989
+ # nat_rules << {
990
+ # :nat_external_port => j.to_s,
991
+ # :nat_internal_port => "22",
992
+ # :nat_protocol => "TCP",
993
+ # :vm_scoped_local_id => value[:vapp_scoped_local_id]
994
+ # }
995
+
996
+ def add_vapp_port_forwarding_rules(vapp_id, network_name, config = {})
997
+ builder = Nokogiri::XML::Builder.new do |xml|
998
+ xml.NetworkConfigSection(
999
+ 'xmlns' => 'http://www.vmware.com/vcloud/v1.5',
1000
+ 'xmlns:ovf' => 'http://schemas.dmtf.org/ovf/envelope/1') {
1001
+ xml['ovf'].Info 'Network configuration'
1002
+ xml.NetworkConfig('networkName' => network_name) {
1003
+ xml.Configuration {
1004
+ xml.ParentNetwork('href' => "#{@api_url}/network/#{config[:parent_network]}")
1005
+ xml.FenceMode(config[:fence_mode] || 'isolated')
1006
+ xml.Features {
1007
+ xml.NatService {
1008
+ xml.IsEnabled 'true'
1009
+ xml.NatType 'portForwarding'
1010
+ xml.Policy(config[:nat_policy_type] || 'allowTraffic')
1011
+
1012
+ pre_existing = get_vapp_port_forwarding_rules(vapp_id)
1013
+
1014
+ config[:nat_rules].concat(pre_existing)
1015
+
1016
+ config[:nat_rules].each do |nat_rule|
1017
+ xml.NatRule {
1018
+ xml.VmRule {
1019
+ xml.ExternalPort nat_rule[:nat_external_port]
1020
+ xml.VAppScopedVmId nat_rule[:vapp_scoped_local_id]
1021
+ xml.VmNicId(nat_rule[:nat_vmnic_id] || '0')
1022
+ xml.InternalPort nat_rule[:nat_internal_port]
1023
+ xml.Protocol(nat_rule[:nat_protocol] || 'TCP')
1024
+ }
1025
+ }
1026
+ end
1027
+ }
1028
+ }
1029
+ }
1030
+ }
1031
+ }
1032
+ end
1033
+
1034
+ params = {
1035
+ 'method' => :put,
1036
+ 'command' => "/vApp/vapp-#{vapp_id}/networkConfigSection"
1037
+ }
1038
+
1039
+ _response, headers = send_request(
1040
+ params,
1041
+ builder.to_xml,
1042
+ 'application/vnd.vmware.vcloud.networkConfigSection+xml'
1043
+ )
1044
+
1045
+ task_id = URI(headers['Location']).path.gsub('/api/task/', '')
1046
+ task_id
1047
+ end
1048
+ ##
1049
+ # Get vApp port forwarding rules
1050
+ #
1051
+ # - vapp_id: id of the vApp
1052
+ def get_vapp_port_forwarding_rules(vapp_id)
1053
+ params = {
1054
+ 'method' => :get,
1055
+ 'command' => "/vApp/vapp-#{vapp_id}/networkConfigSection"
1056
+ }
1057
+
1058
+ response, _headers = send_request(params)
1059
+
1060
+ # FIXME: this will return nil if the vApp uses multiple vApp Networks
1061
+ # with Edge devices in natRouted/portForwarding mode.
1062
+ config = response.css(
1063
+ 'NetworkConfigSection/NetworkConfig/Configuration'
1064
+ )
1065
+ fence_mode = config.css('/FenceMode').text
1066
+ nat_type = config.css('/Features/NatService/NatType').text
1067
+
1068
+ unless fence_mode == 'natRouted'
1069
+ fail Errors::InvalidStateError,
1070
+ 'Invalid request because FenceMode must be natRouted.'
1071
+ end
1072
+
1073
+ unless nat_type == 'portForwarding'
1074
+ fail Errors::InvalidStateError,
1075
+ 'Invalid request because NatType must be portForwarding.'
1076
+ end
1077
+
1078
+ nat_rules = []
1079
+ config.css('/Features/NatService/NatRule').each do |rule|
1080
+ # portforwarding rules information
1081
+ vm_rule = rule.css('VmRule')
1082
+
1083
+ nat_rules << {
1084
+ :nat_external_ip => vm_rule.css('ExternalIpAddress').text,
1085
+ :nat_external_port => vm_rule.css('ExternalPort').text,
1086
+ :vapp_scoped_local_id => vm_rule.css('VAppScopedVmId').text,
1087
+ :vm_nic_id => vm_rule.css('VmNicId').text,
1088
+ :nat_internal_port => vm_rule.css('InternalPort').text,
1089
+ :nat_protocol => vm_rule.css('Protocol').text
1090
+ }
1091
+ end
1092
+ nat_rules
1093
+ end
1094
+
1095
+ ##
1096
+ # Find an edge gateway id from the edge name and vdc_id
1097
+ #
1098
+ # - edge_gateway_name: Name of the vSE
1099
+ # - vdc_id: virtual datacenter id
1100
+ #
1101
+ def find_edge_gateway_id(edge_gateway_name, vdc_id)
1102
+ params = {
1103
+ 'method' => :get,
1104
+ 'command' => "/admin/vdc/#{vdc_id}/edgeGateways"
1105
+ }
1106
+
1107
+ response, _headers = send_request(params)
1108
+ edge_gateways = response.css('EdgeGatewayRecord')
1109
+
1110
+ edge_gateways.each do |edge_gateway|
1111
+ if edge_gateway['name'] == edge_gateway_name
1112
+ @edge_gateway_id = URI(edge_gateway['href']).path.gsub(
1113
+ '/api/admin/edgeGateway/', '')
1114
+ end
1115
+ end
1116
+
1117
+ fail Errors::EdgeGWNotFound, :message => edge_gateway_name if @edge_gateway_id.nil?
1118
+
1119
+ @edge_gateway_id
1120
+ end
1121
+
1122
+ ##
1123
+ # Redeploy the vShield Edge Gateway VM, due to some knowns issues
1124
+ # where the current rules are not "applied" and the EdgeGW is in an
1125
+ # unmanageable state.
1126
+ #
1127
+ def redeploy_edge_gateway(edge_gateway_id)
1128
+ params = {
1129
+ 'method' => :post,
1130
+ 'command' => "/admin/edgeGateway/#{edge_gateway_id}/action/redeploy"
1131
+ }
1132
+
1133
+ _response, headers = send_request(params)
1134
+ task_id = URI(headers['Location']).path.gsub('/api/task/', '')
1135
+ task_id
1136
+ end
1137
+
1138
+ ##
1139
+ # Find an edge gateway network from the edge name and vdc_id, and ip
1140
+ #
1141
+ # - edge_gateway_name: Name of the vSE
1142
+ # - vdc_id: virtual datacenter id
1143
+ # - edge_gateway_ip: public ip associated to that vSE
1144
+ #
1145
+
1146
+ def find_edge_gateway_network(edge_gateway_name, vdc_id, edge_gateway_ip)
1147
+ edge_gateway_id = find_edge_gateway_id(edge_gateway_name, vdc_id)
1148
+
1149
+ params = {
1150
+ 'method' => :get,
1151
+ 'command' => "/admin/edgeGateway/#{edge_gateway_id}"
1152
+ }
1153
+
1154
+ response, _headers = send_request(params)
1155
+ response.css(
1156
+ 'EdgeGateway Configuration GatewayInterfaces GatewayInterface'
1157
+ ).each do |gw|
1158
+ # Only check uplinks, avoid another check
1159
+ if gw.css('InterfaceType').text == 'uplink'
1160
+
1161
+ # Loop on all sub-allocation pools
1162
+ gw.css('SubnetParticipation IpRanges IpRange').each do |cur_range|
1163
+
1164
+ low_ip = cur_range.css('StartAddress').first.text
1165
+ high_ip = cur_range.css('EndAddress').first.text
1166
+
1167
+ range_ip_low = NetAddr.ip_to_i(low_ip)
1168
+ range_ip_high = NetAddr.ip_to_i(high_ip)
1169
+ @test_ip = NetAddr.ip_to_i(edge_gateway_ip)
1170
+
1171
+ @logger.debug("Our IP range: #{range_ip_low..range_ip_high}")
1172
+ @logger.debug("Our Test IP #{@test_ip}")
1173
+
1174
+ case @test_ip
1175
+ when range_ip_low..range_ip_high then
1176
+ @logger.debug("Found IP #{NetAddr.i_to_ip(@test_ip)}")
1177
+ @network_id = gw.css('Network').first[:href]
1178
+ end
1179
+ end
1180
+ end
1181
+ end
1182
+
1183
+ fail Errors::EdgeGWIPNotFound,
1184
+ :message => NetAddr.i_to_ip(@test_ip) if @network_id.nil?
1185
+
1186
+ @network_id
1187
+ end
1188
+
1189
+ ##
1190
+ # Check wether the specified Network is connected to the specified
1191
+ # Organization Edge
1192
+ #
1193
+ # - edge_gateway_name: Name of the vSE
1194
+ # - vdc_id: virtual datacenter id
1195
+ # - vdc_network_name: VDC Network Name to check
1196
+ #
1197
+
1198
+ def check_edge_gateway_network(edge_gateway_name, vdc_id, vdc_network_name)
1199
+ edge_gateway_id = find_edge_gateway_id(edge_gateway_name, vdc_id)
1200
+
1201
+ params = {
1202
+ 'method' => :get,
1203
+ 'command' => "/admin/edgeGateway/#{edge_gateway_id}"
1204
+ }
1205
+
1206
+ response, _headers = send_request(params)
1207
+ response.css(
1208
+ 'EdgeGateway Configuration GatewayInterfaces GatewayInterface'
1209
+ ).each do |gw|
1210
+ # Only check internal connections, avoid another check
1211
+ if gw.css('InterfaceType').text == 'internal'
1212
+
1213
+ # Loop on all sub-allocation pools
1214
+ networks = gw.css('Network')
1215
+
1216
+ networks.each do |network|
1217
+ @network_test = true if network['name'] == vdc_network_name
1218
+ end
1219
+ end
1220
+ end
1221
+
1222
+ fail Errors::EdgeGWNotConnected,
1223
+ :edge => edge_gateway_name,
1224
+ :network => vdc_network_name unless @network_test
1225
+
1226
+ @network_test
1227
+ end
1228
+
1229
+ ##
1230
+ # Get Org Edge port forwarding and firewall rules
1231
+ #
1232
+ # - vapp_id: id of the vapp to be modified
1233
+ # - network_name: name of the vapp network to be modified
1234
+ # - config: hash with network configuration specifications,
1235
+ # must contain an array inside :nat_rules with the nat rules
1236
+ # to be applied.
1237
+ def get_edge_gateway_rules(edge_gateway_name, vdc_id)
1238
+ edge_gateway_id = find_edge_gateway_id(edge_gateway_name, vdc_id)
1239
+
1240
+ params = {
1241
+ 'method' => :get,
1242
+ 'command' => "/admin/edgeGateway/#{edge_gateway_id}"
1243
+ }
1244
+
1245
+ response, _headers = send_request(params)
1246
+
1247
+ nat_fw_rules = []
1248
+
1249
+ interesting = response.css(
1250
+ 'EdgeGateway Configuration EdgeGatewayServiceConfiguration'
1251
+ )
1252
+ interesting.css('NatService NatRule').each do |node|
1253
+ if node.css('RuleType').text == 'DNAT'
1254
+ gw_node = node.css('GatewayNatRule')
1255
+ nat_fw_rules << {
1256
+ :rule_type => 'DNAT',
1257
+ :original_ip => gw_node.css('OriginalIp').text,
1258
+ :original_port => gw_node.css('OriginalPort').text,
1259
+ :translated_ip => gw_node.css('TranslatedIp').text,
1260
+ :translated_port => gw_node.css('TranslatedPort').text,
1261
+ :protocol => gw_node.css('Protocol').text,
1262
+ :is_enabled => node.css('IsEnabled').text
1263
+ }
1264
+
1265
+ end
1266
+ if node.css('RuleType').text == 'SNAT'
1267
+ gw_node = node.css('GatewayNatRule')
1268
+ nat_fw_rules << {
1269
+ :rule_type => 'SNAT',
1270
+ :interface_name => gw_node.css('Interface').first['name'],
1271
+ :original_ip => gw_node.css('OriginalIp').text,
1272
+ :translated_ip => gw_node.css('TranslatedIp').text,
1273
+ :is_enabled => node.css('IsEnabled').text
1274
+ }
1275
+ end
1276
+ end
1277
+
1278
+ interesting.css('FirewallService FirewallRule').each do |node|
1279
+ if node.css('Port').text == '-1'
1280
+ nat_fw_rules << {
1281
+ :rule_type => 'Firewall',
1282
+ :id => node.css('Id').text,
1283
+ :policy => node.css('Policy').text,
1284
+ :description => node.css('Description').text,
1285
+ :destination_ip => node.css('DestinationIp').text,
1286
+ :destination_portrange => node.css('DestinationPortRange').text,
1287
+ :source_ip => node.css('SourceIp').text,
1288
+ :source_portrange => node.css('SourcePortRange').text,
1289
+ :is_enabled => node.css('IsEnabled').text
1290
+ }
1291
+ end
1292
+ end
1293
+
1294
+ nat_fw_rules
1295
+ end
1296
+
1297
+ ##
1298
+ # Remove NAT/FW rules from a edge gateway device
1299
+ #
1300
+ # - edge_gateway_name: Name of the vSE
1301
+ # - vdc_id: virtual datacenter id
1302
+ # - edge_gateway_ip: public ip associated the vSE
1303
+ # - vapp_id: vApp identifier to correlate with the vApp Edge
1304
+
1305
+ def remove_edge_gateway_rules(edge_gateway_name, vdc_id, edge_gateway_ip, vapp_id)
1306
+ edge_vapp_ip = get_vapp_edge_public_ip(vapp_id)
1307
+ edge_gateway_id = find_edge_gateway_id(edge_gateway_name, vdc_id)
1308
+
1309
+ params = {
1310
+ 'method' => :get,
1311
+ 'command' => "/admin/edgeGateway/#{edge_gateway_id}"
1312
+ }
1313
+
1314
+ response, _headers = send_request(params)
1315
+
1316
+ interesting = response.css(
1317
+ 'EdgeGateway Configuration EdgeGatewayServiceConfiguration'
1318
+ )
1319
+ interesting.css('NatService NatRule').each do |node|
1320
+ if node.css('RuleType').text == 'DNAT' &&
1321
+ node.css('GatewayNatRule/OriginalIp').text == edge_gateway_ip &&
1322
+ node.css('GatewayNatRule/TranslatedIp').text == edge_vapp_ip
1323
+ node.remove
1324
+ end
1325
+ if node.css('RuleType').text == 'SNAT' &&
1326
+ node.css('GatewayNatRule/OriginalIp').text == edge_vapp_ip &&
1327
+ node.css('GatewayNatRule/TranslatedIp').text == edge_gateway_ip
1328
+ node.remove
1329
+ end
1330
+ end
1331
+
1332
+ interesting.css('FirewallService FirewallRule').each do |node|
1333
+ if node.css('Port').text == '-1' &&
1334
+ node.css('DestinationIp').text == edge_gateway_ip &&
1335
+ node.css('DestinationPortRange').text == 'Any'
1336
+ node.remove
1337
+ end
1338
+ end
1339
+
1340
+ builder = Nokogiri::XML::Builder.new
1341
+ builder << interesting
1342
+
1343
+ remove_edge_rules = Nokogiri::XML(builder.to_xml)
1344
+
1345
+ xml = remove_edge_rules.at_css 'EdgeGatewayServiceConfiguration'
1346
+ xml['xmlns'] = 'http://www.vmware.com/vcloud/v1.5'
1347
+
1348
+ params = {
1349
+ 'method' => :post,
1350
+ 'command' => "/admin/edgeGateway/#{edge_gateway_id}/action/" +
1351
+ 'configureServices'
1352
+ }
1353
+
1354
+ _response, headers = send_request(
1355
+ params,
1356
+ remove_edge_rules.to_xml,
1357
+ 'application/vnd.vmware.admin.edgeGatewayServiceConfiguration+xml'
1358
+ )
1359
+
1360
+ task_id = URI(headers['Location']).path.gsub('/api/task/', '')
1361
+ task_id
1362
+ end
1363
+
1364
+ #
1365
+ # Add Org Edge port forwarding and firewall rules
1366
+ #
1367
+ # - vapp_id: id of the vapp to be modified
1368
+ # - network_name: name of the vapp network to be modified
1369
+ # - ports: array with port numbers to forward 1:1 to vApp.
1370
+ def add_edge_gateway_rules(edge_gateway_name, vdc_id, edge_gateway_ip, vapp_id, ports)
1371
+ edge_vapp_ip = get_vapp_edge_public_ip(vapp_id)
1372
+ edge_network_id = find_edge_gateway_network(
1373
+ edge_gateway_name,
1374
+ vdc_id,
1375
+ edge_gateway_ip
1376
+ )
1377
+ edge_gateway_id = find_edge_gateway_id(edge_gateway_name, vdc_id)
1378
+
1379
+ ### FIXME: tsugliani
1380
+ # We need to check the previous variables, especially (edge_*)
1381
+ # which can fail in some *weird* situations.
1382
+ params = {
1383
+ 'method' => :get,
1384
+ 'command' => "/admin/edgeGateway/#{edge_gateway_id}"
1385
+ }
1386
+
1387
+ response, _headers = send_request(params)
1388
+
1389
+ interesting = response.css(
1390
+ 'EdgeGateway Configuration EdgeGatewayServiceConfiguration'
1391
+ )
1392
+
1393
+ add_snat_rule = true
1394
+ interesting.css('NatService NatRule').each do |node|
1395
+ if node.css('RuleType').text == 'DNAT' &&
1396
+ node.css('GatewayNatRule/OriginalIp').text == edge_gateway_ip &&
1397
+ node.css('GatewayNatRule/TranslatedIp').text == edge_vapp_ip &&
1398
+ node.css('GatewayNatRule/OriginalPort').text == 'any'
1399
+ # remove old DNAT rule any -> any from older vagrant-vcloudair versions
1400
+ node.remove
1401
+ end
1402
+ if node.css('RuleType').text == 'SNAT' &&
1403
+ node.css('GatewayNatRule/OriginalIp').text == edge_vapp_ip &&
1404
+ node.css('GatewayNatRule/TranslatedIp').text == edge_gateway_ip
1405
+ add_snat_rule = false
1406
+ end
1407
+ end
1408
+
1409
+ add_firewall_rule = true
1410
+ interesting.css('FirewallService FirewallRule').each do |node|
1411
+ if node.css('Port').text == '-1' &&
1412
+ node.css('DestinationIp').text == edge_gateway_ip &&
1413
+ node.css('DestinationPortRange').text == 'Any'
1414
+ add_firewall_rule = false
1415
+ end
1416
+ end
1417
+
1418
+ builder = Nokogiri::XML::Builder.new
1419
+ builder << interesting
1420
+
1421
+ set_edge_rules = Nokogiri::XML(builder.to_xml) do |config|
1422
+ config.default_xml.noblanks
1423
+ end
1424
+
1425
+ nat_rules = set_edge_rules.at_css('NatService')
1426
+
1427
+ # Add all DNAT port rules edge -> vApp for the given list
1428
+ ports.each do |port|
1429
+ nat_rule = Nokogiri::XML::Builder.new do |xml|
1430
+ xml.NatRule {
1431
+ xml.RuleType 'DNAT'
1432
+ xml.IsEnabled 'true'
1433
+ xml.GatewayNatRule {
1434
+ xml.Interface('href' => edge_network_id )
1435
+ xml.OriginalIp edge_gateway_ip
1436
+ xml.OriginalPort port
1437
+ xml.TranslatedIp edge_vapp_ip
1438
+ xml.TranslatedPort port
1439
+ xml.Protocol 'tcpudp'
1440
+ }
1441
+ }
1442
+ end
1443
+ nat_rules << nat_rule.doc.root.to_xml
1444
+ end
1445
+
1446
+ if (add_snat_rule)
1447
+ snat_rule = Nokogiri::XML::Builder.new do |xml|
1448
+ xml.NatRule {
1449
+ xml.RuleType 'SNAT'
1450
+ xml.IsEnabled 'true'
1451
+ xml.GatewayNatRule {
1452
+ xml.Interface('href' => edge_network_id )
1453
+ xml.OriginalIp edge_vapp_ip
1454
+ xml.TranslatedIp edge_gateway_ip
1455
+ xml.Protocol 'any'
1456
+ }
1457
+ }
1458
+ end
1459
+ nat_rules << snat_rule.doc.root.to_xml
1460
+ end
1461
+
1462
+
1463
+ if (add_firewall_rule)
1464
+ firewall_rule_1 = Nokogiri::XML::Builder.new do |xml|
1465
+ xml.FirewallRule {
1466
+ xml.IsEnabled 'true'
1467
+ xml.Description 'Allow Vagrant Communications'
1468
+ xml.Policy 'allow'
1469
+ xml.Protocols {
1470
+ xml.Any 'true'
1471
+ }
1472
+ xml.DestinationPortRange 'Any'
1473
+ xml.DestinationIp edge_gateway_ip
1474
+ xml.SourcePortRange 'Any'
1475
+ xml.SourceIp 'Any'
1476
+ xml.EnableLogging 'false'
1477
+ }
1478
+ end
1479
+ fw_rules = set_edge_rules.at_css('FirewallService')
1480
+ fw_rules << firewall_rule_1.doc.root.to_xml
1481
+ end
1482
+
1483
+ xml = set_edge_rules.at_css 'EdgeGatewayServiceConfiguration'
1484
+ xml['xmlns'] = 'http://www.vmware.com/vcloud/v1.5'
1485
+
1486
+ params = {
1487
+ 'method' => :post,
1488
+ 'command' => "/admin/edgeGateway/#{edge_gateway_id}/action/" +
1489
+ 'configureServices'
1490
+ }
1491
+
1492
+ _response, headers = send_request(
1493
+ params,
1494
+ set_edge_rules.to_xml,
1495
+ 'application/vnd.vmware.admin.edgeGatewayServiceConfiguration+xml'
1496
+ )
1497
+
1498
+ task_id = URI(headers['Location']).path.gsub('/api/task/', '')
1499
+ task_id
1500
+ end
1501
+
1502
+
1503
+ ##
1504
+ # get vApp edge public IP from the vApp ID
1505
+ # Only works when:
1506
+ # - vApp needs to be poweredOn
1507
+ # - FenceMode is set to "natRouted"
1508
+ # - NatType" is set to "portForwarding
1509
+ # This will be required to know how to connect to VMs behind the Edge
1510
+ # device.
1511
+ def get_vapp_edge_public_ip(vapp_id)
1512
+ return @cached_vapp_edge_public_ips[vapp_id] unless @cached_vapp_edge_public_ips[vapp_id].nil?
1513
+
1514
+ # Check the network configuration section
1515
+ params = {
1516
+ 'method' => :get,
1517
+ 'command' => "/vApp/vapp-#{vapp_id}/networkConfigSection"
1518
+ }
1519
+
1520
+ response, _headers = send_request(params)
1521
+
1522
+ # FIXME: this will return nil if the vApp uses multiple vApp Networks
1523
+ # with Edge devices in natRouted/portForwarding mode.
1524
+ config = response.css(
1525
+ 'NetworkConfigSection/NetworkConfig/Configuration'
1526
+ )
1527
+
1528
+ fence_mode = config.css('/FenceMode').text
1529
+ nat_type = config.css('/Features/NatService/NatType').text
1530
+
1531
+ unless fence_mode == 'natRouted'
1532
+ fail Errors::InvalidStateError,
1533
+ 'Invalid request because FenceMode must be natRouted.'
1534
+ end
1535
+
1536
+ unless nat_type == 'portForwarding'
1537
+ fail Errors::InvalidStateError,
1538
+ 'Invalid request because NatType must be portForwarding.'
1539
+ end
1540
+
1541
+ # Check the routerInfo configuration where the global external IP
1542
+ # is defined
1543
+ edge_ip = config.css('/RouterInfo/ExternalIp').text
1544
+ if edge_ip == ''
1545
+ return nil
1546
+ else
1547
+ @cached_vapp_edge_public_ips[vapp_id] = edge_ip
1548
+ return edge_ip
1549
+ end
1550
+ end
1551
+
1552
+ ##
1553
+ # Upload an OVF package
1554
+ # - vdc_id
1555
+ # - vappName
1556
+ # - vappDescription
1557
+ # - ovfFile
1558
+ # - catalogId
1559
+ # - uploadOptions {}
1560
+ def upload_ovf(vdc_id, vapp_name, vapp_description, ovf_file, catalog_id, upload_options = {})
1561
+ # if send_manifest is not set, setting it true
1562
+ if upload_options[:send_manifest].nil? ||
1563
+ upload_options[:send_manifest]
1564
+ upload_manifest = 'true'
1565
+ else
1566
+ upload_manifest = 'false'
1567
+ end
1568
+
1569
+ builder = Nokogiri::XML::Builder.new do |xml|
1570
+ xml.UploadVAppTemplateParams(
1571
+ 'xmlns' => 'http://www.vmware.com/vcloud/v1.5',
1572
+ 'xmlns:ovf' => 'http://schemas.dmtf.org/ovf/envelope/1',
1573
+ 'manifestRequired' => upload_manifest,
1574
+ 'name' => vapp_name) {
1575
+ xml.Description vapp_description
1576
+ }
1577
+ end
1578
+
1579
+ params = {
1580
+ 'method' => :post,
1581
+ 'command' => "/vdc/#{vdc_id}/action/uploadVAppTemplate"
1582
+ }
1583
+
1584
+ @logger.debug('Sending uploadVAppTemplate request...')
1585
+
1586
+ response, headers = send_request(
1587
+ params,
1588
+ builder.to_xml,
1589
+ 'application/vnd.vmware.vcloud.uploadVAppTemplateParams+xml'
1590
+ )
1591
+
1592
+ # Get vAppTemplate Link from location
1593
+ vapp_template = URI(headers['Location']).path.gsub(
1594
+ '/api/vAppTemplate/vappTemplate-', ''
1595
+ )
1596
+
1597
+ @logger.debug("Getting vAppTemplate ID: #{vapp_template}")
1598
+ descriptor_upload = URI(response.css(
1599
+ "Files Link [rel='upload:default']"
1600
+ ).first[:href]).path.gsub('/transfer/', '')
1601
+ transfer_guid = descriptor_upload.gsub('/descriptor.ovf', '')
1602
+
1603
+ ovf_file_basename = File.basename(ovf_file, '.ovf')
1604
+ ovf_dir = File.dirname(ovf_file)
1605
+
1606
+ # Send OVF Descriptor
1607
+ @logger.debug('Sending OVF Descriptor...')
1608
+ upload_url = "/transfer/#{descriptor_upload}"
1609
+ upload_filename = "#{ovf_dir}/#{ovf_file_basename}.ovf"
1610
+ upload_file(
1611
+ upload_url,
1612
+ upload_filename,
1613
+ vapp_template,
1614
+ upload_options
1615
+ )
1616
+
1617
+ # Begin the catch for upload interruption
1618
+ begin
1619
+ params = {
1620
+ 'method' => :get,
1621
+ 'command' => "/vAppTemplate/vappTemplate-#{vapp_template}"
1622
+ }
1623
+
1624
+ response, _headers = send_request(params)
1625
+
1626
+ task = response.css(
1627
+ "VAppTemplate Task[operationName='vdcUploadOvfContents']"
1628
+ ).first
1629
+ task_id = URI(task['href']).path.gsub('/api/task/', '')
1630
+
1631
+ # Loop to wait for the upload links to show up in the vAppTemplate
1632
+ # we just created
1633
+ @logger.debug(
1634
+ 'Waiting for the upload links to show up in the vAppTemplate ' \
1635
+ 'we just created.'
1636
+ )
1637
+ while true
1638
+ response, _headers = send_request(params)
1639
+ @logger.debug('Request...')
1640
+ break unless response.css("Files Link [rel='upload:default']").count == 1
1641
+ sleep 1
1642
+ end
1643
+
1644
+ if upload_manifest == 'true'
1645
+ upload_url = "/transfer/#{transfer_guid}/descriptor.mf"
1646
+ upload_filename = "#{ovf_dir}/#{ovf_file_basename}.mf"
1647
+ upload_file(
1648
+ upload_url,
1649
+ upload_filename,
1650
+ vapp_template,
1651
+ upload_options
1652
+ )
1653
+ end
1654
+
1655
+ # Start uploading OVF VMDK files
1656
+ params = {
1657
+ 'method' => :get,
1658
+ 'command' => "/vAppTemplate/vappTemplate-#{vapp_template}"
1659
+ }
1660
+ response, _headers = send_request(params)
1661
+ response.css(
1662
+ "Files File [bytesTransferred='0'] Link [rel='upload:default']"
1663
+ ).each do |file|
1664
+ file_name = URI(file[:href]).path.gsub("/transfer/#{transfer_guid}/", '')
1665
+ upload_filename = "#{ovf_dir}/#{file_name}"
1666
+ upload_url = "/transfer/#{transfer_guid}/#{file_name}"
1667
+ upload_file(
1668
+ upload_url,
1669
+ upload_filename,
1670
+ vapp_template,
1671
+ upload_options
1672
+ )
1673
+ end
1674
+
1675
+ # Add item to the catalog catalog_id
1676
+ builder = Nokogiri::XML::Builder.new do |xml|
1677
+ xml.CatalogItem(
1678
+ 'xmlns' => 'http://www.vmware.com/vcloud/v1.5',
1679
+ 'type' => 'application/vnd.vmware.vcloud.catalogItem+xml',
1680
+ 'name' => vapp_name) {
1681
+ xml.Description vapp_description
1682
+ xml.Entity(
1683
+ 'href' => "#{@api_url}/vAppTemplate/" +
1684
+ "vappTemplate-#{vapp_template}"
1685
+ )
1686
+ }
1687
+ end
1688
+
1689
+ params = {
1690
+ 'method' => :post,
1691
+ 'command' => "/catalog/#{catalog_id}/catalogItems"
1692
+ }
1693
+ # No debug here (tsugliani)
1694
+ _response, _headers = send_request(
1695
+ params,
1696
+ builder.to_xml,
1697
+ 'application/vnd.vmware.vcloud.catalogItem+xml'
1698
+ )
1699
+
1700
+ task_id
1701
+
1702
+ ######
1703
+
1704
+ rescue Exception => e
1705
+ puts "Exception detected: #{e.message}."
1706
+ puts 'Aborting task...'
1707
+
1708
+ # Get vAppTemplate Task
1709
+ params = {
1710
+ 'method' => :get,
1711
+ 'command' => "/vAppTemplate/vappTemplate-#{vapp_template}"
1712
+ }
1713
+ response, _headers = send_request(params)
1714
+
1715
+ # Cancel Task
1716
+ cancel_hook = URI(response.css(
1717
+ "Tasks Task Link [rel='task:cancel']"
1718
+ ).first[:href]).path.gsub('/api', '')
1719
+
1720
+ params = {
1721
+ 'method' => :post,
1722
+ 'command' => cancel_hook
1723
+ }
1724
+ # FIXME: No debug here (tsugliani)
1725
+ _response, _headers = send_request(params)
1726
+ raise
1727
+ end
1728
+ end
1729
+
1730
+ ##
1731
+ # Fetch information for a given task
1732
+ def get_task(task_id)
1733
+ params = {
1734
+ 'method' => :get,
1735
+ 'command' => "/task/#{task_id}"
1736
+ }
1737
+
1738
+ response, _headers = send_request(params)
1739
+
1740
+ task = response.css('Task').first
1741
+ status = task['status']
1742
+ start_time = task['startTime']
1743
+ end_time = task['endTime']
1744
+
1745
+ {
1746
+ :status => status,
1747
+ :start_time => start_time,
1748
+ :end_time => end_time,
1749
+ :response => response
1750
+ }
1751
+ end
1752
+
1753
+ ##
1754
+ # Poll a given task until completion
1755
+ def wait_task_completion(task_id)
1756
+ task, errormsg = nil
1757
+ loop do
1758
+ task = get_task(task_id)
1759
+ # @logger.debug(
1760
+ # "Evaluating taskid: #{task_id}, current status #{task[:status]}"
1761
+ # )
1762
+ break if !['queued','preRunning','running'].include?(task[:status])
1763
+ sleep 5
1764
+ end
1765
+
1766
+ if task[:status] == 'error'
1767
+ @logger.debug('Task Error')
1768
+ errormsg = task[:response].css('Error').first
1769
+ @logger.debug(
1770
+ "Task Error Message #{errormsg['majorErrorCode']} - " +
1771
+ "#{errormsg['message']}"
1772
+ )
1773
+ errormsg =
1774
+ "Error code #{errormsg['majorErrorCode']} - #{errormsg['message']}"
1775
+ end
1776
+
1777
+ {
1778
+ :status => task[:status],
1779
+ :errormsg => errormsg,
1780
+ :start_time => task[:start_time],
1781
+ :end_time => task[:end_time]
1782
+ }
1783
+ end
1784
+
1785
+ ##
1786
+ # Set vApp Network Config
1787
+ def set_vapp_network_config(vapp_id, network_name, config = {})
1788
+ builder = Nokogiri::XML::Builder.new do |xml|
1789
+ xml.NetworkConfigSection(
1790
+ 'xmlns' => 'http://www.vmware.com/vcloud/v1.5',
1791
+ 'xmlns:ovf' => 'http://schemas.dmtf.org/ovf/envelope/1'
1792
+ ) {
1793
+ xml['ovf'].Info 'Network configuration'
1794
+ xml.NetworkConfig('networkName' => network_name) {
1795
+ xml.Configuration {
1796
+ xml.FenceMode(config[:fence_mode] || 'isolated')
1797
+ xml.RetainNetInfoAcrossDeployments(config[:retain_net] || false)
1798
+ xml.ParentNetwork('href' => config[:parent_network])
1799
+ }
1800
+ }
1801
+ }
1802
+ end
1803
+
1804
+ params = {
1805
+ 'method' => :put,
1806
+ 'command' => "/vApp/vapp-#{vapp_id}/networkConfigSection"
1807
+ }
1808
+
1809
+ _response, headers = send_request(
1810
+ params,
1811
+ builder.to_xml,
1812
+ 'application/vnd.vmware.vcloud.networkConfigSection+xml'
1813
+ )
1814
+
1815
+ task_id = URI(headers['Location']).path.gsub('/api/task/', '')
1816
+ task_id
1817
+ end
1818
+
1819
+ ##
1820
+ # Set VM Network Config
1821
+ def set_vm_network_config(vm_id, network_name, config = {})
1822
+ builder = Nokogiri::XML::Builder.new do |xml|
1823
+ xml.NetworkConnectionSection(
1824
+ 'xmlns' => 'http://www.vmware.com/vcloud/v1.5',
1825
+ 'xmlns:ovf' => 'http://schemas.dmtf.org/ovf/envelope/1') {
1826
+ xml['ovf'].Info 'VM Network configuration'
1827
+ xml.PrimaryNetworkConnectionIndex(config[:primary_index] || 0)
1828
+ xml.NetworkConnection(
1829
+ 'network' => network_name,
1830
+ 'needsCustomization' => true
1831
+ ) {
1832
+ xml.NetworkConnectionIndex(config[:network_index] || 0)
1833
+ xml.IpAddress config[:ip] if config[:ip]
1834
+ xml.IsConnected(config[:is_connected] || true)
1835
+ xml.IpAddressAllocationMode config[:ip_allocation_mode] if config[:ip_allocation_mode]
1836
+ }
1837
+ }
1838
+ end
1839
+
1840
+ params = {
1841
+ 'method' => :put,
1842
+ 'command' => "/vApp/vm-#{vm_id}/networkConnectionSection"
1843
+ }
1844
+
1845
+ _response, headers = send_request(
1846
+ params,
1847
+ builder.to_xml,
1848
+ 'application/vnd.vmware.vcloud.networkConnectionSection+xml'
1849
+ )
1850
+
1851
+ task_id = URI(headers['Location']).path.gsub('/api/task/', '')
1852
+ task_id
1853
+ end
1854
+
1855
+ ##
1856
+ # Set VM Guest Customization Config
1857
+ def set_vm_guest_customization(vm_id, computer_name, config = {})
1858
+ builder = Nokogiri::XML::Builder.new do |xml|
1859
+ xml.GuestCustomizationSection(
1860
+ 'xmlns' => 'http://www.vmware.com/vcloud/v1.5',
1861
+ 'xmlns:ovf' => 'http://schemas.dmtf.org/ovf/envelope/1') {
1862
+ xml['ovf'].Info 'VM Guest Customization configuration'
1863
+ xml.Enabled config[:enabled] if config[:enabled]
1864
+ xml.AdminPasswordEnabled config[:admin_passwd_enabled] if config[:admin_passwd_enabled]
1865
+ xml.AdminPassword config[:admin_passwd] if config[:admin_passwd]
1866
+ xml.ComputerName computer_name
1867
+ }
1868
+ end
1869
+
1870
+ params = {
1871
+ 'method' => :put,
1872
+ 'command' => "/vApp/vm-#{vm_id}/guestCustomizationSection"
1873
+ }
1874
+
1875
+ _response, headers = send_request(
1876
+ params,
1877
+ builder.to_xml,
1878
+ 'application/vnd.vmware.vcloud.guestCustomizationSection+xml'
1879
+ )
1880
+ task_id = URI(headers['Location']).path.gsub('/api/task/', '')
1881
+ task_id
1882
+ end
1883
+
1884
+ # Enable VM Nested Hardware-Assisted Virtualization
1885
+ def set_vm_nested_hypervisor(vm_id, enable)
1886
+ action = enable ? "enable" : "disable"
1887
+ params = {
1888
+ 'method' => :post,
1889
+ 'command' => "/vApp/vm-#{vm_id}/action/#{action}NestedHypervisor"
1890
+ }
1891
+
1892
+ _response, headers = send_request(params)
1893
+ task_id = URI(headers['Location']).path.gsub('/api/task/', '')
1894
+ task_id
1895
+ end
1896
+
1897
+ ##
1898
+ # Set memory and number of cpus in virtualHardwareSection of a given vm
1899
+ # returns task_id or nil if there is no task to wait for
1900
+ def set_vm_hardware(vm_id, cfg)
1901
+ params = {
1902
+ 'method' => :get,
1903
+ 'command' => "/vApp/vm-#{vm_id}/virtualHardwareSection"
1904
+ }
1905
+
1906
+ changed = false
1907
+ response, _headers = send_request(params)
1908
+
1909
+ response.css('ovf|Item').each do |item|
1910
+ type = item.css('rasd|ResourceType').first
1911
+ if type.content == '3'
1912
+ # cpus
1913
+ if cfg.cpus
1914
+ if item.at_css('rasd|VirtualQuantity').content != cfg.cpus.to_s
1915
+ item.at_css('rasd|VirtualQuantity').content = cfg.cpus
1916
+ item.at_css('rasd|ElementName').content = "#{cfg.cpus} virtual CPU(s)"
1917
+ changed = true
1918
+ end
1919
+ end
1920
+ elsif type.content == '4'
1921
+ # memory
1922
+ if cfg.memory
1923
+ if item.at_css('rasd|VirtualQuantity').content != cfg.memory.to_s
1924
+ item.at_css('rasd|VirtualQuantity').content = cfg.memory
1925
+ item.at_css('rasd|ElementName').content = "#{cfg.memory} MB of memory"
1926
+ changed = true
1927
+ end
1928
+ end
1929
+ end
1930
+ end
1931
+
1932
+ if changed
1933
+ params = {
1934
+ 'method' => :put,
1935
+ 'command' => "/vApp/vm-#{vm_id}/virtualHardwareSection"
1936
+ }
1937
+
1938
+ _response, headers = send_request(
1939
+ params,
1940
+ response.to_xml,
1941
+ 'application/vnd.vmware.vcloud.virtualhardwaresection+xml'
1942
+ )
1943
+
1944
+ task_id = URI(headers['Location']).path.gsub('/api/task/', '')
1945
+ task_id
1946
+ else
1947
+ return nil
1948
+ end
1949
+ end
1950
+
1951
+
1952
+ ##
1953
+ # Fetch details about a given VM
1954
+ def get_vm(vm_id)
1955
+ params = {
1956
+ 'method' => :get,
1957
+ 'command' => "/vApp/vm-#{vm_id}"
1958
+ }
1959
+
1960
+ response, _headers = send_request(params)
1961
+
1962
+ os_desc = response.css(
1963
+ 'ovf|OperatingSystemSection ovf|Description'
1964
+ ).first.text
1965
+
1966
+ networks = {}
1967
+ response.css('NetworkConnection').each do |network|
1968
+ ip = network.css('IpAddress').first
1969
+ ip = ip.text if ip
1970
+
1971
+ networks[network['network']] = {
1972
+ :index => network.css(
1973
+ 'NetworkConnectionIndex'
1974
+ ).first.text,
1975
+ :ip => ip,
1976
+ :is_connected => network.css(
1977
+ 'IsConnected'
1978
+ ).first.text,
1979
+ :mac_address => network.css(
1980
+ 'MACAddress'
1981
+ ).first.text,
1982
+ :ip_allocation_mode => network.css(
1983
+ 'IpAddressAllocationMode'
1984
+ ).first.text
1985
+ }
1986
+ end
1987
+
1988
+ admin_password = response.css(
1989
+ 'GuestCustomizationSection AdminPassword'
1990
+ ).first
1991
+ admin_password = admin_password.text if admin_password
1992
+
1993
+ # make the lines shorter by adjusting the nokogiri css namespace
1994
+ guest_css = response.css('GuestCustomizationSection')
1995
+ guest_customizations = {
1996
+ :enabled => guest_css.css('Enabled').first.text,
1997
+ :admin_passwd_enabled => guest_css.css(
1998
+ 'AdminPasswordEnabled'
1999
+ ).first.text,
2000
+ :admin_passwd_auto => guest_css.css(
2001
+ 'AdminPasswordAuto'
2002
+ ).first.text,
2003
+ :admin_passwd => admin_password,
2004
+ :reset_passwd_required => guest_css.css(
2005
+ 'ResetPasswordRequired'
2006
+ ).first.text,
2007
+ :computer_name => guest_css.css('ComputerName').first.text
2008
+ }
2009
+
2010
+ {
2011
+ :os_desc => os_desc,
2012
+ :networks => networks,
2013
+ :guest_customizations => guest_customizations
2014
+ }
2015
+ end
2016
+ end # Class Version 5.1
2017
+ end # Module Driver
2018
+ end # module VCloudAir
2019
+ end # Module VagrantPlugins