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.
- checksums.yaml +4 -4
- data/README.textile +116 -66
- data/examples/{accountInformation.rb → account_info.rb} +6 -4
- data/examples/{createTicket.rb → create_ticket.rb} +6 -3
- data/examples/{openTickets.rb → open_tickets.rb} +6 -4
- data/examples/ticket_info.rb +8 -8
- data/lib/softlayer/APIParameterFilter.rb +131 -0
- data/lib/softlayer/Client.rb +100 -0
- data/lib/softlayer/Config.rb +77 -0
- data/lib/softlayer/ObjectFilter.rb +232 -0
- data/lib/softlayer/Service.rb +321 -0
- data/lib/softlayer/base.rb +11 -7
- data/lib/softlayer/object_mask_helpers.rb +60 -13
- data/lib/softlayer_api.rb +6 -1
- metadata +21 -17
- data/lib/softlayer/service.rb +0 -467
@@ -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
|
data/lib/softlayer/base.rb
CHANGED
@@ -20,25 +20,24 @@
|
|
20
20
|
# THE SOFTWARE.
|
21
21
|
#
|
22
22
|
|
23
|
-
|
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 = "
|
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/
|
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/
|
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
|
25
|
-
return
|
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
|
-
|
28
|
-
|
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
|
-
|
31
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
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
|
-
|
44
|
-
|
45
|
-
|
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
|