rackspace 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 5f21137370f518602031bc7bc0c3ec8983188ff1
4
+ data.tar.gz: 744d67b457cb92494713f485d7693bfdab7dbfa3
5
+ SHA512:
6
+ metadata.gz: 9faeabd8841aeed72d33d70b11777ae185f1c77d26ed92978d07491669597a244c1578d50cdf77e1426a205bb526b01555f138ee680207d91a082c84437560e3
7
+ data.tar.gz: 71eb99c1571bcaf16338f347f560b7eb251a392ffe6fa52b5e99eaac9830db54bf8cfbf26ee832c7eb108bd71eb8ffd53365bbca7adc5d307cd0095a1bd266c0
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ *.gem
data/README.md ADDED
@@ -0,0 +1,39 @@
1
+ rackspace
2
+ =========
3
+
4
+ (A reconstruction of the rackspace gem that was deleted from github).
5
+
6
+ A simple ruby interface to various Rackspace Cloud APIs.
7
+ Currently, the only implemented API is the Cloud Load Balancer API, but
8
+ others can be implemented very easily.
9
+
10
+ Load Balancers
11
+ ==============
12
+
13
+ Using the API is fairly simple, and the methods are largely self-documenting:
14
+
15
+ ```ruby
16
+ #!/usr/bin/env ruby
17
+ require 'rackspace'
18
+ require 'pp'
19
+
20
+ lbs = Rackspace::LoadBalancers.new()
21
+ lbs.authenticate({
22
+ :username => 'user',
23
+ :key => 'api-key-here'
24
+ })
25
+
26
+ # Pretty-print a list of load balancer names
27
+ puts "Load balancers:\n"
28
+ pp list = lbs.list()
29
+
30
+ # ... and the list of nodes
31
+ puts "Nodes on #{list[0]}:\n"
32
+ pp lbs.list_nodes(list[0])
33
+ ```
34
+
35
+ TODO
36
+ ====
37
+
38
+ * Implement other APIs/features
39
+ * Better documentation
data/lib/rackspace.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'rackspace/rackspace'
2
+ require 'rackspace/loadbalancers'
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ #
4
+ # Interface to Rackspace's Cloud Load Balancer API
5
+ #
6
+ # TODO: SSL Termination, Virtual IPs, ACLs, Usage Reports, Health Monitors,
7
+ # Connection Persistence/Logging/Throttling, Content Caching
8
+ #
9
+ class Rackspace::LoadBalancers < Rackspace
10
+ def initialize
11
+ super
12
+ @ltype = 'loadBalancers' # for list()
13
+ @service = 'cloudLoadBalancers' # from @@auth_config
14
+ @base_uri = '/loadbalancers' # for Rackspace::get/put/etc.
15
+ @metadata = {}
16
+ end
17
+
18
+ # Get load balancer details
19
+ def get_details(name)
20
+ if @x_list[name][@ltype.chop]
21
+ # Get metadata for this lb
22
+ md = get(name,'metadata')
23
+ @metadata[name] = { 'lb' => md[md.keys[0]] }
24
+ end
25
+
26
+ @x_list[name][@ltype.chop]
27
+ end
28
+
29
+ # Create a load balancer
30
+ def create(attributes)
31
+ post(attributes)
32
+ end
33
+
34
+ # Update load balancer attributes
35
+ def update(name,attributes)
36
+ allowed_attribs = [ 'name', 'algorithm', 'protocol', 'port' ]
37
+ attributes.keys.each { |k| attributes.delete(k) unless allowed_attribs.index(k) }
38
+ put({ @ltype.chop => attributes },name)
39
+ end
40
+
41
+ # Get load balancer stats
42
+ def stats(name)
43
+ get(name,'stats')
44
+ end
45
+
46
+ # Get Allowed Domains
47
+ def get_allowed_domains
48
+ out = []
49
+ ad = get(nil,'alloweddomains')
50
+ ad[ad.keys[0]].each { |x| out.push(x['allowedDomain']['name']) }
51
+ return out
52
+ end
53
+
54
+ # List nodes
55
+ def list_nodes(name)
56
+ @metadata[name] = { 'lb' => {} } unless @metadata[name]
57
+ @metadata[name]['nodes'] = {}
58
+ nodes = get_subitems(name,'nodes','address')
59
+ nodes.each { |node|
60
+ # Get metadata for this node
61
+ md = get(name,'nodes/' + @s2id[name]['nodes'][node['address']] + '/metadata')
62
+ @metadata[name]['nodes'][node['address']] = md[md.keys[0]]
63
+ }
64
+ end
65
+
66
+ # Add nodes
67
+ def add_nodes(name,nodes)
68
+ nodes.each { |node|
69
+ raise 'You must specify an address and a port when adding a node' unless node[:address] && node[:port]
70
+ node[:condition] = 'ENABLED' unless node[:condition]
71
+ node[:type] = 'PRIMARY' unless node[:type]
72
+ }
73
+
74
+ create_subitems(name,'nodes',nodes)
75
+ end
76
+
77
+ # Modify nodes
78
+ def update_nodes(name,nodes)
79
+ update_subitems(name,'nodes',[ 'condition', 'weight', 'type' ],nodes)
80
+ end
81
+
82
+ # Delete nodes
83
+ def delete_nodes(name,nodes)
84
+ delete_subitems(name,'nodes',nodes)
85
+ end
86
+
87
+ # Get error page
88
+ def get_error_page(name)
89
+ get(name,'errorpage')['content']
90
+ end
91
+
92
+ # Set error page
93
+ def set_error_page(name,content)
94
+ put({ :content => content },name,'errorpage')
95
+ end
96
+
97
+ # Delete error page
98
+ def delete_error_page(name)
99
+ delete(name,'errorpage')
100
+ end
101
+
102
+ # List Virtual IPs
103
+ def list_vips(name)
104
+ get_subitems(name,'virtualIps','address')
105
+ end
106
+
107
+ # Add Virtual IPs
108
+ def add_vips(name,vips)
109
+ vips.each { |vip|
110
+ raise 'You must specify the ipVersion and type when adding a virtual IP' unless vip[:ipVersion] && vip[:type]
111
+ }
112
+
113
+ create_subitems(name,'virtualIps',vips)
114
+ end
115
+
116
+ # Delete Virtual IPs
117
+ def delete_vips(name,vips)
118
+ delete_subitems(name,'virtualIps',vips)
119
+ end
120
+
121
+ # Get metadata
122
+ def get_metadata(name,node=nil)
123
+ raise "No such node: #{node} on #{name}" unless !node || @s2id[name]['nodes'][node]
124
+ mda = node ? @metadata[name]['nodes'][node] : @metadata[name]['lb']
125
+ out = {}
126
+ mda.each { |md| out[md['key']] = md['value'] }
127
+ return out
128
+ end
129
+
130
+ # Add metadata
131
+ def add_metadata(name,values,node=nil)
132
+ raise "No such node: #{node} on #{name}" unless !node || @s2id[name]['nodes'][node]
133
+ resp = post({ 'metadata' => values },name,(node ? 'nodes/' + @s2id[name]['nodes'][node] + '/' : '') + 'metadata')
134
+ resp['metadata'].each { |md|
135
+ if node
136
+ @metadata[name]['nodes'][node].push(md)
137
+ else
138
+ @metadata[name]['lb'].push(md)
139
+ end
140
+ }
141
+ end
142
+
143
+ # Update metadata
144
+ def update_metadata(name,values,node=nil)
145
+ raise "No such node: #{node} on #{name}" unless !node || @s2id[name]['nodes'][node]
146
+ mda = (node ? @metadata[name]['nodes'][node] : @metadata[name]['lb'])
147
+
148
+ values.keys.each { |k|
149
+ md = nil
150
+ mda.each { |mdx| md = mdx if mdx['key'] == k }
151
+ if md
152
+ put({ 'meta' => { 'value' => values[k] }},name,
153
+ (node ? 'nodes/' + @s2id[name]['nodes'][node] + '/' : '') + 'metadata/' + md['id'].to_s)
154
+ md['value'] = values[k]
155
+ end
156
+ }
157
+ end
158
+
159
+ # Delete metadata
160
+ def delete_metadata(name,key,node=nil)
161
+ raise "No such node: #{node} on #{name}" unless !node || @s2id[name]['nodes'][node]
162
+ md = nil
163
+ mda = (node ? @metadata[name]['nodes'][node] : @metadata[name]['lb'])
164
+ mda.each { |mdx| md = mdx if mdx['key'] == key }
165
+ if md
166
+ delete(name,(node ? 'nodes/' + @s2id[name]['nodes'][node] + '/' : '') + 'metadata/' + md['id'].to_s)
167
+ mda.delete(md)
168
+ end
169
+ end
170
+ end
171
+
172
+ # vi:set ts=2:
@@ -0,0 +1,307 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+ require 'json'
6
+
7
+ #
8
+ # Base class for Rackspace Cloud API Services
9
+ #
10
+ # This class handles authentication, and all basic
11
+ # service features (GET,PUT,DELETE) transparently.
12
+ #
13
+ class Rackspace
14
+ def initialize
15
+ @@credentials = {}
16
+ @@authenticated = 0
17
+ @@auth_config = {}
18
+ @ltype = nil
19
+ @service = nil
20
+ @base_uri = nil
21
+ @region = 'ORD'
22
+ @name2id = {}
23
+ @x_list = {}
24
+ @s2id = {} # Subitem to ID
25
+ @nfxx = {} # name field lookup
26
+ end
27
+
28
+ # Get/Set region
29
+ def region(region=nil)
30
+ @region = (region ? region : @region)
31
+ return @region
32
+ end
33
+
34
+ # Do an actual request, returns a hash representing the JSON result (or do custom processing)
35
+ def do_req(verb,url,body=nil,fun=nil)
36
+ raise 'This method shouldn\'t be called directly' unless (@ltype && @service && @base_uri)
37
+
38
+ # We don't need to do this when authenticating (obviously)
39
+ unless url.index('v1.1/auth')
40
+ raise 'You must call authenticate() before anything else' unless @@authenticated
41
+
42
+ # Get the service map entries for this service
43
+ raise 'No service map for service "' + @service + '"' unless cfg = @@auth_config['auth']['serviceCatalog'][@service]
44
+
45
+ # Get the service URL based on region (TODO: handle snet)
46
+ av_regions = []
47
+ regioned = false
48
+ service_url = nil
49
+ cfg.each { |x|
50
+ if (x['region'])
51
+ av_regions.push(x['region'])
52
+ regioned = true
53
+ service_url = (x['region'] == @region) ? x['publicURL'] : service_url
54
+ elsif (x['publicURL'])
55
+ service_url = x['publicURL']
56
+ end
57
+ }
58
+
59
+ # Make sure region is valid (if applicable)
60
+ raise 'Invalid region "' + @region + '" for service "' + @service +
61
+ '" available: [' + av_regions.join(',') + "]" if !service_url && regioned
62
+ raise 'Unable to determine service_url for service "' + @service + '"' if !service_url
63
+ else
64
+ service_url = ''
65
+ end
66
+
67
+ # Extract info from the url
68
+ uri = URI(service_url + url)
69
+ http = Net::HTTP.new(uri.host,uri.port)
70
+ http.use_ssl = true if uri.scheme == 'https'
71
+
72
+ # Figure the type of request to make
73
+ case verb
74
+ when 'GET'
75
+ req = Net::HTTP::Get.new(uri.path)
76
+ when 'POST'
77
+ raise "POST requires a body" unless body
78
+ req = Net::HTTP::Post.new(uri.path)
79
+ when 'PUT'
80
+ raise "PUT requires a body" unless body
81
+ req = Net::HTTP::Put.new(uri.path)
82
+ when 'DELETE'
83
+ req = Net::HTTP::Delete.new(uri.path)
84
+ else
85
+ raise 'Invalid HTTP verb: "' + verb + '"'
86
+ end
87
+
88
+ # Add Accept
89
+ req.add_field('Accept','application/json')
90
+
91
+ # Add the body
92
+ req.content_type = 'application/json'
93
+ req.body = body
94
+
95
+ # Do it (and handle HTTP error responses)
96
+ fun = fun ? fun : proc { |resp| JSON.parse(resp.body) }
97
+
98
+ begin
99
+ # Delete and add the token (in case of 'retry')
100
+ req.delete('X-Auth-Token')
101
+ req.add_field('X-Auth-Token',@@auth_config['auth']['token']['id']) if @@authenticated && @@auth_config && !@@auth_config.empty?
102
+
103
+ # Send the request, get the response
104
+ resp = http.request(req)
105
+
106
+ case resp.code
107
+ when '200', '201', '202'
108
+ fun.call(resp)
109
+ when '400'
110
+ raise 'Bad Request: ' + resp.body
111
+ when '401'
112
+ bod = JSON.parse(resp.body)
113
+ raise Rackspace::ExpiredToken, bod if @@authenticated && @@auth_config && !@@auth_config.empty?
114
+ raise 'Unauthorized: ' + (bod.empty? ? resp.body : (bod['unauthorized'] ? bod['unauthorized']['message'] : bod['message']))
115
+ when '404'
116
+ raise 'Resource not found'
117
+ when '413'
118
+ raise 'Don\'t be a butt-knocker. You\'re sending too many requests.'
119
+ when '422'
120
+ bod = JSON.parse(resp.body)
121
+ raise Rackspace::Retry, bod['message'] if /considered immutable/.match(bod['message'])
122
+ raise bod['message']
123
+ when '500'
124
+ raise 'Internal Error: ' + resp.body
125
+ when '503'
126
+ raise 'Service Unavailable: ' + resp.body
127
+ else
128
+ raise 'Unknown Response: [' + resp.code + '] ' + resp.body
129
+ end
130
+ rescue Rackspace::ExpiredToken
131
+ authenticate(@@credentials)
132
+ retry
133
+ rescue Rackspace::Retry
134
+ sleep(5)
135
+ retry
136
+ end
137
+ end
138
+
139
+ #
140
+ # Authenticate to Rackspace (v1.1)
141
+ #
142
+ # credentials is a hash of:
143
+ # {
144
+ # :username => 'username',
145
+ # :key => 'api_key'
146
+ # }
147
+ #
148
+ # uk should be 1 for UK-based accounts
149
+ #
150
+ def authenticate(credentials, uk=0)
151
+ # Check to make sure we have all our credentials
152
+ raise 'Missing :username' unless credentials[:username]
153
+ raise 'Missing :key' unless credentials[:key]
154
+ creds = {'credentials' => credentials}.to_json
155
+ @@credentials = credentials
156
+
157
+ # Post, and handle the response
158
+ do_req('POST','https://' + ((uk == 1) ? 'lon.' : '') + 'identity.api.rackspacecloud.com/v1.1/auth',creds,proc { |resp|
159
+ @@auth_config = JSON.parse(resp.body)
160
+ @@authenticated = 1
161
+ })
162
+ end
163
+
164
+ # List all 'things' of the given type
165
+ def list
166
+ raise 'This method shouldn\'t be called directly' unless (@ltype && @service && @base_uri)
167
+ raise 'You must call authenticate() before anything else' unless @@authenticated
168
+ @x_list = {}
169
+
170
+ do_req('GET',@base_uri,nil,proc { |resp|
171
+ JSON.parse(resp.body)[@ltype].each { |z|
172
+ @name2id[z['name']] = z['id'].to_s
173
+ @x_list[z['name']] = get(z['name'])
174
+ }
175
+ })
176
+
177
+ return @x_list.keys
178
+ end
179
+
180
+ # Get one/all resource(s)
181
+ def get(name=nil,sub_uri=nil)
182
+ # Otherwise, assume the user knows what they want
183
+ raise 'Rackspace::get: ' + @ltype + '/' + name + ' doesn\'t exist' unless !name || (name && @name2id[name])
184
+ uri = @base_uri
185
+ uri = uri + '/' + @name2id[name] if name
186
+ uri = uri + '/' + sub_uri if sub_uri
187
+ do_req('GET',uri)
188
+ end
189
+
190
+ #
191
+ # Create one resource
192
+ #
193
+ # If name is specified, it is assumed that a sub-object is intended
194
+ # If sub_uri is specified it is assumed to be a URI relative to name
195
+ #
196
+ # This update the name -> id mapping for the main resource
197
+ # sub-resources should be handled by the actual API implementation.
198
+ #
199
+ def post(attributes,name=nil,sub_uri=nil)
200
+ raise 'Rackspace::post: name must be specified' unless (name || name = attributes['name'])
201
+ raise 'Rackspace::post: attributes must be specified' unless attributes
202
+ uri = @base_uri
203
+ uri = uri + '/' + @name2id[name] if name
204
+ uri = uri + '/' + sub_uri if sub_uri
205
+ do_req('POST',uri,attributes.to_json,proc { |resp|
206
+ data = JSON.parse(resp.body)
207
+ unless sub_uri
208
+ @x_list[name] = data
209
+ @name2id[name] = @x_list[name][@ltype]['id']
210
+ end
211
+
212
+ return data
213
+ })
214
+ end
215
+
216
+ # Modify one resource
217
+ def put(attributes,name,sub_uri=nil)
218
+ raise 'Rackspace::put: name must be specified' unless name
219
+ raise 'Rackspace::put: attributes must be specified' unless attributes && attributes[attributes.keys[0]]
220
+ raise 'Rackspace::put: ' + @ltype + '/' + name + ' doesn\'t exist' unless @name2id[name]
221
+ uri = @base_uri + '/' + @name2id[name]
222
+ uri = uri + '/' + sub_uri if sub_uri
223
+ do_req('PUT',uri,attributes.to_json,proc { |resp|
224
+ # Handle name changes
225
+ attributes = attributes[attributes.keys[0]]
226
+ if (!sub_uri && attributes['name'] && attributes['name'] != name)
227
+ @x_list[attributes['name']] = @x_list[name]
228
+ @name2id[attributes['name']] = @name2id[name]
229
+ @name2id.delete(name)
230
+ @x_list.delete(name)
231
+ name = attributes['name']
232
+ end
233
+
234
+ # Update attributes
235
+ attributes.keys.each { |k| @x_list[name][@ltype.chop][k] = attributes[k] } unless sub_uri
236
+ })
237
+ end
238
+
239
+ # Delete one resource
240
+ def delete(name,sub_uri=nil)
241
+ raise 'Rackspace::delete: ' + @ltype + '/' + name + ' doesn\'t exist' unless (name && @name2id[name])
242
+ uri = @base_uri + '/' + @name2id[name]
243
+ uri = uri + '/' + sub_uri if sub_uri
244
+ do_req('DELETE',uri,nil,proc { |resp|
245
+ unless sub_uri
246
+ @name2id.delete(name)
247
+ @x_list.delete(name)
248
+ end
249
+ })
250
+ end
251
+
252
+ # Get subitems
253
+ def get_subitems(name,type,namefield)
254
+ @s2id[name] = { type => {} }
255
+ @nfxx[type] = namefield unless @nfxx[type]
256
+ items = @x_list[name][@ltype.chop][type]
257
+ items.each { |item| @s2id[name][type][item[namefield]] = item['id'].to_s }
258
+ return items
259
+ end
260
+
261
+ # Create subitems
262
+ def create_subitems(name,type,attributes)
263
+ resp = post({ type => attributes },name,type.downcase)[type]
264
+ resp.each { |item|
265
+ @s2id[name][type][item[@nfxx[type]]] = item['id'].to_s
266
+ @x_list[name][@ltype.chop][type].push(item)
267
+ }
268
+ return resp
269
+ end
270
+
271
+ # Update subitems
272
+ def update_subitems(name,type,allowed_attribs,items)
273
+ items.keys.each { |item|
274
+ raise "No such #{type.chop}: #{item} on #{name}" unless @s2id[name][type][item]
275
+ items[item].keys.each { |k| items[item].delete(k) unless allowed_attribs.index(k) }
276
+ put({ type.chop => items[item] },name,type.downcase + '/' + @s2id[name][type][item])
277
+
278
+ # Update the local copy
279
+ @x_list[name][@ltype.chop][type].each { |xitem|
280
+ if xitem[@nfxx[type]] == item
281
+ items[item].keys.each { |k| xitem[k] = items[item][k] if xitem[k] && items[item][k] }
282
+ end
283
+ }
284
+ }
285
+ end
286
+
287
+ # Delete subitems
288
+ def delete_subitems(name,type,items)
289
+ items.each { |item|
290
+ if @s2id[name][type][item]
291
+ delete(name,type.downcase + '/' + @s2id[name][type][item])
292
+ @s2id[name][type].delete(item)
293
+ @x_list[name][@ltype.chop][type].each { |xitem|
294
+ @x_list[name][@ltype.chop][type].delete(xitem) if xitem[@nfxx[type]] == item
295
+ }
296
+ end
297
+ }
298
+ end
299
+ end
300
+
301
+ class Rackspace::ExpiredToken < Exception
302
+ end
303
+
304
+ class Rackspace::Retry < Exception
305
+ end
306
+
307
+ # vi:set ts=2:
data/rackspace.gemspec ADDED
@@ -0,0 +1,18 @@
1
+ # coding: utf-8
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "rackspace"
5
+ spec.version = "0.1.2"
6
+ spec.authors = ["Tim Hentenaar", "Richard Grainger"]
7
+ spec.email = ["grainger@gmail.com"]
8
+
9
+ spec.summary = "Simple interface to Rackspace's Cloud APIs"
10
+ spec.description = "\tRuby interface to various Rackspace Cloud APIs.\n\n\tCurrently only the Cloud Load Balancers API is supported. See the README for more\n\tdetails.\n"
11
+ spec.homepage = "https://gitlab.com/harbottle/rackspace"
12
+
13
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
14
+ spec.require_paths << "lib"
15
+
16
+ spec.add_dependency("json", ">=0")
17
+
18
+ end
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rackspace
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - Tim Hentenaar
8
+ - Richard Grainger
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2018-04-20 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: json
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
28
+ description: "\tRuby interface to various Rackspace Cloud APIs.\n\n\tCurrently only
29
+ the Cloud Load Balancers API is supported. See the README for more\n\tdetails.\n"
30
+ email:
31
+ - grainger@gmail.com
32
+ executables: []
33
+ extensions: []
34
+ extra_rdoc_files: []
35
+ files:
36
+ - ".gitignore"
37
+ - README.md
38
+ - lib/rackspace.rb
39
+ - lib/rackspace/loadbalancers.rb
40
+ - lib/rackspace/rackspace.rb
41
+ - rackspace.gemspec
42
+ homepage: https://gitlab.com/harbottle/rackspace
43
+ licenses: []
44
+ metadata: {}
45
+ post_install_message:
46
+ rdoc_options: []
47
+ require_paths:
48
+ - lib
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ requirements: []
61
+ rubyforge_project:
62
+ rubygems_version: 2.6.14
63
+ signing_key:
64
+ specification_version: 4
65
+ summary: Simple interface to Rackspace's Cloud APIs
66
+ test_files: []