From bbd692427632bf525f0fa46eeab0de0f8a563661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Sun, 27 Feb 2022 17:31:50 +0100 Subject: Initial code add. --- package.json | 33 +++++++++++++++++ pyenc/__init__.py | 80 ++++++++++++++++++++++++++++++++++++++++ pyenc/db.py | 28 ++++++++++++++ pyenc/enc.py | 25 +++++++++++++ pyenc/model.py | 70 +++++++++++++++++++++++++++++++++++ pyenc/static/.gitignore | 1 + pyenc/static/css/style.css | 35 ++++++++++++++++++ pyenc/static/js-source/script.js | 62 +++++++++++++++++++++++++++++++ pyenc/templates/base.html | 28 ++++++++++++++ pyenc/templates/start_page.html | 33 +++++++++++++++++ requirements.txt | 21 +++++++++++ 11 files changed, 416 insertions(+) create mode 100644 package.json create mode 100644 pyenc/__init__.py create mode 100644 pyenc/db.py create mode 100644 pyenc/enc.py create mode 100644 pyenc/model.py create mode 100644 pyenc/static/.gitignore create mode 100644 pyenc/static/css/style.css create mode 100644 pyenc/static/js-source/script.js create mode 100644 pyenc/templates/base.html create mode 100644 pyenc/templates/start_page.html create mode 100644 requirements.txt 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 (
+ + +
+ + + ) + } + + 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 (
    + {this.props.nodes.map(node =>
  • + + +
  • )} +
) + } +} + +window.onload = function() { + for (let el of document.getElementsByClassName('class-search')) { + ReactDOM.render((), 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 @@ + + + + + + + + + Puppet Node Classifier + + +
+ {% with messages = get_flashed_messages() %} + {% if messages %} +
    + {% for message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+ {% endif %} + {% endwith %} +
+
+ {% block content %} + {% endblock %} +
+ + 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 %} +

This certainly is a page

+
+ {% for host in hosts %} +
+

{{ host.fqdn }}

+
+ +
+
Environment
+
{{ host.environment }}
+
Classes
+
+
    + {% for cls in host.classes %} + {% with id = 'r' + str(random.getrandbits(64)) %} +
  • + + +
  • + {% endwith %} + {% endfor %} +
+
+
+ +
+ +
+ {% endfor %} +
+{% 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 -- cgit v1.2.3