bjn_inventory 1.3.1 → 1.5.1

Sign up to get free protection for your applications and to get access to all the features.
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: