right_api_client 1.5.9 → 1.5.12

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -10,4 +10,3 @@ nbproject
10
10
  coverage/*
11
11
  doc/*
12
12
  *~
13
- Gemfile.lock
@@ -1,4 +1,17 @@
1
- = right_api_client - CHANGELOG.rdoc
1
+ = right_api_client - CHANGELOG.md
2
+
3
+ == right_api_client - 1.5.11
4
+ #33 fix specs and readme
5
+ #32 fix nested array of hashes params
6
+ #30 README markdown formatting
7
+ #29 rename read me and add mandatory ownership string
8
+
9
+ == right_api_client - 1.5.10
10
+ #28 Preserve the last request made and make it accessible
11
+ #26 Fix specs. Separate out unit tests and functional tests.
12
+ #25 Add support for other RightScale endpoints.
13
+ #23 Fix child_account update href.
14
+ #20 Always store the latest cookies. Also includes a jump from rspec 1.3.0 to 2.9.0 and spec infrastructure reorganization.
2
15
 
3
16
  == right_api_client - 1.5.9
4
17
  Downgrade even further to Ruby 1.8.7. Should still work in Ruby 1.9.x.
data/Gemfile CHANGED
@@ -1,3 +1,3 @@
1
- source 'http://rubygems.org'
1
+ source 'https://rubygems.org'
2
2
 
3
3
  gemspec
@@ -1,40 +1,46 @@
1
- = RightScale API Client
1
+ # RightScale API Client
2
2
 
3
- The right_api_client gem simplifies the use of RightScale's MultiCloud API. It provides
3
+ The right\_api\_client gem simplifies the use of RightScale's MultiCloud API. It provides
4
4
  a simple object model of the API resources, and handles all of the fine details involved
5
5
  in making HTTP calls and translating their responses.
6
6
  It is assumed that users are already familiar with the RightScale API:
7
- - API Documentation: http://support.rightscale.com/12-Guides/RightScale_API_1.5
8
- - API Reference Docs: http://support.rightscale.com/api1.5
9
7
 
10
- == Installation
8
+ - API Documentation: http://support.rightscale.com/12-Guides/RightScale_API_1.5
9
+ - API Reference Docs: http://reference.rightscale.com/api1.5
10
+
11
+ Maintained by the RightScale "Yellow_team"
12
+
13
+ ## Installation
11
14
  Ruby 1.8.7 or higher is required.
15
+
12
16
  gem install right_api_client
13
17
 
14
- == Versioning
15
- The right_api_client gem is versioned using the usual X.Y.Z notation, where X.Y is the
18
+ ## Versioning
19
+ The right\_api\_client gem is versioned using the usual X.Y.Z notation, where X.Y is the
16
20
  RightScale API version, and Z is the client version. For example, if you want to use
17
21
  RightScale API 1.5, you should use the latest version of the 1.5 gem. This will ensure
18
22
  that you get the latest bug fixes for the client that is compatible with that API version.
19
23
 
20
- == Usage Instructions
24
+ ## Usage Instructions
21
25
  New users can start with the following few lines of code and navigate their way around the API by following
22
26
  the available methods. You can find your account id by logging into the RightScale dashboard (https://my.rightscale.com),
23
27
  navigate to the Settings > Account Settings page. The account is is at the end of the browser address bar.
28
+
24
29
  require 'right_api_client'
25
30
  @client = RightApi::Client.new(:email => 'my@email.com', :password => 'my_password', :account_id => 'my_account_id')
26
31
  puts "Available methods: #{@client.api_methods}"
27
32
 
28
33
  The client makes working with and getting to know the API much easier. It spiders the API dynamically to
29
34
  discover its resources on the fly. At every step, the user has the ability to query api_methods(), which
30
- indicates the potential methods that can be called. The <tt>config/login.yml.example</tt> file provides
35
+ indicates the potential methods that can be called. The ```config/login.yml.example``` file provides
31
36
  details of different login parameters.
32
37
 
33
- === Making API calls
38
+ ### Making API calls
34
39
  Essentially, just follow the RightScale API documentation (available from http://support.rightscale.com)
35
40
  and treat every resource in the paths as objects that can call other objects using the dot (.) operator:
36
41
 
37
42
  Examples:
43
+
38
44
  - Index: /api/clouds/:cloud_id/datacenters => @client.clouds(:id => :cloud_id).show.datacenters.index
39
45
  - Show: /api/clouds/:cloud_id/datacenters/:id => @client.clouds(:id => :cloud_id).show.datacenters(:id => :datacenter_id).show
40
46
  - Create: /api/deployments/:deployment_id/servers => @client.deployments(:id => :deployment_id).show.servers.create
@@ -44,29 +50,38 @@ Examples:
44
50
 
45
51
  As seen above, whenever you need to chain methods, you must call .show before specifying the next method.
46
52
 
47
- === Parameters
53
+ ### Parameters
48
54
  Pass-in parameters to the method that they belong to. Lets say you want to filter on the index for deployments:
55
+
49
56
  @client.deployments.index(:filter => ['name==my_deployment'])
57
+
50
58
  The filter is the parameter for the index call and not the deployment call.
51
59
 
52
- === Logging HTTP Requests
53
- The HTTP calls made by right_api_client can be logged in two ways:
60
+ ### Logging HTTP Requests
61
+ The HTTP calls made by right\_api\_client can be logged in two ways:
54
62
  1. Log to a file
63
+
55
64
  @client.log('~/right_api_client.log')
56
- 2. Log to SDTOUT
65
+
66
+ 2. Log to STDOUT
67
+
57
68
  @client.log(STDOUT)
58
69
 
59
- == Examples
70
+ ## Examples
60
71
  Get a list of all servers (aka doing an Index call)
72
+
61
73
  @client.servers.index
62
74
 
63
75
  Get a list of all servers in a deployment
76
+
64
77
  @client.deployments(:id => 'my_deployment_id').show.servers.index
65
78
 
66
79
  Get a particular server (aka doing a Show call)
80
+
67
81
  @client.servers(:id => 'my_server_id').show
68
82
 
69
83
  Creating a server involves setting up the required parameters, then calling the create method
84
+
70
85
  server_template_href = @client.server_templates.index(:filter => ['name==Base ServerTemplate']).first.href
71
86
  cloud = @client.clouds(:id => 'my_cloud_id').show
72
87
  params = { :server => {
@@ -83,48 +98,56 @@ Creating a server involves setting up the required parameters, then calling the
83
98
  new_server.api_methods
84
99
 
85
100
  Launch the newly created server. Inputs are a bit tricky so they have to be set in a long string
101
+
86
102
  inputs = "inputs[][name]=NAME1&inputs[][value]=text:VALUE1&inputs[][name]=NAME2&inputs[][value]=text:VALUE2"
87
103
  new_server.show.launch(inputs)
88
104
 
89
105
  Run a script on the server. The API does not currently expose right_scripts, hence, the script href has
90
106
  to be retrieved from the dashboard and put in the following href format.
107
+
91
108
  script_href = "right_script_href=/api/right_scripts/382371"
92
109
  task = new_server.show.current_instance.show.run_executable(script_href + "&inputs[][name]=TEST_NAME&inputs[][value]=text:VALUE1")
93
110
  task.show.api_methods
94
111
 
95
112
  Update the server's name
113
+
96
114
  params = { :server => {:name => 'New Server Name'}}
97
115
  @client.servers(:id => 'my_server_id').update(params)
98
116
 
99
117
  Terminate the server (i.e. shutdown its current_instance)
118
+
100
119
  @client.servers(:id => 'my_server_id').show.terminate
101
120
 
102
121
  Destroy the server (i.e. delete it)
122
+
103
123
  @client.servers(:id => 'my_server_id').destroy
104
124
 
105
- == Object Types
125
+ ## Object Types
106
126
  The client returns 3 types of objects:
107
- - <b>Resources</b>: returned when you are querying a collection of resources, e.g.: <tt>client.deployments</tt>
108
- - <b>Resource</b>: returned when you specify an id and therefore a specific resource, e.g.: <tt>@client.deployments(:id => :deployment_id)</tt>
109
- - When the content-type is type=collection then an array of Resource objects will be returned, e.g.: <tt>@client.deployments.index</tt>
110
- - When the content-type is not a collection then a Resource object will be returned, e.g.: <tt>@client.deployments(:id => deployment_id).show</tt>
111
- - <b>ResourceDetail</b>: returned when you do a .show on a Resource, e.g.: <tt>client.deployments(:id => deployment_id).show</tt>
112
- <b>On all 3 types of objects you can query <tt>.api_methods</tt> to see a list of available methods, e.g.: <tt>client.deployments.api_methods</tt></b>
113
-
114
- === Exceptions:
115
- - <tt>inputs.index</tt> will return an array of ResourceDetail objects since you cannot do a .show on an input
116
- - <tt>session.index</tt> will return a ResourceDetail object since you cannot do a .show on a session
117
- - <tt>tags.by_resource, tags.by_tag</tt> will return an array of ResourceDetail objects since you cannot do a .show on a resource_tag
118
- - <tt>monitoring_metrics(:id => :m_m_id).show.data</tt> will return a ResourceDetail object since you cannot do
127
+
128
+ - <b>Resources</b>: returned when you are querying a collection of resources, e.g.: ```client.deployments```
129
+ - <b>Resource</b>: returned when you specify an id and therefore a specific resource, e.g.: ```@client.deployments(:id => :deployment_id)```
130
+ - When the content-type is type=collection then an array of Resource objects will be returned, e.g.: ```@client.deployments.index```
131
+ - When the content-type is not a collection then a Resource object will be returned, e.g.: ```@client.deployments(:id => deployment_id).show```
132
+ - <b>ResourceDetail</b>: returned when you do a .show on a Resource, e.g.: ```client.deployments(:id => deployment_id).show```
133
+
134
+ <b>On all 3 types of objects you can query ```.api_methods``` to see a list of available methods, e.g.: ```client.deployments.api_methods```</b>
135
+
136
+ ### Exceptions:
137
+ - ```inputs.index``` will return an array of ResourceDetail objects since you cannot do a .show on an input
138
+ - ```session.index``` will return a ResourceDetail object since you cannot do a .show on a session
139
+ - ```tags.by_resource, tags.by_tag``` will return an array of ResourceDetail objects since you cannot do a .show on a resource_tag
140
+ - ```monitoring_metrics(:id => :m_m_id).show.data``` will return a ResourceDetail object since you cannot do
119
141
  a .show on a monitoring_metric_data
120
142
 
121
- == Instance Facing Calls:
122
- The client also supports 'instance facing calls', which use the instance_token to login.
143
+ ## Instance Facing Calls:
144
+ The client also supports 'instance facing calls', which use the instance\_token to login.
123
145
  Unlike regular email-password logins, instance-facing-calls are limited in the amount of allowable calls.
124
146
  Since in most of the cases, the calls are scoped to the instance's cloud (or the instance itself), the cloud_id and
125
147
  the instance_id will be automatically recorded by the client, so that the user does not need to specify it.
126
148
 
127
- === Examples
149
+ ### Examples
150
+
128
151
  @instance_client = RightApi::Client.new(:instance_token => 'my_token', :account_id => 'my_account_id')
129
152
  @instance_client.volume_attachments links to /api/clouds/:cloud_id/volume_attachments
130
153
  @instance_client.volumes_snapshots links to /api/clouds/:cloud_id/volumes_snapshots
@@ -133,27 +156,30 @@ the instance_id will be automatically recorded by the client, so that the user d
133
156
  @instance_client.backups links to /api/backups
134
157
  @instance_client.live_tasks(:id) links to /api/clouds/:cloud_id/instances/:instance_id/live/tasks/:id
135
158
 
136
- === Notes
137
- For volume_attachments and volumes_snapshots you can also go through the volume:
159
+ ### Notes
160
+ For volume\_attachments and volumes\_snapshots you can also go through the volume:
161
+
138
162
  @instance_client.volumes(:id => :volume_id).show.volume_attachments
139
163
  which maps to:
140
164
  /api/clouds/:cloud_id/volumes/:volume_id/volume_attachment
141
- The instance's volume_attachments can be accessed using:
165
+
166
+ The instance's volume\_attachments can be accessed using:
167
+
142
168
  @instance_client.get_instance.volume_attachments
143
169
  which maps to:
144
170
  /api/clouds/:cloud_id/instances/:instance_id/volume_attachments
145
171
 
146
- Because the cloud_id and the instance_id are automatically added by the client, scripts that work for regular
172
+ Because the ```cloud_id``` and the ```instance_id``` are automatically added by the client, scripts that work for regular
147
173
  email-password logins will have to be modified for instance-facing calls. The main reason behind this is the
148
- inability for instance-facing calls to access the clouds resource (i.e.: <tt>@instance_client.clouds(:id=> :cloud_id).show</tt> will fail)
174
+ inability for instance-facing calls to access the clouds resource (i.e.: ```@instance_client.clouds(:id=> :cloud_id).show``` will fail)
149
175
 
150
- When you query <tt>api_methods</tt>, it will list all of the methods that one sees with regular email-password logins.
176
+ When you query ```api_methods```, it will list all of the methods that one sees with regular email-password logins.
151
177
  Due to the limiting scope of the instance-facing calls, only a subset of these methods can be called
152
178
  (see the API Reference Docs for valid methods). If you call a method that instance's are not authorized to access,
153
179
  you will get a 403 Permission Denied error.
154
180
 
155
181
 
156
- = Design Decisions
182
+ # Design Decisions
157
183
  In the code, we only hard-code CRUD operations for resources. We use the .show and .index methods to make the client
158
184
  more efficient. Since it dynamically creates methods it needs to query the API at times. The .show and the .index make
159
185
  it explicit that querying needs to take place. Without them a GET would have to be queried every step of the way
@@ -163,16 +189,16 @@ first do an index call).
163
189
  <b>In general, when a new API resource is added, you need to indicate in the client whether index, show, create, update
164
190
  and delete methods are allowed for that resource.</b>
165
191
 
166
- == Special Cases
167
- === Returning resource_types that are not actual API resources:
192
+ ## Special Cases
193
+ ### Returning resource\_types that are not actual API resources:
168
194
  - tags:
169
- - by_resource, by_tag: both return a COLLECTION of resource_type = RESOURCE_TAG
195
+ - by\_resource, by\_tag: both return a COLLECTION of resource\_type = RESOURCE\_TAG
170
196
  - no show or index is defined for that resource_type, therefore return a collection of ResourceDetail objects
171
197
  - data:
172
- - querying .data for monitoring_metrics:
173
- - no show is defined for that resource_type, therefore return a ResourceDetail object
198
+ - querying .data for monitoring\_metrics:
199
+ - no show is defined for that resource\_type, therefore return a ResourceDetail object
174
200
 
175
- === Index call does not act like an index call
201
+ ### Index call does not act like an index call
176
202
  - session:
177
203
  - session.index should act like a show call and not like an index call (since you cannot query show).
178
204
  Therefore it should return a ResourceDetail object
@@ -180,23 +206,36 @@ and delete methods are allowed for that resource.</b>
180
206
  - inputs.index cannot return a collection of Resource objects since .show is not allowed. Therefore it should
181
207
  return a collection of ResourceDetail objects
182
208
 
183
- === Having a resource_type that cannot be accurately determined from the URL:
184
- - In server_arrays.show: resource_type = current_instance(s) (although it should be instance(s))
185
- - In multi_cloud_images.show: resource_type = setting(s) (although it should be multi_cloud_image_setting)
186
- Put these special cases in the <tt>RightApi::Helper::INCONSISTENT_RESOURCE_TYPES</tt> hash.
209
+ ### Having a resource\_type that cannot be accurately determined from the URL:
210
+ - In server\_arrays.show: resource\_type = current\_instance(s) (although it should be instance(s))
211
+ - In multi\_cloud\_images.show: resource\_type = setting(s) (although it should be multi\_cloud\_image\_setting)
212
+
213
+ Put these special cases in the ```RightApi::Helper::INCONSISTENT_RESOURCE_TYPES``` hash.
214
+
215
+ ### Method defined on the generic resource\_type itself
216
+ - 'instances' => {:multi\_terminate => 'do\_post', :multi\_run\_executable => 'do\_post'},
217
+ - 'inputs' => {:multi\_update => 'do\_put'},
218
+ - 'tags' => {:by\_tag => 'do\_post', :by\_resource => 'do\_post', :multi\_add => 'do\_post', :multi\_delete =>'do\_post'},
219
+ - 'backups' => {:cleanup => 'do\_post'}
220
+
221
+ Put these special cases in the ```RightApi::Helper::RESOURCE_TYPE_SPECIAL_ACTIONS``` hash.
222
+
223
+ ### Resources are not linked together
224
+ - In ResourceDetail, resource\_type = Instance, need live\_tasks as a method.
225
+
226
+
227
+ # Testing
228
+
229
+ ## Unit Testing
230
+ bundle exec rspec spec/unit
187
231
 
188
- === Method defined on the generic resource_type itself
189
- - 'instances' => {:multi_terminate => 'do_post', :multi_run_executable => 'do_post'},
190
- - 'inputs' => {:multi_update => 'do_put'},
191
- - 'tags' => {:by_tag => 'do_post', :by_resource => 'do_post', :multi_add => 'do_post', :multi_delete =>'do_post'},
192
- - 'backups' => {:cleanup => 'do_post'}
193
- Put these special cases in the <tt>RightApi::Helper::RESOURCE_TYPE_SPECIAL_ACTIONS</tt> hash.
232
+ ## Functional Testing
233
+ See Usage Instructions for how to configure functional testing.
194
234
 
195
- === Resources are not linked together
196
- - In ResourceDetail, resource_type = Instance, need live_tasks as a method.
235
+ bundle exec rspec spec/functional
197
236
 
198
- = Troubleshooting
237
+ # Troubleshooting
199
238
 
200
- == Wrong ruby version
239
+ ## Wrong ruby version
201
240
 
202
241
  Ruby 1.8.7 or higher is required.
@@ -6,9 +6,14 @@
6
6
  # The account number is at the end of the browser address bar.
7
7
  :account_id: my_account_id
8
8
 
9
- # There are three login mechanisms:
9
+ # There are four login mechanisms:
10
10
 
11
- # 1. Use the following parameters to login with your email and password:
11
+ # 0. Use the following parameters to login with your email and base64-encoded password:
12
+ # To encode your plaintext password, use 'base64' command or similar.
13
+ :email: my@email.com
14
+ :password_base64: my_password_encoded_as_base64
15
+
16
+ # 1. Use the following parameters to login with your email and plaintext password:
12
17
  :email: my@email.com
13
18
  :password: my_password
14
19
 
@@ -20,6 +25,6 @@
20
25
  # 3. Use the following parameter to login with pre-authenticated cookies:
21
26
  :cookies: my_cookie_string
22
27
 
23
- # The following are optional parameters can also be set:
28
+ # The following are optional parameters:
24
29
  :api_url: (this defaults to https://my.rightscale.com)
25
- :api_version: (this defaults to 1.5)
30
+ :api_version: (this defaults to 1.5)
@@ -2,13 +2,14 @@ require 'rest_client'
2
2
  require 'json'
3
3
  require 'set'
4
4
  require 'cgi'
5
+ require 'base64'
5
6
 
6
7
  require File.expand_path('../version', __FILE__) unless defined?(RightApi::Client::VERSION)
7
8
  require File.expand_path('../helper', __FILE__)
8
9
  require File.expand_path('../resource', __FILE__)
9
10
  require File.expand_path('../resource_detail', __FILE__)
10
11
  require File.expand_path('../resources', __FILE__)
11
- require File.expand_path('../exceptions', __FILE__)
12
+ require File.expand_path('../errors', __FILE__)
12
13
 
13
14
  # RightApiClient has the generic get/post/delete/put calls that are used by resources
14
15
  module RightApi
@@ -20,23 +21,35 @@ module RightApi
20
21
  DEFAULT_API_URL = 'https://my.rightscale.com'
21
22
 
22
23
  # permitted parameters for initializing
23
- AUTH_PARAMS = %w(email password account_id api_url api_version cookies instance_token)
24
- attr_reader :cookies, :instance_token
24
+ AUTH_PARAMS = %w[
25
+ email password_base64 password account_id api_url api_version
26
+ cookies instance_token
27
+ ]
28
+
29
+ attr_reader :cookies, :instance_token, :last_request
25
30
 
26
31
  def initialize(args)
32
+
27
33
  raise 'This API client is only compatible with Ruby 1.8.7 and upwards.' if (RUBY_VERSION < '1.8.7')
34
+
28
35
  @api_url, @api_version = DEFAULT_API_URL, API_VERSION
36
+
29
37
  # Initializing all instance variables from hash
30
38
  args.each { |key,value|
31
39
  instance_variable_set("@#{key}", value) if value && AUTH_PARAMS.include?(key.to_s)
32
40
  } if args.is_a? Hash
33
41
 
34
42
  raise 'This API client is only compatible with the RightScale API 1.5 and upwards.' if (Float(@api_version) < 1.5)
43
+
35
44
  @rest_client = RestClient::Resource.new(@api_url, :timeout => -1)
45
+ @last_request = {}
46
+
47
+ # There are three options for login: credentials, instance token,
48
+ # or if the user already has the cookies they can just use those.
49
+ # See config/login.yml.example for more info.
50
+ login() unless @cookies
36
51
 
37
- # There are three options for login: credentials, instance token, or if the user already
38
- # has the cookies they can just use those. See config/login.yml.example for more info.
39
- @cookies ||= login()
52
+ timestamp_cookies
40
53
 
41
54
  # Add the top level links for instance_facing_calls
42
55
  if @instance_token
@@ -82,32 +95,48 @@ module RightApi
82
95
  RestClient.log = file
83
96
  end
84
97
 
85
- # Users shouldn't need to call the following methods directly
98
+ # Given a path returns a RightApiClient::Resource instance.
99
+ #
100
+ def resource(path)
101
+
102
+ r = Resource.process(self, *do_get(path))
103
+
104
+ r.respond_to?(:show) ? r.show : r
105
+ end
106
+
107
+ # Seems #resource tends to expand (call index) on Resources instances,
108
+ # so this is a workaround.
109
+ #
110
+ def resources(type, path)
111
+
112
+ Resources.new(self, path, type)
113
+ end
114
+
115
+ protected
86
116
 
87
117
  def login
88
- if @instance_token
89
- params = {
90
- 'instance_token' => @instance_token
91
- }
92
- path = ROOT_INSTANCE_RESOURCE
118
+ params, path = if @instance_token
119
+ [ { 'instance_token' => @instance_token },
120
+ ROOT_INSTANCE_RESOURCE ]
121
+ elsif @password_base64
122
+ [ { 'email' => @email, 'password' => Base64.decode64(@password_base64) },
123
+ ROOT_RESOURCE ]
93
124
  else
94
- params = {
95
- 'email' => @email,
96
- 'password' => @password,
97
- }
98
- path = ROOT_RESOURCE
125
+ [ { 'email' => @email, 'password' => @password },
126
+ ROOT_RESOURCE ]
99
127
  end
100
128
  params['account_href'] = "/api/accounts/#{@account_id}"
101
129
 
102
- response = @rest_client[path].post(params, 'X_API_VERSION' => @api_version) do |response, request, result|
103
- case response.code
104
- when 302
105
- response
130
+ response = @rest_client[path].post(params, 'X_API_VERSION' => @api_version) do |response, request, result, &block|
131
+ if response.code == 302
132
+ update_api_url(response)
133
+ response.follow_redirection(request, result, &block)
106
134
  else
107
135
  response.return!(request, result)
108
136
  end
109
137
  end
110
- response.cookies
138
+
139
+ update_cookies(response)
111
140
  end
112
141
 
113
142
  # Returns the request headers
@@ -115,39 +144,57 @@ module RightApi
115
144
  {'X_API_VERSION' => @api_version, :cookies => @cookies, :accept => :json}
116
145
  end
117
146
 
147
+ def update_last_request(request, response)
148
+ @last_request[:request] = request
149
+ @last_request[:response] = response
150
+ end
151
+
118
152
  # Generic get
119
153
  # params are NOT read only
120
154
  def do_get(path, params={})
155
+
121
156
  # Resource id is a special param as it needs to be added to the path
122
157
  path = add_id_and_params_to_path(path, params)
123
158
 
159
+ req, res = nil
160
+
124
161
  begin
125
162
  # Return content type so the resulting resource object knows what kind of resource it is.
126
- resource_type, body = @rest_client[path].get(headers) do |response, request, result|
163
+ resource_type, body = @rest_client[path].get(headers) do |response, request, result, &block|
164
+ req, res = request, response
165
+ update_cookies(response)
166
+ update_last_request(request, response)
167
+
127
168
  case response.code
128
169
  when 200
129
- # Get the resource_type from the content_type, the resource_type will
130
- # be used later to add relevant methods to relevant resources.
131
- type = ''
132
- if result.content_type.index('rightscale')
133
- type = get_resource_type(result.content_type)
170
+ # Get the resource_type from the content_type, the resource_type
171
+ # will be used later to add relevant methods to relevant resources
172
+ type = if result.content_type.index('rightscale')
173
+ get_resource_type(result.content_type)
174
+ else
175
+ ''
134
176
  end
135
177
 
136
178
  [type, response.body]
179
+ when 301, 302
180
+ update_api_url(response)
181
+ response.follow_redirection(request, result, &block)
137
182
  when 404
138
- raise Exceptions::UnknownRouteException.new("HTTP Code: #{response.code.to_s}, Response body: #{response.body}")
183
+ raise UnknownRouteError.new(request, response)
139
184
  else
140
- raise Exceptions::ApiException.new("HTTP Code: #{response.code.to_s}, Response body: #{response.body}")
185
+ raise ApiError.new(request, response)
141
186
  end
142
187
  end
143
- rescue Exceptions::ApiException => e
188
+ rescue ApiError => e
144
189
  if re_login?(e)
145
- #Session cookie is expired or invalid
146
- @cookies = login()
190
+ # session cookie is expired or invalid
191
+ login()
147
192
  retry
148
193
  else
149
- raise e
194
+ raise wrap(e, :get, path, params, req, res)
150
195
  end
196
+ rescue => e
197
+ raise wrap(e, :get, path, params, req, res)
151
198
  end
152
199
 
153
200
  data = if resource_type == 'text'
@@ -161,8 +208,16 @@ module RightApi
161
208
 
162
209
  # Generic post
163
210
  def do_post(path, params={})
211
+ params = fix_array_of_hashes(params)
212
+
213
+ req, res = nil
214
+
164
215
  begin
165
216
  @rest_client[path].post(params, headers) do |response, request, result|
217
+ req, res = request, response
218
+ update_cookies(response)
219
+ update_last_request(request, response)
220
+
166
221
  case response.code
167
222
  when 201, 202
168
223
  # Create and return the resource
@@ -173,6 +228,8 @@ module RightApi
173
228
  # This is based on the assumption that we can determine the resource_type without doing a do_get
174
229
  resource_type = get_singular(relative_href.split('/')[-2])
175
230
  RightApi::Resource.process(self, resource_type, relative_href)
231
+ when 204
232
+ nil
176
233
  when 200..299
177
234
  # This is needed for the tags Resource -- which returns a 200 and has a content type
178
235
  # therefore, ResourceDetail objects needs to be returned
@@ -188,62 +245,89 @@ module RightApi
188
245
  response.return!(request, result)
189
246
  end
190
247
  when 404
191
- raise Exceptions::UnknownRouteException.new("HTTP Code: #{response.code.to_s}, Response body: #{response.body}")
248
+ raise UnknownRouteError.new(request, response)
192
249
  else
193
- raise Exceptions::ApiException.new("HTTP Code: #{response.code.to_s}, Response body: #{response.body}")
250
+ raise ApiError.new(request, response)
194
251
  end
195
252
  end
196
- rescue Exceptions::ApiException => e
253
+
254
+ rescue ApiError => e
197
255
  if re_login?(e)
198
- @cookies = login()
256
+ login()
199
257
  retry
200
258
  else
201
- raise e
259
+ raise wrap(e, :post, path, params, req, res)
202
260
  end
261
+ rescue => e
262
+ raise wrap(e, :post, path, params, req, res)
203
263
  end
204
264
  end
205
265
 
206
266
  # Generic delete
207
- def do_delete(path)
267
+ def do_delete(path, params={})
268
+ # Resource id is a special param as it needs to be added to the path
269
+ path = add_id_and_params_to_path(path, params)
270
+
271
+ req, res = nil
272
+
208
273
  begin
209
274
  @rest_client[path].delete(headers) do |response, request, result|
275
+ req, res = request, response
276
+ update_cookies(response)
277
+ update_last_request(request, response)
278
+
210
279
  case response.code
211
- when 200, 204
280
+ when 200
281
+ when 204
282
+ nil
212
283
  when 404
213
- raise Exceptions::UnknownRouteException.new("HTTP Code: #{response.code.to_s}, Response body: #{response.body}")
284
+ raise UnknownRouteError.new(request, response)
214
285
  else
215
- raise Exceptions::ApiException.new("HTTP Code: #{response.code.to_s}, Response body: #{response.body}")
286
+ raise ApiError.new(request, response)
216
287
  end
217
288
  end
218
- rescue Exceptions::ApiException => e
289
+ rescue ApiError => e
219
290
  if re_login?(e)
220
- @cookies = login()
291
+ login()
221
292
  retry
222
293
  else
223
- raise e
294
+ raise wrap(e, :delete, path, params, req, res)
224
295
  end
296
+ rescue => e
297
+ raise wrap(e, :delete, path, params, req, res)
225
298
  end
226
299
  end
227
300
 
228
301
  # Generic put
229
302
  def do_put(path, params={})
303
+ params = fix_array_of_hashes(params)
304
+
305
+ req, res = nil
306
+
230
307
  begin
231
308
  @rest_client[path].put(params, headers) do |response, request, result|
309
+ req, res = request, response
310
+ update_cookies(response)
311
+ update_last_request(request, response)
312
+
232
313
  case response.code
233
314
  when 204
315
+ nil
234
316
  when 404
235
- raise Exceptions::UnknownRouteException.new("HTTP Code: #{response.code.to_s}, Response body: #{response.body}")
317
+ raise UnknownRouteError.new(request, response)
236
318
  else
237
- raise Exceptions::ApiException.new("HTTP Code: #{response.code.to_s}, Response body: #{response.body}")
319
+ raise ApiError.new(request, response)
238
320
  end
239
321
  end
240
- rescue Exceptions::ApiException => e
322
+ rescue ApiError => e
241
323
  if re_login?(e)
242
- @cookies = login()
324
+ login()
243
325
  retry
244
326
  else
245
- raise e
327
+ raise wrap(e, :put, path, params, req, res)
246
328
  end
329
+ rescue => e
330
+ raise wrap(e, :put, path, params, req, res)
247
331
  end
248
332
  end
249
333
 
@@ -255,6 +339,67 @@ module RightApi
255
339
  def get_resource_type(content_type)
256
340
  content_type.scan(/\.rightscale\.(.*)\+json/)[0][0]
257
341
  end
342
+
343
+ # Makes sure the @cookies have a timestamp.
344
+ #
345
+ def timestamp_cookies
346
+
347
+ return unless @cookies
348
+
349
+ class << @cookies; attr_accessor :timestamp; end
350
+ @cookies.timestamp = Time.now
351
+ end
352
+
353
+ # Sets the @cookies (and timestamp it).
354
+ #
355
+ def update_cookies(response)
356
+
357
+ return unless response.cookies
358
+
359
+ (@cookies ||= {}).merge!(response.cookies)
360
+ timestamp_cookies
361
+ end
362
+
363
+ #
364
+ # A helper class for error details
365
+ #
366
+ class ErrorDetails
367
+
368
+ attr_reader :method, :path, :params, :request, :response
369
+
370
+ def initialize(me, pt, ps, rq, rs)
371
+
372
+ @method = me
373
+ @path = pt
374
+ @params = ps
375
+ @request = rq
376
+ @response = rs
377
+ end
378
+
379
+ def code
380
+
381
+ @response ? @response.code : nil
382
+ end
383
+ end
384
+
385
+ # Adds details (path, params) to an error. Returns the error.
386
+ #
387
+ def wrap(error, method, path, params, request, response)
388
+
389
+ class << error; attr_accessor :_details; end
390
+ error._details = ErrorDetails.new(method, path, params, request, response)
391
+
392
+ error
393
+ end
394
+
395
+ private
396
+
397
+ def update_api_url(response)
398
+ # Update the rest client url if we are redirected to another endpoint
399
+ uri = URI.parse(response.headers[:location])
400
+ @api_url = "#{uri.scheme}://#{uri.host}"
401
+ @rest_client = RestClient::Resource.new(@api_url, :timeout => -1)
402
+ end
258
403
  end
259
404
  end
260
405