raca 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
|