diff --git a/README.md b/README.md index ba327a4b4f..11348aed11 100644 --- a/README.md +++ b/README.md @@ -765,6 +765,50 @@ Example: `bolt task run sensu::install_agent backend=sensu_backend:8081 subscrip Example: `bolt task run sensu::install_agent backend=sensu_backend:8081 subscription=windows output=true --nodes windows` +### Bolt Inventory + +This module provides a plugin to populate Bolt v2 inventory targets. + +In order to use the `sensu` inventory plugin the host executing Bolt must have `sensuctl` configured, see [Basic Sensu CLI](#basic-sensu-cli). + +Example of configuring the Bolt inventory with two groups. The `linux` group pulls Sensu Go entities in the `default` namespace with the `linux` subscription. The `linux-qa` group is the same as `linux` group but instead pulling entities from the `qa` namespace. + +```yaml +version: 2 +groups: + - name: linux + targets: + - _plugin: sensu + namespace: default + subscription: linux + - name: linux-qa + targets: + - _plugin: sensu + namespace: qa + subscription: linux +``` + +If your entities have more than one network interface it may be necessary to specify the order of interfaces to search when looking for the IP address: + +```yaml +version: 2 +groups: + - name: linux + targets: + - _plugin: sensu + namespace: default + subscription: linux + interface_list: + - eth0 + - eth1 +``` + +The following rules for interface matching determine the value used for `uri`. + +1. If `interface_list` was defined then find first match +1. If `interface_list` not defined and only one interface, use that as ipaddress +1. If `interface_list` is not defined and more than one interface, use name + ## Reference ### Facts diff --git a/bolt_plugin.json b/bolt_plugin.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/bolt_plugin.json @@ -0,0 +1 @@ +{} diff --git a/spec/acceptance/sensu_bolt_tasks_spec.rb b/spec/acceptance/sensu_bolt_tasks_spec.rb index 46589eeafd..6f73ab0dd0 100644 --- a/spec/acceptance/sensu_bolt_tasks_spec.rb +++ b/spec/acceptance/sensu_bolt_tasks_spec.rb @@ -284,3 +284,40 @@ class { '::sensu::agent': end end end + +describe 'sensu bolt inventory', if: RSpec.configuration.sensu_full do + backend = hosts_as('sensu_backend')[0] + agent = hosts_as('sensu_agent')[0] + context 'setup' do + it 'should work without errors' do + agent_pp = <<-EOS + class { '::sensu::agent': + backends => ['sensu_backend:8081'], + } + EOS + pp = <<-EOS + include ::sensu::backend + #{agent_pp} + EOS + apply_manifest_on(agent, agent_pp, :catch_failures => true) + apply_manifest_on(backend, pp, :catch_failures => true) + inventory_cfg1 = <<-EOS +version: 2 +groups: + - name: linux + targets: + - _plugin: sensu + EOS + create_remote_file(backend, '/root/.puppetlabs/bolt/inventory1.yaml', inventory_cfg1) + end + end + + context 'inventory' do + it 'produces inventory' do + on backend, 'bolt inventory show --targets linux --format json -i /root/.puppetlabs/bolt/inventory1.yaml' do + data = JSON.parse(stdout) + expect(data["count"]).to eq(2) + end + end + end +end diff --git a/spec/fixtures/tasks/resolve_reference/entities1.json b/spec/fixtures/tasks/resolve_reference/entities1.json new file mode 100644 index 0000000000..c3b655504d --- /dev/null +++ b/spec/fixtures/tasks/resolve_reference/entities1.json @@ -0,0 +1,353 @@ +[ + { + "entity_class": "agent", + "system": { + "hostname": "el7-agent.example.com", + "os": "linux", + "platform": "centos", + "platform_family": "rhel", + "platform_version": "7.6.1810", + "network": { + "interfaces": [ + { + "name": "lo", + "addresses": [ + "127.0.0.1/8", + "::1/128" + ] + }, + { + "name": "eth0", + "mac": "52:54:00:26:10:60", + "addresses": [ + "10.0.2.15/24", + "fe80::5054:ff:fe26:1060/64" + ] + }, + { + "name": "eth1", + "mac": "08:00:27:04:81:77", + "addresses": [ + "192.168.52.11/24", + "fe80::a00:27ff:fe04:8177/64" + ] + } + ] + }, + "arch": "amd64" + }, + "subscriptions": [ + "linux", + "entity:el7-agent.example.com" + ], + "last_seen": 1568497126, + "deregister": false, + "deregistration": {}, + "user": "agent", + "redact": [ + "password", + "passwd", + "pass", + "api_key", + "api_token", + "access_key", + "secret_key", + "private_key", + "secret" + ], + "metadata": { + "name": "el7-agent.example.com", + "namespace": "default" + }, + "sensu_agent_version": "" + }, + { + "entity_class": "agent", + "system": { + "hostname": "sensu-backend.example.com", + "os": "linux", + "platform": "centos", + "platform_family": "rhel", + "platform_version": "7.4.1708", + "network": { + "interfaces": [ + { + "name": "lo", + "addresses": [ + "127.0.0.1/8", + "::1/128" + ] + }, + { + "name": "eth0", + "mac": "52:54:00:da:a7:10", + "addresses": [ + "10.0.2.15/24", + "fe80::5054:ff:feda:a710/64" + ] + }, + { + "name": "eth1", + "mac": "08:00:27:7a:3f:38", + "addresses": [ + "192.168.52.10/24", + "fe80::a00:27ff:fe7a:3f38/64" + ] + } + ] + }, + "arch": "amd64" + }, + "subscriptions": [ + "entity:sensu-backend.example.com" + ], + "last_seen": 1573506728, + "deregister": false, + "deregistration": {}, + "user": "agent", + "redact": [ + "password", + "passwd", + "pass", + "api_key", + "api_token", + "access_key", + "secret_key", + "private_key", + "secret" + ], + "metadata": { + "name": "sensu-backend.example.com", + "namespace": "default" + }, + "sensu_agent_version": "5.14.1" + }, + { + "entity_class": "agent", + "system": { + "hostname": "win2012r2-agent", + "os": "windows", + "platform": "Microsoft Windows Server 2012 R2 Standard", + "platform_family": "Server", + "platform_version": "6.3.9600 Build 9600", + "network": { + "interfaces": [ + { + "name": "Ethernet 2", + "mac": "08:00:27:f9:a8:38", + "addresses": [ + "fe80::4dd:5644:505f:8bdb/64", + "192.168.52.24/24" + ] + }, + { + "name": "Ethernet", + "mac": "08:00:27:d5:9d:5a", + "addresses": [ + "fe80::e488:b85c:5262:ff86/64", + "10.0.2.15/24" + ] + }, + { + "name": "Loopback Pseudo-Interface 1", + "addresses": [ + "::1/128", + "127.0.0.1/8" + ] + }, + { + "name": "isatap.{31B440AF-632F-4FBF-8FFB-C2A80DC19FBA}", + "mac": "00:00:00:00:00:00:00:e0", + "addresses": [ + "fe80::5efe:c0a8:3418/128" + ] + }, + { + "name": "isatap.{DD72B02C-4E48-4924-8D0F-F80EA2755534}", + "mac": "00:00:00:00:00:00:00:e0", + "addresses": [ + "fe80::5efe:a00:20f/128" + ] + } + ] + }, + "arch": "amd64" + }, + "subscriptions": [ + "windows", + "entity:win2012r2-agent" + ], + "last_seen": 1568480067, + "deregister": false, + "deregistration": {}, + "user": "agent", + "redact": [ + "password", + "passwd", + "pass", + "api_key", + "api_token", + "access_key", + "secret_key", + "private_key", + "secret" + ], + "metadata": { + "name": "win2012r2-agent", + "namespace": "default" + }, + "sensu_agent_version": "" + }, + { + "entity_class": "agent", + "system": { + "hostname": "win2016-agent", + "os": "windows", + "platform": "Microsoft Windows Server 2016 Datacenter Evaluation", + "platform_family": "Server", + "platform_version": "10.0.14393 Build 14393", + "network": { + "interfaces": [ + { + "name": "Ethernet", + "mac": "08:00:27:5b:c4:19", + "addresses": [ + "fe80::fd05:cca8:8abc:1bd7/64", + "10.0.2.15/24" + ] + }, + { + "name": "Ethernet 2", + "mac": "08:00:27:df:81:b8", + "addresses": [ + "fe80::91dd:9473:f3c4:d126/64", + "192.168.52.26/24" + ] + }, + { + "name": "Loopback Pseudo-Interface 1", + "addresses": [ + "::1/128", + "127.0.0.1/8" + ] + }, + { + "name": "isatap.{5664CB88-D0B0-4AC6-A773-47F4225ACE02}", + "mac": "00:00:00:00:00:00:00:e0", + "addresses": [ + "fe80::5efe:a00:20f/128" + ] + }, + { + "name": "isatap.{17F06521-8485-46B5-8743-B22D083C966B}", + "mac": "00:00:00:00:00:00:00:e0", + "addresses": [ + "fe80::5efe:c0a8:341a/128" + ] + } + ] + }, + "arch": "amd64" + }, + "subscriptions": [ + "windows", + "entity:win2016-agent" + ], + "last_seen": 1568846066, + "deregister": false, + "deregistration": {}, + "user": "agent", + "redact": [ + "password", + "passwd", + "pass", + "api_key", + "api_token", + "access_key", + "secret_key", + "private_key", + "secret" + ], + "metadata": { + "name": "win2016-agent", + "namespace": "default" + }, + "sensu_agent_version": "" + }, + { + "entity_class": "agent", + "system": { + "hostname": "win2016-agent-bolt", + "os": "windows", + "platform": "Microsoft Windows Server 2016 Datacenter Evaluation", + "platform_family": "Server", + "platform_version": "10.0.14393 Build 14393", + "network": { + "interfaces": [ + { + "name": "Ethernet", + "mac": "08:00:27:5b:c4:19", + "addresses": [ + "fe80::5410:8c9a:d8ea:d5eb/64", + "10.0.2.15/24" + ] + }, + { + "name": "Ethernet 2", + "mac": "08:00:27:a0:83:46", + "addresses": [ + "fe80::a045:af1a:da31:1967/64", + "192.168.52.28/24" + ] + }, + { + "name": "Loopback Pseudo-Interface 1", + "addresses": [ + "::1/128", + "127.0.0.1/8" + ] + }, + { + "name": "isatap.{47619EEE-CC3D-4AF0-A1C9-773FBD9588A8}", + "mac": "00:00:00:00:00:00:00:e0", + "addresses": [ + "fe80::5efe:a00:20f/128" + ] + }, + { + "name": "isatap.{3C08F25D-C308-46B7-BAB5-A028FF8B9A09}", + "mac": "00:00:00:00:00:00:00:e0", + "addresses": [ + "fe80::5efe:c0a8:341c/128" + ] + } + ] + }, + "arch": "amd64" + }, + "subscriptions": [ + "windows", + "entity:win2016-agent-bolt" + ], + "last_seen": 1569081707, + "deregister": false, + "deregistration": {}, + "user": "agent", + "redact": [ + "password", + "passwd", + "pass", + "api_key", + "api_token", + "access_key", + "secret_key", + "private_key", + "secret" + ], + "metadata": { + "name": "win2016-agent-bolt", + "namespace": "default" + }, + "sensu_agent_version": "" + } +] diff --git a/spec/tasks/resolve_reference_spec.rb b/spec/tasks/resolve_reference_spec.rb new file mode 100644 index 0000000000..1251388eb8 --- /dev/null +++ b/spec/tasks/resolve_reference_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper' +require_relative '../../tasks/resolve_reference.rb' + +describe SensuResolveReference do + let(:entities) { my_fixture_read('entities1.json') } + before(:each) do + allow(described_class).to receive(:sensuctl_entities).and_return(JSON.parse(entities)) + end + + it 'returns all linux entities' do + expected_targets = [ + {'name' => 'el7-agent.example.com', 'uri' => 'el7-agent.example.com'} + ] + params = { 'subscription' => 'linux' } + targets = described_class.resolve_reference(params) + expect(targets).to eq(expected_targets) + end + + it 'returns all linux entities with ipaddress uri and returns name' do + expected_targets = [ + {'name' => 'el7-agent.example.com', 'uri' => 'el7-agent.example.com'} + ] + params = { 'subscription' => 'linux', 'uri_ipaddress' => true } + targets = described_class.resolve_reference(params) + expect(targets).to eq(expected_targets) + end + + it 'returns all linux entities with ipaddress uri and interface_list' do + expected_targets = [ + {'name' => 'el7-agent.example.com', 'uri' => '192.168.52.11'} + ] + params = { 'subscription' => 'linux', 'uri_ipaddress' => true, 'interface_list' => ['eth1'] } + targets = described_class.resolve_reference(params) + expect(targets).to eq(expected_targets) + end + + it 'returns all windows entities' do + expected_targets = [ + {'name' => 'win2012r2-agent', 'uri' => 'win2012r2-agent'}, + {'name' => 'win2016-agent', 'uri' => 'win2016-agent'}, + {'name' => 'win2016-agent-bolt', 'uri' => 'win2016-agent-bolt'} + ] + params = { 'subscription' => 'windows' } + targets = described_class.resolve_reference(params) + expect(targets).to eq(expected_targets) + end + + it 'returns all windows entities with ip addresses' do + expected_targets = [ + {'name' => 'win2012r2-agent', 'uri' => '192.168.52.24'}, + {'name' => 'win2016-agent', 'uri' => '192.168.52.26'}, + {'name' => 'win2016-agent-bolt', 'uri' => '192.168.52.28'} + ] + params = { 'subscription' => 'windows', 'uri_ipaddress' => true, 'interface_list' => ['Ethernet 2']} + targets = described_class.resolve_reference(params) + expect(targets).to eq(expected_targets) + end +end diff --git a/tasks/resolve_reference.json b/tasks/resolve_reference.json new file mode 100644 index 0000000000..2dc5f3cb44 --- /dev/null +++ b/tasks/resolve_reference.json @@ -0,0 +1,18 @@ +{ + "description": "Generate targets from Sensu Go", + "input_method": "stdin", + "parameters": { + "namespace": { + "type": "Optional[String[1]]" + }, + "subscription": { + "type": "Optional[String[1]]" + }, + "interface_list": { + "type": "Optional[Array]" + }, + "uri_ipaddress": { + "type": "Optional[Boolean]" + } + } +} diff --git a/tasks/resolve_reference.rb b/tasks/resolve_reference.rb new file mode 100644 index 0000000000..e22adc12b3 --- /dev/null +++ b/tasks/resolve_reference.rb @@ -0,0 +1,94 @@ +#!/opt/puppetlabs/puppet/bin/ruby +require 'resolv' +require 'json' +require 'open3' + +class SensuResolveReference + def self.is_ip?(ip) + !!(ip =~ Resolv::IPv4::Regex) + end + + def self.sensuctl_entities(namespace) + cmd = ['sensuctl','entity','list','--format','json'] + if namespace + cmd << '--namespace' + cmd << namespace + end + stdout, stderr, status = Open3.capture3(cmd.join(' ')) + if status != 0 + raise Exception, "Failed to execute #{cmd.join(' ')}: #{stdout + stderr}" + end + entities = JSON.parse(stdout) + entities + end + + def self.entities_to_targets(entities, interface_list, uri_ipaddress) + targets = [] + entities.each do |e| + target = {} + target['name'] = e['metadata']['name'] + # Get IP address for URI + # If interface_list was defined then find first match + # If interface_list not defined and only one interface, use that as ipaddress + # If interface_list not defined and more than one interface, use name + ipaddress = target['name'] + interface_address = {} + e['system']['network']['interfaces'].each do |i| + i['addresses'].each do |a| + address = a.split('/')[0] + next unless is_ip?(address) + next if address =~ /^127/ + interface_address[i['name']] = address + break + end + end + if interface_list + interface_list.each do |i| + if interface_address.key?(i) + ipaddress = interface_address[i] + break + end + end + else + if interface_address.keys.size == 1 + i = interface_address.keys[0] + ipaddress = interface_address[i] + end + end + target['uri'] = ipaddress + targets << target + end + targets + end + + def self.resolve_reference(params) + namespace = params['namespace'] + subscription = params['subscription'] + interface_list = params['interface_list'] + uri_ipaddress = params['uri_ipaddress'] + + entities = sensuctl_entities(namespace) + + if subscription + entities.select! { |e| e['subscriptions'].include?(subscription) } + end + + targets = entities_to_targets(entities, interface_list, uri_ipaddress) + targets + end + + def self.run + params = JSON.parse(STDIN.read) + + targets = resolve_reference(params) + + puts({ value: targets }.to_json) + + rescue Exception => e + puts({ _error: e.message }.to_json) + exit 1 + end +end + +SensuResolveReference.run if $PROGRAM_NAME == __FILE__ +