raca 0.2.0 → 0.3.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: 0f887cfa6d624450eb2805122c998cc29acc0b01
4
- data.tar.gz: 89109fe3d3b728239a0ab9795d24508b96bff2ae
3
+ metadata.gz: 264c32ae5539a36329c0b7acbbc054f1d70eb142
4
+ data.tar.gz: 25a6c347e4cb9e4573c993a5591426f66f0b5872
5
5
  SHA512:
6
- metadata.gz: 65b7f065c88ec68907b6580fc65b1ed05187988807a450bfee8dff001d2bb0c2ac552b973bdc188567edd0bbcc1bb12ef5ca2de9e732dd9713df64220d8f8c80
7
- data.tar.gz: ce61916a750f740b82b569be8913eaa806dffab65e3ad34013d49702ceaca9aa63ab7a01333a277adf51dc674427984c2fb534fc8f6cde915438ad1ce5007df8
6
+ metadata.gz: 932604c188aef930a67ef37950efe1de4bf2abf7ea272c2f2f2519e5d43c0e97cd7df18edda157f6b83aa1daa0f25de97ea81be613e0d477054ef833dbc6183f
7
+ data.tar.gz: 53fff6b33e8809a152ccf05f45259518c4a5ecc0e2c6e7d9d4fffa67814f6a7c936c76f8fa5739d3ec0581220af2fdcb21fc6e48aa8b0c686002da2d46ede8c9
data/CHANGELOG ADDED
@@ -0,0 +1,24 @@
1
+ v0.3.0 (12th April 2014)
2
+ * Added an optional headers param to Raca::Container#upload
3
+ * Added the details option to Raca::Container#list
4
+ * Fixed Raca::Container#list to return all items for containers that
5
+ contain more than 10,000 objects
6
+ * Fixed Raca::Container to work with container and object names that
7
+ need to be escaped (spaces, some punctuation, utf8 characters, etc)
8
+ * Refactored internal management of HTTP requests to reduce duplication
9
+ and improve testability. There should be no visible change to the public
10
+ API of raca
11
+
12
+ v0.2.0 (19th March 2014)
13
+ * Breaking API changes to Raca::Servers and Raca:Server
14
+ * Moved the create() method from Server to Servers
15
+ * Added Raca::Account#service_names
16
+ * Stop returning HTTP response objects from many methods on Raca::Container
17
+ * Added some custom error classes for common issues (timeouts, 404s, etc) to
18
+ make recovering easier for the user
19
+
20
+ v0.1.1 (25th February 2014)
21
+ * Add Raca::Container#object_metadata
22
+
23
+ v0.1.0 (24th February 2014)
24
+ * Initial Release
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2014 The Conversation
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown CHANGED
@@ -45,10 +45,13 @@ You can view the token that will be used for subsequent requests:
45
45
 
46
46
  puts account.auth_token
47
47
 
48
- Or you can view the URLs for each rackspace cloud API:
48
+ List the available APIs:
49
+
50
+ puts account.service_names
51
+
52
+ ... and then view the URLs for each service:
49
53
 
50
54
  puts account.public_endpoint("cloudFiles", :ord)
51
- puts account.service_endpoint("cloudFiles", :ord)
52
55
 
53
56
  ### Cloud Files
54
57
 
@@ -157,3 +160,8 @@ impact on application boot times.
157
160
 
158
161
  The Raca version number is < 1.0 because it's highly unstable. Until we release
159
162
  a 1.0.0, consider the API of this gem to be unstable.
163
+
164
+ ## License
165
+
166
+ This library is released undr the MIT License. See the included MIT-LICENSE file
167
+ for further details
data/Rakefile CHANGED
@@ -18,5 +18,5 @@ Cane::RakeTask.new(:cane) do |cane|
18
18
  cane.style_measure = 148
19
19
 
20
20
  # 0 is the goal
21
- cane.max_violations = 3
21
+ cane.max_violations = 2
22
22
  end
data/lib/raca/account.rb CHANGED
@@ -70,7 +70,7 @@ module Raca
70
70
  Raca::Containers.new(self, region)
