bjn_inventory 1.3.1 → 1.5.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9f4dd05a9291f7ca5aa3054f5247e7bdb48b2d8b
4
- data.tar.gz: b26400af160b26f373de7bcc736c4b98e1995e12
3
+ metadata.gz: dcd99bdf01564c9b9328bd816c7d682dca6ac94d
4
+ data.tar.gz: 003a564bc69f38caa487927671c7f98e4eaa201e
5
5
  SHA512:
6
- metadata.gz: 680f516945b414a66bfdf4a2ba4368d18e5738bfc5b9c179e15c3d7576cd492548e85f71bcdfbabd348dff1023b9dc003bf4af950c1b35d4cd37cebcdb68dcef
7
- data.tar.gz: 42e9b62882481e6d18605e4bd9cb0e74f3fe2fb38232d0a19fd889e94ea5a543f9a180e844bc87949c9b5350cc89d48b549bb9f218e0930c9e222af5c8f4155a
6
+ metadata.gz: 891137548bf9420e4a7bd654d23776257d9d1253d4f349439183dee2efc45cfb00717ac3ab2e9321d96b587ab902a36161d1bcc103ea09487cde7931e657147d
7
+ data.tar.gz: e839db1948ea6d4bc9861edf16547bd0304f05e308606918e1b2a89e38511bdf11340bd712c7debe44ebf5eb7d7c24e23a3969c7d7d92b45a2571de2be295c39
data/.gitignore CHANGED
@@ -9,7 +9,9 @@
9
9
  /spec/reports/
10
10
  /tmp/
11
11
  /d42/
12
+ /cache/
12
13
  /aws/
13
14
  /inv/
14
15
  tasks/package/artifacts
15
16
  /tasks/test/reports/
17
+ .ruby-version
data/README.md CHANGED
@@ -196,6 +196,171 @@ to `ec2_instance` unconditionally:
196
196
  system_type always 'ec2_instance'
