diff options
-rw-r--r-- | pyenc/__init__.py | 48 | ||||
-rw-r--r-- | pyenc/app/model.py | 2 | ||||
-rw-r--r-- | pyenc/enumerate_classes.py | 1 | ||||
-rw-r--r-- | pyenc/static/css/style.scss | 33 | ||||
-rw-r--r-- | pyenc/static/js/script2.js | 164 | ||||
-rw-r--r-- | pyenc/templates/base.html | 1 | ||||
-rw-r--r-- | pyenc/templates/class.html | 9 | ||||
-rw-r--r-- | pyenc/templates/file.html | 2 | ||||
-rw-r--r-- | pyenc/templates/host.html | 18 | ||||
-rw-r--r-- | pyenc/templates/list.html | 2 |
10 files changed, 261 insertions, 19 deletions
diff --git a/pyenc/__init__.py b/pyenc/__init__.py index 6400b49..28829a1 100644 --- a/pyenc/__init__.py +++ b/pyenc/__init__.py @@ -9,8 +9,14 @@ functionallity is pulled in from other modules. import logging import random import os.path +import subprocess -from domonic.html import a +from domonic.html import ( + a, + div, + img, + span, +) import flask from flask import ( Flask, @@ -20,6 +26,9 @@ from flask import ( url_for ) from sqlalchemy.orm import joinedload +import werkzeug.datastructures +import http.client +import urllib.parse from .app import model from .app import cmdline @@ -60,19 +69,19 @@ def create_app(): return flask.render_template( 'list.html', title='Hosts', - items=[a(x.fqdn, _href=f'/host/{x.fqdn}') + items=[div(img(_class='distroicon', + _height=16, + **{'_data-host':x.fqdn}), + a(x.fqdn, _href=f'/host/{x.fqdn}'), + span(x.summary,_class='summary') if x.summary else [], + _class='hostline', + ) for x in result]) @app.route('/host/<host>') def host(host): host = model.Host.query.where(model.Host.fqdn == host).one() - return flask.render_template( - 'host.html', - title=host.fqdn, - env=host.environment.name, - classes=[a(x.name, _href=f'/class/{x.name}') - for x in host.classes]) - + return flask.render_template('host.html', host=host) @app.route('/environment') def environments(): @@ -104,7 +113,8 @@ def create_app(): return flask.render_template( 'list.html', title='Classes', - items=[a(cls.name, _href=f'/class/{cls.name}') + items=[div(a(cls.name, _href=f'/class/{cls.name}'), + span(_class="count", **{'_data-cls': cls.name})) for cls in clss]) @app.route('/class/<name>') @@ -129,6 +139,24 @@ def create_app(): title=f'{environment}/{path}', content=content) + @app.route('/pdb') + def pdb_proxy(): + h1 = http.client.HTTPConnection('busting.adrift.space:8080') + params = urllib.parse.urlencode({ + 'query': request.args.get('query') + }) + type = request.args.get('type') + # h1.request('GET', f'/pdb/query/v4?{params}') + h1.request('GET', f'/pdb/query/v4/{type}?{params}') + r1 = h1.getresponse() + data = r1.read() + d = werkzeug.datastructures.Headers() + for key, value in r1.headers.items(): + d.add(key, value) + return flask.Response(response=[data], + status=r1.status, + headers=d) + # API? @app.route('/remove', methods=['POST']) def remove_classes(): diff --git a/pyenc/app/model.py b/pyenc/app/model.py index fed56f2..032b6d3 100644 --- a/pyenc/app/model.py +++ b/pyenc/app/model.py @@ -73,6 +73,8 @@ class Host(db.Model): environment_id = db.Column(db.Integer, db.ForeignKey(f'{Environment.__tablename__}.id')) environment = db.relationship('Environment', back_populates='hosts') + summary = db.Column(db.Text) + classes = db.relationship( 'PuppetClass', back_populates='hosts', diff --git a/pyenc/enumerate_classes.py b/pyenc/enumerate_classes.py index 29cf2f7..7991246 100644 --- a/pyenc/enumerate_classes.py +++ b/pyenc/enumerate_classes.py @@ -261,6 +261,7 @@ def run(path_base: Path = '/etc/puppetlabs/code/environments', """), {'name': class_name}) # Add class to environment (if not already there) + # TODO this adds to much db.engine.execute(text(""" INSERT INTO environment_classes (environment_id, class_id) SELECT :env, id FROM puppet_class WHERE puppet_class.name = :name diff --git a/pyenc/static/css/style.scss b/pyenc/static/css/style.scss index 9c4e76f..b5b34ea 100644 --- a/pyenc/static/css/style.scss +++ b/pyenc/static/css/style.scss @@ -70,3 +70,36 @@ body > main { table { font-size: 80%; } + + +.generic-list { + display: grid; + grid-template-columns: 1.2em 1fr 1fr; + align-items: center; + + li, div { + display: contents; + } + + img { + grid-column: 1; + } + + a { + grid-column: 2; + } + + .summary { + grid-column: 3; + } +} + +.distroicon { + height: 1em; +} + + +p.summary { + background-color: lightgray; + padding: 1em; +} diff --git a/pyenc/static/js/script2.js b/pyenc/static/js/script2.js new file mode 100644 index 0000000..6f06fc7 --- /dev/null +++ b/pyenc/static/js/script2.js @@ -0,0 +1,164 @@ +const distro_names = new Map([['archlinux', 'arch'], +]) + +String.prototype.toTitleCase = function () { + if (this.length == 0) return "" + + return this[0].toUpperCase() + this.substring(1).toLowerCase() +} + +async function pdb(type, query) { + const url = new URL('/pdb', window.location) + const payload = JSON.stringify(query) + url.searchParams.append('type', type); + url.searchParams.append('query', payload); + let response = await fetch (url.href) + if (! response.ok) { + throw response.text() + } + return response.json() + +} + +function upcasePuppet(s) { + return s.split('::').map(s => s.toTitleCase()).join('::') +} + +function downcasePuppet(s) { + return s.split('::').map(s => s.toLowerCase()).join('::') +} + +function formatValue(value, target) { + if (value instanceof Array) { + let ul = document.createElement('ul'); + for (let item of value) { + let li = document.createElement('li'); + formatValue(item, li); + ul.appendChild(li); + } + target.appendChild(ul); + } else if (value instanceof Object) { + let dl = document.createElement('dl'); + for (let [k, v] of Object.entries(value)) { + let dt = document.createElement('dt'); + let dd = document.createElement('dd'); + dt.textContent = k; + formatValue(v, dd); + dl.appendChild(dt); + dl.appendChild(dd); + } + target.appendChild(dl); + } else { + target.textContent = value; + } +} + + +/* For host page, find all classes that host has, and display it along with all + its parameters */ +async function populate_host_classes(class_container, fqdn) { + const resources = await pdb('resources', ['extract', ['title', 'parameters'], + ['and', + ['=', 'certname', fqdn], + ['=', 'type', 'Class']]]) + if (resources.length == 0) { + class_container.textContent = 'No classes found' + return + } + let root = document.createElement('dl'); + class_container.appendChild(root); + for (let item of resources) { + let dt = document.createElement('dt'); + let a = document.createElement('a'); + a.textContent = item['title'] + a.href = '/class/' + downcasePuppet(item['title']) + dt.appendChild(a) + + // let dd = document.createElement('dd'); + // let dl = document.createElement('dl'); + // dd.appendChild(dl); + // for (let [key, value] of Object.entries(item['parameters'])) { + // let dt_ = document.createElement('dt'); + // dt_.textContent = key; + // dl.appendChild(dt_); + + // let dd_ = document.createElement('dd'); + // formatValue(value, dd_); + // dl.appendChild(dd_); + // } + root.appendChild(dt); + // root.appendChild(dd); + } +} + +async function populate_class_hosts(host_container, title) { + // resources[certname, file]{type = "Class" and title = "Puppet"} + const resources = await pdb('resources', ['extract', ['certname', 'file'], + ['and', + ['=', 'title', upcasePuppet(title)], + ['=', 'type', 'Class']]]) + if (resources.length == 0) { + host_container.textContent = 'No hosts uses this class' + return + } + + let ul = document.createElement('ul'); + host_container.appendChild(ul); + for (let item of resources) { + let fqdn = item['certname'] + let include_position = item['filename'] + + let li = document.createElement('li'); + let a = document.createElement('a'); + a.textContent = fqdn + a.href = `/host/${fqdn}` + + let span = document.createElement('span'); + span.textContent = include_position; + span.classList.add('include-position'); + + li.appendChild(a, span); + ul.appendChild(li); + } +} + +window.addEventListener('load', async function () { + {/* for host list */ + let j = await pdb('facts', ['extract', ['certname', 'value'], ['=', 'name', 'operatingsystem']]) + for (let item of j) { + // NOTE these should always be cached + const target = document.querySelector(`img.distroicon[data-host="${item["certname"]}"]`); + if (! target) continue; + + let value = item['value'] + target.alt = value + let name = value.toLowerCase() + let filename = distro_names.get(name) || name + target.src = `/static/distro-icon/128_${filename}.png` + } + } + + {/* For class list */ + let m = new Map + let j = await pdb('resources', + ['extract', [['function', 'count'], 'title'], + ['=', 'type', 'Class'], + ['group_by', 'title']]) + for (let item of j) { + m.set(item['title'], item['count']); + } + + for (let el of document.querySelectorAll('[data-cls]')) { + let count = m.get(upcasePuppet(el.dataset.cls)) || '0' + el.innerText = `(${count})`; + } + } + + // {/* For environment list */ + // for (let env_type of ['facts_environment', 'report_environment']) { + // pdb('nodes', ['extract', [['function', 'count'], env_type], + // ['group_by', env_type]]) + // } + // } + +}); diff --git a/pyenc/templates/base.html b/pyenc/templates/base.html index b927b3d..a1c8874 100644 --- a/pyenc/templates/base.html +++ b/pyenc/templates/base.html @@ -7,6 +7,7 @@ <link rel="icon" href="{{ url_for('static', filename='icon.svg') }}"/> <link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet"/> <!-- <script type="module" src="{{ url_for('static', filename='js/script.js') }}"></script> --> + <script src="{{ url_for('static', filename='js/script2.js') }}"></script> <title>Puppet Classifier</title> </head> <body> diff --git a/pyenc/templates/class.html b/pyenc/templates/class.html index bfae1b2..3c8b809 100644 --- a/pyenc/templates/class.html +++ b/pyenc/templates/class.html @@ -29,7 +29,7 @@ {% endfor %} </tbody> </table> - <dt>Hosts</dt> + <dt>Direct Hosts</dt> <dd> <ul> {% for host in cls.hosts %} @@ -37,6 +37,13 @@ {% endfor %} </ul> </dd> + <dt>All Hosts</dt> + <dd id="class-hosts"> + </dd> </dl> +<script> + let node = document.getElementById('class-hosts') + populate_class_hosts(node, "{{ title }}") +</script> {% endblock %} {# ft:jinja #} diff --git a/pyenc/templates/file.html b/pyenc/templates/file.html index 19bc811..84b9e86 100644 --- a/pyenc/templates/file.html +++ b/pyenc/templates/file.html @@ -1,6 +1,6 @@ {% extends "base.html" %} {% block content %} <h1>File ‘{{ title }}’</h1> -<pre>{{ content }}</pre> +<pre class="highlight">{{ content|safe }}</pre> {% endblock %} {# ft:jinja #} diff --git a/pyenc/templates/host.html b/pyenc/templates/host.html index 84e76e3..917d535 100644 --- a/pyenc/templates/host.html +++ b/pyenc/templates/host.html @@ -1,24 +1,30 @@ {% extends "base.html" %} {% block content %} -<h1>Host ‘{{ title }}’</h1> +<h1>Host ‘{{ host.fqdn }}’</h1> +<p class="summary">{{ host.summary }}</p> <dl> <dt>Environment</dt> - <dd><a href="/environment/{{ env }}">{{ env }}</a></dd> + <dd><a href="/environment/{{ host.environment.name }}" + >{{ host.environment.name }}</a></dd> <dt>Direct Classes</dt> <dd> <ul> - {% for cls in classes %} - <li>{{ cls|safe }}</li> + {% for cls in host.classes %} + <li><a href="/class/{{ cls.name }}">{{ cls.name }}</a></li> {% endfor %} </ul> </dd> + <dt>All Classes</dt> + <dd id="all-classes"></dd> <!-- - - indirect classes - - basic description - basic system info - link to further documentation - notes --> </dl> +<script> + let node = document.getElementById('all-classes') + populate_host_classes(node, "{{ host.fqdn }}") +</script> {% endblock %} {# ft:jinja #} diff --git a/pyenc/templates/list.html b/pyenc/templates/list.html index dd267c5..e7e1f4c 100644 --- a/pyenc/templates/list.html +++ b/pyenc/templates/list.html @@ -1,7 +1,7 @@ {% extends "base.html" %} {% block content %} <h1>{{ title }}</h1> -<ul> +<ul class='generic-list'> {% for item in items %} <li>{{ item|safe }}</li> {% endfor %} |