71
71
  end
72
72
 
73
- # Return a Raca::Containers object for a region. Use this to interact with the
73
+ # Return a Raca::Servers object for a region. Use this to interact with the
74
74
  # next gen cloud servers service.
75
75
  #
76
76
  # account = Raca::Account.new("username", "secret")
@@ -81,9 +81,11 @@ module Raca
81
81
  end
82
82
 
83
83
  # Raca classes use this method to occasionally re-authenticate with the rackspace
84
- # servers. You can probable ignore it.
84
+ # servers. You can probably ignore it.
85
85
  #
86
86
  def refresh_cache
87
+ # Raca::HttpClient depends on Raca::Account, so we intentionally don't use it here
88
+ # to avoid a circular dependency
87
89
  Net::HTTP.new('identity.api.rackspacecloud.com', 443).tap {|http|
88
90
  http.use_ssl = true
89
91
  }.start {|http|
@@ -108,6 +110,12 @@ module Raca
108
110
  }
109
111
  end
110
112
 
113
+ # Return a Raca::HttpClient suitable for making requests to hostname.
114
+ #
115
+ def http_client(hostname)
116
+ Raca::HttpClient.new(self, hostname)
117
+ end
118
+
111
119
  private
112
120
 
113
121
  def raise_on_error(response)
@@ -1,4 +1,3 @@
1
- require 'net/http'
2
1
  require 'digest/md5'
3
2
  require 'openssl'
4
3
  require 'uri'
@@ -15,7 +14,6 @@ module Raca
15
14
  MAX_ITEMS_PER_LIST = 10_000
16
15
  LARGE_FILE_THRESHOLD = 5_368_709_120 # 5 Gb
17
16
  LARGE_FILE_SEGMENT_SIZE = 104_857_600 # 100 Mb
18
- RETRY_PAUSE = 5
19
17
 
20
18
  attr_reader :container_name
21
19
 
@@ -30,13 +28,16 @@ module Raca
30
28
 
31
29
  # Upload data_or_path (which may be a filename or an IO) to the container, as key.
32
30
  #
33
- def upload(key, data_or_path)
31
+ # If headers are provided they will be added to to upload request. Use this to
32
+ # manually specify content type, content disposition, CORS headers, etc.
33
+ #
34
+ def upload(key, data_or_path, headers = {})
34
35
  case data_or_path
35
36
  when StringIO, File
36
- upload_io(key, data_or_path, data_or_path.size)
37
+ upload_io(key, data_or_path, data_or_path.size, headers)
37
38
  when String
38
39
  File.open(data_or_path, "rb") do |io|
39
- upload_io(key, io, io.stat.size)
40
+ upload_io(key, io, io.stat.size, headers)
40
41
  end
41
42
  else
42
43
  raise ArgumentError, "data_or_path must be an IO with data or filename string"
@@ -48,7 +49,8 @@ module Raca
48
49
  #
49
50
  def delete(key)
50
51
  log "deleting #{key} from #{container_path}"
51
- response = storage_request(Net::HTTP::Delete.new(File.join(container_path, key)))
52
+ object_path = File.join(container_path, Raca::Util.url_encode(key))
53
+ response = storage_client.delete(object_path)
52
54
  (200..299).cover?(response.code.to_i)
53
55
  end
54
56
 
@@ -61,20 +63,20 @@ module Raca
61
63
  #
62
64
  def purge_from_akamai(key, email_address)
63
65
  log "Requesting #{File.join(container_path, key)} to be purged from the CDN"
64
- response = cdn_request(Net::HTTP::Delete.new(
65
- File.join(container_path, key),
66
+ response = cdn_client.delete(
67
+ File.join(container_path, Raca::Util.url_encode(key)),
66
68
  'X-Purge-Email' => email_address
67
- ))
69
+ )
68
70
  (200..299).cover?(response.code.to_i)
69
71
  end
70
72
 
71
73
  # Returns some metadata about a single object in this container.
72
74
  #
73
75
  def object_metadata(key)
