diff --git a/app/factory.py b/app/factory.py index b608df8a3e76ff011993e76299d806c97ddb37bd..90740127e0c89aab961c465ad13f5f91658f461b 100644 --- a/app/factory.py +++ b/app/factory.py @@ -14,7 +14,7 @@ from flask import Flask from whitenoise import WhiteNoise from . import settings from .extensions import db, migrate, login_manager, ldap_manager, bootstrap, admin, mail, jwt, toolbar -from .models import Action, Manufacturer, Model, Location, Status +from .models import Action, Manufacturer, Model, Location, Status, Network, Host, Mac from .admin.views import AdminModelView, ItemAdmin, UserAdmin, GroupAdmin from .main.views import bp as main from .users.views import bp as users @@ -101,6 +101,9 @@ def create_app(config=None): admin.add_view(AdminModelView(Location, db.session)) admin.add_view(AdminModelView(Status, db.session)) admin.add_view(ItemAdmin(db.session)) + admin.add_view(AdminModelView(Network, db.session)) + admin.add_view(AdminModelView(Host, db.session)) + admin.add_view(AdminModelView(Mac, db.session)) app.register_blueprint(main) app.register_blueprint(users) diff --git a/app/models.py b/app/models.py index 0d2f7b732106e1b61e08bf26c9457e27e173bdd1..40d439a615c86b218e3e58f19e9e2d63c8c175bf 100644 --- a/app/models.py +++ b/app/models.py @@ -9,9 +9,11 @@ This module implements the models used in the app. :license: BSD 2-Clause, see LICENSE for more details. """ +import ipaddress import re import qrcode import sqlalchemy as sa +from sqlalchemy.dialects import postgresql from sqlalchemy.orm import validates from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy_continuum import make_versioned, version_class @@ -185,6 +187,7 @@ class Model(QRCodeMixin, db.Model): class Location(QRCodeMixin, db.Model): items = db.relationship('Item', back_populates='location') + networks = db.relationship('Network', backref='location') class Status(QRCodeMixin, db.Model): @@ -213,6 +216,7 @@ class Item(db.Model): location = db.relationship('Location', back_populates='items') status = db.relationship('Status', back_populates='items') children = db.relationship('Item', backref=db.backref('parent', remote_side=[id])) + macs = db.relationship('Mac', backref='item') def __init__(self, ics_id=None, serial_number=None, manufacturer=None, model=None, location=None, status=None): # All arguments must be optional for this class to work with flask-admin! @@ -249,6 +253,7 @@ class Item(db.Model): } if long: d['children'] = [utils.format_field(child) for child in self.children] + d['macs'] = [mac.to_dict(long=True) for mac in self.macs] d['history'] = self.history() return d @@ -272,6 +277,99 @@ class Item(db.Model): return versions +class Network(db.Model): + id = db.Column(db.Integer, primary_key=True) + label = db.Column(db.Text) + prefix = db.Column(postgresql.CIDR, nullable=False, unique=True) + first = db.Column(postgresql.INET, nullable=False, unique=True) + last = db.Column(postgresql.INET, nullable=False, unique=True) + gateway = db.Column(postgresql.INET) + vlanid = db.Column(db.Integer, unique=True) + location_id = db.Column(db.Integer, db.ForeignKey('location.id')) + + hosts = db.relationship('Host', backref='network') + + __table_args__ = ( + sa.CheckConstraint('first < last', name='first_less_than_last'), + sa.CheckConstraint('first << prefix', name='first_in_prefix'), + sa.CheckConstraint('last << prefix', name='last_in_prefix'), + ) + + def __str__(self): + return str(self.prefix) + + @validates('hosts') + def validate_hosts(self, key, host): + """Ensure the host IP is in the network range""" + addr = ipaddress.ip_address(host.ip) + net = ipaddress.ip_network(self.prefix) + if addr not in net: + raise utils.CSEntryError(f'IP address shall be in network {self.prefix}', status_code=422) + if addr < ipaddress.ip_address(self.first) or addr > ipaddress.ip_address(self.last): + raise utils.CSEntryError(f'IP address shall be in range {self.first} - {self.last}', status_code=422) + return host + + def to_dict(self): + return { + 'id': self.id, + 'label': self.label, + 'prefix': self.prefix, + 'first': self.first, + 'last': self.last, + 'gateway': self.gateway, + 'vlanid': self.vlanid, + 'location': utils.format_field(self.location), + } + + +class Host(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, unique=True) + + mac = db.relationship('Mac', backref='host') + + def __str__(self): + return str(self.ip) + + def to_dict(self, long=False): + d = { + 'id': self.id, + 'ip': self.ip, + 'name': self.name, + 'network_id': self.network_id, + } + if long: + d['mac'] = getattr(self.mac, 'address', None) + return d + + +class Mac(db.Model): + id = db.Column(db.Integer, primary_key=True) + address = db.Column(postgresql.MACADDR, nullable=False, unique=True) + host_id = db.Column(db.Integer, db.ForeignKey('host.id'), unique=True) + item_id = db.Column(db.Integer, db.ForeignKey('item.id'), nullable=False) + + def __str__(self): + return str(self.address) + + def to_dict(self, long=False): + d = { + 'id': self.id, + 'address': self.address, + 'host_id': self.host_id, + 'item_id': self.item_id, + } + if long: + d['item_ics_id'] = self.item.ics_id + try: + d['host'] = self.host.to_dict() + except AttributeError: + d['host'] = None + return d + + # call configure_mappers after defining all the models # required by sqlalchemy_continuum sa.orm.configure_mappers()