raca 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 96ffbbfd74e51b99894091ce939cb3521766e52c
4
- data.tar.gz: 0551b2e01de644084a4099a984a25c90e6add01c
3
+ metadata.gz: 0f887cfa6d624450eb2805122c998cc29acc0b01
4
+ data.tar.gz: 89109fe3d3b728239a0ab9795d24508b96bff2ae
5
5
  SHA512:
6
- metadata.gz: 9b5cf1bc1d8c2a93d6c17bef62e9fd9e6bdd35df9dd56915309ccc771cb48659703561ea3d1d2e45b46d701c11a48d12f2662c22b3f4f248370076aedf556720
7
- data.tar.gz: 90ebbc528ed8b7a8782ea9a7dd30a3c4a30d2799ea6267995a114fe68a7a6e687c34ab0d2ecbc24b61df4be33a1c05716f4eb899646c95a5a8748eefaf7c2da6
6
+ metadata.gz: 65b7f065c88ec68907b6580fc65b1ed05187988807a450bfee8dff001d2bb0c2ac552b973bdc188567edd0bbcc1bb12ef5ca2de9e732dd9713df64220d8f8c80
7
+ data.tar.gz: ce61916a750f740b82b569be8913eaa806dffab65e3ad34013d49702ceaca9aa63ab7a01333a277adf51dc674427984c2fb534fc8f6cde915438ad1ce5007df8
data/README.markdown CHANGED
@@ -113,9 +113,34 @@ is the temp URL key that can be set using Raca::Containers#set_temp_url_key
113
113
 
114
114
  ### Cloud Servers
115
115
 
116
- account = Raca::Account.new("username", "api_key")
117
- server = account.servers(:ord).get("foo")
118
- puts server.public_addresses
116
+ Using an existing Raca::Account object, retrieve a collection of Cloud Servers
117
+ from a region like so:
118
+
119
+ ord_servers = account.servers(:ord)
120
+
121
+ You can retrieve a existing server from the collection:
122
+
123
+ a_server = ord_servers.get("server_name")
124
+
125
+ Retrieve some details on the server:
126
+
127
+ put a_server.metadata
128
+
129
+ You can use the collection to create a brand new server:
130
+
131
+ a_server = ord_servers.create("server_name", "1Gb", "Ubuntu 10.04 LTS")
132
+
133
+ ## General API principles
134
+
135
+ Methods that make calls to an API should never return a raw HTTP response
136
+ object. If a sensible return value is expected (retrieving metadata, listing
137
+ matches, etc) then that should always be returned. If return value isn't obvious
138
+ (change remote state, deleting an object, etc) then a simple boolean or similar
139
+ should be returned to indicate success.
140
+
141
+ If an unexpected error occurs (a network timeout, a 500, etc) then an exception
142
+ should be raised.
143
+
119
144
 
120
145
  ## Why not fog?
121
146
 
data/Rakefile CHANGED
@@ -1,9 +1,22 @@
1
1
  require 'rspec/core/rake_task'
2
+ require "cane/rake_task"
2
3
 
3
- task default: :spec
4
+ task default: [:cane, :spec]
4
5
 
5
6
  desc "Run all rspec files"
6
7
  RSpec::Core::RakeTask.new("spec") do |t|
7
8
  t.rspec_opts = ["--color", "--format progress"]
8
9
  t.ruby_opts = "-w"
9
10
  end
11
+
12
+ desc "Run cane to check quality metrics"
13
+ Cane::RakeTask.new(:cane) do |cane|
14
+ # keep the ABC complexity of methods to something reasonable
15
+ cane.abc_max = 15
16
+
17
+ # keep line lengths to something that fit into a reasonable split terminal
18
+ cane.style_measure = 148
19
+
20
+ # 0 is the goal
21
+ cane.max_violations = 3
22
+ end
data/lib/raca/account.rb CHANGED
@@ -3,12 +3,9 @@ require 'json'
3
3
 
4
4
  module Raca
5
5
 
6
- # The rackspace auth API accepts a username and API key and returns a range
7
- # of settings that are used for interacting with their other APIS. Think
8
- # auth tokens, hostnames, paths, etc.
9
- #
10
- # This class caches these settings so we don't have to continually use our
11
- # username/key to retrieve them.
6
+ # This is your entrypoint to the rackspace API. Start by creating a
7
+ # Raca::Account object and then use the instance method to access each of
8
+ # the supported rackspace APIs.
12
9
  #
13
10
  class Account
14
11
 
@@ -21,10 +18,25 @@ module Raca
21
18
  end
22
19
  end
23
20
 
21
+ # Return the temporary token that should be used when making further API
22
+ # requests.
23
+ #
24
+ # account = Raca::Account.new("username", "secret")
25
+ # puts account.auth_token
26
+ #
24
27
  def auth_token
