diff --git a/app/models.py b/app/models.py index 2a6475992651d3d60011294558f1371ff8d0b7b6..c32d22e40da6417399c2b9062437d4ee58e05d6d 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 c78c52d943edfc1a1bc8c1b9c9b4fd592d5818df..c141eb3847e4217ef5ae5961c4b545728e37f9dc 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 fdd817eca6e8ac8771800a02623ef351574cf5ed..3ffb830a298a32731f674a44f9cfd6979bf4c78f 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 133109982fddedad05196dcf1019609b7ec925e8..c9bf938015d63deffd7ed93efc1e6268dd4c8183 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 0835929b3555881476ca9417bbcbe9f6fd74e999..3aff063a68c06960429f87a87c2f40cc36da3254 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 0000000000000000000000000000000000000000..5fadfb9525a6c66ee552cb52a52f8afd72c150a2 --- /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 0000000000000000000000000000000000000000..fedffe3cd2e077c64aff0349f168a89899ec316a --- /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 0000000000000000000000000000000000000000..a7cfe3e6632cfd115b23f902abd32b4c7d834de5 --- /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 0000000000000000000000000000000000000000..38efba32e2c16bcf6fda6d1d590060c0fb89438b --- /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 %}