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 +4 -4
- data/.gitignore +2 -0
- data/README.md +165 -0
- data/bin/ansible-from +11 -2
- data/bin/aws-elb-source +47 -0
- data/bin/{inventory_model → inventory-model} +9 -2
- data/bin/service-map +52 -0
- data/bjn_inventory.gemspec +2 -1
- data/lib/bjn_inventory/ansible.rb +12 -55
- data/lib/bjn_inventory/bykey.rb +64 -0
- data/lib/bjn_inventory/default_logger.rb +5 -1
- data/lib/bjn_inventory/service_map.rb +116 -0
- data/lib/bjn_inventory/source_command/aws_elb.rb +86 -0
- data/lib/bjn_inventory/util/filter/json_aws.rb +108 -0
- data/lib/bjn_inventory/util/filter.rb +42 -0
- data/lib/bjn_inventory/version.rb +1 -1
- metadata +13 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dcd99bdf01564c9b9328bd816c7d682dca6ac94d
|
4
|
+
data.tar.gz: 003a564bc69f38caa487927671c7f98e4eaa201e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 891137548bf9420e4a7bd654d23776257d9d1253d4f349439183dee2efc45cfb00717ac3ab2e9321d96b587ab902a36161d1bcc103ea09487cde7931e657147d
|
7
|
+
data.tar.gz: e839db1948ea6d4bc9861edf16547bd0304f05e308606918e1b2a89e38511bdf11340bd712c7debe44ebf5eb7d7c24e23a3969c7d7d92b45a2571de2be295c39
|
data/.gitignore
CHANGED
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 :
|
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[:
|
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))
|
data/bin/aws-elb-source
ADDED
@@ -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',
|
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
|
-
|
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)
|
data/bjn_inventory.gemspec
CHANGED
@@ -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", "
|
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
|
9
|
-
|
10
|
-
|
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(
|
19
|
+
group_by.concat(args)
|
51
20
|
|
52
|
-
|
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 =
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
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 }]
|
data/lib/bjn_inventory/bykey.rb
CHANGED
@@ -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
|
-
|
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
|
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.
|
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-
|
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
|
-
-
|
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/
|
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:
|