collins_client 0.2.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,46 @@
1
+ require 'collins/api/util/errors'
2
+
3
+ module Collins; module Api; module Util
4
+
5
+ module Responses
6
+ include Collins::Api::Util::Errors
7
+
8
+ protected
9
+ def parse_response response, options
10
+ do_raise = options[:raise] != false
11
+ if options.include?(:expects) && ![options[:expects]].flatten.include?(response.code) then
12
+ handle_error(response) if do_raise
13
+ if options.include?(:default) then
14
+ return options[:default]
15
+ else
16
+ raise UnexpectedResponseError.new("Expected code #{options[:expects]}, got #{response.code}")
17
+ end
18
+ end
19
+ handle_error(response) if do_raise
20
+ json = response.parsed_response
21
+ if options.include?(:as) then
22
+ case options[:as]
23
+ when :asset
24
+ json = Collins::Asset.from_json(json)
25
+ when :bare_asset
26
+ json = Collins::Asset.from_json(json, true)
27
+ when :data
28
+ json = json["data"]
29
+ when :status
30
+ json = json["data"]["SUCCESS"]
31
+ when :message
32
+ json = json["data"]["MESSAGE"]
33
+ when :paginated
34
+ json = json["data"]["Data"]
35
+ end
36
+ end
37
+ if block_given? then
38
+ yield(json)
39
+ else
40
+ json
41
+ end
42
+ end
43
+
44
+ end # Responses module
45
+
46
+ end; end; end
@@ -0,0 +1,311 @@
1
+ require 'collins/address'
2
+ require 'collins/ipmi'
3
+ require 'collins/power'
4
+ require 'collins/state'
5
+ require 'date'
6
+
7
+ module Collins
8
+
9
+ # Represents the basic notion of a collins asset
10
+ class Asset
11
+
12
+ # Default time format when displaying dates associated with an asset
13
+ DATETIME_FORMAT = "%F %T"
14
+
15
+ # Asset finder related parameter descriptions
16
+ # @note these exist here instead of the API module for convenience
17
+ module Find
18
+ # Find API parameters that are dates
19
+ # @return [Array<String>] Date related query parameters
20
+ DATE_PARAMS = [
21
+ "createdAfter", "createdBefore", "updatedAfter", "updatedBefore"
22
+ ]
23
+ # Find API parameters that are not dates
24
+ # This list exists so that when assets are being queries, we know what keys in the find hash
25
+ # are attributes of the asset (such as hostname), and which are nort (such as sort or page).
26
+ # @return [Array,<String>] Non-date related query parameters that are 'reserved'
27
+ GENERAL_PARAMS = [
28
+ "details", "tag", "type", "status", "page", "size", "sort", "state", "operation", "remoteLookup"
29
+ ]
30
+ # @return [Array<String>] DATE_PARAMS plus GENERAL_PARAMS
31
+ ALL_PARAMS = DATE_PARAMS + GENERAL_PARAMS
32
+
33
+ class << self
34
+ def to_a
35
+ Collins::Asset::Find::ALL_PARAMS
36
+ end
37
+ def valid? key
38
+ to_a.include?(key.to_s)
39
+ end
40
+ end
41
+ end
42
+
43
+ include Collins::Util
44
+
45
+ # @return [Array<CollinsAddress>] Addresses associated with asset
46
+ attr_reader :addresses
47
+ # @return [DateTime,NilClass] Timestamp. Can be nil
48
+ attr_reader :created, :updated, :deleted
49
+ # @return [Fixnum] Asset ID or 0
50
+ attr_reader :id
51
+ # @return [Collins::Ipmi] IPMI information
52
+ attr_reader :ipmi
53
+ # @return [String] multi-collins location
54
+ attr_reader :location
55
+ # @return [Collins::Power] Power configuration information
56
+ attr_reader :power
57
+ # @return [Collins::AssetState] Asset state, or nil
58
+ attr_reader :state
59
+ # @return [String] Asset status, or empty string
60
+ attr_reader :status
61
+ # @return [String] Asset tag, or empty string
62
+ attr_reader :tag
63
+ # @return [String] Asset type, or empty string
64
+ attr_reader :type
65
+ # @return [Hash] All additional asset metadata
66
+ attr_reader :extras
67
+
68
+ class << self
69
+ # Given a Hash deserialized from JSON, convert to an Asset
70
+ # @param [Hash] json_hash Asset representation
71
+ # @param [Boolean] bare_asset Exists for API compatability, largely not needed
72
+ # @return [Collins::Asset] The asset
73
+ # @raise [Collins::CollinsError] If the specified hash is invalid
74
+ def from_json json_hash, bare_asset = false
75
+ (raise Collins::CollinsError.new("Invalid JSON specified for Asset.from_json")) if (json_hash.nil? || !json_hash.is_a?(Hash))
76
+ json = deep_copy_hash json_hash
77
+ json = if json["data"] then json["data"] else json end
78
+ if bare_asset or !json.include?("ASSET") then
79
+ asset = Collins::Asset.new json
80
+ else
81
+ asset = Collins::Asset.new json.delete("ASSET")
82
+ end
83
+ asset.send('ipmi='.to_sym, Collins::Ipmi.from_json(json.delete("IPMI")))
84
+ asset.send('addresses='.to_sym, Collins::Address.from_json(json.delete("ADDRESSES")))
85
+ asset.send('power='.to_sym, Collins::Power.from_json(json.delete("POWER")))
86
+ asset.send('location=', json.delete("LOCATION"))
87
+ asset.send('extras=', json)
88
+ asset
89
+ end
90
+
91
+ # Convenience method for parsing asset ISO8601 date times
92
+ # @param [String] s the ISO8601 datetime
93
+ # @return [DateTime]
94
+ def format_date_string s
95
+ parsed = DateTime.parse(s)
96
+ parsed.strftime("%FT%T")
97
+ end
98
+ end
99
+
100
+ # Create an Asset
101
+ # @param [Hash] opts Asset parameters
102
+ # @option opts [String] :tag The asset tag
103
+ # @option opts [String] :created The creation DateTime
104
+ # @option opts [Fixnum] :id The ID of the asset
105
+ # @option opts [String] :status The asset status
106
+ # @option opts [String] :type The asset type
107
+ # @option opts [String] :updated The update DateTime
108
+ # @option opts [String] :deleted The delete DateTime
109
+ def initialize opts = {}
110
+ @extras = {}
111
+ @addresses = []
112
+ if opts.is_a?(String) then
113
+ model = {:tag => opts}
114
+ else
115
+ model = opts
116
+ end
117
+ hash = symbolize_hash(model).inject({}) do |result, (k,v)|
118
+ result[k.downcase] = v
119
+ result
120
+ end
121
+ @created = parse_datetime hash.delete(:created).to_s
122
+ @id = hash.delete(:id).to_s.to_i
123
+ @status = hash.delete(:status).to_s
124
+ @tag = hash.delete(:tag).to_s
125
+ @type = hash.delete(:type).to_s
126
+ @state = Collins::AssetState.from_json(hash.delete(:state))
127
+ @updated = parse_datetime hash.delete(:updated).to_s
128
+ @deleted = parse_datetime hash.delete(:deleted).to_s
129
+ hash.each {|k,v| @extras[k] = v}
130
+ end
131
+
132
+ # @return [Collins::Address,NilClass] First available backend address
133
+ def backend_address
134
+ backend_addresses.first if backend_address?
135
+ end
136
+ # @return [Boolean] True if asset has a backend address
137
+ def backend_address?
138
+ backend_addresses.length > 0
139
+ end
140
+ # @return [Array<Collins::Address>] Array of backend addresses
141
+ def backend_addresses
142
+ addresses.select{|a| a.is_private?}
143
+ end
144
+
145
+ # @deprecated Users are encouraged to use {#backend_address}
146
+ # @return [String,NilClass] Address of first available backend address
147
+ def backend_ip_address
148
+ backend_address.address if backend_address?
149
+ end
150
+ # @deprecated Users are encouraged to uses {#backend_addresses}
151
+ # @return [Array<String>] Backend IP addresses
152
+ def backend_ip_addresses
153
+ backend_addresses.map{|a| a.address}
154
+ end
155
+
156
+ # @return [Collins::Address,NilClass] First available public address
157
+ def public_address
158
+ public_addresses.first if public_address?
159
+ end
160
+ # @return [Boolean] True if asset has a public address
161
+ def public_address?
162
+ public_addresses.length > 0
163
+ end
164
+ # @return [Array<Collins::Address>] Array of public addresses
165
+ def public_addresses
166
+ addresses.select{|a| a.is_public?}
167
+ end
168
+
169
+ # @deprecated Users are encouraged to use {#public_address}
170
+ # @return [String,NilClass] Address of first available public address
171
+ def public_ip_address
172
+ public_address.address if public_address?
173
+ end
174
+ # @deprecated Users are encouraged to uses {#public_addresses}
175
+ # @return [Array<String>] Public IP addresses
176
+ def public_ip_addresses
177
+ public_addresses.map{|a| a.address}
178
+ end
179
+
180
+ # @return [String,NilClass] Netmask of first available backend address
181
+ def backend_netmask
182
+ backend_address.netmask if backend_address?
183
+ end
184
+ # @return [Array<String>] Array of backend netmasks
185
+ def backend_netmasks
186
+ backend_addresses.map{|i| i.netmask}
187
+ end
188
+
189
+ # Return the gateway address for the specified pool, or the first gateway
190
+ # @note If there is no address in the specified pool, the gateway of the first usable address is
191
+ # used, which may not be desired.
192
+ # @param [String] pool The address pool to find a gateway on
193
+ # @return [String] Gateway address, or nil
194
+ def gateway_address pool = "default"
195
+ address = addresses.select{|a| a.pool == pool}.map{|a| a.gateway}.first
196
+ return address if address
197
+ if addresses.length > 0 then
198
+ addresses.first.gateway
199
+ else
200
+ nil
201
+ end
202
+ end
203
+
204
+ # @return [Object,NilClass] See {#method_missing}
205
+ def get_attribute name
206
+ extract(extras, "ATTRIBS", "0", name.to_s.upcase)
207
+ end
208
+
209
+ # @return [Fixnum] Number of CPU's found
210
+ def cpu_count
211
+ (extract(extras, "HARDWARE", "CPU") || []).length
212
+ end
213
+ # @return [Array<Hash>] CPU information
214
+ def cpus
215
+ extract(extras, "HARDWARE", "CPU") || []
216
+ end
217
+
218
+ # @return [Array<Hash>] Disk information
219
+ def disks
220
+ extract(extras, "HARDWARE", "DISK") || []
221
+ end
222
+ # @return [Array<Hash>] Memory information
223
+ def memory
224
+ extract(extras, "HARDWARE", "MEMORY") || []
225
+ end
226
+
227
+ # @return [Array<Hash>] NIC information
228
+ def nics
229
+ extract(extras, "HARDWARE", "NIC") || []
230
+ end
231
+ # @return [Fixnum] Number of physical interfaces
232
+ def physical_nic_count
233
+ nics.length
234
+ end
235
+ # @return [Array<String>] MAC addresses associated with assets
236
+ def mac_addresses
237
+ nics.map{|n| n["MAC_ADDRESS"]}.select{|a| !a.nil?}
238
+ end
239
+
240
+ # @return [String] Human readable asset with no meta attributes
241
+ def to_s
242
+ updated_t = format_datetime(updated, "Never")
243
+ created_t = format_datetime(created, "Never")
244
+ ipmi_i = ipmi.nil? ? "No IPMI Data" : ipmi.to_s
245
+ "Asset(id = #{id}, tag = #{tag}, status = #{status}, type = #{type}, created = #{created_t}, updated = #{updated_t}, ipmi = #{ipmi_i}, state = #{state.to_s})"
246
+ end
247
+
248
+ def respond_to? name
249
+ if extract(extras, "ATTRIBS", "0", name.to_s.upcase).nil? then
250
+ super
251
+ else
252
+ true
253
+ end
254
+ end
255
+
256
+ protected
257
+ # We do not allow these to be externally writable since we won't actually update any of the data
258
+ attr_writer :addresses, :created, :id, :ipmi, :location, :power, :state, :status, :tag, :type, :updated, :extras
259
+
260
+ # Convenience method for {#get_attribute}
261
+ #
262
+ # This 'magic' method allows you to retrieve attributes on an asset, or check if an attribute
263
+ # exists via a predicate method.
264
+ #
265
+ # @example
266
+ # real_asset.hostname # => "foo"
267
+ # bare_asset.hostname # => nil
268
+ # real_asset.hostname? # => true
269
+ # bare_asset.hostname? # => false
270
+ #
271
+ # @note This is never called directly
272
+ # @return [NilClass,Object] Nil if attribute not found, otherwise the attribute value
273
+ def method_missing(m, *args, &block)
274
+ name = m.to_s.upcase
275
+ is_bool = name.end_with?('?')
276
+ if is_bool then
277
+ name = name.sub('?', '')
278
+ respond_to?(name)
279
+ else
280
+ extract(extras, "ATTRIBS", "0", name)
281
+ end
282
+ end
283
+
284
+ def parse_datetime value
285
+ return nil if (value.nil? or value.empty?)
286
+ DateTime.parse value
287
+ end
288
+
289
+ def format_datetime value, default
290
+ if value then
291
+ value.strftime(DATETIME_FORMAT)
292
+ else
293
+ default
294
+ end
295
+ end
296
+
297
+ # Convenience method for finding something in a (potentially) deep hash
298
+ def extract(hash, *args)
299
+ begin
300
+ tmp = hash
301
+ args.each do |arg|
302
+ tmp = tmp[arg]
303
+ end
304
+ tmp
305
+ rescue
306
+ nil
307
+ end
308
+ end
309
+
310
+ end
311
+ end
@@ -0,0 +1,57 @@
1
+ module Collins
2
+
3
+ # Convenience class for making collins calls for only a single asset
4
+ class AssetClient
5
+
6
+ def initialize asset, client, logger
7
+ @asset = asset
8
+ if asset.is_a?(Collins::Asset) then
9
+ @tag = asset.tag
10
+ else
11
+ @tag = asset
12
+ end
13
+ @client = client
14
+ @logger = logger
15
+ end
16
+
17
+ def to_s
18
+ "AssetClient(asset = #{@tag}, client = #{@client})"
19
+ end
20
+
21
+ # Fill in the missing asset parameter on the dynamic method if needed
22
+ #
23
+ # If {Collins::Client} responds to the method, and the method requires an `asset_or_tag`, we
24
+ # insert the asset specified during initialization into the args array. If the method does not
25
+ # require an `asset_or_tag`, we simply proxy the method call as is. If {Collins::Client} does
26
+ # not respond to the method, we defer to `super`.
27
+ #
28
+ # @example
29
+ # collins_client.get('some_tag') # => returns that asset
30
+ # collins_client.with_asset('some_tag').get # => returns that same asset
31
+ #
32
+ # @note this method should never be called directly
33
+ def method_missing meth, *args, &block
34
+ if @client.respond_to?(meth) then
35
+ method_parameters = @client.class.instance_method(meth).parameters
36
+ asset_idx = method_parameters.find_index do |item|
37
+ item[1] == :asset_or_tag
38
+ end
39
+ if asset_idx.nil? then
40
+ @client.send(meth, *args, &block)
41
+ else
42
+ args_with_asset = args.insert(asset_idx, @tag)
43
+ logger.debug("Doing #{meth}(#{args_with_asset.join(',')}) for #{@tag}")
44
+ @client.send(meth, *args_with_asset, &block)
45
+ end
46
+ else
47
+ super
48
+ end
49
+ end
50
+
51
+ def respond_to? meth, include_private = false
52
+ @client.respond_to?(meth)
53
+ end
54
+
55
+ end
56
+
57
+ end
@@ -0,0 +1,100 @@
1
+ require 'collins/api'
2
+ require 'collins/asset_client'
3
+ require 'httparty'
4
+
5
+ module Collins
6
+
7
+ # Primary interface for interacting with collins
8
+ #
9
+ # @example
10
+ # client = Collins::Client.new :host => '...', :username => '...', :password => '...'
11
+ # client.get 'asset_tag'
12
+ class Client
13
+
14
+ # @see Collins::Api#headers
15
+ attr_reader :headers
16
+ # @see Collins::Api#host
17
+ attr_reader :host
18
+ # @see Collins::Api#locations
19
+ attr_reader :locations
20
+ # @see Collins::Api#logger
21
+ attr_reader :logger
22
+ # @see Collins::Api#timeout_i
23
+ attr_reader :timeout_i
24
+ # @see Collins::Api#password
25
+ attr_reader :password
26
+ # @return [Boolean] strict mode throws exceptions when unexpected responses occur
27
+ attr_reader :strict
28
+ # @see Collins::Api#username
29
+ attr_reader :username
30
+
31
+ include HTTParty
32
+ include Collins::Api
33
+ include Collins::Util
34
+
35
+ # Create a collins client instance
36
+ # @param [Hash] options host, username and password are required
37
+ # @option options [String] :host a scheme, hostname and port (e.g. https://hostname)
38
+ # @option options [Logger] :logger a logger to use, one is created if none is specified
39
+ # @option options [Fixnum] :timeout (10) timeout in seconds to wait for a response
40
+ # @option options [String] :username username for authentication
41
+ # @option options [String] :password password for authentication
42
+ # @option options [String] :managed_process see {#manage_process}
43
+ # @option options [Boolean] :strict (false) see {#strict}
44
+ def initialize options = {}
45
+ config = symbolize_hash options
46
+ @locations = {}
47
+ @headers = {}
48
+ @host = fix_hostname(config.fetch(:host, ""))
49
+ @logger = get_logger config.merge(:progname => 'Collins_Client')
50
+ @timeout_i = config.fetch(:timeout, 10).to_i
51
+ @username = config.fetch(:username, "")
52
+ @password = config.fetch(:password, "")
53
+ @strict = config.fetch(:strict, false)
54
+ @managed_process = config.fetch(:managed_process, nil)
55
+ require_non_empty(@host, "Collins::Client host must be specified")
56
+ require_non_empty(@username, "Collins::Client username must be specified")
57
+ require_non_empty(@password, "Collins::Client password must be specified")
58
+ end
59
+
60
+ # Interact with a collins managed process
61
+ # @param [String] name Name of process
62
+ # @raise [CollinsError] if no managed process is specified/found
63
+ # @return [Collins::ManagedState::Mixin] see mixin for more information
64
+ def manage_process name = nil
65
+ name = @managed_process if name.nil?
66
+ if name then
67
+ begin
68
+ Collins.const_get(name).new(self).run
69
+ rescue Exception => e
70
+ raise CollinsError.new(e.message)
71
+ end
72
+ else
73
+ raise CollinsError.new("No managed process specified")
74
+ end
75
+ end
76
+
77
+ # @return [String] Collins::Client(host = hostname)
78
+ def to_s
79
+ "Collins::Client(host = #{@host})"
80
+ end
81
+
82
+ # @see Collins::Api#strict?
83
+ def strict? default = false
84
+ @strict || default
85
+ end
86
+
87
+ # Use the specified asset for subsequent method calls
88
+ # @param [Collins::Asset,String] asset The asset to use for operations
89
+ # @return [Collins::AssetClient] Provides most of the same methods as {Collins::Client} but with no need to specfiy the asset for those methods
90
+ def with_asset asset
91
+ Collins::AssetClient.new(asset, self, @logger)
92
+ end
93
+
94
+ protected
95
+ def fix_hostname hostname
96
+ hostname.is_a?(String) ? hostname.gsub(/\/+$/, '') : hostname
97
+ end
98
+
99
+ end
100
+ end