-
Notifications
You must be signed in to change notification settings - Fork 66
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Split nsot/models.py to one model per file
This breaks up the models.py monolith so that each model (and its associated other bits) each live in their own file. Hopefully this makes things easier to read, understand, and PRs easier to review. One downside is that we lose git history on each of these files, but this commit message hopefully serves as a good redirectoin to find the original file, and I think the benefits outweigh that negative. Fixes #248
- Loading branch information
Showing
16 changed files
with
2,673 additions
and
2,536 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
# -*- coding: utf-8 -*- | ||
from django.db import models as djmodels | ||
|
||
from .assignment import Assignment | ||
from .attribute import Attribute | ||
from .change import Change | ||
from .circuit import Circuit | ||
from .device import Device | ||
from .interface import Interface | ||
from .network import Network | ||
from .protocol import Protocol | ||
from .protocol_type import ProtocolType | ||
from .resource import Resource | ||
from .site import Site | ||
from .user import User | ||
from .value import Value | ||
|
||
|
||
__all__ = [ | ||
'Assignment', | ||
'Attribute', | ||
'Change', | ||
'Circuit', | ||
'Device', | ||
'Interface', | ||
'Network', | ||
'Protocol', | ||
'ProtocolType', | ||
'Site', | ||
'User', | ||
'Value', | ||
] | ||
|
||
|
||
# Global signals | ||
def delete_resource_values(sender, instance, **kwargs): | ||
"""Delete values when a Resource object is deleted.""" | ||
instance.attributes.delete() # These are instances of Value | ||
|
||
|
||
resource_subclasses = Resource.__subclasses__() | ||
for model_class in resource_subclasses: | ||
# Value post_delete | ||
djmodels.signals.post_delete.connect( | ||
delete_resource_values, | ||
sender=model_class, | ||
dispatch_uid='value_post_delete_' + model_class.__name__ | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
from __future__ import unicode_literals | ||
|
||
from django.db import models | ||
|
||
from .. import exc, validators | ||
|
||
|
||
class Assignment(models.Model): | ||
""" | ||
DB object for assignment of addresses to interfaces (on devices). | ||
This is used to enforce constraints at the relationship level for addition | ||
of new address assignments. | ||
""" | ||
address = models.ForeignKey( | ||
'Network', related_name='assignments', db_index=True, | ||
help_text='Network to which this assignment is bound.' | ||
) | ||
interface = models.ForeignKey( | ||
'Interface', related_name='assignments', db_index=True, | ||
help_text='Interface to which this assignment is bound.' | ||
) | ||
created = models.DateTimeField(auto_now_add=True) | ||
|
||
def __unicode__(self): | ||
return u'interface=%s, address=%s' % (self.interface, self.address) | ||
|
||
class Meta: | ||
unique_together = ('address', 'interface') | ||
index_together = unique_together | ||
|
||
def clean_address(self, value): | ||
"""Enforce that new addresses can only be host addresses.""" | ||
addr = validators.validate_host_address(value) | ||
|
||
# Enforce uniqueness upon assignment. | ||
existing = Assignment.objects.filter(address=addr) | ||
if existing.filter(interface__device=self.interface.device).exists(): | ||
raise exc.ValidationError({ | ||
'address': 'Address already assigned to this Device.' | ||
}) | ||
|
||
return value | ||
|
||
def clean_fields(self, exclude=None): | ||
self.clean_address(self.address) | ||
self.address.set_assigned() | ||
|
||
def save(self, *args, **kwargs): | ||
self.full_clean() | ||
super(Assignment, self).save(*args, **kwargs) | ||
|
||
def to_dict(self): | ||
return { | ||
'id': self.id, | ||
'device': self.interface.device.id, | ||
'hostname': self.interface.device_hostname, | ||
'interface': self.interface.id, | ||
'interface_name': self.interface.name, | ||
'address': self.address.cidr, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,213 @@ | ||
from __future__ import unicode_literals | ||
|
||
import re | ||
|
||
from django.conf import settings | ||
from django.db import models | ||
|
||
from .. import exc, fields, validators | ||
from . import constants | ||
|
||
|
||
class Attribute(models.Model): | ||
"""Represents a flexible attribute for Resource objects.""" | ||
# This is purposely not unique as there is a compound index with site_id. | ||
name = models.CharField( | ||
max_length=64, null=False, db_index=True, | ||
help_text='The name of the Attribute.' | ||
) | ||
description = models.CharField( | ||
max_length=255, default='', blank=True, null=False, | ||
help_text='A helpful description of the Attribute.' | ||
) | ||
|
||
# The resource must contain a key and value | ||
required = models.BooleanField( | ||
default=False, null=False, | ||
help_text='Whether the Attribute should be required.' | ||
) | ||
|
||
# In UIs this attribute will be displayed by default. Required implies | ||
# display. | ||
display = models.BooleanField( | ||
default=False, null=False, | ||
help_text=( | ||
'Whether the Attribute should be be displayed by default in ' | ||
'UIs. If required is set, this is also set.' | ||
) | ||
) | ||
|
||
# Attribute values are expected as lists of strings. | ||
multi = models.BooleanField( | ||
default=False, null=False, | ||
help_text='Whether the Attribute should be treated as a list type.' | ||
) | ||
|
||
constraints = fields.JSONField( | ||
'Constraints', null=False, blank=True, | ||
help_text='Dictionary of Attribute constraints.' | ||
) | ||
|
||
site = models.ForeignKey( | ||
'Site', db_index=True, related_name='attributes', | ||
on_delete=models.PROTECT, verbose_name='Site', | ||
help_text='Unique ID of the Site this Attribute is under.' | ||
) | ||
|
||
resource_name = models.CharField( | ||
'Resource Name', max_length=20, null=False, db_index=True, | ||
choices=constants.RESOURCE_CHOICES, | ||
help_text='The name of the Resource to which this Attribute is bound.' | ||
) | ||
|
||
def __unicode__(self): | ||
return u'%s %s (site_id: %s)' % ( | ||
self.resource_name, self.name, self.site_id | ||
) | ||
|
||
class Meta: | ||
unique_together = ('site', 'resource_name', 'name') | ||
index_together = unique_together | ||
|
||
@classmethod | ||
def all_by_name(cls, resource_name=None, site=None): | ||
if resource_name is None: | ||
raise SyntaxError('You must provided a resource_name.') | ||
if site is None: | ||
raise SyntaxError('You must provided a site.') | ||
|
||
query = cls.objects.filter(resource_name=resource_name, site=site) | ||
|
||
return { | ||
attribute.name: attribute | ||
for attribute in query.all() | ||
} | ||
|
||
def clean_constraints(self, value): | ||
"""Enforce formatting of constraints.""" | ||
if not isinstance(value, dict): | ||
raise exc.ValidationError({ | ||
'constraints': 'Expected dictionary but received {}.'.format( | ||
type(value)) | ||
}) | ||
|
||
constraints = { | ||
'allow_empty': value.get('allow_empty', False), | ||
'pattern': value.get('pattern', ''), | ||
'valid_values': value.get('valid_values', []), | ||
} | ||
|
||
if not isinstance(constraints['allow_empty'], bool): | ||
raise exc.ValidationError({ | ||
'constraints': 'allow_empty expected type bool.' | ||
}) | ||
|
||
if not isinstance(constraints['pattern'], basestring): | ||
raise exc.ValidationError({ | ||
'constraints': 'pattern expected type string.' | ||
}) | ||
|
||
if not isinstance(constraints['valid_values'], list): | ||
raise exc.ValidationError({ | ||
'constraints': 'valid_values expected type list.' | ||
}) | ||
|
||
return constraints | ||
|
||
def clean_display(self, value): | ||
if self.required: | ||
return True | ||
return value | ||
|
||
def clean_resource_name(self, value): | ||
if value not in constants.VALID_ATTRIBUTE_RESOURCES: | ||
raise exc.ValidationError({ | ||
'resource_name': 'Invalid resource name: %r.' % value | ||
}) | ||
return value | ||
|
||
def clean_name(self, value): | ||
value = validators.validate_name(value) | ||
|
||
if not settings.ATTRIBUTE_NAME.match(value): | ||
raise exc.ValidationError({ | ||
'name': 'Invalid name: %r.' % value | ||
}) | ||
|
||
return value or False | ||
|
||
def clean_fields(self, exclude=None): | ||
self.constraints = self.clean_constraints(self.constraints) | ||
self.display = self.clean_display(self.display) | ||
self.resource_name = self.clean_resource_name(self.resource_name) | ||
self.name = self.clean_name(self.name) | ||
|
||
def _validate_single_value(self, value, constraints=None): | ||
if not isinstance(value, basestring): | ||
raise exc.ValidationError({ | ||
'value': 'Attribute values must be a string type' | ||
}) | ||
|
||
if constraints is None: | ||
constraints = self.constraints | ||
|
||
allow_empty = constraints.get('allow_empty', False) | ||
if not allow_empty and not value: | ||
raise exc.ValidationError({ | ||
'constraints': "Attribute {} doesn't allow empty values" | ||
.format(self.name) | ||
}) | ||
|
||
pattern = constraints.get('pattern') | ||
if pattern and not re.match(pattern, value): | ||
raise exc.ValidationError({ | ||
'pattern': "Attribute value {} for {} didn't match pattern: {}" | ||
.format(value, self.name, pattern) | ||
}) | ||
|
||
valid_values = set(constraints.get('valid_values', [])) | ||
if valid_values and value not in valid_values: | ||
raise exc.ValidationError( | ||
'Attribute value {} for {} not a valid value: {}' | ||
.format(value, self.name, ', '.join(valid_values)) | ||
) | ||
|
||
return { | ||
'attribute_id': self.id, | ||
'value': value, | ||
} | ||
|
||
def validate_value(self, value): | ||
if self.multi: | ||
if not isinstance(value, list): | ||
raise exc.ValidationError({ | ||
'multi': 'Attribute values must be a list type' | ||
}) | ||
else: | ||
value = [value] | ||
|
||
inserts = [] | ||
# This does a deserialization so save the result | ||
constraints = self.constraints | ||
for val in value: | ||
inserts.append(self._validate_single_value(val, constraints)) | ||
|
||
return inserts | ||
|
||
def save(self, *args, **kwargs): | ||
"""Always enforce constraints.""" | ||
self.full_clean() | ||
super(Attribute, self).save(*args, **kwargs) | ||
|
||
def to_dict(self): | ||
return { | ||
'id': self.id, | ||
'site_id': self.site_id, | ||
'description': self.description, | ||
'name': self.name, | ||
'resource_name': self.resource_name, | ||
'required': self.required, | ||
'display': self.display, | ||
'multi': self.multi, | ||
'constraints': self.constraints, | ||
} |
Oops, something went wrong.