25
- extract_value(cloudfiles_data, "access", "token", "id")
28
+ extract_value(identity_data, "access", "token", "id")
26
29
  end
27
30
 
31
+ # Return the public API URL for a particular rackspace service.
32
+ #
33
+ # Use Account#service_names to see a list of valid service_name's for this.
34
+ #
35
+ # Check the project README for an updated list of the available regions.
36
+ #
37
+ # account = Raca::Account.new("username", "secret")
38
+ # puts account.public_endpoint("cloudServers", :syd)
39
+ #
28
40
  def public_endpoint(service_name, region)
29
41
  region = region.to_s.upcase
30
42
  endpoints = service_endpoints(service_name)
@@ -32,14 +44,45 @@ module Raca
32
44
  regional_endpoint["publicURL"]
33
45
  end
34
46
 
47
+ # Return the names of the available services. As rackspace add new services and
48
+ # APIs they should appear here.
49
+ #
50
+ # Any name returned from here can be passe to #public_endpoint to get the API
51
+ # endpoint for that service
52
+ #
53
+ # account = Raca::Account.new("username", "secret")
54
+ # puts account.service_names
55
+ #
56
+ def service_names
57
+ catalog = extract_value(identity_data, "access", "serviceCatalog") || {}
58
+ catalog.map { |service|
59
+ service["name"]
60
+ }
61
+ end
62
+
63
+ # Return a Raca::Containers object for a region. Use this to interact with the
64
+ # cloud files service.
65
+ #
66
+ # account = Raca::Account.new("username", "secret")
67
+ # puts account.containers(:ord)
68
+ #
35
69
  def containers(region)
36
70
  Raca::Containers.new(self, region)
37
71
  end
38
72
 
73
+ # Return a Raca::Containers object for a region. Use this to interact with the
74
+ # next gen cloud servers service.
75
+ #
76
+ # account = Raca::Account.new("username", "secret")
77
+ # puts account.servers(:ord)
78
+ #
39
79
  def servers(region)
40
80
  Raca::Servers.new(self, region)
41
81
  end
42
82
 
83
+ # Raca classes use this method to occasionally re-authenticate with the rackspace
84
+ # servers. You can probable ignore it.
85
+ #
43
86
  def refresh_cache
44
87
  Net::HTTP.new('identity.api.rackspacecloud.com', 443).tap {|http|
45
88
  http.use_ssl = true
@@ -57,14 +100,27 @@ module Raca
57
100
  JSON.dump(payload),
58
101
  {'Content-Type' => 'application/json'},
59
102
  )
60
- if response.is_a? Net::HTTPSuccess
103
+ if response.is_a?(Net::HTTPSuccess)
61
104
  cache_write(cache_key, JSON.load(response.body))
105
+ else
106
+ raise_on_error(response)
62
107
  end
63
108
  }
64
109
  end
65
110
 
66
111
  private
67
112
 
113
+ def raise_on_error(response)
114
+ error_klass = case response.code.to_i
115
+ when 400 then BadRequestError
116
+ when 404 then NotFoundError
117
+ when 500 then ServerError
118
+ else
119
+ HTTPError
120
+ end
121
+ raise error_klass, "Rackspace returned HTTP status #{response.code}"
122
+ end
123
+
68
124
  # This method is opaque, but it was the best I could come up with using just
69
125
  # the standard library. Sorry.
70
126
  #
@@ -94,7 +150,7 @@ module Raca
94
150
  # cloud servers, dns, etc)
95
151
  #
96
152
  def service_endpoints(service_name)
97
- catalog = extract_value(cloudfiles_data, "access", "serviceCatalog") || {}
153
+ catalog = extract_value(identity_data, "access", "serviceCatalog") || {}
98
154
  service = catalog.detect { |s| s["name"] == service_name } || {}
99
155
  service["endpoints"] || []
100
156
  end
@@ -115,7 +171,7 @@ module Raca
115
171
  end
116
172
  end
117
173
 
118
- def cloudfiles_data
174
+ def identity_data
119
175
  refresh_cache unless cache_read(cache_key)
120
176
 
121
177
  cache_read(cache_key) || {}
@@ -4,13 +4,18 @@ require 'openssl'
4
4
  require 'uri'
5
5
 
6
6
  module Raca
7
- # Handy abstraction for interacting with a single Cloud Files container. We
8
- # could use fog or similar, but this ~200 line class is simple and does
9
- # everything we need.
7
+
8
+ # Represents a single cloud files container. Contains methods for uploading,
9
+ # downloading, collecting stats, listing files, etc.
10
+ #
11
+ # You probably don't want to instantiate this directly,
12
+ # see Raca::Account#containers
13
+ #
10
14
  class Container
