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 +4 -4
- data/CHANGELOG +24 -0
- data/MIT-LICENSE +20 -0
- data/README.markdown +10 -2
- data/Rakefile +1 -1
- data/lib/raca/account.rb +10 -2
- data/lib/raca/container.rb +66 -99
- data/lib/raca/containers.rb +4 -43
- data/lib/raca/http_client.rb +123 -0
- data/lib/raca/server.rb +11 -46
- data/lib/raca/servers.rb +14 -48
- data/lib/raca/util.rb +11 -0
- data/lib/raca.rb +2 -0
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 264c32ae5539a36329c0b7acbbc054f1d70eb142
|
4
|
+
data.tar.gz: 25a6c347e4cb9e4573c993a5591426f66f0b5872
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
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::
|
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
|
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)
|
data/lib/raca/container.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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 =
|
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 =
|
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
|
-
|
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,
|
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 #{
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
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 #{
|
126
|
-
items.
|
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 =
|
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 =
|
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 =
|
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}#{
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
257
|
-
|
258
|
-
|
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
|
274
|
-
|
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
|
293
|
-
|
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)
|
data/lib/raca/containers.rb
CHANGED
@@ -23,7 +23,7 @@ module Raca
|
|
23
23
|
#
|
24
24
|
def metadata
|
25
25
|
log "retrieving containers metadata from #{storage_path}"
|
26
|
-
response =
|
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
|
-
|
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
|
60
|
-
|
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 =
|
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 =
|
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
|
72
|
-
|
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
|
-
|
60
|
-
data = JSON.parse(
|
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 =
|
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 =
|
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 =
|
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
|
143
|
-
|
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
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.
|
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-
|
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
|