Skip to content
Snippets Groups Projects
Commit d8fdf80d authored by Benjamin Bertrand's avatar Benjamin Bertrand
Browse files

Generate random MAC for Virtual hosts

Fixes INFRA-197
parent 43e91441
No related branches found
No related tags found
No related merge requests found
......@@ -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(),
......
......@@ -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)
......@@ -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'
......@@ -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);
......
......@@ -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) }}
......
......@@ -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) }}
......
......@@ -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))
......@@ -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')
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment