From a5963f99c73b8de8e7985d13e8fb5190e46164f3 Mon Sep 17 00:00:00 2001
From: Benjamin Bertrand <benjamin.bertrand@esss.se>
Date: Wed, 13 Dec 2017 12:57:45 +0100
Subject: [PATCH] Refactor database schema

- Rename Host to Interface
- Create new Host table to group interfaces
---
 app/admin/views.py           |  9 -----
 app/api/main.py              | 20 +++++-----
 app/factory.py               |  4 +-
 app/models.py                | 75 ++++++++++++++++++++++++------------
 app/templates/view_item.html |  6 +--
 5 files changed, 65 insertions(+), 49 deletions(-)

diff --git a/app/admin/views.py b/app/admin/views.py
index bbecc9a..ba0d6aa 100644
--- a/app/admin/views.py
+++ b/app/admin/views.py
@@ -98,12 +98,3 @@ class NetworkAdmin(AdminModelView):
             'filters': [lambda x: x or None],
         },
     }
-
-
-class HostAdmin(AdminModelView):
-
-    form_args = {
-        'name': {
-            'filters': [lambda x: x or None],
-        }
-    }
diff --git a/app/api/main.py b/app/api/main.py
index f097d5f..5ace0ee 100644
--- a/app/api/main.py
+++ b/app/api/main.py
@@ -14,7 +14,7 @@ from flask import (current_app, Blueprint, jsonify, request)
 from flask_jwt_extended import jwt_required
 from flask_ldap3_login import AuthenticationResponseStatus
 from ..extensions import ldap_manager, db
-from ..models import Item, Manufacturer, Model, Location, Status, Action, Network, Host
+from ..models import Item, Manufacturer, Model, Location, Status, Action, Network, Interface
 from .. import utils, tokens
 from ..decorators import jwt_groups_accepted
 
@@ -258,19 +258,19 @@ def create_network():
         'vlan_name', 'vlan_id', 'address', 'first_ip', 'last_ip', 'scope'))
 
 
-@bp.route('/hosts')
+@bp.route('/interfaces')
 @jwt_required
-def get_hosts():
+def get_interfaces():
     # TODO: add pagination
-    query = utils.get_query(Host.query, request.args)
-    hosts = query.order_by(Host.ip)
-    data = [host.to_dict() for host in hosts]
+    query = utils.get_query(Interface.query, request.args)
+    interfaces = query.order_by(Interface.ip)
+    data = [interface.to_dict() for interface in interfaces]
     return jsonify(data)
 
 
-@bp.route('/hosts', methods=['POST'])
+@bp.route('/interfaces', methods=['POST'])
 @jwt_required
 @jwt_groups_accepted('admin', 'create')
-def create_host():
-    """Create a new host"""
-    return create_generic_model(Host, mandatory_fields=('network', 'ip', 'name'))
+def create_interface():
+    """Create a new interface"""
+    return create_generic_model(Interface, mandatory_fields=('network', 'ip', 'name'))
diff --git a/app/factory.py b/app/factory.py
index 8c73d05..a29a72b 100644
--- a/app/factory.py
+++ b/app/factory.py
@@ -15,7 +15,7 @@ from whitenoise import WhiteNoise
 from . import settings, models
 from .extensions import db, migrate, login_manager, ldap_manager, bootstrap, admin, mail, jwt, toolbar
 from .admin.views import (AdminModelView, ItemAdmin, UserAdmin, GroupAdmin, TokenAdmin,
-                          NetworkAdmin, HostAdmin)
+                          NetworkAdmin)
 from .main.views import bp as main
 from .users.views import bp as users
 from .api.main import bp as api
@@ -104,7 +104,7 @@ def create_app(config=None):
     admin.add_view(ItemAdmin(models.Item, db.session))
     admin.add_view(AdminModelView(models.NetworkScope, db.session))
     admin.add_view(NetworkAdmin(models.Network, db.session))
-    admin.add_view(HostAdmin(models.Host, db.session))
+    admin.add_view(AdminModelView(models.Interface, db.session))
     admin.add_view(AdminModelView(models.Mac, db.session))
     admin.add_view(AdminModelView(models.Cname, db.session))
 
