softlayer_api 1.0.8 → 2.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,321 @@
1
+ #
2
+ # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved.
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+ #
21
+
22
+ require 'xmlrpc/client'
23
+
24
+ # The XML-RPC spec calls for the "faultCode" in faults to be an integer
25
+ # but the SoftLayer XML-RPC API can return strings as the "faultCode"
26
+ #
27
+ # We monkey patch the module method XMLRPC::FaultException::Convert::fault
28
+ # so that it does pretty much what the default does without checking
29
+ # to ensure that the faultCode is an integer
30
+ module XMLRPC::Convert
31
+ def self.fault(hash)
32
+ if hash.kind_of? Hash and hash.size == 2 and
33
+ hash.has_key? "faultCode" and hash.has_key? "faultString" and
34
+ (hash["faultCode"].kind_of?(Integer) || hash["faultCode"].kind_of?(String)) and hash["faultString"].kind_of? String
35
+
36
+ XMLRPC::FaultException.new(hash["faultCode"], hash["faultString"])
37
+ else
38
+ super
39
+ end
40
+ end
41
+ end
42
+
43
+ # The XMLRPC client uses a fixed user agent string, but we want to
44
+ # supply our own, so we add a method to XMLRPC::Client that lets
45
+ # us change it.
46
+ class XMLRPC::Client
47
+ def self.set_user_agent(new_agent)
48
+ remove_const(:USER_AGENT) if const_defined?(:USER_AGENT)
49
+ const_set(:USER_AGENT, new_agent)
50
+ end
51
+ end
52
+
53
+ module SoftLayer
54
+
55
+ # = SoftLayer API Service
56
+ #
57
+ # Instances of this class are the runtime representation of
58
+ # services in the SoftLayer API. They handle communication with
59
+ # the SoftLayer servers.
60
+ #
61
+ # You typically should not need to create services directly.
62
+ # instead, you should be creating a client and then using it to
63
+ # obtain individual services. For example:
64
+ #
65
+ # client = SoftLayer::Client.new(:username => "Joe", :api_key=>"feeddeadbeefbadfood...")
66
+ # account_service = client.service_named("Account") # returns the SoftLayer_Account service
67
+ # account_service = client['Account'] # Exactly the same as above
68
+ #
69
+ # For backward compatibility, a service can be constructed by passing
70
+ # client initialization options, however if you do so you will need to
71
+ # prepend the "SoftLayer_" on the front of the service name. For Example:
72
+ #
73
+ # account_service = SoftLayer::Service("SoftLayer_Account",
74
+ # :username=>"<your user name here>"
75
+ # :api_key=>"<your api key here>")
76
+ #
77
+ # A service communicates with the SoftLayer API through the the XML-RPC
78
+ # interface using Ruby's built in classes
79
+ #
80
+ # Once you have a service, you can invoke methods in the service like this:
81
+ #
82
+ # account_service.getOpenTickets
83
+ # => {... lots of information here representing the list of open tickets ...}
84
+ #
85
+ class Service
86
+ # The name of the service that this object calls. Cannot be emtpy or nil.
87
+ attr_reader :service_name
88
+ attr_reader :client
89
+
90
+ def initialize(service_name, options = {})
91
+ raise ArgumentError,"Please provide a service name" if service_name.nil? || service_name.empty?
92
+
93
+ # remember the service name
94
+ @service_name = service_name;
95
+
96
+ # Collect the keys relevant to client creation and pass them on to construct
97
+ # the client if one is needed.
98
+ client_keys = [:username, :api_key, :endpoint_url]
99
+ client_options = options.inject({}) do |new_hash, pair|
100
+ if client_keys.include? pair[0]
101
+ new_hash[pair[0]] = pair[1]
102
+ end
103
+
104
+ new_hash
105
+ end
106
+
107
+ # if the options hash already has a client
108
+ # go ahead and use it
109
+ if options.has_key? :client
110
+ if !client_options.empty?
111
+ raise RuntimeError, "Attempting to construct a service both with a client, and with client initialization options. Only one or the other should be provided."
112
+ end
113
+
114
+ @client = options[:client]
115
+ else
116
+ # Accepting client initialization options here
117
+ # is a backward-compatibility feature.
118
+
119
+ if $DEBUG
120
+ $stderr.puts %q{
121
+ Creating services with Client initialization options is deprecated and may be removed
122
+ in a future release. Please change your code to create a client and obtain a service
123
+ using either client.service_named('<service_name_here>') or client['<service_name_here>']}
124
+ end
125
+
126
+ @client = SoftLayer::Client.new(client_options)
127
+ end
128
+
129
+ # this has proven to be very helpful during debugging. It helps prevent infinite recursion
130
+ # when you don't get a method call just right
131
+ @method_missing_call_depth = 0 if $DEBUG
132
+ end
133
+
134
+ # Returns a related service with the given service name. The related service
135
+ # will use the same client as this service
136
+ def related_service_named(service_name)
137
+ @client.service_named(service_name)
138
+ end
139
+
140
+ # Use this as part of a method call chain to identify a particular
141
+ # object as the target of the request. The parameter is the SoftLayer
142
+ # object identifier you are interested in. For example, this call
143
+ # would return the ticket whose ID is 35212
144
+ #
145
+ # ticket_service.object_with_id(35212).getObject
146
+ #
147
+ def object_with_id(object_of_interest)
148
+ proxy = APIParameterFilter.new(self)
149
+ return proxy.object_with_id(object_of_interest)
150
+ end
151
+
152
+ # Use this as part of a method call chain to add an object mask to
153
+ # the request. The arguments to object mask should be well formed
154
+ # Extended Object Mask strings:
155
+ #
156
+ # ticket_service.object_mask("mask[ticket.createDate, ticket.modifyDate]", "mask(SoftLayer_Some_Type).aProperty").getObject
157
+ #
158
+ # The object_mask becomes part of the request sent to the server
159
+ #
160
+ def object_mask(*args)
161
+ proxy = APIParameterFilter.new(self)
162
+ return proxy.object_mask(*args)
163
+ end
164
+
165
+ # Use this as part of a method call chain to reduce the number
166
+ # of results returned from the server. For example, if the server has a list
167
+ # of 100 entities and you only want 5 of them, you can get the first five
168
+ # by using result_limit(0,5). Then for the next 5 you would use
169
+ # result_limit(5,5), then result_limit(10,5) etc.
170
+ def result_limit(offset, limit)
171
+ proxy = APIParameterFilter.new(self)
172
+ return proxy.result_limit(offset, limit)
173
+ end
174
+
175
+ # Add an object filter to the request.
176
+ def object_filter(filter)
177
+ proxy = APIParameterFilter.new(self)
178
+ return proxy.object_filter(filter)
179
+ end
180
+
181
+ # This is the primary mechanism by which requests are made. If you call
182
+ # the service with a method it doesn't understand, it will send a call to
183
+ # the endpoint for a method of the same name.
184
+ def method_missing(method_name, *args, &block)
185
+ # During development, if you end up with a stray name in some
186
+ # code, you can end up in an infinite recursive loop as method_missing
187
+ # tries to resolve that name (believe me... it happens).
188
+ # This mechanism looks for what it considers to be an unreasonable call
189
+ # depth and kills the loop quickly.
190
+ if($DEBUG)
191
+ @method_missing_call_depth += 1
192
+ if @method_missing_call_depth > 3 # 3 is somewhat arbitrary... really the call depth should only ever be 1
193
+ @method_missing_call_depth = 0
194
+ raise "stop infinite recursion #{method_name}, #{args.inspect}"
195
+ end
196
+ end
197
+
198
+ # if we're in debug mode, we put out a little helpful information
199
+ puts "SoftLayer::Service#method_missing called #{method_name}, #{args.inspect}" if $DEBUG
200
+
201
+ if(!block && method_name.to_s.match(/[[:alnum:]]+/))
202
+ result = call_softlayer_api_with_params(method_name, nil, args);
203
+ else
204
+ result = super
205
+ end
206
+
207
+ if($DEBUG)
208
+ @method_missing_call_depth -= 1
209
+ end
210
+
211
+ return result
212
+ end
213
+
214
+ # Issue an HTTP request to call the given method from the SoftLayer API with
215
+ # the parameters and arguments given.
216
+ #
217
+ # Parameters are information _about_ the call, the object mask or the
218
+ # particular object in the SoftLayer API you are calling.
219
+ #
220
+ # Arguments are the arguments to the SoftLayer method that you wish to
221
+ # invoke.
222
+ #
223
+ # This is intended to be used in the internal
224
+ # processing of method_missing and need not be called directly.
225
+ def call_softlayer_api_with_params(method_name, parameters, args)
226
+ additional_headers = {};
227
+
228
+ # The client knows about authentication, so ask him for the auth headers
229
+ authentication_headers = self.client.authentication_headers
230
+ additional_headers.merge!(authentication_headers)
231
+
232
+ if parameters && parameters.server_object_filter
233
+ additional_headers.merge!("#{@service_name}ObjectFilter" => parameters.server_object_filter)
234
+ end
235
+
236
+ # Object masks go into the headers too.
237
+ if parameters && parameters.server_object_mask && parameters.server_object_mask.count != 0
238
+ object_mask = parameters.server_object_mask
239
+ object_mask_string = object_mask.count == 1 ? object_mask[0] : "[#{object_mask.join(',')}]"
240
+
241
+ additional_headers.merge!("SoftLayer_ObjectMask" => { "mask" => object_mask_string }) unless object_mask_string.empty?
242
+ end
243
+
244
+ # Result limits go into the headers
245
+ if (parameters && parameters.server_result_limit)
246
+ additional_headers.merge!("resultLimit" => { "limit" => parameters.server_result_limit, "offset" => (parameters.server_result_offset || 0) })
247
+ end
248
+
249
+ # Add an object id to the headers.
250
+ if parameters && parameters.server_object_id
251
+ additional_headers.merge!("#{@service_name}InitParameters" => { "id" => parameters.server_object_id })
252
+ end
253
+
254
+ # This is a workaround for a potential problem that arises from mis-using the
255
+ # API. If you call SoftLayer_Virtual_Guest and you call the getObject method
256
+ # but pass a virtual guest as a parameter, what happens is the getObject method
257
+ # is called through an HTTP POST verb and the API creates a new VirtualServer that
258
+ # is a copy of the one you passed in.
259
+ #
260
+ # The counter-intuitive creation of a new Virtual Server is unexpected and, even worse,
261
+ # is something you can be billed for. To prevent that, we ignore the request
262
+ # body on a "getObject" call and print out a warning.
263
+ if (method_name == :getObject) && (nil != args) && (!args.empty?) then
264
+ $stderr.puts "Warning - The getObject method takes no parameters. The parameters you have provided will be ignored."
265
+ args = nil
266
+ end
267
+
268
+ # Collect all the different header pieces into a single hash that
269
+ # will become the first argument to the call.
270
+ call_headers = {
271
+ "headers" => additional_headers
272
+ }
273
+
274
+ begin
275
+ call_value = xmlrpc_client.call(method_name.to_s, call_headers, *args)
276
+ rescue XMLRPC::FaultException => e
277
+ puts "A XMLRPC Fault was returned #{e}" if $DEBUG
278
+ raise
279
+ end
280
+
281
+ return call_value
282
+ end
283
+
284
+ # If this is not defined for Service, then when you print a service object
285
+ # the code will try to convert it to an array and end up calling method_missing
286
+ #
287
+ # We define this here to prevent odd calls to the Softlayer API
288
+ def to_ary
289
+ nil
290
+ end
291
+
292
+ private
293
+
294
+ def xmlrpc_client()
295
+ if !@xmlrpc_client
296
+ @xmlrpc_client = XMLRPC::Client.new2(URI.join(@client.endpoint_url,@service_name).to_s)
297
+
298
+ # this is a workaround for a bug in later versions of the XML-RPC client in Ruby Core.
299
+ # see https://bugs.ruby-lang.org/issues/8182
300
+ @xmlrpc_client.http_header_extra = {
301
+ "Accept-Encoding" => "identity",
302
+ "User-Agent" => @client.user_agent
303
+ }
304
+
305
+ if $DEBUG
306
+ if !@xmlrpc_client.respond_to?(:http)
307
+ class << @xmlrpc_client
308
+ def http
309
+ return @http
310
+ end
311
+ end
312
+ end
313
+
314
+ @xmlrpc_client.http.set_debug_output($stderr)
315
+ end # $DEBUG
316
+ end
317
+
318
+ @xmlrpc_client
319
+ end
320
+ end # Service class
321
+ end # module SoftLayer
@@ -20,25 +20,24 @@
20
20
  # THE SOFTWARE.
