diff --git a/nsot/admin.py b/nsot/admin.py index 4f70ae3d..65e0396d 100644 --- a/nsot/admin.py +++ b/nsot/admin.py @@ -84,3 +84,16 @@ class InterfaceAdmin(admin.ModelAdmin): fields = list_display admin.site.register(models.Interface, InterfaceAdmin) + + +class IterableAdmin(admin.ModelAdmin): + list_display = ('name', 'description', 'min_val', 'max_val', 'increment', 'site') + fields = list_display + +admin.site.register(models.Iterable, IterableAdmin) + +class ItervalueAdmin(admin.ModelAdmin): + list_display = ('value', 'iterable', 'site') + fields = list_display + +admin.site.register(models.Itervalue, ItervalueAdmin) diff --git a/nsot/api/filters.py b/nsot/api/filters.py index f1af9787..86c6202d 100644 --- a/nsot/api/filters.py +++ b/nsot/api/filters.py @@ -47,6 +47,12 @@ class Meta: model = models.Device fields = ['hostname', 'attributes'] +class ItervalueFilter(ResourceFilter): + """Filter for Itervalue objects.""" + class Meta: + model = models.Itervalue + fields = ['iterable', 'value', 'attributes'] + class NetworkFilter(ResourceFilter): """Filter for Network objects.""" diff --git a/nsot/api/serializers.py b/nsot/api/serializers.py index f708c565..556a6ebe 100644 --- a/nsot/api/serializers.py +++ b/nsot/api/serializers.py @@ -490,6 +490,89 @@ class Meta: fields = ('id', 'name', 'description', 'type', 'mac_address', 'speed', 'parent_id', 'addresses', 'attributes') +########### +# Iterable +########### +class IterableSerializer(NsotSerializer): + """Used for GET, DELETE on Iterables.""" + class Meta: + model = models.Iterable + fields = '__all__' + +class IterableCreateSerializer(IterableSerializer): + """Used for POST on Iterables.""" + site_id = fields.IntegerField( + label = get_field_attr(models.Iterable, 'site', 'verbose_name'), + help_text = get_field_attr(models.Iterable, 'site', 'help_text') + ) + + class Meta: + model = models.Iterable + fields = ( 'name', 'description', 'min_val', 'max_val', 'increment', 'site_id') + + +class IterableUpdateSerializer(BulkSerializerMixin, + IterableCreateSerializer): + """ Used for PUT on Iterables. """ + class Meta: + model = models.Iterable + list_serializer_class = BulkListSerializer + fields = ('id', 'name', 'description', 'min_val', 'max_val', 'increment') + + +class IterablePartialUpdateSerializer(BulkSerializerMixin, + IterableCreateSerializer): + """ Used for PATCH, on Iterables. """ + class Meta: + model = models.Iterable + list_serializer_class = BulkListSerializer + fields = ('id', 'name', 'description', 'min_val', 'max_val', 'increment') + + +########### +# Itervalue +########### +class ItervalueSerializer(ResourceSerializer): + """Used for GET, DELETE on Itervalues.""" + class Meta: + model = models.Itervalue + fields = '__all__' + + +class ItervalueCreateSerializer(ItervalueSerializer): + """Used for POST on Itervalues.""" + site_id = fields.IntegerField( + label = get_field_attr(models.Iterable, 'site', 'verbose_name'), + help_text = get_field_attr(models.Iterable, 'site', 'help_text') + ) + + class Meta: + model = models.Itervalue + fields = ('iterable', 'value', 'attributes', 'site_id') + + +class ItervalueUpdateSerializer(BulkSerializerMixin, + ItervalueCreateSerializer): + """ Used for PUT on Itervalues. """ + attributes = JSONDictField( + required=True, + help_text='Dictionary of attributes to set.' + ) + + class Meta: + model = models.Itervalue + list_serializer_class = BulkListSerializer + fields = ('id', 'iterable', 'value', 'attributes') + + +class ItervaluePartialUpdateSerializer(BulkSerializerMixin, + ItervalueCreateSerializer): + """ Used for PATCH, on Itervalues. """ + class Meta: + model = models.Itervalue + list_serializer_class = BulkListSerializer + fields = ('id', 'iterable', 'value', 'attributes') + ######### # Circuit diff --git a/nsot/api/urls.py b/nsot/api/urls.py index eb85f4de..7bbf8ce9 100644 --- a/nsot/api/urls.py +++ b/nsot/api/urls.py @@ -20,6 +20,8 @@ router.register(r'networks', views.NetworkViewSet) router.register(r'users', views.UserViewSet) router.register(r'values', views.ValueViewSet) +router.register(r'iterables', views.IterableViewSet) +router.register(r'itervalues', views.ItervalueViewSet) # Nested router for resources under /sites sites_router = routers.BulkNestedRouter( @@ -34,6 +36,8 @@ sites_router.register(r'interfaces', views.InterfaceViewSet) sites_router.register(r'networks', views.NetworkViewSet) sites_router.register(r'values', views.ValueViewSet) +sites_router.register(r'iterables', views.IterableViewSet) +sites_router.register(r'itervalues', views.ItervalueViewSet) # Wire up our API using automatic URL routing. # Additionally, we include login URLs for the browsable API. diff --git a/nsot/api/views.py b/nsot/api/views.py index 5aec8c4d..c352d951 100644 --- a/nsot/api/views.py +++ b/nsot/api/views.py @@ -373,6 +373,54 @@ def get_serializer_class(self): return serializers.AttributeUpdateSerializer return self.serializer_class +class IterableViewSet(ResourceViewSet): + """ + API endpoint that allows Iterables to be viewed or edited. + """ + queryset = models.Iterable.objects.all() + serializer_class = serializers.IterableSerializer + filter_fields = ('name', 'description', 'min_val', 'max_val', 'increment') + natural_key = 'name' + + def get_serializer_class(self): + if self.request.method == 'POST': + return serializers.IterableCreateSerializer + if self.request.method in ('PUT'): + return serializers.IterableUpdateSerializer + if self.request.method in ('PATCH'): + return serializers.IterablePartialUpdateSerializer + return self.serializer_class + + def get_natural_key_kwargs(self, filter_value): + """Return a dict of kwargs for natural_key lookup.""" + return {self.natural_key: filter_value} + + @detail_route(methods=['get']) + def next_value(self, request, pk=None, site_pk=None, *args, **kwargs): + """Return next available Iterable value from this Network.""" + iterable = self.get_resource_object(pk, site_pk) + value = iterable.get_next_value() + return self.success(value) + + +class ItervalueViewSet(ResourceViewSet): + """ + API endpoint that allows Itervalues to be viewed or edited. + """ + queryset = models.Itervalue.objects.all() + serializer_class = serializers.ItervalueSerializer + filter_class = filters.ItervalueFilter + + def get_serializer_class(self): + if self.request.method == 'POST': + return serializers.ItervalueCreateSerializer + if self.request.method in ('PUT'): + return serializers.ItervalueUpdateSerializer + if self.request.method in ('PATCH'): + return serializers.ItervaluePartialUpdateSerializer + + return self.serializer_class + class DeviceViewSet(ResourceViewSet): """ diff --git a/nsot/migrations/0026_auto_20160920_1209.py b/nsot/migrations/0026_auto_20160920_1209.py new file mode 100644 index 00000000..78cef280 --- /dev/null +++ b/nsot/migrations/0026_auto_20160920_1209.py @@ -0,0 +1,254 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import nsot.fields +import nsot.util.core +import django.db.models.deletion +from django.conf import settings +import django_extensions.db.fields.json + + +class Migration(migrations.Migration): + + dependencies = [ + ('nsot', '0025_value_site'), + ] + + operations = [ + migrations.CreateModel( + name='Iterable', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(help_text='The name of the Iterable.', unique=True, max_length=255)), + ('description', models.TextField(default='', help_text='A helpful description for the Iterable.', blank=True)), + ('min_val', models.PositiveIntegerField(default=1, help_text='The minimum value of the Iterable.')), + ('max_val', models.PositiveIntegerField(default=100, help_text='The maximum value of the Iterable.')), + ('increment', models.PositiveIntegerField(default=1, help_text='Increment value of the Iterable by.')), + ], + ), + migrations.CreateModel( + name='IterValue', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('value', models.IntegerField(default=1, help_text='The value of the iterable.')), + ('unique_id', models.TextField(help_text='An identification for the value table, used to id different rows that have this tag', blank=True)), + ('iterable', models.ForeignKey(related_name='itervalue', on_delete=django.db.models.deletion.PROTECT, to='nsot.Iterable')), + ], + ), + migrations.AlterField( + model_name='assignment', + name='address', + field=models.ForeignKey(related_name='assignments', to='nsot.Network', help_text='Network to which this assignment is bound.'), + ), + migrations.AlterField( + model_name='assignment', + name='interface', + field=models.ForeignKey(related_name='assignments', to='nsot.Interface', help_text='Interface to which this assignment is bound.'), + ), + migrations.AlterField( + model_name='attribute', + name='constraints', + field=django_extensions.db.fields.json.JSONField(help_text='Dictionary of Attribute constraints.', verbose_name='Constraints', blank=True), + ), + migrations.AlterField( + model_name='attribute', + name='description', + field=models.CharField(default='', help_text='A helpful description of the Attribute.', max_length=255, blank=True), + ), + migrations.AlterField( + model_name='attribute', + name='display', + field=models.BooleanField(default=False, help_text='Whether the Attribute should be be displayed by default in UIs. If required is set, this is also set.'), + ), + migrations.AlterField( + model_name='attribute', + name='multi', + field=models.BooleanField(default=False, help_text='Whether the Attribute should be treated as a list type.'), + ), + migrations.AlterField( + model_name='attribute', + name='name', + field=models.CharField(help_text='The name of the Attribute.', max_length=64, db_index=True), + ), + migrations.AlterField( + model_name='attribute', + name='required', + field=models.BooleanField(default=False, help_text='Whether the Attribute should be required.'), + ), + migrations.AlterField( + model_name='attribute', + name='resource_name', + field=models.CharField(help_text='The name of the Resource to which this Attribute is bound.', max_length=20, verbose_name='Resource Name', db_index=True, choices=[('Device', 'Device'), ('Interface', 'Interface'), ('Network', 'Network')]), + ), + migrations.AlterField( + model_name='attribute', + name='site', + field=models.ForeignKey(related_name='attributes', on_delete=django.db.models.deletion.PROTECT, verbose_name='Site', to='nsot.Site', help_text='Unique ID of the Site this Attribute is under.'), + ), + migrations.AlterField( + model_name='change', + name='_resource', + field=django_extensions.db.fields.json.JSONField(help_text='Local cache of the changed Resource. (Internal use only)', verbose_name='Resource', blank=True), + ), + migrations.AlterField( + model_name='change', + name='change_at', + field=models.DateTimeField(help_text='The timestamp of this Change.', auto_now_add=True), + ), + migrations.AlterField( + model_name='change', + name='event', + field=models.CharField(help_text='The type of event this Change represents.', max_length=10, choices=[('Create', 'Create'), ('Update', 'Update'), ('Delete', 'Delete')]), + ), + migrations.AlterField( + model_name='change', + name='resource_id', + field=models.IntegerField(help_text='The unique ID of the Resource for this Change.', verbose_name='Resource ID'), + ), + migrations.AlterField( + model_name='change', + name='resource_name', + field=models.CharField(help_text='The name of the Resource for this Change.', max_length=20, verbose_name='Resource Type', db_index=True, choices=[('Network', 'Network'), ('Attribute', 'Attribute'), ('IterValue', 'IterValue'), ('Site', 'Site'), ('Interface', 'Interface'), ('Device', 'Device'), ('Iterable', 'Iterable')]), + ), + migrations.AlterField( + model_name='change', + name='site', + field=models.ForeignKey(related_name='changes', verbose_name='Site', to='nsot.Site', help_text='Unique ID of the Site this Change is under.'), + ), + migrations.AlterField( + model_name='change', + name='user', + field=models.ForeignKey(related_name='changes', to=settings.AUTH_USER_MODEL, help_text='The User that initiated this Change.'), + ), + migrations.AlterField( + model_name='device', + name='_attributes_cache', + field=django_extensions.db.fields.json.JSONField(help_text='Local cache of attributes. (Internal use only)', blank=True), + ), + migrations.AlterField( + model_name='device', + name='hostname', + field=models.CharField(help_text='The hostname of the Device.', max_length=255, db_index=True), + ), + migrations.AlterField( + model_name='device', + name='site', + field=models.ForeignKey(related_name='devices', on_delete=django.db.models.deletion.PROTECT, verbose_name='Site', to='nsot.Site', help_text='Unique ID of the Site this Device is under.'), + ), + migrations.AlterField( + model_name='interface', + name='_attributes_cache', + field=django_extensions.db.fields.json.JSONField(help_text='Local cache of attributes. (Internal use only)', blank=True), + ), + migrations.AlterField( + model_name='interface', + name='addresses', + field=models.ManyToManyField(help_text='Network addresses assigned to this Interface', related_name='addresses', through='nsot.Assignment', to='nsot.Network', db_index=True), + ), + migrations.AlterField( + model_name='interface', + name='site', + field=models.ForeignKey(related_name='interfaces', on_delete=django.db.models.deletion.PROTECT, to='nsot.Site', help_text='Unique ID of the Site this Interface is under.'), + ), + migrations.AlterField( + model_name='interface', + name='speed', + field=models.IntegerField(default=1000, help_text='Integer of Mbps of interface (e.g. 20000 for 20 Gbps). If not provided, defaults to 1000.', db_index=True, blank=True), + ), + migrations.AlterField( + model_name='network', + name='_attributes_cache', + field=django_extensions.db.fields.json.JSONField(help_text='Local cache of attributes. (Internal use only)', blank=True), + ), + migrations.AlterField( + model_name='network', + name='broadcast_address', + field=nsot.fields.BinaryIPAddressField(help_text='The broadcast address for the Network. (Internal use only)', max_length=16, db_index=True), + ), + migrations.AlterField( + model_name='network', + name='is_ip', + field=models.BooleanField(default=False, help_text='Whether the Network is a host address or not.', db_index=True), + ), + migrations.AlterField( + model_name='network', + name='network_address', + field=nsot.fields.BinaryIPAddressField(help_text='The network address for the Network. The network address and the prefix length together uniquely define a network.', max_length=16, verbose_name='Network Address', db_index=True), + ), + migrations.AlterField( + model_name='network', + name='parent', + field=models.ForeignKey(related_name='children', on_delete=django.db.models.deletion.PROTECT, default=None, blank=True, to='nsot.Network', help_text='The parent Network of the Network.', null=True), + ), + migrations.AlterField( + model_name='network', + name='prefix_length', + field=models.IntegerField(help_text='Length of the Network prefix, in bits.', verbose_name='Prefix Length', db_index=True), + ), + migrations.AlterField( + model_name='network', + name='site', + field=models.ForeignKey(related_name='networks', on_delete=django.db.models.deletion.PROTECT, verbose_name='Site', to='nsot.Site', help_text='Unique ID of the Site this Network is under.'), + ), + migrations.AlterField( + model_name='network', + name='state', + field=models.CharField(default='allocated', help_text='The allocation state of the Network.', max_length=20, db_index=True, choices=[('allocated', 'Allocated'), ('assigned', 'Assigned'), ('orphaned', 'Orphaned'), ('reserved', 'Reserved')]), + ), + migrations.AlterField( + model_name='site', + name='description', + field=models.TextField(default='', help_text='A helpful description for the Site.', blank=True), + ), + migrations.AlterField( + model_name='site', + name='name', + field=models.CharField(help_text='The name of the Site.', unique=True, max_length=255), + ), + migrations.AlterField( + model_name='user', + name='secret_key', + field=models.CharField(default=nsot.util.core.generate_secret_key, help_text="The user's secret_key used for API authentication.", max_length=44), + ), + migrations.AlterField( + model_name='value', + name='attribute', + field=models.ForeignKey(related_name='values', on_delete=django.db.models.deletion.PROTECT, to='nsot.Attribute', help_text='The Attribute to which this Value is assigned.'), + ), + migrations.AlterField( + model_name='value', + name='name', + field=models.CharField(help_text='The name of the Attribute to which the Value is bound. (Internal use only)', max_length=64, verbose_name='Name', blank=True), + ), + migrations.AlterField( + model_name='value', + name='resource_id', + field=models.IntegerField(help_text='The unique ID of the Resource to which the Value is bound.', verbose_name='Resource ID'), + ), + migrations.AlterField( + model_name='value', + name='resource_name', + field=models.CharField(help_text='The name of the Resource type to which the Value is bound.', max_length=20, verbose_name='Resource Type', db_index=True, choices=[('Network', 'Network'), ('Attribute', 'Attribute'), ('IterValue', 'IterValue'), ('Site', 'Site'), ('Interface', 'Interface'), ('Device', 'Device'), ('Iterable', 'Iterable')]), + ), + migrations.AlterField( + model_name='value', + name='site', + field=models.ForeignKey(related_name='values', on_delete=django.db.models.deletion.PROTECT, verbose_name='Site', to='nsot.Site', help_text='Unique ID of the Site this Value is under.'), + ), + migrations.AlterField( + model_name='value', + name='value', + field=models.CharField(help_text='The Attribute value.', max_length=255, db_index=True, blank=True), + ), + migrations.AddField( + model_name='itervalue', + name='site', + field=models.ForeignKey(related_name='itervalue', on_delete=django.db.models.deletion.PROTECT, verbose_name='Site', to='nsot.Site', help_text='Unique ID of the Site this IterValue is under.'), + ), + migrations.AddField( + model_name='iterable', + name='site', + field=models.ForeignKey(related_name='Iterable', on_delete=django.db.models.deletion.PROTECT, verbose_name='Site', to='nsot.Site', help_text='Unique ID of the Site this Attribute is under.'), + ), + ] diff --git a/nsot/migrations/0027_auto_20160925_1135.py b/nsot/migrations/0027_auto_20160925_1135.py new file mode 100644 index 00000000..cf499d1b --- /dev/null +++ b/nsot/migrations/0027_auto_20160925_1135.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import django_extensions.db.fields.json + + +class Migration(migrations.Migration): + + dependencies = [ + ('nsot', '0026_auto_20160920_1209'), + ] + + operations = [ + migrations.RemoveField( + model_name='itervalue', + name='unique_id', + ), + migrations.AddField( + model_name='itervalue', + name='_attributes_cache', + field=django_extensions.db.fields.json.JSONField(help_text='Local cache of attributes. (Internal use only)', blank=True), + ), + ] diff --git a/nsot/migrations/0028_auto_20161004_0830.py b/nsot/migrations/0028_auto_20161004_0830.py new file mode 100644 index 00000000..f8e1df2a --- /dev/null +++ b/nsot/migrations/0028_auto_20161004_0830.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('nsot', '0027_auto_20160925_1135'), + ] + + operations = [ + migrations.AlterModelOptions( + name='itervalue', + options={'verbose_name': 'itervalue'}, + ), + migrations.AlterField( + model_name='attribute', + name='resource_name', + field=models.CharField(help_text='The name of the Resource to which this Attribute is bound.', max_length=20, verbose_name='Resource Name', db_index=True, choices=[('Device', 'Device'), ('Interface', 'Interface'), ('Itervalue', 'Itervalue'), ('Network', 'Network')]), + ), + migrations.AlterField( + model_name='change', + name='resource_name', + field=models.CharField(help_text='The name of the Resource for this Change.', max_length=20, verbose_name='Resource Type', db_index=True, choices=[('Network', 'Network'), ('Attribute', 'Attribute'), ('Itervalue', 'Itervalue'), ('Site', 'Site'), ('Interface', 'Interface'), ('Device', 'Device'), ('Iterable', 'Iterable')]), + ), + migrations.AlterField( + model_name='itervalue', + name='site', + field=models.ForeignKey(related_name='itervalue', on_delete=django.db.models.deletion.PROTECT, verbose_name='Site', to='nsot.Site', help_text='Unique ID of the Site this Itervalue is under.'), + ), + migrations.AlterField( + model_name='value', + name='resource_name', + field=models.CharField(help_text='The name of the Resource type to which the Value is bound.', max_length=20, verbose_name='Resource Type', db_index=True, choices=[('Network', 'Network'), ('Attribute', 'Attribute'), ('Itervalue', 'Itervalue'), ('Site', 'Site'), ('Interface', 'Interface'), ('Device', 'Device'), ('Iterable', 'Iterable')]), + ), + ] diff --git a/nsot/migrations/0029_auto_20161005_1445.py b/nsot/migrations/0029_auto_20161005_1445.py new file mode 100644 index 00000000..d8309919 --- /dev/null +++ b/nsot/migrations/0029_auto_20161005_1445.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import django_extensions.db.fields.json + + +class Migration(migrations.Migration): + + dependencies = [ + ('nsot', '0028_auto_20161004_0830'), + ] + + operations = [ + migrations.AddField( + model_name='iterable', + name='_attributes_cache', + field=django_extensions.db.fields.json.JSONField(help_text='Local cache of attributes. (Internal use only)', blank=True), + ), + migrations.AlterField( + model_name='attribute', + name='resource_name', + field=models.CharField(help_text='The name of the Resource to which this Attribute is bound.', max_length=20, verbose_name='Resource Name', db_index=True, choices=[('Device', 'Device'), ('Interface', 'Interface'), ('Itervalue', 'Itervalue'), ('Network', 'Network'), ('Iterable', 'Iterable')]), + ), + migrations.AlterField( + model_name='iterable', + name='site', + field=models.ForeignKey(related_name='iterable', on_delete=django.db.models.deletion.PROTECT, verbose_name='Site', to='nsot.Site', help_text='Unique ID of the Site this Attribute is under.'), + ), + ] diff --git a/nsot/migrations/0030_auto_20161006_0416.py b/nsot/migrations/0030_auto_20161006_0416.py new file mode 100644 index 00000000..c2e175d7 --- /dev/null +++ b/nsot/migrations/0030_auto_20161006_0416.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('nsot', '0029_auto_20161005_1445'), + ] + + operations = [ + migrations.RemoveField( + model_name='iterable', + name='_attributes_cache', + ), + migrations.AlterField( + model_name='attribute', + name='resource_name', + field=models.CharField(help_text='The name of the Resource to which this Attribute is bound.', max_length=20, verbose_name='Resource Name', db_index=True, choices=[('Device', 'Device'), ('Interface', 'Interface'), ('Itervalue', 'Itervalue'), ('Network', 'Network')]), + ), + ] diff --git a/nsot/migrations/0032_merge.py b/nsot/migrations/0032_merge.py new file mode 100644 index 00000000..a42f6918 --- /dev/null +++ b/nsot/migrations/0032_merge.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('nsot', '0031_populate_circuit_name_slug'), + ('nsot', '0030_auto_20161006_0416'), + ] + + operations = [ + ] diff --git a/nsot/models.py b/nsot/models.py index 8f10e5a6..cd98bb14 100644 --- a/nsot/models.py +++ b/nsot/models.py @@ -29,7 +29,7 @@ # These are constants that becuase they are tied directly to the underlying # objects are explicitly NOT USER CONFIGURABLE. RESOURCE_BY_IDX = ( - 'Site', 'Network', 'Attribute', 'Device', 'Interface', 'Circuit' + 'Site', 'Network', 'Attribute', 'Device', 'Interface', 'Circuit', 'Iterable', 'Itervalue' ) RESOURCE_BY_NAME = { obj_type: idx @@ -40,7 +40,7 @@ VALID_CHANGE_RESOURCES = set(RESOURCE_BY_IDX) VALID_ATTRIBUTE_RESOURCES = set([ - 'Network', 'Device', 'Interface', 'Circuit' + 'Network', 'Device', 'Interface', 'Circuit', 'Itervalue' ]) # Lists of 2-tuples of (value, option) for displaying choices in certain model @@ -2085,7 +2085,6 @@ def clean_fields(self, exclude=None): # Site doesn't have an id to itself, so if obj is a Site, use it. self.site = obj if isinstance(obj, Site) else obj.site - serializer_class = self.get_serializer_for_resource(self.resource_name) serializer = serializer_class(obj) self._resource = serializer.data @@ -2111,6 +2110,148 @@ def to_dict(self): } +class Iterable(models.Model): + """Generic iterable for stateful services - vlan#, po#, tenant ID etc""" + ''' + min/max_val = defines the valid range for the Iterable + increment = steps to increment the Iterable + + ''' + name = models.CharField( + max_length=255, unique=True, help_text='The name of the Iterable.' + ) + description = models.TextField(default='', blank=True, \ + help_text='A helpful description for the Iterable') + min_val = models.PositiveIntegerField( + default=1, help_text='The minimum value of the Iterable.' + ) + max_val = models.PositiveIntegerField( + default=100, help_text='The maximum value of the Iterable.' + ) + increment = models.PositiveIntegerField( + default = 1, help_text='Increment value of the Iterable by.' + ) + site = models.ForeignKey( + Site, db_index=True, related_name='iterable', + on_delete=models.PROTECT, verbose_name='Site', + help_text='Unique ID of the Site this Attribute is under.' + ) + + def __unicode__(self): + return u'name=%s, min=%s, max=%s, increment=%s' % (self.name, + self.min_val, + self.max_val, + self.increment + ) + + def get_next_value(self): + "Get the next value of the iterable" + try: + "First try to generate the next value based on the current \ + allocation" + curr_val = Itervalue.objects.filter(iterable=self.id). \ + order_by('-value').values_list('value', flat=True)[0] + incr = self.increment + next_val = curr_val + incr + try: + if self.min_val <= next_val <= self.max_val: + return [next_val] + else: + raise exc.ValidationError({ + 'next_val': 'Out of range' + }) + + except: + log.debug('value out of range - exceeded') + raise exc.ValidationError({ + 'next_val': 'Out of range' + }) + except IndexError: + "Index Error implies that the table has not been \ + intialized - so assign the first value" + return [self.min_val] + + def clean_fields(self, exclude=None): + if not self.increment <= self.max_val: + raise exc.ValidationError({ 'increment': 'Increment should \ + be less than the max value for it to be useable' }) + + def save(self, *args, **kwargs): + self.full_clean() + super(Iterable, self).save(*args, **kwargs) + + + def to_dict(self): + return { + 'id': self.id, + 'name': self.name, + 'description': self.description, + 'min_val': self.min_val, + 'max_val': self.max_val, + 'increment': self.increment, + } + + +class Itervalue(Resource): + """Value table for the generic iterable defined above""" + ''' + value = contains the value + getnext = returns the next iterated value for this particular + Iterable. + This table uses the attribute. + The intention of attribute here is to potentially associate a + "service key" (or any other KV pairs), + that will keep track of the (potentially multiple iterable) values + associated with a particular automation instance (e.g an ansible + playbook that needs next available vlan numbers, portchannel + numbers etc). We can then use this service key to perform CRUD + operations on those values (in other words on the invocation + instance of the automation service/playbook) iterable = Foreign + key that ties the Iterable with the value + ''' + iterable = models.ForeignKey(Iterable, on_delete=models.PROTECT, + related_name='itervalue') + + value = models.IntegerField( + default=1, help_text='The value of the iterable.' + ) + # BORROW the logic from class Value - for easier mgmt of the Itervalue + # We are currently inferring the site_id from the parent Attribute in + # .save() method. We don't want to even care about the site_id, but it + # simplifies managing them this way. + site = models.ForeignKey( + Site, db_index=True, related_name='itervalue', + on_delete=models.PROTECT, verbose_name='Site', + help_text='Unique ID of the Site this Itervalue is under.' + ) + + class Meta: + """Itervalue Meta class""" + verbose_name = "itervalue" + + def __unicode__(self): + return u'value=%s, iterable=%s' % (self.value, self.iterable.name) + + def clean_fields(self, exclude=None): + query = Itervalue.objects.all() + dupe = query.filter(iterable=self.iterable, + value = self.value) + if len(dupe) > 1: #dupe will have more than 1 element if + #duplicate exists + raise exc.ValidationError({'duplicate': 'Itervalue already exists'}) + def save(self, *args, **kwargs): + self.full_clean() + super(Itervalue, self).save(*args, **kwargs) + + def to_dict(self): + return { + 'id': self.id, + 'value': self.value, + 'iterable': self.iterable.id, + 'attributes': self.get_attributes() + } + + # Signals def delete_resource_values(sender, instance, **kwargs): """Delete values when a Resource object is deleted.""" diff --git a/nsot/static/src/js/app.js b/nsot/static/src/js/app.js index ac651d67..eecc5159 100644 --- a/nsot/static/src/js/app.js +++ b/nsot/static/src/js/app.js @@ -153,6 +153,14 @@ templateUrl: "attribute.html", controller: "AttributeController" }) + .when("/sites/:siteId/iterables", { + templateUrl: "iterables.html", + controller: "IterablesController" + }) + .when("/sites/:siteId/iterables/:iterableId", { + templateUrl: "iterable.html", + controller: "IterableController" + }) .when("/sites/:siteId/changes", { templateUrl: "changes.html", controller: "ChangesController" diff --git a/nsot/static/src/js/services.js b/nsot/static/src/js/services.js index f0288893..aae5be67 100644 --- a/nsot/static/src/js/services.js +++ b/nsot/static/src/js/services.js @@ -164,6 +164,44 @@ return Attribute; }); + app.factory("Iterable", function($resource, $http){ + var Iterable = $resource( + "/api/sites/:siteId/iterables/:id/", + { siteId: "@siteId", id: "@id" }, + buildActions($http, "iterable", "iterables") + ); + + Iterable.prototype.updateFromForm = function(formData) { + return _.extend(this, { + name: formData.name, + description: formData.description, + min_val: formData.min_val, + max_val: formData.max_val, + increment: formData.increment, + }); + }; + + Iterable.fromForm = function(formData) { + var attr = new Iterable(); + attr.updateFromForm(formData); + return attr; + }; + + Iterable.prototype.toForm = function() { + return { + name: this.name, + description: this.description, + min_val: this.min_val, + max_val: this.max_val, + increment: this.increment, + //allowEmpty: this.constraints.allow_empty, + }; + }; + + return Iterable; + }); + + app.factory("Network", function($resource, $http){ var Network = $resource( "/api/sites/:siteId/networks/:id/", diff --git a/nsot/static/src/templates/includes/iterables-form.html b/nsot/static/src/templates/includes/iterables-form.html new file mode 100644 index 00000000..6d0e14b3 --- /dev/null +++ b/nsot/static/src/templates/includes/iterables-form.html @@ -0,0 +1,85 @@ +
+
+
+ + +
+ + [[attribute.name]] + +
+ +
+
+ + +
+ + [[attribute.resource_name]] + +
+
+
+ + +
+

