softlayer_api 1.0.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.
@@ -0,0 +1,61 @@
1
+ # Copyright (c) 2010, SoftLayer Technologies, Inc. All rights reserved.
2
+ #
3
+ # Redistribution and use in source and binary forms, with or without
4
+ # modification, are permitted provided that the following conditions are met:
5
+ #
6
+ # * Redistributions of source code must retain the above copyright notice,
7
+ # this list of conditions and the following disclaimer.
8
+ # * Redistributions in binary form must reproduce the above copyright notice,
9
+ # this list of conditions and the following disclaimer in the documentation
10
+ # and/or other materials provided with the distribution.
11
+ # * Neither SoftLayer Technologies, Inc. nor the names of its contributors may
12
+ # be used to endorse or promote products derived from this software without
13
+ # specific prior written permission.
14
+ #
15
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
19
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
20
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
21
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
24
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
25
+ # POSSIBILITY OF SUCH DAMAGE.
26
+
27
+ # The SoftLayer Module
28
+ #
29
+ # This module is used to provide a namespace for SoftLayer code. It also declares a number of
30
+ # global variables:
31
+ # - <tt>$SL_API_USERNAME</tt> - The default username passed by clients to the server for authentication.
32
+ # Set this if you want to use the same username for all clients and don't want to have to specify it when the client is created
33
+ # - <tt>$SL_API_KEY</tt> - The default API key passed by clients to the server for authentication.
34
+ # Set this if you want to use the same api for all clients and don't want to have to specify it when the client is created
35
+ # - <tt>$SL_API_BASE_URL</tt>- The default URL used to access the SoftLayer API. This defaults to the value of SoftLayer::API_PUBLIC_ENDPOINT
36
+ #
37
+ module SoftLayer
38
+ VERSION = "1.0.0"
39
+
40
+ # The base URL of the SoftLayer API's REST-like endpoints available to the public internet.
41
+ API_PUBLIC_ENDPOINT = 'https://api.softlayer.com/rest/v3/'
42
+
43
+ # The base URL of the SoftLayer API's REST-like endpoints available through SoftLayer's private network
44
+ API_PRIVATE_ENDPOINT = 'https://api.service.softlayer.com/rest/v3/'
45
+
46
+ #
47
+ # These globals can be used to simplify client creation
48
+ #
49
+
50
+ # Set this if you want to provide a default username for each service as it is created.
51
+ # usernames provided to the service initializer will override the global
52
+ $SL_API_USERNAME = nil
53
+
54
+ # Set this if you want to provide a default api_key for each service as it is
55
+ # created. API keys provided in the constructor when a service is created will
56
+ # override the values in this global
57
+ $SL_API_KEY = nil
58
+
59
+ # The base URL used for the SoftLayer API's
60
+ $SL_API_BASE_URL = SoftLayer::API_PUBLIC_ENDPOINT
61
+ end # module SoftLayer
@@ -0,0 +1,48 @@
1
+ # Copyright (c) 2010, SoftLayer Technologies, Inc. All rights reserved.
2
+ #
3
+ # Redistribution and use in source and binary forms, with or without
4
+ # modification, are permitted provided that the following conditions are met:
5
+ #
6
+ # * Redistributions of source code must retain the above copyright notice,
7
+ # this list of conditions and the following disclaimer.
8
+ # * Redistributions in binary form must reproduce the above copyright notice,
9
+ # this list of conditions and the following disclaimer in the documentation
10
+ # and/or other materials provided with the distribution.
11
+ # * Neither SoftLayer Technologies, Inc. nor the names of its contributors may
12
+ # be used to endorse or promote products derived from this software without
13
+ # specific prior written permission.
14
+ #
15
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
19
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
20
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
21
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
24
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
25
+ # POSSIBILITY OF SUCH DAMAGE.
26
+
27
+ class Hash
28
+ def to_sl_object_mask(base = "")
29
+ # ask the children to convert themselves with the key as the base
30
+ masked_children = self.map { |key, value| result = value.to_sl_object_mask(key); }.flatten
31
+
32
+ # now resolve the children with respect to the base passed in.
33
+ masked_children.map { |mask_item| mask_item.to_sl_object_mask(base) }
34
+ end
35
+ end
36
+
37
+ class Array
38
+ def to_sl_object_mask(base = "")
39
+ self.map { |item| item.to_sl_object_mask(base) }.flatten
40
+ end
41
+ end
42
+
43
+ class String
44
+ def to_sl_object_mask(base = "")
45
+ return base if self.empty?
46
+ base.empty? ? self : "#{base}.#{self}"
47
+ end
48
+ end
@@ -0,0 +1,407 @@
1
+ # Copyright (c) 2010, SoftLayer Technologies, Inc. All rights reserved.
2
+ #
3
+ # Redistribution and use in source and binary forms, with or without
4
+ # modification, are permitted provided that the following conditions are met:
5
+ #
6
+ # * Redistributions of source code must retain the above copyright notice,
7
+ # this list of conditions and the following disclaimer.
8
+ # * Redistributions in binary form must reproduce the above copyright notice,
9
+ # this list of conditions and the following disclaimer in the documentation
10
+ # and/or other materials provided with the distribution.
11
+ # * Neither SoftLayer Technologies, Inc. nor the names of its contributors may
12
+ # be used to endorse or promote products derived from this software without
13
+ # specific prior written permission.
14
+ #
15
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
19
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
20
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
21
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
24
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
25
+ # POSSIBILITY OF SUCH DAMAGE.
26
+
27
+ require 'rubygems'
28
+ require 'net/https'
29
+ require 'json/add/core'
30
+
31
+ module SoftLayer
32
+ # A subclass of Exception with nothing new provided. This simply provides
33
+ # a unique type for exceptions from the SoftLayer API
34
+ class SoftLayerAPIException < RuntimeError
35
+ end
36
+
37
+ # An APIParameterFilter is an intermediary object that understands how
38
+ # to accept the other API parameter filters and carry their values to
39
+ # method_missing in Service. Instances of this class are created
40
+ # internally by the Service in it's handling of a method call and you
41
+ # should not have to create instances of this class directly.
42
+ #
43
+ # Instead, to use an API filter, you add a filter method to the call
44
+ # chain when you call a method through a SoftLayer::Service
45
+ #
46
+ # For example, given a SoftLayer::Service instance called "account_service"
47
+ # you could take advantage of the API filter that identifies a particular
48
+ # object known to that service using the 'object_with_id" method :
49
+ #
50
+ # account_service.object_with_id(91234).getSomeAttribute
51
+ #
52
+ # The invocation of object_with_id will cause an instance of this
53
+ # class to be instantiated with the service as its target.
54
+ #
55
+ class APIParameterFilter
56
+ attr_accessor :target
57
+ attr_accessor :parameters
58
+
59
+ def initialize
60
+ @parameters = {}
61
+ end
62
+
63
+ def server_object_id
64
+ self.parameters[:server_object_id]
65
+ end
66
+
67
+ def server_object_mask
68
+ self.parameters[:object_mask]
69
+ end
70
+
71
+ def object_with_id(value)
72
+ merged_object = APIParameterFilter.new;
73
+ merged_object.target = self.target
74
+ merged_object.parameters = @parameters.merge({ :server_object_id => value })
75
+ merged_object
76
+ end
77
+
78
+ def object_mask(*args)
79
+ merged_object = APIParameterFilter.new;
80
+ merged_object.target = self.target
81
+ merged_object.parameters = @parameters.merge({ :object_mask => args }) if args && !args.empty?
82
+ merged_object
83
+ end
84
+
85
+ def method_missing(method_name, *args, &block)
86
+ return @target.call_softlayer_api_with_params(method_name, self, args, &block)
87
+ end
88
+ end
89
+
90
+ # = SoftLayer API Service
91
+ #
92
+ # Instances of this class represent services in the SoftLayer API.
93
+ #
94
+ # You create a service with the name of one of the SoftLayer services
95
+ # (documented on the http://sldn.softlayer.com web site). Once created
96
+ # you can use the service to make method calls to the SoftLayer API.
97
+ #
98
+ # A typical use might look something like
99
+ #
100
+ # account_service = SoftLayer::Service("SoftLayer_Account", :username=>"<your user name here>" :api_key=>"<your api key here>")
101
+ #
102
+ # then to invoke a method simply call the service:
103
+ #
104
+ # account_service.getOpenTickets
105
+ # => {... lots of information here representing the list of open tickets ...}
106
+ #
107
+ class Service
108
+ # The name of the service that this object calls. Cannot be emtpy or nil.
109
+ attr_accessor :service_name
110
+
111
+ # A username passed as authentication for each request. Cannot be emtpy or nil.
112
+ attr_accessor :username
113
+
114
+ # An API key passed as part of the authentication of each request. Cannot be emtpy or nil.
115
+ attr_accessor :api_key
116
+
117
+ # The base URL for requests that are passed to the server. Cannot be emtpy or nil.
118
+ attr_accessor :endpoint_url
119
+
120
+ # Initialize an instance of the Client class. You pass in the service name
121
+ # and optionally hash arguments specifying how the client should access the
122
+ # SoftLayer API.
123
+ #
124
+ # The following symbols can be used as hash arguments to pass options to the constructor:
125
+ # - <tt>:username</tt> - a non-empty string providing the username to use for requests to the service
126
+ # - <tt>:api_key</tt> - a non-empty string providing the api key to use for requests to the service
127
+ # - <tt>:endpoint_url</tt> - a non-empty string providing the endpoint URL to use for requests to the service
128
+ #
129
+ # If any of the options above are missing then the constructor will try to use the corresponding
130
+ # global variable declared in the SoftLayer Module:
131
+ # - <tt>$SL_API_USERNAME</tt>
132
+ # - <tt>$SL_API_KEY</tt>
133
+ # - <tt>$SL_API_BASE_URL</tt>
134
+ #
135
+ def initialize(service_name, options = {})
136
+ raise SoftLayerAPIException.new("Please provide a service name") if service_name.nil? || service_name.empty?
137
+ self.service_name = service_name;
138
+
139
+ # pick up the username provided in options or the default one from the *globals*
140
+ self.username = options[:username] || $SL_API_USERNAME || ""
141
+
142
+ # pick up the api_key provided in options or the default one from the globals
143
+ self.api_key = options[:api_key] || $SL_API_KEY || ""
144
+
145
+ # pick up the url endpoint from options or the default one in the globals OR the
146
+ # public endpoint
147
+ self.endpoint_url = options[:endpoint_url] || $SL_API_BASE_URL || API_PUBLIC_ENDPOINT
148
+
149
+ if($DEBUG)
150
+ @method_missing_call_depth = 0
151
+ end
152
+ end #initalize
153
+
154
+
155
+ # Use this as part of a method call chain to identify a particular
156
+ # object as the target of the request. The parameter is the SoftLayer
157
+ # object identifier you are interested in. For example, this call
158
+ # would return the ticket whose ID is 35212
159
+ #
160
+ # ticket_service.object_with_id(35212).getObject
161
+ #
162
+ def object_with_id(object_of_interest)
163
+ proxy = APIParameterFilter.new
164
+ proxy.target = self
165
+
166
+ return proxy.object_with_id(object_of_interest)
167
+ end
168
+
169
+ # Use this as part of a method call chain to add an object mask to
170
+ # the request.The arguments to object mask should be the strings
171
+ # that are the keys of the mask:
172
+ #
173
+ # ticket_service.object_mask("createDate", "modifyDate").getObject
174
+ #
175
+ # Before being used, the string passed will be url-encoded by this
176
+ # routine. (i.e. there is no need to url-encode the strings beforehand)
177
+ #
178
+ # As an implementation detail, the object_mask becomes part of the
179
+ # query on the url sent to the API server
180
+ #
181
+ def object_mask(*args)
182
+ proxy = APIParameterFilter.new
183
+ proxy.target = self
184
+
185
+ return proxy.object_mask(*args)
186
+ end
187
+
188
+ # This is the primary mechanism by which requests are made. If you call
189
+ # the service with a method it doesn't understand, it will send a call to
190
+ # the endpoint for a method of the same name.
191
+ #
192
+ def method_missing(method_name, *args, &block)
193
+ # During development, if you end up with a stray name in some
194
+ # code, you can end up in an infinite recursive loop as method_missing
195
+ # tries to resolve that name (believe me... it happens).
196
+ # This mechanism looks for what it considers to be an unreasonable call
197
+ # depth and kills the loop quickly.
198
+ if($DEBUG)
199
+ @method_missing_call_depth += 1
200
+ if @method_missing_call_depth > 3 # 3 is somewhat arbitrary... really the call depth should only ever be 1
201
+ @method_missing_call_depth = 0
202
+ raise "stop infinite recursion #{method_name}, #{args.inspect}"
203
+ end
204
+ end
205
+
206
+ # if we're in debug mode, we put out a little helpful information
207
+ puts "SoftLayer::Service#method_missing called #{method_name}, #{args.inspect}" if $DEBUG
208
+
209
+ result = call_softlayer_api_with_params(method_name, nil, args, &block);
210
+
211
+ if($DEBUG)
212
+ @method_missing_call_depth -= 1
213
+ end
214
+
215
+ return result
216
+ end
217
+
218
+ # Issue an HTTP request to call the given method from the SoftLayer API with
219
+ # the parameters and arguments given.
220
+ #
221
+ # Parameters are information _about_ the call, the object mask or the
222
+ # particular object in the SoftLayer API you are calling.
223
+ #
224
+ # Arguments are the arguments to the SoftLayer method that you wish to
225
+ # invoke.
226
+ #
227
+ # This is intended to be used in the internal
228
+ # processing of method_missing and need not be called directly.
229
+ def call_softlayer_api_with_params(method_name, parameters, args, &block)
230
+ # find out what URL will invoke the method (with the given parameters)
231
+ request_url = url_to_call_method(method_name, parameters)
232
+
233
+ # construct an HTTP request for that method with the given URL
234
+ http_request = http_request_for_method(method_name, request_url);
235
+ http_request.basic_auth(self.username, self.api_key)
236
+
237
+ # marshall the arguments into the http_request
238
+ request_body = marshall_arguments_for_call(args)
239
+
240
+ # If you provide arguments to a call that is not supposed to have
241
+ # arguments, this will print a warning to the console.
242
+ if request_body && !http_request.request_body_permitted?
243
+ $stderr.puts("Warning - The HTTP request for #{method_name} does not allow arguments to be passed to the server")
244
+ else
245
+ # Otherwise, add the arguments as the body of the request
246
+ http_request.body = request_body
247
+ end
248
+
249
+ # Send the url request and recover the results. Parse the response (if any)
250
+ # as JSON
251
+ json_results = issue_http_request(request_url, http_request, &block)
252
+ if json_results
253
+ # The JSON parser for Ruby parses JSON "Text" according to RFC 4627, but
254
+ # not JSON values. As a result, 'JSON.parse("true")' yields a parsing
255
+ # exception. To work around this, we force the result JSON text by
256
+ # including it in Array markers, then take the first element of the
257
+ # resulting array as the result of the parsing. This should allow values
258
+ # like true, false, null, and numbers to parse the same way they would in
259
+ # a browser.
260
+ parsed_json = JSON.parse("[ #{json_results} ]")[0]
261
+
262
+ # if the results indicate an error, convert it into an exception
263
+ if parsed_json.kind_of?(Hash) && parsed_json['error']
264
+ raise SoftLayerAPIException.new(parsed_json['error'])
265
+ end
266
+ else
267
+ parsed_json = nil
268
+ end
269
+
270
+ # return the results, if any
271
+ return parsed_json
272
+ end
273
+
274
+ # Marshall the arguments into a JSON string suitable for the body of
275
+ # an HTTP message. This is intended to be used in the internal
276
+ # processing of method_missing and need not be called directly.
277
+ def marshall_arguments_for_call(args)
278
+ request_body = nil;
279
+
280
+ if(args && !args.empty?)
281
+ request_body = {"parameters" => args}.to_json
282
+ end
283
+
284
+ return request_body
285
+ end
286
+
287
+ # Given a method name, determine the appropriate HTTP mechanism
288
+ # for sending a request to execute that method to the server.
289
+ # and create a Net::HTTP request of that type. This is intended
290
+ # to be used in the internal processing of method_missing and
291
+ # need not be called directly.
292
+ def http_request_for_method(method_name, method_url)
293
+ content_type_header = {"Content-Type" => "application/json"}
294
+
295
+ case method_name.to_s
296
+ when /^get/
297
+ Net::HTTP::Get.new(method_url.request_uri())
298
+ when /^edit/
299
+ Net::HTTP::Put.new(method_url.request_uri(), content_type_header)
300
+ when /^delete/
301
+ Net::HTTP::Delete.new(method_url.request_uri())
302
+ when /^create/, /^add/, /^remove/, /^findBy/
303
+ Net::HTTP::Post.new(method_url.request_uri(), content_type_header)
304
+ else
305
+ Net::HTTP::Get.new(method_url.request_uri())
306
+ end
307
+ end
308
+
309
+ # Connect to the network and request the content of the resource
310
+ # specified. This is used to do the actual work of connecting
311
+ # to the SoftLayer servers and exchange data. This is intended
312
+ # to be used in the internal processing of method_missing and
313
+ # need not be called directly.
314
+ def issue_http_request(request_url, http_request, &block)
315
+ # create and run an SSL request
316
+ https = Net::HTTP.new(request_url.host, request_url.port)
317
+ https.use_ssl = true
318
+
319
+ # This line silences an annoying warning message if you're in debug mode
320
+ https.verify_mode = OpenSSL::SSL::VERIFY_NONE if $DEBUG
321
+
322
+ https.start do |http|
323
+
324
+ puts "SoftLayer API issuing an HTTP request for #{request_url}" if $DEBUG
325
+
326
+ response = https.request(http_request)
327
+
328
+ case response
329
+ when Net::HTTPSuccess
330
+ return response.body
331
+ else
332
+ # We have an error. It might have a meaningful error message
333
+ # from the server. Check to see if there is a body and whether
334
+ # or not that body parses as JSON. If it does, then we return
335
+ # that as a result (assuming it's an error)
336
+ json_parses = false
337
+ body = response.body
338
+
339
+ begin
340
+ if body
341
+ JSON.parse(body)
342
+ json_parses = true
343
+ end
344
+ rescue => json_parse_exception
345
+ json_parses = false;
346
+ end
347
+
348
+ # Let the HTTP library generate and raise an exception if
349
+ # the body was empty or could not be parsed as JSON
350
+ response.value() if !json_parses
351
+
352
+ return body
353
+ end
354
+ end
355
+ end
356
+
357
+ # Construct a URL for calling the given method on this endpoint and
358
+ # expecting a JSON response. This is intended to be used in the internal
359
+ # processing of method_missing and need not be called directly.
360
+ def url_to_call_method(method_name, parameters)
361
+ method_path = method_name.to_s
362
+
363
+ # if there's an object ID on the parameters, add that to the URL
364
+ if(parameters && parameters.server_object_id)
365
+ method_path = parameters.server_object_id.to_s + "/" + method_path
366
+ end
367
+
368
+ # tag ".json" onto the method path (if it's not already there)
369
+ method_path.sub!(%r|(\.json){0,1}$|, ".json")
370
+
371
+ # put the whole thing together into a URL
372
+ # (reusing a variation on the clever regular expression above. This one appends a "slash"
373
+ # to the service name if theres not already one there otherwise. Without it URI.join
374
+ # doesn't do the right thing)
375
+ uri = URI.join(self.endpoint_url, self.service_name.sub(%r{/*$},"/"), method_path)
376
+
377
+ query_string = nil
378
+
379
+ if(parameters && parameters.server_object_mask)
380
+ mask_value = parameters.server_object_mask.to_sl_object_mask.map { |mask_key| URI.encode(mask_key.to_s.strip) }.join(";")
381
+ query_string = "objectMask=#{mask_value}"
382
+ end
383
+
384
+ uri.query = query_string
385
+
386
+ return uri
387
+ end
388
+
389
+ # Change the username. The username cannot be nil or the empty string.
390
+ def username= (name)
391
+ raise SoftLayerAPIException.new("Please provide a username") if name.nil? || name.empty?
392
+ @username = name.strip
393
+ end
394
+
395
+ # Change the api_key. It cannot be nil or the empty string.
396
+ def api_key= (new_key)
397
+ raise SoftLayerAPIException.new("Please provide an api_key") if new_key.nil? || new_key.empty?
398
+ @api_key = new_key.strip
399
+ end
400
+
401
+ # Change the endpoint_url. It cannot be nil or the empty string.
402
+ def endpoint_url= (new_url)
403
+ raise SoftLayerAPIException.new("The endpoint url cannot be nil or empty") if new_url.nil? || new_url.empty?
404
+ @endpoint_url = new_url.strip
405
+ end
406
+ end # class Service
407
+ end # module SoftLayer