aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHugo Hörnquist <hugo@lysator.liu.se>2022-02-27 17:31:50 +0100
committerHugo Hörnquist <hugo@lysator.liu.se>2022-02-27 17:31:50 +0100
commitbbd692427632bf525f0fa46eeab0de0f8a563661 (patch)
tree6c13fdc978e3b5e97498bc16322d5ee56c8c1617
parentInitial commit. (diff)
downloadpuppet-classifier-bbd692427632bf525f0fa46eeab0de0f8a563661.tar.gz
puppet-classifier-bbd692427632bf525f0fa46eeab0de0f8a563661.tar.xz
Initial code add.
-rw-r--r--package.json33
-rw-r--r--pyenc/__init__.py80
-rw-r--r--pyenc/db.py28
-rw-r--r--pyenc/enc.py25
-rw-r--r--pyenc/model.py70
-rw-r--r--pyenc/static/.gitignore1
-rw-r--r--pyenc/static/css/style.css35
-rw-r--r--pyenc/static/js-source/script.js62
-rw-r--r--pyenc/templates/base.html28
-rw-r--r--pyenc/templates/start_page.html33
-rw-r--r--requirements.txt21
11 files changed, 416 insertions, 0 deletions
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..f7440c4
--- /dev/null
+++ b/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "puppet-classifier",
+ "version": "0.1",
+ "private": true,
+ "dependencies": {
+ "package.json": "^2.0.1",
+ "react": "^17.0.2"
+ },
+ "babel": {
+ "presets": [
+ "@babel/preset-env",
+ "@babel/preset-react"
+ ],
+ "plugins": []
+ },
+ "scripts": {
+ "build": "browserify --debug -t babelify static/js-source/script.js -o static/js/script.js",
+ "watch": "watchify --debug -t babelify static/js-source/script.js -o static/js/script.js"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.16.7",
+ "@babel/preset-env": "^7.16.7",
+ "@babel/preset-react": "^7.16.7",
+ "babel-cli": "^6.26.0",
+ "babel-loader": "^8.0.4",
+ "babel-preset-react": "^6.24.1",
+ "babel-preset-react-app": "^3.1.2",
+ "babelify": "^10.0.0",
+ "browserify": "^17.0.0",
+ "react-dom": "^17.0.2",
+ "watchify": "^4.0.0"
+ }
+}
diff --git a/pyenc/__init__.py b/pyenc/__init__.py
new file mode 100644
index 0000000..6323fbe
--- /dev/null
+++ b/pyenc/__init__.py
@@ -0,0 +1,80 @@
+"""
+app object setup for application
+"""
+
+import flask
+from flask import (
+ Flask,
+ request,
+ Response,
+ flash,
+ redirect,
+ url_for
+ )
+
+import random
+import json
+
+from . import model
+
+import yaml
+
+def create_app():
+ app = Flask(__name__, instance_relative_config=True)
+
+ app.config.from_pyfile('settings.py')
+
+ from . import db
+ db.init_app(app)
+
+ from . import enc
+ enc.init_app(app)
+
+ # not API
+ @app.route('/')
+ def root_page():
+ return flask.render_template('start_page.html',
+ hosts=model.Host.query.order_by(model.Host.fqdn),
+ random=random,
+ str=str,
+ )
+
+ # API?
+ @app.route('/remove', methods=['POST'])
+ def remove_classes():
+ print(request.form['fqdn'])
+ print(request.form.getlist('cls'))
+ flash('Classes removed')
+ return redirect(url_for('root_page'))
+
+ # API
+ @app.route('/list-classes')
+ def list_classes():
+ q = request.args.get('q', '')
+ qq = '%{}%'.format('%'.join(q.split(' ')))
+
+ results = model.PuppetClass.query.where(model.PuppetClass.class_name.like(qq)).all()
+ print(qq)
+ return Response(json.dumps([x.class_name for x in results]),
+ mimetype='application/json')
+
+ @app.route('/enc')
+ def enc():
+ fqdn = request.args.get('fqdn', 'default')
+ host = model.Host.query.where(model.Host.fqdn==fqdn).first()
+ if not host:
+ return Response(f"No host with name {fqdn}",
+ status=404,
+ )
+
+ out = {
+ 'environment': host.environment,
+ 'classes': [cls.class_name for cls in host.classes],
+ }
+ return Response(yaml.dump(out),
+ mimetype='application/x-yaml')
+
+
+
+
+ return app
diff --git a/pyenc/db.py b/pyenc/db.py
new file mode 100644
index 0000000..ea7cc14
--- /dev/null
+++ b/pyenc/db.py
@@ -0,0 +1,28 @@
+"""
+Database connection for application
+"""
+
+import click
+from flask import current_app, g
+from flask.cli import with_appcontext
+from .model import db
+
+@with_appcontext
+def init_db():
+ db.create_all()
+
+@click.command('init-db')
+@with_appcontext
+def init_db_command():
+ """
+ """
+ # init_db()
+ #print(db)
+ print(db)
+ db.create_all()
+ click.echo('Initialized the database.')
+
+def init_app(app):
+ # app.teardown_appcontext(close_db)
+ db.init_app(app)
+ app.cli.add_command(init_db_command)
diff --git a/pyenc/enc.py b/pyenc/enc.py
new file mode 100644
index 0000000..7ec3a8e
--- /dev/null
+++ b/pyenc/enc.py
@@ -0,0 +1,25 @@
+
+import click
+from flask import current_app, g
+from flask.cli import with_appcontext
+from .db import db
+from . import model
+
+import yaml
+
+@click.command('enc')
+@click.option('--fqdn', help='Node to get data for')
+@with_appcontext
+def run_enc(fqdn):
+ host = model.Host.query.where(model.Host.fqdn==fqdn).first()
+ if not host:
+ print(f"No host with name {fqdn}")
+ return 1
+ out = {
+ 'environment': host.environment,
+ 'classes': [cls.class_name for cls in host.classes],
+ }
+ print(yaml.dump(out))
+
+def init_app(app):
+ app.cli.add_command(run_enc)
diff --git a/pyenc/model.py b/pyenc/model.py
new file mode 100644
index 0000000..dc0db05
--- /dev/null
+++ b/pyenc/model.py
@@ -0,0 +1,70 @@
+"""
+Database model for application
+"""
+
+from flask_sqlalchemy import SQLAlchemy
+import requests
+import yaml
+
+db = SQLAlchemy()
+
+host_classes = db.Table('host_classes',
+ db.Column('host_id', db.ForeignKey('host.id')),
+ db.Column('class_id', db.ForeignKey('puppet_class.id')))
+
+# class HostClasses(db.Model):
+# __tablename__ = 'host_classes'
+# id = db.Column(db.Integer, primary_key=True)
+# host_id = db.Column(db.Integer, db.ForeignKey('host.id'), nullable=False)
+# class_id = db.Column(db.Integer, db.ForeignKey('puppet_class.id'), nullable=False)
+
+class Host(db.Model):
+ __tablename__ = 'host'
+ id = db.Column(db.Integer, primary_key=True)
+ fqdn = db.Column(db.Text, nullable=False)
+ environment = db.Column(db.Text)
+ # classes = db.relationship('HostClasses', backref='host', lazy='dynamic')
+ classes = db.relationship('PuppetClass',
+ back_populates='hosts',
+ secondary=host_classes)
+
+class PuppetClass(db.Model):
+ __tablename__ = 'puppet_class'
+ id = db.Column(db.Integer, primary_key=True)
+ class_name = db.Column(db.Text, nullable=False)
+ source_file = db.Column(db.Text)
+ hosts = db.relationship('Host',
+ back_populates='classes',
+ secondary=host_classes)
+
+
+##################################################
+# Everything below should be removed
+
+
+def import_from_puppetdb():
+ payload = {'query': 'nodes {}'}
+ r = requests.post('http://busting.adrift.space:8080/pdb/query/v4',
+ data=json.dumps(payload))
+ for item in r.json():
+ db.session.add(
+ Host(fqdn=item['certname'],
+ environment=item['catalog_environment']))
+ db.session.commit()
+
+def import_more():
+ with open('/usr/local/puppet/nodes.yaml') as f:
+ data = yaml.full_load(f)
+ for fqdn, val in data.items():
+ h = Host.query.where(Host.fqdn==fqdn).first()
+ print(h)
+ if not h: continue
+ # print(h)
+ classes = data[h.fqdn]['classes']
+ if type(classes) == dict:
+ classes = classes.keys()
+ cls = PuppetClass.query.where(PuppetClass.class_name.in_(classes)).all()
+ print(cls)
+ for c in cls:
+ h.classes.append(c)
+ db.session.commit()
diff --git a/pyenc/static/.gitignore b/pyenc/static/.gitignore
new file mode 100644
index 0000000..6a63c9d
--- /dev/null
+++ b/pyenc/static/.gitignore
@@ -0,0 +1 @@
+js/
diff --git a/pyenc/static/css/style.css b/pyenc/static/css/style.css
new file mode 100644
index 0000000..6510ceb
--- /dev/null
+++ b/pyenc/static/css/style.css
@@ -0,0 +1,35 @@
+td {
+ padding-right: 1ex;
+}
+
+h2 {
+ font-family: mono;
+ font-size: 14pt;
+}
+
+dt {
+ font-weight: bold;
+}
+
+dd ul {
+ padding-left: 0;
+}
+
+.hosts {
+ display: flex;
+ flex-wrap: wrap;
+}
+
+.host {
+ display: block;
+ border: 1px solid black;
+ padding: 1em;
+}
+
+.flashes li {
+ display: block;
+ max-width: 100%;
+ padding: 2em;
+ background-color: yellow;
+ border: 2px solid orange;
+}
diff --git a/pyenc/static/js-source/script.js b/pyenc/static/js-source/script.js
new file mode 100644
index 0000000..1f5c6d6
--- /dev/null
+++ b/pyenc/static/js-source/script.js
@@ -0,0 +1,62 @@
+let React = require('react')
+let ReactDOM = require('react-dom')
+
+// npx babel --watch src --out-dir . --presets react-app/prod
+
+class ClassListForm extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ selected: [],
+ classList: [],
+ }
+ }
+
+ onChange = (e) => {
+ fetch('/list-classes?q=' + e.target.value)
+ .then(resp => resp.json())
+ .then((data) => {
+ this.setState({classList: data})
+ })
+ }
+
+ render() {
+ return (<form>
+ <input type="text"
+ placeholder="class name"
+ onInput={this.onChange}/>
+ <PuppetClassList onChange={this.onClassChange} nodes={this.state.selected}/>
+ <hr/>
+ <PuppetClassList onChange={this.onClassChange} nodes={this.state.classList}/>
+ <input type="submit" value="Add Selected"/>
+ </form>)
+ }
+
+ onClassChange = (e) => {
+ if (e.target.checked) {
+ this.setState((state, props) => ({
+ selected: state.selected.concat(e.target.value)
+ }))
+ } else {
+ }
+ }
+}
+
+class PuppetClassList extends React.Component {
+
+ render() {
+ return (<ul>
+ {this.props.nodes.map(node => <li key={node}>
+ <input checked value={node} onChange={this.props.onChange} type="checkbox"/>
+ <label>{node}</label>
+ </li>)}
+ </ul>)
+ }
+}
+
+window.onload = function() {
+ for (let el of document.getElementsByClassName('class-search')) {
+ ReactDOM.render((<ClassListForm/>), el)
+ }
+}
diff --git a/pyenc/templates/base.html b/pyenc/templates/base.html
new file mode 100644
index 0000000..9256734
--- /dev/null
+++ b/pyenc/templates/base.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8"/>
+ <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
+ <meta name="viewport" content="width=device-width, intial-scale=1"/>
+ <link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet"/>
+ <script type="module" src="{{ url_for('static', filename='js/script.js') }}"></script>
+ <title>Puppet Node Classifier</title>
+ </head>
+ <body>
+ <div id="flash">
+ {% with messages = get_flashed_messages() %}
+ {% if messages %}
+ <ul class="flashes">
+ {% for message in messages %}
+ <li>{{ message }}</li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+ {% endwith %}
+ </div>
+ <main>
+ {% block content %}
+ {% endblock %}
+ </main>
+ </body>
+</html>
diff --git a/pyenc/templates/start_page.html b/pyenc/templates/start_page.html
new file mode 100644
index 0000000..dccb1c6
--- /dev/null
+++ b/pyenc/templates/start_page.html
@@ -0,0 +1,33 @@
+{% extends "base.html" %}
+{% block content %}
+ <h1>This certainly is a page</h1>
+ <div class="hosts">
+ {% for host in hosts %}
+ <div class="host">
+ <h2>{{ host.fqdn }}</h2>
+ <form method="POST" action="/remove">
+ <input type="hidden" name="fqdn" value="{{ host.fqdn }}"/>
+ <dl>
+ <dt>Environment</dt>
+ <dd>{{ host.environment }}</dd>
+ <dt>Classes</dt>
+ <dd>
+ <ul>
+ {% for cls in host.classes %}
+ {% with id = 'r' + str(random.getrandbits(64)) %}
+ <li>
+ <input id="{{ id }}" type="checkbox" name="cls" value="{{ cls.class_name }}"/>
+ <label for="{{ id }}">{{ cls.class_name }}</label>
+ </li>
+ {% endwith %}
+ {% endfor %}
+ </ul>
+ </dd>
+ </dl>
+ <input type="submit" value="Remove Selected"/>
+ </form>
+ <div class="class-search"></div>
+ </div>
+ {% endfor %}
+ </div>
+{% endblock %}
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..f807e52
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,21 @@
+certifi==2021.10.8
+charset-normalizer==2.0.12
+click==8.0.4
+Deprecated==1.2.13
+Flask==2.0.3
+Flask-SQLAlchemy==2.5.1
+greenlet==1.1.2
+idna==3.3
+itsdangerous==2.1.0
+Jinja2==3.0.3
+MarkupSafe==2.1.0
+packaging==21.3
+py-redis==1.1.1
+pyparsing==3.0.7
+PyYAML==6.0
+redis==4.1.4
+requests==2.27.1
+SQLAlchemy==1.4.31
+urllib3==1.26.8
+Werkzeug==2.0.3
+wrapt==1.13.3