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 +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:
|