11
15
  MAX_ITEMS_PER_LIST = 10_000
12
16
  LARGE_FILE_THRESHOLD = 5_368_709_120 # 5 Gb
13
17
  LARGE_FILE_SEGMENT_SIZE = 104_857_600 # 100 Mb
18
+ RETRY_PAUSE = 5
14
19
 
15
20
  attr_reader :container_name
16
21
 
@@ -24,6 +29,7 @@ module Raca
24
29
  end
25
30
 
26
31
  # Upload data_or_path (which may be a filename or an IO) to the container, as key.
32
+ #
27
33
  def upload(key, data_or_path)
28
34
  case data_or_path
29
35
  when StringIO, File
@@ -39,9 +45,11 @@ module Raca
39
45
 
40
46
  # Delete +key+ from the container. If the container is on the CDN, the object will
41
47
  # still be served from the CDN until the TTL expires.
48
+ #
42
49
  def delete(key)
43
50
  log "deleting #{key} from #{container_path}"
44
- storage_request(Net::HTTP::Delete.new(File.join(container_path, key)))
51
+ response = storage_request(Net::HTTP::Delete.new(File.join(container_path, key)))
52
+ (200..299).cover?(response.code.to_i)
45
53
  end
46
54
 
47
55
  # Remove +key+ from the CDN edge nodes on which it is currently cached. The object is
@@ -50,12 +58,14 @@ module Raca
50
58
  #
51
59
  # This shouldn't be used except when it's really required (e.g. when a piece has to be
52
60
  # taken down) because it's expensive: it lodges a support ticket at Akamai. (!)
61
+ #
53
62
  def purge_from_akamai(key, email_address)
54
63
  log "Requesting #{File.join(container_path, key)} to be purged from the CDN"
55
- cdn_request(Net::HTTP::Delete.new(
64
+ response = cdn_request(Net::HTTP::Delete.new(
56
65
  File.join(container_path, key),
57
66
  'X-Purge-Email' => email_address
58
67
  ))
68
+ (200..299).cover?(response.code.to_i)
59
69
  end
60
70
 
61
71
  # Returns some metadata about a single object in this container.
@@ -71,15 +81,20 @@ module Raca
71
81
  }
72
82
  end
73
83
 
84
+ # Download the object at key into a local file at filepath.
85
+ #
86
+ # Returns the number of downloaded bytes.
87
+ #
74
88
  def download(key, filepath)
75
89
  log "downloading #{key} from #{container_path}"
76
- storage_request(Net::HTTP::Get.new(File.join(container_path, key))) do |response|
90
+ response = storage_request(Net::HTTP::Get.new(File.join(container_path, key))) do |response|
77
91
  File.open(filepath, 'wb') do |io|
78
92
  response.read_body do |chunk|
79
93
  io.write(chunk)
80
94
  end
81
95
  end
82
96
  end
97
+ response["Content-Length"].to_i
83
98
  end
84
99
 
85
100
  # Return an array of files in the container.
@@ -89,6 +104,7 @@ module Raca
89
104
  # max - the maximum number of items to return
90
105
  # marker - return items alphabetically after this key. Useful for pagination
91
106
  # prefix - only return items that start with this string
107
+ #
92
108
  def list(options = {})
93
109
  max = options.fetch(:max, MAX_ITEMS_PER_LIST)
94
110
  marker = options.fetch(:marker, nil)
@@ -112,11 +128,18 @@ module Raca
112
128
  }
113
129
  end
114
130
 
131
+ # Returns an array of object keys that start with prefix. This is a convenience
132
+ # method that is equivilant to:
133
+ #
134
+ # container.list(prefix: "foo/bar/")
135
+ #
115
136
  def search(prefix)
116
137
  log "retrieving container listing from #{container_path} items starting with #{prefix}"
117
138
  list(prefix: prefix)
118
139
  end
119
140
 
141
+ # Return some basic stats on the current container.
142
+ #
120
143
  def metadata
121
144
  log "retrieving container metadata from #{container_path}"
122
145
  response = storage_request(Net::HTTP::Head.new(container_path))
@@ -126,6 +149,9 @@ module Raca
126
149
  }
127
150
  end
128
151
 
152
+ # Return the key details for CDN access to this container. Can be called
153
+ # on non CDN enabled containers, but the details won't make much sense.
154
+ #
129
155
  def cdn_metadata
130
156
  log "retrieving container CDN metadata from #{container_path}"
131
157
  response = cdn_request(Net::HTTP::Head.new(container_path))
@@ -143,10 +169,13 @@ module Raca
143
169
  # via the CDN. CDN enabling can be done via the web UI but only with a TTL of 72 hours.
