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

Refactor database schema

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