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]]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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]]
+
+
+
+
+
+
+
+
+
+
+ [[updateError.code]] - [[updateError.message]]
+
+
+
+
+
+
+
+
+
+ [[deleteError.code]] - [[deleteError.message]]
+
+ Are you sure you want to delete this iterable?
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+ [[error.code]] - [[error.message]]
+
+
+
+
+
+
+
+
+ Iterables
+
+ No Iterables
+
+
+
+
+
+ | Name |
+ Description |
+ Min value |
+ Max value |
+ incrementor |
+
+
+
+
+ | [[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()