diff --git a/app/models.py b/app/models.py index 2de41f1a98ccca242731a7fc1ad72ca4bfe14bc5..374708397dacf0b1198064a0b0afc99ced26961e 100644 --- a/app/models.py +++ b/app/models.py @@ -588,6 +588,7 @@ class NetworkScope(db.Model): first_vlan = db.Column(db.Integer, nullable=False, unique=True) last_vlan = db.Column(db.Integer, nullable=False, unique=True) supernet = db.Column(postgresql.CIDR, nullable=False, unique=True) + description = db.Column(db.Text) networks = db.relationship('Network', backref='scope') @@ -644,6 +645,7 @@ class NetworkScope(db.Model): 'first_vlan': self.first_vlan, 'last_vlan': self.last_vlan, 'supernet': self.supernet, + 'description': self.description, } diff --git a/app/network/forms.py b/app/network/forms.py index 84573811c6fea5386f6be6ca6fb8f852f6652be8..c19392bfafe84857f4e9e45333ea1013c948eeff 100644 --- a/app/network/forms.py +++ b/app/network/forms.py @@ -10,10 +10,10 @@ This module defines the network blueprint forms. """ from flask_login import current_user -from wtforms import (SelectField, StringField, TextAreaField, +from wtforms import (SelectField, StringField, TextAreaField, IntegerField, SelectMultipleField, BooleanField, validators) from ..helpers import CSEntryForm -from ..validators import Unique, RegexpList, HOST_NAME_RE, VLAN_NAME_RE +from ..validators import Unique, RegexpList, IPNetwork, HOST_NAME_RE, VLAN_NAME_RE from .. import utils, models @@ -40,10 +40,24 @@ class NoValidateSelectField(SelectField): pass +class NetworkScopeForm(CSEntryForm): + name = StringField('Name', + description='name must be 3-25 characters long and contain only letters, numbers and dash', + validators=[validators.InputRequired(), + validators.Regexp(VLAN_NAME_RE), + Unique(models.NetworkScope, column='name')]) + description = TextAreaField('Description') + first_vlan = IntegerField('First vlan') + last_vlan = IntegerField('Last vlan') + supernet = StringField('Supernet', + validators=[validators.InputRequired(), + IPNetwork()]) + + class NetworkForm(CSEntryForm): scope_id = SelectField('Network Scope') vlan_name = StringField('Vlan name', - description='hostname must be 3-25 characters long and contain only letters, numbers and dash', + description='vlan name must be 3-25 characters long and contain only letters, numbers and dash', validators=[validators.InputRequired(), validators.Regexp(VLAN_NAME_RE), Unique(models.Network, column='vlan_name')]) diff --git a/app/network/views.py b/app/network/views.py index a14be57fc6e6641ce7c0644bc2d844ab63418eaa..3840b50b49f40b1f4a24a0bb4fde97d7fb421687 100644 --- a/app/network/views.py +++ b/app/network/views.py @@ -14,7 +14,8 @@ 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, InterfaceForm, HostInterfaceForm, NetworkForm +from .forms import (HostForm, InterfaceForm, HostInterfaceForm, NetworkForm, + NetworkScopeForm) from ..extensions import db from ..decorators import login_groups_accepted from .. import models, utils @@ -200,6 +201,36 @@ def delete_interface(): return redirect(url_for('network.view_host', name=hostname)) +@bp.route('/scopes') +@login_required +def list_scopes(): + return render_template('network/scopes.html') + + +@bp.route('/scopes/create', methods=('GET', 'POST')) +@login_groups_accepted('admin') +def create_scope(): + form = NetworkScopeForm() + if form.validate_on_submit(): + scope = models.NetworkScope(name=form.name.data, + description=form.description.data or None, + first_vlan=form.first_vlan.data, + last_vlan=form.last_vlan.data, + supernet=form.supernet.data) + current_app.logger.debug(f'Trying to create: {scope!r}') + db.session.add(scope) + 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'Network Scope {scope} created!', 'success') + return redirect(url_for('network.create_scope')) + return render_template('network/create_scope.html', form=form) + + @bp.route('/_retrieve_hosts') @login_required def retrieve_hosts(): @@ -345,3 +376,15 @@ def retrieve_ips(subnet, prefix): 'selected_first': first, 'selected_last': last} return jsonify(data=data) + + +@bp.route('/_retrieve_scopes') +@login_required +def retrieve_scopes(): + data = [(scope.name, + scope.description, + scope.first_vlan, + scope.last_vlan, + scope.supernet) + for scope in models.NetworkScope.query.all()] + return jsonify(data=data) diff --git a/app/static/js/scopes.js b/app/static/js/scopes.js new file mode 100644 index 0000000000000000000000000000000000000000..28a2432c4b8715f45da5cc5cdd7b93982a780940 --- /dev/null +++ b/app/static/js/scopes.js @@ -0,0 +1,14 @@ +$(document).ready(function() { + + var scopes_table = $("#scopes_table").DataTable({ + "ajax": function(data, callback, settings) { + $.getJSON( + $SCRIPT_ROOT + "/network/_retrieve_scopes", + function(json) { + callback(json); + }); + }, + "paging": false + }); + +}); diff --git a/app/templates/base-fluid.html b/app/templates/base-fluid.html index 3aff063a68c06960429f87a87c2f40cc36da3254..58172667dda15ded4d6af78856de8f15dd3af3b5 100644 --- a/app/templates/base-fluid.html +++ b/app/templates/base-fluid.html @@ -16,12 +16,14 @@ href="{{ url_for('inventory.attributes', kind='Manufacturer') }}">Attributes</a> <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") %} + {% elif path.startswith("/network") %} <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> - {% endif %} + <a class="list-group-item list-group-item-action {{ is_active(path.startswith("/network/scopes")) }}" + href="{{ url_for('network.list_scopes') }}">Network Scopes</a> + {% endif %} </div> </div> diff --git a/app/templates/network/create_scope.html b/app/templates/network/create_scope.html new file mode 100644 index 0000000000000000000000000000000000000000..06712457f96bbfdd25db3763237675cf25393514 --- /dev/null +++ b/app/templates/network/create_scope.html @@ -0,0 +1,20 @@ +{% extends "network/scopes.html" %} +{% from "_helpers.html" import render_field %} + +{% block title %}Register Network Scope - CSEntry{% endblock %} + +{% block scopes_main %} + <form id="scopeForm" method="POST"> + {{ form.hidden_tag() }} + {{ render_field(form.name) }} + {{ render_field(form.description) }} + {{ render_field(form.first_vlan) }} + {{ render_field(form.last_vlan) }} + {{ render_field(form.supernet) }} + <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/scopes.html b/app/templates/network/scopes.html new file mode 100644 index 0000000000000000000000000000000000000000..0c56c085c17f8bcfd962127d5610518d40fed883 --- /dev/null +++ b/app/templates/network/scopes.html @@ -0,0 +1,37 @@ +{% extends "base-fluid.html" %} +{% from "_helpers.html" import is_active %} + +{% block title %}Network Scopes - CSEntry{% endblock %} + +{% block main %} + {% set path = request.path %} + <ul class="nav nav-tabs"> + <li class="nav-item"> + <a class="nav-link {{ is_active(path.endswith("/network/scopes")) }}" href="{{ url_for('network.list_scopes') }}">List network scopes</a> + </li> + <li class="nav-item"> + <a class="nav-link {{ is_active(path.startswith("/network/scopes/create")) }}" href="{{ url_for('network.create_scope') }}">Register new network scope</a> + </li> + {% block scopes_nav %}{% endblock %} + </ul> + + <br> + + {% block scopes_main %} + <table id="scopes_table" class="table table-bordered table-hover table-sm" cellspacing="0" width="100%"> + <thead> + <tr> + <th>Name</th> + <th>Description</th> + <th>First vlan</th> + <th>Last vlan</th> + <th>Supernet</th> + </tr> + </thead> + </table> + {%- endblock %} +{%- endblock %} + +{% block csentry_scripts %} + <script src="{{ url_for('static', filename='js/scopes.js') }}"></script> +{% endblock %}