74
- object_path = File.join(container_path, key)
76
+ object_path = File.join(container_path, Raca::Util.url_encode(key))
75
77
  log "Requesting metadata from #{object_path}"
76
78
 
77
- response = storage_request(Net::HTTP::Head.new(object_path))
79
+ response = storage_client.head(object_path)
78
80
  {
79
81
  :content_type => response["Content-Type"],
80
82
  :bytes => response["Content-Length"].to_i
@@ -87,7 +89,8 @@ module Raca
87
89
  #
88
90
  def download(key, filepath)
89
91
  log "downloading #{key} from #{container_path}"
90
- response = storage_request(Net::HTTP::Get.new(File.join(container_path, key))) do |response|
92
+ object_path = File.join(container_path, Raca::Util.url_encode(key))
93
+ response = storage_client.get(object_path) do |response|
91
94
  File.open(filepath, 'wb') do |io|
92
95
  response.read_body do |chunk|
93
96
  io.write(chunk)
@@ -104,26 +107,31 @@ module Raca
104
107
  # max - the maximum number of items to return
105
108
  # marker - return items alphabetically after this key. Useful for pagination
106
109
  # prefix - only return items that start with this string
110
+ # details - return extra details for each file - size, md5, etc
107
111
  #
108
112
  def list(options = {})
109
- max = options.fetch(:max, MAX_ITEMS_PER_LIST)
113
+ max = options.fetch(:max, 100_000_000)
110
114
  marker = options.fetch(:marker, nil)
111
115
  prefix = options.fetch(:prefix, nil)
116
+ details = options.fetch(:details, nil)
112
117
  limit = [max, MAX_ITEMS_PER_LIST].min
113
- log "retrieving up to #{limit} of #{max} items from #{container_path}"
114
- query_string = "limit=#{limit}"
115
- query_string += "&marker=#{marker}" if marker
116
- query_string += "&prefix=#{prefix}" if prefix
117
- request = Net::HTTP::Get.new(container_path + "?#{query_string}")
118
- result = storage_request(request).body || ""
119
- result.split("\n").tap {|items|
118
+ log "retrieving up to #{max} items from #{container_path}"
119
+ request_path = list_request_path(marker, prefix, details, limit)
120
+ result = storage_client.get(request_path).body || ""
121
+ if details
122
+ result = JSON.parse(result)
123
+ else
124
+ result = result.split("\n")
125
+ end
126
+ result.tap {|items|
120
127
  if max <= limit
121
128
  log "Got #{items.length} items; we don't need any more."
122
129
  elsif items.length < limit
123
130
  log "Got #{items.length} items; there can't be any more."
124
131
  else
125
- log "Got #{items.length} items; requesting #{max - limit} more."
126
- items.concat list(max: max - limit, marker: items.last, prefix: prefix)
132
+ log "Got #{items.length} items; requesting #{limit} more."
133
+ details ? marker = items.last["name"] : marker = items.last
134
+ items.concat list(max: max-items.length, marker: marker, prefix: prefix, details: details)
127
135
  end
128
136
  }
129
137
  end
@@ -142,7 +150,7 @@ module Raca
142
150
  #
143
151
  def metadata
144
152
  log "retrieving container metadata from #{container_path}"
145
- response = storage_request(Net::HTTP::Head.new(container_path))
153
+ response = storage_client.head(container_path)
146
154
  {
147
155
  :objects => response["X-Container-Object-Count"].to_i,
148
156
  :bytes => response["X-Container-Bytes-Used"].to_i
@@ -154,7 +162,7 @@ module Raca
154
162
  #
155
163
  def cdn_metadata
156
164
  log "retrieving container CDN metadata from #{container_path}"
157
- response = cdn_request(Net::HTTP::Head.new(container_path))
165
+ response = cdn_client.head(container_path)
158
166
  {
159
167
  :cdn_enabled => response["X-CDN-Enabled"] == "True",
160
168
  :host => response["X-CDN-URI"],
@@ -174,7 +182,7 @@ module Raca
174
182
  def cdn_enable(ttl = 259200)
175
183
  log "enabling CDN access to #{container_path} with a cache expiry of #{ttl / 60} minutes"
176
184
 
177
- response = cdn_request(Net::HTTP::Put.new(container_path, "X-TTL" => ttl.to_i.to_s))
185
+ response = cdn_client.put(container_path, "X-TTL" => ttl.to_i.to_s)
178
186
  (200..299).cover?(response.code.to_i)
179
187
  end
180
188
 
@@ -187,49 +195,54 @@ module Raca
187
195
  method = 'GET'
188
196
  expires = expires_at.to_i
189
197
  path = File.join(container_path, object_key)
198
+ encoded_path = File.join(container_path, Raca::Util.url_encode(object_key))
190
199
  data = "#{method}\n#{expires}\n#{path}"
191
200
 
192
201
  hmac = OpenSSL::HMAC.new(temp_url_key, digest)
193
202
  hmac << data
194
203
 
195
- "https://#{storage_host}#{path}?temp_url_sig=#{hmac.hexdigest}&temp_url_expires=#{expires}"
204
+ "https://#{storage_host}#{encoded_path}?temp_url_sig=#{hmac.hexdigest}&temp_url_expires=#{expires}"
196
205
  end
197
206
 
198
207
  private
199
208
 
200
- def upload_io(key, io, byte_count)
209
+ # build the request path for listing the contents of a container
210
+ #
211
+ def list_request_path(marker, prefix, details, limit)
212
+ query_string = "limit=#{limit}"
213
+ query_string += "&marker=#{Raca::Util.url_encode(marker)}" if marker
214
+ query_string += "&prefix=#{Raca::Util.url_encode(prefix)}" if prefix
215
+ query_string += "&format=json" if details
216
+ container_path + "?#{query_string}"
217
+ end
218
+
219
+
220
+ def upload_io(key, io, byte_count, headers = {})
201
221
  if byte_count <= LARGE_FILE_THRESHOLD
202
- upload_io_standard(key, io, byte_count)
222
+ upload_io_standard(key, io, byte_count, headers)
203
223
  else
204
- upload_io_large(key, io, byte_count)
224
+ upload_io_large(key, io, byte_count, headers)
205
225
  end
206
226
  end
207
227
 
208
- def upload_io_standard(key, io, byte_count)
209
- full_path = File.join(container_path, key)
228
+ def upload_io_standard(key, io, byte_count, headers = {})
229
+ full_path = File.join(container_path, Raca::Util.url_encode(key))
210
230
 
211
- headers = {}
212
- headers['Content-Type'] = extension_content_type(full_path)
231
+ headers['Content-Type'] ||= extension_content_type(full_path)
213
232
  if io.respond_to?(:path)
214
233
  headers['Content-Type'] ||= extension_content_type(io.path)
215
- headers['Content-Type'] ||= file_content_type(io.path)
216
234
  end
217
- headers['Etag'] = md5_io(io)
235
+ headers['Etag'] = md5_io(io)
218
236
  headers['Content-Type'] ||= "application/octet-stream"
219
237
  if content_type_needs_cors(key)
220
238
  headers['Access-Control-Allow-Origin'] = "*"
221
239
  end
222
240
 
223
241
  log "uploading #{byte_count} bytes to #{full_path}"
224
-
225
- request = Net::HTTP::Put.new(full_path, headers)
226
- request.body_stream = io
227
- request.content_length = byte_count
228
- response = storage_request(request)
229
- response['ETag']
242
+ put_upload(full_path, headers, byte_count, io)
230
243
  end
231
244
 
232
- def upload_io_large(key, io, byte_count)
245
+ def upload_io_large(key, io, byte_count, headers = {})
233
246
  segment_count = (byte_count.to_f / LARGE_FILE_SEGMENT_SIZE).ceil
234
247
  segments = []
235
248
  while segments.size < segment_count
@@ -237,67 +250,25 @@ module Raca
237
250
  segment_key = "%s.%03d" % [key, segments.size]
238
251
  io.seek(start_pos)
239
252
  segment_io = StringIO.new(io.read(LARGE_FILE_SEGMENT_SIZE))
240
- etag = upload_io_standard(segment_key, segment_io, segment_io.size)
253
+ etag = upload_io_standard(segment_key, segment_io, segment_io.size, headers)
241
254
  segments << {path: "#{@container_name}/#{segment_key}", etag: etag, size_bytes: segment_io.size}
242
255
  end
243
- manifest_key = "#{key}?multipart-manifest=put"
256
+ full_path = File.join(container_path, Raca::Util.url_encode(key)) + "?multipart-manifest=put"
244
257
  manifest_body = StringIO.new(JSON.dump(segments))
245
- upload_io_standard(manifest_key, manifest_body, manifest_body.size)
246
- end
247
-
248
- def cdn_request(request, &block)
249
- cloud_request(request, cdn_host, &block)
250
- end
251
-
252
- def storage_request(request, &block)
253
- cloud_request(request, storage_host, &block)
258
+ put_upload(full_path, {'Etag' => md5_io(manifest_body)}, manifest_body.string.bytesize, manifest_body)
254
259
  end
255
260
 
256
- def cloud_request(request, hostname, retries = 0, &block)
257
- cloud_http(hostname) do |http|
258
- request['X-Auth-Token'] = @account.auth_token
259
- http.request(request, &block)
260
- end
261
- rescue Timeout::Error
262
- if retries >= 3
263
- raise Raca::TimeoutError, "Timeout from Rackspace while trying #{request.class} to #{request.path}"
264
- end
265
-
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)
269
-
270
- cloud_request(request, hostname, retries + 1, &block)
261
+ def put_upload(full_path, headers, byte_count, io)
262
+ response = storage_client.streaming_put(full_path, io, byte_count, headers)
263
+ response['ETag']
271
264
  end
272
265
 
273
- def cloud_http(hostname, &block)
274
- Net::HTTP.new(hostname, 443).tap {|http|
275
- http.use_ssl = true
276
- http.read_timeout = 70
277
- }.start do |http|
278
- response = block.call http
279
- if response.is_a?(Net::HTTPUnauthorized)
280
- log "Rackspace returned HTTP 401; refreshing auth before retrying."
281
- @account.refresh_cache
282
- response = block.call http
283
- end
284
- if response.is_a?(Net::HTTPSuccess)
285
- response
286
- else
287
- raise_on_error(response)
288
- end
289
- end
266
+ def cdn_client
267
+ @cdn_client ||= @account.http_client(cdn_host)
290
268
  end
291
269
 
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}"
270
+ def storage_client
271
+ @storage_client ||= @account.http_client(storage_host)
301
272
  end
302
273
 
303
274
  def log(msg)
@@ -323,11 +294,7 @@ module Raca
323
294
  end
324
295
 
325
296
  def container_path
326
- @container_path ||= File.join(storage_path, container_name)
327
- end
328
-
329
- def file_content_type(path)
330
- `file -b --mime-type \"#{path.gsub('"', '\"')}\"`.chomp
297
+ @container_path ||= File.join(storage_path, Raca::Util.url_encode(container_name))
331
298
  end
332
299
 
333
300
  def extension_content_type(path)
@@ -23,7 +23,7 @@ module Raca
23
23
  #
24
24
  def metadata
25
25
  log "retrieving containers metadata from #{storage_path}"
26
- response = storage_request(Net::HTTP::Head.new(storage_path))
26
+ response = storage_client.head(storage_path)
27
27
  {
28
28
  :containers => response["X-Account-Container-Count"].to_i,
29
29
  :objects => response["X-Account-Object-Count"].to_i,
@@ -41,8 +41,7 @@ module Raca
41
41
  def set_temp_url_key(secret)
42
42
  log "setting Account Temp URL Key on #{storage_path}"
43
43
 
44
- request = Net::HTTP::Post.new(storage_path, "X-Account-Meta-Temp-Url-Key" => secret.to_s)
45
- response = storage_request(request)
44
+ response = storage_client.post(storage_path, nil, "X-Account-Meta-Temp-Url-Key" => secret.to_s)
46
45
  (200..299).cover?(response.code.to_i)
47
46
  end
48
47
 
@@ -56,46 +55,8 @@ module Raca
56
55
  URI.parse(@storage_url).path
57
56
  end
58
57
 
59
- def storage_request(request, &block)
60
- cloud_request(request, storage_host, &block)
61
- end
62
-
63
- def cloud_request(request, hostname, &block)
64
- cloud_http(hostname) do |http|
65
- request['X-Auth-Token'] = @account.auth_token
66
- http.request(request, &block)
67
- end
68
- end
69
-
70
- def cloud_http(hostname, &block)
71
- Net::HTTP.new(hostname, 443).tap {|http|
72
- http.use_ssl = true
73
- http.read_timeout = 70
74
- }.start do |http|
75
- response = block.call http
76
- if response.is_a?(Net::HTTPUnauthorized)
77
- log "Rackspace returned HTTP 401; refreshing auth before retrying."
78
- @account.refresh_cache
79
- response = block.call http
80
- end
81
- if response.is_a?(Net::HTTPSuccess)
82
- response
83
- else
84
- raise_on_error(response)
85
- end
86
- response
87
- end
88
- end
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}"
58
+ def storage_client
59
+ @storage_client ||= @account.http_client(storage_host)
99
60
  end
100
61
 
101
62
  def log(msg)
@@ -0,0 +1,123 @@
1
+ require 'net/http'
2
+ require 'openssl'
3
+
4
+ module Raca
5
+
6
+ # A thin wrapper around Net::HTTP. It's aware of some common details of
7
+ # the rackspace APIs and has an API to match.
8
+ #
9
+ # You probably don't want to instantiate this directly,
10
+ # see Raca::Account#http_client
11
+ #
12
+ class HttpClient
13
+ RETRY_PAUSE = 5
14
+
15
+ def initialize(account, hostname, opts = {})
16
+ @account, @hostname = account, hostname.to_s
17
+ raise ArgumentError, "hostname must be plain hostname, leave the protocol out" if @hostname[/\Ahttp/]
18
+ @logger = opts[:logger]
19
+ @logger ||= Rails.logger if defined?(Rails)
20
+ end
21
+
22
+ def get(path, headers = {}, &block)
23
+ cloud_request(Net::HTTP::Get.new(path, headers), &block)
24
+ end
25
+
26
+ def head(path, headers = {})
27
+ cloud_request(Net::HTTP::Head.new(path, headers))
28
+ end
29
+
30
+ def delete(path, headers = {})
31
+ cloud_request(Net::HTTP::Delete.new(path, headers))
32
+ end
33
+
34
+ def put(path, headers = {})
35
+ cloud_request(Net::HTTP::Put.new(path, headers))
36
+ end
37
+
38
+ def streaming_put(path, io, byte_count, headers = {})
39
+ request = Net::HTTP::Put.new(path, headers)
40
+ request.body_stream = io
41
+ request.content_length = byte_count
42
+ cloud_request(request)
43
+ end
44
+
45
+ def post(path, body, headers = {})
46
+ request = Net::HTTP::Post.new(path, headers)
47
+ request.body = body if body
48
+ cloud_request(request)
49
+ end
50
+
51
+ private
52
+
53
+ # perform an HTTP request to rackpsace.
54
+ #
55
+ # request is a Net::HTTP request object.
56
+ # retries is an int that counts up as the request is tried after a timeout.
57
+ # This can be called with and without a block. Without a block, the response
58
+ # is returned as you'd expect
59
+ #
60
+ # response = http_client.cloud_request(request)
61
+ #
62
+ # With the block form, the response is yielded to the block:
63
+ #
64
+ # http_client.cloud_request(request) do |response|
65
+ # puts response
66
+ # end
67
+ #
68
+ def cloud_request(request, retries = 0, &block)
69
+ cloud_http do |http|
70
+ request['X-Auth-Token'] = @account.auth_token
71
+ http.request(request, &block)
72
+ end
73
+ rescue Timeout::Error
74
+ if retries >= 3
75
+ raise Raca::TimeoutError, "Timeout from Rackspace while trying #{request.class} to #{request.path}"
76
+ end
77
+
78
+ retry_interval = RETRY_PAUSE + (retries.to_i * RETRY_PAUSE) # Retry after 5, 10, 15 and 20 seconds
79
+ log "Rackspace timed out: retrying after #{retry_interval}s"
80
+ sleep(retry_interval)
81
+
82
+ cloud_request(request, retries + 1, &block)
83
+ end
84
+
85
+ def cloud_http(&block)
86
+ Net::HTTP.new(@hostname, 443).tap {|http|
87
+ http.use_ssl = true
88
+ http.read_timeout = 70
89
+ }.start do |http|
90
+ response = block.call http
91
+ if response.is_a?(Net::HTTPUnauthorized)
92
+ log "Rackspace returned HTTP 401; refreshing auth before retrying."
93
+ @account.refresh_cache
94
+ response = block.call http
95
+ end
96
+ if response.is_a?(Net::HTTPSuccess)
97
+ response
98
+ else
99
+ raise_on_error(response)
100
+ end
101
+ end
102
+ end
103
+
104
+ def raise_on_error(response)
105
+ error_klass = case response.code.to_i
106
+ when 400 then BadRequestError
107
+ when 404 then NotFoundError
108
+ when 500 then ServerError
109
+ else
110
+ HTTPError
111
+ end
112
+ raise error_klass, "Rackspace returned HTTP status #{response.code}"
113
+ end
114
+
115
+ def log(msg)
116
+ if @logger.respond_to?(:debug)
117
+ @logger.debug msg
118
+ end
119
+ end
120
+
121
+ end
122
+ end
123
+
data/lib/raca/server.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  require 'json'
2
2
  require 'base64'
3
- require 'net/http'
4
3
 
5
4
  module Raca
6
5
  # Represents a single cloud server. Contains methods for deleting a server,
@@ -21,7 +20,7 @@ module Raca
21
20
  end
22
21
 
23
22
  def delete!
24
- response = cloud_request(Net::HTTP::Delete.new(server_path))
23
+ response = servers_client.delete(server_path, json_headers)
25
24
  response.is_a? Net::HTTPSuccess
26
25
  end
27
26
 
@@ -50,12 +49,19 @@ module Raca
50
49
  # A Hash of various matadata about the server
51
50
  #
52
51
  def details
53
- data = cloud_request(Net::HTTP::Get.new(server_path)).body
52
+ data = servers_client.get(server_path, json_headers).body
54
53
  JSON.parse(data)['server']
55
54
  end
56
55
 
57
56
  private
58
57
 
58
+ def json_headers
59
+ {
60
+ 'Content-Type' => 'application/json',
61
+ 'Accept' => 'application/json'
62
+ }
63
+ end
64
+
59
65
  def servers_host
60
66
  @servers_host ||= URI.parse(@servers_url).host
61
67
  end
@@ -68,49 +74,8 @@ module Raca
68
74
  @server_path ||= File.join(account_path, "servers", @server_id.to_s)
69
75
  end
70
76
 
71
- def cloud_request(request, body = nil)
72
- request['X-Auth-Token'] = @account.auth_token
73
- request['Content-Type'] = 'application/json'
74
- request['Accept'] = 'application/json'
75
- cloud_http(servers_host) do |http|
76
- http.request(request, body)
77
- end
78
- end
79
-
80
- def cloud_http(hostname, retries = 3, &block)
81
- http = Net::HTTP.new(hostname, 443)
82
- http.use_ssl = true
83
- http.start do |http|
84
- response = block.call http
85
- if response.is_a?(Net::HTTPUnauthorized)
86
- log "Rackspace returned HTTP 401; refreshing auth before retrying."
87
- @account.refresh_cache
88
- response = block.call http
89
- end
90
- if response.is_a?(Net::HTTPSuccess)
91
- response
92
- else
93
- raise_on_error(response)
94
- end
95
- response
96
- end
97
- rescue Timeout::Error
98
- if retries <= 0
99
- raise Raca::TimeoutError, "Timeout from Rackspace while trying #{request.class} to #{request.path}"
100
- end
101
-
102
- cloud_http(hostname, retries - 1, &block)
103
- end
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}"
77
+ def servers_client
78
+ @servers_client ||= @account.http_client(servers_host)
114
79
  end
115
80
 
116
81
  def log(msg)
data/lib/raca/servers.rb CHANGED
@@ -56,13 +56,20 @@ module Raca
56
56
  }
57
57
  end
58
58
 
59
- data = cloud_request(Net::HTTP::Post.new(servers_path), JSON.dump(request)).body
60
- data = JSON.parse(data)['server']
59
+ response = servers_client.post(servers_path, JSON.dump(request), json_headers)
60
+ data = JSON.parse(response.body)['server']
61
61
  Raca::Server.new(@account, @region, data['id'])
62
62
  end
63
63
 
64
64
  private
65
65
 
66
+ def json_headers
67
+ {
68
+ 'Content-Type' => 'application/json',
69
+ 'Accept' => 'application/json'
70
+ }
71
+ end
72
+
66
73
  def servers_host
67
74
  @servers_host ||= URI.parse(@servers_url).host
68
75
  end
@@ -84,7 +91,7 @@ module Raca
84
91
  end
85
92
 
86
93
  def list
87
- json = cloud_request(Net::HTTP::Get.new(servers_path)).body
94
+ json = servers_client.get(servers_path, json_headers).body
88
95
  JSON.parse(json)['servers']
89
96
  end
90
97
 
@@ -97,7 +104,7 @@ module Raca
97
104
 
98
105
  def flavors
99
106
  @flavors ||= begin
100
- data = cloud_request(Net::HTTP::Get.new(flavors_path)).body
107
+ data = servers_client.get(flavors_path, json_headers).body
101
108
  JSON.parse(data)['flavors']