21
21
  #
22
22
 
23
- # The SoftLayer Module
24
- #
23
+ require 'rubygems'
24
+
25
25
  # This module is used to provide a namespace for SoftLayer code. It also declares a number of
26
26
  # global variables:
27
27
  # - <tt>$SL_API_USERNAME</tt> - The default username passed by clients to the server for authentication.
28
28
  # 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
29
29
  # - <tt>$SL_API_KEY</tt> - The default API key passed by clients to the server for authentication.
30
30
  # 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
31
- # - <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
31
+ # - <tt>$SL_API_BASE_URL</tt>- The default URL used to access the SoftLayer API. This defaults to the value of <tt>SoftLayer::API_PUBLIC_ENDPOINT</tt>
32
32
  #
33
-
34
33
  module SoftLayer
35
- VERSION = "1.0.8" # version history at the bottom of the file.
34
+ VERSION = "2.0.0" # version history in the CHANGELOG.textile file at the root of the source
36
35
 
37
36
  # The base URL of the SoftLayer API's REST-like endpoints available to the public internet.
38
- API_PUBLIC_ENDPOINT = 'https://api.softlayer.com/rest/v3/'
37
+ API_PUBLIC_ENDPOINT = 'https://api.softlayer.com/xmlrpc/v3/'
39
38
 
