ovirt-engine-sdk 4.0.1 → 4.4.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|