dropmyemail-openstack 1.0.9

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