From 5ae80ca783823bcd25f216be922dce16a13ad5e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Wed, 17 Aug 2022 02:19:27 +0200 Subject: work --- pyenc/__init__.py | 48 ++++++++++--- pyenc/app/model.py | 2 + pyenc/enumerate_classes.py | 1 + pyenc/static/css/style.scss | 33 +++++++++ pyenc/static/js/script2.js | 164 ++++++++++++++++++++++++++++++++++++++++++++ pyenc/templates/base.html | 1 + pyenc/templates/class.html | 9 ++- pyenc/templates/file.html | 2 +- pyenc/templates/host.html | 18 +++-- pyenc/templates/list.html | 2 +- 10 files changed, 261 insertions(+), 19 deletions(-) create mode 100644 pyenc/static/js/script2.js 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/') 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/') @@ -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 @@ + Puppet Classifier 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 %} -
Hosts
+
Direct Hosts
    {% for host in cls.hosts %} @@ -37,6 +37,13 @@ {% endfor %}
+
All Hosts
+
+
+ {% 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 %}

File ‘{{ title }}’

-
{{ content }}
+
{{ content|safe }}
{% 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 %} -

Host ‘{{ title }}’

+

Host ‘{{ host.fqdn }}’

+

{{ host.summary }}

Environment
-
{{ env }}
+
{{ host.environment.name }}
Direct Classes
    - {% for cls in classes %} -
  • {{ cls|safe }}
  • + {% for cls in host.classes %} +
  • {{ cls.name }}
  • {% endfor %}
+
All Classes
+
+ {% 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 %}

{{ title }}

-
    +
      {% for item in items %}
    • {{ item|safe }}
    • {% endfor %} -- cgit v1.2.3