102
109
  end
103
110
  end
@@ -119,7 +126,7 @@ module Raca
119
126
 
120
127
  def images
121
128
  @images ||= begin
122
- data = cloud_request(Net::HTTP::Get.new(images_path)).body
129
+ data = servers_client.get(images_path, json_headers).body
123
130
  JSON.parse(data)['images']
124
131
  end
125
132
  end
@@ -139,49 +146,8 @@ module Raca
139
146
  end
140
147
  end
141
148
 
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}"
149
+ def servers_client
150
+ @servers_client ||= @account.http_client(servers_host)
185
151
  end
186
152
 
187
153
  def log(msg)
data/lib/raca/util.rb ADDED
@@ -0,0 +1,11 @@
1
+ module Raca
2
+ # Misc helper methods used across the codebase
3
+ class Util
4
+ # CGI.escape, but without special treatment on spaces
5
+ def self.url_encode(str)
6
+ str.gsub(%r{([^a-zA-Z0-9_./-]+)}) do |match|
7
+ '%' + match.unpack('H2*' * match.bytesize).join('%').upcase
8
+ end
9
+ end
10
+ end
11
+ end
data/lib/raca.rb CHANGED
@@ -3,5 +3,7 @@ require 'raca/errors'
3
3
  require 'raca/account'
4
4
  require 'raca/container'
5
5
  require 'raca/containers'
6
+ require 'raca/http_client'
6
7
  require 'raca/server'
7
8
  require 'raca/servers'
9
+ require 'raca/util'
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.2.0
4
+ version: 0.3.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-03-19 00:00:00.000000000 Z
11
+ date: 2014-04-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -87,6 +87,8 @@ executables: []
87
87
  extensions: []
88
88
  extra_rdoc_files: []
89
89
  files:
90
+ - CHANGELOG
91
+ - MIT-LICENSE
90
92
  - README.markdown
91
93
  - Rakefile
92
94
  - lib/raca.rb
@@ -94,8 +96,10 @@ files:
94
96
  - lib/raca/container.rb
95
97
  - lib/raca/containers.rb
96
98
  - lib/raca/errors.rb
99
+ - lib/raca/http_client.rb
97
100
  - lib/raca/server.rb
98
101
  - lib/raca/servers.rb
102
+ - lib/raca/util.rb
99
103
  homepage: http://github.com/conversation/raca
100
104
  licenses:
101
105
  - MIT