diff --git a/app/models.py b/app/models.py
index 46f4578..0dfc218 100644
--- a/app/models.py
+++ b/app/models.py
@@ -309,7 +309,7 @@ class Network(db.Model):
     admin_only = db.Column(db.Boolean, nullable=False, default=False)
     scope_id = db.Column(db.Integer, db.ForeignKey('network_scope.id'), nullable=False)
 
-    hosts = db.relationship('Host', backref='network')
+    interfaces = db.relationship('Interface', backref='network')
 
     __table_args__ = (
         sa.CheckConstraint('first_ip < last_ip', name='first_ip_less_than_last_ip'),
@@ -344,7 +344,7 @@ class Network(db.Model):
 
         The range is defined by the first and last IP
         """
-        return [addr for addr in self.network_ip.hosts()
+        return [addr for addr in self.network_ip.interfaces()
                 if self.first <= addr <= self.last]
 
     def used_ips(self):
@@ -352,7 +352,7 @@ class Network(db.Model):
 
         The list is sorted
         """
-        return sorted(host.address for host in self.hosts)
+        return sorted(interface.address for interface in self.interfaces)
 
     def available_ips(self):
         """Return the list of IP addresses available"""
@@ -387,13 +387,13 @@ class Network(db.Model):
             raise ValidationError(f'Last IP address {ip} is less than the first address {self.first}')
         return ip
 
-    @validates('hosts')
-    def validate_hosts(self, key, host):
-        """Ensure the host IP is in the network range"""
-        addr, net = self.ip_in_network(host.ip, self.address)
+    @validates('interfaces')
+    def validate_interfaces(self, key, interface):
+        """Ensure the interface IP is in the network range"""
+        addr, net = self.ip_in_network(interface.ip, self.address)
         if addr < self.first or addr > self.last:
-            raise ValidationError(f'IP address {host.ip} is not in range {self.first} - {self.last}')
-        return host
+            raise ValidationError(f'IP address {interface.ip} is not in range {self.first} - {self.last}')
+        return interface
 
     def to_dict(self):
         return {
@@ -410,11 +410,11 @@ class Network(db.Model):
         }
 
 
-# Table required for Many-to-Many relationships between hosts and tags
-hosttags_table = db.Table(
-    'hosttags',
+# Table required for Many-to-Many relationships between interfaces and tags
+interfacetags_table = db.Table(
+    'interfacetags',
     db.Column('tag_id', db.Integer, db.ForeignKey('tag.id'), primary_key=True),
-    db.Column('host_id', db.Integer, db.ForeignKey('host.id'), primary_key=True)
+    db.Column('interface_id', db.Integer, db.ForeignKey('interface.id'), primary_key=True)
 )
 
 
@@ -423,23 +423,48 @@ class Tag(QRCodeMixin, db.Model):
 
 
 class Host(db.Model):
+    id = db.Column(db.Integer, primary_key=True)
+    name = db.Column(db.Text, nullable=False, unique=True)
+    type = db.Column(db.Text)
+    description = db.Column(db.Text)
+    item_id = db.Column(db.Integer, db.ForeignKey('item.id'))
+
+    interfaces = db.relationship('Interface', backref='host')
+
+    def __str__(self):
+        return str(self.name)
+
+    @validates('name')
+    def validate_name(self, key, string):
+        """Ensure the name 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('Interface name shall match [a-z0-9\-]{2,20}')
+        return lower_string
+
+
+class Interface(db.Model):
     id = db.Column(db.Integer, primary_key=True)
     network_id = db.Column(db.Integer, db.ForeignKey('network.id'), nullable=False)
     ip = db.Column(postgresql.INET, nullable=False, unique=True)
     name = db.Column(db.Text, nullable=False, unique=True)
     description = db.Column(db.Text)
     mac_id = db.Column(db.Integer, db.ForeignKey('mac.id'))
+    host_id = db.Column(db.Integer, db.ForeignKey('host.id'))
 
-    cnames = db.relationship('Cname', backref='host')
-    tags = db.relationship('Tag', secondary=hosttags_table, lazy='subquery',
-                           backref=db.backref('hosts', lazy=True))
+    cnames = db.relationship('Cname', backref='interface')
+    tags = db.relationship('Tag', secondary=interfacetags_table, lazy='subquery',
+                           backref=db.backref('interfaces', lazy=True))
 
     def __init__(self, **kwargs):
         # Automatically convert network to an instance of Network if it was passed
         # as an address string
         if 'network' in kwargs:
             kwargs['network'] = utils.convert_to_model(kwargs['network'], Network, 'address')
-        # WARNING! Setting self.network will call validates_hosts in the Network class
+        # WARNING! Setting self.network will call validates_interfaces in the Network class
         # For the validation to work, self.ip must be set before!
         # Ensure that ip is passed before network
         try:
@@ -451,13 +476,13 @@ class Host(db.Model):
 
     @validates('name')
     def validate_name(self, key, string):
-        """Ensure the hostname matches the required format"""
+        """Ensure the name 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}')
+            raise ValidationError('Interface name shall match [a-z0-9\-]{2,20}')
         return lower_string
 
     @property
@@ -468,7 +493,7 @@ class Host(db.Model):
         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})'
