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.
@@ -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,8 @@
1
+ # This gem is versioned with the usual X.Y.Z notation, where X.Y is the API version, and Z is the client version.
2
+ module RightApi
3
+ class Client
4
+ API_VERSION = '1.5'
5
+ CLIENT_VERSION = '1'
6
+ VERSION = "#{API_VERSION}.#{CLIENT_VERSION}"
7
+ end
8
+ 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