144
170
  # Using the API it's possible to set a TTL of 50 years.
145
171
  #
146
- def cdn_enable(ttl = 72.hours.to_i)
172
+ # TTL is defined in seconds, default is 72 hours.
173
+ #
174
+ def cdn_enable(ttl = 259200)
147
175
  log "enabling CDN access to #{container_path} with a cache expiry of #{ttl / 60} minutes"
148
176
 
149
- cdn_request Net::HTTP::Put.new(container_path, "X-TTL" => ttl.to_i.to_s)
177
+ response = cdn_request(Net::HTTP::Put.new(container_path, "X-TTL" => ttl.to_i.to_s))
178
+ (200..299).cover?(response.code.to_i)
150
179
  end
151
180
 
152
181
  # Generate a expiring URL for a file that is otherwise private. useful for providing temporary
@@ -184,8 +213,8 @@ module Raca
184
213
  if io.respond_to?(:path)
185
214
  headers['Content-Type'] ||= extension_content_type(io.path)
186
215
  headers['Content-Type'] ||= file_content_type(io.path)
187
- headers['Etag'] = md5(io.path)
188
216
  end
217
+ headers['Etag'] = md5_io(io)
189
218
  headers['Content-Type'] ||= "application/octet-stream"
190
219
  if content_type_needs_cors(key)
191
220
  headers['Access-Control-Allow-Origin'] = "*"
@@ -196,7 +225,8 @@ module Raca
196
225
  request = Net::HTTP::Put.new(full_path, headers)
197
226
  request.body_stream = io
198
227
  request.content_length = byte_count
199
- storage_request(request)
228
+ response = storage_request(request)
229
+ response['ETag']
200
230
  end
201
231
 
202
232
  def upload_io_large(key, io, byte_count)
@@ -207,8 +237,8 @@ module Raca
207
237
  segment_key = "%s.%03d" % [key, segments.size]
208
238
  io.seek(start_pos)
209
239
  segment_io = StringIO.new(io.read(LARGE_FILE_SEGMENT_SIZE))
210
- result = upload_io_standard(segment_key, segment_io, segment_io.size)
211
- segments << {path: "#{@container_name}/#{segment_key}", etag: result["ETag"], size_bytes: segment_io.size}
240
+ etag = upload_io_standard(segment_key, segment_io, segment_io.size)
241
+ segments << {path: "#{@container_name}/#{segment_key}", etag: etag, size_bytes: segment_io.size}
212
242
  end
213
243
  manifest_key = "#{key}?multipart-manifest=put"
214
244
  manifest_body = StringIO.new(JSON.dump(segments))
@@ -230,14 +260,12 @@ module Raca
230
260
  end
231
261
  rescue Timeout::Error
232
262
  if retries >= 3
233
- raise "Timeout from Rackspace at #{Time.now} while trying #{request.class} to #{request.path}"
263
+ raise Raca::TimeoutError, "Timeout from Rackspace while trying #{request.class} to #{request.path}"
234
264
  end
235
265
 
236
- unless defined?(Rails) && Rails.env.test?
237
- retry_interval = 5 + (retries.to_i * 5) # Retry after 5, 10, 15 and 20 seconds
238
- log "Rackspace timed out: retrying after #{retry_interval}s"
239
- sleep(retry_interval)
240
- end
266
+ retry_interval = RETRY_PAUSE + (retries.to_i * RETRY_PAUSE) # Retry after 5, 10, 15 and 20 seconds
267
+ log "Rackspace timed out: retrying after #{retry_interval}s"
268
+ sleep(retry_interval)
241
269
 
242
270
  cloud_request(request, hostname, retries + 1, &block)
243
271
  end
@@ -253,11 +281,25 @@ module Raca
253
281
  @account.refresh_cache
254
282
  response = block.call http
255
283
  end
256
- raise "Failure: Rackspace returned #{response.inspect}" unless response.is_a?(Net::HTTPSuccess)
257
- response
284
+ if response.is_a?(Net::HTTPSuccess)
285
+ response
286
+ else
287
+ raise_on_error(response)
288
+ end
258
289
  end
259
290
  end
260
291
 
292
+ def raise_on_error(response)
293
+ error_klass = case response.code.to_i
294
+ when 400 then BadRequestError
295
+ when 404 then NotFoundError
296
+ when 500 then ServerError
297
+ else
298
+ HTTPError
299
+ end
300
+ raise error_klass, "Rackspace returned HTTP status #{response.code}"
301
+ end
302
+
261
303
  def log(msg)
262
304
  if @logger.respond_to?(:debug)
263
305
  @logger.debug msg
@@ -308,14 +350,14 @@ module Raca
308
350
  [".eot",".ttf",".woff"].include?(File.extname(path))
