openstack 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/COPYING +7 -0
- data/README.rdoc +189 -0
- data/VERSION +1 -0
- data/lib/openstack.rb +107 -0
- data/lib/openstack/compute/address.rb +39 -0
- data/lib/openstack/compute/connection.rb +207 -0
- data/lib/openstack/compute/flavor.rb +35 -0
- data/lib/openstack/compute/image.rb +73 -0
- data/lib/openstack/compute/metadata.rb +116 -0
- data/lib/openstack/compute/personalities.rb +23 -0
- data/lib/openstack/compute/server.rb +249 -0
- data/lib/openstack/connection.rb +462 -0
- data/lib/openstack/swift/connection.rb +185 -0
- data/lib/openstack/swift/container.rb +214 -0
- data/lib/openstack/swift/storage_object.rb +311 -0
- metadata +96 -0
@@ -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
|
+
|