+        return f'Interface(id={self.id}, network_id={self.network_id}, ip={self.ip}, name={self.name}, description={self.description}, mac={self.mac})'
 
     def to_dict(self, long=False):
         d = {
@@ -486,9 +511,9 @@ class Host(db.Model):
 class Mac(db.Model):
     id = db.Column(db.Integer, primary_key=True)
     address = db.Column(postgresql.MACADDR, nullable=False, unique=True)
-    item_id = db.Column(db.Integer, db.ForeignKey('item.id'), nullable=False)
+    item_id = db.Column(db.Integer, db.ForeignKey('item.id'))
 
-    hosts = db.relationship('Host', backref='mac')
+    interfaces = db.relationship('Interface', backref='mac')
 
     def __str__(self):
         return str(self.address)
@@ -501,14 +526,14 @@ class Mac(db.Model):
         }
         if long:
             d['item_ics_id'] = self.item.ics_id
-            d['hosts'] = [host.to_dict() for host in self.hosts]
+            d['interfaces'] = [interface.to_dict() for interface in self.interfaces]
         return d
 
 
 class Cname(db.Model):
     id = db.Column(db.Integer, primary_key=True)
     name = db.Column(db.Text, nullable=False, unique=True)
-    host_id = db.Column(db.Integer, db.ForeignKey('host.id'), nullable=False, unique=True)
+    interface_id = db.Column(db.Integer, db.ForeignKey('interface.id'), nullable=False, unique=True)
 
     def __str__(self):
         return str(self.name)
@@ -517,7 +542,7 @@ class Cname(db.Model):
         return {
             'id': self.id,
             'name': self.name,
-            'host_id': self.host_id,
+            'interface_id': self.interface_id,
         }
 
 
diff --git a/app/templates/view_item.html b/app/templates/view_item.html
index 355bfc4..27e75dc 100644
--- a/app/templates/view_item.html
+++ b/app/templates/view_item.html
@@ -39,9 +39,9 @@
       {% set macloop = loop %}
       <dt class="col-sm-3">MAC Address{{ loop.index }}</dt>
       <dd class="col-sm-9">{{ mac.address }}</dd>
-      {% for host in mac.hosts %}
-        <dt class="col-sm-3">Host{{ macloop.index }}-{{ loop.index }}</dt>
-        <dd class="col-sm-9">IP: {{ host.ip }} / name: {{ host.name }}</dd>
+      {% for interface in mac.interfaces %}
+        <dt class="col-sm-3">Interface{{ macloop.index }}-{{ loop.index }}</dt>
+        <dd class="col-sm-9">IP: {{ interface.ip }} / name: {{ interface.name }}</dd>
       {% endfor %}
     {% endfor %}
     <dt class="col-sm-3">Parent</dt>
-- 
GitLab