From a5905e5b34908d8511275fe7e76c8b22ec262e2c Mon Sep 17 00:00:00 2001
From: Benjamin Bertrand <benjamin.bertrand@esss.se>
Date: Sat, 23 Dec 2017 00:18:26 +0100
Subject: [PATCH] Change interface mac select field to string field

One should be able to enter a MAC when creating/editing an interface.
---
 app/helpers.py                              | 21 +++++++++++++++++++++
 app/network/forms.py                        |  9 ++++++---
 app/network/views.py                        | 21 +++++++++++++++++----
 app/templates/network/create_host.html      |  2 +-
 app/templates/network/create_interface.html |  2 +-
 app/templates/network/edit_interface.html   |  2 +-
 app/validators.py                           |  2 +-
 7 files changed, 48 insertions(+), 11 deletions(-)

diff --git a/app/helpers.py b/app/helpers.py
index 92db026..d68b378 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 c19392b..145a8a9 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 f17b43c..d7c09cb 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 b2b0548..775a29b 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 01f4688..2da2cdb 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 621feb7..5ddaf85 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 004543e..6c8ae87 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:
-- 
GitLab