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