diff --git a/README.md b/README.md index 1aa83db51..9524fb1d3 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ The file has to be identical to [default_settings.py](https://github.com/voxpupuli/puppetboard/blob/master/puppetboard/default_settings.py) but should only override the settings you need changed. -If you run PuppetDB and Puppetboard on the same machine the default settings provided will be enough to get you started +If you run PuppetDB and Puppetboard on the same machine the default settings provided will be enough to get you started and you won't need a custom settings file. Assuming your webserver and PuppetDB machine are not identical you will at least have to change the following settings: @@ -151,7 +151,7 @@ PUPPETDB_CERT="-----BEGIN CERTIFICATE----- PUPPETDB_CERT=LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQouLi4KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ== ``` -For information about how to generate the correct keys please refer to the +For information about how to generate the correct keys please refer to the [pypuppetdb documentation](https://pypuppetdb.readthedocs.io/en/latest/connecting.html#ssl). Alternatively it is possible to explicitly specify the protocol to be used setting the `PUPPETDB_PROTO` variable. @@ -186,6 +186,7 @@ Other settings that might be interesting, in no particular order: values being unique per node, like ipaddress, uuid, and serial number, as well as structured facts it was no longer feasible to generate a graph for everything. - `INVENTORY_FACTS`: A list of tuples that serve as the column header and the fact name to search for to create +- `INVENTORY_FACT_TEMPLATES`: A mapping between fact name and jinja template to customize display the inventory page. If a fact is not found for a node then `undef` is printed. - `ENABLE_CATALOG`: If set to `True` allows the user to view a node's latest catalog. This includes all managed resources, their file-system locations and their relationships, if available. Defaults to `False`. diff --git a/puppetboard/app.py b/puppetboard/app.py index d0c7e65b0..4de2f5dc5 100644 --- a/puppetboard/app.py +++ b/puppetboard/app.py @@ -72,7 +72,27 @@ def now(format='%m/%d/%Y %H:%M:%S'): def version(): return __version__ - return dict(now=now, version=version) + def fact_os_detection(os_facts): + os_name = "" + os_family = os_facts['family'] + + try: + if os_family == "windows": + os_name = os_facts["windows"]["product_name"] + elif os_family == "Darwin": + os_name = os_facts["macosx"]["product"] + else: + os_name = os_facts["distro"]["description"] + except KeyError: + pass + + return os_name + + return dict( + now=now, + version=version, + fact_os_detection=fact_os_detection, + ) @app.route('/offline/') diff --git a/puppetboard/default_settings.py b/puppetboard/default_settings.py index c088ad149..107ca63e3 100644 --- a/puppetboard/default_settings.py +++ b/puppetboard/default_settings.py @@ -43,12 +43,21 @@ 'osfamily', 'puppetversion', 'processorcount'] -INVENTORY_FACTS = [('Hostname', 'fqdn'), +INVENTORY_FACTS = [('Hostname', 'trusted'), ('IP Address', 'ipaddress'), - ('OS', 'lsbdistdescription'), + ('OS', 'os'), ('Architecture', 'hardwaremodel'), ('Kernel Version', 'kernelrelease'), ('Puppet Version', 'puppetversion'), ] + +INVENTORY_FACT_TEMPLATES = { + 'trusted': ( + """""" + """{{value.hostname}}""" + """""" + ), + 'os': "{{ fact_os_detection(value) }}", +} REFRESH_RATE = 30 DAILY_REPORTS_CHART_ENABLED = True DAILY_REPORTS_CHART_DAYS = 8 diff --git a/puppetboard/docker_settings.py b/puppetboard/docker_settings.py index d9811efb3..8a3cc334d 100644 --- a/puppetboard/docker_settings.py +++ b/puppetboard/docker_settings.py @@ -1,3 +1,4 @@ +import json import os import tempfile import base64 @@ -104,9 +105,9 @@ def coerce_bool(v, default): # export INVENTORY_FACTS="Hostname, fqdn, IP Address, ipaddress,.. etc" # Define default array of of strings, this code is a bit neater than having # a large string -INVENTORY_FACTS_DEFAULT = ','.join(['Hostname', 'fqdn', - 'IP Address', 'ipaddress', - 'OS', 'lsbdistdescription', +INVENTORY_FACTS_DEFAULT = ','.join(['Hostname', 'trusted', + 'IP Address', 'networking', + 'OS', 'os', 'Architecture', 'hardwaremodel', 'Kernel Version', 'kernelrelease', 'Puppet Version', 'puppetversion']) @@ -115,6 +116,23 @@ def coerce_bool(v, default): # array: ['Key', 'Value'] INV_STR = os.getenv('INVENTORY_FACTS', INVENTORY_FACTS_DEFAULT).split(',') +# To render jinja template we expect env var to be JSON +INVENTORY_FACT_TEMPLATES = { + 'trusted': ( + """""" + """{{value.hostname}}""" + """""" + ), + 'networking': """{{ value.ip }}""", + 'os': "{{ fact_os_detection(value) }}", +} + +INV_TPL_STR = os.getenv('INVENTORY_FACT_TEMPLATES') + +if INV_TPL_STR: + INVENTORY_FACT_TEMPLATES = json.loads(INV_TPL_STR) + + # Take the Array and convert it to a tuple INVENTORY_FACTS = [(INV_STR[i].strip(), INV_STR[i + 1].strip()) for i in range(0, len(INV_STR), 2)] diff --git a/puppetboard/views/inventory.py b/puppetboard/views/inventory.py index 81304beeb..c83493795 100644 --- a/puppetboard/views/inventory.py +++ b/puppetboard/views/inventory.py @@ -1,5 +1,5 @@ from flask import ( - render_template, request + render_template, request, render_template_string ) from pypuppetdb.QueryBuilder import (AndOperator, EqualsOperator, OrOperator) @@ -58,6 +58,7 @@ def inventory_ajax(env): envs = environments() check_env(env, envs) headers, fact_names = inventory_facts() + fact_templates = app.config['INVENTORY_FACT_TEMPLATES'] query = AndOperator() fact_query = OrOperator() @@ -73,7 +74,18 @@ def inventory_ajax(env): for fact in facts: if fact.node not in fact_data: fact_data[fact.node] = {} - fact_data[fact.node][fact.name] = fact.value + + fact_value = fact.value + + if fact.name in fact_templates: + fact_template = fact_templates[fact.name] + fact_value = render_template_string( + fact_template, + current_env=env, + value=fact_value, + ) + + fact_data[fact.node][fact.name] = fact_value total = len(fact_data) diff --git a/test/test_docker_settings.py b/test/test_docker_settings.py index 92752f94b..c009a26e7 100644 --- a/test/test_docker_settings.py +++ b/test/test_docker_settings.py @@ -112,6 +112,19 @@ def test_invtory_facts_custom(cleanup_env): validate_facts(docker_settings.INVENTORY_FACTS) +def test_inventory_fact_tempaltes_default(cleanup_env): + assert isinstance(docker_settings.INVENTORY_FACT_TEMPLATES, dict) + assert len(docker_settings.INVENTORY_FACT_TEMPLATES) == 3 + + +def test_inventory_fact_tempaltes_custom(cleanup_env): + os.environ['INVENTORY_FACT_TEMPLATES'] = """{"os": "{{ fact_os_detection(value) }}"}""" + reload(docker_settings) + + assert isinstance(docker_settings.INVENTORY_FACT_TEMPLATES, dict) + assert len(docker_settings.INVENTORY_FACT_TEMPLATES) == 1 + + def test_graph_facts_defautl(cleanup_env): facts = docker_settings.GRAPH_FACTS assert isinstance(facts, list) diff --git a/test/views/test_inventory.py b/test/views/test_inventory.py index 5b6a30c72..6152de019 100644 --- a/test/views/test_inventory.py +++ b/test/views/test_inventory.py @@ -4,31 +4,257 @@ from pypuppetdb.types import Fact from puppetboard import app -from puppetboard.views.inventory import inventory_facts @pytest.fixture def mock_puppetdb_inventory_facts(mocker): - nodes = ['node1', 'node2'] + node_facts = [ + { + "node": "node-debian.test.domain", + "environment": "production", + "facts": { + "hardwaremodel": "x86_64", + "kernelrelease": "5.10.0-17-amd64", + "puppetversion": "6.27.0", + "trusted": { + "domain": "local", + "certname": "node-debian.test.domain", + "hostname": "node-debian", + "extensions": {}, + "authenticated": "remote", + }, + "os": { + "name": "Debian", + "distro": { + "id": "Debian", + "release": {"full": "11.4", "major": "11", "minor": "4"}, + "codename": "bullseye", + "description": "Debian GNU/Linux 11 (bullseye)", + }, + "family": "Debian", + "release": {"full": "11.4", "major": "11", "minor": "4"}, + "selinux": {"enabled": False}, + "hardware": "x86_64", + "architecture": "amd64", + }, + "networking": { + "interfaces": { + "lo": { + "ip": "127.0.0.1", + "mtu": 65536, + "netmask": "255.0.0.0", + "network": "127.0.0.0", + "bindings": [ + { + "address": "127.0.0.1", + "netmask": "255.0.0.0", + "network": "127.0.0.0", + } + ], + }, + "eth0": { + "ip": "192.168.0.2", + "mac": "", + "mtu": 1500, + "dhcp": "192.168.0.1", + "netmask": "255.255.255.0", + "network": "192.168.0.0", + "bindings": [ + { + "address": "192.168.0.2", + "netmask": "255.255.255.0", + "network": "192.168.0.0", + } + ], + }, + }, + "ip": "192.168.0.2", + "primary": "eth0", + "mtu": 1500, + "hostname": "node-debian", + "dhcp": "192.168.0.1", + "fqdn": "node-debian.test.domain", + "netmask": "255.255.255.0", + "network": "192.168.0.0", + "domain": "test.domain", + "mac": "", + }, + }, + }, + { + "node": "node-windows.test.domain", + "environment": "production", + "facts": { + "hardwaremodel": "x86_64", + "kernelrelease": "10.0.19041", + "puppetversion": "6.27.0", + "trusted": { + "domain": "local", + "certname": "node-windows.test.domain", + "hostname": "node-windows", + "extensions": {}, + "authenticated": "remote", + }, + "os": { + "name": "windows", + "family": "windows", + "release": {"full": "10", "major": "10"}, + "windows": { + "system32": "C:\\WINDOWS\\system32", + "edition_id": "Professional", + "release_id": "2009", + "product_name": "Windows 10 Pro", + "installation_type": "Client", + }, + "hardware": "x86_64", + "architecture": "x64", + }, + "networking": { + "interfaces": { + "Ethernet 1": { + "ip": "192.168.0.3", + "mtu": 1500, + "bindings": [ + { + "address": "192.168.0.3", + "netmask": "255.255.255.0", + "network": "192.168.0.0", + } + ], + "netmask": "255.255.255.0", + "network": "192.168.0.0", + "mac": "", + } + }, + "ip": "192.168.0.3", + "primary": "Ethernet 1", + "mtu": 1500, + "hostname": "node-windows", + "fqdn": "node-windows.test.domain", + "netmask": "255.255.255.0", + "network": "192.168.0.0", + "domain": "test.domain", + "mac": "", + }, + }, + }, + { + "node": "node-mac.test.domain", + "environment": "production", + "facts": { + "hardwaremodel": "x86_64", + "kernelrelease": "21.6.0", + "puppetversion": "6.27.0", + "trusted": { + "domain": "local", + "certname": "node-mac.test.domain", + "hostname": "node-mac", + "extensions": {}, + "authenticated": "remote", + }, + "os": { + "name": "Darwin", + "family": "Darwin", + "macosx": { + "build": "21G72", + "product": "macOS", + "version": { + "full": "12.5", + "major": "12", + "minor": "5", + "patch": "0", + }, + }, + "release": {"full": "21.6.0", "major": "21", "minor": "6"}, + "hardware": "x86_64", + "architecture": "x86_64", + }, + "networking": { + "interfaces": { + "lo0": { + "ip": "127.0.0.1", + "bindings6": [ + { + "scope6": "host", + "address": "::1", + "netmask": "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", + "network": "::1", + }, + { + "scope6": "link", + "address": "fe80::1", + "netmask": "ffff:ffff:ffff:ffff::", + "network": "fe80::", + }, + ], + "mtu": 16384, + "bindings": [ + { + "address": "127.0.0.1", + "netmask": "255.0.0.0", + "network": "127.0.0.0", + } + ], + "network6": "::1", + "netmask6": "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", + "ip6": "::1", + "netmask": "255.0.0.0", + "network": "127.0.0.0", + "scope6": "host", + }, + "en0": { + "ip": "192.168.0.4", + "mac": "", + "mtu": 1500, + "dhcp": "192.168.0.1", + "netmask": "255.255.255.0", + "network": "192.168.0.0", + "bindings": [ + { + "address": "192.168.0.4", + "netmask": "255.255.255.0", + "network": "192.168.0.0", + } + ], + }, + }, + "ip": "192.168.0.4", + "primary": "en0", + "mtu": 1500, + "hostname": "node-mac", + "dhcp": "192.168.0.1", + "fqdn": "node-mac.test.domain", + "netmask": "255.255.255.0", + "network": "192.168.0.0", + "domain": "test.domain", + "mac": "", + }, + }, + }, + ] + facts_list = [ Fact( - node=node, + node=node['node'], + environment=node['environment'], name=fact_name, - value='foobar', - environment='production', + value=fact_value, ) - for node in nodes - for fact_name in inventory_facts()[1] # fact names + for node in node_facts + for fact_name, fact_value in node['facts'].items() ] - return mocker.patch.object(app.puppetdb, 'facts', return_value=iter(facts_list)) + return mocker.patch.object(app.puppetdb, "facts", return_value=iter(facts_list)) -def test_inventory_json(client, mocker, - mock_puppetdb_environments, - mock_puppetdb_inventory_facts): +def test_inventory_json( + client, + mocker, + mock_puppetdb_environments, + mock_puppetdb_inventory_facts, +): - rv = client.get('/inventory/json') + rv = client.get("/inventory/json") assert rv.status_code == 200 - result_json = json.loads(rv.data.decode('utf-8')) - assert len(result_json['data']) == 2 + result_json = json.loads(rv.data.decode("utf-8")) + assert len(result_json["data"]) == 3