openstack 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,462 @@
1
+ module OpenStack
2
+
3
+ class Connection
4
+
5
+ attr_reader :authuser
6
+ attr_reader :authtenant
7
+ attr_reader :authkey
8
+ attr_reader :auth_method
9
+ attr_accessor :authtoken
10
+ attr_accessor :authok
11
+ attr_accessor :service_host
12
+ attr_accessor :service_path
13
+ attr_accessor :service_port
14
+ attr_accessor :service_scheme
15
+ attr_reader :auth_host
16
+ attr_reader :auth_port
17
+ attr_reader :auth_scheme
18
+ attr_reader :auth_path
19
+ attr_reader :service_name
20
+ attr_reader :service_type
21
+ attr_reader :proxy_host
22
+ attr_reader :proxy_port
23
+ attr_reader :region
24
+
25
+ attr_reader :http
26
+ attr_reader :is_debug
27
+
28
+ # Creates and returns a new Connection object, depending on the service_type
29
+ # passed in the options:
30
+ #
31
+ # e.g:
32
+ # os = OpenStack::Connection.create({:username => "herp@derp.com", :api_key=>"password",
33
+ # :auth_url => "https://region-a.geo-1.identity.cloudsvc.com:35357/v2.0/",
34
+ # :authtenant=>"herp@derp.com-default-tenant", :service_type=>"object-store")
35
+ #
36
+ # Will return an OpenStack::Swift::Connection object.
37
+ #
38
+ # options hash:
39
+ #
40
+ # :username - Your OpenStack username *required*
41
+ # :tenant - Your OpenStack tenant *required*. Defaults to username.
42
+ # :api_key - Your OpenStack API key *required*
43
+ # :auth_url - Configurable auth_url endpoint.
44
+ # :service_name - (Optional for v2.0 auth only). The optional name of the compute service to use.
45
+ # :service_type - (Optional for v2.0 auth only). Defaults to "compute"
46
+ # :region - (Optional for v2.0 auth only). The specific service region to use. Defaults to first returned region.
47
+ # :retry_auth - Whether to retry if your auth token expires (defaults to true)
48
+ # :proxy_host - If you need to connect through a proxy, supply the hostname here
49
+ # :proxy_port - If you need to connect through a proxy, supply the port here
50
+ #
51
+ # The options hash is used to create a new OpenStack::Connection object
52
+ # (private constructor) and this is passed to the constructor of OpenStack::Compute::Connection
53
+ # or OpenStack::Swift::Connection (depending on :service_type) where authentication is done using
54
+ # OpenStack::Authentication.
55
+ #
56
+ def self.create(options = {:retry_auth => true})
57
+ #call private constructor and grab instance vars
58
+ connection = new(options)
59
+ case connection.service_type
60
+ when "compute"
61
+ OpenStack::Compute::Connection.new(connection)
62
+ when "object-store"
63
+ OpenStack::Swift::Connection.new(connection)
64
+ else
65
+ raise Exception::InvalidArgument, "Invalid :service_type parameter: #{@service_type}"
66
+ end
67
+ end
68
+
69
+ private_class_method :new
70
+
71
+ def initialize(options = {:retry_auth => true})
72
+ @authuser = options[:username] || (raise Exception::MissingArgument, "Must supply a :username")
73
+ @authkey = options[:api_key] || (raise Exception::MissingArgument, "Must supply an :api_key")
74
+ @auth_url = options[:auth_url] || (raise Exception::MissingArgument, "Must supply an :auth_url")
75
+ @authtenant = options[:authtenant] || @authuser
76
+ @auth_method = options[:auth_method] || "password"
77
+ @service_name = options[:service_name] || nil
78
+ @service_type = options[:service_type] || "compute"
79
+ @region = options[:region] || @region = nil
80
+ @is_debug = options[:is_debug]
81
+ auth_uri=nil
82
+ begin
83
+ auth_uri=URI.parse(@auth_url)
84
+ rescue Exception => e
85
+ raise Exception::InvalidArgument, "Invalid :auth_url parameter: #{e.message}"
86
+ end
87
+ raise Exception::InvalidArgument, "Invalid :auth_url parameter." if auth_uri.nil? or auth_uri.host.nil?
88
+ @auth_host = auth_uri.host
89
+ @auth_port = auth_uri.port
90
+ @auth_scheme = auth_uri.scheme
91
+ @auth_path = auth_uri.path
92
+ @retry_auth = options[:retry_auth]
93
+ @proxy_host = options[:proxy_host]
94
+ @proxy_port = options[:proxy_port]
95
+ @authok = false
96
+ @http = {}
97
+ end
98
+
99
+ #specialised from of csreq for PUT object... uses body_stream if possible
100
+ def put_object(server,path,port,scheme,headers = {},data = nil,attempts = 0) # :nodoc:
101
+ start = Time.now
102
+ if data.respond_to? :read
103
+ headers['Transfer-Encoding'] = 'chunked'
104
+ hdrhash = headerprep(headers)
105
+ request = Net::HTTP::Put.new(path,hdrhash)
106
+ chunked = OpenStack::Swift::ChunkedConnectionWrapper.new(data, 65535)
107
+ request.body_stream = chunked
108
+ else
109
+ headers['Content-Length'] = (body.respond_to?(:lstat))? body.lstat.size.to_s : ((body.respond_to?(:size))? body.size.to_s : "0")
110
+ hdrhash = headerprep(headers)
111
+ request = Net::HTTP::Put.new(path,hdrhash)
112
+ request.body = data
113
+ end
114
+ start_http(server,path,port,scheme,hdrhash)
115
+ response = @http[server].request(request)
116
+ if @is_debug
117
+ puts "REQUEST: #{method} => #{path}"
118
+ puts data if data
119
+ puts "RESPONSE: #{response.body}"
120
+ puts '----------------------------------------'
121
+ end
122
+ raise OpenStack::Exception::ExpiredAuthToken if response.code == "401"
123
+ response
124
+ rescue Errno::EPIPE, Timeout::Error, Errno::EINVAL, EOFError
125
+ # Server closed the connection, retry
126
+ raise OpenStack::Exception::Connection, "Unable to reconnect to #{server} after #{attempts} attempts" if attempts >= 5
127
+ attempts += 1
128
+ @http[server].finish if @http[server].started?
129
+ start_http(server,path,port,scheme,headers)
130
+ retry
131
+ rescue OpenStack::Exception::ExpiredAuthToken
132
+ raise OpenStack::Exception::Connection, "Authentication token expired and you have requested not to retry" if @retry_auth == false
133
+ OpenStack::Authentication.init(self)
134
+ retry
135
+ end
136
+
137
+
138
+ # This method actually makes the HTTP REST calls out to the server
139
+ def csreq(method,server,path,port,scheme,headers = {},data = nil,attempts = 0, &block) # :nodoc:
140
+ start = Time.now
141
+ hdrhash = headerprep(headers)
142
+ start_http(server,path,port,scheme,hdrhash)
143
+ request = Net::HTTP.const_get(method.to_s.capitalize).new(path,hdrhash)
144
+ request.body = data
145
+ if block_given?
146
+ response = @http[server].request(request) do |res|
147
+ res.read_body do |b|
148
+ yield b
149
+ end
150
+ end
151
+ else
152
+ response = @http[server].request(request)
153
+ end
154
+ if @is_debug
155
+ puts "REQUEST: #{method} => #{path}"
156
+ puts data if data
157
+ puts "RESPONSE: #{response.body}"
158
+ puts '----------------------------------------'
159
+ end
160
+ raise OpenStack::Exception::ExpiredAuthToken if response.code == "401"
161
+ response
162
+ rescue Errno::EPIPE, Timeout::Error, Errno::EINVAL, EOFError
163
+ # Server closed the connection, retry
164
+ raise OpenStack::Exception::Connection, "Unable to reconnect to #{server} after #{attempts} attempts" if attempts >= 5
165
+ attempts += 1
166
+ @http[server].finish if @http[server].started?
167
+ start_http(server,path,port,scheme,headers)
168
+ retry
169
+ rescue OpenStack::Exception::ExpiredAuthToken
170
+ raise OpenStack::Exception::Connection, "Authentication token expired and you have requested not to retry" if @retry_auth == false
171
+ OpenStack::Authentication.init(self)
172
+ retry
173
+ end
174
+
175
+ # This is a much more sane way to make a http request to the api.
176
+ # Example: res = conn.req('GET', "/servers/#{id}")
177
+ def req(method, path, options = {})
178
+ server = options[:server] || @service_host
179
+ port = options[:port] || @service_port
180
+ scheme = options[:scheme] || @service_scheme
181
+ headers = options[:headers] || {'content-type' => 'application/json'}
182
+ data = options[:data]
183
+ attempts = options[:attempts] || 0
184
+ path = @service_path + path
185
+ res = csreq(method,server,path,port,scheme,headers,data,attempts)
186
+ if not res.code.match(/^20.$/)
187
+ OpenStack::Exception.raise_exception(res)
188
+ end
189
+ return res
190
+ end;
191
+
192
+ private
193
+
194
+ # Sets up standard HTTP headers
195
+ def headerprep(headers = {}) # :nodoc:
196
+ default_headers = {}
197
+ default_headers["X-Auth-Token"] = @authtoken if authok
198
+ default_headers["X-Storage-Token"] = @authtoken if authok
199
+ default_headers["Connection"] = "Keep-Alive"
200
+ default_headers["User-Agent"] = "OpenStack Ruby API #{VERSION}"
201
+ default_headers["Accept"] = "application/json"
202
+ default_headers.merge(headers)
203
+ end
204
+
205
+ # Starts (or restarts) the HTTP connection
206
+ def start_http(server,path,port,scheme,headers) # :nodoc:
207
+ if (@http[server].nil?)
208
+ begin
209
+ @http[server] = Net::HTTP::Proxy(@proxy_host, @proxy_port).new(server,port)
210
+ if scheme == "https"
211
+ @http[server].use_ssl = true
212
+ @http[server].verify_mode = OpenSSL::SSL::VERIFY_NONE
213
+ end
214
+ @http[server].start
215
+ rescue
216
+ raise OpenStack::Exception::Connection, "Unable to connect to #{server}"
217
+ end
218
+ end
219
+ end
220
+
221
+ end #end class Connection
222
+
223
+ #============================
224
+ # OpenStack::Authentication
225
+ #============================
226
+
227
+ class Authentication
228
+
229
+ # Performs an authentication to the OpenStack auth server.
230
+ # If it succeeds, it sets the service_host, service_path, service_port,
231
+ # service_scheme, authtoken, and authok variables on the connection.
232
+ # If it fails, it raises an exception.
233
+
234
+ def self.init(conn)
235
+ if conn.auth_path =~ /.*v2.0\/?$/
236
+ AuthV20.new(conn)
237
+ else
238
+ AuthV10.new(conn)
239
+ end
240
+ end
241
+
242
+ end
243
+
244
+ private
245
+
246
+ class AuthV20
247
+ attr_reader :uri
248
+
249
+ def initialize(connection)
250
+ begin
251
+ server = Net::HTTP::Proxy(connection.proxy_host, connection.proxy_port).new(connection.auth_host, connection.auth_port)
252
+ if connection.auth_scheme == "https"
253
+ server.use_ssl = true
254
+ server.verify_mode = OpenSSL::SSL::VERIFY_NONE
255
+ end
256
+ server.start
257
+ rescue
258
+ raise OpenStack::Exception::Connection, "Unable to connect to #{server}"
259
+ end
260
+
261
+ @uri = String.new
262
+
263
+ if connection.auth_method == "password"
264
+ auth_data = JSON.generate({ "auth" => { "passwordCredentials" => { "username" => connection.authuser, "password" => connection.authkey }, "tenantName" => connection.authtenant}})
265
+ elsif connection.auth_method == "rax-kskey"
266
+ auth_data = JSON.generate({"auth" => {"RAX-KSKEY:apiKeyCredentials" => {"username" => connection.authuser, "apiKey" => connection.authkey}}})
267
+ else
268
+ raise Exception::InvalidArgument, "Unrecognized auth method #{connection.auth_method}"
269
+ end
270
+
271
+ response = server.post(connection.auth_path.chomp("/")+"/tokens", auth_data, {'Content-Type' => 'application/json'})
272
+ if (response.code =~ /^20./)
273
+ resp_data=JSON.parse(response.body)
274
+ connection.authtoken = resp_data['access']['token']['id']
275
+ resp_data['access']['serviceCatalog'].each do |service|
276
+ if service['type'] == connection.service_type
277
+ endpoints = service["endpoints"]
278
+ if connection.region
279
+ endpoints.each do |ep|
280
+ if ep["region"] and ep["region"].upcase == connection.region.upcase
281
+ @uri = URI.parse(ep["publicURL"])
282
+ end
283
+ end
284
+ else
285
+ @uri = URI.parse(endpoints[0]["publicURL"])
286
+ end
287
+ if @uri == ""
288
+ raise OpenStack::Exception::Authentication, "No API endpoint for region #{connection.region}"
289
+ else
290
+ connection.service_host = @uri.host
291
+ connection.service_path = @uri.path
292
+ connection.service_port = @uri.port
293
+ connection.service_scheme = @uri.scheme
294
+ connection.authok = true
295
+ end
296
+ end
297
+ end
298
+ else
299
+ connection.authtoken = false
300
+ raise OpenStack::Exception::Authentication, "Authentication failed with response code #{response.code}"
301
+ end
302
+ server.finish
303
+ end
304
+ end
305
+
306
+ class AuthV10
307
+
308
+ def initialize(connection)
309
+ hdrhash = { "X-Auth-User" => connection.authuser, "X-Auth-Key" => connection.authkey }
310
+ begin
311
+ server = Net::HTTP::Proxy(connection.proxy_host, connection.proxy_port).new(connection.auth_host, connection.auth_port)
312
+ if connection.auth_scheme == "https"
313
+ server.use_ssl = true
314
+ server.verify_mode = OpenSSL::SSL::VERIFY_NONE
315
+ end
316
+ server.start
317
+ rescue
318
+ raise OpenStack::Exception::Connection, "Unable to connect to #{server}"
319
+ end
320
+ response = server.get(connection.auth_path, hdrhash)
321
+ if (response.code =~ /^20./)
322
+ connection.authtoken = response["x-auth-token"]
323
+ case connection.service_type
324
+ when "compute"
325
+ uri = URI.parse(response["x-server-management-url"])
326
+ when "object-store"
327
+ uri = URI.parse(response["x-storage-url"])
328
+ end
329
+ raise OpenStack::Exception::Authentication, "Unexpected Response from #{connection.auth_host} - couldn't get service URLs: \"x-server-management-url\" is: #{response["x-server-management-url"]} and \"x-storage-url\" is: #{response["x-storage-url"]}" if (uri.host.nil? || uri.host=="")
330
+ connection.service_host = uri.host
331
+ connection.service_path = uri.path
332
+ connection.service_port = uri.port
333
+ connection.service_scheme = uri.scheme
334
+ connection.authok = true
335
+ else
336
+ connection.authok = false
337
+ raise OpenStack::Exception::Authentication, "Authentication failed with response code #{response.code}"
338
+ end
339
+ server.finish
340
+ end
341
+
342
+ end
343
+
344
+
345
+ #============================
346
+ # OpenStack::Exception
347
+ #============================
348
+
349
+ class Exception
350
+
351
+ class ComputeError < StandardError
352
+
353
+ attr_reader :response_body
354
+ attr_reader :response_code
355
+
356
+ def initialize(message, code, response_body)
357
+ @response_code=code
358
+ @response_body=response_body
359
+ super(message)
360
+ end
361
+
362
+ end
363
+
364
+ class ComputeFault < ComputeError # :nodoc:
365
+ end
366
+ class ServiceUnavailable < ComputeError # :nodoc:
367
+ end
368
+ class Unauthorized < ComputeError # :nodoc:
369
+ end
370
+ class BadRequest < ComputeError # :nodoc:
371
+ end
372
+ class OverLimit < ComputeError # :nodoc:
373
+ end
374
+ class BadMediaType < ComputeError # :nodoc:
375
+ end
376
+ class BadMethod < ComputeError # :nodoc:
377
+ end
378
+ class ItemNotFound < ComputeError # :nodoc:
379
+ end
380
+ class BuildInProgress < ComputeError # :nodoc:
381
+ end
382
+ class ServerCapacityUnavailable < ComputeError # :nodoc:
383
+ end
384
+ class BackupOrResizeInProgress < ComputeError # :nodoc:
385
+ end
386
+ class ResizeNotAllowed < ComputeError # :nodoc:
387
+ end
388
+ class NotImplemented < ComputeError # :nodoc:
389
+ end
390
+ class Other < ComputeError # :nodoc:
391
+ end
392
+ class ResourceStateConflict < ComputeError # :nodoc:
393
+ end
394
+
395
+ # Plus some others that we define here
396
+
397
+ class ExpiredAuthToken < StandardError # :nodoc:
398
+ end
399
+ class MissingArgument < StandardError # :nodoc:
400
+ end
401
+ class InvalidArgument < StandardError # :nodoc:
402
+ end
403
+ class TooManyPersonalityItems < StandardError # :nodoc:
404
+ end
405
+ class PersonalityFilePathTooLong < StandardError # :nodoc:
406
+ end
407
+ class PersonalityFileTooLarge < StandardError # :nodoc:
408
+ end
409
+ class Authentication < StandardError # :nodoc:
410
+ end
411
+ class Connection < StandardError # :nodoc:
412
+ end
413
+
414
+ # In the event of a non-200 HTTP status code, this method takes the HTTP response, parses
415
+ # the JSON from the body to get more information about the exception, then raises the
416
+ # proper error. Note that all exceptions are scoped in the OpenStack::Compute::Exception namespace.
417
+ def self.raise_exception(response)
418
+ return if response.code =~ /^20.$/
419
+ begin
420
+ fault = nil
421
+ info = nil
422
+ if response.body.nil? && response.code == "404" #HEAD ops no body returned
423
+ exception_class = self.const_get("ItemNotFound")
424
+ raise exception_class.new("The resource could not be found", "404", "")
425
+ else
426
+ JSON.parse(response.body).each_pair do |key, val|
427
+ fault=key
428
+ info=val
429
+ end
430
+ exception_class = self.const_get(fault[0,1].capitalize+fault[1,fault.length])
431
+ raise exception_class.new(info["message"], response.code, response.body)
432
+ end
433
+ rescue JSON::ParserError => parse_error
434
+ deal_with_faulty_error(response, parse_error)
435
+ rescue NameError
436
+ raise OpenStack::Exception::Other.new("The server returned status #{response.code}", response.code, response.body)
437
+ end
438
+ end
439
+
440
+ private
441
+
442
+ #e.g. os.delete("non-existant") ==> response.body is:
443
+ # "404 Not Found\n\nThe resource could not be found.\n\n "
444
+ # which doesn't parse. Deal with such cases here if possible (JSON::ParserError)
445
+ def self.deal_with_faulty_error(response, parse_error)
446
+ case response.code
447
+ when "404"
448
+ klass = self.const_get("ItemNotFound")
449
+ msg = "The resource could not be found"
450
+ when "409"
451
+ klass = self.const_get("ResourceStateConflict")
452
+ msg = "There was a conflict with the state of the resource"
453
+ else
454
+ klass = self.const_get("Other")
455
+ msg = "Oops - not sure what happened: #{parse_error}"
456
+ end
457
+ raise klass.new(msg, response.code.to_s, response.body)
458
+ end
459
+ end
460
+
461
+ end
462
+