openstack 1.0.0

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.
@@ -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
+