diff --git a/app/helpers.py b/app/helpers.py index 92db026455e99e51cba5f541e60d96749631903f..d68b378d57e85661f3415a75761a0160a81843ab 100644 --- a/app/helpers.py +++ b/app/helpers.py @@ -9,7 +9,10 @@ This module implements helpers functions for the models. :license: BSD 2-Clause, see LICENSE for more details. """ +import sqlalchemy as sa from flask_wtf import FlaskForm +from .extensions import db +from . import models class CSEntryForm(FlaskForm): @@ -25,3 +28,21 @@ class CSEntryForm(FlaskForm): super().__init__(obj=obj, **kwargs) else: super().__init__(formdata=formdata, obj=obj, **kwargs) + + +def associate_mac_to_interface(address, interface): + """Associate the given address to an interface + + The Mac is retrieved if it exists or created otherwise + + :param address: Mac address string + :param interface: Interface instance + """ + if not address: + return + try: + mac = models.Mac.query.filter_by(address=address).one() + except sa.orm.exc.NoResultFound: + mac = models.Mac(address=address) + db.session.add(mac) + mac.interfaces.append(interface) diff --git a/app/network/forms.py b/app/network/forms.py index c19392bfafe84857f4e9e45333ea1013c948eeff..145a8a927302d49b0948511dd84656b46b459f1b 100644 --- a/app/network/forms.py +++ b/app/network/forms.py @@ -13,7 +13,8 @@ from flask_login import current_user from wtforms import (SelectField, StringField, TextAreaField, IntegerField, SelectMultipleField, BooleanField, validators) from ..helpers import CSEntryForm -from ..validators import Unique, RegexpList, IPNetwork, HOST_NAME_RE, VLAN_NAME_RE +from ..validators import (Unique, RegexpList, IPNetwork, HOST_NAME_RE, + VLAN_NAME_RE, MAC_ADDRESS_RE) from .. import utils, models @@ -103,7 +104,10 @@ class InterfaceForm(CSEntryForm): validators.Regexp(HOST_NAME_RE), Unique(models.Interface)], filters=[utils.lowercase_field]) - mac_id = SelectField('MAC', coerce=utils.coerce_to_str_or_none) + mac_address = StringField( + 'MAC', + validators=[validators.Optional(), + validators.Regexp(MAC_ADDRESS_RE, message='Invalid MAC address')]) cnames_string = StringField( 'Cnames', description='space separated list of cnames (must be 2-20 characters long and contain only letters, numbers and dash)', @@ -121,7 +125,6 @@ class InterfaceForm(CSEntryForm): network_query = models.Network.query.filter(models.Network.admin_only.is_(False)) self.network_id.choices = utils.get_model_choices(models.Network, allow_none=False, 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') diff --git a/app/network/views.py b/app/network/views.py index f17b43cea52d69c4863a5a377a68efc2637610d4..d7c09cb68c1f6333dbf7dc18841415e89cdad208 100644 --- a/app/network/views.py +++ b/app/network/views.py @@ -18,7 +18,7 @@ from .forms import (HostForm, InterfaceForm, HostInterfaceForm, NetworkForm, NetworkScopeForm) from ..extensions import db from ..decorators import login_groups_accepted -from .. import models, utils +from .. import models, utils, helpers bp = Blueprint('network', __name__) @@ -58,9 +58,9 @@ def create_host(): interface = models.Interface(name=form.interface_name.data, ip=form.ip.data, network_id=network_id, - mac_id=form.mac_id.data, tags=tags) interface.cnames = [models.Cname(name=name) for name in form.cnames_string.data.split()] + helpers.associate_mac_to_interface(form.mac_address.data, interface) host.interfaces = [interface] current_app.logger.debug(f'Trying to create: {host!r}') db.session.add(host) @@ -123,9 +123,9 @@ def create_interface(hostname): name=form.interface_name.data, ip=form.ip.data, network_id=form.network_id.data, - mac_id=form.mac_id.data, tags=tags) interface.cnames = [models.Cname(name=name) for name in form.cnames_string.data.split()] + helpers.associate_mac_to_interface(form.mac_address.data, interface) current_app.logger.debug(f'Trying to create: {interface!r}') db.session.add(interface) try: @@ -145,8 +145,13 @@ def create_interface(hostname): def edit_interface(name): interface = models.Interface.query.filter_by(name=name).first_or_404() cnames_string = ' '.join([str(cname) for cname in interface.cnames]) + try: + mac_address = interface.mac.address + except AttributeError: + mac_address = '' form = InterfaceForm(request.form, obj=interface, interface_name=interface.name, + mac_address=mac_address, cnames_string=cnames_string) ips = [interface.ip] ips.extend([str(address) for address in interface.network.available_ips()]) @@ -155,7 +160,15 @@ def edit_interface(name): 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 + if form.mac_address.data: + if form.mac_address.data != mac_address: + # The MAC changed - add the new one to the interface + # that will remove the association to the previous one + helpers.associate_mac_to_interface(form.mac_address.data, interface) + # else: nothing to do (address didn't change) + else: + # No MAC associated + interface.mac_id = None # Delete the cnames that have been removed new_cnames_string = form.cnames_string.data.split() for (index, cname) in enumerate(interface.cnames): diff --git a/app/templates/network/create_host.html b/app/templates/network/create_host.html index b2b054835d565d49612df7fe9ef522e687325567..775a29b06d3783807a5ab74e33eebfaf07c33b61 100644 --- a/app/templates/network/create_host.html +++ b/app/templates/network/create_host.html @@ -13,7 +13,7 @@ {{ render_field(form.network_id) }} {{ render_field(form.ip) }} {{ render_field(form.interface_name, class_="text-lowercase") }} - {{ render_field(form.mac_id) }} + {{ render_field(form.mac_address) }} {{ render_field(form.cnames_string) }} {{ render_field(form.tags) }} <div class="form-group row"> diff --git a/app/templates/network/create_interface.html b/app/templates/network/create_interface.html index 01f46882a1a4da8c98efc201f76ce8e7a203853b..2da2cdb5da5d10094ce6216eb0390ebeae1be784 100644 --- a/app/templates/network/create_interface.html +++ b/app/templates/network/create_interface.html @@ -23,7 +23,7 @@ {{ 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.mac_address) }} {{ render_field(form.cnames_string) }} {{ render_field(form.tags) }} <div class="form-group row"> diff --git a/app/templates/network/edit_interface.html b/app/templates/network/edit_interface.html index 621feb7253cbffb924f8c4fdc295a38e0815e32e..5ddaf855f9e1ed09442e21712dcafedddde22c2c 100644 --- a/app/templates/network/edit_interface.html +++ b/app/templates/network/edit_interface.html @@ -26,7 +26,7 @@ {{ 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.mac_address) }} {{ render_field(form.cnames_string) }} {{ render_field(form.tags) }} <div class="form-group row"> diff --git a/app/validators.py b/app/validators.py index 004543edbe2776d633c232b2c6ece09f73e742db..6c8ae870c02983efb7d2d7c6421fb019b442c439 100644 --- a/app/validators.py +++ b/app/validators.py @@ -17,7 +17,7 @@ from wtforms import ValidationError ICS_ID_RE = re.compile('[A-Z]{3}[0-9]{3}') HOST_NAME_RE = re.compile('^[a-z0-9\-]{2,20}$') VLAN_NAME_RE = re.compile('^[A-Za-z0-9\-]{3,25}$') -MAC_ADDRESS_RE = re.compile('^(?:[0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2}$') +MAC_ADDRESS_RE = re.compile('^(?:[0-9a-fA-F]{2}[:-]?){5}[0-9a-fA-F]{2}$') class IPNetwork: