diff --git a/app/main/forms.py b/app/main/forms.py index 66a4a00036b84b7f53f1aed6af8fa9622a193fe0..8ee9fd4e9d19d3f9d650c3d103db4132df327a8f 100644 --- a/app/main/forms.py +++ b/app/main/forms.py @@ -42,8 +42,16 @@ class HostForm(FlaskForm): # The list of IPs is dynamically created on the browser side # depending on the selected network ip = NoValidateSelectField('IP', choices=[]) - name = StringField('Hostname') + name = StringField('Hostname', + description='hostname must be 2-20 characters long and contain only letters, numbers and dash', + validators=[validators.InputRequired(), + validators.Regexp(models.HOST_NAME_RE)], + filters=[utils.lowercase_field]) + description = StringField('Description') + mac_id = SelectField('MAC') def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.network_id.choices = [(str(network.id), network.address) for network in models.Network.query.all()] + self.network_id.choices = [(str(network.id), network.vlan_name) for network in models.Network.query.all()] + self.mac_id.choices = [('', '')] + self.mac_id.choices.extend([(str(mac.id), mac.address) for mac in models.Mac.query.all()]) diff --git a/app/main/views.py b/app/main/views.py index 6acd739ab407390d8a02ed2479d351764967ca03..6b058fb7e220d24374c07dd9fab6c3558cda02e8 100644 --- a/app/main/views.py +++ b/app/main/views.py @@ -65,10 +65,17 @@ def index(): @bp.route('/items') @login_required -def items_index(): +def list_items(): return render_template('items.html') +@bp.route('/items/create', methods=('GET', 'POST')) +@login_groups_accepted('admin', 'create') +def create_item(): + form = None + return render_template('create_item.html', form=form) + + @bp.route('/view/<ics_id>') @login_required def view_item(ics_id): @@ -119,9 +126,15 @@ def retrieve_qrcodes_name(kind): return jsonify(data=data) -@bp.route('/hosts', methods=('GET', 'POST')) +@bp.route('/hosts') +@login_required +def list_hosts(): + return render_template('hosts.html') + + +@bp.route('/hosts/create', methods=('GET', 'POST')) @login_groups_accepted('admin', 'create') -def hosts_index(): +def create_host(): # Try to get the network_id from the URL parameters # to display the form with the same selected network # when reloading the page (redirect after submit) @@ -135,7 +148,9 @@ def hosts_index(): if form.validate_on_submit(): host = models.Host(ip=form.ip.data, network_id=form.network_id.data, - name=form.name.data) + name=form.name.data, + description=form.description.data or None, + mac_id=form.mac_id.data or None) current_app.logger.debug(f'Trying to create: {host!r}') db.session.add(host) try: @@ -144,14 +159,20 @@ def hosts_index(): db.session.rollback() current_app.logger.warning(f'{e}') flash(f'{e}', 'error') - return redirect(url_for('main.hosts_index', network_id=host.network_id)) - return render_template('hosts.html', form=form) + else: + flash(f'Host {host} created!', 'success') + return redirect(url_for('main.create_host', network_id=host.network_id)) + return render_template('create_host.html', form=form) @bp.route('/_retrieve_hosts') @login_required def retrieve_hosts(): - data = [(host.name, host.ip, host.network.vlan_name) for host in models.Host.query.all()] + data = [(host.name, + host.ip, + host.description, + str(host.mac), + host.network.vlan_name) for host in models.Host.query.all()] return jsonify(data=data) diff --git a/app/models.py b/app/models.py index 7108bfa982ae0a77fd07d370950304d7f02f8a1c..d76880a98343be9ac121fc0faf270606948238d3 100644 --- a/app/models.py +++ b/app/models.py @@ -27,6 +27,7 @@ from . import utils ICS_ID_RE = re.compile('[A-Z]{3}[0-9]{3}') +HOST_NAME_RE = re.compile('^[a-z0-9\-]{2,20}$') make_versioned(plugins=[FlaskUserPlugin()]) @@ -420,12 +421,23 @@ class Host(db.Model): else: super().__init__(ip=ip, **kwargs) + @validates('name') + def validate_name(self, key, string): + """Ensure the hostname matches the required format""" + if string is None: + return None + # Force the string to lowercase + lower_string = string.lower() + if HOST_NAME_RE.fullmatch(lower_string) is None: + raise ValidationError('Host name shall match [a-z0-9\-]{2,20}') + return lower_string + @property def address(self): return ipaddress.ip_address(self.ip) def __str__(self): - return str(self.ip) + return str(self.name) def __repr__(self): return f'Host(id={self.id}, network_id={self.network_id}, ip={self.ip}, name={self.name}, description={self.description}, mac={self.mac})' diff --git a/app/templates/_helpers.html b/app/templates/_helpers.html index 8c761e0919d77223bc39e320c07f983cf45a6769..22aff7a4fe6e0400fc488c2d98ccd7b5eb9c18fd 100644 --- a/app/templates/_helpers.html +++ b/app/templates/_helpers.html @@ -15,3 +15,27 @@ {{ link_to_item(ics_id) }} {% endfor %} {%- endmacro %} + +{% macro render_field(field) -%} + {% set field_class = kwargs.pop('class_', '') + ' form-control' %} + {% if field.errors %} + {% set field_class = field_class + ' is-invalid' %} + {% endif %} + + <div class="form-group row"> + {{ field.label(class_="col-sm-2 col-form-label") }} + <div class="col-sm-10"> + {{ field(class_=field_class) }} + {% if field.description %} + <small class="form-text text-muted"> + {{ field.description|safe }} + </small> + {% endif %} + {% if field.errors %} + <div class="invalid-feedback"> + {{ field.errors|join(' / ') }} + </div> + {% endif %} + </div> + </div> +{%- endmacro %} diff --git a/app/templates/base.html b/app/templates/base.html index b853c9e03a2dc6247bf0cb663bea6b74ee6fc456..4b1e84d7526cb01723b0fedee8ee3fa5c337dec1 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -23,8 +23,8 @@ <div class="collapse navbar-collapse" id="navbarSupportedContent"> <div class="navbar-nav mr-auto"> {% set path = request.path %} - <a class="nav-item nav-link {{ is_active(path.startswith("/items")) }}" href="{{ url_for('main.items_index') }}">Items</a> - <a class="nav-item nav-link {{ is_active(path == "/hosts") }}" href="{{ url_for('main.hosts_index') }}">Hosts</a> + <a class="nav-item nav-link {{ is_active(path.startswith("/items")) }}" href="{{ url_for('main.list_items') }}">Items</a> + <a class="nav-item nav-link {{ is_active(path.startswith("/hosts")) }}" href="{{ url_for('main.list_hosts') }}">Hosts</a> <div class="dropdown {{ is_active(path.startswith("/qrcodes")) }}"> <a class="nav-link dropdown-toggle" href="#" id="qrcodesDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> QR Codes diff --git a/app/templates/create_host.html b/app/templates/create_host.html new file mode 100644 index 0000000000000000000000000000000000000000..ad7c97336d0ba06e5a5bbf87f38f14f00a6425db --- /dev/null +++ b/app/templates/create_host.html @@ -0,0 +1,35 @@ +{%- extends "base.html" %} +{% from "_helpers.html" import render_field %} + +{% block title %}Hosts - CSEntry{% endblock %} + +{% block main %} + <ul class="nav nav-tabs"> + <li class="nav-item"> + <a class="nav-link" href="{{ url_for('main.list_hosts') }}">List hosts</a> + </li> + <li class="nav-item"> + <a class="nav-link active" href="{{ url_for('main.create_host') }}">Register new host</a> + </li> + </ul> + + <br> + + <form id="hostForm" method="POST"> + {{ form.hidden_tag() }} + {{ render_field(form.network_id) }} + {{ render_field(form.ip) }} + {{ render_field(form.name, class_="text-lowercase") }} + {{ render_field(form.description) }} + {{ render_field(form.mac_id) }} + <div class="form-group row"> + <div class="col-sm-10"> + <button type="submit" class="btn btn-primary">Submit</button> + </div> + </div> + </form> +{%- endblock %} + +{% block csentry_scripts %} + <script src="{{ url_for('static', filename='js/hosts.js') }}"></script> +{% endblock %} diff --git a/app/templates/create_item.html b/app/templates/create_item.html new file mode 100644 index 0000000000000000000000000000000000000000..56a5f18812f57c8c8715e5d5d3ed25a0d57705ed --- /dev/null +++ b/app/templates/create_item.html @@ -0,0 +1,26 @@ +{%- extends "base.html" %} + +{% block title %}Items - CSEntry{% endblock %} + +{% block main %} + <ul class="nav nav-tabs"> + <li class="nav-item"> + <a class="nav-link" href="{{ url_for('main.list_items') }}">List items</a> + </li> + <li class="nav-item"> + <a class="nav-link active" href="{{ url_for('main.create_item') }}">Register new item</a> + </li> + </ul> + + <br> + + <div class="card"> + <h4 class="card-header">Register new item</h4> + <div class="card-body"> + </div> + </div> +{%- endblock %} + +{% block csentry_scripts %} + <script src="{{ url_for('static', filename='js/items.js') }}"></script> +{% endblock %} diff --git a/app/templates/hosts.html b/app/templates/hosts.html index 6f22bf9e47386582e0822d10fd0912bc14ed07c1..344106cd581af0833c25099a47bbabea8b041e13 100644 --- a/app/templates/hosts.html +++ b/app/templates/hosts.html @@ -3,45 +3,24 @@ {% block title %}Hosts - CSEntry{% endblock %} {% block main %} - <div class="card"> - <h4 class="card-header">Register new host</h4> - <div class="card-body"> - <form id="hostForm" method="POST"> - {{ form.hidden_tag() }} - <div class="form-group row"> - {{ form.network_id.label(class_="col-sm-2 col-form-label") }} - <div class="col-sm-10"> - {{ form.network_id(class_="form-control") }} - </div> - </div> - <div class="form-group row"> - {{ form.ip.label(class_="col-sm-2 col-form-label") }} - <div class="col-sm-10"> - {{ form.ip(class_="form-control") }} - </div> - </div> - <div class="form-group row"> - {{ form.name.label(class_="col-sm-2 col-form-label") }} - <div class="col-sm-10"> - {{ form.name(class_="form-control") }} - </div> - </div> - <div class="form-group row"> - <div class="col-sm-10"> - <button type="submit" class="btn btn-primary">Submit</button> - </div> - </div> - </form> - </div> - </div> + <ul class="nav nav-tabs"> + <li class="nav-item"> + <a class="nav-link active" href="{{ url_for('main.list_hosts') }}">List hosts</a> + </li> + <li class="nav-item"> + <a class="nav-link" href="{{ url_for('main.create_host') }}">Register new host</a> + </li> + </ul> - <hr class="separator"> + <br> <table id="hosts_table" class="table table-bordered table-hover table-sm"> <thead> <tr> <th>Hostname</th> <th>IP</th> + <th>Description</th> + <th>MAC</th> <th>Network</th> </tr> </thead> diff --git a/app/templates/items.html b/app/templates/items.html index adfbdb337337bd4b952acdec554941f1664d141d..9576fc17f967356022172e8da89d69136aae734b 100644 --- a/app/templates/items.html +++ b/app/templates/items.html @@ -3,13 +3,17 @@ {% block title %}Items - CSEntry{% endblock %} {% block main %} - <div class="card"> - <h4 class="card-header">Register new item</h4> - <div class="card-body"> - </div> - </div> + <ul class="nav nav-tabs"> + <li class="nav-item"> + <a class="nav-link active" href="{{ url_for('main.list_items') }}">List items</a> + </li> + <li class="nav-item"> + <a class="nav-link" href="{{ url_for('main.create_item') }}">Register new item</a> + </li> + </ul> + + <br> - <hr class="separator"> <table id="items_table" class="table table-bordered table-hover table-sm"> <thead> <tr> diff --git a/app/utils.py b/app/utils.py index a20b7e9bc3e8d81c6503f2ce1a853e32deaa9c1c..aab9a5a15cdd8eef01e68cab5f4714b86bbd5bc4 100644 --- a/app/utils.py +++ b/app/utils.py @@ -121,3 +121,11 @@ def get_query(query, args): except (sa.exc.InvalidRequestError, AttributeError) as e: raise CSEntryError('Invalid query arguments', status_code=422) return query + + +def lowercase_field(value): + """Filter to force form value to lowercase""" + try: + return value.lower() + except AttributeError: + return value