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,131 @@
1
+
2
+ module SoftLayer
3
+ # An <tt>APIParameterFilter</tt> is an intermediary object that understands how
4
+ # to accept the other API parameter filters and carry their values to
5
+ # <tt>method_missing</tt> in <tt>Service</tt>. Instances of this class are created
6
+ # internally by the <tt>Service</tt> in its handling of a method call and you
7
+ # should not have to create instances of this class directly.
8
+ #
9
+ # Instead, to use an API filter, you add a filter method to the call
10
+ # chain when you call a method through a <tt>SoftLayer::Service</tt>
11
+ #
12
+ # For example, given a <tt>SoftLayer::Service</tt> instance called <tt>account_service</tt>
13
+ # you could take advantage of the API filter that identifies a particular
14
+ # object known to that service using the <tt>object_with_id</tt> method :
15
+ #
16
+ # account_service.object_with_id(91234).getSomeAttribute
17
+ #
18
+ # The invocation of <tt>object_with_id</tt> will cause an instance of this
19
+ # class to be created with the service as its target.
20
+ #
21
+ class APIParameterFilter
22
+ attr_reader :target
23
+ attr_reader :parameters
24
+
25
+ def initialize(target, starting_parameters = nil)
26
+ @target = target
27
+ @parameters = starting_parameters || {}
28
+ end
29
+
30
+ # Adds an API filter that narrows the scope of a call to an object with
31
+ # a particular ID. For example, if you want to get the ticket
32
+ # with an ID of 12345 from the ticket service you might use
33
+ #
34
+ # ticket_service.object_with_id(12345).getObject
35
+ def object_with_id(value)
36
+ # we create a new object in case the user wants to store off the
37
+ # filter chain and reuse it later
38
+ APIParameterFilter.new(self.target, @parameters.merge({ :server_object_id => value }))
39
+ end
40
+
41
+ # Use this as part of a method call chain to add an object mask to
42
+ # the request. The arguments to object mask should be well formed
43
+ # Extended Object Mask strings:
44
+ #
45
+ # ticket_service.object_mask(
46
+ # "mask[createDate, modifyDate]",
47
+ # "mask(SoftLayer_Some_Type).aProperty").getObject
48
+ #
49
+ # The object_mask becomes part of the request sent to the server
50
+ #
51
+ def object_mask(*args)
52
+ raise ArgumentError, "object_mask expects well-formatted root object mask strings" if args.empty? || (1 == args.count && !args[0])
53
+ raise ArgumentError, "object_mask expects well-formatted root object mask strings" if args.find { |arg| !(arg.kind_of?(String)) }
54
+
55
+ object_mask = (@parameters[:object_mask] || []) + args
56
+
57
+ # we create a new object in case the user wants to store off the
58
+ # filter chain and reuse it later
59
+ APIParameterFilter.new(self.target, @parameters.merge({ :object_mask => object_mask }));
60
+ end
61
+
62
+ # Adds a result limit which helps you page through a long list of entities
63
+ #
64
+ # The offset is the index of the first item you wish to have returned
65
+ # The limit describes how many items you wish the call to return.
66
+ #
67
+ # For example, if you wanted to get five open tickets from the account
68
+ # starting with the tenth item in the open tickets list you might call
69
+ #
70
+ # account_service.result_limit(10, 5).getOpenTickets
71
+ def result_limit(offset, limit)
72
+ # we create a new object in case the user wants to store off the
73
+ # filter chain and reuse it later
74
+ APIParameterFilter.new(self.target, @parameters.merge({ :result_offset => offset, :result_limit => limit }))
75
+ end
76
+
77
+ # Adds an object_filter to the result. An Object Filter allows you
78
+ # to specify criteria which are used to filter the results returned
79
+ # by the server.
80
+ def object_filter(filter)
81
+ raise ArgumentError, "Object mask expects mask properties" if filter.nil?
82
+
83
+ # we create a new object in case the user wants to store off the
84
+ # filter chain and reuse it later
85
+ APIParameterFilter.new(self.target, @parameters.merge({:object_filter => filter}));
86
+ end
87
+
88
+ # A utility method that returns the server object ID (if any) stored
89
+ # in this parameter set.
90
+ def server_object_id
91
+ self.parameters[:server_object_id]
92
+ end
93
+
94
+ # a utility method that returns the object mask (if any) stored
95
+ # in this parameter set.
96
+ def server_object_mask
97
+ self.parameters[:object_mask]
98
+ end
99
+
100
+ # a utility method that returns the starting index of the result limit (if any) stored
101
+ # in this parameter set.
102
+ def server_result_limit
103
+ self.parameters[:result_limit]
104
+ end
105
+
106
+ # a utility method that returns the starting index of the result limit offset (if any) stored
107
+ # in this parameter set.
108
+ def server_result_offset
109
+ self.parameters[:result_offset]
110
+ end
111
+
112
+ def server_object_filter
113
+ self.parameters[:object_filter]
114
+ end
115
+
116
+ # This allows the filters to be used at the end of a long chain of calls that ends
117
+ # at a service.
118
+ def method_missing(method_name, *args, &block)
119
+ puts "SoftLayer::APIParameterFilter#method_missing called #{method_name}, #{args.inspect}" if $DEBUG
120
+
121
+ if(!block && method_name.to_s.match(/[[:alnum:]]+/))
122
+ result = @target.call_softlayer_api_with_params(method_name, self, args)
123
+ else
124
+ result = super
125
+ end
126
+
127
+ result
128
+ end
129
+ end
130
+
131
+ end # module SoftLayer
@@ -0,0 +1,100 @@
1
+ module SoftLayer
2
+ # Initialize an instance of the Client class. You pass in the service name
3
+ # and optionally hash arguments specifying how the client should access the
4
+ # SoftLayer API.
5
+ #
6
+ # The following symbols can be used as hash arguments to pass options to the constructor:
7
+ # - <tt>:username</tt> - a non-empty string providing the username to use for requests to the service
8
+ # - <tt>:api_key</tt> - a non-empty string providing the api key to use for requests to the service
9
+ # - <tt>:endpoint_url</tt> - a non-empty string providing the endpoint URL to use for requests to the service
10
+ #
11
+ # If any of the options above are missing then the constructor will try to use the corresponding
12
+ # global variable declared in the SoftLayer Module:
13
+ # - <tt>$SL_API_USERNAME</tt>
14
+ # - <tt>$SL_API_KEY</tt>
15
+ # - <tt>$SL_API_BASE_URL</tt>
16
+ #
17
+ class Client
18
+ # A username passed as authentication for each request. Cannot be emtpy or nil.
19
+ attr_reader :username
20
+
21
+ # An API key passed as part of the authentication of each request. Cannot be emtpy or nil.
22
+ attr_reader :api_key
23
+
24
+ # The base URL for requests that are passed to the server. Cannot be emtpy or nil.
25
+ attr_reader :endpoint_url
26
+
27
+ # A string passsed as the value for the User-Agent header when requests are sent to SoftLayer API.
28
+ attr_accessor :user_agent
29
+
30
+ def initialize(options = {})
31
+ @services = { }
32
+
33
+ settings = Config.client_settings(options)
34
+
35
+ # pick up the username from the options, the global, or assume no username
36
+ @username = settings[:username] || ""
37
+
38
+ # do a similar thing for the api key
39
+ @api_key = settings[:api_key] || ""
40
+
41
+ # and the endpoint url
42
+ @endpoint_url = settings[:endpoint_url] || API_PUBLIC_ENDPOINT
43
+
44
+ @user_agent = settings[:user_agent] || "softlayer_api gem/#{SoftLayer::VERSION} (Ruby #{RUBY_PLATFORM}/#{RUBY_VERSION})"
45
+
46
+ raise "A SoftLayer Client requires a username" if !@username || @username.empty?
47
+ raise "A SoftLayer Client requires an api_key" if !@api_key || @api_key.empty?
48
+ raise "A SoftLayer Clietn requires an enpoint URL" if !@endpoint_url || @endpoint_url.empty?
49
+ end
50
+
51
+ # return a hash of the authentication headers for the client
52
+ def authentication_headers
53
+ {
54
+ "authenticate" => {
55
+ "username" => @username,
56
+ "apiKey" => @api_key
57
+ }
58
+ }
59
+ end
60
+
61
+ # Returns a service with the given name.
62
+ #
63
+ # If a service has already been created by this client that same service
64
+ # will be returned each time it is called for by name. Otherwise the system
65
+ # will try to construct a new service object and return that.
66
+ #
67
+ #
68
+ # If the service has to be created then the service_options will be passed
69
+ # along to the creative function. However, when returning a previously created
70
+ # Service, the service_options will be ignored.
71
+ #
72
+ # If the service_name provided does not start with 'SoftLayer__' that prefix
73
+ # will be added
74
+ def service_named(service_name, service_options = {})
75
+ raise ArgumentError,"Please provide a service name" if service_name.nil? || service_name.empty?
76
+
77
+ # strip whitespace from service_name and
78
+ # ensure that it start with "SoftLayer_".
79
+ #
80
+ # if it does not, then add it
81
+ service_name.strip!
82
+ if not service_name =~ /\ASoftLayer_/
83
+ service_name = "SoftLayer_#{service_name}"
84
+ end
85
+
86
+ # if we've already created this service, just return it
87
+ # otherwise create a new service
88
+ service_key = service_name.to_sym
89
+ if !@services.has_key?(service_key)
90
+ @services[service_key] = SoftLayer::Service.new(service_name, {:client => self}.merge(service_options))
91
+ end
92
+
93
+ @services[service_key]
94
+ end
95
+
96
+ def [](service_name)
97
+ service_named(service_name)
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,77 @@
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
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+ #
22
+
23
+ require 'configparser'
24
+
25
+ module SoftLayer
26
+ class Config
27
+ def Config.globals_settings
28
+ result = {}
29
+ result[:username] = $SL_API_USERNAME if $SL_API_USERNAME
30
+ result[:api_key] = $SL_API_KEY if $SL_API_KEY
31
+ result[:endpoint_url] = $SL_API_BASE_URL ? $SL_API_BASE_URL : API_PUBLIC_ENDPOINT
32
+ result
33
+ end
34
+
35
+ def Config.environment_settings
36
+ result = {}
37
+ result[:username] = ENV["SL_USERNAME"] if ENV["SL_USERNAME"]
38
+ result[:api_key] = ENV["SL_API_KEY"] if ENV["SL_API_KEY"]
39
+ result
40
+ end
41
+
42
+ FILE_LOCATIONS = ['/etc/softlayer.conf', '~/.softlayer']
43
+
44
+ def Config.file_settings(*additional_files)
45
+ result = {}
46
+
47
+ search_path = FILE_LOCATIONS
48
+ search_path = search_path + additional_files if additional_files
49
+ search_path = search_path.map { |file_path| File.expand_path(file_path) }
50
+
51
+ search_path.each do |file_path|
52
+
53
+ if File.readable? file_path
54
+ config = ConfigParser.new file_path
55
+ softlayer_section = config["softlayer"]
56
+
57
+ if softlayer_section
58
+ result[:username] = softlayer_section['username'] if softlayer_section['username']
59
+ result[:endpoint_url] = softlayer_section['endpoint_url'] if softlayer_section['endpoint_url']
60
+ result[:api_key] = softlayer_section['api_key'] if softlayer_section['api_key']
61
+ end
62
+ end
63
+ end
64
+
65
+ result
66
+ end
67
+
68
+ def Config.client_settings(provided_settings = {})
69
+ settings = file_settings
70
+ settings.merge! environment_settings
71
+ settings.merge! globals_settings
72
+ settings.merge! provided_settings
73
+
74
+ settings
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,232 @@
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
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+ #
22
+
23
+ module SoftLayer
24
+ OBJECT_FILTER_OPERATORS = [
25
+ '*=', # Contains (ignoring case)
26
+ '^=', # Begins with (ignoring case)
27
+ '$=', # Ends with (ignoring_case)
28
+ '_=', # Matches (ignoring case)
29
+ '!=', # Is not Equal To (case sensitive)
30
+ '<=', # Less than or Equal To (case sensitive)
31
+ '>=', # Greater than or Equal To (case sensitive)
32
+ '<', # Less Than (case sensitive)
33
+ '>', # Greater Than (case sensitive)
34
+ '~', # Contains (case sensitive)
35
+ '!~' # Does not Contain (case sensitive)
36
+ ]
37
+
38
+ # A class whose instances represent an Object Filter operation.
39
+ class ObjectFilterOperation
40
+ attr_reader :operator
41
+ attr_reader :value
42
+
43
+ def initialize(operator, value)
44
+ raise ArgumentException, "An unknown operator was given" if !OBJECT_FILTER_OPERATORS.include?(operator.strip)
45
+ raise ArgumentException, "Expected a value" if value.nil? || (value.respond_to?(:empty?) && value.empty?)
46
+
47
+ @operator = operator.strip
48
+ @value = value.strip
49
+ end
50
+
51
+ def to_h
52
+ { 'operation' => "#{operator} #{value}"}
53
+ end
54
+ end
55
+
56
+ # Routines that are valid within the block provided to a call to
57
+ # ObjectFilter.build.
58
+ class ObjectFilterBlockHandler
59
+ # contains wihout considering case
60
+ def contains(value)
61
+ ObjectFilterOperation.new('*=', value)
62
+ end
63
+
64
+ # case insensitive begins with
65
+ def begins_with(value)
66
+ ObjectFilterOperation.new('^=', value)
67
+ end
68
+
69
+ # case insensitive ends with
70
+ def ends_with(value)
71
+ ObjectFilterOperation.new('$=', value)
72
+ end
73
+
74
+ # matches exactly (ignoring case)
75
+ def is(value)
76
+ ObjectFilterOperation.new('_=', value)
77
+ end
78
+
79
+ def is_not(value)
80
+ ObjectFilterOperation.new('!=', value)
81
+ end
82
+
83
+ def is_greater_than(value)
84
+ ObjectFilterOperation.new('>', value)
85
+ end
86
+
87
+ def is_less_than(value)
88
+ ObjectFilterOperation.new('<', value)
89
+ end
90
+
91
+ def is_greater_or_equal_to(value)
92
+ ObjectFilterOperation.new('>=', value)
93
+ end
94
+
95
+ def is_less_or_equal_to(value)
96
+ ObjectFilterOperation.new('<=', value)
97
+ end
98
+
99
+ def contains_exactly(value)
100
+ ObjectFilterOperation.new('~', value)
101
+ end
102
+
103
+ def does_not_contain(value)
104
+ ObjectFilterOperation.new('!~', value)
105
+ end
106
+ end
107
+
108
+ # An ObjectFilter is a hash that, when asked to provide
109
+ # an value for an unknown key, will create a sub element
110
+ # at that key that is itself an object filter. So if you
111
+ # start with an empty object filter and ask for <tt>object_filter["foo"]</tt>
112
+ # then foo will be +added+ to the object and the value of that
113
+ # key will be an Object Filter <tt>{ "foo" => {} }</tt>
114
+ #
115
+ # This allows you to create object filters by chaining [] calls:
116
+ # object_filter["foo"]["bar"]["baz"] = 3 yields {"foo" => { "bar" => {"baz" => 3}}}
117
+ #
118
+ class ObjectFilter < Hash
119
+ # The default initialize for a hash is overridden
120
+ # so that object filters create sub-filters when asked
121
+ # for missing keys.
122
+ def initialize
123
+ super do |hash, key|
124
+ hash[key] = ObjectFilter.new
125
+ end
126
+ end
127
+
128
+ # Builds an object filter with the given key path, a dot separated list of property keys.
129
+ # The filter itself can be provided as a query string (in the query parameter)
130
+ # or by providing a block that calls routines in the ObjectFilterBlockHandler class.
131
+ def self.build(key_path, query = nil, &block)
132
+ raise ArgumentError, "The key path to build cannot be empty" if !key_path
133
+
134
+ # Split the keypath into its constituent parts and notify the user
135
+ # if there are no parts
136
+ keys = key_path.split('.')
137
+ raise ArgumentError, "The key path to build cannot be empty" if keys.empty?
138
+
139
+ # This will be the result of the build
140
+ result = ObjectFilter.new
141
+
142
+ # chase down the key path to the last-but-one key
143
+ current_level = result
144
+ while keys.count > 1
145
+ current_level = current_level[keys.shift]
146
+ end
147
+
148
+ # if there is a block, then the query will come from
149
+ # calling the block. We warn in debug mode if you override a
150
+ # query that was passed directly with the value from a block.
151
+ if block
152
+ $stderr.puts "The query from the block passed to ObjectFilter:build will override the query passed as a parameter" if $DEBUG && query
153
+ block_handler = ObjectFilterBlockHandler.new
154
+ query = block_handler.instance_eval(&block)
155
+ end
156
+
157
+ # If we have a query, we assign it's value to the last key
158
+ # otherwise, we build an emtpy filter at the bottom
159
+ if query
160
+ case
161
+ when query.kind_of?(Numeric)
162
+ current_level[keys.shift] = { 'operation' => query }
163
+ when query.kind_of?(SoftLayer::ObjectFilterOperation)
164
+ current_level[keys.shift] = query.to_h
165
+ when query.kind_of?(String)
166
+ current_level[keys.shift] = query_to_filter_operation(query)
167
+ when query.kind_of?(Hash)
168
+ current_level[keys.shift] = query
169
+ else
170
+ current_level[keys.shift]
171
+ end
172
+ else
173
+ current_level[keys.shift]
174
+ end
175
+
176
+ result
177
+ end
178
+
179
+ # This method tries to simplify creating a correct object filter structure
180
+ # by allowing the caller to provide a string in a simple query language.
181
+ # It then translates that string into an Object Filter operation structure
182
+ #
183
+ # Object Filter comparisons are done using operators. Some operators make
184
+ # case sensitive comparisons and some do not. The general form of an Object
185
+ # Filter operation is an operator follwed by the value used in the comparison.
186
+ # e.g.
187
+ # "*= smaug"
188
+ #
189
+ # The query language also accepts some aliases using asterisks
190
+ # in a regular-expression-like way. Those aliases look like:
191
+ #
192
+ # 'value' Exact value match (translates to '_= value')
193
+ # 'value*' Begins with value (translates to '^= value')
194
+ # '*value' Ends with value (translates to '$= value')
195
+ # '*value*' Contains value (translates to '*= value')
196
+ #
197
+ def self.query_to_filter_operation(query)
198
+ if query.kind_of? String then
199
+ query.strip!
200
+
201
+ begin
202
+ return { 'operation' => Integer(query) }
203
+ rescue
204
+ end
205
+
206
+ operator = OBJECT_FILTER_OPERATORS.find do | operator_string |
207
+ query[0 ... operator_string.length] == operator_string
208
+ end
209
+
210
+ if operator then
211
+ operation = "#{operator} #{query[operator.length..-1].strip}"
212
+ else
213
+ case query
214
+ when /\A\*(.*)\*\Z/
215
+ operation = "*= #{$1}"
216
+ when /\A\*(.*)/
217
+ operation = "$= #{$1}"
218
+ when /\A(.*)\*\Z/
219
+ operation = "^= #{$1}"
220
+ else
221
+ operation = "_= #{query}"
222
+ end #case
223
+ end #if
224
+ else
225
+ operation = query.to_i
226
+ end # query is string
227
+
228
+ { 'operation' => operation }
229
+ end # query_to_filter_operation
230
+
231
+ end # ObjectFilter
232
+ end # SoftLayer