raca 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.markdown +28 -3
- data/Rakefile +14 -1
- data/lib/raca/account.rb +66 -10
- data/lib/raca/container.rb +68 -26
- data/lib/raca/containers.rb +32 -4
- data/lib/raca/errors.rb +17 -0
- data/lib/raca/server.rb +31 -129
- data/lib/raca/servers.rb +186 -1
- data/lib/raca.rb +2 -0
- metadata +18 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0f887cfa6d624450eb2805122c998cc29acc0b01
|
4
|
+
data.tar.gz: 89109fe3d3b728239a0ab9795d24508b96bff2ae
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
117
|
-
|
118
|
-
|
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
|
-
#
|
7
|
-
#
|
8
|
-
#
|
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(
|
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?
|
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(
|
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
|
174
|
+
def identity_data
|
119
175
|
refresh_cache unless cache_read(cache_key)
|
120
176
|
|
121
177
|
cache_read(cache_key) || {}
|
data/lib/raca/container.rb
CHANGED
@@ -4,13 +4,18 @@ require 'openssl'
|
|
4
4
|
require 'uri'
|
5
5
|
|
6
6
|
module Raca
|
7
|
-
|
8
|
-
#
|
9
|
-
#
|
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
|
-
|
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
|
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
|
-
|
211
|
-
segments << {path: "#{@container_name}/#{segment_key}", etag:
|
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
|
263
|
+
raise Raca::TimeoutError, "Timeout from Rackspace while trying #{request.class} to #{request.path}"
|
234
264
|
end
|
235
265
|
|
236
|
-
|
237
|
-
|
238
|
-
|
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
|
-
|
257
|
-
|
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
|
353
|
+
def md5_io(io)
|
354
|
+
io.seek(0)
|
312
355
|
digest = Digest::MD5.new
|
313
|
-
|
314
|
-
|
315
|
-
|
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
|
data/lib/raca/containers.rb
CHANGED
@@ -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
|
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
|
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
|
-
|
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
|
-
|
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
|
data/lib/raca/errors.rb
ADDED
@@ -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
|
-
#
|
7
|
-
#
|
8
|
-
#
|
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 :
|
14
|
+
attr_reader :server_id
|
12
15
|
|
13
|
-
def initialize(account, region,
|
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
|
-
@
|
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
|
143
|
-
@
|
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
|
-
|
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
|
209
|
-
|
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
|
-
|
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
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.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-
|
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.
|
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
|