diff --git a/app/network/forms.py b/app/network/forms.py index b82c482ca76914485e3babed083b784ec8111d60..755626abac6176bf3de2b5024192968448dffe21 100644 --- a/app/network/forms.py +++ b/app/network/forms.py @@ -133,6 +133,7 @@ class InterfaceForm(CSEntryForm): Unique(models.Interface), starts_with_hostname], filters=[utils.lowercase_field]) + random_mac = BooleanField('Random MAC', default=False) mac_address = StringField( 'MAC', validators=[validators.Optional(), diff --git a/app/network/views.py b/app/network/views.py index 3f1fe565df4100017a824750e3e8a87fca398090..eefcadc79ac3fc566eadedf80f212125f9b7be56 100644 --- a/app/network/views.py +++ b/app/network/views.py @@ -32,15 +32,16 @@ def list_hosts(): @bp.route('/hosts/create', methods=('GET', 'POST')) @login_groups_accepted('admin', 'create') def create_host(): + kwargs = {'random_mac': True} # Try to get the network_id from the session # to pre-fill the form with the same network try: network_id = session['network_id'] except KeyError: - # No need to pass request.form when no extra keywords are given - form = HostInterfaceForm() + pass else: - form = HostInterfaceForm(request.form, network_id=network_id) + kwargs['network_id'] = network_id + form = HostInterfaceForm(request.form, **kwargs) # Remove the host_id field inherited from the InterfaceForm # It's not used in this form del form.host_id @@ -112,7 +113,9 @@ def edit_host(name): @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) + random_mac = host.type == 'Virtual' + form = InterfaceForm(request.form, host_id=host.id, interface_name=host.name, + random_mac=random_mac) 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 @@ -153,6 +156,8 @@ def edit_interface(name): interface_name=interface.name, mac_address=mac_address, cnames_string=cnames_string) + # Remove the random_mac field (not used when editing) + del form.random_mac ips = [interface.ip] ips.extend([str(address) for address in interface.network.available_ips()]) form.ip.choices = utils.get_choices(ips) @@ -441,3 +446,10 @@ def retrieve_domains(): data = [(domain.name,) for domain in models.Domain.query.all()] return jsonify(data=data) + + +@bp.route('/_generate_random_mac') +@login_required +def generate_random_mac(): + data = {'mac': utils.random_mac()} + return jsonify(data=data) diff --git a/app/settings.py b/app/settings.py index 2812fae4f15202dfc6d001e7546c1ac7b9d964ce..9ec9c7da2deab19c61c98c60ce8dea835e191766 100644 --- a/app/settings.py +++ b/app/settings.py @@ -58,3 +58,7 @@ NETWORK_DEFAULT_PREFIX = 24 # (waiting for a real label to be assigned) # WARNING: This is defined here as a global settings but should not be changed! TEMPORARY_ICS_ID = 'ZZ' + +# CSENTRY MAC organizationally unique identifier +# This is a locally administered address +MAC_OUI = '02:42:42' diff --git a/app/static/js/hosts.js b/app/static/js/hosts.js index 4c4b976d8bc9cd9e6d5141c2dcb81bf19ff30e93..6a4ec41bd890a49467488aab9d9014e9b9f9f7e3 100644 --- a/app/static/js/hosts.js +++ b/app/static/js/hosts.js @@ -16,6 +16,23 @@ $(document).ready(function() { ); } + // If random_mac is checked, generate a random address + // Empty the field otherwise + function fill_mac_address() { + if( $("#random_mac").prop("checked") ) { + $.getJSON( + $SCRIPT_ROOT + "/network/_generate_random_mac", + function(json) { + $("#mac_address").val(json.data.mac); + $("#mac_address").prop("readonly", true); + } + ); + } else { + $("#mac_address").val(""); + $("#mac_address").prop("readonly", false); + } + } + // Populate IP select field on first page load for: // - register new host // - add interface @@ -32,13 +49,16 @@ $(document).ready(function() { // Enable / disable item_id field depending on type // Item can only be assigned for physical hosts + // And check / uncheck random_mac checkbox $("#type").on('change', function() { var host_type = $(this).val(); if( host_type == "Physical" ) { $("#item_id").prop("disabled", false); + $("#random_mac").prop("checked", false).change(); } else { $("#item_id").val(""); $("#item_id").prop("disabled", true); + $("#random_mac").prop("checked", true).change(); } }); @@ -48,6 +68,16 @@ $(document).ready(function() { $("#interface_name").val(hostname); }); + // Fill MAC address on page load + if( $("#random_mac").length ) { + fill_mac_address(); + } + + // Fill or clear MAC address depending on random_mac checkbox + $("#random_mac").on('change', function() { + fill_mac_address(); + }); + // Force the first interface to have the hostname as name // This only applies to the hostForm (not when editing or adding interfaces) $("#hostForm input[name=interface_name]").prop("readonly", true); diff --git a/app/templates/network/create_host.html b/app/templates/network/create_host.html index 775a29b06d3783807a5ab74e33eebfaf07c33b61..1a2f35fce0c32fb6b0d072006604fe561a5483b3 100644 --- a/app/templates/network/create_host.html +++ b/app/templates/network/create_host.html @@ -13,6 +13,7 @@ {{ render_field(form.network_id) }} {{ render_field(form.ip) }} {{ render_field(form.interface_name, class_="text-lowercase") }} + {{ render_field(form.random_mac) }} {{ render_field(form.mac_address) }} {{ render_field(form.cnames_string) }} {{ render_field(form.tags) }} diff --git a/app/templates/network/create_interface.html b/app/templates/network/create_interface.html index 2da2cdb5da5d10094ce6216eb0390ebeae1be784..85ed4ba24e7937f5a10f2c3d50932b143673108a 100644 --- a/app/templates/network/create_interface.html +++ b/app/templates/network/create_interface.html @@ -23,6 +23,7 @@ {{ render_field(form.interface_name, class_="text-lowercase") }} {{ render_field(form.network_id) }} {{ render_field(form.ip) }} + {{ render_field(form.random_mac) }} {{ render_field(form.mac_address) }} {{ render_field(form.cnames_string) }} {{ render_field(form.tags) }} diff --git a/app/utils.py b/app/utils.py index ee68efe225dbaf374d35eb21056db9843229a9f1..5c84920ee30722a282387d03fd6f60bdb6bdd869 100644 --- a/app/utils.py +++ b/app/utils.py @@ -12,8 +12,10 @@ This module implements utility functions. import base64 import datetime import io +import random import sqlalchemy as sa import dateutil.parser +from flask import current_app from flask.globals import _app_ctx_stack, _request_ctx_stack from flask_login import current_user from flask_jwt_extended import get_current_user @@ -179,3 +181,12 @@ def parse_to_utc(string): # Convert to UTC and remove timezone d = d.astimezone(datetime.timezone.utc) return d.replace(tzinfo=None) + + +def random_mac(): + """Return a random MAC address""" + octets = [random.randint(0x00, 0xFF), + random.randint(0x00, 0xFF), + random.randint(0x00, 0xFF)] + octets = [f'{nb:02x}' for nb in octets] + return ':'.join((current_app.config['MAC_OUI'], *octets)) diff --git a/tests/functional/test_web.py b/tests/functional/test_web.py index 644b41d6627b102a46f24b2e75a29c63e189cc43..6da1cb8c9329244559102e64724f7eaa6c5389d2 100644 --- a/tests/functional/test_web.py +++ b/tests/functional/test_web.py @@ -11,6 +11,7 @@ This module defines basic web tests. """ import json import pytest +import re def get(client, url): @@ -81,3 +82,10 @@ def test_retrieve_items(logged_client, item_factory): items = response.json['data'] assert set(serial_numbers) == set(item[4] for item in items) assert len(items[0]) == 11 + + +def test_generate_random_mac(logged_client): + response = get(logged_client, '/network/_generate_random_mac') + mac = response.json['data']['mac'] + assert re.match('^(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$', mac) is not None + assert mac.startswith('02:42:42')