309
351
  end
310
352
 
311
- def md5(path)
353
+ def md5_io(io)
354
+ io.seek(0)
312
355
  digest = Digest::MD5.new
313
- File.open(path, 'rb') do |f|
314
- # read in 128K chunks
315
- f.each(1024 * 128) do |chunk|
316
- digest << chunk
317
- end
356
+ # read in 128K chunks
357
+ io.each(1024 * 128) do |chunk|
358
+ digest << chunk
318
359
  end
360
+ io.seek(0)
319
361
  digest.hexdigest
320
362
  end
321
363
  end
@@ -1,4 +1,12 @@
1
1
  module Raca
2
+ # Represents a collection of cloud files containers within a single region.
3
+ #
4
+ # There's a handful of methods that relate to the entire collection, but this
5
+ # is primarily used to retrieve a single Raca::Container object.
6
+ #
7
+ # You probably don't want to instantiate this directly,
8
+ # see Raca::Account#containers
9
+ #
2
10
  class Containers
3
11
  def initialize(account, region, opts = {})
4
12
  @account, @region = account, region
@@ -23,14 +31,19 @@ module Raca
23
31
  }
24
32
  end
25
33
 
26
- # Set the secret key that will be used to generate expiring URLs for all cloud files containers on the current account. This value should be passed to the expiring_url() method.
34
+ # Set the secret key that will be used to generate expiring URLs for all cloud
35
+ # files containers on the current account. This value should be passed to the
36
+ # expiring_url() method.
27
37
  #
28
- # Use this with caution, this will invalidate all previously generated expiring URLS *FOR THE ENTIRE ACCOUNT*
38
+ # Use this with caution, this will invalidate all previously generated expiring
39
+ # URLS *FOR THE ENTIRE ACCOUNT*
29
40
  #
30
41
  def set_temp_url_key(secret)
31
42
  log "setting Account Temp URL Key on #{storage_path}"
32
43
 
33
- storage_request Net::HTTP::Post.new(storage_path, "X-Account-Meta-Temp-Url-Key" => secret.to_s)
44
+ request = Net::HTTP::Post.new(storage_path, "X-Account-Meta-Temp-Url-Key" => secret.to_s)
45
+ response = storage_request(request)
46
+ (200..299).cover?(response.code.to_i)
34
47
  end
35
48
 
36
49
  private
@@ -65,11 +78,26 @@ module Raca
65
78
  @account.refresh_cache
66
79
  response = block.call http
67
80
  end
68
- raise "Failure: Rackspace returned #{response.inspect}" unless response.is_a?(Net::HTTPSuccess)
81
+ if response.is_a?(Net::HTTPSuccess)
82
+ response
83
+ else
84
+ raise_on_error(response)
85
+ end
69
86
  response
70
87
  end
71
88
  end
72
89
 
90
+ def raise_on_error(response)
91
+ error_klass = case response.code.to_i
92
+ when 400 then BadRequestError
93
+ when 404 then NotFoundError
94
+ when 500 then ServerError
95
+ else
96
+ HTTPError
97
+ end
98
+ raise error_klass, "Rackspace returned HTTP status #{response.code}"
99
+ end
100
+
73
101
  def log(msg)
74
102
  if @logger.respond_to?(:debug)
75
103
  @logger.debug msg
@@ -0,0 +1,17 @@
1
+ module Raca
2
+ # base error for unexpected HTTP responses from rackspace
3
+ class HTTPError < RuntimeError; end
4
+
5
+ # for 400 responses from rackspace
6
+ class BadRequestError < HTTPError; end
7
+
8
+ # for 404 responses from rackspace
9
+ class NotFoundError < HTTPError; end
10
+
11
+ # for 500 responses from rackspace
12
+ class ServerError < HTTPError; end
13
+
14
+ # for rackspace timeouts
15
+ class TimeoutError < RuntimeError; end
16
+
17
+ end
data/lib/raca/server.rb CHANGED
@@ -3,69 +3,24 @@ require 'base64'
3
3
  require 'net/http'
4
4
 
5
5
  module Raca
6
- # Handy abstraction for interacting with a single rackspace Clpud Server. We
7
- # could use fog or similar, but this ~200 line class is simple and does
8
- # everything we need.
6
+ # Represents a single cloud server. Contains methods for deleting a server,
7
+ # listing IP addresses, checking the state, etc.
8
+ #
9
+ # You probably don't want to instantiate this directly,
10
+ # see Raca::Account#servers
11
+ #
9
12
  class Server
10
13
 
11
- attr_reader :server_name, :server_id
14
+ attr_reader :server_id
12
15
 
13
- def initialize(account, region, server_name)
16
+ def initialize(account, region, server_id)
14
17
  @account = account
15
18
  @region = region
16
19
  @servers_url = @account.public_endpoint("cloudServersOpenStack", region)
17
- @server_name = server_name
18
- @server_id = find_server_id(server_name)
19
- end
20
-
21
- # return true if this server exists on Rackspace
22
- #
23
- def exists?
24
- @server_id != nil
25
- end
26
-
27
- # create this server on Rackspace.
28
- #
29
- # flavor_name is a string that describes the amount of RAM. If you enter
30
- # an invalid option a list of valid options will be raised.
31
- #
32
- # image_name is a string that describes the OS image to use. If you enter
33
- # an invalid option a list of valid options will be raised. I suggest
34
- # starting with 'Ubuntu 10.04 LTS'
35
- #
36
- # files is an optional Hash of path to blobs. Use it to place a file on the
37
- # disk of the new server.
38
- #
39
- # Use it like this:
40
- #
41
- # server.create(512, "Ubuntu 10.04 LTS", "/root/.ssh/authorised_keys" => File.read("/foo"))
42
- #
43
- def create(flavor_name, image_name, files = {})
44
- raise ArgumentError, "server already exists" if exists?
45
-
46
- request = {
47
- "server" => {
48
- "name" => @server_name,
49
- "imageRef" => image_name_to_id(image_name),
50
- "flavorRef" => flavor_name_to_id(flavor_name),
51
- }
52
- }
53
- files.each do |path, blob|
54
- request['server']['personality'] ||= []
55
- request['server']['personality'] << {
56
- 'path' => path,
57
- 'contents' => Base64.encode64(blob)
58
- }
59
- end
60
-
61
- data = cloud_request(Net::HTTP::Post.new(servers_path), JSON.dump(request)).body
62
- data = JSON.parse(data)['server']
63
- @server_id = data['id']
20
+ @server_id = server_id
64
21
  end
65
22
 
66
23
  def delete!
67
- raise ArgumentError, "server doesn't exist" unless exists?
68
-
69
24
  response = cloud_request(Net::HTTP::Delete.new(server_path))
70
25
  response.is_a? Net::HTTPSuccess
71
26
  end
@@ -95,42 +50,12 @@ module Raca
95
50
  # A Hash of various matadata about the server
96
51
  #
97
52
  def details
98
- raise ArgumentError, "server doesn't exist" unless exists?
99
-
100
53
  data = cloud_request(Net::HTTP::Get.new(server_path)).body
101
54
  JSON.parse(data)['server']
102
55
  end
103
56
 
104
57
  private
105
58
 
106
- def list
107
- json = cloud_request(Net::HTTP::Get.new(servers_path)).body
108
- JSON.parse(json)['servers']
109
- end
110
-
111
- def find_server_id(server_name)
112
- server = list.detect {|row|
113
- row["name"] == server_name
114
- }
115
- server ? server["id"] : nil
116
- end
117
-
118
- def flavors_path
119
- @flavors_path ||= File.join(account_path, "flavors")
120
- end
121
-
122
- def images_path
123
- @images_path ||= File.join(account_path, "images")
124
- end
125
-
126
- def server_path
127
- @server_path ||= File.join(account_path, "servers", @server_id.to_s)
128
- end
129
-
130
- def servers_path
131
- @servers_path ||= File.join(account_path, "servers")
132
- end
133
-
134
59
  def servers_host
135
60
  @servers_host ||= URI.parse(@servers_url).host
136
61
  end
@@ -139,48 +64,8 @@ module Raca
139
64
  @account_path ||= URI.parse(@servers_url).path
140
65
  end
141
66
 
142
- def flavors
143
- @flavors ||= begin
144
- data = cloud_request(Net::HTTP::Get.new(flavors_path)).body
145
- JSON.parse(data)['flavors']
146
- end
147
- end
148
-
149
- def flavor_names
150
- flavors.map {|row| row['name'] }
151
- end
152
-
153
- def flavor_name_to_id(str)
154
- flavor = flavors.detect {|row|
155
- row['name'].downcase.include?(str.to_s.downcase)
156
- }
157
- if flavor
158
- flavor['id']
159
- else
160
- raise ArgumentError, "valid flavors are: #{flavor_names.join(', ')}"
161
- end
162
- end
163
-
164
- def images
165
- @images ||= begin
166
- data = cloud_request(Net::HTTP::Get.new(images_path)).body
167
- JSON.parse(data)['images']
168
- end
169
- end
170
-
171
- def image_names
172
- images.map {|row| row['name'] }
173
- end
174
-
175
- def image_name_to_id(str)
176
- image = images.detect {|row|
177
- row['name'].downcase.include?(str.to_s.downcase)
178
- }
179
- if image
180
- image['id']
181
- else
182
- raise ArgumentError, "valid images are: #{image_names.join(', ')}"
183
- end
67
+ def server_path
68
+ @server_path ||= File.join(account_path, "servers", @server_id.to_s)
184
69
  end