Constraints

+
+ + +
+
+ + +
+ + + + diff --git a/nsot/static/src/templates/iterable.html b/nsot/static/src/templates/iterable.html new file mode 100644 index 00000000..32c19cfe --- /dev/null +++ b/nsot/static/src/templates/iterable.html @@ -0,0 +1,85 @@ + +
+ + + + + +
+ + + Iterable + + +
+
Name
+
[[iterable.name]]
+ +
Description
+
[[iterable.description]]
+ +
Minimum Value
+
[[iterable.min_val]]
+ +
Maximum Value
+
[[iterable.max_val]]
+ +
Incrementor
+
[[iterable.increment]]
+ +
+ +
+
+
+ + + + + + + + + + + +
diff --git a/nsot/static/src/templates/iterables.html b/nsot/static/src/templates/iterables.html new file mode 100644 index 00000000..fedc532e --- /dev/null +++ b/nsot/static/src/templates/iterables.html @@ -0,0 +1,67 @@ + +
+ + + + + + + + + +
+ + Iterables + + No Iterables + + + + + + + + + + + + + + + + + + + +
NameDescriptionMin valueMax valueincrementor
[[itr.resource_name]] + [[itr.name]] + [[itr.description]]
+
+
+ +
+ + +
diff --git a/nsot/templates/ui/menu.html b/nsot/templates/ui/menu.html index 6fa18d59..b6aa0b1a 100644 --- a/nsot/templates/ui/menu.html +++ b/nsot/templates/ui/menu.html @@ -43,6 +43,14 @@ Attributes +
  • + + Iterable + +
  • +
  • diff --git a/pip-selfcheck.json b/pip-selfcheck.json new file mode 100644 index 00000000..5eb78a0c --- /dev/null +++ b/pip-selfcheck.json @@ -0,0 +1 @@ +{"last_check":"2016-10-06T19:17:00Z","pypi_version":"8.1.2"} \ No newline at end of file diff --git a/tests/api_tests/test_iterables.py b/tests/api_tests/test_iterables.py new file mode 100644 index 00000000..bbc27445 --- /dev/null +++ b/tests/api_tests/test_iterables.py @@ -0,0 +1,187 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import pytest + +# Allow everything in there to access the DB +pytestmark = pytest.mark.django_db + +import copy +from django.core.urlresolvers import reverse +import json +import logging +from rest_framework import status + + +from .fixtures import live_server, client, user, site +from .util import ( + assert_created, assert_error, assert_success, assert_deleted, load_json, + Client, load, get_result +) + + +log = logging.getLogger(__name__) + + +def test_creation(client, site): + """Test creation of Iterables.""" + + itr_uri = site.list_uri('iterable') + # Successfully create an iterable + itr_resp = client.create(itr_uri, name='itr1', description="test-iterable", min_val=10, max_val=20, increment=1) + itr = get_result(itr_resp) + itr_obj_uri = site.detail_uri('iterable', id=itr['id']) + assert_created(itr_resp, itr_obj_uri) + + #Validate that the repsonse (which contains the creation data) matches the GET response, upon quering the API + payload = get_result(itr_resp) + expected = [payload] + get_resp = client.get(itr_uri) + assert_success(client.get(itr_uri), expected) + # Successfully get a single Network Attribute + assert_success(client.get(itr_obj_uri), payload) + + # Verify successful get of single Iterable by natural_key + itr_natural_uri = site.detail_uri('iterable', id='itr1') + assert_success(client.get(itr_natural_uri), itr) +def test_bulk_operations(client, site): + """Test creating/updating multiple Iterables at once.""" + + itr_uri = site.list_uri('iterable') + + # Successfully create a collection of Iterables + collection = [ + {'name': 'itr1', 'description': 'iterable1', 'min_val': 10, 'max_val': 20, 'increment': 1}, + {'name': 'itr2', 'description': 'iterable2', 'min_val': 10, 'max_val': 20, 'increment': 1}, + {'name': 'itr3', 'description': 'iterable3', 'min_val': 10, 'max_val': 20, 'increment': 1}, + ] + collection_response = client.post( + itr_uri, + data=json.dumps(collection) + ) + assert_created(collection_response, None) + + # Successfully get all created Attributes + output = collection_response.json() + cli_resp = client.get(itr_uri) + payload = get_result(output) + + assert_success( + client.get(itr_uri), + payload + ) + + # Test bulk update to add a description to each Attribute +# updated = copy.deepcopy(payload) +# +# for item in updated: +# item['description'] = 'This is the best attribute ever.' +# updated_resp = client.put(itr_uri, data=json.dumps(updated)) +# expected = updated_resp.json() +# +# assert updated == expected + +# +def test_update(client, site): + """Test updating Attributes w/ PUT.""" + + itr_uri = site.list_uri('iterable') + + itr_resp = client.create(itr_uri, name='itr1', description="test-iterable", min_val=10, max_val=20, increment=1) + itr = get_result(itr_resp) + + itr_obj_uri = site.detail_uri('iterable', id=itr['id']) +# # Update the description + params = {'description': 'Iterable 1', 'id': itr['id'], 'name': itr['name'], 'min_val': itr['min_val'], 'max_val': itr['max_val'] } + itr1 = copy.deepcopy(itr) + itr1.update(params) + + client.update(itr_obj_uri, **params), + assert_success( + client.update(itr_obj_uri, **params), + itr1 + ) + + # Reset the object back to it's initial state! + assert_success( + client.update(itr_obj_uri, **itr), + itr + ) + + +def test_partial_update(site, client): + """Test PATCH operations to partially update an Iterable.""" + + itr_uri = site.list_uri('iterable') + + itr_resp = client.create(itr_uri, name='itr1', description="test-iterable", min_val=10, max_val=20, increment=1) + itr = get_result(itr_resp) + + itr_pk_uri = site.detail_uri('iterable', id=itr['id']) + + # Update display + params = {'description': 'Iterable 1'} + payload = copy.deepcopy(itr) + payload.update(params) + + assert_success( + client.partial_update(itr_pk_uri, **params), + payload + ) + + +def test_getnext(client, site): + """ Test that the next value for the iterable is returned""" + itr_uri = site.list_uri('iterable') + + itr_resp = client.create(itr_uri, name='itr1', description="test-iterable", min_val=10, max_val=20, increment=1) + itr = get_result(itr_resp) + + itr_pk_uri = site.detail_uri('iterable', id=itr['id']) + + uri = reverse('iterable-next-value', args=(site.id, itr['id'])) + + expected = [10] # Minimum val is offered up, since no other values are assigned + + assert client.get(uri).json() == expected + + +def test_deletion(client, site): + """Test DELETE operations for Iterable.""" + itr_uri = site.list_uri('iterable') + + itr_resp = client.create(itr_uri, name='itr1', description="test-iterable", min_val=10, max_val=20, increment=1) + itr = get_result(itr_resp) + + itr_pk_uri = site.detail_uri('iterable', id=itr['id']) + assert_deleted(client.delete(itr_pk_uri)) + +def test_del_protect(client, site): + """Test DELETE Protection operations for Iterable.""" + itr_uri = site.list_uri('iterable') + + itr_resp = client.create(itr_uri, name='itr1', description="test-iterable", min_val=10, max_val=20, increment=1) + itr = get_result(itr_resp) + + itr_pk_uri = site.detail_uri('iterable', id=itr['id']) + + itrval_uri = site.list_uri('itervalue') + nval = client.get(reverse('iterable-next-value', args=(site.id, itr['id']))).json()[0] #Get the next value to assign to the itervalue + itrval_resp = client.create(itrval_uri, iterable=itr['id'], value=nval, unique_id='uuid_custA_site1') # create the iterval + + itrval_resp_dict = get_result(itrval_resp) + itrval_obj_uri = site.detail_uri('itervalue', id=itrval_resp_dict['id']) + # Don't allow delete when there's a value associated + assert_error( + client.delete(itr_pk_uri), + status.HTTP_409_CONFLICT + ) + + # Now delete the Value + client.delete(itrval_obj_uri) + + # And safely delete the Iterable + assert_deleted(client.delete(itr_pk_uri)) + + + diff --git a/tests/api_tests/test_itervalues.py b/tests/api_tests/test_itervalues.py new file mode 100644 index 00000000..c8b3de57 --- /dev/null +++ b/tests/api_tests/test_itervalues.py @@ -0,0 +1,254 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import pytest + +# Allow everything in there to access the DB +pytestmark = pytest.mark.django_db + +import copy +from django.core.urlresolvers import reverse +import json +import logging +from rest_framework import status + +from .fixtures import live_server, client, user, site +from .util import ( + assert_created, assert_error, assert_success, assert_deleted, load_json, + Client, load, filter_values, get_result +) + + +log = logging.getLogger(__name__) + +def test_creation(client, site): + """Test creation of an iterable value""" + #First create the Iterable itr1 + itr_uri = site.list_uri('iterable') + itr_resp = client.create(itr_uri, name='itr1', description="test-iterable", min_val=10, max_val=20, increment=1) + itr = get_result(itr_resp) + + #Create the attribute + attr_uri = site.list_uri('attribute') + client.create(attr_uri, resource_name='Itervalue', name='service_key') + + #Create the first value + itrval_uri = site.list_uri('itervalue') + nval = client.get(reverse('iterable-next-value', args=(site.id, itr['id']))).json()[0] #Get the next value to assign to the itervalue + itrval_resp = client.create(itrval_uri, iterable=itr['id'], value=nval, attributes={'service_key': 'custA01_key1'}) # create the iterval + + payload = get_result(itrval_resp) + itrval_obj_uri = site.detail_uri('itervalue', id=payload['id']) + assert_success(client.get(itrval_obj_uri), payload) + +def test_filters(site, client): + """Test attribute filter for Itervalue""" + itr_uri = site.list_uri('iterable') + itr_resp1 = client.create(itr_uri, name='itr1', description="test-iterable1", min_val=10, max_val=20, increment=1) + itr1 = get_result(itr_resp1) + itr_resp2 = client.create(itr_uri, name='itr2', description="test-iterable2", min_val=100, max_val=200, increment=1) + itr2 = get_result(itr_resp2) + + #Create the attribute + attr_uri = site.list_uri('attribute') + client.create(attr_uri, resource_name='Itervalue', name='service_key') + #### create itervalues for itr1 + #create the first value + itrval_uri = site.list_uri('itervalue') + nval = client.get(reverse('iterable-next-value', args=(site.id, itr1['id']))).json()[0] #get the next value to assign to the itervalue + itrval_resp = client.create(itrval_uri, iterable=itr1['id'], value=nval, attributes={'service_key': 'custa01_key1'}) # create the iterval + #create the next value + nval = client.get(reverse('iterable-next-value', args=(site.id, itr1['id']))).json()[0] #get the next value to assign to the itervalue + itrval_resp = client.create(itrval_uri, iterable=itr1['id'], value=nval, attributes={'service_key': 'custa01_key2'}) # create the iterval + #create the next value + nval = client.get(reverse('iterable-next-value', args=(site.id, itr1['id']))).json()[0] #get the next value to assign to the itervalue + itrval_resp = client.create(itrval_uri, iterable=itr1['id'], value=nval, attributes={'service_key': 'custb01_key1'}) # create the iterval + #create the next value + nval = client.get(reverse('iterable-next-value', args=(site.id, itr1['id']))).json()[0] #get the next value to assign to the itervalue + itrval_resp = client.create(itrval_uri, iterable=itr1['id'], value=nval, attributes={'service_key': 'custb01_key2'}) # create the iterval + #### create itervalues for itr2 + #create the first value + itrval_uri = site.list_uri('itervalue') + nval = client.get(reverse('iterable-next-value', args=(site.id, itr2['id']))).json()[0] #get the next value to assign to the itervalue + itrval_resp = client.create(itrval_uri, iterable=itr2['id'], value=nval, attributes={'service_key': 'custa01_key1'}) # create the iterval + #create the next value + nval = client.get(reverse('iterable-next-value', args=(site.id, itr2['id']))).json()[0] #get the next value to assign to the itervalue + itrval_resp = client.create(itrval_uri, iterable=itr2['id'], value=nval, attributes={'service_key': 'custa01_key2'}) # create the iterval + #create the next value + nval = client.get(reverse('iterable-next-value', args=(site.id, itr2['id']))).json()[0] #get the next value to assign to the itervalue + itrval_resp = client.create(itrval_uri, iterable=itr2['id'], value=nval, attributes={'service_key': 'custb01_key1'}) # create the iterval + #create the next value + nval = client.get(reverse('iterable-next-value', args=(site.id, itr2['id']))).json()[0] #get the next value to assign to the itervalue + itrval_resp = client.create(itrval_uri, iterable=itr2['id'], value=nval, attributes={'service_key': 'custb01_key2'}) # create the iterval + + + #Test lookup by attribute + expected = [ 12, 102 ] #Values assigned to custb01_key1 + returned = get_result(client.retrieve(itrval_uri, attributes='service_key=custb01_key1')) + result = [ val['value'] for val in returned ] + assert result == expected + + + +def test_update(client, site): + """Test PUT method""" + itr_uri = site.list_uri('iterable') + + itr_resp = client.create(itr_uri, name='itr1', description="test-iterable", min_val=10, max_val=20, increment=1) + itr = get_result(itr_resp) + + #Create the attribute + attr_uri = site.list_uri('attribute') + client.create(attr_uri, resource_name='Itervalue', name='service_key') + + + itr_pk_uri = site.detail_uri('iterable', id=itr['id']) + + itrval_uri = site.list_uri('itervalue') + nval = client.get(reverse('iterable-next-value', args=(site.id, itr['id']))).json()[0] #Get the next value to assign to the itervalue + itrval_resp = client.create(itrval_uri, iterable=itr['id'], value=nval, attributes={'service_key': 'custA01_key1'}) # create the iterval + + itrval_resp_dict = get_result(itrval_resp) + itrval_obj_uri = site.detail_uri('itervalue', id=itrval_resp_dict['id']) + #Update the attribute + params = {'iterable': itr['id'], 'value': nval, 'attributes': {'service_key': 'UPDATED'} } + itrval_backup = copy.deepcopy(itrval_resp_dict) + itrval_backup.update(params) + assert_success( + client.update(itrval_obj_uri, **params), + itrval_backup + ) + + #Reset the object back to it's intital state + assert_success( + client.update(itrval_obj_uri, **itrval_resp_dict), + itrval_resp_dict + ) + + + +def test_partial_update(client, site): + """Test PATCH method""" + itr_uri = site.list_uri('iterable') + + itr_resp = client.create(itr_uri, name='itr1', description="test-iterable", min_val=10, max_val=20, increment=1) + itr = get_result(itr_resp) + #Create the attribute + attr_uri = site.list_uri('attribute') + client.create(attr_uri, resource_name='Itervalue', name='service_key') + + itrval_uri = site.list_uri('itervalue') + itr_pk_uri = site.detail_uri('iterable', id=itr['id']) + nval = client.get(reverse('iterable-next-value', args=(site.id, itr['id']))).json()[0] #Get the next value to assign to the itervalue + itrval_resp = client.create(itrval_uri, iterable=itr['id'], value=nval, attributes={'service_key': 'custA01_key1'}) # create the iterval + + itrval_resp_dict = get_result(itrval_resp) + itrval_obj_uri = site.detail_uri('itervalue', id=itrval_resp_dict['id']) + #Update the U_ID + params = {'attributes': {'service_key': 'UPDATED'}} + itrval_backup = copy.deepcopy(itrval_resp_dict) + itrval_backup.update(params) + assert_success( + client.partial_update(itrval_obj_uri, **params), + itrval_backup + ) + +def test_deletion(client, site): + """Test DELETE method""" + itr_uri = site.list_uri('iterable') + + itr_resp = client.create(itr_uri, name='itr1', description="test-iterable", min_val=10, max_val=20, increment=1) + itr = get_result(itr_resp) + + #Create the attribute + attr_uri = site.list_uri('attribute') + client.create(attr_uri, resource_name='Itervalue', name='service_key') + itr_pk_uri = site.detail_uri('iterable', id=itr['id']) + + itrval_uri = site.list_uri('itervalue') + nval = client.get(reverse('iterable-next-value', args=(site.id, itr['id']))).json()[0] #Get the next value to assign to the itervalue + + itrval_resp = client.create(itrval_uri, iterable=itr['id'], value=nval, attributes={'service_key': 'custA01_key1'}) # create the iterval + itrval_resp_dict = get_result(itrval_resp) + itrval_obj_uri = site.detail_uri('itervalue', id=itrval_resp_dict['id']) + + + assert_deleted(client.delete(itrval_obj_uri)) + +def test_skey_deletion(client, site): + """Test deletion of multiple itervalues based on the service key attribute""" + + itr_uri = site.list_uri('iterable') + itr_resp1 = client.create(itr_uri, name='itr1', description="test-iterable1", min_val=10, max_val=20, increment=1) + itr1 = get_result(itr_resp1) + itr_resp2 = client.create(itr_uri, name='itr2', description="test-iterable2", min_val=100, max_val=200, increment=1) + itr2 = get_result(itr_resp2) + + #Create the attribute + attr_uri = site.list_uri('attribute') + client.create(attr_uri, resource_name='Itervalue', name='service_key') + #### create itervalues for itr1 + #create the first value + itrval_uri = site.list_uri('itervalue') + nval = client.get(reverse('iterable-next-value', args=(site.id, itr1['id']))).json()[0] #get the next value to assign to the itervalue + itrval_resp = client.create(itrval_uri, iterable=itr1['id'], value=nval, attributes={'service_key': 'custa01_key1'}) # create the iterval + #create the next value + nval = client.get(reverse('iterable-next-value', args=(site.id, itr1['id']))).json()[0] #get the next value to assign to the itervalue + itrval_resp = client.create(itrval_uri, iterable=itr1['id'], value=nval, attributes={'service_key': 'custa01_key2'}) # create the iterval + #create the next value + nval = client.get(reverse('iterable-next-value', args=(site.id, itr1['id']))).json()[0] #get the next value to assign to the itervalue + itrval_resp = client.create(itrval_uri, iterable=itr1['id'], value=nval, attributes={'service_key': 'custb01_key1'}) # create the iterval + #create the next value + nval = client.get(reverse('iterable-next-value', args=(site.id, itr1['id']))).json()[0] #get the next value to assign to the itervalue + itrval_resp = client.create(itrval_uri, iterable=itr1['id'], value=nval, attributes={'service_key': 'custb01_key2'}) # create the iterval + #### create itervalues for itr2 + #create the first value + itrval_uri = site.list_uri('itervalue') + nval = client.get(reverse('iterable-next-value', args=(site.id, itr2['id']))).json()[0] #get the next value to assign to the itervalue + itrval_resp = client.create(itrval_uri, iterable=itr2['id'], value=nval, attributes={'service_key': 'custa01_key1'}) # create the iterval + #create the next value + nval = client.get(reverse('iterable-next-value', args=(site.id, itr2['id']))).json()[0] #get the next value to assign to the itervalue + itrval_resp = client.create(itrval_uri, iterable=itr2['id'], value=nval, attributes={'service_key': 'custa01_key2'}) # create the iterval + #create the next value + nval = client.get(reverse('iterable-next-value', args=(site.id, itr2['id']))).json()[0] #get the next value to assign to the itervalue + itrval_resp = client.create(itrval_uri, iterable=itr2['id'], value=nval, attributes={'service_key': 'custb01_key1'}) # create the iterval 1 + #create the next value + nval = client.get(reverse('iterable-next-value', args=(site.id, itr2['id']))).json()[0] #get the next value to assign to the itervalue + itrval_resp = client.create(itrval_uri, iterable=itr2['id'], value=nval, attributes={'service_key': 'custb01_key2'}) # create the iterval + + + #Test lookup by attribute + expected = [ 12, 102 ] #Values assigned to custb01_key1 + returned = get_result(client.retrieve(itrval_uri, attributes='service_key=custb01_key1')) + for iv in returned: + ival_uri = site.detail_uri('itervalue', id=iv['id']) + client.delete(ival_uri) + + #Assert that all values associate with custb, key1 are gone, but cust b key1 is still around + assert get_result(client.retrieve(itrval_uri, attributes='service_key=custb01_key1')) == [] + custb_key2 = get_result(client.retrieve(itrval_uri, attributes='service_key=custb01_key2')) + result = [ val['value'] for val in custb_key2 ] + assert result == [13, 103] + + +# """Test DELETE method""" +# itr_uri = site.list_uri('iterable') +# +# itr_resp = client.create(itr_uri, name='itr1', description="test-iterable", min_val=10, max_val=20, increment=1) +# itr = get_result(itr_resp) +# +# #Create the attribute +# attr_uri = site.list_uri('attribute') +# client.create(attr_uri, resource_name='Itervalue', name='service_key') +# itr_pk_uri = site.detail_uri('iterable', id=itr['id']) +# +# itrval_uri = site.list_uri('itervalue') +# nval = client.get(reverse('iterable-next-value', args=(site.id, itr['id']))).json()[0] #Get the next value to assign to the itervalue +# +# itrval_resp = client.create(itrval_uri, iterable=itr['id'], value=nval, attributes={'service_key': 'custA01_key1'}) # create the iterval +# itrval_resp_dict = get_result(itrval_resp) +# itrval_obj_uri = site.detail_uri('itervalue', id=itrval_resp_dict['id']) +# +# +# assert_deleted(client.delete(itrval_obj_uri)) +# diff --git a/tests/api_tests/test_networks.py b/tests/api_tests/test_networks.py index 061eef22..66c864a8 100644 --- a/tests/api_tests/test_networks.py +++ b/tests/api_tests/test_networks.py @@ -647,7 +647,6 @@ def test_get_next_detail_routes(site, client): natural_uri = reverse( 'network-next-address', args=(site.id, mkcidr(net_25)) ) - # A single /32 assert_success(client.retrieve(uri), [u'10.16.2.3/32']) assert_success(client.retrieve(natural_uri), [u'10.16.2.3/32']) diff --git a/tests/model_tests/test_iterables.py b/tests/model_tests/test_iterables.py new file mode 100644 index 00000000..44e2d73c --- /dev/null +++ b/tests/model_tests/test_iterables.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import pytest +# Allow everything in there to access the DB +pytestmark = pytest.mark.django_db + +from django.db import IntegrityError +from django.db.models import ProtectedError +from django.core.exceptions import (ValidationError as DjangoValidationError, + MultipleObjectsReturned) +import logging + +from nsot import exc, models + +from .fixtures import admin_user, user, site, transactional_db + + +def test_creation(site): + itr = models.Iterable.objects.create( + site = site, + name='test-vlan', + description='test vlan for testing', + min_val = 50, + max_val = 70, + increment = 2, + ) + + iterable = models.Iterable.objects.all() + + assert iterable.count() == 1 + assert iterable[0].id == itr.id + assert iterable[0].site_id == site.id + assert iterable[0].name == itr.name + assert iterable[0].min_val == itr.min_val + assert iterable[0].max_val == itr.max_val + assert iterable[0].increment == itr.increment + + +def test_nextval(site): + itr = models.Iterable.objects.create( + name='auto-increment-test', + description='test vlan for testing', + min_val = 50, + max_val = 70, + increment = 2, + site = site + ) + #Create the Attribute + models.Attribute.objects.create( + site=site, + resource_name='Itervalue', name='service_key' + ) + #Create a test value and assign the min val to it + itrv1 = models.Itervalue.objects.create( + value = itr.get_next_value()[0], + iterable=itr, + attributes={'service_key': 'cust01A_key1'}, + site=site + ) + + + #Now, assert that the next value is last assigned + incr + assert itr.get_next_value() == [52] + +def test_valrange(site): + itr = models.Iterable.objects.create( + name='auto-increment-test', + description='test vlan for testing', + min_val = 10, + max_val = 15, + increment = 10, + site = site + ) + #Create an attribute for the Itervalue + models.Attribute.objects.create( + site=site, + resource_name='Itervalue', name='service_key' + ) + + #Create a test value and assign the min val to it + itrv0 = models.Itervalue.objects.create( + value = 10, + iterable=itr, + attributes={'service_key': 'custA01_key1'}, + site=site + ) + + #Now, assert that the next value is last assigned + incr exceeds range is not assigned + with pytest.raises(exc.ValidationError): + assert itr.get_next_value() == [25] + + + #Catch the exception, if increment is > max_val + with pytest.raises(exc.ValidationError): + itr1 = models.Iterable.objects.create( + name='increment-below-min', + description='test vlan for testing', + min_val = 10, + max_val = 15, + increment = 16, + site = site + ) + +def test_save(site): + iterable = models.Iterable.objects.create( + name='testsave', + description='testsave Iterable', + min_val = 50, + max_val = 70, + increment = 2, + site = site + ) + + iterable.save() + +def test_deletion(site): + iterable = models.Iterable.objects.create( + name='test2', + description='test2 Iterable', + min_val = 50, + max_val = 70, + increment = 2, + site = site + ) + + iterable.delete() + diff --git a/tests/model_tests/test_itervalues.py b/tests/model_tests/test_itervalues.py new file mode 100644 index 00000000..5df572fe --- /dev/null +++ b/tests/model_tests/test_itervalues.py @@ -0,0 +1,306 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import pytest +# Allow everything in there to access the DB +pytestmark = pytest.mark.django_db + +from django.db import IntegrityError +from django.db.models import ProtectedError +from django.core.exceptions import (ValidationError as DjangoValidationError, + MultipleObjectsReturned) +import logging + +from nsot import exc, models + +from .fixtures import admin_user, user, site, transactional_db + + +def test_creation(site): + itr = models.Iterable.objects.create( + name='test-vlan', + description='test vlan for testing', + min_val = 1, + max_val = 70, + increment = 2, + site = site + ) + #Create the Attribute + models.Attribute.objects.create( + site=site, + resource_name='Itervalue', name='service_key' + ) + + + itrv1 = models.Itervalue.objects.create( + iterable=itr, + site=site, + attributes={'service_key': 'skey_custA_01'} + ) + + + assert itrv1.iterable.id == itr.id + assert itrv1.get_attributes() == {'service_key': 'skey_custA_01'} + #Validate per tests in test_devices + itrv1.set_attributes({}) + assert itrv1.get_attributes() == {} + + with pytest.raises(exc.ValidationError): + itrv1.set_attributes(None) + + with pytest.raises(exc.ValidationError): + itrv1.set_attributes({0: 'value'}) + + with pytest.raises(exc.ValidationError): + itrv1.set_attributes({'key': 0}) + + with pytest.raises(exc.ValidationError): + itrv1.set_attributes({'made_up': 'value'}) + +def test_getnext(site): + itr = models.Iterable.objects.create( + name='test-vlan', + description='test vlan for testing', + min_val = 5, + max_val = 70, + increment = 2, + site = site + ) + itr2 = models.Iterable.objects.create( + name='test-vrf', + description='test vrf for testing', + min_val = 1200, + max_val = 2200, + increment = 100, + site = site + ) + #Create the Attribute + models.Attribute.objects.create( + site=site, + resource_name='Itervalue', name='service_key' + ) + itrv1 = models.Itervalue.objects.create( + value = itr.get_next_value()[0], + attributes={'service_key': 'skey_custA_01'}, + iterable=itr, + site=site + ) + itrv2 = models.Itervalue.objects.create( + value = itr.get_next_value()[0], + attributes={'service_key': 'skey_custA_02'}, + iterable=itr, + site=site + ) + itrv3 = models.Itervalue.objects.create( + value = itr2.get_next_value()[0], + attributes={'service_key': 'skey_custB_01'}, + iterable=itr2, + site=site + ) + itrv4 = models.Itervalue.objects.create( + value = itr2.get_next_value()[0], + attributes={'service_key': 'skey_custB_02'}, + iterable=itr2, + site=site + ) + + assert itr.get_next_value()[0] == 9 + assert itrv2.value == 7 + assert itrv4.value == 1300 + +def test_retrive(site): + itr = models.Iterable.objects.create( + name='test-vlan', + description='test vlan for testing', + min_val = 150, + max_val = 170, + increment = 2, + site = site + ) + itr2 = models.Iterable.objects.create( + name='test-vrf', + description='test vrf for testing', + min_val = 1200, + max_val = 2200, + increment = 100, + site = site + ) + #Create the Attribute + models.Attribute.objects.create( + site=site, + resource_name='Itervalue', name='service_key' + ) + itrv1 = models.Itervalue.objects.create( + value = itr.get_next_value()[0], + attributes={'service_key': 'skey_custA_01'}, + iterable=itr, + site=site + ) + itrv2 = models.Itervalue.objects.create( + value = itr.get_next_value()[0], + attributes={'service_key': 'skey_custA_02'}, + iterable=itr, + site=site + ) + itrv3 = models.Itervalue.objects.create( + value = itr2.get_next_value()[0], + attributes={'service_key': 'skey_custB_01'}, + iterable=itr2, + site=site + ) + itrv4 = models.Itervalue.objects.create( + value = itr2.get_next_value()[0], + attributes={'service_key': 'skey_custB_02'}, + iterable=itr2, + site=site + ) + + assert list(site.itervalue.all()) == [itrv1, itrv2, itrv3, itrv4] + + #Retrive by attribute + assert list(site.itervalue.by_attribute('service_key', 'skey_custB_02')) == [ itrv4 ] + assert list(site.itervalue.by_attribute('service_key', 'skey_custB_01')) == [ itrv3 ] + assert list(site.itervalue.by_attribute('service_key', 'skey_custA_02')) == [ itrv2 ] + assert list(site.itervalue.by_attribute('service_key', 'skey_custA_01')) == [ itrv1 ] + +def test_save(site): + itr = models.Iterable.objects.create( + name='test-vlan', + description='test vlan for testing', + min_val = 22, + max_val = 99, + increment = 2, + site = site + ) + #Create the Attribute + models.Attribute.objects.create( + site=site, + resource_name='Itervalue', name='service_key' + ) + + itrvX = models.Itervalue.objects.create( + value = itr.get_next_value()[0], + attributes={'service_key': 'skey_custX_01'}, + iterable=itr, + site=site + ) + itrvX.save() + +def test_delete(site): + "Delete all rows in Itervalues given the service identifier criteria" + itr = models.Iterable.objects.create( + name='test-vlan', + description='test vlan for testing', + min_val = 50, + max_val = 70, + increment = 2, + site = site + ) + #Create the Attribute + models.Attribute.objects.create( + site=site, + resource_name='Itervalue', name='service_key' + ) + + itrv1 = models.Itervalue.objects.create( + value = itr.get_next_value()[0], + attributes={'service_key': 'skey_custB_02'}, + iterable=itr, + site=site + ) + site.itervalue.by_attribute('service_key', 'skey_custB_02').delete() + +def test_duplicate_values(site): + "Test that duplicate itervalues cannot exist" + itr = models.Iterable.objects.create( + name='test-vlan', + description='test vlan for testing', + min_val = 500, + max_val = 700, + increment = 2, + site = site + ) + #Create the Attribute + models.Attribute.objects.create( + site=site, + resource_name='Itervalue', name='service_key' + ) + + itrv1 = models.Itervalue.objects.create( + value = itr.get_next_value()[0], #This should assign the value 50 + attributes={'service_key': 'skey_custA_01'}, + iterable=itr, + site=site + + ) + itrv1.save() + + itrv2 = models.Itervalue.objects.create( + value = 500, #Try to manually assign duplicate value + attributes={'service_key': 'skey_custA_02'}, + iterable=itr, + site=site + ) + with pytest.raises(exc.ValidationError): + itrv2.save() #The model should catch the dupe and raise an E + + site.itervalue.by_attribute('service_key', 'skey_custA_01').delete() + + + +def test_protected_delete(site): + "Delete all rows in Itervalues given the service identifier criteria" + itr = models.Iterable.objects.create( + name='test-vlan', + description='test vlan for testing', + min_val = 50, + max_val = 70, + increment = 2, + site = site + ) + itr2 = models.Iterable.objects.create( + name='test-vrf', + description='test vrf for testing', + min_val = 1200, + max_val = 2200, + increment = 100, + site = site + ) + + #Create the Attribute + models.Attribute.objects.create( + site=site, + resource_name='Itervalue', name='service_key' + ) + + + itrv1 = models.Itervalue.objects.create( + value = itr.get_next_value()[0], + attributes={'service_key': 'skey_custA_01'}, + iterable=itr, + site=site + + ) + itrv2 = models.Itervalue.objects.create( + value = itr.get_next_value()[0], + attributes={'service_key': 'skey_custA_02'}, + iterable=itr, + site=site + + ) + itrv3 = models.Itervalue.objects.create( + value = itr2.get_next_value()[0], + attributes={'service_key': 'skey_custB_01'}, + iterable=itr2, + site=site + + ) + itrv4 = models.Itervalue.objects.create( + value = itr2.get_next_value()[0], + attributes={'service_key': 'skey_custB_02'}, + iterable=itr2, + site=site + + ) + with pytest.raises(exc.ProtectedError): + models.Iterable.objects.all().delete()