From 52a5789c856ac3e9e17833ae3e6bc10b0cb1472b Mon Sep 17 00:00:00 2001 From: Vadim Aleksandrov Date: Mon, 22 Jan 2018 18:22:19 +0300 Subject: [PATCH 1/4] Add first working draft of domain templating functionality --- app/models.py | 84 ++++++- app/templates/base.html | 1 + app/templates/domain_add.html | 9 + app/templates/template.html | 119 +++++++++ app/templates/template_add.html | 104 ++++++++ app/templates/template_edit.html | 415 +++++++++++++++++++++++++++++++ app/views.py | 137 +++++++++- create_db.py | 26 +- 8 files changed, 889 insertions(+), 6 deletions(-) create mode 100644 app/templates/template.html create mode 100644 app/templates/template_add.html create mode 100644 app/templates/template_edit.html diff --git a/app/models.py b/app/models.py index 9aee967..9bf2b58 100644 --- a/app/models.py +++ b/app/models.py @@ -1039,7 +1039,9 @@ class Record(object): }) postdata_for_new = {"rrsets": final_records} - + logging.info(postdata_for_new) + logging.info(postdata_for_delete) + logging.info(urlparse.urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/%s' % domain)) try: headers = {} headers['X-API-Key'] = PDNS_API_KEY @@ -1358,3 +1360,83 @@ class Setting(db.Model): logging.debug(traceback.format_exec()) db.session.rollback() return False + + +class DomainTemplate(db.Model): + __tablename__ = "domain_template" + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(255), index=True, unique=True) + description = db.Column(db.String(255)) + records = db.relationship('DomainTemplateRecord', back_populates='template', cascade="all, delete-orphan") + + def __repr__(self): + return '' % self.name + + def __init__(self, name=None, description=None): + self.id = None + self.name = name + self.description = description + + def replace_records(self, records): + try: + self.records = [] + for record in records: + self.records.append(record) + db.session.commit() + return {'status': 'ok', 'msg': 'Template records have been modified'} + except Exception, e: + logging.error('Cannot create template records' + str(e)) + db.session.rollback() + return {'status': 'error', 'msg': 'Can not create template records'} + + def create(self): + try: + db.session.add(self) + db.session.commit() + return {'status': 'ok', 'msg': 'Template has been created'} + except Exception, e: + logging.error('Can not update domain template table.' + str(e)) + db.session.rollback() + return {'status': 'error', 'msg': 'Can not update domain template table'} + + def delete_template(self): + try: + self.records = [] + db.session.delete(self) + db.session.commit() + return {'status': 'ok', 'msg': 'Template has been deleted'} + except Exception, e: + logging.error('Can not delete domain template.' + str(e)) + db.session.rollback() + return {'status': 'error', 'msg': 'Can not delete domain template'} + + +class DomainTemplateRecord(db.Model): + __tablename__ = "domain_template_record" + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(255)) + type = db.Column(db.String(64)) + ttl = db.Column(db.Integer) + data = db.Column(db.String(255)) + status = db.Column(db.Boolean) + template_id = db.Column(db.Integer, db.ForeignKey('domain_template.id')) + template = db.relationship('DomainTemplate', back_populates='records') + + def __repr__(self): + return '' % self.id + + def __init__(self, id=None, name=None, type=None, ttl=None, data=None, status=None): + self.id = id + self.name = name + self.type = type + self.ttl = ttl + self.data = data + self.status = status + + def apply(self): + try: + db.session.commit() + except Exception, e: + logging.error('Can not update domain template table.' + str(e)) + db.session.rollback() + return {'status': 'error', 'msg': 'Can not update domain template table'} diff --git a/app/templates/base.html b/app/templates/base.html index 06f239b..81eec24 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -127,6 +127,7 @@
  • New Domain
  • ADMINISTRATION
  • Admin Console
  • +
  • Domain Templates
  • Users
  • History
  • Settings
  • diff --git a/app/templates/domain_add.html b/app/templates/domain_add.html index 04add24..8da216a 100644 --- a/app/templates/domain_add.html +++ b/app/templates/domain_add.html @@ -47,6 +47,15 @@ +
    + + +
    diff --git a/app/templates/template.html b/app/templates/template.html new file mode 100644 index 0000000..ac58c82 --- /dev/null +++ b/app/templates/template.html @@ -0,0 +1,119 @@ +{% extends "base.html" %} +{% block title %}DNS Control Panel - Templates{% endblock %} + +{% block dashboard_stat %} + +
    +

    + Templates + List +

    + +
    +{% endblock %} +{% block content %} + +
    + {% with errors = get_flashed_messages(category_filter=["error"]) %} {% if errors %} +
    +
    +
    + +

    + Error! +

    +
    + x +
      + {%- for msg in errors %} +
    • {{ msg }}
    • {% endfor -%} +
    +
    +
    +
    +
    +{% endif %} {% endwith %} +
    +
    +
    +
    +

    Templates

    +
    + +
    + + + + + + + + + + + + {% for template in templates %} + + + + + + + + {% endfor %} + +
    NameDescriptionNumber of RecordsEditDelete
    + {{ template.name }} + + {{ template.description }} + + {{ template.records|count }} + + + + + + + + +
    +
    + +
    + +
    + +
    + +
    + +{% endblock %} +{% block extrascripts %} + +{% endblock %} +{% block modals %} +{% endblock %} diff --git a/app/templates/template_add.html b/app/templates/template_add.html new file mode 100644 index 0000000..772c204 --- /dev/null +++ b/app/templates/template_add.html @@ -0,0 +1,104 @@ +{% extends "base.html" %} +{% block title %}DNS Control Panel - Create Template{% endblock %} + +{% block dashboard_stat %} + +
    +

    + Template + Create +

    + +
    +{% endblock %} + +{% block content %} +
    +{% with errors = get_flashed_messages(category_filter=["error"]) %} {% +if errors %} +
    +
    +
    + +

    + Error! +

    +
    + x +
      + {%- for msg in errors %} +
    • {{ msg }}
    • {% endfor -%} +
    +
    +
    +
    +
    +{% endif %} {% endwith %} + +
    +
    +
    +
    +

    Create new template

    +
    + + +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    +

    Help with creating a new template

    +
    +
    +
    +
    Template name
    +
    Enter your template name, this is the name of the template that + will be shown to users. The name should not have any spaces but + can have symbols.
    +
    Template description
    +
    Enter your template description, this is to help better + identify the template.
    +
    +
    +
    +
    +
    +
    +{% endblock %} +{% block extrascripts %} + +{% endblock %} diff --git a/app/templates/template_edit.html b/app/templates/template_edit.html new file mode 100644 index 0000000..64f9799 --- /dev/null +++ b/app/templates/template_edit.html @@ -0,0 +1,415 @@ +{% extends "base.html" %} +{% block title %}DNS Control Panel - Edit Template{% endblock %} + +{% block dashboard_stat %} +
    +

    + Edit template {{ template }} +

    + +
    +{% endblock %} + +{% block content %} +
    +
    +
    +
    +
    +

    Manage Template Records for {{ template }}

    +
    +
    + + +
    +
    + + + + + + + + + + + + + + + {% for record in records %} + + + + + + + + + + + + {% endfor %} + +
    NameTypeStatusTTLDataEditDeleteID
    + {{ record.name }} + + {{ record.type }} + + {{ record.status }} + + {{ record.ttl }} + + {{ record.data }} + + + + + + {{ record.id }} +
    +
    +
    +
    +
    +
    +{% endblock %} +{% block extrascripts %} + +{% endblock %} +{% block modals %} + + + +{% endblock %} diff --git a/app/views.py b/app/views.py index 8cc8761..8cab755 100644 --- a/app/views.py +++ b/app/views.py @@ -11,12 +11,12 @@ from io import BytesIO import jinja2 import qrcode as qrc import qrcode.image.svg as qrc_svg -from flask import g, request, make_response, jsonify, render_template, session, redirect, url_for, send_from_directory, abort +from flask import g, request, make_response, jsonify, render_template, session, redirect, url_for, send_from_directory, abort, flash from flask_login import login_user, logout_user, current_user, login_required from werkzeug import secure_filename from werkzeug.security import gen_salt -from .models import User, Domain, Record, Server, History, Anonymous, Setting, DomainSetting +from .models import User, Domain, Record, Server, History, Anonymous, Setting, DomainSetting, DomainTemplate, DomainTemplateRecord from app import app, login_manager, github from lib import utils @@ -331,10 +331,12 @@ def domain(domain_name): @login_required @admin_role_required def domain_add(): + templates = DomainTemplate.query.all() if request.method == 'POST': try: domain_name = request.form.getlist('domain_name')[0] domain_type = request.form.getlist('radio_type')[0] + domain_template = request.form.getlist('domain_template')[0] soa_edit_api = request.form.getlist('radio_type_soa_edit_api')[0] if ' ' in domain_name or not domain_name or not domain_type: @@ -352,12 +354,27 @@ def domain_add(): if result['status'] == 'ok': history = History(msg='Add domain %s' % domain_name, detail=str({'domain_type': domain_type, 'domain_master_ips': domain_master_ips}), created_by=current_user.username) history.add() + if domain_template != '0': + template = DomainTemplate.query.filter(DomainTemplate.id == domain_template).first() + template_records = DomainTemplateRecord.query.filter(DomainTemplateRecord.template_id == domain_template).all() + record_data = [] + for template_record in template_records: + record_row = {'record_data': template_record.data, 'record_name': template_record.name, 'record_status': template_record.status, 'record_ttl': template_record.ttl, 'record_type': template_record.type} + record_data.append(record_row) + r = Record() + result = r.apply(domain_name, record_data) + if result['status'] == 'ok': + history = History(msg='Applying template %s to %s, created records successfully.' % (template.name, domain_name), detail=str(result), created_by=current_user.username) + history.add() + else: + history = History(msg='Applying template %s to %s, FAILED to created records.' % (template.name, domain_name), detail=str(result), created_by=current_user.username) + history.add() return redirect(url_for('dashboard')) else: return render_template('errors/400.html', msg=result['msg']), 400 except: return redirect(url_for('error', code=500)) - return render_template('domain_add.html') + return render_template('domain_add.html', templates=templates) @app.route('/admin/domain//delete', methods=['GET']) @@ -518,6 +535,120 @@ def admin_setdomainsetting(domain_name): return make_response(jsonify( { 'status': 'error', 'msg': 'There is something wrong, please contact Administrator.' } ), 400) +@app.route('/templates', methods=['GET', 'POST']) +@app.route('/templates/list', methods=['GET', 'POST']) +@login_required +@admin_role_required +def templates(): + templates = DomainTemplate.query.all() + return render_template('template.html', templates=templates) + + +@app.route('/template/create', methods=['GET', 'POST']) +@login_required +@admin_role_required +def create_template(): + if request.method == 'GET': + return render_template('template_add.html') + if request.method == 'POST': + try: + name = request.form.getlist('name')[0] + description = request.form.getlist('description')[0] + + if ' ' in name or not name or not type: + flash("Please correct your input", 'error') + return redirect(url_for('create_template')) + + if DomainTemplate.query.filter(DomainTemplate.name == name).first(): + flash("A template with the name %s already exists!" % name, 'error') + return redirect(url_for('create_template')) + t = DomainTemplate(name=name, description=description) + result = t.create() + if result['status'] == 'ok': + history = History(msg='Add domain template %s' % name, detail=str({'name': name, 'description': description}), created_by=current_user.username) + history.add() + return redirect(url_for('templates')) + else: + flash(result['msg'], 'error') + return redirect(url_for('create_template')) + except Exception: + print traceback.format_exc() + return redirect(url_for('error', code=500)) + return redirect(url_for('templates')) + + +@app.route('/template//edit', methods=['GET']) +@login_required +@admin_role_required +def edit_template(template): + try: + t = DomainTemplate.query.filter(DomainTemplate.name == template).first() + if t is not None: + records = [] + for jr in t.records: + if jr.type in app.config['RECORDS_ALLOW_EDIT']: + record = DomainTemplateRecord(name=jr.name, type=jr.type, status='Disabled' if jr.status else 'Active', ttl=jr.ttl, data=jr.data) + records.append(record) + + return render_template('template_edit.html', template=t.name, records=records, editable_records=app.config['RECORDS_ALLOW_EDIT']) + except Exception: + print traceback.format_exc() + return redirect(url_for('error', code=500)) + return redirect(url_for('templates')) + + +@app.route('/template//apply', methods=['POST'], strict_slashes=False) +@login_required +def apply_records(template): + try: + pdata = request.data + jdata = json.loads(pdata) + records = [] + + for j in jdata: + name = '@' if j['record_name'] in ['@', ''] else j['record_name'] + type = j['record_type'] + data = j['record_data'] + disabled = True if j['record_status'] == 'Disabled' else False + ttl = int(j['record_ttl']) if j['record_ttl'] else 3600 + + dtr = DomainTemplateRecord(name=name, type=type, data=data, status=disabled, ttl=ttl) + records.append(dtr) + + t = DomainTemplate.query.filter(DomainTemplate.name == template).first() + result = t.replace_records(records) + if result['status'] == 'ok': + history = History(msg='Apply domain template record changes to domain template %s' % template, detail=str(jdata), created_by=current_user.username) + history.add() + return make_response(jsonify(result), 200) + else: + return make_response(jsonify(result), 400) + except: + print traceback.format_exc() + return make_response(jsonify({'status': 'error', 'msg': 'Error when applying new changes'}), 500) + + +@app.route('/template//delete', methods=['GET']) +@login_required +@admin_role_required +def delete_template(template): + try: + t = DomainTemplate.query.filter(DomainTemplate.name == template).first() + if t is not None: + result = t.delete_template() + if result['status'] == 'ok': + history = History(msg='Deleted domain template %s' % template, detail=str({'name': template}), created_by=current_user.username) + history.add() + return redirect(url_for('templates')) + else: + flash(result['msg'], 'error') + return redirect(url_for('templates')) + except Exception: + print traceback.format_exc() + return redirect(url_for('error', code=500)) + return redirect(url_for('templates')) + + @app.route('/admin', methods=['GET', 'POST']) @login_required @admin_role_required diff --git a/create_db.py b/create_db.py index 41c874d..25899fd 100755 --- a/create_db.py +++ b/create_db.py @@ -4,7 +4,7 @@ from migrate.versioning import api from config import SQLALCHEMY_DATABASE_URI from config import SQLALCHEMY_MIGRATE_REPO from app import db -from app.models import Role, Setting +from app.models import Role, Setting, DomainTemplate import os.path import time import sys @@ -65,6 +65,23 @@ def init_settings(db, setting_names): for setting in settings: db.session.add(setting) + +def init_domain_templates(db, domain_template_names): + + # Get key name of data + name_of_domain_templates = map(lambda r: r.name, domain_template_names) + + # Query to get current data + rows = db.session.query(DomainTemplate).filter(DomainTemplate.name.in_(name_of_domain_templates)).all() + + # Check which data that need to insert + name_of_rows = map(lambda r: r.name, rows) + domain_templates = filter(lambda r: r.name not in name_of_rows, domain_template_names) + + # Insert data + for domain_template in domain_templates: + db.session.add(domain_template) + def init_records(): # Create initial user roles and turn off maintenance mode init_roles(db, [ @@ -80,7 +97,12 @@ def init_records(): Setting('default_domain_table_size', '10'), Setting('auto_ptr','False') ]) - + # TODO: add sample records to sample templates + init_domain_templates(db, [ + DomainTemplate('basic_template_1', 'Basic Template #1'), + DomainTemplate('basic_template_2', 'Basic Template #2'), + DomainTemplate('basic_template_3', 'Basic Template #3') + ]) db_commit = db.session.commit() commit_version_control(db_commit) From 12cfc4dbc10393f9db3ccfb60df197d001f2306b Mon Sep 17 00:00:00 2001 From: Vadim Aleksandrov Date: Tue, 23 Jan 2018 18:23:21 +0300 Subject: [PATCH 2/4] Added the ability to create a template based on the zone records --- app/templates/dashboard.html | 53 ++++++++++++++++++++++++++++ app/templates/template.html | 2 +- app/templates/template_edit.html | 2 +- app/views.py | 60 ++++++++++++++++++++++++++++++++ 4 files changed, 115 insertions(+), 2 deletions(-) diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html index dee30fc..2db64aa 100644 --- a/app/templates/dashboard.html +++ b/app/templates/dashboard.html @@ -181,6 +181,11 @@ {% else %} + {% if current_user.role.name == 'Administrator' %} + + {% endif %} @@ -242,6 +247,31 @@ var domain = $(this).prop('id'); getdnssec($SCRIPT_ROOT + '/domain/' + domain + '/dnssec'); }); + + $(document.body).on("click", ".button_template", function (e) { + var modal = $("#modal_template"); + var domain = $(this).prop('id'); + var form = " \ + \ + \ + \ + \ + "; + modal.find('.modal-body p').html(form); + modal.find('#button_save').click(function() { + var data = {}; + data['name'] = modal.find('#template_name').val(); + data['description'] = modal.find('#template_description').val(); + data['domain'] = modal.find('#domain').val(); + applyChanges(data, $SCRIPT_ROOT + "{{ url_for('create_template_from_zone') }}", true); + modal.modal('hide'); + }) + modal.find('#button_close').click(function() { + modal.modal('hide'); + }) + + modal.modal('show'); + }); {% endblock %} {% block modals %} @@ -291,4 +321,27 @@ + {% endblock %} diff --git a/app/templates/template.html b/app/templates/template.html index ac58c82..baaada0 100644 --- a/app/templates/template.html +++ b/app/templates/template.html @@ -65,7 +65,7 @@ {% for template in templates %} - {{ template.name }} + {{ template.name }} {{ template.description }} diff --git a/app/templates/template_edit.html b/app/templates/template_edit.html index 64f9799..3445eb0 100644 --- a/app/templates/template_edit.html +++ b/app/templates/template_edit.html @@ -265,7 +265,6 @@ \ \ "; - modal.modal('show'); } else { var parts = record_data.val().split(" "); var form = " \ @@ -288,6 +287,7 @@ record_data.val(data); modal.modal('hide'); }) + modal.modal('show'); } else if (record_type == "SOA") { var modal = $("#modal_custom_record"); if (record_data.val() == "") { diff --git a/app/views.py b/app/views.py index 8cab755..8f4cc0a 100644 --- a/app/views.py +++ b/app/views.py @@ -577,6 +577,66 @@ def create_template(): return redirect(url_for('templates')) +@app.route('/template/createfromzone', methods=['POST']) +@login_required +@admin_role_required +def create_template_from_zone(): + try: + pdata = request.data + jdata = json.loads(pdata) + name = jdata['name'] + description = jdata['description'] + domain_name = jdata['domain'] + + if ' ' in name or not name or not type: + return make_response(jsonify({'status': 'error', 'msg': 'Please correct template name'}), 500) + + if DomainTemplate.query.filter(DomainTemplate.name == name).first(): + return make_response(jsonify({'status': 'error', 'msg': 'A template with the name %s already exists!' % name}), 500) + + t = DomainTemplate(name=name, description=description) + result = t.create() + if result['status'] == 'ok': + history = History(msg='Add domain template %s' % name, detail=str({'name': name, 'description': description}), created_by=current_user.username) + history.add() + + records = [] + r = Record() + domain = Domain.query.filter(Domain.name == domain_name).first() + if domain: + # query domain info from PowerDNS API + zone_info = r.get_record_data(domain.name) + if zone_info: + jrecords = zone_info['records'] + + if NEW_SCHEMA: + for jr in jrecords: + name = '@' if jr['name'] == domain_name else jr['name'] + if jr['type'] in app.config['RECORDS_ALLOW_EDIT']: + for subrecord in jr['records']: + + record = DomainTemplateRecord(name=name, type=jr['type'], status=True if subrecord['disabled'] else False, ttl=jr['ttl'], data=subrecord['content']) + records.append(record) + else: + for jr in jrecords: + if jr['type'] in app.config['RECORDS_ALLOW_EDIT']: + record = DomainTemplateRecord(name=name, type=jr['type'], status=True if jr['disabled'] else False, ttl=jr['ttl'], data=jr['content']) + records.append(record) + result_records = t.replace_records(records) + + if result_records['status'] == 'ok': + return make_response(jsonify({'status': 'ok', 'msg': result['msg']}), 200) + else: + result = t.delete_template() + return make_response(jsonify({'status': 'error', 'msg': result_records['msg']}), 500) + + else: + return make_response(jsonify({'status': 'error', 'msg': result['msg']}), 500) + except Exception: + print traceback.format_exc() + return make_response(jsonify({'status': 'error', 'msg': 'Error when applying new changes'}), 500) + + @app.route('/template//edit', methods=['GET']) @login_required @admin_role_required From 0355fe429364cbe47bdb56f9bc89852d846155f1 Mon Sep 17 00:00:00 2001 From: Vadim Aleksandrov Date: Wed, 7 Feb 2018 16:09:06 +0300 Subject: [PATCH 3/4] Join "Edit" and "Delete" button into th on templates page --- app/templates/template.html | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/templates/template.html b/app/templates/template.html index baaada0..19c8efc 100644 --- a/app/templates/template.html +++ b/app/templates/template.html @@ -57,8 +57,7 @@ Name Description Number of Records - Edit - Delete + Action @@ -79,8 +78,6 @@ Edit  - - {% else %} {% endif %} - {% if record.is_allowed() %} + {% if record.is_allowed_delete() %} - {% else %} - {% endif %} {% else %} - + - - + + {% endif %} diff --git a/app/views.py b/app/views.py index 8f4cc0a..b59cfee 100644 --- a/app/views.py +++ b/app/views.py @@ -966,7 +966,7 @@ def dyndns_update(): r = Record() r.name = hostname # check if the user requested record exists within this domain - if r.exists(domain.name) and r.is_allowed: + if r.exists(domain.name) and r.is_allowed_edit(): if r.data == myip: # record content did not change, return 'nochg' history = History(msg="DynDNS update: attempted update of %s but record did not change" % hostname, created_by=current_user.username) @@ -981,7 +981,7 @@ def dyndns_update(): return render_template('dyndns.html', response='good'), 200 else: return render_template('dyndns.html', response='911'), 200 - elif r.is_allowed: + elif r.is_allowed_edit(): ondemand_creation = DomainSetting.query.filter(DomainSetting.domain == domain).filter(DomainSetting.setting == 'create_via_dyndns').first() if (ondemand_creation != None) and (strtobool(ondemand_creation.value) == True): record = Record(name=hostname,type='A',data=myip,status=False,ttl=3600)