right_api_client 1.5.1
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.
- data/.gitignore +13 -0
- data/CHANGELOG.rdoc +5 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +19 -0
- data/README.rdoc +196 -0
- data/Rakefile +17 -0
- data/config/login.yml.example +25 -0
- data/lib/right_api_client.rb +1 -0
- data/lib/right_api_client/client.rb +245 -0
- data/lib/right_api_client/helper.rb +192 -0
- data/lib/right_api_client/resource.rb +61 -0
- data/lib/right_api_client/resource_detail.rb +115 -0
- data/lib/right_api_client/resources.rb +51 -0
- data/lib/right_api_client/version.rb +8 -0
- data/login_to_client_irb.rb +15 -0
- data/right_api_client.gemspec +31 -0
- data/right_api_client.rconf +10 -0
- data/spec/client_spec.rb +60 -0
- data/spec/instance_facing_spec.rb +25 -0
- data/spec/resource_detail_spec.rb +59 -0
- data/spec/resource_spec.rb +25 -0
- data/spec/resources_spec.rb +25 -0
- data/spec/spec_helper.rb +41 -0
- metadata +181 -0
@@ -0,0 +1,192 @@
|
|
1
|
+
# Methods shared by the Client, Resource and Resources.
|
2
|
+
module RightApi::Helper
|
3
|
+
# Some resource_types are not the same as the last thing in the URL, put these here to ensure consistency
|
4
|
+
INCONSISTENT_RESOURCE_TYPES = {
|
5
|
+
'current_instance' => 'instance',
|
6
|
+
'data' => 'monitoring_metric_data',
|
7
|
+
'setting' => 'multi_cloud_image_setting'
|
8
|
+
}
|
9
|
+
|
10
|
+
# The API does not provide information about the basic actions that can be
|
11
|
+
# performed on a resource so define them here:
|
12
|
+
RESOURCE_ACTIONS = {
|
13
|
+
:create => ['deployments', 'server_arrays', 'servers', 'ssh_keys', 'volumes', 'volume_snapshots', 'volume_attachments', 'backups'],
|
14
|
+
:destroy => ['deployment', 'server_array', 'server', 'ssh_key', 'volume', 'volume_snapshot', 'volume_attachment', 'backup'],
|
15
|
+
:update => ['deployment', 'instance', 'server_array', 'server', 'backup'],
|
16
|
+
:no_index => ['tags', 'tasks', 'monitoring_metric_data'], # Easier to specify those that don't need an index call
|
17
|
+
:no_show => ['input', 'session', 'resource_tag'] # Once again, easier to define those that don't have a show call
|
18
|
+
}
|
19
|
+
|
20
|
+
# Some RightApi::Resources have methods that operate on the resource type itself
|
21
|
+
# and not on a particular one (ie: without specifying an id). Place these here:
|
22
|
+
RESOURCE_SPECIAL_ACTIONS = {
|
23
|
+
'instances' => {:multi_terminate => 'do_post', :multi_run_executable => 'do_post'},
|
24
|
+
'inputs' => {:multi_update => 'do_put'},
|
25
|
+
'tags' => {:by_tag => 'do_post', :by_resource => 'do_post', :multi_add => 'do_post', :multi_delete =>'do_post'},
|
26
|
+
'backups' => {:cleanup => 'do_post'}
|
27
|
+
}
|
28
|
+
|
29
|
+
# List of resources that are available as instance-facing calls
|
30
|
+
INSTANCE_FACING_RESOURCES = [:backups, :live_tasks, :volumes, :volume_attachments, :volume_snapshots, :volume_types]
|
31
|
+
|
32
|
+
# Helper used to add methods to classes dynamically
|
33
|
+
def define_instance_method(meth, &blk)
|
34
|
+
(class << self; self; end).module_eval do
|
35
|
+
define_method(meth, &blk)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Helper method that returns all api methods available to a client or resource
|
40
|
+
def api_methods
|
41
|
+
self.methods(false)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Helper method that creates instance methods out of the associated resources from links
|
45
|
+
# Some resources have many links with the same rel.
|
46
|
+
# We want to capture all these href in the same method, returning an array
|
47
|
+
def get_associated_resources(client, links, associations)
|
48
|
+
# First go through the links and group the rels together
|
49
|
+
rels = {}
|
50
|
+
links.each do |link|
|
51
|
+
if rels[link['rel'].to_sym] # if we have already seen this rel attribute
|
52
|
+
rels[link['rel'].to_sym] << link['href']
|
53
|
+
else
|
54
|
+
rels[link['rel'].to_sym] = [link['href']]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Note: hrefs will be an array, even if there is only one link with that rel
|
59
|
+
rels.each do |rel,hrefs|
|
60
|
+
# Add the link to the associations set if present. This is to accommodate ResourceDetail objects
|
61
|
+
associations << rel if associations != nil
|
62
|
+
|
63
|
+
# Create methods so that the link can be followed
|
64
|
+
define_instance_method(rel) do |*args|
|
65
|
+
if hrefs.size == 1 # Only one link for the specific rel attribute
|
66
|
+
if has_id(*args) || is_singular?(rel)
|
67
|
+
# User wants a single resource. Either doing a show, update, delete...
|
68
|
+
# get the resource_type
|
69
|
+
|
70
|
+
# Special case: calling .data you don't want a resources object back
|
71
|
+
# but rather all its details since you cannot do a show
|
72
|
+
return RightApi::ResourceDetail.new(client, *client.do_get(hrefs.first, *args)) if rel == :data
|
73
|
+
|
74
|
+
if is_singular?(rel)
|
75
|
+
# Then the href will be: /resource_type/:id
|
76
|
+
resource_type = get_singular(hrefs.first.split('/')[-2])
|
77
|
+
else
|
78
|
+
# Else the href will be: /resource_type
|
79
|
+
resource_type = get_singular(hrefs.first.split('/')[-1])
|
80
|
+
end
|
81
|
+
path = add_id_and_params_to_path(hrefs.first, *args)
|
82
|
+
RightApi::Resource.process(client, resource_type, path)
|
83
|
+
else
|
84
|
+
# Returns the class of this resource
|
85
|
+
resource_type = hrefs.first.split('/')[-1]
|
86
|
+
path = add_id_and_params_to_path(hrefs.first, *args)
|
87
|
+
RightApi::Resources.new(client, path, resource_type)
|
88
|
+
end
|
89
|
+
else
|
90
|
+
# There were multiple links with the same relation name
|
91
|
+
# This occurs in tags.by_resource
|
92
|
+
resources = []
|
93
|
+
if has_id(*args) || is_singular?(rel)
|
94
|
+
hrefs.each do |href|
|
95
|
+
# User wants a single resource. Either doing a show, update, delete...
|
96
|
+
if is_singular?(rel)
|
97
|
+
resource_type = get_singular(href.split('/')[-2])
|
98
|
+
else
|
99
|
+
resource_type = get_singular(href.split('/')[-1])
|
100
|
+
end
|
101
|
+
path = add_id_and_params_to_path(href, *args)
|
102
|
+
resources << RightApi::Resource.process(client, resource_type, path)
|
103
|
+
end
|
104
|
+
else
|
105
|
+
hrefs.each do |href|
|
106
|
+
# Returns the class of this resource
|
107
|
+
resource_type = href.split('/')[-1]
|
108
|
+
path = add_id_and_params_to_path(href, *args)
|
109
|
+
resources << RightApi::Resources.new(client, path, resource_type)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
# return the array of resource objects
|
113
|
+
resources
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
|
120
|
+
# Helper method that checks whether params contains a key :id
|
121
|
+
def has_id(params = {})
|
122
|
+
params.has_key?(:id)
|
123
|
+
end
|
124
|
+
|
125
|
+
# Helper method that adds filters and other parameters to the path
|
126
|
+
# Normally you would just pass a hash of query params to RestClient,
|
127
|
+
# but unfortunately it only takes them as a hash, and for filtering
|
128
|
+
# we need to pass multiple parameters with the same key. The result
|
129
|
+
# is that we have to build up the query string manually.
|
130
|
+
# This does not modify the original_path but will change the params
|
131
|
+
def add_id_and_params_to_path(original_path, params = {})
|
132
|
+
path = original_path.dup
|
133
|
+
|
134
|
+
path += "/#{params.delete(:id)}" if has_id(params)
|
135
|
+
filters = params.delete(:filter)
|
136
|
+
params_string = params.map{|k,v| "#{k.to_s}=#{CGI::escape(v.to_s)}" }.join('&')
|
137
|
+
if filters && filters.any?
|
138
|
+
path += "?filter[]=" + filters.map{|f| CGI::escape(f) }.join('&filter[]=')
|
139
|
+
path += "&#{params_string}"
|
140
|
+
else
|
141
|
+
path += "?#{params_string}"
|
142
|
+
end
|
143
|
+
|
144
|
+
# If present, remove ? and & at end of path
|
145
|
+
path.chomp!('&')
|
146
|
+
path.chomp!('?')
|
147
|
+
path
|
148
|
+
end
|
149
|
+
|
150
|
+
# Helper method that inserts the given term at the correct place in the path
|
151
|
+
# If there are parameters in the path then insert it before them.
|
152
|
+
# Will not change path.
|
153
|
+
def insert_in_path(path, term)
|
154
|
+
if path.index('?')
|
155
|
+
# sub returns a copy of path
|
156
|
+
new_path = path.sub('?', "/#{term}?")
|
157
|
+
else
|
158
|
+
new_path = "#{path}/#{term}"
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Helper method that checks whether the string is singular
|
163
|
+
def is_singular?(str)
|
164
|
+
return true if ['data'].include?(str.to_s)
|
165
|
+
(str.to_s)[-1] != 's'
|
166
|
+
end
|
167
|
+
|
168
|
+
# Does not modify links
|
169
|
+
def get_href_from_links(links)
|
170
|
+
if links
|
171
|
+
self_link = links.detect{|link| link["rel"] == "self"}
|
172
|
+
return self_link["href"] if self_link
|
173
|
+
end
|
174
|
+
return nil
|
175
|
+
end
|
176
|
+
|
177
|
+
# This will modify links
|
178
|
+
def get_and_delete_href_from_links(links)
|
179
|
+
if links
|
180
|
+
self_link = links.detect{|link| link["rel"] == "self"}
|
181
|
+
return links.delete(self_link)["href"] if self_link
|
182
|
+
end
|
183
|
+
return nil
|
184
|
+
end
|
185
|
+
|
186
|
+
# Will not change obj
|
187
|
+
def get_singular(obj)
|
188
|
+
str = obj.to_s.dup
|
189
|
+
str.chomp!('s')
|
190
|
+
str
|
191
|
+
end
|
192
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module RightApi
|
2
|
+
# Represents a Resource. This is a filler class for a single resource.
|
3
|
+
# This class dynamically adds methods and properties to instances depending on what type of resource they are.
|
4
|
+
class Resource
|
5
|
+
include Helper
|
6
|
+
|
7
|
+
# Will create a (or an array of) new Resource object(s)
|
8
|
+
# All parameters are treated as read only
|
9
|
+
def self.process(client, resource_type, path, data={})
|
10
|
+
if data.kind_of?(Array) # This is needed for the index call to return an array of all the resources
|
11
|
+
data.collect do |obj|
|
12
|
+
# Ideally all objects should have a links attribute that will have a link called 'self' which is the href.
|
13
|
+
# For exceptions like inputs, use the path itself.
|
14
|
+
obj_href = client.get_href_from_links(obj["links"]) || path
|
15
|
+
ResourceDetail.new(client, resource_type, obj_href, obj)
|
16
|
+
end
|
17
|
+
else
|
18
|
+
RightApi::Resource.new(client, resource_type, path, data)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def inspect
|
23
|
+
"#<#{self.class.name} " +
|
24
|
+
"resource_type=\"#{@resource_type}\"" +
|
25
|
+
"#{', name='+@hash["name"].inspect if @hash.has_key?("name")}" +
|
26
|
+
"#{', resource_uid='+@hash["resource_uid"].inspect if @hash.has_key?("resource_uid")}>"
|
27
|
+
end
|
28
|
+
|
29
|
+
# Hash is only used for index calls so we can parse out the name and resource_uid for the inspect call
|
30
|
+
# All parameters are treated as read only
|
31
|
+
def initialize(client, resource_type, href, hash={})
|
32
|
+
if INCONSISTENT_RESOURCE_TYPES.has_key?(resource_type)
|
33
|
+
resource_type = INCONSISTENT_RESOURCE_TYPES[resource_type]
|
34
|
+
end
|
35
|
+
# For the inspect function:
|
36
|
+
@resource_type = resource_type
|
37
|
+
@hash = hash
|
38
|
+
|
39
|
+
# Add destroy method to relevant resources
|
40
|
+
if Helper::RESOURCE_ACTIONS[:destroy].include?(resource_type)
|
41
|
+
define_instance_method('destroy') do
|
42
|
+
client.do_delete(href)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Add update method to relevant resources
|
47
|
+
if Helper::RESOURCE_ACTIONS[:update].include?(resource_type)
|
48
|
+
define_instance_method('update') do |*args|
|
49
|
+
client.do_put(href, *args)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Add show method to relevant resources
|
54
|
+
if !Helper::RESOURCE_ACTIONS[:no_show].include?(resource_type)
|
55
|
+
define_instance_method('show') do |*args|
|
56
|
+
RightApi::ResourceDetail.new(client, *client.do_get(href, *args))
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
module RightApi
|
2
|
+
# Takes the information returned from the API and converts it into instance methods
|
3
|
+
class ResourceDetail
|
4
|
+
include Helper
|
5
|
+
attr_reader :client, :attributes, :associations, :actions, :raw, :resource_type
|
6
|
+
|
7
|
+
def inspect
|
8
|
+
"#<#{self.class.name} " +
|
9
|
+
"resource_type=\"#{@resource_type}\"" +
|
10
|
+
"#{', name=' + name.inspect if self.respond_to?(:name)}" +
|
11
|
+
"#{', resource_uid='+ resource_uid.inspect if self.respond_to?(:resource_uid)}>"
|
12
|
+
end
|
13
|
+
|
14
|
+
# ResourceDetail will MODIFY hash
|
15
|
+
def initialize(client, resource_type, href, hash)
|
16
|
+
@client = client
|
17
|
+
@resource_type = resource_type
|
18
|
+
@raw = hash.dup
|
19
|
+
@attributes, @associations, @actions = Set.new, Set.new, Set.new
|
20
|
+
|
21
|
+
links = hash.delete('links') || []
|
22
|
+
raw_actions = hash.delete('actions') || []
|
23
|
+
|
24
|
+
# We have to delete the self href from the links because later we will
|
25
|
+
# go through these links and add them in as methods
|
26
|
+
self_hash = get_and_delete_href_from_links(links)
|
27
|
+
if self_hash != nil
|
28
|
+
hash['href'] = self_hash
|
29
|
+
end
|
30
|
+
|
31
|
+
# Add links to attributes set and create a method that returns the links
|
32
|
+
attributes << :links
|
33
|
+
define_instance_method(:links) { return links }
|
34
|
+
|
35
|
+
# Follow the actions:
|
36
|
+
# API doesn't tell us whether a resource action is a GET or a POST, but
|
37
|
+
# they are all post so add them all as posts for now.
|
38
|
+
raw_actions.each do |action|
|
39
|
+
action_name = action['rel']
|
40
|
+
# Add it to the actions set
|
41
|
+
actions << action_name.to_sym
|
42
|
+
|
43
|
+
define_instance_method(action_name.to_sym) do |*args|
|
44
|
+
action_href = hash['href'] + "/" + action['rel']
|
45
|
+
client.do_post(action_href, *args)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Follow the links to create methods
|
50
|
+
get_associated_resources(client, links, associations)
|
51
|
+
|
52
|
+
# Some resources are not linked together, so they have to be manually
|
53
|
+
# added here.
|
54
|
+
case resource_type
|
55
|
+
when 'instance'
|
56
|
+
define_instance_method('live_tasks') do |*args|
|
57
|
+
if has_id(*args)
|
58
|
+
path = href + '/live/tasks'
|
59
|
+
path = add_id_and_params_to_path(path, *args)
|
60
|
+
RightApi::Resource.process(client, 'live_task', path)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Add the rest as instance methods
|
66
|
+
hash.each do |k, v|
|
67
|
+
# If a parent resource is requested with a view then it might return
|
68
|
+
# extra data that can be used to build child resources here, without
|
69
|
+
# doing another get request.
|
70
|
+
if associations.include?(k.to_sym)
|
71
|
+
# v might be an array or hash so use include rather than has_key
|
72
|
+
if v.include?('links')
|
73
|
+
child_self_link = v['links'].find { |target| target['rel'] == 'self' }
|
74
|
+
if child_self_link
|
75
|
+
child_href = child_self_link['href']
|
76
|
+
if child_href
|
77
|
+
# Currently, only instances need this optimization, but in the
|
78
|
+
# future we might like to extract resource_type from child_href
|
79
|
+
# and not hard-code it.
|
80
|
+
if child_href.index('instance')
|
81
|
+
define_instance_method(k) { RightApi::Resource.process(client, 'instance', child_href, v) }
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
else
|
87
|
+
# Add it to the attributes set and create a method for it
|
88
|
+
attributes << k.to_sym
|
89
|
+
define_instance_method(k) { return v }
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Add destroy method to relevant resources
|
94
|
+
if Helper::RESOURCE_ACTIONS[:destroy].include?(resource_type)
|
95
|
+
define_instance_method('destroy') do
|
96
|
+
client.do_delete(href)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Add update method to relevant resources
|
101
|
+
if Helper::RESOURCE_ACTIONS[:update].include?(resource_type)
|
102
|
+
define_instance_method('update') do |*args|
|
103
|
+
client.do_put(href, *args)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Add show method to relevant resources
|
108
|
+
if !Helper::RESOURCE_ACTIONS[:no_show].include?(resource_type)
|
109
|
+
define_instance_method('show') do |*args|
|
110
|
+
self
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module RightApi
|
2
|
+
# This class defines the different resource types and the methods that one can call on them
|
3
|
+
# This class dynamically adds methods and properties to instances depending on what type of resource they are.
|
4
|
+
# This is a filler class so that we don't always have to do an index before anything else
|
5
|
+
# This class gets instantiated when the user calls (for example) client.clouds ... (ie. when you want the generic class: no id present)
|
6
|
+
class Resources
|
7
|
+
include Helper
|
8
|
+
|
9
|
+
def inspect
|
10
|
+
"#<#{self.class.name} " +
|
11
|
+
"resource_type=\"#{@resource_type}\">"
|
12
|
+
end
|
13
|
+
|
14
|
+
# Since this is just a filler class, only define instance methods and the method api_methods()
|
15
|
+
# Resource_type should always be plural.
|
16
|
+
# All parameters are treated as read only
|
17
|
+
def initialize(client, path, resource_type)
|
18
|
+
if INCONSISTENT_RESOURCE_TYPES.has_key?(get_singular(resource_type))
|
19
|
+
resource_type = INCONSISTENT_RESOURCE_TYPES[get_singular(resource_type)] + 's'
|
20
|
+
end
|
21
|
+
@resource_type = resource_type
|
22
|
+
# Add create methods for the relevant root RightApi::Resources
|
23
|
+
if Helper::RESOURCE_ACTIONS[:create].include?(resource_type)
|
24
|
+
self.define_instance_method('create') do |*args|
|
25
|
+
client.do_post(path, *args)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Add in index methods for the relevant root RightApi::Resources
|
30
|
+
if !Helper::RESOURCE_ACTIONS[:no_index].include?(resource_type)
|
31
|
+
self.define_instance_method('index') do |*args|
|
32
|
+
# Session uses .index like a .show (so need to treat it as a special case)
|
33
|
+
if resource_type == 'session'
|
34
|
+
ResourceDetail.new(client, *client.do_get(path, *args))
|
35
|
+
else
|
36
|
+
RightApi::Resource.process(client, *client.do_get(path, *args))
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Adding in special cases
|
42
|
+
Helper::RESOURCE_SPECIAL_ACTIONS[resource_type].each do |meth, action|
|
43
|
+
# Insert_in_path will NOT modify path
|
44
|
+
action_path = insert_in_path(path, meth)
|
45
|
+
self.define_instance_method(meth) do |*args|
|
46
|
+
client.send action, action_path, *args
|
47
|
+
end
|
48
|
+
end if Helper::RESOURCE_SPECIAL_ACTIONS[resource_type]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# A quick way to login to the API and jump into IRB so you can experiment with the client.
|
2
|
+
# Add this to your bash profile to make it simpler:
|
3
|
+
# alias client='bundle exec ruby login_to_client_irb.rb'
|
4
|
+
|
5
|
+
require File.expand_path('../lib/right_api_client', __FILE__)
|
6
|
+
require 'yaml'
|
7
|
+
require 'irb'
|
8
|
+
|
9
|
+
begin
|
10
|
+
@client = RightApi::Client.new(YAML.load_file(File.expand_path('../config/login.yml', __FILE__)))
|
11
|
+
puts "logged-in to the API, use the '@client' variable to use the client, e.g. '@client.session.index.message' will output:"
|
12
|
+
puts @client.session.index.message
|
13
|
+
end
|
14
|
+
|
15
|
+
IRB.start
|