knife-vcenter 2.0.0 → 2.0.1
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 +5 -5
- data/{LICENSE.txt → LICENSE} +0 -0
- data/lib/base.rb +8 -3
- data/lib/chef/knife/cloud/vcenter_service.rb +117 -81
- data/lib/chef/knife/cloud/vcenter_service_helpers.rb +15 -3
- data/lib/chef/knife/cloud/vcenter_service_options.rb +18 -17
- data/lib/chef/knife/vcenter_cluster_list.rb +19 -13
- data/lib/chef/knife/vcenter_datacenter_list.rb +19 -13
- data/lib/chef/knife/vcenter_host_list.rb +28 -19
- data/lib/chef/knife/vcenter_vm_clone.rb +39 -28
- data/lib/chef/knife/vcenter_vm_create.rb +26 -21
- data/lib/chef/knife/vcenter_vm_delete.rb +15 -10
- data/lib/chef/knife/vcenter_vm_list.rb +31 -21
- data/lib/chef/knife/vcenter_vm_show.rb +12 -13
- data/lib/knife-vcenter/version.rb +4 -2
- data/lib/lookup_service_helper.rb +421 -427
- data/lib/sso.rb +213 -218
- data/lib/support/clone_vm.rb +4 -4
- data/spec/spec_helper.rb +2 -2
- data/spec/unit/vcenter_vm_list_spec.rb +21 -22
- metadata +6 -58
- data/.github/ISSUE_TEMPLATE.md +0 -22
- data/.github/PULL_REQUEST_TEMPLATE.md +0 -14
- data/.gitignore +0 -21
- data/.rubocop.yml +0 -18
- data/.travis.yml +0 -16
- data/CHANGELOG.md +0 -34
- data/Gemfile +0 -3
- data/README.md +0 -219
- data/Rakefile +0 -20
- data/knife-vcenter.gemspec +0 -37
data/lib/sso.rb
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
# Copyright 2014-2017 VMware, Inc. All Rights Reserved.
|
2
2
|
# SPDX-License-Identifier: MIT
|
3
3
|
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
4
|
+
require "savon"
|
5
|
+
require "nokogiri"
|
6
|
+
require "date"
|
7
|
+
require "securerandom"
|
8
8
|
|
9
9
|
# A little utility library for VMware SSO.
|
10
10
|
# For now, this is not a general purpose library that covers all
|
@@ -12,258 +12,253 @@ require 'securerandom'
|
|
12
12
|
# Specifically, the support is limited to the following:
|
13
13
|
# * request bearer token.
|
14
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
|
15
35
|
|
16
|
-
#
|
17
|
-
|
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
|
18
44
|
|
19
|
-
#
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
26
65
|
|
27
|
-
#
|
28
|
-
|
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
|
29
86
|
|
30
|
-
|
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
|
31
95
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
end
|
96
|
+
# Builds the request XML content.
|
97
|
+
def request_xml
|
98
|
+
builder = Builder::XmlMarkup.new
|
99
|
+
builder.instruct!(:xml, encoding: "UTF-8")
|
37
100
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
self.password = password
|
44
|
-
self # enable builder pattern
|
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
|
45
106
|
end
|
46
|
-
|
47
|
-
|
48
|
-
def client
|
49
|
-
# construct and init the client proxy
|
50
|
-
@client ||= Savon.client do |globals|
|
51
|
-
# see: http://savonrb.com/version2/globals.html
|
52
|
-
globals.wsdl wsdl_url
|
53
|
-
globals.endpoint sso_url
|
54
|
-
|
55
|
-
globals.strip_namespaces false
|
56
|
-
globals.env_namespace :S
|
57
|
-
|
58
|
-
# set like this so https connection does not fail
|
59
|
-
# TODO: find an acceptable solution for production
|
60
|
-
globals.ssl_verify_mode :none
|
61
|
-
|
62
|
-
# dev/debug settings
|
63
|
-
# globals.pretty_print_xml ENV['DEBUG_SOAP']
|
64
|
-
# globals.log ENV['DEBUG_SOAP']
|
65
|
-
end
|
107
|
+
envelope.tag!("S:Body") do |body|
|
108
|
+
body_xml(body)
|
66
109
|
end
|
110
|
+
end
|
111
|
+
builder.target!
|
112
|
+
end
|
67
113
|
|
68
|
-
|
69
|
-
|
70
|
-
def request_bearer_token()
|
71
|
-
rst = RequestSecurityToken.new(client, username, password)
|
72
|
-
rst.invoke()
|
73
|
-
rst.saml_token
|
74
|
-
end
|
114
|
+
def has_header?
|
115
|
+
true
|
75
116
|
end
|
76
117
|
|
77
|
-
#
|
78
|
-
|
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
|
79
123
|
|
80
|
-
|
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
|
81
129
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
def initialize(operation, client)
|
86
|
-
@operation = operation
|
87
|
-
@client = client
|
88
|
-
end
|
130
|
+
# Gets the response XML content.
|
131
|
+
def response_xml
|
132
|
+
raise "illegal state: response not set yet" if response.nil?
|
89
133
|
|
90
|
-
|
91
|
-
|
92
|
-
request = request_xml.to_s
|
93
|
-
puts "request = #{request}" if ENV['DEBUG']
|
94
|
-
@response = client.call(operation, xml:request)
|
95
|
-
puts "response = #{response}" if ENV['DEBUG']
|
96
|
-
self # for chaining with new
|
97
|
-
end
|
134
|
+
@response_xml ||= Nokogiri::XML(response.to_xml)
|
135
|
+
end
|
98
136
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
builder.tag!("S:Envelope", NAMESPACES) do |envelope|
|
105
|
-
if has_header?
|
106
|
-
envelope.tag!("S:Header") do |header|
|
107
|
-
header_xml(header)
|
108
|
-
end
|
109
|
-
end
|
110
|
-
envelope.tag!("S:Body") do |body|
|
111
|
-
body_xml(body)
|
112
|
-
end
|
113
|
-
end
|
114
|
-
builder.target!
|
115
|
-
end
|
137
|
+
def response_hash
|
138
|
+
@response_hash ||= response.to_hash
|
139
|
+
end
|
140
|
+
end
|
116
141
|
|
117
|
-
|
118
|
-
|
119
|
-
|
142
|
+
# Encapsulates an issue operation that requests a security token
|
143
|
+
# from the SSO service.
|
144
|
+
class RequestSecurityToken < SoapInvocable
|
145
|
+
attr_accessor :request_type, :delegatable
|
120
146
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
raise 'abstract method not implemented!'
|
125
|
-
end
|
147
|
+
# Constructs a new instance.
|
148
|
+
def initialize(client, username, password, hours = 2)
|
149
|
+
super(:issue, client)
|
126
150
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
raise 'abstract method not implemented!'
|
131
|
-
end
|
151
|
+
@username = username
|
152
|
+
@password = password
|
153
|
+
@hours = hours
|
132
154
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
end
|
138
|
-
|
139
|
-
def response_hash
|
140
|
-
@response_hash ||= response.to_hash
|
141
|
-
end
|
155
|
+
# TODO: these things should be configurable, so we can get
|
156
|
+
# non-delegatable tokens, HoK tokens, etc.
|
157
|
+
@request_type = "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue"
|
158
|
+
@delegatable = true
|
142
159
|
end
|
143
160
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
attr_accessor :request_type, :delegatable
|
149
|
-
|
150
|
-
# Constructs a new instance.
|
151
|
-
def initialize(client, username, password, hours=2)
|
152
|
-
super(:issue, client)
|
153
|
-
|
154
|
-
@username = username
|
155
|
-
@password = password
|
156
|
-
@hours = hours
|
161
|
+
def now
|
162
|
+
@now ||= Time.now.utc.to_datetime
|
163
|
+
end
|
157
164
|
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
@delegatable = true
|
162
|
-
end
|
165
|
+
def created
|
166
|
+
@created ||= now.strftime(DATE_FORMAT)
|
167
|
+
end
|
163
168
|
|
164
|
-
|
165
|
-
|
166
|
-
|
169
|
+
def future
|
170
|
+
@future ||= now + (2 / 24.0) # days (for DateTime math)
|
171
|
+
end
|
167
172
|
|
168
|
-
|
169
|
-
|
170
|
-
|
173
|
+
def expires
|
174
|
+
@expires ||= future.strftime(DATE_FORMAT)
|
175
|
+
end
|
171
176
|
|
172
|
-
|
173
|
-
|
177
|
+
# Builds the header XML for the SOAP request.
|
178
|
+
def header_xml(header)
|
179
|
+
id = "uuid-" + SecureRandom.uuid
|
180
|
+
|
181
|
+
# header.tag!("x:Security", "x:mustUnderstand" => "1") do |security|
|
182
|
+
header.tag!("x:Security") do |security|
|
183
|
+
security.tag!("u:Timestamp", "u:Id" => "_0") do |timestamp|
|
184
|
+
timestamp.tag!("u:Created") do |element|
|
185
|
+
element << created
|
186
|
+
end
|
187
|
+
timestamp.tag!("u:Expires") do |element|
|
188
|
+
element << expires
|
189
|
+
end
|
174
190
|
end
|
175
|
-
|
176
|
-
|
177
|
-
|
191
|
+
security.tag!("x:UsernameToken", "u:Id" => id) do |utoken|
|
192
|
+
utoken.tag!("x:Username") do |element|
|
193
|
+
element << @username
|
194
|
+
end
|
195
|
+
utoken.tag!("x:Password") do |element|
|
196
|
+
element << @password
|
197
|
+
end
|
178
198
|
end
|
199
|
+
end
|
200
|
+
end
|
179
201
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
header.tag!("x:Security") do |security|
|
186
|
-
security.tag!("u:Timestamp", "u:Id" => "_0") do |timestamp|
|
187
|
-
timestamp.tag!("u:Created") do |element|
|
188
|
-
element << created
|
189
|
-
end
|
190
|
-
timestamp.tag!("u:Expires") do |element|
|
191
|
-
element << expires
|
192
|
-
end
|
193
|
-
end
|
194
|
-
security.tag!("x:UsernameToken", "u:Id" => id) do |utoken|
|
195
|
-
utoken.tag!("x:Username") do |element|
|
196
|
-
element << @username
|
197
|
-
end
|
198
|
-
utoken.tag!("x:Password") do |element|
|
199
|
-
element << @password
|
200
|
-
end
|
201
|
-
end
|
202
|
-
end
|
202
|
+
# Builds the body XML for the SOAP request.
|
203
|
+
def body_xml(body)
|
204
|
+
body.tag!("wst:RequestSecurityToken") do |rst|
|
205
|
+
rst.tag!("wst:RequestType") do |element|
|
206
|
+
element << request_type
|
203
207
|
end
|
204
|
-
|
205
|
-
|
206
|
-
def body_xml(body)
|
207
|
-
body.tag!("wst:RequestSecurityToken") do |rst|
|
208
|
-
rst.tag!("wst:RequestType") do |element|
|
209
|
-
element << request_type
|
210
|
-
end
|
211
|
-
rst.tag!("wst:Delegatable") do |element|
|
212
|
-
element << delegatable.to_s
|
213
|
-
end
|
214
|
-
=begin
|
215
|
-
#TODO: we don't seem to need this, but I'm leaving this
|
216
|
-
#here for now as a reminder.
|
217
|
-
rst.tag!("wst:Lifetime") do |lifetime|
|
218
|
-
lifetime.tag!("u:Created") do |element|
|
219
|
-
element << created
|
220
|
-
end
|
221
|
-
lifetime.tag!("u:Expires") do |element|
|
222
|
-
element << expires
|
223
|
-
end
|
224
|
-
end
|
225
|
-
=end
|
226
|
-
end
|
208
|
+
rst.tag!("wst:Delegatable") do |element|
|
209
|
+
element << delegatable.to_s
|
227
210
|
end
|
211
|
+
# #TODO: we don't seem to need this, but I'm leaving this
|
212
|
+
# #here for now as a reminder.
|
213
|
+
# rst.tag!("wst:Lifetime") do |lifetime|
|
214
|
+
# lifetime.tag!("u:Created") do |element|
|
215
|
+
# element << created
|
216
|
+
# end
|
217
|
+
# lifetime.tag!("u:Expires") do |element|
|
218
|
+
# element << expires
|
219
|
+
# end
|
220
|
+
# end
|
221
|
+
end
|
222
|
+
end
|
228
223
|
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
end
|
224
|
+
# Gets the saml_token from the SOAP response body.
|
225
|
+
# @return [SamlToken] the requested SAML token
|
226
|
+
def saml_token
|
227
|
+
assertion = response_xml.at_xpath("//saml2:Assertion",
|
228
|
+
"saml2" => "urn:oasis:names:tc:SAML:2.0:assertion")
|
229
|
+
SamlToken.new(assertion)
|
236
230
|
end
|
231
|
+
end
|
237
232
|
|
238
|
-
|
239
|
-
|
240
|
-
|
233
|
+
# Holds a SAML token.
|
234
|
+
class SamlToken
|
235
|
+
attr_reader :xml
|
241
236
|
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
237
|
+
# Creates a new instance.
|
238
|
+
def initialize(xml)
|
239
|
+
@xml = xml
|
240
|
+
end
|
246
241
|
|
247
|
-
|
242
|
+
# TODO: add some getters for interesting content
|
248
243
|
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
end
|
244
|
+
def to_s
|
245
|
+
esc_token = xml.to_xml(indent: 0, encoding: "UTF-8")
|
246
|
+
esc_token = esc_token.delete("\n")
|
247
|
+
esc_token
|
254
248
|
end
|
249
|
+
end
|
255
250
|
end
|
256
251
|
|
257
252
|
# main: quick self tester
|
258
|
-
if
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
253
|
+
if $PROGRAM_NAME == __FILE__
|
254
|
+
cloudvm_ip = ARGV[0]
|
255
|
+
cloudvm_ip ||= "10.20.17.0"
|
256
|
+
# cloudvm_ip ||= "10.67.245.207"
|
257
|
+
sso_url = "https://#{cloudvm_ip}/sts/STSService/vsphere.local"
|
258
|
+
wsdl_url = "#{sso_url}?wsdl"
|
259
|
+
sso = SSO::Connection.new(sso_url, wsdl_url)
|
260
|
+
# sso.login("administrator@vsphere.local", "Admin!23")
|
261
|
+
sso.login("root", "vmware")
|
262
|
+
token = sso.request_bearer_token
|
263
|
+
puts token.to_s
|
269
264
|
end
|