185
70
 
186
71
  def cloud_request(request, body = nil)
@@ -202,15 +87,32 @@ module Raca
202
87
  @account.refresh_cache
203
88
  response = block.call http
204
89
  end
205
- raise "Failure: Rackspace returned #{response.inspect}" unless response.is_a?(Net::HTTPSuccess)
90
+ if response.is_a?(Net::HTTPSuccess)
91
+ response
92
+ else
93
+ raise_on_error(response)
94
+ end
206
95
  response
207
96
  end
208
- rescue Timeout::Error => e
209
- raise e if retries <= 0
97
+ rescue Timeout::Error
98
+ if retries <= 0
99
+ raise Raca::TimeoutError, "Timeout from Rackspace while trying #{request.class} to #{request.path}"
100
+ end
210
101
 
211
102
  cloud_http(hostname, retries - 1, &block)
212
103
  end
213
104
 
105
+ def raise_on_error(response)
106
+ error_klass = case response.code.to_i
107
+ when 400 then BadRequestError
108
+ when 404 then NotFoundError
109
+ when 500 then ServerError
110
+ else
111
+ HTTPError
112
+ end
113
+ raise error_klass, "Rackspace returned HTTP status #{response.code}"
114
+ end
115
+
214
116
  def log(msg)
215
117
  if defined?(Rails)
216
118
  Rails.logger.info msg
data/lib/raca/servers.rb CHANGED
@@ -1,11 +1,196 @@
1
1
  module Raca
2
+ # Represents a collection of cloud servers within a single region.
3
+ #
4
+ # There's currently no methods that relate to the entire collection,
5
+ # this is primarily used to retrieve a single Raca::Server object.
6
+ #
7
+ # You probably don't want to instantiate this directly,
8
+ # see Raca::Account#servers
9
+ #
2
10
  class Servers
3
11
  def initialize(account, region)
4
12
  @account, @region = account, region
13
+ @servers_url = @account.public_endpoint("cloudServersOpenStack", region)
5
14
  end
6
15
 
7
16
  def get(server_name)
8
- Raca::Server.new(@account, @region, server_name)
17
+ server_id = find_server_id(server_name)
18
+ if server_id
19
+ Raca::Server.new(@account, @region, server_id)
20
+ else
21
+ nil
22
+ end
9
23
  end
