aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--pyenc/__init__.py48
-rw-r--r--pyenc/app/model.py2
-rw-r--r--pyenc/enumerate_classes.py1
-rw-r--r--pyenc/static/css/style.scss33
-rw-r--r--pyenc/static/js/script2.js164
-rw-r--r--pyenc/templates/base.html1
-rw-r--r--pyenc/templates/class.html9
-rw-r--r--pyenc/templates/file.html2
-rw-r--r--pyenc/templates/host.html18
-rw-r--r--pyenc/templates/list.html2
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 %}