40
39
  # The base URL of the SoftLayer API's REST-like endpoints available through SoftLayer's private network
41
- API_PRIVATE_ENDPOINT = 'https://api.service.softlayer.com/rest/v3/'
40
+ API_PRIVATE_ENDPOINT = 'https://api.service.softlayer.com/xmlrpc/v3/'
42
41
 
43
42
  #
44
43
  # These globals can be used to simplify client creation
@@ -56,3 +55,8 @@ module SoftLayer
56
55
  # The base URL used for the SoftLayer API's
57
56
  $SL_API_BASE_URL = SoftLayer::API_PUBLIC_ENDPOINT
58
57
  end # module SoftLayer
58
+
59
+ #
60
+ # History:
61
+ #
62
+ # The history has been moved to the CHANGELOG.textile file in the source directory
@@ -20,28 +20,75 @@
20
20
  # THE SOFTWARE.
21
21
  #
22
22
 
23
+ # Ruby Hash Class
23
24
  class Hash
24
- def to_sl_object_mask(base = "")
25
- return base if(self.empty?)
25
+ def __valid_root_property_key?(key_string)
26
+ return key_string == "mask" || (0 == (key_string =~ /\Amask\([a-z][a-z0-9_]*\)\z/i))
27
+ end
28
+
29
+ def to_sl_object_mask()
30
+ raise RuntimeError, "An object mask must contain properties" if empty?
31
+ raise RuntimeError, "An object mask must start with root properties" if keys().find { |key| !__valid_root_property_key?(key) }
26
32
 