24
+
25
+ # create a new server on Rackspace.
26
+ #
27
+ # server_name is a free text name you want to assign the server.
28
+ #
29
+ # flavor_name is a string that describes the amount of RAM. If you enter
30
+ # an invalid option a list of valid options will be raised.
31
+ #
32
+ # image_name is a string that describes the OS image to use. If you enter
33
+ # an invalid option a list of valid options will be raised. I suggest
34
+ # starting with 'Ubuntu 10.04 LTS'
35
+ #
36
+ # files is an optional Hash of path to blobs. Use it to place a file on the
37
+ # disk of the new server.
38
+ #
39
+ # Use it like this:
40
+ #
41
+ # server.create("my-server", 512, "Ubuntu 10.04 LTS", "/root/.ssh/authorised_keys" => File.read("/foo"))
42
+ #
43
+ def create(server_name, flavor_name, image_name, files = {})
44
+ request = {
45
+ "server" => {
46
+ "name" => server_name,
47
+ "imageRef" => image_name_to_id(image_name),
48
+ "flavorRef" => flavor_name_to_id(flavor_name),
49
+ }
50
+ }
51
+ files.each do |path, blob|
52
+ request['server']['personality'] ||= []
53
+ request['server']['personality'] << {
54
+ 'path' => path,
55
+ 'contents' => Base64.encode64(blob)
56
+ }
57
+ end
58
+
59
+ data = cloud_request(Net::HTTP::Post.new(servers_path), JSON.dump(request)).body
60
+ data = JSON.parse(data)['server']
61
+ Raca::Server.new(@account, @region, data['id'])
62
+ end
63
+
64
+ private
65
+
66
+ def servers_host
67
+ @servers_host ||= URI.parse(@servers_url).host
68
+ end
69
+
70
+ def account_path
71
+ @account_path ||= URI.parse(@servers_url).path
72
+ end
73
+
74
+ def flavors_path
75
+ @flavors_path ||= File.join(account_path, "flavors")
76
+ end
77
+
78
+ def images_path
79
+ @images_path ||= File.join(account_path, "images")
80
+ end
81
+
82
+ def servers_path
83
+ @servers_path ||= File.join(account_path, "servers")
84
+ end
85
+
86
+ def list
87
+ json = cloud_request(Net::HTTP::Get.new(servers_path)).body
88
+ JSON.parse(json)['servers']
89
+ end
90
+
91
+ def find_server_id(server_name)
92
+ server = list.detect {|row|
93
+ row["name"] == server_name
94
+ }
95
+ server ? server["id"] : nil
96
+ end
97
+
98
+ def flavors
99
+ @flavors ||= begin
100
+ data = cloud_request(Net::HTTP::Get.new(flavors_path)).body
101
+ JSON.parse(data)['flavors']
102
+ end
103
+ end
104
+
105
+ def flavor_names
106
+ flavors.map {|row| row['name'] }
107
+ end
108
+
109
+ def flavor_name_to_id(str)
110
+ flavor = flavors.detect {|row|
111
+ row['name'].downcase.include?(str.to_s.downcase)
112
+ }
113
+ if flavor
114
+ flavor['id']
115
+ else
116
+ raise ArgumentError, "valid flavors are: #{flavor_names.join(', ')}"
117
+ end
118
+ end
119
+
120
+ def images
121
+ @images ||= begin
122
+ data = cloud_request(Net::HTTP::Get.new(images_path)).body
123
+ JSON.parse(data)['images']
124
+ end
125
+ end
126
+
127
+ def image_names
128
+ images.map {|row| row['name'] }
129
+ end
130
+
131
+ def image_name_to_id(str)
132
+ image = images.detect {|row|
133
+ row['name'].downcase.include?(str.to_s.downcase)
134
+ }
135
+ if image
136
+ image['id']
137
+ else
138
+ raise ArgumentError, "valid images are: #{image_names.join(', ')}"
139
+ end
140
+ end
141
+
142
+ def cloud_request(request, body = nil)
143
+ request['X-Auth-Token'] = @account.auth_token
144
+ request['Content-Type'] = 'application/json'
145
+ request['Accept'] = 'application/json'
146
+ cloud_http(servers_host) do |http|
147
+ http.request(request, body)
148
+ end
149
+ end
150
+
151
+ def cloud_http(hostname, retries = 3, &block)
152
+ http = Net::HTTP.new(hostname, 443)
153
+ http.use_ssl = true
154
+ http.start do |http|
155
+ response = block.call http
156
+ if response.is_a?(Net::HTTPUnauthorized)
157
+ log "Rackspace returned HTTP 401; refreshing auth before retrying."
158
+ @account.refresh_cache
159
+ response = block.call http
160
+ end
161
+ if response.is_a?(Net::HTTPSuccess)
162
+ response
163
+ else
164
+ raise_on_error(response)
165
+ end
166
+ response
167
+ end
168
+ rescue Timeout::Error
169
+ if retries <= 0
170
+ raise Raca::TimeoutError, "Timeout from Rackspace while trying #{request.class} to #{request.path}"
171
+ end
172
+
173
+ cloud_http(hostname, retries - 1, &block)
174
+ end
175
+
176
+ def raise_on_error(response)
177
+ error_klass = case response.code.to_i
178
+ when 400 then BadRequestError
179
+ when 404 then NotFoundError
180
+ when 500 then ServerError
181
+ else
182
+ HTTPError
183
+ end
184
+ raise error_klass, "Rackspace returned HTTP status #{response.code}"
185
+ end
186
+
187
+ def log(msg)
188
+ if defined?(Rails)
189
+ Rails.logger.info msg
190
+ else
191
+ puts msg
192
+ end
193
+ end
194
+
10
195
  end
11
196
  end
data/lib/raca.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require 'raca/errors'
2
+
1
3
  require 'raca/account'
2
4
  require 'raca/container'
3
5
  require 'raca/containers'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: raca
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Healy
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-02-24 00:00:00.000000000 Z
11
+ date: 2014-03-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: cane
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
69
83
  description: A simple wrapper for the Rackspace Cloud API with no dependencies
70
84
  email:
71
85
  - james.healy@theconversation.edu.au
@@ -79,6 +93,7 @@ files:
79
93
  - lib/raca/account.rb
80
94
  - lib/raca/container.rb
81
95
  - lib/raca/containers.rb
96
+ - lib/raca/errors.rb
82
97
  - lib/raca/server.rb
83
98
  - lib/raca/servers.rb
84
99
  homepage: http://github.com/conversation/raca
@@ -104,7 +119,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
104
119
  version: '0'
105
120
  requirements: []
106
121
  rubyforge_project:
107
- rubygems_version: 2.2.0
122
+ rubygems_version: 2.2.2
108
123
  signing_key:
109
124
  specification_version: 4
110
125
  summary: A simple wrapper for the Rackspace Cloud API with no dependencies