collins_client 0.2.7

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,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