ovirt-engine-sdk 4.0.1 → 4.4.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/CHANGES.adoc +684 -0
- data/README.adoc +729 -32
- data/ext/ovirtsdk4c/extconf.rb +31 -5
- data/ext/ovirtsdk4c/ov_error.c +9 -2
- data/ext/ovirtsdk4c/ov_error.h +3 -1
- data/ext/ovirtsdk4c/ov_http_client.c +1218 -0
- data/ext/ovirtsdk4c/ov_http_client.h +75 -0
- data/ext/ovirtsdk4c/ov_http_request.c +397 -0
- data/ext/ovirtsdk4c/ov_http_request.h +54 -0
- data/ext/ovirtsdk4c/ov_http_response.c +210 -0
- data/ext/ovirtsdk4c/ov_http_response.h +41 -0
- data/ext/ovirtsdk4c/ov_http_transfer.c +91 -0
- data/ext/ovirtsdk4c/ov_http_transfer.h +47 -0
- data/ext/ovirtsdk4c/ov_module.h +2 -2
- data/ext/ovirtsdk4c/ov_string.c +43 -0
- data/ext/ovirtsdk4c/ov_string.h +25 -0
- data/ext/ovirtsdk4c/ov_xml_reader.c +115 -99
- data/ext/ovirtsdk4c/ov_xml_reader.h +20 -3
- data/ext/ovirtsdk4c/ov_xml_writer.c +95 -77
- data/ext/ovirtsdk4c/ov_xml_writer.h +18 -3
- data/ext/ovirtsdk4c/ovirtsdk4c.c +10 -2
- data/lib/ovirtsdk4/connection.rb +695 -0
- data/lib/ovirtsdk4/errors.rb +70 -0
- data/lib/ovirtsdk4/probe.rb +324 -0
- data/lib/ovirtsdk4/reader.rb +74 -40
- data/lib/ovirtsdk4/readers.rb +3325 -976
- data/lib/ovirtsdk4/service.rb +439 -48
- data/lib/ovirtsdk4/services.rb +29365 -21180
- data/lib/ovirtsdk4/type.rb +20 -6
- data/lib/ovirtsdk4/types.rb +15048 -3198
- data/lib/ovirtsdk4/version.rb +1 -1
- data/lib/ovirtsdk4/writer.rb +108 -13
- data/lib/ovirtsdk4/writers.rb +1373 -294
- data/lib/ovirtsdk4.rb +4 -2
- metadata +88 -36
- data/lib/ovirtsdk4/http.rb +0 -548
@@ -0,0 +1,695 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2015-2017 Red Hat, Inc.
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
# you may not use this file except in compliance with the License.
|
6
|
+
# You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
#
|
16
|
+
|
17
|
+
require 'json'
|
18
|
+
require 'tempfile'
|
19
|
+
require 'uri'
|
20
|
+
|
21
|
+
module OvirtSDK4
|
22
|
+
#
|
23
|
+
# This class is responsible for managing an HTTP connection to the engine server. It is intended as the entry
|
24
|
+
# point for the SDK, and it provides access to the `system` service and, from there, to the rest of the services
|
25
|
+
# provided by the API.
|
26
|
+
#
|
27
|
+
class Connection
|
28
|
+
#
|
29
|
+
# Creates a new connection to the API server.
|
30
|
+
#
|
31
|
+
# [source,ruby]
|
32
|
+
# ----
|
33
|
+
# connection = OvirtSDK4::Connection.new(
|
34
|
+
# url: 'https://engine.example.com/ovirt-engine/api',
|
35
|
+
# username: 'admin@internal',
|
36
|
+
# password: '...',
|
37
|
+
# ca_file:'/etc/pki/ovirt-engine/ca.pem'
|
38
|
+
# )
|
39
|
+
# ----
|
40
|
+
#
|
41
|
+
# @param opts [Hash] The options used to create the connection.
|
42
|
+
#
|
43
|
+
# @option opts [String] :url A string containing the base URL of the server, usually something like
|
44
|
+
# `\https://server.example.com/ovirt-engine/api`.
|
45
|
+
#
|
46
|
+
# @option opts [String] :username The name of the user, something like `admin@internal`.
|
47
|
+
#
|
48
|
+
# @option opts [String] :password The password of the user.
|
49
|
+
#
|
50
|
+
# @option opts [String] :token The token used to authenticate. Optionally the caller can explicitly provide
|
51
|
+
# the token, instead of the user name and password. If the token isn't provided then it will be automatically
|
52
|
+
# created.
|
53
|
+
#
|
54
|
+
# @option opts [Boolean] :insecure (false) A boolean flag that indicates if the server TLS certificate and host
|
55
|
+
# name should be checked.
|
56
|
+
#
|
57
|
+
# @option opts [String] :ca_file The name of a PEM file containing the trusted CA certificates. The certificate
|
58
|
+
# presented by the server will be verified using these CA certificates. If neither this nor the `ca_certs`
|
59
|
+
# options are provided, then the system wide CA certificates store is used. If both options are provided,
|
60
|
+
# then the certificates from both options will be trusted.
|
61
|
+
#
|
62
|
+
# @option opts [Array<String>] :ca_certs An array of strings containing the trusted CA certificates, in PEM
|
63
|
+
# format. The certificate presented by the server will be verified using these CA certificates. If neither this
|
64
|
+
# nor the `ca_file` options are provided, then the system wide CA certificates store is used. If both options
|
65
|
+
# are provided, then the certificates from both options will be trusted.
|
66
|
+
#
|
67
|
+
# @option opts [Boolean] :debug (false) A boolean flag indicating if debug output should be generated. If the
|
68
|
+
# values is `true` and the `log` parameter isn't `nil` then the data sent to and received from the server will be
|
69
|
+
# written to the log. Be aware that user names and passwords will also be written, so handle with care.
|
70
|
+
#
|
71
|
+
# @option opts [Logger] :log The logger where the log messages will be written.
|
72
|
+
#
|
73
|
+
# @option opts [Boolean] :kerberos (false) A boolean flag indicating if Kerberos authentication should be used
|
74
|
+
# instead of user name and password to obtain the OAuth token.
|
75
|
+
#
|
76
|
+
# @option opts [Integer] :timeout (0) The maximun total time to wait for the response, in seconds. A value of zero
|
77
|
+
# (the default) means wait for ever. If the timeout expires before the response is received a `TimeoutError`
|
78
|
+
# exception will be raised.
|
79
|
+
#
|
80
|
+
# @option opts [Integer] :connect_timeout (300) The maximun time to wait for connection establishment, in seconds.
|
81
|
+
# If the timeout expires before the connection is established a `TimeoutError` exception will be raised.
|
82
|
+
#
|
83
|
+
# @option opts [Boolean] :compress (true) A boolean flag indicating if the SDK should ask the server to send
|
84
|
+
# compressed responses. Note that this is a hint for the server, and that it may return uncompressed data even
|
85
|
+
# when this parameter is set to `true`. Also, compression will be automatically disabled when the `debug`
|
86
|
+
# parameter is set to `true`, as otherwise the debug output will be compressed as well, and then it isn't
|
87
|
+
# useful.
|
88
|
+
#
|
89
|
+
# @option opts [String] :proxy_url A string containing the protocol, address and port number of the proxy server
|
90
|
+
# to use to connect to the server. For example, in order to use the HTTP proxy `proxy.example.com` that is
|
91
|
+
# listening on port `3128` the value should be `http://proxy.example.com:3128`. This is optional, and if not
|
92
|
+
# given the connection will go directly to the server specified in the `url` parameter.
|
93
|
+
#
|
94
|
+
# @option opts [String] :proxy_username The name of the user to authenticate to the proxy server.
|
95
|
+
#
|
96
|
+
# @option opts [String] :proxy_password The password of the user to authenticate to the proxy server.
|
97
|
+
#
|
98
|
+
# @option opts [Hash] :headers Custom HTTP headers to send with all requests. The keys of the hash can be
|
99
|
+
# strings of symbols, and they will be used as the names of the headers. The values of the hash will be used
|
100
|
+
# as the names of the headers. If the same header is provided here and in the `headers` parameter of a specific
|
101
|
+
# method call, then the `headers` parameter of the specific method call will have precedence.
|
102
|
+
#
|
103
|
+
# @option opts [Integer] :connections (1) The maximum number of connections to open to the host. The value must
|
104
|
+
# be greater than 0.
|
105
|
+
#
|
106
|
+
# @option opts [Integer] :pipeline (0) The maximum number of request to put in an HTTP pipeline without waiting for
|
107
|
+
# the response. If the value is `0` (the default) then pipelining is disabled.
|
108
|
+
#
|
109
|
+
def initialize(opts = {})
|
110
|
+
# Get the values of the parameters and assign default values:
|
111
|
+
@url = opts[:url]
|
112
|
+
@username = opts[:username]
|
113
|
+
@password = opts[:password]
|
114
|
+
@token = opts[:token]
|
115
|
+
@insecure = opts[:insecure] || false
|
116
|
+
@ca_file = opts[:ca_file]
|
117
|
+
@ca_certs = opts[:ca_certs]
|
118
|
+
@debug = opts[:debug] || false
|
119
|
+
@log = opts[:log]
|
120
|
+
@kerberos = opts[:kerberos] || false
|
121
|
+
@timeout = opts[:timeout] || 0
|
122
|
+
@connect_timeout = opts[:connect_timeout] || 0
|
123
|
+
@compress = opts[:compress] || true
|
124
|
+
@proxy_url = opts[:proxy_url]
|
125
|
+
@proxy_username = opts[:proxy_username]
|
126
|
+
@proxy_password = opts[:proxy_password]
|
127
|
+
@headers = opts[:headers]
|
128
|
+
@connections = opts[:connections] || 1
|
129
|
+
@pipeline = opts[:pipeline] || 0
|
130
|
+
|
131
|
+
# Check that the URL has been provided:
|
132
|
+
raise ArgumentError, "The 'url' option is mandatory" unless @url
|
133
|
+
|
134
|
+
# Automatically disable compression when debug is enabled, as otherwise the debug output generated by
|
135
|
+
# libcurl is also compressed, and that isn't useful for debugging:
|
136
|
+
@compress = false if @debug
|
137
|
+
|
138
|
+
# Create a temporary file to store the CA certificates, and populate it with the contents of the 'ca_file' and
|
139
|
+
# 'ca_certs' options. The file will be removed when the connection is closed.
|
140
|
+
@ca_store = nil
|
141
|
+
if @ca_file || @ca_certs
|
142
|
+
@ca_store = Tempfile.new('ca_store')
|
143
|
+
@ca_store.write(::File.read(@ca_file)) if @ca_file
|
144
|
+
if @ca_certs
|
145
|
+
@ca_certs.each do |ca_cert|
|
146
|
+
@ca_store.write(ca_cert)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
@ca_store.close
|
150
|
+
end
|
151
|
+
|
152
|
+
# Create the mutex that will be used to prevents simultaneous access to the same HTTP client by multiple threads:
|
153
|
+
@mutex = Mutex.new
|
154
|
+
|
155
|
+
# Create the HTTP client:
|
156
|
+
@client = HttpClient.new(
|
157
|
+
insecure: @insecure,
|
158
|
+
ca_file: @ca_store ? @ca_store.path : nil,
|
159
|
+
debug: @debug,
|
160
|
+
log: @log,
|
161
|
+
timeout: @timeout,
|
162
|
+
connect_timeout: @connect_timeout,
|
163
|
+
compress: @compress,
|
164
|
+
proxy_url: @proxy_url,
|
165
|
+
proxy_username: @proxy_username,
|
166
|
+
proxy_password: @proxy_password,
|
167
|
+
connections: @connections,
|
168
|
+
pipeline: @pipeline
|
169
|
+
)
|
170
|
+
end
|
171
|
+
|
172
|
+
#
|
173
|
+
# Returns a reference to the root of the services tree.
|
174
|
+
#
|
175
|
+
# @return [SystemService]
|
176
|
+
#
|
177
|
+
def system_service
|
178
|
+
@system_service ||= SystemService.new(self, '')
|
179
|
+
end
|
180
|
+
|
181
|
+
#
|
182
|
+
# Returns a reference to the service corresponding to the given path. For example, if the `path` parameter
|
183
|
+
# is `vms/123/diskattachments` then it will return a reference to the service that manages the disk
|
184
|
+
# attachments for the virtual machine with identifier `123`.
|
185
|
+
#
|
186
|
+
# @param path [String] The path of the service, for example `vms/123/diskattachments`.
|
187
|
+
# @return [Service]
|
188
|
+
# @raise [Error] If there is no service corresponding to the given path.
|
189
|
+
#
|
190
|
+
def service(path)
|
191
|
+
system_service.service(path)
|
192
|
+
end
|
193
|
+
|
194
|
+
#
|
195
|
+
# Sends an HTTP request, making sure that multiple threads are coordinated correctly.
|
196
|
+
#
|
197
|
+
# @param request [HttpRequest] The request object containing the details of the HTTP request to send.
|
198
|
+
#
|
199
|
+
# @api private
|
200
|
+
#
|
201
|
+
def send(request)
|
202
|
+
@mutex.synchronize { internal_send(request) }
|
203
|
+
end
|
204
|
+
|
205
|
+
#
|
206
|
+
# Waits for the response to the given request, making sure that multiple threads are coordinated correctly.
|
207
|
+
#
|
208
|
+
# @param request [HttpRequest] The request object whose corresponding response you want to wait for.
|
209
|
+
# @return [HttpResponse] A request object containing the details of the HTTP response received.
|
210
|
+
#
|
211
|
+
# @api private
|
212
|
+
#
|
213
|
+
def wait(request)
|
214
|
+
@mutex.synchronize { internal_wait(request) }
|
215
|
+
end
|
216
|
+
|
217
|
+
#
|
218
|
+
# Tests the connectivity with the server. If connectivity works correctly it returns `true`. If there is any
|
219
|
+
# connectivity problem it will either return `false` or raise an exception if the `raise_exception` parameter is
|
220
|
+
# `true`.
|
221
|
+
#
|
222
|
+
# @param raise_exception [Boolean]
|
223
|
+
#
|
224
|
+
# @param timeout [Integer] (nil) The maximun total time to wait for the test to complete, in seconds. If the value
|
225
|
+
# is `nil` (the default) then the timeout set globally for the connection will be used.
|
226
|
+
#
|
227
|
+
# @return [Boolean]
|
228
|
+
#
|
229
|
+
def test(raise_exception = false, timeout = nil)
|
230
|
+
system_service.get(timeout: timeout)
|
231
|
+
true
|
232
|
+
rescue StandardError
|
233
|
+
raise if raise_exception
|
234
|
+
|
235
|
+
false
|
236
|
+
end
|
237
|
+
|
238
|
+
#
|
239
|
+
# Performs the authentication process and returns the authentication token. Usually there is no need to
|
240
|
+
# call this method, as authentication is performed automatically when needed. But in some situations it
|
241
|
+
# may be useful to perform authentication explicitly, and then use the obtained token to create other
|
242
|
+
# connections, using the `token` parameter of the constructor instead of the user name and password.
|
243
|
+
#
|
244
|
+
# @return [String]
|
245
|
+
#
|
246
|
+
def authenticate
|
247
|
+
# rubocop:disable Naming/MemoizedInstanceVariableName
|
248
|
+
@token ||= create_access_token
|
249
|
+
# rubocop:enable Naming/MemoizedInstanceVariableName
|
250
|
+
end
|
251
|
+
|
252
|
+
#
|
253
|
+
# Indicates if the given object is a link. An object is a link if it has an `href` attribute.
|
254
|
+
#
|
255
|
+
# @return [Boolean]
|
256
|
+
#
|
257
|
+
def link?(object)
|
258
|
+
!object.href.nil?
|
259
|
+
end
|
260
|
+
|
261
|
+
#
|
262
|
+
# The `link?` method used to be named `is_link?`, and we need to preserve it for backwards compatibility, but try to
|
263
|
+
# avoid using it.
|
264
|
+
#
|
265
|
+
# @return [Boolean]
|
266
|
+
#
|
267
|
+
# @deprecated Please use `link?` instead.
|
268
|
+
#
|
269
|
+
alias is_link? link?
|
270
|
+
|
271
|
+
#
|
272
|
+
# Follows the `href` attribute of the given object, retrieves the target object and returns it.
|
273
|
+
#
|
274
|
+
# @param object [Type] The object containing the `href` attribute.
|
275
|
+
# @raise [Error] If the `href` attribute has no value, or the link can't be followed.
|
276
|
+
#
|
277
|
+
def follow_link(object)
|
278
|
+
# Check that the "href" has a value, as it is needed in order to retrieve the representation of the object:
|
279
|
+
href = object.href
|
280
|
+
raise Error, "Can't follow link because the 'href' attribute doesn't have a value" if href.nil?
|
281
|
+
|
282
|
+
# Check that the value of the "href" attribute is compatible with the base URL of the connection:
|
283
|
+
prefix = URI(@url).path
|
284
|
+
prefix += '/' unless prefix.end_with?('/')
|
285
|
+
unless href.start_with?(prefix)
|
286
|
+
raise Error, "The URL '#{href}' isn't compatible with the base URL of the connection"
|
287
|
+
end
|
288
|
+
|
289
|
+
# Remove the prefix from the URL, follow the path to the relevant service and invoke the "get" or "list" method
|
290
|
+
# to retrieve its representation:
|
291
|
+
path = href[prefix.length..-1]
|
292
|
+
service = service(path)
|
293
|
+
if object.is_a?(Array)
|
294
|
+
service.list
|
295
|
+
else
|
296
|
+
service.get
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
#
|
301
|
+
# Releases the resources used by this connection, making sure that multiple threads are coordinated correctly.
|
302
|
+
#
|
303
|
+
def close
|
304
|
+
@mutex.synchronize { internal_close }
|
305
|
+
end
|
306
|
+
|
307
|
+
#
|
308
|
+
# Checks that the content type of the given response is JSON. If it is JSON then it does nothing. If it isn't
|
309
|
+
# JSON then it raises an exception.
|
310
|
+
#
|
311
|
+
# @param response [HttpResponse] The HTTP response to check.
|
312
|
+
#
|
313
|
+
# @api private
|
314
|
+
#
|
315
|
+
def check_json_content_type(response)
|
316
|
+
check_content_type(JSON_CONTENT_TYPE_RE, 'JSON', response)
|
317
|
+
end
|
318
|
+
|
319
|
+
#
|
320
|
+
# Checks that the content type of the given response is XML. If it is XML then it does nothing. If it isn't
|
321
|
+
# XML then it raises an exception.
|
322
|
+
#
|
323
|
+
# @param response [HttpResponse] The HTTP response to check.
|
324
|
+
#
|
325
|
+
# @api private
|
326
|
+
#
|
327
|
+
def check_xml_content_type(response)
|
328
|
+
check_content_type(XML_CONTENT_TYPE_RE, 'XML', response)
|
329
|
+
end
|
330
|
+
|
331
|
+
#
|
332
|
+
# Creates and raises an error containing the details of the given HTTP response.
|
333
|
+
#
|
334
|
+
# @param response [HttpResponse] The HTTP response where the details of the raised error will be taken from.
|
335
|
+
# @param detail [String, Fault] (nil) The detail of the error. It can be a string or a `Fault` object.
|
336
|
+
#
|
337
|
+
# @api private
|
338
|
+
#
|
339
|
+
def raise_error(response, detail = nil)
|
340
|
+
# Check if the detail is a fault:
|
341
|
+
fault = detail.is_a?(Fault) ? detail : nil
|
342
|
+
|
343
|
+
# Build the error message from the response and the fault:
|
344
|
+
message = ''
|
345
|
+
unless fault.nil?
|
346
|
+
unless fault.reason.nil?
|
347
|
+
message << ' ' unless message.empty?
|
348
|
+
message << "Fault reason is \"#{fault.reason}\"."
|
349
|
+
end
|
350
|
+
unless fault.detail.nil?
|
351
|
+
message << ' ' unless message.empty?
|
352
|
+
message << "Fault detail is \"#{fault.detail}\"."
|
353
|
+
end
|
354
|
+
end
|
355
|
+
unless response.nil?
|
356
|
+
unless response.code.nil?
|
357
|
+
message << ' ' unless message.empty?
|
358
|
+
message << "HTTP response code is #{response.code}."
|
359
|
+
end
|
360
|
+
unless response.message.nil?
|
361
|
+
message << ' ' unless message.empty?
|
362
|
+
message << "HTTP response message is \"#{response.message}\"."
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
# If the detail is a string, append it to the message:
|
367
|
+
if detail.is_a?(String)
|
368
|
+
message << ' ' unless message.empty?
|
369
|
+
message << detail
|
370
|
+
message << '.'
|
371
|
+
end
|
372
|
+
|
373
|
+
# Create and populate the error:
|
374
|
+
klass = Error
|
375
|
+
unless response.nil?
|
376
|
+
case response.code
|
377
|
+
when 401, 403
|
378
|
+
klass = AuthError
|
379
|
+
when 404
|
380
|
+
klass = NotFoundError
|
381
|
+
end
|
382
|
+
end
|
383
|
+
error = klass.new(message)
|
384
|
+
error.code = response.code if response
|
385
|
+
error.fault = fault
|
386
|
+
|
387
|
+
raise error
|
388
|
+
end
|
389
|
+
|
390
|
+
#
|
391
|
+
# Returns a string representation of the connection.
|
392
|
+
#
|
393
|
+
# @return [String] The string representation.
|
394
|
+
#
|
395
|
+
def inspect
|
396
|
+
"#<#{self.class.name}:#{@url}>"
|
397
|
+
end
|
398
|
+
|
399
|
+
#
|
400
|
+
# Returns a string representation of the connection.
|
401
|
+
#
|
402
|
+
# @return [String] The string representation.
|
403
|
+
#
|
404
|
+
def to_s
|
405
|
+
inspect
|
406
|
+
end
|
407
|
+
|
408
|
+
#
|
409
|
+
# Returns a string representation of the connection.
|
410
|
+
#
|
411
|
+
# @return [String] The string representation.
|
412
|
+
#
|
413
|
+
|
414
|
+
private
|
415
|
+
|
416
|
+
#
|
417
|
+
# Regular expression used to check JSON content type.
|
418
|
+
#
|
419
|
+
# @api private
|
420
|
+
#
|
421
|
+
JSON_CONTENT_TYPE_RE = %r{^\s*(application|text)/json\s*(;.*)?$}i.freeze
|
422
|
+
|
423
|
+
#
|
424
|
+
# Regular expression used to check XML content type.
|
425
|
+
#
|
426
|
+
# @api private
|
427
|
+
#
|
428
|
+
XML_CONTENT_TYPE_RE = %r{^\s*(application|text)/xml\s*(;.*)?$}i.freeze
|
429
|
+
|
430
|
+
#
|
431
|
+
# The typical URL path, used just to generate informative error messages.
|
432
|
+
#
|
433
|
+
# @api private
|
434
|
+
#
|
435
|
+
TYPICAL_PATH = '/ovirt-engine/api'.freeze
|
436
|
+
|
437
|
+
#
|
438
|
+
# Checks the content type of the given HTTP response and raises an exception if it isn't the expected one.
|
439
|
+
#
|
440
|
+
# @param expected_re [Regex] The regular expression used to check the expected content type.
|
441
|
+
# @param expected_name [String] The name of the expected content type.
|
442
|
+
# @param response [HttpResponse] The HTTP response to check.
|
443
|
+
#
|
444
|
+
# @api private
|
445
|
+
#
|
446
|
+
def check_content_type(expected_re, expected_name, response)
|
447
|
+
content_type = response.headers['content-type']
|
448
|
+
return if expected_re =~ content_type
|
449
|
+
|
450
|
+
detail = "The response content type '#{content_type}' isn't #{expected_name}"
|
451
|
+
url = URI(@url)
|
452
|
+
if url.path != TYPICAL_PATH
|
453
|
+
detail << ". Is the path '#{url.path}' included in the 'url' parameter correct?"
|
454
|
+
detail << " The typical one is '#{TYPICAL_PATH}'"
|
455
|
+
end
|
456
|
+
raise_error(response, detail)
|
457
|
+
end
|
458
|
+
|
459
|
+
#
|
460
|
+
# Obtains the access token from SSO to be used for bearer authentication.
|
461
|
+
#
|
462
|
+
# @return [String] The access token.
|
463
|
+
#
|
464
|
+
# @api private
|
465
|
+
#
|
466
|
+
def create_access_token
|
467
|
+
# Build the URL and parameters required for the request:
|
468
|
+
url, parameters = build_sso_auth_request
|
469
|
+
|
470
|
+
# Send the request and wait for the request:
|
471
|
+
response = get_sso_response(url, parameters)
|
472
|
+
response = response[0] if response.is_a?(Array)
|
473
|
+
|
474
|
+
# Check the response and raise an error if it contains an error code:
|
475
|
+
error = get_sso_error_message(response)
|
476
|
+
raise AuthError, "Error during SSO authentication: #{error}" if error
|
477
|
+
|
478
|
+
response['access_token']
|
479
|
+
end
|
480
|
+
|
481
|
+
#
|
482
|
+
# Revoke the SSO access token.
|
483
|
+
#
|
484
|
+
# @api private
|
485
|
+
#
|
486
|
+
def revoke_access_token
|
487
|
+
# Build the URL and parameters required for the request:
|
488
|
+
url, parameters = build_sso_revoke_request
|
489
|
+
|
490
|
+
# Send the request and wait for the response:
|
491
|
+
response = get_sso_response(url, parameters)
|
492
|
+
response = response[0] if response.is_a?(Array)
|
493
|
+
|
494
|
+
# Check the response and raise an error if it contains an error code:
|
495
|
+
error = get_sso_error_message(response)
|
496
|
+
raise AuthError, "Error during SSO revoke: #{error}" if error
|
497
|
+
end
|
498
|
+
|
499
|
+
#
|
500
|
+
# Execute a get request to the SSO server and return the response.
|
501
|
+
#
|
502
|
+
# @param url [String] The URL of the SSO server.
|
503
|
+
#
|
504
|
+
# @param parameters [Hash] The parameters to send to the SSO server.
|
505
|
+
#
|
506
|
+
# @return [Hash] The JSON response.
|
507
|
+
#
|
508
|
+
# @api private
|
509
|
+
#
|
510
|
+
def get_sso_response(url, parameters)
|
511
|
+
# Create the request:
|
512
|
+
request = HttpRequest.new
|
513
|
+
request.method = :POST
|
514
|
+
request.url = url
|
515
|
+
request.headers = {
|
516
|
+
'User-Agent' => "RubySDK/#{VERSION}",
|
517
|
+
'Content-Type' => 'application/x-www-form-urlencoded',
|
518
|
+
'Accept' => 'application/json'
|
519
|
+
}
|
520
|
+
request.body = URI.encode_www_form(parameters)
|
521
|
+
|
522
|
+
# Add the global headers:
|
523
|
+
request.headers.merge!(@headers) if @headers
|
524
|
+
|
525
|
+
# Send the request and wait for the response:
|
526
|
+
@client.send(request)
|
527
|
+
response = @client.wait(request)
|
528
|
+
raise response if response.is_a?(Exception)
|
529
|
+
|
530
|
+
# Check the returned content type:
|
531
|
+
check_json_content_type(response)
|
532
|
+
|
533
|
+
# Parse and return the JSON response:
|
534
|
+
JSON.parse(response.body)
|
535
|
+
end
|
536
|
+
|
537
|
+
#
|
538
|
+
# Builds a the URL and parameters to acquire the access token from SSO.
|
539
|
+
#
|
540
|
+
# @return [Array] An array containing two elements, the first is the URL of the SSO service and the second is a hash
|
541
|
+
# containing the parameters required to perform authentication.
|
542
|
+
#
|
543
|
+
# @api private
|
544
|
+
#
|
545
|
+
def build_sso_auth_request
|
546
|
+
# Compute the entry point and the parameters:
|
547
|
+
parameters = {
|
548
|
+
scope: 'ovirt-app-api'
|
549
|
+
}
|
550
|
+
if @kerberos
|
551
|
+
entry_point = 'token-http-auth'
|
552
|
+
parameters[:grant_type] = 'urn:ovirt:params:oauth:grant-type:http'
|
553
|
+
else
|
554
|
+
entry_point = 'token'
|
555
|
+
parameters.merge!(
|
556
|
+
grant_type: 'password',
|
557
|
+
username: @username,
|
558
|
+
password: @password
|
559
|
+
)
|
560
|
+
end
|
561
|
+
|
562
|
+
# Compute the URL:
|
563
|
+
url = URI(@url.to_s)
|
564
|
+
url.path = "/ovirt-engine/sso/oauth/#{entry_point}"
|
565
|
+
url = url.to_s
|
566
|
+
|
567
|
+
# Return the pair containing the URL and the parameters:
|
568
|
+
[url, parameters]
|
569
|
+
end
|
570
|
+
|
571
|
+
#
|
572
|
+
# Builds a the URL and parameters to revoke the SSO access token
|
573
|
+
#
|
574
|
+
# @return [Array] An array containing two elements, the first is the URL of the SSO service and the second is a hash
|
575
|
+
# containing the parameters required to perform the revoke.
|
576
|
+
#
|
577
|
+
# @api private
|
578
|
+
#
|
579
|
+
def build_sso_revoke_request
|
580
|
+
# Compute the parameters:
|
581
|
+
parameters = {
|
582
|
+
scope: '',
|
583
|
+
token: @token
|
584
|
+
}
|
585
|
+
|
586
|
+
# Compute the URL:
|
587
|
+
url = URI(@url.to_s)
|
588
|
+
url.path = '/ovirt-engine/services/sso-logout'
|
589
|
+
url = url.to_s
|
590
|
+
|
591
|
+
# Return the pair containing the URL and the parameters:
|
592
|
+
[url, parameters]
|
593
|
+
end
|
594
|
+
|
595
|
+
#
|
596
|
+
# Extrats the error message from the given SSO response.
|
597
|
+
#
|
598
|
+
# @param response [Hash] The result of parsing the JSON document returned by the SSO server.
|
599
|
+
# @return [String] The error message, or `nil` if there was no error.
|
600
|
+
#
|
601
|
+
def get_sso_error_message(response)
|
602
|
+
# OAuth uses the 'error_code' attribute for the error code, and 'error' for the error description. But OpenID uses
|
603
|
+
# 'error' for the error code and 'error_description' for the description. So we need to check if the
|
604
|
+
# 'error_description' attribute is present, and extract the code and description accordingly.
|
605
|
+
description = response['error_description']
|
606
|
+
if description.nil?
|
607
|
+
code = response['error_code']
|
608
|
+
description = response['error']
|
609
|
+
else
|
610
|
+
code = response['error']
|
611
|
+
end
|
612
|
+
"#{code}: #{description}" if code
|
613
|
+
end
|
614
|
+
|
615
|
+
#
|
616
|
+
# Sends an HTTP request.
|
617
|
+
#
|
618
|
+
# @param request [HttpRequest] The request object containing the details of the HTTP request to send.
|
619
|
+
#
|
620
|
+
# @api private
|
621
|
+
#
|
622
|
+
def internal_send(request)
|
623
|
+
# Add the base URL to the request:
|
624
|
+
request.url = request.url.nil? ? request.url = @url : "#{@url}/#{request.url}"
|
625
|
+
|
626
|
+
# Set the headers common to all requests:
|
627
|
+
request.headers.merge!(
|
628
|
+
'User-Agent' => "RubySDK/#{VERSION}",
|
629
|
+
'Version' => '4',
|
630
|
+
'Content-Type' => 'application/xml',
|
631
|
+
'Accept' => 'application/xml'
|
632
|
+
)
|
633
|
+
|
634
|
+
# Older versions of the engine (before 4.1) required the 'all_content' as an HTTP header instead of a query
|
635
|
+
# parameter. In order to better support those older versions of the engine we need to check if this parameter is
|
636
|
+
# included in the request, and add the corresponding header.
|
637
|
+
unless request.query.nil?
|
638
|
+
all_content = request.query[:all_content]
|
639
|
+
request.headers['All-Content'] = all_content unless all_content.nil?
|
640
|
+
end
|
641
|
+
|
642
|
+
# Add the global headers, but without replacing the values that may already exist:
|
643
|
+
request.headers.merge!(@headers) { |_name, local, _global| local } if @headers
|
644
|
+
|
645
|
+
# Set the authentication token:
|
646
|
+
@token ||= create_access_token
|
647
|
+
request.token = @token
|
648
|
+
|
649
|
+
# Send the request:
|
650
|
+
@client.send(request)
|
651
|
+
end
|
652
|
+
|
653
|
+
#
|
654
|
+
# Waits for the response to the given request.
|
655
|
+
#
|
656
|
+
# @param request [HttpRequest] The request object whose corresponding response you want to wait for.
|
657
|
+
# @return [Response] A request object containing the details of the HTTP response received.
|
658
|
+
#
|
659
|
+
# @api private
|
660
|
+
#
|
661
|
+
def internal_wait(request)
|
662
|
+
# Wait for the response:
|
663
|
+
response = @client.wait(request)
|
664
|
+
raise response if response.is_a?(Exception)
|
665
|
+
|
666
|
+
# If the request failed because of authentication, and it wasn't a request to the SSO service, then the
|
667
|
+
# most likely cause is an expired SSO token. In this case we need to request a new token, and try the original
|
668
|
+
# request again, but only once. It if fails again, we just return the failed response.
|
669
|
+
if response.code == 401 && request.token
|
670
|
+
@token = create_access_token
|
671
|
+
request.token = @token
|
672
|
+
@client.send(request)
|
673
|
+
response = @client.wait(request)
|
674
|
+
end
|
675
|
+
|
676
|
+
response
|
677
|
+
end
|
678
|
+
|
679
|
+
#
|
680
|
+
# Releases the resources used by this connection.
|
681
|
+
#
|
682
|
+
# @api private
|
683
|
+
#
|
684
|
+
def internal_close
|
685
|
+
# Revoke the SSO access token:
|
686
|
+
revoke_access_token if @token
|
687
|
+
|
688
|
+
# Close the HTTP client:
|
689
|
+
@client.close if @client
|
690
|
+
|
691
|
+
# Remove the temporary file that contains the trusted CA certificates:
|
692
|
+
@ca_store.unlink if @ca_store
|
693
|
+
end
|
694
|
+
end
|
695
|
+
end
|