197
197
  ```
198
198
 
199
+ ## Commands
200
+
201
+ ### Formatters
202
+
203
+ The inventory can be output in different formats. Two formatters
204
+ are provided which can display the inventory as model-conformant
205
+ devices: either as a JSON array or an object keyed by an identifying
206
+ field (`inventory-model`); or in the
207
+ [Ansible Dynamic Inventory](http://docs.ansible.com/ansible/latest/intro_dynamic_inventory.html)
208
+ format.
209
+
210
+ The `refresh_inventory_data` formatter formats the inventory into
211
+ groups and devices in a file tree. A `devices.json` index is produced,
212
+ with all devices, and each device also gets a file in `devices/<key>.json`.
213
+ In addition the groups are listed in a `groups.json` index, and each
214
+ has a list of devices in `groups/<group>.json`. This facilitates sharing
215
+ the inventory over the web, as well (though **bjn_inventory** is not
216
+ itself a network service).
217
+
218
+ ### Downloaders
219
+
220
+ The overall design of the software encourages you to download entries
221
+ from inventory sources in a "close to raw" manner, and let
222
+ the source merging and mapping rules transform the entries into
223
+ devices. Because the core inventory generation offers no mapping,
224
+ the downloader is also the point at which to filter out entries
225
+ that should not be devices.
226
+
227
+ For convenience, AWS downloaders are provided for instances
228
+ (`aws-ec2-source`), classic ELB (`aws-elb-source`) and RDS
229
+ (`aws-rds-source`) resources. Run each with valid AWS credentials in
230
+ your `~/.aws/credentials` file to see what the output looks like.
231
+ They each accept a `--filter` argument: in the EC2 downloader's case,
232
+ this is passed to the AWS API to filter instances according to the
233
+ attributes and tags it offers; in the case of the ELB downloader,
234
+ the syntax is the same, but it is enforced by an internal rules-
235
+ matching library. The RDS downloader also does filtering based only
236
+ on tags, using the `--tag-filters` option.
237
+
238
+ ### Service Maps
239
+
240
+ A common use case for device inventories is to be able to present
241
+ service endpoints calculated dynamically from inventory. These could
242
+ be used as-is, imported into a service discovery system, etc. To
243
+ facilitate this, a special formatter is provided which maps devices
244
+ and groups into "service endpoints", which are defined by a service
245
+ map.
246
+
247
+ A service map is a JSON object stored in a file (and provided to the
248
+ `service-map` command via the `--map` option), where the keys are
249
+ service prefixes and the values are objects. It can be arbitrarily
250
+ deeply nested, with the deepest object in the tree being a service
251
+ specifier.
252
+
253
+ A service specifier consists of the special field `hosts`, the value
254
+ of which is a list of groups. Each device in all the groups is added
255
+ to the service under the preceding prefix. The other fields in the
256
+ service specifier are arbitrary; each is passed through unchanged, so
257
+ that the "leaf" of the service map consists of an object where the
258
+ service specifier's key is joined with each of the specifier fields by
259
+ a dot (`.`); and the `hosts` field has each device's endpoint listed
260
+ with the `join_with` character (by default, a comma; or as a JSON
261
+ array).
262
+
263
+ The groups are determined by a group specification: similar to the
264
+ `ansible-from` command, a JSON object (in the file given by the
265
+ `--groups` option) with a `"group_by"` key, the value of which is a
266
+ list of fields to create groups by. Note that if you use
267
+ the `ansible-from` command, you can use the same groups file; but
268
+ Ansible's `"groups"` key, which specifies groups of groups, is ignored.
269
+
270
+ In the service prefix (the nested keys and objects which precede the
271
+ "leaf" of the service map), device fields can be given with a dollar sign
272
+ (`$`) prepended; these will be interpolated with the actual devices'
273
+ field values. This also means that trees can be copied into the map multiple
274
+ times, once for every unique value of the field in the relevant groups.
275
+
276
+ The above description sounds complicated, but the result is a fairly
277
+ simple way to map service endpoints into an arbitrarily complex tree, and
278
+ a couple of examples are probably best to illustrate the point. Let's say
279
+ you have three devices in your inventory: two in the `us-west-2` region
280
+ and one in the `eu-west-1` region. In your `us-west-2` region, one instance
281
+ has the `web` role and one has the `db` role. In the `us-west-1` region,
282
+ you just have a webserver. Your whole inventory looks like this (for
283
+ example, if you run `inventory-model --manifest manifest.json`:
284
+
285
+ ```javascript
286
+ [
287
+ { "name": "web-01",
288
+ "roles": ["web"],
289
+ "region": "us-west-2" },
290
+ { "name": "db-01",
291
+ "roles": ["db"],
292
+ "region": "us-west-2" },
293
+ { "name": "web-02",
294
+ "roles": ["web"],
295
+ "region": "eu-west-1" }
296
+ ]
297
+ ```
298
+
299
+ You create the following service map:
300
+
301
+
302
+ ```javascript
303
+ {
304
+ "services": {
305
+ "$region": {
306
+ "www": {
307
+ "hosts": ["web"],
308
+ "port": 80
309
+ }
310
+ }
311
+ },
312
+ "monitor": {
313
+ "$region": {
314
+ "nagios": {
315
+ "hosts": ["web", "db"],
316
+ "key": "monitoring-key.rsa"
317
+ }
318
+ }
319
+ }
320
+ }
321
+ ```
322
+
323
+ When you run `service-map --map map.json --manifest manifest.json --hosts-field name`, you
324
+ get the following output:
325
+
326
+ ```javascript
327
+ {
328
+ "services": {
329
+ "us-west-2": {
330
+ "www.hosts": "10.0.1.1",
331
+ "www.port": 80
332
+ },
333
+ "eu-west-1": {
334
+ "www.hosts": "10.0.10.1",
335
+ "www.port": 80
336
+ }
337
+ },
338
+ "monitor": {
339
+ "us-west-2": {
340
+ "nagios.hosts": "10.0.1.1,10.0.1.2",
341
+ "nagios.key": "monitoring-key.rsa"
342
+ },
343
+ "eu-west-1": {
344
+ "nagios.hosts": "10.0.10.1",
345
+ "nagios.key": "monitoring-key.rsa"
346
+ }
347
+ }
348
+ }
349
+ ```
350
+
351
+ If you need to have a key in your service map that starts with a
352
+ dollar sign, use two dollar signs instead.
353
+
354
+ ## TODO
355
+
356
+ * The `refresh_inventory_data` formatter needs to changed to be
357
+ more parallel with the other formatters (`--ansible` should
358
+ be `--groups`, it should probably be named `inventory-files`
359
+ or something.
360
+ * The `aws-rds-source` should be refactored to take the `--filters`
361
+ argument and use **BjnInventory::Util::Filter::JsonAws** like
362
+ `aws-elb-source`.
363
+
199
364
  ## Design Decisions
200
365
 
201
366
  * No calculated fields in the model.
data/bin/ansible-from CHANGED
@@ -17,7 +17,8 @@ parser = Trollop::Parser.new do
17
17
  ansible-from [options]
18
18
  USAGE
19
19
 
20
- opt :ansible, 'Specify ansible groupings file', required: true, type: :string
20
+ opt :groups, 'Specify ansible groupings file', required: false, type: :string
21
+ opt :ansible, 'Synonym for --groups', required: false, type: :string
21
22
  opt :manifest, 'Specify the manifest that defines this inventory', required: true, type: :string
22
23
  opt :debug, 'Enable debug output', :short => '-D'
23
24
  opt :syslog, 'Log to Syslog', :short => '-S'
@@ -28,6 +29,8 @@ opt = Trollop::with_standard_exception_handling parser do
28
29
  parser.parse(ARGV)
29
30
  end
30
31
 
32
+ opt[:groups] = opt[:ansible] if opt[:groups].nil?
33
+
31
34
  if opt[:syslog]
32
35
  require 'syslog/logger'
33
36
  logger = Syslog::Logger.new 'ansible-from'
@@ -35,14 +38,20 @@ else
35
38
  logger = Logger.new STDERR
36
39
  end
37
40
 
41
+
38
42
  if opt[:debug]
39
43
  logger.level = Logger::DEBUG
40
44
  else
41
45
  logger.level = Logger::WARN
42
46
  end
43
47
 
48
+ if opt[:groups].nil?
49
+ logger.fatal "Need to specify --groups file"
50
+ exit 1
51
+ end
52
+
44
53
  manifest = JSON.parse(File.read(opt[:manifest]))
45
- ansible_spec = JSON.parse(File.read(opt[:ansible]))
54
+ ansible_spec = JSON.parse(File.read(opt[:groups]))
46
55
 
47
56
  inventory = BjnInventory::Inventory.new(manifest.merge({logger: logger}))
48
57
  puts JSON.pretty_generate(inventory.by('name').to_ansible(ansible_spec))
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'trollop'
4
+ require 'logger'
5
+
6
+ require 'bjn_inventory/version'
7
+ require 'bjn_inventory/source_command/aws_elb'
8
+
9
+ parser = Trollop::Parser.new do
10
+ version BjnInventory::VERSION
11
+ banner <<-USAGE.gsub(/^\s{8}/,'')
12
+ Usage:
13
+ aws-elb-source [options]
14
+ Download entries corresponding to classic elastic loadbalancers (not application load balancers)
15
+ USAGE
16
+
17
+ opt :region, "Specify region (default is all regions)", type: :string, multi: true
18
+ opt :profile, "Specify AWS client profile", type: :string
19
+ opt :filters, 'Specify AWS filters in JSON (e.g. `[{"name":"tag:ENVIRONMENT","values":["Production"]}]`)', type: :string
20
+ opt :output, "Send output to file instead of stdout", type: :string
21
+ opt :verbose, "Produce verbose output", short: '-v'
22
+ opt :debug, 'Enable debug output', short: '-D'
23
+ opt :syslog, 'Log to Syslog', short: '-S'
24
+ stop_on_unknown
25
+ end
26
+
27
+ opt = Trollop::with_standard_exception_handling parser do
28
+ parser.parse(ARGV)
29
+ end
30
+
31
+ if opt[:syslog]
32
+ require 'syslog/logger'
33
+ logger = Syslog::Logger.new 'aws-elb-source'
34
+ else
35
+ logger = Logger.new STDERR
36
+ end
37
+
38
+ if opt[:debug]
39
+ logger.level = Logger::DEBUG
40
+ elsif opt[:verbose]
41
+ logger.level = Logger::INFO
42
+ else
43
+ logger.level = Logger::WARN
44
+ end
45
+
46
+ command = BjnInventory::SourceCommand::AwsElb.new(opt.merge({ logger: logger }))
47
+ command.run
@@ -15,7 +15,7 @@ parser = Trollop::Parser.new do
15
15
  inventory-devices [options]
16
16
  USAGE
17
17
 
18
- opt :key, 'Specify the key of inventory hash', required: true, type: :string
18
+ opt :key, 'Specify the key of inventory hash', type: :string
19
19
  opt :manifest, 'Specify the manifest that defines this inventory', required: true, type: :string
20
20
  opt :debug, 'Enable debug output', :short => '-D'
21
21
  opt :syslog, 'Log to Syslog', :short => '-S'
@@ -31,4 +31,11 @@ logger.level = opt[:debug]? Logger::DEBUG : Logger::WARN
31
31
 
32
32
  manifest = JSON.parse(File.read(opt[:manifest]))
33
33
  inventory = BjnInventory::Inventory.new(manifest.merge({logger: logger}))
34
- puts JSON.pretty_generate(inventory.by(opt[:key]))
34
+
35
+ if opt[:key]
36
+ results = inventory.by(opt[:key])
37
+ else
38
+ results = inventory.devices.map { |device| device.to_hash }
39
+ end
40
+
41
+ puts JSON.pretty_generate(results)
data/bin/service-map ADDED
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Create list of inventory devices based on device model
4
+
5
+ require 'trollop'
6
+ require 'json'
7
+ require 'logger'
8
+ require 'syslog/logger'
9
+ require 'bjn_inventory'
10
+ require 'bjn_inventory/service_map'
11
+
12
+ parser = Trollop::Parser.new do
13
+ version BjnInventory::VERSION
14
+ banner <<-USAGE.gsub(/^\s{8}/,'')
15
+ Usage:
16
+ inventory-devices [options]
17
+ USAGE
18
+
19
+ opt :map, 'Specify file containing service map', required: true, type: :string
20
+ opt :groups, 'Specify file containing group definitions', required: true, type: :string
21
+ opt :manifest, 'Specify the manifest that defines this inventory', required: true, type: :string
22
+ opt :key, 'Specify device key field', type: :string, default: 'name'
23
+ opt :endpoint_fields, 'Specify the names of fields that can be used', default: ['endpoint', 'ip_address'], type: :strings
24
+ opt(:hosts_field,
25
+ 'Specify the name of the service map field that contains the group specification and on which the endpoints are emitted',
26
+ default: 'hosts', type: :string, short: '-H')
27
+ opt :join_with, 'Use this character to join endpoints (or specify \'json\')', default: ',', type: :string
28
+ opt :debug, 'Enable debug output', :short => '-D'
29
+ opt :syslog, 'Log to Syslog', :short => '-S'
30
+ stop_on_unknown
31
+ end
32
+
33
+ opt = Trollop::with_standard_exception_handling parser do
34
+ parser.parse(ARGV)
35
+ end
36
+
37
+ logger = opt[:syslog]? Syslog::Logger.new('service-map') : Logger.new(STDERR)
38
+ logger.level = opt[:debug]? Logger::DEBUG : Logger::WARN
39
+
40
+ manifest = JSON.parse(File.read(opt[:manifest]))
41
+ service_map = JSON.parse(File.read(opt[:map]))
42
+ groups = JSON.parse(File.read(opt[:groups]))
43
+ inventory = BjnInventory::Inventory.new(manifest.merge({logger: logger}))
44
+
45
+ services = inventory.by(opt[:key]).to_services(map: service_map,
46
+ group_by: groups['group_by'],
47
+ hosts_field: opt[:hosts_field],
48
+ endpoint_fields: opt[:endpoint_fields],
49
+ join: (opt[:join_with] == 'json' ? :json : opt[:join_with]),
50
+ logger: logger)
51
+
52
+ puts JSON.pretty_generate(services)
@@ -19,7 +19,8 @@ Gem::Specification.new do |spec|
19
19
  f.match(%r{^(test|spec|features)/})
20
20
  end
21
21
  spec.bindir = "bin"
22
- spec.executables = ["ansible-from", "aws-ec2-source", "aws-rds-source", "inventory_model", "refresh_inventory_data"]
22
+ spec.executables = ["ansible-from", "aws-ec2-source", "aws-rds-source", "aws-elb-source",
23
+ "inventory-model", "refresh_inventory_data", "service-map"]
23
24
  spec.require_paths = ["lib"]
24
25
 
25
26
  spec.add_dependency 'jsonpath', "~> 0.7.2"
@@ -5,40 +5,9 @@ module BjnInventory
5
5
 
6
6
  class ByKey
7
7
 
8
- def _ansible_name(value)
9
- value.gsub(/[^a-zA-Z0-9_-]+/, '_')
10
- end
11
-
12
- def _field_groups(fields, device, sep='__')
13
- fields = [fields] unless fields.respond_to? :inject
14
- value_map = fields.map do |field|
15
- values = device[field]
16
- values = [values] unless values.respond_to? :map
17
- values.map { |val| _ansible_name(val) }
18
- end
19
- #
20
- # So now we have an array of arrays, eg.:
21
- # fields='region' =>
22
- # [['dc2']]
23
- #
24
- # fields=['roles', 'region'] =>
25
- # [['web', ['dc2']]
26
- # 'db'],
27
- #
28
- groups =
29
- if fields.length == 1
30
- value_map.first
31
- else
32
- driving_array, *rest = value_map
33
- driving_array.product(*rest).map { |compound_value| compound_value.join(sep) }
34
- end
35
- groups
36
- end
37
-
38
- # This basically builds an ansible inventory given a hash of hostvars
39
- def to_ansible(*ansible_spec)
40
- if ansible_spec[-1].respond_to? :to_hash
41
- kwargs = ansible_spec.pop.stringify_keys
8
+ def to_ansible(*args)
9
+ if args[-1].respond_to? :to_hash
10
+ kwargs = args.pop.stringify_keys
42
11
  else
43
12
  kwargs = { }
44
13
  end
@@ -47,31 +16,19 @@ module BjnInventory
47
16
  group_by = kwargs['group_by']
48
17
  group_by = [group_by] unless group_by.respond_to? :each
49
18
  end
50
- group_by.concat(ansible_spec)
19
+ group_by.concat(args)
51
20
 
52
- if group_by.empty?
53
- raise ArgumentError, "Expected group_by either as keyword or as argument list"
54
- end
55
-
56
- logger ||= BjnInventory::DefaultLogger.new
57
- # We need at least one field to create groups
21
+ logger = kwargs['logger'] || BjnInventory::DefaultLogger.new
58
22
  separator = kwargs['separator'] || '__'
59
23
 
60
- ansible_inventory = { '_meta' => {'hostvars' => self.to_hash } }
61
-
62
- self.each do |name, device_hash|
63
- group_by.each do |group_field_spec|
64
- group_field_spec = [group_field_spec] unless group_field_spec.respond_to? :all?
65
- if group_field_spec.all? { |field| !device_hash[field].nil? && !device_hash[field].empty? }
66
- field_groups = _field_groups(group_field_spec, device_hash, separator)
67
- field_groups.each do |group_name|
68
- ansible_inventory[group_name] = [ ] unless ansible_inventory.has_key? group_name
69
- ansible_inventory[group_name] << name
70
- end
71
- end
72
- end
73
- end
24
+ ansible_inventory = self.to_groups(group_by: group_by, logger: logger, separator: separator).
25
+ merge({
26
+ '_meta' => {
27
+ 'hostvars' => self.to_hash
28
+ }
29
+ })
74
30
 
31
+ # Do my own groups
75
32
  if kwargs['groups']
76
33
  ansible_inventory.merge! Hash[kwargs['groups'].map do |group, children|
77
34
  [group, { "hosts" => [ ], "children" => children }]
@@ -2,6 +2,70 @@ module BjnInventory
2
2
 
3
3
  class ByKey < Hash
4
4
 
5
+ def escape_name(value)
6
+ value.gsub(/[^a-zA-Z0-9_-]+/, '_')
7
+ end
8
+
9
+ def _field_groups(fields, device, sep='__')
10
+ fields = [fields] unless fields.respond_to? :inject
11
+ value_map = fields.map do |field|
12
+ values = device[field]
13
+ values = [values] unless values.respond_to? :map
14
+ values.map { |val| escape_name(val) }
15
+ end
16
+ #
17
+ # So now we have an array of arrays, eg.:
18
+ # fields='region' =>
19
+ # [['dc2']]
20
+ #
21
+ # fields=['roles', 'region'] =>
22
+ # [['web', ['dc2']]
23
+ # 'db'],
24
+ #
25
+ groups =
26
+ if fields.length == 1
27
+ value_map.first
28
+ else
29
+ driving_array, *rest = value_map
30
+ driving_array.product(*rest).map { |compound_value| compound_value.join(sep) }
31
+ end
32
+ groups
33
+ end
34
+
35
+ # This basically builds an ansible inventory given a hash of hostvars
36
+ def to_groups(kwargs)
37
+ group_by = kwargs[:group_by]
38
+
39
+ if group_by.empty?
40
+ raise ArgumentError, "Expected group_by either as keyword or as argument list"
41
+ end
42
+
43
+ logger = kwargs[:logger] || BjnInventory::DefaultLogger.new
44
+ # We need at least one field to create groups
45
+ separator = kwargs[:separator] || '__'
46
+
47
+ groups = { }
48
+
49
+ self.each do |name, device_hash|
50
+ group_by.each do |group_field_spec|
51
+ group_field_spec = [group_field_spec] unless group_field_spec.respond_to? :all?
52
+ if group_field_spec.all? { |field| !device_hash[field].nil? && !device_hash[field].empty? }
53
+ field_groups = _field_groups(group_field_spec, device_hash, separator)
54
+ field_groups.each do |group_name|
55
+ groups[group_name] = [ ] unless groups.has_key? group_name
56
+ groups[group_name] << name
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ # I can't really do children, they need to be generators and I'm not
63
+ # about that right now. Save it for ansible.
64
+
65
+ groups
66
+ end
67
+
68
+
5
69
  end
6
70
 
7
71
  end
@@ -6,7 +6,11 @@ module BjnInventory
6
6
 
7
7
  def initialize(progname='bjn_inventory')
8
8
  logger = Logger.new $stderr
9
- logger.level = Logger::WARN
9
+ if ENV['BJN_INVENTORY_DEBUG'] and ! ENV['BJN_INVENTORY_DEBUG'].empty?
10
+ logger.level = Logger::DEBUG
11
+ else
12
+ logger.level = Logger::WARN
13
+ end
10
14
  logger
11
15
  end
12
16
 
@@ -0,0 +1,116 @@
1
+ # Adds the .to_services method to inventory
2
+ # which takes a grouping directive
3
+
4
+ module BjnInventory
5
+
6
+ class ByKey
7
+
8
+ def to_services(kwargs)
9
+ @map = kwargs[:map]
10
+ @group_by = kwargs[:group_by]
11
+ @logger = kwargs[:logger] || BjnInventory::DefaultLogger.new
12
+
13
+ @hosts_field = kwargs[:hosts] || 'hosts'
14
+ @endpoint_fields = kwargs[:endpoint_fields] || ['endpoint', 'ip_address']
15
+ @join = kwargs[:join] || ','
16
+ separator = kwargs[:separator] || '__'
17
+
18
+ raise ArgumentError, "BjnInventory::ByKey#to_services requires a service map" unless @map
19
+ raise ArgumentError, "BjnInventory::ByKey#to_services requires a group_by argument" unless @group_by
20
+
21
+ # Build un-interpolated path => service correspondence
22
+ spec_keys = _pathlist(@map)
23
+ @logger.debug "paths in map: #{spec_keys.inspect}"
24
+
25
+ groups = self.to_groups(group_by: @group_by, separator: separator)
26
+
27
+ service_keylist = _interpolate_pathlist(spec_keys, groups)
28
+
29
+ services = { }
30
+
31
+ service_keylist.each do |path, service|
32
+ _deep_set_service(services, path, service.merge({ @hosts_field => _join_endpoints(service[@hosts_field]) }))
33
+ end
34
+
35
+ services
36
+ end
37
+
38
+ # Take a path--a list of key components, and deeply set them into a hash of hashes
39
+ def _deep_set_service(hsh, path, object)
40
+ car, *cdr = path
41
+ if cdr.empty?
42
+ # This is kind of weird, use-case-specific logic
43
+ object.each do |key, value|
44
+ hsh[car + '.' + key] = value
45
+ end
46
+ else
47
+ hsh[car] = { } unless hsh.has_key? car
48
+ _deep_set_service(hsh[car], cdr, object)
49
+ end
50
+ end
51
+
52
+ # Take uninterpolated paths and expand them with the groups
53
+ # of devices we have
54
+ def _interpolate_pathlist(spec_keys, groups)
55
+ service_keylist = { }
56
+
57
+ # Interpolate based on hosts_field values
58
+ spec_keys.each do |path, service|
59
+ if service[@hosts_field]
60
+ device_names = service[@hosts_field].map { |group| groups[group] || [] }.flatten
61
+ device_names.each do |device_name|
62
+ if self[device_name]
63
+ interpolated_path = _interpolate_path(path, self[device_name])
64
+ next if interpolated_path.any? { |el| el.nil? }
65
+ unless service_keylist.has_key? interpolated_path
66
+ service_keylist[interpolated_path] = service.merge({ @hosts_field => [] })
67
+ end
68
+ service_keylist[interpolated_path][@hosts_field] << _endpoint(self[device_name])
69
+ end
70
+ end
71
+ end
72
+ end
73
+ service_keylist
74
+ end
75
+
76
+ def _interpolate_path(path, device)
77
+ path.map do |component|
78
+ if component.start_with? '$$'
79
+ component[1 .. -1]
80
+ elsif component.start_with? '$'
81
+ device[component[1 .. -1]]
82
+ else
83
+ component
84
+ end
85
+ end
86
+ end
87
+
88
+ def _endpoint(device)
89
+ @endpoint_fields.map { |field| device[field] }.reject { |e| e.nil? }.first
90
+ end
91
+
92
+ def _join_endpoints(endpoints)
93
+ case @join
94
+ when :json
95
+ endpoints.uniq.sort.to_json
96
+ else
97
+ endpoints.uniq.sort.join(@join)
98
+ end
99
+ end
100
+
101
+ # Create a hash where the keys are paths (arrays of path components)
102
+ # and the values are service specs
103
+ def _pathlist(this_map, prefix=[], cur={})
104
+ if this_map.any? { |k, v| ! v.respond_to? :keys }
105
+ cur[prefix] = this_map
106
+ else
107
+ this_map.map do |key, value|
108
+ _pathlist(value, prefix + [key], cur)
109
+ end
110
+ end
111
+ cur
112
+ end
113
+
114
+ end
115
+
116
+ end
@@ -0,0 +1,86 @@
1
+ require 'aws-sdk'
2
+ require 'bjn_inventory/source_command'
3
+ require 'bjn_inventory/util/filter/json_aws'
4
+
5
+ module BjnInventory
6
+
7
+ class SourceCommand
8
+
9
+ class AwsElb < BjnInventory::SourceCommand
10
+
11
+ def set_options(opt)
12
+ # Amazon only allows 20 names in describe_tags, so this is one way of accomplishing that
13
+ @page_size = opt[:page_size] || 20
14
+ @regions = nil
15
+ if opt[:region] and !opt[:region].empty?
16
+ @regions = opt[:region].respond_to?(:to_ary) ? opt[:region] : [ opt[:region] ]
17
+ region = @regions.first
18
+ else
19
+ region = 'us-west-2'
20
+ end
21
+
22
+ @filters = BjnInventory::Util::Filter::JsonAws.new(filters: opt[:filters] || '[ ]', logger: logger)
23
+
24
+ @client = opt[:client]
25
+ @ec2_client = opt[:ec2_client]
26
+ @profile = opt[:profile]
27
+ ec2_client = @ec2_client || new_ec2_client(region)
28
+ if @regions.nil? or @regions.empty?
29
+ logger.debug "Listing regions"
30
+ @regions = ec2_client.describe_regions().regions.map { |region| region.region_name }
31
+ end
32
+ end
33
+
34
+ def new_ec2_client(region=nil)
35
+ opts = {
36
+ region: (region || @regions.first)
37
+ }
38
+ opts.update({profile: @profile}) if @profile
39
+ Aws::EC2::Client.new(opts)
40
+ end
41
+
42
+ def new_client(region=nil)
43
+ opts = {
44
+ region: (region || @regions.first)
45
+ }
46
+ opts.update({profile: @profile}) if @profile
47
+ Aws::ElasticLoadBalancing::Client.new(opts)
48
+ end
49
+
50
+ def retrieve_entries(override_client=nil)
51
+ entries = @regions.map do |region|
52
+ client = override_client || @client || new_client(region)
53
+ _retrieve_entries([], client)
54
+ end.flatten
55
+ end
56
+
57
+ def _retrieve_entries(current, client, marker=nil)
58
+ logger.debug "Describing load balancers (page #{marker})"
59
+ chunk_result = client.describe_load_balancers(marker: marker, page_size: @page_size)
60
+ logger.debug "... described"
61
+ marker = chunk_result.next_marker
62
+ if chunk_result.load_balancer_descriptions.length > 0
63
+ chunk = Hash[chunk_result.load_balancer_descriptions.map { |lb| [lb.load_balancer_name, lb.to_h] }]
64
+ logger.debug "Describing load balancer tags"
65
+ # Amazon only allows 20 names to be submitted
66
+ chunk_tags = Hash[client.describe_tags(load_balancer_names: chunk.keys).tag_descriptions.map do |lbtags|
67
+ [lbtags.load_balancer_name, { tags: lbtags.to_h[:tags] }]
68
+ end]
69
+ logger.debug "... described"
70
+ chunk_list = chunk.map { |lbname, lb| lb.merge(chunk_tags[lbname]) }
71
+ else
72
+ chunk_list = [ ]
73
+ end
74
+ chunk_list = @filters.select(chunk_list)
75
+ if marker
76
+ return _retrieve_entries(current + chunk_list, client, marker)
77
+ else
78
+ return current + chunk_list
79
+ end
80
+ end
81
+
82
+ end
83
+
84
+ end
85
+
86
+ end
@@ -0,0 +1,108 @@
1
+ require 'json'
2
+ require 'bjn_inventory/util/filter'
3
+
4
+ module BjnInventory
5
+
6
+ module Util
7
+
8
+ class Filter
9
+
10
+ # Implements JSON filtering AWS-style, i.e. similar to
11
+ # how the EC2 API filters instances by tags and attributes
12
+ # when querying.
13
+ class JsonAws < BjnInventory::Util::Filter
14
+
15
+ def set_options(opt)
16
+ @filters = JSON.parse(opt[:filters]).map do |filter|
17
+ {
18
+ name: filter['name'],
19
+ matcher: _matcher(filter['values'])
20
+ }
21
+ end
22
+ end
23
+
24
+ def match(entity)
25
+ _match_filters(entity, @filters)
26
+ end
27
+
28
+ def _matcher(values)
29
+ match_one_list = values.map { |expression| _match_one expression }
30
+ proc do |value|
31
+ match_one_list.any? { |match_one| match_one.call value }
32
+ end
33
+ end
34
+
35
+ def _glob2re(str)
36
+ str.scan(/\\\*|\\\?|\?|\*|\\|[^\\\?\*]*/).map do |group|
37
+ case group
38
+ when '\?'
39
+ '\?'
40
+ when '\*'
41
+ '\*'
42
+ when '\\'
43
+ '\\'
44
+ when '*'
45
+ '.*'
46
+ when '?'
47
+ '.'
48
+ else
49
+ Regexp::escape(group)
50
+ end
51
+ end.join('')
52
+ end
53
+
54
+ def _match_one(expression)
55
+ if /\?|\*/.match expression
56
+ rx = Regexp.new('\A' + _glob2re(expression) + '\Z')
57
+ proc do |value|
58
+ rx.match value
59
+ end
60
+ else
61
+ proc do |value|
62
+ expression == value
63
+ end
64
+ end
65
+ end
66
+
67
+ # Since this is an "AWS" style filter, accepts entities
68
+ # with symbols for keys, the way you get when you do an
69
+ # AWS API call and do `.to_h`
70
+ def _match_filters(entity, filters)
71
+ return true if filters.nil? or filters.empty?
72
+ filter_expr = filters[0]
73
+ name = filter_expr[:name]
74
+ matcher = filter_expr[:matcher]
75
+ this_expr =
76
+ begin
77
+ case name
78
+ when 'tag-key'
79
+ (entity[:tags] || [ ]).any? { |tag| matcher.call tag[:key] }
80
+ when 'tag-value'
81
+ (entity[:tags] || [ ]).any? { |tag| matcher.call tag[:value] }
82
+ when /^tag:/
83
+ tag_key = name[4 .. -1]
84
+ entity_value = (entity[:tags] || [ ]).select { |tag| tag[:key] == tag_key }
85
+ if ! entity_value.empty?
86
+ matcher.call entity_value[0][:value]
87
+ else
88
+ false
89
+ end
90
+ else
91
+ matcher.call entity[name.intern]
92
+ end
93
+ rescue StandardError => err
94
+ logger.debug("rejected invalid entity #{entity.inspect}")
95
+ false
96
+ end
97
+ return false unless this_expr
98
+
99
+ _match_filters(entity, filters[1 .. -1])
100
+ end
101
+
102
+ end
103
+
104
+ end
105
+
106
+ end
107
+
108
+ end
@@ -0,0 +1,42 @@
1
+ require 'bjn_inventory/default_logger'
2
+
3
+ module BjnInventory
4
+
5
+ module Util
6
+
7
+ class Filter
8
+
9
+ def initialize(kwargs={})
10
+ @logger = kwargs.delete(:logger) || BjnInventory::DefaultLogger.new
11
+ unless kwargs.has_key? :filters
12
+ raise RuntimeError, "#{self.class} requires :filters to be set"
13
+ end
14
+ @filters = kwargs[:filters]
15
+ set_options(kwargs)
16
+ end
17
+
18
+ def set_options(kwargs={})
19
+ @options = kwargs
20
+ end
21
+
22
+ def logger()
23
+ @logger
24
+ end
25
+
26
+ def match(entity)
27
+ if @filters.respond_to? :call
28
+ @filters.call entity
29
+ else
30
+ raise RuntimeError, "#{self.class} requires :filters to be callable"
31
+ end
32
+ end
33
+
34
+ def select(arr)
35
+ arr.select { |entity| match entity }
36
+ end
37
+
38
+ end
39
+
40
+ end
41
+
42
+ end
@@ -1,3 +1,3 @@
1
1
  module BjnInventory
2
- VERSION = "1.3.1"
2
+ VERSION = "1.5.1"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bjn_inventory
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.1
4
+ version: 1.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Brinkley
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-08-17 00:00:00.000000000 Z
11
+ date: 2017-10-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jsonpath
@@ -115,8 +115,10 @@ executables:
115
115
  - ansible-from
116
116
  - aws-ec2-source
117
117
  - aws-rds-source
118
- - inventory_model
118
+ - aws-elb-source
119
+ - inventory-model
119
120
  - refresh_inventory_data
121
+ - service-map
120
122
  extensions: []
121
123
  extra_rdoc_files: []
122
124
  files:
@@ -128,10 +130,12 @@ files:
128
130
  - Rakefile
129
131
  - bin/ansible-from
130
132
  - bin/aws-ec2-source
133
+ - bin/aws-elb-source
131
134
  - bin/aws-rds-source
132
135
  - bin/console
133
- - bin/inventory_model
136
+ - bin/inventory-model
134
137
  - bin/refresh_inventory_data
138
+ - bin/service-map
135
139
  - bin/setup
136
140
  - bjn_inventory.gemspec
137
141
  - lib/bjn_inventory.rb
@@ -148,9 +152,13 @@ files:
148
152
  - lib/bjn_inventory/inventory/source.rb
149
153
  - lib/bjn_inventory/list.rb
150
154
  - lib/bjn_inventory/metadata.rb
155
+ - lib/bjn_inventory/service_map.rb
151
156
  - lib/bjn_inventory/source_command.rb
152
157
  - lib/bjn_inventory/source_command/aws_ec2.rb
158
+ - lib/bjn_inventory/source_command/aws_elb.rb
153
159
  - lib/bjn_inventory/source_command/aws_rds.rb
160
+ - lib/bjn_inventory/util/filter.rb
161
+ - lib/bjn_inventory/util/filter/json_aws.rb
154
162
  - lib/bjn_inventory/version.rb
155
163
  - lib/inventory.rb
156
164
  - tasks/package/_package.sh
@@ -186,3 +194,4 @@ specification_version: 4
186
194
  summary: Generate inventory lists based on flexible sources, rules and a standard
187
195
  device model
188
196
  test_files: []
197
+ has_rdoc: