raca 0.1.1 → 0.2.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.
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