From 7327009c1d0807895d4e61f9e7125bf6e2a3e2d8 Mon Sep 17 00:00:00 2001 From: Benjamin Bertrand <benjamin.bertrand@esss.se> Date: Tue, 19 Dec 2017 16:16:52 +0100 Subject: [PATCH] Add UI to view/edit host and add/edit interface --- app/models.py | 1 + app/network/forms.py | 14 ++- app/network/views.py | 112 +++++++++++++++++++- app/static/js/hosts.js | 26 ++++- app/templates/base-fluid.html | 2 +- app/templates/network/create_interface.html | 34 ++++++ app/templates/network/edit_host.html | 31 ++++++ app/templates/network/edit_interface.html | 37 +++++++ app/templates/network/view_host.html | 68 ++++++++++++ 9 files changed, 315 insertions(+), 10 deletions(-) create mode 100644 app/templates/network/create_interface.html create mode 100644 app/templates/network/edit_host.html create mode 100644 app/templates/network/edit_interface.html create mode 100644 app/templates/network/view_host.html diff --git a/app/models.py b/app/models.py index 2a64759..c32d22e 100644 --- a/app/models.py +++ b/app/models.py @@ -247,6 +247,7 @@ class Item(db.Model): status = db.relationship('Status', back_populates='items') children = db.relationship('Item', backref=db.backref('parent', remote_side=[id])) macs = db.relationship('Mac', backref='item') + host = db.relationship('Host', uselist=False, backref='item') comments = db.relationship('ItemComment', backref='item') def __init__(self, **kwargs): diff --git a/app/network/forms.py b/app/network/forms.py index c78c52d..c141eb3 100644 --- a/app/network/forms.py +++ b/app/network/forms.py @@ -70,6 +70,14 @@ class HostForm(CSEntryForm): type = SelectField('Type', choices=utils.get_choices(('Virtual', 'Physical'))) description = TextAreaField('Description') item_id = SelectField('Item', coerce=utils.coerce_to_str_or_none) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.item_id.choices = utils.get_model_choices(models.Item, allow_none=True, attr='ics_id') + + +class InterfaceForm(CSEntryForm): + host_id = SelectField('Host') network_id = SelectField('Network') # The list of IPs is dynamically created on the browser side # depending on the selected network @@ -87,7 +95,7 @@ class HostForm(CSEntryForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.item_id.choices = utils.get_model_choices(models.Item, allow_none=True, attr='ics_id') + self.host_id.choices = utils.get_model_choices(models.Host) if current_user.is_admin: network_query = models.Network.query else: @@ -96,3 +104,7 @@ class HostForm(CSEntryForm): attr='vlan_name', query=network_query) self.mac_id.choices = utils.get_model_choices(models.Mac, allow_none=True, attr='address') self.tags.choices = utils.get_model_choices(models.Tag, allow_none=True, attr='name') + + +class HostInterfaceForm(HostForm, InterfaceForm): + pass diff --git a/app/network/views.py b/app/network/views.py index fdd817e..3ffb830 100644 --- a/app/network/views.py +++ b/app/network/views.py @@ -14,10 +14,10 @@ import sqlalchemy as sa from flask import (Blueprint, render_template, jsonify, session, redirect, url_for, request, flash, current_app) from flask_login import login_required -from .forms import HostForm, NetworkForm +from .forms import HostForm, InterfaceForm, HostInterfaceForm, NetworkForm from ..extensions import db from ..decorators import login_groups_accepted -from .. import models +from .. import models, utils bp = Blueprint('network', __name__) @@ -37,9 +37,12 @@ def create_host(): network_id = session['network_id'] except KeyError: # No need to pass request.form when no extra keywords are given - form = HostForm() + form = HostInterfaceForm() else: - form = HostForm(request.form, network_id=network_id) + form = HostInterfaceForm(request.form, network_id=network_id) + # Remove the host_id field inherited from the InterfaceForm + # It's not used in this form + del form.host_id if form.validate_on_submit(): network_id = form.network_id.data host = models.Host(name=form.name.data, @@ -74,6 +77,107 @@ def create_host(): return render_template('network/create_host.html', form=form) +@bp.route('/hosts/view/<name>') +@login_required +def view_host(name): + host = models.Host.query.filter_by(name=name).first_or_404() + return render_template('network/view_host.html', host=host) + + +@bp.route('/hosts/edit/<name>', methods=('GET', 'POST')) +@login_groups_accepted('admin', 'create') +def edit_host(name): + host = models.Host.query.filter_by(name=name).first_or_404() + form = HostForm(request.form, obj=host) + if form.validate_on_submit(): + host.name = form.name.data + host.type = form.type.data + host.item_id = form.item_id.data + host.description = form.description.data or None + current_app.logger.debug(f'Trying to update: {host!r}') + try: + db.session.commit() + except sa.exc.IntegrityError as e: + db.session.rollback() + current_app.logger.warning(f'{e}') + flash(f'{e}', 'error') + else: + flash(f'Host {host} updated!', 'success') + return redirect(url_for('network.view_host', name=host.name)) + return render_template('network/edit_host.html', form=form) + + +@bp.route('/interfaces/create/<hostname>', methods=('GET', 'POST')) +@login_groups_accepted('admin', 'create') +def create_interface(hostname): + host = models.Host.query.filter_by(name=hostname).first_or_404() + form = InterfaceForm(request.form, host_id=host.id, interface_name=host.name) + if form.validate_on_submit(): + # The total number of tags will always be quite small + # It's more efficient to retrieve all of them in one query + # and do the filtering here + all_tags = models.Tag.query.all() + tags = [tag for tag in all_tags if str(tag.id) in form.tags.data] + interface = models.Interface(host_id=host.id, + name=form.interface_name.data, + ip=form.ip.data, + network_id=form.network_id.data, + mac_id=form.mac_id.data, + tags=tags) + current_app.logger.debug(f'Trying to create: {interface!r}') + db.session.add(interface) + try: + db.session.commit() + except sa.exc.IntegrityError as e: + db.session.rollback() + current_app.logger.warning(f'{e}') + flash(f'{e}', 'error') + else: + flash(f'Host {interface} created!', 'success') + return redirect(url_for('network.create_interface', hostname=hostname)) + return render_template('network/create_interface.html', form=form, hostname=hostname) + + +@bp.route('/interfaces/edit/<name>', methods=('GET', 'POST')) +@login_groups_accepted('admin', 'create') +def edit_interface(name): + interface = models.Interface.query.filter_by(name=name).first_or_404() + form = InterfaceForm(request.form, obj=interface, interface_name=interface.name) + ips = [interface.ip] + ips.extend([str(address) for address in interface.network.available_ips()]) + form.ip.choices = utils.get_choices(ips) + if form.validate_on_submit(): + interface.name = form.interface_name.data + interface.ip = form.ip.data + interface.network_id = form.network_id.data + interface.mac_id = form.mac_id.data + all_tags = models.Tag.query.all() + tags = [tag for tag in all_tags if str(tag.id) in form.tags.data] + interface.tags = tags + current_app.logger.debug(f'Trying to update: {interface!r}') + try: + db.session.commit() + except sa.exc.IntegrityError as e: + db.session.rollback() + current_app.logger.warning(f'{e}') + flash(f'{e}', 'error') + else: + flash(f'Interface {interface} updated!', 'success') + return redirect(url_for('network.view_host', name=interface.host.name)) + return render_template('network/edit_interface.html', form=form, hostname=interface.host.name) + + +@bp.route('/interfaces/delete', methods=['POST']) +@login_required +def delete_interface(): + interface = models.Interface.query.get_or_404(request.form['interface_id']) + hostname = interface.host.name + db.session.delete(interface) + db.session.commit() + flash(f'Interface {interface.name} has been deleted', 'success') + return redirect(url_for('network.view_host', name=hostname)) + + @bp.route('/_retrieve_hosts') @login_required def retrieve_hosts(): diff --git a/app/static/js/hosts.js b/app/static/js/hosts.js index 1331099..c9bf938 100644 --- a/app/static/js/hosts.js +++ b/app/static/js/hosts.js @@ -16,9 +16,12 @@ $(document).ready(function() { ); } - // Populate IP select field on first page load - // Populate vlan_id and prefix select field on first page load - if( $("#network_id").length ) { + // Populate IP select field on first page load for: + // - register new host + // - add interface + // Do NOT replace the IPs on edit interface page load! + // (we have to keep the existing IP) + if( $("#hostForm").length || $("#interfaceForm").length ) { update_available_ips(); } @@ -53,7 +56,22 @@ $(document).ready(function() { callback(json); }); }, - "paging": false + "pagingType": "full_numbers", + "pageLength": 20, + "lengthMenu": [[20, 50, 100, -1], [20, 50, 100, "All"]], + "columnDefs": [ + { + "targets": [0], + "render": function(data, type, row) { + // render funtion to create link to Item view page + if ( data === null ) { + return data; + } + var url = $SCRIPT_ROOT + "/network/hosts/view/" + data; + return '<a href="'+ url + '">' + data + '</a>'; + } + } + ] }); }); diff --git a/app/templates/base-fluid.html b/app/templates/base-fluid.html index 0835929..3aff063 100644 --- a/app/templates/base-fluid.html +++ b/app/templates/base-fluid.html @@ -17,7 +17,7 @@ <a class="list-group-item list-group-item-action {{ is_active(path.startswith("/inventory/qrcodes")) }}" href="{{ url_for('inventory.qrcodes', kind='Action') }}">QR Codes</a> {% elif path.startswith("/network") %} - <a class="list-group-item list-group-item-action {{ is_active(path.startswith("/network/hosts")) }}" + <a class="list-group-item list-group-item-action {{ is_active(path.startswith(("/network/hosts", "/network/interfaces"))) }}" href="{{ url_for('network.list_hosts') }}">Hosts</a> <a class="list-group-item list-group-item-action {{ is_active(path.startswith("/network/networks")) }}" href="{{ url_for('network.list_networks') }}">Networks</a> diff --git a/app/templates/network/create_interface.html b/app/templates/network/create_interface.html new file mode 100644 index 0000000..5fadfb9 --- /dev/null +++ b/app/templates/network/create_interface.html @@ -0,0 +1,34 @@ +{% extends "network/hosts.html" %} +{% from "_helpers.html" import render_field %} + +{% block title %}Register Interface - CSEntry{% endblock %} + +{% block hosts_nav %} + <li class="nav-item"> + <a class="nav-link" href="{{ url_for('network.view_host', name=hostname) }}">View host</a> + </li> + <li class="nav-item"> + <a class="nav-link" href="{{ url_for('network.edit_host', name=hostname) }}">Edit host</a> + </li> + <li class="nav-item"> + <a class="nav-link active" href="{{ url_for('network.create_interface', hostname=hostname) }}">Add interface</a> + </li> +{% endblock %} + + +{% block hosts_main %} + <form id="interfaceForm" method="POST"> + {{ form.hidden_tag() }} + {{ render_field(form.host_id, disabled=True) }} + {{ render_field(form.interface_name, class_="text-lowercase") }} + {{ render_field(form.network_id) }} + {{ render_field(form.ip) }} + {{ render_field(form.mac_id) }} + {{ render_field(form.tags) }} + <div class="form-group row"> + <div class="col-sm-10"> + <button type="submit" class="btn btn-primary">Submit</button> + </div> + </div> + </form> +{%- endblock %} diff --git a/app/templates/network/edit_host.html b/app/templates/network/edit_host.html new file mode 100644 index 0000000..fedffe3 --- /dev/null +++ b/app/templates/network/edit_host.html @@ -0,0 +1,31 @@ +{% extends "network/hosts.html" %} +{% from "_helpers.html" import render_field %} + +{% block title %}Edit Host - CSEntry{% endblock %} + +{% block hosts_nav %} + <li class="nav-item"> + <a class="nav-link" href="{{ url_for('network.view_host', name=form.name.data) }}">View host</a> + </li> + <li class="nav-item"> + <a class="nav-link active" href="{{ url_for('network.edit_host', name=form.name.data) }}">Edit host</a> + </li> + <li class="nav-item"> + <a class="nav-link" href="{{ url_for('network.create_interface', hostname=form.name.data) }}">Add interface</a> + </li> +{% endblock %} + +{% block hosts_main %} + <form id="editHostForm" method="POST"> + {{ form.hidden_tag() }} + {{ render_field(form.name, class_="text-lowercase") }} + {{ render_field(form.type) }} + {{ render_field(form.description) }} + {{ render_field(form.item_id, disabled=True) }} + <div class="form-group row"> + <div class="col-sm-10"> + <button type="submit" class="btn btn-primary">Submit</button> + </div> + </div> + </form> +{%- endblock %} diff --git a/app/templates/network/edit_interface.html b/app/templates/network/edit_interface.html new file mode 100644 index 0000000..a7cfe3e --- /dev/null +++ b/app/templates/network/edit_interface.html @@ -0,0 +1,37 @@ +{% extends "network/hosts.html" %} +{% from "_helpers.html" import render_field %} + +{% block title %}Edit Interface - CSEntry{% endblock %} + +{% block hosts_nav %} + <li class="nav-item"> + <a class="nav-link" href="{{ url_for('network.view_host', name=hostname) }}">View host</a> + </li> + <li class="nav-item"> + <a class="nav-link" href="{{ url_for('network.edit_host', name=hostname) }}">Edit host</a> + </li> + <li class="nav-item"> + <a class="nav-link" href="{{ url_for('network.create_interface', hostname=hostname) }}">Add interface</a> + </li> + <li class="nav-item"> + <a class="nav-link active" href="{{ url_for('network.edit_interface', name=hostname) }}">Edit interface</a> + </li> +{% endblock %} + + +{% block hosts_main %} + <form id="editInterfaceForm" method="POST"> + {{ form.hidden_tag() }} + {{ render_field(form.host_id, disabled=True) }} + {{ render_field(form.interface_name, class_="text-lowercase") }} + {{ render_field(form.network_id) }} + {{ render_field(form.ip) }} + {{ render_field(form.mac_id) }} + {{ render_field(form.tags) }} + <div class="form-group row"> + <div class="col-sm-10"> + <button type="submit" class="btn btn-primary">Submit</button> + </div> + </div> + </form> +{%- endblock %} diff --git a/app/templates/network/view_host.html b/app/templates/network/view_host.html new file mode 100644 index 0000000..38efba3 --- /dev/null +++ b/app/templates/network/view_host.html @@ -0,0 +1,68 @@ +{% extends "network/hosts.html" %} +{% from "_helpers.html" import link_to_item %} + +{% block title %}View Host - CSEntry{% endblock %} + +{% block hosts_nav %} + <li class="nav-item"> + <a class="nav-link active" href="{{ url_for('network.view_host', name=host.name) }}">View host</a> + </li> + <li class="nav-item"> + <a class="nav-link" href="{{ url_for('network.edit_host', name=host.name) }}">Edit host</a> + </li> + <li class="nav-item"> + <a class="nav-link" href="{{ url_for('network.create_interface', hostname=host.name) }}">Add interface</a> + </li> +{% endblock %} + +{% block hosts_main %} + <dl class="row"> + <dt class="col-sm-3">Hostname</dt> + <dd class="col-sm-9">{{ host.name }}</dd> + <dt class="col-sm-3">Type</dt> + <dd class="col-sm-9">{{ host.type }}</dd> + {% if host.type == 'Physical' %} + <dt class="col-sm-3">Item</dt> + <dd class="col-sm-9">{{ link_to_item(host.item) }}</dd> + {% endif %} + <dt class="col-sm-3">Description</dt> + <dd class="col-sm-9">{{ host.description }}</dd> + </dl> + <h3>Interfaces</h3> + <table id="interfaces_table" class="table table-hover table-sm"> + <thead> + <tr> + <th width="5%"></th> + <th>Name</th> + <th>Cnames</th> + <th>IP</th> + <th>MAC</th> + <th>Network</th> + <th>Tags</th> + </tr> + </thead> + <tbody> + {% for interface in host.interfaces %} + <tr> + <td class="btn-group mr-2" role="group" aria-label="edit delete interface"> + <a class="btn btn-light" role="button" href="{{ url_for('network.edit_interface', name=interface.name) }}" title="Edit interface"> + <span class="oi oi-pencil" aria-hidden="true"></span> + </a> + <form method="POST" action="/network/interfaces/delete"> + <input id="interface_id" name="interface_id" type="hidden" value="{{ interface.id }}"> + <button type="submit" class="btn btn-light"> + <span class="oi oi-trash" title="Delete interface" aria-hidden="true"></span> + </button> + </form> + </td> + <td>{{ interface.name }}</td> + <td>{{ interface.cnames | join(' ') }}</td> + <td>{{ interface.ip }}</td> + <td>{{ interface.mac }}</td> + <td>{{ interface.network }}</td> + <td>{{ interface.tags | join(' ') }}</td> + </tr> + {% endfor %} + </tbody> + </table> +{%- endblock %} -- GitLab