kitchen-vcenter 1.5.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|