27
- # ask the children to convert themselves with the key as the base
28
- masked_children = self.map { |key, value| result = value.to_sl_object_mask(key); }.flatten
33
+ key_strings = __sl_object_mask_properties_for_keys();
34
+ key_strings.count > 1 ? "[#{key_strings.join(',')}]" : "#{key_strings[0]}"
35
+ end
29
36
 
30
- # now resolve the children with respect to the base passed in.
31
- masked_children.map { |mask_item| mask_item.to_sl_object_mask(base) }
37
+ def to_sl_object_mask_property()
38
+ key_strings = __sl_object_mask_properties_for_keys();
39
+ "#{key_strings.join(',')}"
40
+ end
41
+
42
+ def __sl_object_mask_properties_for_keys
43
+ key_strings = [];
44
+
45
+ each do |key, value|
46
+ return "" if !value
47
+
48
+ string_for_key = key.to_sl_object_mask_property
49
+
50
+ if value.kind_of?(String) || value.kind_of?(Symbol) then
51
+ string_for_key = "#{string_for_key}.#{value.to_sl_object_mask_property}"
52
+ end
53
+
54
+ if value.kind_of?(Array) || value.kind_of?(Hash) then
55
+ value_string = value.to_sl_object_mask_property
56
+ if value_string && !value_string.empty?
57
+ string_for_key = "#{string_for_key}[#{value_string}]"
58
+ end
59
+ end
60
+
61
+ key_strings.push(string_for_key)
62
+ end
63
+
64
+ key_strings
32
65
  end
33
66
  end
34
67
 
68
+ # Ruby Array Class
35
69
  class Array
36
- def to_sl_object_mask(base = "")
37
- return base if self.empty?
38
- self.map { |item| item.to_sl_object_mask(base) }.flatten
70
+ # Returns a string representing the object mask content represented by the
71
+ # Array. Each value in the array is converted to its object mask eqivalent
72
+ def to_sl_object_mask_property()
73
+ return "" if self.empty?
74
+ property_content = map { |item| item.to_sl_object_mask_property() }.flatten.join(",")
75
+ "#{property_content}"
39
76
  end
40
77
  end
41
78
 
79
+ # Ruby String Class
42
80
  class String
43
- def to_sl_object_mask(base = "")
44
- return base if self.empty?
45
- base.empty? ? self : "#{base}.#{self}"
81
+ # Returns a string representing the object mask content represented by the
82
+ # String. Strings are simply represented as copies of themselves. We make
83
+ # a copy in case the original String is modified somewhere along the way
84
+ def to_sl_object_mask_property()
85
+ return self.strip
46
86
  end
47
- end
87
+ end
88
+
89
+ # Ruby Symbol Class
90
+ class Symbol
91
+ def to_sl_object_mask()
92
+ self.to_s.to_sl_object_mask()
93
+ end
94
+ end