kitchen-vcenter 1.5.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/kitchen-vcenter/version.rb +1 -1
- data/lib/kitchen/driver/vcenter.rb +89 -133
- data/lib/support/clone_vm.rb +15 -11
- metadata +9 -13
- data/lib/base.rb +0 -35
- data/lib/lookup_service_helper.rb +0 -464
- data/lib/sso.rb +0 -268
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4830a5da83f0ac1eba8ce606f4151ff369058478452776699f45b52277bd4943
|
4
|
+
data.tar.gz: c7761ff930c8c78194dafe5c5ea7b55e4a27b1db992f4f1c678a66308cf75bb2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4986789d6c04264fcbf426176c3453427cf8a3f4b8cc951efe77a7b4d353b10aa20570c63f59b33294992f8a889b38b49454a25257ae6722a4431bb332272f4a
|
7
|
+
data.tar.gz: 3335539aeb36bd96b4e044c774501bee318ab1130a89cbb46fd319af83ee2d27952a9e8d232f3a1170b6baf196ee63cf6fd08bd952c920733d4993baffe8ea97
|
@@ -18,15 +18,8 @@
|
|
18
18
|
#
|
19
19
|
|
20
20
|
require "kitchen"
|
21
|
-
require "
|
22
|
-
require "
|
23
|
-
require "base"
|
24
|
-
require "lookup_service_helper"
|
25
|
-
require "vapi"
|
26
|
-
require "com/vmware/cis"
|
27
|
-
require "com/vmware/cis/tagging"
|
28
|
-
require "com/vmware/vcenter"
|
29
|
-
require "com/vmware/vcenter/vm"
|
21
|
+
require "vsphere-automation-cis"
|
22
|
+
require "vsphere-automation-vcenter"
|
30
23
|
require "support/clone_vm"
|
31
24
|
require "securerandom"
|
32
25
|
require "uri"
|
@@ -37,7 +30,7 @@ module Kitchen
|
|
37
30
|
module Driver
|
38
31
|
# Extends the Base class for vCenter
|
39
32
|
class Vcenter < Kitchen::Driver::Base
|
40
|
-
attr_accessor :connection_options, :ipaddress, :
|
33
|
+
attr_accessor :connection_options, :ipaddress, :api_client
|
41
34
|
|
42
35
|
required_config :vcenter_username
|
43
36
|
required_config :vcenter_password
|
@@ -53,7 +46,6 @@ module Kitchen
|
|
53
46
|
default_config :resource_pool, nil
|
54
47
|
default_config :clone_type, :full
|
55
48
|
default_config :cluster, nil
|
56
|
-
default_config :lookup_service_host, nil
|
57
49
|
default_config :network_name, nil
|
58
50
|
default_config :tags, nil
|
59
51
|
|
@@ -72,7 +64,6 @@ module Kitchen
|
|
72
64
|
# @todo This does not allow to specify cluster AND pool yet
|
73
65
|
unless config[:cluster].nil?
|
74
66
|
cluster = get_cluster(config[:cluster])
|
75
|
-
# @todo Check for active hosts, to avoid "A specified parameter was not correct: spec.pool"
|
76
67
|
config[:resource_pool] = cluster.resource_pool
|
77
68
|
else
|
78
69
|
# Find the first resource pool on any cluster
|
@@ -111,30 +102,38 @@ module Kitchen
|
|
111
102
|
}
|
112
103
|
|
113
104
|
# Create an object from which the clone operation can be called
|
114
|
-
|
115
|
-
|
116
|
-
|
105
|
+
new_vm = Support::CloneVm.new(connection_options, options)
|
106
|
+
new_vm.clone
|
107
|
+
|
108
|
+
state[:hostname] = new_vm.ip
|
109
|
+
state[:vm_name] = new_vm.name
|
117
110
|
|
118
111
|
unless config[:tags].nil? || config[:tags].empty?
|
112
|
+
tag_api = VSphereAutomation::CIS::TaggingTagApi.new(api_client)
|
113
|
+
vm_tags = tag_api.list.value
|
114
|
+
raise format("No configured tags found on VCenter, but %s specified", config[:tags].to_s) if vm_tags.empty?
|
115
|
+
|
119
116
|
valid_tags = {}
|
120
|
-
vm_tags
|
121
|
-
|
122
|
-
|
123
|
-
valid_tags[tag.name] = tag.id
|
117
|
+
vm_tags.each do |uid|
|
118
|
+
tag = tag_api.get(uid)
|
119
|
+
|
120
|
+
valid_tags[tag.value.name] = tag.value.id if tag.is_a? VSphereAutomation::CIS::CisTaggingTagResult
|
124
121
|
end
|
125
122
|
|
126
123
|
# Error out on undefined tags
|
127
124
|
invalid = config[:tags] - valid_tags.keys
|
128
125
|
raise format("Specified tag(s) %s not valid", invalid.join(",")) unless invalid.empty?
|
129
|
-
|
130
|
-
tag_service = Com::Vmware::Cis::Tagging::TagAssociation.new(vapi_config)
|
131
|
-
|
132
|
-
# calls needs a DynamicID object which we construct from type and mobID
|
133
|
-
mobid = get_vm(config[:vm_name]).vm
|
134
|
-
dynamic_id = Com::Vmware::Vapi::Std::DynamicID.new(type: "VirtualMachine", id: mobid)
|
135
|
-
|
126
|
+
tag_service = VSphereAutomation::CIS::TaggingTagAssociationApi.new(api_client)
|
136
127
|
tag_ids = config[:tags].map { |name| valid_tags[name] }
|
137
|
-
|
128
|
+
|
129
|
+
request_body = {
|
130
|
+
object_id: {
|
131
|
+
id: get_vm(config[:vm_name]).vm,
|
132
|
+
type: "VirtualMachine",
|
133
|
+
},
|
134
|
+
tag_ids: tag_ids,
|
135
|
+
}
|
136
|
+
tag_service.attach_multiple_tags_to_object(request_body)
|
138
137
|
end
|
139
138
|
end
|
140
139
|
|
@@ -146,18 +145,20 @@ module Kitchen
|
|
146
145
|
|
147
146
|
save_and_validate_parameters
|
148
147
|
connect
|
148
|
+
|
149
149
|
vm = get_vm(state[:vm_name])
|
150
|
+
unless vm.nil?
|
151
|
+
vm_api = VSphereAutomation::VCenter::VMApi.new(api_client)
|
150
152
|
|
151
|
-
|
153
|
+
# shut the machine down if it is running
|
154
|
+
if vm.power_state == "POWERED_ON"
|
155
|
+
power = VSphereAutomation::VCenter::VmPowerApi.new(api_client)
|
156
|
+
power.stop(vm.vm)
|
157
|
+
end
|
152
158
|
|
153
|
-
|
154
|
-
|
155
|
-
power = Com::Vmware::Vcenter::Vm::Power.new(vapi_config)
|
156
|
-
power.stop(vm.vm)
|
159
|
+
# delete the vm
|
160
|
+
vm_api.delete(vm.vm)
|
157
161
|
end
|
158
|
-
|
159
|
-
# delete the vm
|
160
|
-
vm_obj.delete(vm.vm)
|
161
162
|
end
|
162
163
|
|
163
164
|
private
|
@@ -195,22 +196,20 @@ module Kitchen
|
|
195
196
|
#
|
196
197
|
# @param [name] name is the name of the datacenter
|
197
198
|
def datacenter_exists?(name)
|
198
|
-
|
199
|
-
|
200
|
-
dc = dc_obj.list(filter)
|
199
|
+
dc_api = VSphereAutomation::VCenter::DatacenterApi.new(api_client)
|
200
|
+
dcs = dc_api.list({ filter_names: name }).value
|
201
201
|
|
202
|
-
raise format("Unable to find data center: %s", name) if
|
202
|
+
raise format("Unable to find data center: %s", name) if dcs.empty?
|
203
203
|
end
|
204
204
|
|
205
205
|
# Checks if a network exists or not
|
206
206
|
#
|
207
207
|
# @param [name] name is the name of the Network
|
208
208
|
def network_exists?(name)
|
209
|
-
|
210
|
-
|
211
|
-
net = net_obj.list(filter)
|
209
|
+
net_api = VSphereAutomation::VCenter::NetworkApi.new(api_client)
|
210
|
+
nets = net_api.list({ filter_names: name }).value
|
212
211
|
|
213
|
-
raise format("Unable to find target network: %s", name) if
|
212
|
+
raise format("Unable to find target network: %s", name) if nets.empty?
|
214
213
|
end
|
215
214
|
|
216
215
|
# Validates the host name of the server you can connect to
|
@@ -218,56 +217,58 @@ module Kitchen
|
|
218
217
|
# @param [name] name is the name of the host
|
219
218
|
def get_host(name)
|
220
219
|
# create a host object to work with
|
221
|
-
|
220
|
+
host_api = VSphereAutomation::VCenter::HostApi.new(api_client)
|
222
221
|
|
223
222
|
if name.nil?
|
224
|
-
|
223
|
+
hosts = host_api.list.value
|
225
224
|
else
|
226
|
-
|
227
|
-
host = host_obj.list(filter)
|
225
|
+
hosts = host_api.list({ filter_names: name }).value
|
228
226
|
end
|
229
227
|
|
230
|
-
raise format("Unable to find target host: %s", name) if
|
228
|
+
raise format("Unable to find target host: %s", name) if hosts.empty?
|
231
229
|
|
232
|
-
|
230
|
+
hosts.first
|
233
231
|
end
|
234
232
|
|
235
233
|
# Gets the folder you want to create the VM
|
236
234
|
#
|
237
235
|
# @param [name] name is the name of the folder
|
238
236
|
def get_folder(name)
|
239
|
-
|
240
|
-
|
241
|
-
folder_obj = Com::Vmware::Vcenter::Folder.new(vapi_config)
|
242
|
-
folder = folder_obj.list(filter)
|
237
|
+
folder_api = VSphereAutomation::VCenter::FolderApi.new(api_client)
|
238
|
+
folders = folder_api.list({ filter_names: name }).value
|
243
239
|
|
244
|
-
raise format("Unable to find folder: %s", name) if
|
240
|
+
raise format("Unable to find folder: %s", name) if folders.empty?
|
245
241
|
|
246
|
-
|
242
|
+
folders.first.folder
|
247
243
|
end
|
248
244
|
|
249
245
|
# Gets the name of the VM you are creating
|
250
246
|
#
|
251
247
|
# @param [name] name is the name of the VM
|
252
248
|
def get_vm(name)
|
253
|
-
|
254
|
-
|
255
|
-
|
249
|
+
vm_api = VSphereAutomation::VCenter::VMApi.new(api_client)
|
250
|
+
vms = vm_api.list({ filter_names: name }).value
|
251
|
+
|
252
|
+
vms.first
|
256
253
|
end
|
257
254
|
|
258
255
|
# Gets the info of the cluster
|
259
256
|
#
|
260
257
|
# @param [name] name is the name of the Cluster
|
261
258
|
def get_cluster(name)
|
262
|
-
|
259
|
+
cluster_api = VSphereAutomation::VCenter::ClusterApi.new(api_client)
|
260
|
+
clusters = cluster_api.list({ filter_names: name }).value
|
263
261
|
|
264
|
-
# @todo: Use Cluster::FilterSpec to only get the cluster which was asked
|
265
|
-
# filter = Com::Vmware::Vcenter::Cluster::FilterSpec.new(clusters: Set.new(['...']))
|
266
|
-
clusters = cl_obj.list.select { |cluster| cluster.name == name }
|
267
262
|
raise format("Unable to find Cluster: %s", name) if clusters.empty?
|
268
263
|
|
269
|
-
cluster_id = clusters
|
270
|
-
|
264
|
+
cluster_id = clusters.first.cluster
|
265
|
+
|
266
|
+
host_api = VSphereAutomation::VCenter::HostApi.new(api_client)
|
267
|
+
hosts = host_api.list({ filter_clusters: cluster_id, connection_states: "CONNECTED" }).value
|
268
|
+
|
269
|
+
raise format("Unable to find active host in cluster %s", name) if hosts.empty?
|
270
|
+
|
271
|
+
cluster_api.get(cluster_id).value
|
271
272
|
end
|
272
273
|
|
273
274
|
# Gets the name of the resource pool
|
@@ -276,92 +277,47 @@ module Kitchen
|
|
276
277
|
# @param [name] name is the name of the ResourcePool
|
277
278
|
def get_resource_pool(name)
|
278
279
|
# Create a resource pool object
|
279
|
-
|
280
|
+
rp_api = VSphereAutomation::VCenter::ResourcePoolApi.new(api_client)
|
280
281
|
|
281
282
|
# If no name has been set, use the first resource pool that can be found,
|
282
283
|
# otherwise try to find by given name
|
283
284
|
if name.nil?
|
284
285
|
# Remove default pool for first pass (<= 1.2.1 behaviour to pick first user-defined pool found)
|
285
|
-
|
286
|
-
debug("Search of all resource pools found: " +
|
286
|
+
resource_pools = rp_api.list.value.delete_if { |pool| pool.name == "Resources" }
|
287
|
+
debug("Search of all resource pools found: " + resource_pools.map { |pool| pool.name }.to_s)
|
287
288
|
|
288
289
|
# Revert to default pool, if no user-defined pool found (> 1.2.1 behaviour)
|
289
290
|
# (This one might not be found under some circumstances by the statement above)
|
290
|
-
return get_resource_pool("Resources") if
|
291
|
+
return get_resource_pool("Resources") if resource_pools.empty?
|
291
292
|
else
|
292
|
-
|
293
|
-
|
294
|
-
resource_pool = rp_obj.list(filter)
|
295
|
-
debug("Search for resource pools found: " + resource_pool.map { |pool| pool.name }.to_s)
|
293
|
+
resource_pools = rp_api.list({ filter_names: name }).value
|
294
|
+
debug("Search for resource pools found: " + resource_pools.map { |pool| pool.name }.to_s)
|
296
295
|
end
|
297
296
|
|
298
|
-
raise format("Unable to find Resource Pool: %s", name) if
|
297
|
+
raise format("Unable to find Resource Pool: %s", name) if resource_pools.empty?
|
299
298
|
|
300
|
-
|
301
|
-
end
|
302
|
-
|
303
|
-
# Get location of lookup service
|
304
|
-
def lookup_service_host
|
305
|
-
# Allow manual overrides
|
306
|
-
return config[:lookup_service_host] unless config[:lookup_service_host].nil?
|
307
|
-
|
308
|
-
# Retrieve SSO service via RbVmomi, which is always co-located with the Lookup Service.
|
309
|
-
vim = RbVmomi::VIM.connect @connection_options
|
310
|
-
vim_settings = vim.serviceContent.setting.setting
|
311
|
-
sso_url = vim_settings.select { |o| o.key == "config.vpxd.sso.sts.uri" }&.first&.value
|
312
|
-
|
313
|
-
# Configuration fallback, if no SSO URL found for some reason
|
314
|
-
ls_host = sso_url.nil? ? config[:vcenter_host] : URI.parse(sso_url).host
|
315
|
-
debug("Using Lookup Service at: " + ls_host)
|
316
|
-
|
317
|
-
ls_host
|
318
|
-
end
|
319
|
-
|
320
|
-
# Get vCenter FQDN
|
321
|
-
def vcenter_host
|
322
|
-
# Retrieve SSO service via RbVmomi, which is always co-located with the Lookup Service.
|
323
|
-
vim = RbVmomi::VIM.connect @connection_options
|
324
|
-
vim_settings = vim.serviceContent.setting.setting
|
325
|
-
|
326
|
-
vim_settings.select { |o| o.key == "VirtualCenter.FQDN" }.first.value
|
299
|
+
resource_pools.first.resource_pool
|
327
300
|
end
|
328
301
|
|
329
302
|
# The main connect method
|
330
303
|
#
|
331
304
|
def connect
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
ssl_options[:verify] = config[:vcenter_disable_ssl_verify] ? :none : :peer
|
349
|
-
@vapi_config = VAPI::Bindings::VapiConfig.new(vapi_url, ssl_options)
|
350
|
-
|
351
|
-
# get the SSO url
|
352
|
-
sso_url = lookup_service_helper.find_sso_url
|
353
|
-
sso = SSO::Connection.new(sso_url).login(config[:vcenter_username], config[:vcenter_password])
|
354
|
-
token = sso.request_bearer_token
|
355
|
-
vapi_config.set_security_context(
|
356
|
-
VAPI::Security.create_saml_bearer_security_context(token.to_s)
|
357
|
-
)
|
358
|
-
|
359
|
-
# Login and get the session information
|
360
|
-
@session_svc = Com::Vmware::Cis::Session.new(vapi_config)
|
361
|
-
@session_id = session_svc.create
|
362
|
-
vapi_config.set_security_context(
|
363
|
-
VAPI::Security.create_session_security_context(session_id)
|
364
|
-
)
|
305
|
+
configuration = VSphereAutomation::Configuration.new.tap do |c|
|
306
|
+
c.host = config[:vcenter_host]
|
307
|
+
c.username = config[:vcenter_username]
|
308
|
+
c.password = config[:vcenter_password]
|
309
|
+
c.scheme = "https"
|
310
|
+
c.verify_ssl = config[:vcenter_disable_ssl_verify] ? false : true
|
311
|
+
c.verify_ssl_host = config[:vcenter_disable_ssl_verify] ? false : true
|
312
|
+
end
|
313
|
+
|
314
|
+
@api_client = VSphereAutomation::ApiClient.new(configuration)
|
315
|
+
api_client.default_headers["Authorization"] = configuration.basic_auth_token
|
316
|
+
|
317
|
+
session_api = VSphereAutomation::CIS::SessionApi.new(api_client)
|
318
|
+
session_id = session_api.create("").value
|
319
|
+
|
320
|
+
api_client.default_headers["vmware-api-session-id"] = session_id
|
365
321
|
end
|
366
322
|
end
|
367
323
|
end
|
data/lib/support/clone_vm.rb
CHANGED
@@ -2,10 +2,11 @@ require "rbvmomi"
|
|
2
2
|
|
3
3
|
class Support
|
4
4
|
class CloneVm
|
5
|
-
attr_reader :vim, :options
|
5
|
+
attr_reader :vim, :options, :vm, :name, :path
|
6
6
|
|
7
7
|
def initialize(conn_opts, options)
|
8
8
|
@options = options
|
9
|
+
@name = options[:name]
|
9
10
|
|
10
11
|
# Connect to vSphere
|
11
12
|
@vim ||= RbVmomi::VIM.connect conn_opts
|
@@ -83,7 +84,7 @@ class Support
|
|
83
84
|
# @todo not working yet
|
84
85
|
# relocate_spec.folder = dest_folder
|
85
86
|
clone_spec = RbVmomi::VIM.VirtualMachineInstantCloneSpec(location: relocate_spec,
|
86
|
-
name:
|
87
|
+
name: name)
|
87
88
|
|
88
89
|
task = src_vm.InstantClone_Task(spec: clone_spec)
|
89
90
|
else
|
@@ -91,24 +92,27 @@ class Support
|
|
91
92
|
powerOn: options[:poweron],
|
92
93
|
template: false)
|
93
94
|
|
94
|
-
task = src_vm.CloneVM_Task(spec: clone_spec, folder: dest_folder, name:
|
95
|
+
task = src_vm.CloneVM_Task(spec: clone_spec, folder: dest_folder, name: name)
|
95
96
|
end
|
96
97
|
task.wait_for_completion
|
97
98
|
|
98
99
|
# get the IP address of the machine for bootstrapping
|
99
100
|
# machine name is based on the path, e.g. that includes the folder
|
100
|
-
|
101
|
-
|
101
|
+
@path = options[:folder].nil? ? name : format("%s/%s", options[:folder][:name], name)
|
102
|
+
@vm = dc.find_vm(path)
|
102
103
|
|
103
|
-
if
|
104
|
-
puts format("Unable to find machine: %s",
|
104
|
+
if vm.nil?
|
105
|
+
puts format("Unable to find machine: %s", path)
|
105
106
|
else
|
106
107
|
puts "Waiting for network interfaces to become available..."
|
107
|
-
sleep 2 while
|
108
|
-
new_vm.guest.net[0].ipConfig.ipAddress.detect do |addr|
|
109
|
-
addr.origin != "linklayer"
|
110
|
-
end.ipAddress
|
108
|
+
sleep 2 while vm.guest.net.empty? || !vm.guest.ipAddress
|
111
109
|
end
|
112
110
|
end
|
111
|
+
|
112
|
+
def ip
|
113
|
+
vm.guest.net[0].ipConfig.ipAddress.detect do |addr|
|
114
|
+
addr.origin != "linklayer"
|
115
|
+
end.ipAddress
|
116
|
+
end
|
113
117
|
end
|
114
118
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kitchen-vcenter
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chef Software
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-01-
|
11
|
+
date: 2019-01-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rbvmomi
|
@@ -58,28 +58,28 @@ dependencies:
|
|
58
58
|
requirements:
|
59
59
|
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: '
|
61
|
+
version: '0.1'
|
62
62
|
type: :runtime
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version: '
|
68
|
+
version: '0.1'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: bundler
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
|
-
- - "
|
73
|
+
- - ">="
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version: '
|
75
|
+
version: '0'
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
|
-
- - "
|
80
|
+
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version: '
|
82
|
+
version: '0'
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
84
|
name: rake
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -102,11 +102,8 @@ extensions: []
|
|
102
102
|
extra_rdoc_files: []
|
103
103
|
files:
|
104
104
|
- LICENSE
|
105
|
-
- lib/base.rb
|
106
105
|
- lib/kitchen-vcenter/version.rb
|
107
106
|
- lib/kitchen/driver/vcenter.rb
|
108
|
-
- lib/lookup_service_helper.rb
|
109
|
-
- lib/sso.rb
|
110
107
|
- lib/support/clone_vm.rb
|
111
108
|
homepage: https://github.com/chef/kitchen-vcenter
|
112
109
|
licenses:
|
@@ -127,8 +124,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
127
124
|
- !ruby/object:Gem::Version
|
128
125
|
version: '0'
|
129
126
|
requirements: []
|
130
|
-
|
131
|
-
rubygems_version: 2.7.6
|
127
|
+
rubygems_version: 3.0.2
|
132
128
|
signing_key:
|
133
129
|
specification_version: 4
|
134
130
|
summary: Test Kitchen driver for VMare vCenter
|
data/lib/base.rb
DELETED
@@ -1,35 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
#
|
3
|
-
# Author:: Chef Partner Engineering (<partnereng@chef.io>)
|
4
|
-
# Copyright:: Copyright (c) 2017 Chef Software, Inc.
|
5
|
-
# License:: Apache License, Version 2.0
|
6
|
-
#
|
7
|
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
8
|
-
# you may not use this file except in compliance with the License.
|
9
|
-
# You may obtain a copy of the License at
|
10
|
-
#
|
11
|
-
# http://www.apache.org/licenses/LICENSE-2.0
|
12
|
-
#
|
13
|
-
# Unless required by applicable law or agreed to in writing, software
|
14
|
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
15
|
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
16
|
-
# See the License for the specific language governing permissions and
|
17
|
-
# limitations under the License.
|
18
|
-
#
|
19
|
-
|
20
|
-
require "logger"
|
21
|
-
|
22
|
-
module Base
|
23
|
-
attr_accessor :log
|
24
|
-
|
25
|
-
def self.log
|
26
|
-
@log ||= init_logger
|
27
|
-
end
|
28
|
-
|
29
|
-
def self.init_logger
|
30
|
-
log = Logger.new(STDOUT)
|
31
|
-
log.progname = "Knife VCenter"
|
32
|
-
log.level = Logger::INFO
|
33
|
-
log
|
34
|
-
end
|
35
|
-
end
|
@@ -1,464 +0,0 @@
|
|
1
|
-
# Copyright 2014-2017 VMware, Inc. All Rights Reserved.
|
2
|
-
# SPDX-License-Identifier: MIT
|
3
|
-
|
4
|
-
require "savon"
|
5
|
-
require "nokogiri"
|
6
|
-
require "base"
|
7
|
-
# require 'sample/framework/sample_base'
|
8
|
-
|
9
|
-
# Utility class that helps use the lookup service.
|
10
|
-
class LookupServiceHelper
|
11
|
-
attr_reader :sample, :wsdl_url, :soap_url
|
12
|
-
attr_reader :serviceRegistration
|
13
|
-
|
14
|
-
# Constructs a new instance.
|
15
|
-
# @param [Object] host the associated sample, which provides access
|
16
|
-
# to the configuration properties of the sample
|
17
|
-
def initialize(host)
|
18
|
-
@soap_url = format("https://%s/lookupservice/sdk", host)
|
19
|
-
@wsdl_url = format("https://%s/lookupservice/wsdl/lookup.wsdl", host)
|
20
|
-
end
|
21
|
-
|
22
|
-
# Connects to the lookup service.
|
23
|
-
def connect
|
24
|
-
rsc = RetrieveServiceContent.new(client).invoke
|
25
|
-
@serviceRegistration = rsc.get_service_registration
|
26
|
-
Base.log.info "service registration = #{serviceRegistration}"
|
27
|
-
end
|
28
|
-
|
29
|
-
# Finds the SSO service URL.
|
30
|
-
# In a MxN setup where there are more than one PSC nodes;
|
31
|
-
# This method returns the first SSO service endpoint URL
|
32
|
-
# as returned by the lookup service.
|
33
|
-
#
|
34
|
-
# @return [String] SSO Service endpoint URL.
|
35
|
-
def find_sso_url
|
36
|
-
result = find_service_url(product = "com.vmware.cis",
|
37
|
-
service = "cs.identity",
|
38
|
-
endpoint = "com.vmware.cis.cs.identity.sso",
|
39
|
-
protocol = "wsTrust")
|
40
|
-
raise "SSO URL not found" unless result && result.size > 0
|
41
|
-
|
42
|
-
result.values[0]
|
43
|
-
end
|
44
|
-
|
45
|
-
# Finds all the vAPI service endpoint URLs.
|
46
|
-
# In a MxN setup where there are more than one management node;
|
47
|
-
# this method returns more than one URL
|
48
|
-
#
|
49
|
-
# @return [Hash] vapi service endpoint URLs in a dictionary
|
50
|
-
# where the key is the node_id and the value is the service URL.
|
51
|
-
def find_vapi_urls
|
52
|
-
find_service_url(product = "com.vmware.cis",
|
53
|
-
service = "cs.vapi",
|
54
|
-
endpoint = "com.vmware.vapi.endpoint",
|
55
|
-
protocol = "vapi.json.https.public")
|
56
|
-
end
|
57
|
-
|
58
|
-
# Finds the vapi service endpoint URL of a management node.
|
59
|
-
#
|
60
|
-
# @param node_id [String] The UUID of the management node.
|
61
|
-
# @return [String] vapi service endpoint URL of a management node or
|
62
|
-
# nil if no vapi endpoint is found.
|
63
|
-
def find_vapi_url(node_id)
|
64
|
-
raise "node_id is required" if node_id.nil?
|
65
|
-
result = find_vapi_urls()
|
66
|
-
raise "VAPI URLs not found" unless result && result.size > 0
|
67
|
-
result[node_id]
|
68
|
-
end
|
69
|
-
|
70
|
-
# Finds all the vim service endpoint URLs
|
71
|
-
# In a MxN setup where there are more than one management node;
|
72
|
-
# this method returns more than one URL
|
73
|
-
#
|
74
|
-
# @return [Hash] vim service endpoint URLs in a dictionary where
|
75
|
-
# the key is the node_id and the value is the service URL.
|
76
|
-
def find_vim_urls
|
77
|
-
find_service_url(product = "com.vmware.cis",
|
78
|
-
service = "vcenterserver",
|
79
|
-
endpoint = "com.vmware.vim",
|
80
|
-
protocol = "vmomi")
|
81
|
-
end
|
82
|
-
|
83
|
-
# Finds the vim service endpoint URL of a management node
|
84
|
-
#
|
85
|
-
# @param node_id [String] The UUID of the management node.
|
86
|
-
# @return [String] vim service endpoint URL of a management node or
|
87
|
-
# nil if no vim endpoint is found.
|
88
|
-
def find_vim_url(node_id)
|
89
|
-
raise "node_id is required" if node_id.nil?
|
90
|
-
result = find_vim_urls()
|
91
|
-
raise "VIM URLs not found" unless result && result.size > 0
|
92
|
-
result[node_id]
|
93
|
-
end
|
94
|
-
|
95
|
-
# Finds all the spbm service endpoint URLs
|
96
|
-
# In a MxN setup where there are more than one management node;
|
97
|
-
# this method returns more than one URL
|
98
|
-
#
|
99
|
-
# @return [Hash] spbm service endpoint URLs in a dictionary where
|
100
|
-
# the key is the node_id and the value is the service URL.
|
101
|
-
def find_vim_pbm_urls
|
102
|
-
find_service_url(product = "com.vmware.vim.sms",
|
103
|
-
service = "sms",
|
104
|
-
endpoint = "com.vmware.vim.pbm",
|
105
|
-
protocol = "https")
|
106
|
-
end
|
107
|
-
|
108
|
-
# Finds the spbm service endpoint URL of a management node
|
109
|
-
#
|
110
|
-
# @param node_id [String] The UUID of the management node.
|
111
|
-
# @return [String] spbm service endpoint URL of a management node or
|
112
|
-
# nil if no spbm endpoint is found.
|
113
|
-
def find_vim_pbm_url(node_id)
|
114
|
-
raise "node_id is required" if node_id.nil?
|
115
|
-
result = find_vim_pbm_urls()
|
116
|
-
raise "PBM URLs not found" unless result && result.size > 0
|
117
|
-
result[node_id]
|
118
|
-
end
|
119
|
-
|
120
|
-
# Get the management node id from the instance name
|
121
|
-
#
|
122
|
-
# @param instance_name [String] The instance name of the management node
|
123
|
-
# @return [String] The UUID of the management node or
|
124
|
-
# nil is no management node is found by the given instance name
|
125
|
-
def get_mgmt_node_id(instance_name)
|
126
|
-
raise "instance_name is required" if instance_name.nil?
|
127
|
-
|
128
|
-
result = find_mgmt_nodes
|
129
|
-
raise "Management nodes not found" unless result && !result.empty?
|
130
|
-
|
131
|
-
result[instance_name]
|
132
|
-
end
|
133
|
-
|
134
|
-
def get_mgmt_node_instance_name(node_id)
|
135
|
-
raise "node_id is required" if node_id.nil?
|
136
|
-
|
137
|
-
result = find_mgmt_nodes
|
138
|
-
raise "Management nodes not found" unless result && !result.empty?
|
139
|
-
|
140
|
-
result.each { |k, v| return k if v == node_id }
|
141
|
-
nil
|
142
|
-
end
|
143
|
-
|
144
|
-
# Finds the instance name and UUID of the management node for M1xN1 or
|
145
|
-
# when the PSC and management services all reside on a single node.
|
146
|
-
def get_default_mgmt_node
|
147
|
-
result = find_mgmt_nodes
|
148
|
-
raise "Management nodes not found" unless result && !result.empty?
|
149
|
-
|
150
|
-
# WHY: raise MultipleManagementNodeException.new if result.size > 1
|
151
|
-
[result.keys[0], result.values[0]]
|
152
|
-
end
|
153
|
-
|
154
|
-
# Finds all the management nodes
|
155
|
-
#
|
156
|
-
# @return [Hash] management node instance name and node id (UUID) in a dictionary.
|
157
|
-
def find_mgmt_nodes
|
158
|
-
# assert self.serviceRegistration is not None
|
159
|
-
list = List.new(client, "com.vmware.cis", "vcenterserver",
|
160
|
-
"vmomi", "com.vmware.vim")
|
161
|
-
|
162
|
-
list.invoke
|
163
|
-
list.get_instance_names
|
164
|
-
end
|
165
|
-
|
166
|
-
private
|
167
|
-
|
168
|
-
# Finds a service URL with the given attributes.
|
169
|
-
def find_service_url(product, service, endpoint, protocol)
|
170
|
-
# assert serviceRegistration is not None
|
171
|
-
list = List.new(client, product, service, protocol, endpoint)
|
172
|
-
|
173
|
-
list.invoke
|
174
|
-
list.get_service_endpoints
|
175
|
-
end
|
176
|
-
|
177
|
-
# Gets or creates the Savon client instance.
|
178
|
-
def client
|
179
|
-
@client ||= Savon.client do |globals|
|
180
|
-
# see: http://savonrb.com/version2/globals.html
|
181
|
-
globals.wsdl wsdl_url
|
182
|
-
globals.endpoint soap_url
|
183
|
-
|
184
|
-
globals.strip_namespaces false
|
185
|
-
globals.env_namespace :S
|
186
|
-
|
187
|
-
# set like this so https connection does not fail
|
188
|
-
# TODO: find an acceptable solution for production
|
189
|
-
globals.ssl_verify_mode :none
|
190
|
-
|
191
|
-
# dev/debug settings
|
192
|
-
# globals.pretty_print_xml ENV['DEBUG_SOAP']
|
193
|
-
# globals.log ENV['DEBUG_SOAP']
|
194
|
-
end
|
195
|
-
end
|
196
|
-
end
|
197
|
-
|
198
|
-
# @abstract Base class for invocable service calls.
|
199
|
-
class Invocable
|
200
|
-
attr_reader :operation, :client, :response
|
201
|
-
|
202
|
-
# Constructs a new instance.
|
203
|
-
# @param operation [Symbol] the operation name
|
204
|
-
# @param client [Savon::Client] the client
|
205
|
-
def initialize(operation, client)
|
206
|
-
@operation = operation
|
207
|
-
@client = client
|
208
|
-
end
|
209
|
-
|
210
|
-
# Invokes the service call represented by this type.
|
211
|
-
def invoke
|
212
|
-
request = request_xml.to_s
|
213
|
-
Base.log.debug(request)
|
214
|
-
@response = client.call(operation, xml: request)
|
215
|
-
Base.log.debug(response)
|
216
|
-
self # for chaining with new
|
217
|
-
end
|
218
|
-
|
219
|
-
# Builds the request XML content.
|
220
|
-
def request_xml
|
221
|
-
builder = Builder::XmlMarkup.new
|
222
|
-
builder.instruct!(:xml, encoding: "UTF-8")
|
223
|
-
|
224
|
-
builder.tag!("S:Envelope",
|
225
|
-
"xmlns:S" => "http://schemas.xmlsoap.org/soap/envelope/") do |envelope|
|
226
|
-
envelope.tag!("S:Body") do |body|
|
227
|
-
body_xml(body)
|
228
|
-
end
|
229
|
-
end
|
230
|
-
builder.target!
|
231
|
-
end
|
232
|
-
|
233
|
-
# Builds the body portion of the request XML content.
|
234
|
-
# Specific service operations must override this method.
|
235
|
-
def body_xml
|
236
|
-
raise "abstract method not implemented!"
|
237
|
-
end
|
238
|
-
|
239
|
-
# Gets the response XML content.
|
240
|
-
def response_xml
|
241
|
-
raise "illegal state: response not set yet" if response.nil?
|
242
|
-
|
243
|
-
@response_xml ||= Nokogiri::XML(response.to_xml)
|
244
|
-
end
|
245
|
-
|
246
|
-
def response_hash
|
247
|
-
@response_hash ||= response.to_hash
|
248
|
-
end
|
249
|
-
end
|
250
|
-
|
251
|
-
# Encapsulates the list operation of the lookup service.
|
252
|
-
class List < Invocable
|
253
|
-
# Constructs a new instance.
|
254
|
-
def initialize(client, product, service, protocol, endpoint)
|
255
|
-
super(:list, client)
|
256
|
-
|
257
|
-
@product = product
|
258
|
-
@service = service
|
259
|
-
@protocol = protocol
|
260
|
-
@endpoint = endpoint
|
261
|
-
end
|
262
|
-
|
263
|
-
=begin
|
264
|
-
<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
|
265
|
-
<S:Body>
|
266
|
-
<List xmlns="urn:lookup">
|
267
|
-
<_this type="LookupServiceRegistration">ServiceRegistration</_this>
|
268
|
-
<filterCriteria>
|
269
|
-
<serviceType>
|
270
|
-
<product>com.vmware.cis</product>
|
271
|
-
<type>cs.identity</type>
|
272
|
-
</serviceType>
|
273
|
-
<endpointType>
|
274
|
-
<protocol>wsTrust</protocol>
|
275
|
-
<type>com.vmware.cis.cs.identity.sso</type>
|
276
|
-
</endpointType>
|
277
|
-
</filterCriteria>
|
278
|
-
</List>
|
279
|
-
</S:Body>
|
280
|
-
</S:Envelope>
|
281
|
-
=end
|
282
|
-
def body_xml(body)
|
283
|
-
body.tag!("List", "xmlns" => "urn:lookup") do |list|
|
284
|
-
# TODO: use the copy that was retrieved on startup?
|
285
|
-
list.tag!("_this",
|
286
|
-
"type" => "LookupServiceRegistration") do |this|
|
287
|
-
this << "ServiceRegistration"
|
288
|
-
end
|
289
|
-
list.tag!("filterCriteria") do |criteria|
|
290
|
-
criteria.tag!("serviceType") do |stype|
|
291
|
-
stype.tag!("product") do |p|
|
292
|
-
p << @product
|
293
|
-
end
|
294
|
-
stype.tag!("type") do |t|
|
295
|
-
t << @service
|
296
|
-
end
|
297
|
-
end
|
298
|
-
criteria.tag!("endpointType") do |etype|
|
299
|
-
etype.tag!("protocol") do |p|
|
300
|
-
p << @protocol
|
301
|
-
end
|
302
|
-
etype.tag!("type") do |t|
|
303
|
-
t << @endpoint
|
304
|
-
end
|
305
|
-
end
|
306
|
-
end
|
307
|
-
end
|
308
|
-
end
|
309
|
-
|
310
|
-
# Gets the service endpoint information from the response.
|
311
|
-
# Support for MxN.
|
312
|
-
# @return [Hash] a hash where the key is NodeId and the Value is a Service URL
|
313
|
-
def get_service_endpoints
|
314
|
-
result = {}
|
315
|
-
=begin
|
316
|
-
<ListResponse xmlns="urn:lookup">
|
317
|
-
<returnval>
|
318
|
-
<serviceVersion>2.0</serviceVersion>
|
319
|
-
<vendorNameResourceKey/>
|
320
|
-
<vendorNameDefault/>
|
321
|
-
<vendorProductInfoResourceKey/>
|
322
|
-
<vendorProductInfoDefault/>
|
323
|
-
<serviceEndpoints>
|
324
|
-
<url>https://pa-rdinfra3-vm7-dhcp5583.eng.vmware.com/sts/STSService/vsphere.local</url>
|
325
|
-
<endpointType>
|
326
|
-
<protocol>wsTrust</protocol>
|
327
|
-
<type>com.vmware.cis.cs.identity.sso</type>
|
328
|
-
</endpointType>
|
329
|
-
<sslTrust>
|
330
|
-
...
|
331
|
-
</sslTrust>
|
332
|
-
</serviceEndpoints>
|
333
|
-
<serviceNameResourceKey/>
|
334
|
-
<serviceNameDefault/>
|
335
|
-
<serviceDescriptionResourceKey/>
|
336
|
-
<serviceDescriptionDefault/>
|
337
|
-
<ownerId>pa-rdinfra3-vm7-dhcp5583.eng.vmware.com@vsphere.local</ownerId>
|
338
|
-
<serviceType>
|
339
|
-
<product>com.vmware.cis</product>
|
340
|
-
<type>cs.identity</type>
|
341
|
-
</serviceType>
|
342
|
-
<nodeId/>
|
343
|
-
<serviceId>6a8a5058-5d3d-4d42-bb5e-383b91c8732e</serviceId>
|
344
|
-
<siteId>default-first-site</siteId>
|
345
|
-
</returnval>
|
346
|
-
</ListResponse>
|
347
|
-
=end
|
348
|
-
Base.log.debug "List: response_hash = #{response_hash}"
|
349
|
-
return_val = response_hash[:list_response][:returnval]
|
350
|
-
return_val = [return_val] if return_val.is_a? Hash
|
351
|
-
return_val.each do |entry|
|
352
|
-
# FYI: the node_id is sometimes null, so use the service_id in this case
|
353
|
-
node_id = entry[:node_id] || entry[:service_id]
|
354
|
-
result[node_id] = entry[:service_endpoints][:url]
|
355
|
-
end
|
356
|
-
Base.log.debug "List: result = #{result}"
|
357
|
-
result
|
358
|
-
end
|
359
|
-
|
360
|
-
def get_instance_names
|
361
|
-
result = {}
|
362
|
-
=begin
|
363
|
-
<serviceAttributes>
|
364
|
-
<key>com.vmware.cis.cm.GroupInternalId</key>
|
365
|
-
<value>com.vmware.vim.vcenter</value>
|
366
|
-
</serviceAttributes>
|
367
|
-
<serviceAttributes>
|
368
|
-
<key>com.vmware.cis.cm.ControlScript</key>
|
369
|
-
<value>vmware-vpxd.sh</value>
|
370
|
-
</serviceAttributes>
|
371
|
-
<serviceAttributes>
|
372
|
-
<key>com.vmware.cis.cm.HostId</key>
|
373
|
-
<value>906477a1-24c6-4d48-9e99-55ef962878f7</value>
|
374
|
-
</serviceAttributes>
|
375
|
-
<serviceAttributes>
|
376
|
-
<key>com.vmware.vim.vcenter.instanceName</key>
|
377
|
-
<value>pa-rdinfra3-vm7-dhcp5583.eng.vmware.com</value>
|
378
|
-
</serviceAttributes>
|
379
|
-
=end
|
380
|
-
Base.log.debug "List: response_hash = #{response_hash}"
|
381
|
-
return_val = response_hash[:list_response][:returnval]
|
382
|
-
return_val = [return_val] if return_val.is_a? Hash
|
383
|
-
return_val.each do |entry|
|
384
|
-
node_id = entry[:node_id]
|
385
|
-
# TODO: is it possible there be 0 or 1 attrs? if so, deal with it.
|
386
|
-
attrs = entry[:service_attributes]
|
387
|
-
Base.log.debug "List: attrs=#{attrs}"
|
388
|
-
attrs.each do |attr|
|
389
|
-
if attr[:key] == "com.vmware.vim.vcenter.instanceName"
|
390
|
-
result[attr[:value]] = node_id
|
391
|
-
end
|
392
|
-
end
|
393
|
-
end
|
394
|
-
Base.log.debug "List: result = #{result}"
|
395
|
-
result
|
396
|
-
end
|
397
|
-
end
|
398
|
-
|
399
|
-
# Encapsulates the RetrieveServiceContent operation of the lookup service.
|
400
|
-
class RetrieveServiceContent < Invocable
|
401
|
-
|
402
|
-
# Constructs a new instance.
|
403
|
-
def initialize(client)
|
404
|
-
super(:retrieve_service_content, client)
|
405
|
-
end
|
406
|
-
|
407
|
-
=begin
|
408
|
-
<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
|
409
|
-
<S:Body>
|
410
|
-
<RetrieveServiceContent xmlns="urn:lookup">
|
411
|
-
<_this type="LookupServiceInstance">ServiceInstance</_this>
|
412
|
-
</RetrieveServiceContent>
|
413
|
-
</S:Body>
|
414
|
-
</S:Envelope>
|
415
|
-
=end
|
416
|
-
def body_xml(body)
|
417
|
-
body.tag!("RetrieveServiceContent", "xmlns" => "urn:lookup") do |rsc|
|
418
|
-
rsc.tag!("_this", "type" => "LookupServiceInstance") do |this|
|
419
|
-
this << "ServiceInstance"
|
420
|
-
end
|
421
|
-
end
|
422
|
-
end
|
423
|
-
|
424
|
-
=begin
|
425
|
-
...
|
426
|
-
<RetrieveServiceContentResponse xmlns="urn:lookup">
|
427
|
-
<returnval>
|
428
|
-
<lookupService type="LookupLookupService">lookupService</lookupService>
|
429
|
-
<serviceRegistration type="LookupServiceRegistration">ServiceRegistration</serviceRegistration>
|
430
|
-
<deploymentInformationService type="LookupDeploymentInformationService">deploymentInformationService</deploymentInformationService>
|
431
|
-
<l10n type="LookupL10n">l10n</l10n>
|
432
|
-
</returnval>
|
433
|
-
</RetrieveServiceContentResponse>
|
434
|
-
...
|
435
|
-
=end
|
436
|
-
def get_service_registration
|
437
|
-
Base.log.debug "RetrieveServiceContent: response_hash = #{response_hash}"
|
438
|
-
return_val = response_hash[:retrieve_service_content_response][:returnval]
|
439
|
-
result = return_val[:service_registration]
|
440
|
-
Base.log.debug "RetrieveServiceContent: result = #{result}"
|
441
|
-
result
|
442
|
-
end
|
443
|
-
end
|
444
|
-
|
445
|
-
class MultipleManagementNodeException < RuntimeError
|
446
|
-
end
|
447
|
-
|
448
|
-
# main: quick self tester
|
449
|
-
if __FILE__ == $0
|
450
|
-
Base.log.level = Logger::DEBUG if ENV["DEBUG"]
|
451
|
-
sample = SelfTestSample.new
|
452
|
-
sample.ls_ip = ARGV[0] || "10.67.245.207"
|
453
|
-
# MXN: sample.ls_ip = '10.160.42.83'
|
454
|
-
# MXN: sample.ls_ip = '10.160.35.191'
|
455
|
-
# MAYBE: sample.main() # for arg parsing
|
456
|
-
ls_helper = LookupServiceHelper.new(sample)
|
457
|
-
ls_helper.connect
|
458
|
-
puts "***************************************"
|
459
|
-
puts "SSO URL: #{ls_helper.find_sso_url}"
|
460
|
-
puts "VAPI URL: #{ls_helper.find_vapi_urls}"
|
461
|
-
puts "VIM URL: #{ls_helper.find_vim_urls}"
|
462
|
-
puts "PBM URL: #{ls_helper.find_vim_pbm_urls}"
|
463
|
-
puts "Mgmt Nodes: #{ls_helper.find_mgmt_nodes}"
|
464
|
-
end
|
data/lib/sso.rb
DELETED
@@ -1,268 +0,0 @@
|
|
1
|
-
# Copyright 2014-2017 VMware, Inc. All Rights Reserved.
|
2
|
-
# SPDX-License-Identifier: MIT
|
3
|
-
|
4
|
-
require "savon"
|
5
|
-
require "nokogiri"
|
6
|
-
require "date"
|
7
|
-
require "securerandom"
|
8
|
-
|
9
|
-
# A little utility library for VMware SSO.
|
10
|
-
# For now, this is not a general purpose library that covers all
|
11
|
-
# the interfaces of the SSO service.
|
12
|
-
# Specifically, the support is limited to the following:
|
13
|
-
# * request bearer token.
|
14
|
-
module SSO
|
15
|
-
# The XML date format.
|
16
|
-
DATE_FORMAT = "%FT%T.%LZ".freeze
|
17
|
-
|
18
|
-
# The XML namespaces that are required: SOAP, WSDL, et al.
|
19
|
-
NAMESPACES = {
|
20
|
-
"xmlns:S" => "http://schemas.xmlsoap.org/soap/envelope/",
|
21
|
-
"xmlns:wst" => "http://docs.oasis-open.org/ws-sx/ws-trust/200512",
|
22
|
-
"xmlns:u" => "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd",
|
23
|
-
"xmlns:x" => "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd",
|
24
|
-
}.freeze
|
25
|
-
|
26
|
-
# Provides the connection details for the SSO service.
|
27
|
-
class Connection
|
28
|
-
attr_accessor :sso_url, :wsdl_url, :username, :password
|
29
|
-
|
30
|
-
# Creates a new instance.
|
31
|
-
def initialize(sso_url, wsdl_url = nil)
|
32
|
-
self.sso_url = sso_url
|
33
|
-
self.wsdl_url = wsdl_url || "#{sso_url}?wsdl"
|
34
|
-
end
|
35
|
-
|
36
|
-
# Login with the given credentials.
|
37
|
-
# Note: this does not invoke a login action, but rather stores the
|
38
|
-
# credentials for use later.
|
39
|
-
def login(username, password)
|
40
|
-
self.username = username
|
41
|
-
self.password = password
|
42
|
-
self # enable builder pattern
|
43
|
-
end
|
44
|
-
|
45
|
-
# Gets (or creates) the Savon client instance.
|
46
|
-
def client
|
47
|
-
# construct and init the client proxy
|
48
|
-
@client ||= Savon.client do |globals|
|
49
|
-
# see: http://savonrb.com/version2/globals.html
|
50
|
-
globals.wsdl wsdl_url
|
51
|
-
globals.endpoint sso_url
|
52
|
-
|
53
|
-
globals.strip_namespaces false
|
54
|
-
globals.env_namespace :S
|
55
|
-
|
56
|
-
# set like this so https connection does not fail
|
57
|
-
# TODO: find an acceptable solution for production
|
58
|
-
globals.ssl_verify_mode :none
|
59
|
-
|
60
|
-
# dev/debug settings
|
61
|
-
# globals.pretty_print_xml ENV['DEBUG_SOAP']
|
62
|
-
# globals.log ENV['DEBUG_SOAP']
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
# Invokes the request bearer token operation.
|
67
|
-
# @return [SamlToken]
|
68
|
-
def request_bearer_token
|
69
|
-
rst = RequestSecurityToken.new(client, username, password)
|
70
|
-
rst.invoke
|
71
|
-
rst.saml_token
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
# @abstract Base class for invocable service calls.
|
76
|
-
class SoapInvocable
|
77
|
-
attr_reader :operation, :client, :response
|
78
|
-
|
79
|
-
# Constructs a new instance.
|
80
|
-
# @param operation [Symbol] the SOAP operation name (in Symbol form)
|
81
|
-
# @param client [Savon::Client] the client
|
82
|
-
def initialize(operation, client)
|
83
|
-
@operation = operation
|
84
|
-
@client = client
|
85
|
-
end
|
86
|
-
|
87
|
-
# Invokes the service call represented by this type.
|
88
|
-
def invoke
|
89
|
-
request = request_xml.to_s
|
90
|
-
puts "request = #{request}" if ENV["DEBUG"]
|
91
|
-
@response = client.call(operation, xml: request)
|
92
|
-
puts "response = #{response}" if ENV["DEBUG"]
|
93
|
-
self # for chaining with new
|
94
|
-
end
|
95
|
-
|
96
|
-
# Builds the request XML content.
|
97
|
-
def request_xml
|
98
|
-
builder = Builder::XmlMarkup.new
|
99
|
-
builder.instruct!(:xml, encoding: "UTF-8")
|
100
|
-
|
101
|
-
builder.tag!("S:Envelope", NAMESPACES) do |envelope|
|
102
|
-
if has_header?
|
103
|
-
envelope.tag!("S:Header") do |header|
|
104
|
-
header_xml(header)
|
105
|
-
end
|
106
|
-
end
|
107
|
-
envelope.tag!("S:Body") do |body|
|
108
|
-
body_xml(body)
|
109
|
-
end
|
110
|
-
end
|
111
|
-
builder.target!
|
112
|
-
end
|
113
|
-
|
114
|
-
def has_header?
|
115
|
-
true
|
116
|
-
end
|
117
|
-
|
118
|
-
# Builds the header portion of the SOAP request.
|
119
|
-
# Specific service operations must override this method.
|
120
|
-
def header_xml(_header)
|
121
|
-
raise "abstract method not implemented!"
|
122
|
-
end
|
123
|
-
|
124
|
-
# Builds the body portion of the SOAP request.
|
125
|
-
# Specific service operations must override this method.
|
126
|
-
def body_xml(_body)
|
127
|
-
raise "abstract method not implemented!"
|
128
|
-
end
|
129
|
-
|
130
|
-
# Gets the response XML content.
|
131
|
-
def response_xml
|
132
|
-
raise "illegal state: response not set yet" if response.nil?
|
133
|
-
|
134
|
-
@response_xml ||= Nokogiri::XML(response.to_xml)
|
135
|
-
end
|
136
|
-
|
137
|
-
def response_hash
|
138
|
-
@response_hash ||= response.to_hash
|
139
|
-
end
|
140
|
-
end
|
141
|
-
|
142
|
-
# Encapsulates an issue operation that requests a security token
|
143
|
-
# from the SSO service.
|
144
|
-
class RequestSecurityToken < SoapInvocable
|
145
|
-
|
146
|
-
attr_accessor :request_type, :delegatable
|
147
|
-
|
148
|
-
# Constructs a new instance.
|
149
|
-
def initialize(client, username, password, hours = 2)
|
150
|
-
super(:issue, client)
|
151
|
-
|
152
|
-
@username = username
|
153
|
-
@password = password
|
154
|
-
@hours = hours
|
155
|
-
|
156
|
-
# TODO: these things should be configurable, so we can get
|
157
|
-
# non-delegatable tokens, HoK tokens, etc.
|
158
|
-
@request_type = "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue"
|
159
|
-
@delegatable = true
|
160
|
-
end
|
161
|
-
|
162
|
-
def now
|
163
|
-
@now ||= Time.now.utc.to_datetime
|
164
|
-
end
|
165
|
-
|
166
|
-
def created
|
167
|
-
@created ||= now.strftime(DATE_FORMAT)
|
168
|
-
end
|
169
|
-
|
170
|
-
def future
|
171
|
-
@future ||= now + (2 / 24.0) # days (for DateTime math)
|
172
|
-
end
|
173
|
-
|
174
|
-
def expires
|
175
|
-
@expires ||= future.strftime(DATE_FORMAT)
|
176
|
-
end
|
177
|
-
|
178
|
-
# Builds the header XML for the SOAP request.
|
179
|
-
def header_xml(header)
|
180
|
-
id = "uuid-" + SecureRandom.uuid
|
181
|
-
|
182
|
-
# header.tag!("x:Security", "x:mustUnderstand" => "1") do |security|
|
183
|
-
header.tag!("x:Security") do |security|
|
184
|
-
security.tag!("u:Timestamp", "u:Id" => "_0") do |timestamp|
|
185
|
-
timestamp.tag!("u:Created") do |element|
|
186
|
-
element << created
|
187
|
-
end
|
188
|
-
timestamp.tag!("u:Expires") do |element|
|
189
|
-
element << expires
|
190
|
-
end
|
191
|
-
end
|
192
|
-
|
193
|
-
security.tag!("x:UsernameToken", "u:Id" => id) do |utoken|
|
194
|
-
utoken.tag!("x:Username") do |element|
|
195
|
-
element << @username
|
196
|
-
end
|
197
|
-
utoken.tag!("x:Password") do |element|
|
198
|
-
element << @password
|
199
|
-
end
|
200
|
-
end
|
201
|
-
end
|
202
|
-
end
|
203
|
-
|
204
|
-
# Builds the body XML for the SOAP request.
|
205
|
-
def body_xml(body)
|
206
|
-
body.tag!("wst:RequestSecurityToken") do |rst|
|
207
|
-
rst.tag!("wst:RequestType") do |element|
|
208
|
-
element << request_type
|
209
|
-
end
|
210
|
-
rst.tag!("wst:Delegatable") do |element|
|
211
|
-
element << delegatable.to_s
|
212
|
-
end
|
213
|
-
=begin
|
214
|
-
#TODO: we don't seem to need this, but I'm leaving this
|
215
|
-
#here for now as a reminder.
|
216
|
-
rst.tag!("wst:Lifetime") do |lifetime|
|
217
|
-
lifetime.tag!("u:Created") do |element|
|
218
|
-
element << created
|
219
|
-
end
|
220
|
-
lifetime.tag!("u:Expires") do |element|
|
221
|
-
element << expires
|
222
|
-
end
|
223
|
-
end
|
224
|
-
=end
|
225
|
-
end
|
226
|
-
end
|
227
|
-
|
228
|
-
# Gets the saml_token from the SOAP response body.
|
229
|
-
# @return [SamlToken] the requested SAML token
|
230
|
-
def saml_token
|
231
|
-
assertion = response_xml.at_xpath("//saml2:Assertion",
|
232
|
-
"saml2" => "urn:oasis:names:tc:SAML:2.0:assertion")
|
233
|
-
SamlToken.new(assertion)
|
234
|
-
end
|
235
|
-
end
|
236
|
-
|
237
|
-
# Holds a SAML token.
|
238
|
-
class SamlToken
|
239
|
-
attr_reader :xml
|
240
|
-
|
241
|
-
# Creates a new instance.
|
242
|
-
def initialize(xml)
|
243
|
-
@xml = xml
|
244
|
-
end
|
245
|
-
|
246
|
-
# TODO: add some getters for interesting content
|
247
|
-
|
248
|
-
def to_s
|
249
|
-
esc_token = xml.to_xml(indent: 0, encoding: "UTF-8")
|
250
|
-
esc_token = esc_token.delete("\n")
|
251
|
-
esc_token
|
252
|
-
end
|
253
|
-
end
|
254
|
-
end
|
255
|
-
|
256
|
-
# main: quick self tester
|
257
|
-
if __FILE__ == $0
|
258
|
-
cloudvm_ip = ARGV[0]
|
259
|
-
cloudvm_ip ||= "10.20.17.0"
|
260
|
-
# cloudvm_ip ||= "10.67.245.207"
|
261
|
-
sso_url = "https://#{cloudvm_ip}/sts/STSService/vsphere.local"
|
262
|
-
wsdl_url = "#{sso_url}?wsdl"
|
263
|
-
sso = SSO::Connection.new(sso_url, wsdl_url)
|
264
|
-
# sso.login("administrator@vsphere.local", "Admin!23")
|
265
|
-
sso.login("root", "vmware")
|
266
|
-
token = sso.request_bearer_token
|
267
|
-
puts token.to_s
|
268
|
-
end
|