diff --git a/nsot/admin.py b/nsot/admin.py index 9026a29..31ad78b 100644 --- a/nsot/admin.py +++ b/nsot/admin.py @@ -99,3 +99,9 @@ class InterfaceAdmin(GuardedModelAdmin): 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) diff --git a/nsot/api/filters.py b/nsot/api/filters.py index f1af978..ce0c020 100644 --- a/nsot/api/filters.py +++ b/nsot/api/filters.py @@ -155,3 +155,8 @@ class CircuitFilter(ResourceFilter): class Meta: model = models.Circuit fields = ['endpoint_a', 'endpoint_z', 'name', 'attributes'] + +class IterableFilter(ResourceFilter): + class Meta: + model = models.Iterable + fields = ['name', 'attributes'] diff --git a/nsot/api/serializers.py b/nsot/api/serializers.py index 32da577..1ce69f5 100644 --- a/nsot/api/serializers.py +++ b/nsot/api/serializers.py @@ -612,3 +612,58 @@ def validate(self, attrs): else: msg = 'Must include "email" and "secret_key"' raise exc.ValidationError(msg) + +########### +# Iterables +########### + +class IterableSerializer(ResourceSerializer): + """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') + ) + #parent = fields.IntegerField( + #label = get_field_attr(models.Iterable, 'parent', 'name'), + #help_text = get_field_attr(models.Iterable, 'parent', 'help_text') + #) + + class Meta: + model = models.Iterable + fields = ( 'name', 'description', 'value', 'parent', + 'min_val', 'max_val', 'increment', 'site_id', 'attributes') + #fields = '__all__' + + +class IterableUpdateSerializer(BulkSerializerMixin, + IterableCreateSerializer): + """ Used for PUT on Iterables. """ + attributes = JSONDictField( + required=True, + help_text='Dictionary of attributes to set.' + ) + + class Meta: + model = models.Iterable + list_serializer_class = BulkListSerializer + fields = ('id', 'name', 'description', 'value', + 'min_val', 'max_val', 'increment', 'attributes') + #fields = '__all__' + + +class IterablePartialUpdateSerializer(BulkSerializerMixin, + IterableCreateSerializer): + """ Used for PATCH, on Iterables. """ + class Meta: + model = models.Iterable + list_serializer_class = BulkListSerializer + fields = ('id', 'name', 'description', 'value', + 'min_val', 'max_val', 'increment', 'attributes') + #fields = '__all__' \ No newline at end of file diff --git a/nsot/api/urls.py b/nsot/api/urls.py index eb85f4d..f6b60c7 100644 --- a/nsot/api/urls.py +++ b/nsot/api/urls.py @@ -19,8 +19,10 @@ router.register(r'interfaces', views.InterfaceViewSet) router.register(r'networks', views.NetworkViewSet) router.register(r'users', views.UserViewSet) +router.register(r'iterables', views.IterableViewSet) router.register(r'values', views.ValueViewSet) + # Nested router for resources under /sites sites_router = routers.BulkNestedRouter( router, r'sites', lookup='site', trailing_slash=settings.APPEND_SLASH @@ -34,6 +36,7 @@ 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) # 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 a5bef72..a19a68c 100644 --- a/nsot/api/views.py +++ b/nsot/api/views.py @@ -970,3 +970,32 @@ def post(self, request, *args, **kwargs): ('data', True), ]) ) +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', 'attributes') + filter_class = filters.IterableFilter + 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 value from this Iterable""" + dynamicresource = self.get_resource_object(pk, site_pk) + value = dynamicresource.get_next_value() + return self.success(value) \ No newline at end of file diff --git a/nsot/migrations/0036_auto_20171006_0118.py b/nsot/migrations/0036_auto_20171006_0118.py new file mode 100644 index 0000000..dbdc60f --- /dev/null +++ b/nsot/migrations/0036_auto_20171006_0118.py @@ -0,0 +1,50 @@ +# -*- 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', '0035_fix_interface_name_slug'), + ] + + operations = [ + migrations.CreateModel( + name='Iterable', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('_attributes_cache', django_extensions.db.fields.json.JSONField(help_text='Local cache of attributes. (Internal use only)', blank=True)), + ('name', models.CharField(help_text='The name of the Iterable.', max_length=255, db_index=True)), + ('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='Value to increment the Iterable.')), + ('is_resource', models.BooleanField(default=False, help_text='Will this resource have children', db_index=True)), + ('value', models.IntegerField(help_text='Current Value of Iterable', null=True)), + ('parent', models.ForeignKey(related_name='children', on_delete=django.db.models.deletion.PROTECT, default=None, blank=True, to='nsot.Iterable', help_text='The parent DynamicResouce', null=True)), + ('site', 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 assigned to this Iterable')), + ], + ), + 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'), ('Iterable', 'Iterable'), ('Network', 'Network'), ('Circuit', 'Circuit')]), + ), + 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, editable=False), + ), + migrations.AlterUniqueTogether( + name='iterable', + unique_together=set([('site', 'name', 'value', 'parent')]), + ), + migrations.AlterIndexTogether( + name='iterable', + index_together=set([('site', 'name', 'value', 'parent')]), + ), + ] diff --git a/nsot/migrations/0037_auto_20171006_0914.py b/nsot/migrations/0037_auto_20171006_0914.py new file mode 100644 index 0000000..5138fe7 --- /dev/null +++ b/nsot/migrations/0037_auto_20171006_0914.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('nsot', '0036_auto_20171006_0118'), + ] + + operations = [ + 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'), ('Site', 'Site'), ('Interface', 'Interface'), ('Circuit', 'Circuit'), ('Device', 'Device'), ('Iterable', 'Iterable')]), + ), + 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'), ('Site', 'Site'), ('Interface', 'Interface'), ('Circuit', 'Circuit'), ('Device', 'Device'), ('Iterable', 'Iterable')]), + ), + ] diff --git a/nsot/migrations/0038_remove_iterable_is_resource.py b/nsot/migrations/0038_remove_iterable_is_resource.py new file mode 100644 index 0000000..a178da7 --- /dev/null +++ b/nsot/migrations/0038_remove_iterable_is_resource.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('nsot', '0037_auto_20171006_0914'), + ] + + operations = [ + migrations.RemoveField( + model_name='iterable', + name='is_resource', + ), + ] diff --git a/nsot/migrations/0039_auto_20171006_1021.py b/nsot/migrations/0039_auto_20171006_1021.py new file mode 100644 index 0000000..31740ed --- /dev/null +++ b/nsot/migrations/0039_auto_20171006_1021.py @@ -0,0 +1,20 @@ +# -*- 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', '0038_remove_iterable_is_resource'), + ] + + operations = [ + migrations.AlterField( + model_name='iterable', + name='parent', + field=models.ForeignKey(related_name='children', on_delete=django.db.models.deletion.PROTECT, default=None, blank=True, to='nsot.Iterable', help_text='The parent Iterable', null=True), + ), + ] diff --git a/nsot/models.py b/nsot/models.py index b68b6af..30f76b1 100644 --- a/nsot/models.py +++ b/nsot/models.py @@ -27,7 +27,9 @@ # 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' ) RESOURCE_BY_NAME = { obj_type: idx @@ -38,7 +40,7 @@ VALID_CHANGE_RESOURCES = set(RESOURCE_BY_IDX) VALID_ATTRIBUTE_RESOURCES = set([ - 'Network', 'Device', 'Interface', 'Circuit' + 'Network', 'Device', 'Interface', 'Circuit', 'Iterable' ]) # Lists of 2-tuples of (value, option) for displaying choices in certain model @@ -1977,6 +1979,138 @@ def to_dict(self): 'constraints': self.constraints, } +class Iterable(Resource): + ''' Generic Resource for Incrementing/Non-Incrementing Pools + Example: vlans, tenant ID, etc... + min/max_val = Valid range of values for Resource + increment = step to increment Resource type: integer (ex: 1, 10, 15) + Note: queried by name of by attrs + ''' + + name = models.CharField( + max_length=255, db_index=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='Value to increment the Iterable.' + ) + site = models.ForeignKey( + Site, db_index=True, related_name='iterable', + on_delete=models.PROTECT, verbose_name='Site', + help_text='Unique ID of the Site assigned to this Iterable' + ) + + parent = models.ForeignKey( + 'self', blank=True, null=True, related_name='children', default=None, + db_index=True, on_delete=models.PROTECT, + help_text='The parent Iterable' + ) + ''' Placeholder + Can we implement something like 'is_ip' + + # is_resource = models.BooleanField( + # null=False, default=False, db_index=True, + # help_text='Will this resource have children' + # ) + ''' + + value = models.IntegerField( + null=True, + help_text='Current Value of Iterable' + ) + + def __unicode__(self): + return u'name=%s, parent=%s, value=%s, min=%s, max=%s, increment=%s, site_id: %s' % (self.name, + self.parent_id, + self.value, + self.min_val, + self.max_val, + self.increment, + self.site_id ) + + class Meta: + unique_together = ('site', 'name', 'value', 'parent') + index_together = unique_together + + + def get_next_value(self): + """ + Return next value of Iterable: + if there is no parent or no children `aka: new`, return min_val + """ + try: + children = self.get_children() + if self.value is None and self.parent is None and len(children) == 0: + current_value = self.min_val + return [current_value] + else: + current_value = children.order_by('-value').values_list('value', flat=True)[0] + + incr = self.increment + next_val = current_value + 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('Iterable value out of range - exceeded') + raise exc.ValidationError({ + 'next_val': 'Out of range' + }) + except IndexError: + return [self.min_val] + + def get_children(self): + query = Iterable.objects.all() + return query.filter(parent__id=self.id) + + + def get_default_min_max_val(self): + '''Placeholder: + should there be a method to provide min/max value if parent is provided? + example: if an iterable has a parent, + and thus obtained by using the 'get_next_value' method + the min/max could be set to current value, making it non-incrementing + like the 'is_ip' from networks + ''' + pass + + 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, + 'site_id': self.site_id, + 'parent': self.parent_id, + 'name': self.name, + 'description': self.description, + 'min_val': self.min_val, + 'max_val': self.max_val, + 'value': self.value, + 'increment': self.increment, + #'is_resource': self.is_resource, #placeholder - thinking to implement something like 'is_ip' + 'attributes': self.get_attributes() + } class Value(models.Model): """Represents a value for an attribute attached to a Resource.""" 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 0000000..ba91ca3 --- /dev/null +++ b/nsot/static/src/templates/includes/iterables-form.html @@ -0,0 +1,241 @@ +
+
+ +
+ + +
+
+
+ +
+
+ +
+ + +
+
+ +
+
+ +
+ + +
+
+ +
+ +
+ + +
+
+
+ +
+
+
+ + +
+
+
+ +

Attributes

+ +
+ +
+ +
+
+ +
+ +
+
+ + +
+
+ +
+ + + +
+
+ +
+ +
+ + \ No newline at end of file diff --git a/nsot/static/src/templates/iterable.html b/nsot/static/src/templates/iterable.html new file mode 100644 index 0000000..7c23842 --- /dev/null +++ b/nsot/static/src/templates/iterable.html @@ -0,0 +1,88 @@ + +
+ + + + + +
+ + + Iterable + + +
+
Name
+
[[iterable.name]]
+ +
Description
+
[[iterable.description]]
+ +
Minimum Value
+
[[iterable.min_val]]
+ +
Maximum Value
+
[[iterable.max_val]]
+ +
Incrementor
+
[[iterable.increment]]
+ +
Attributes
+
[[iterable.attributes]]
+ +
+ +
+
+
+ + + + + + + + + + + +
\ No newline at end of file diff --git a/nsot/static/src/templates/iterables.html b/nsot/static/src/templates/iterables.html new file mode 100644 index 0000000..42ae7ff --- /dev/null +++ b/nsot/static/src/templates/iterables.html @@ -0,0 +1,72 @@ + +
+ + + + + + + + + + +
+ + Iterables + + No Iterables + + + + + + + + + + + + + + + + + + + + + + + +
NameDescriptionMin valueMax valueIncrementAttributes
+ [[dr.name]] + [[dr.description]][[dr.min_val]][[dr.max_val]][[dr.increment]][[dr.attributes]]
+
+
+ +
+ + +
\ No newline at end of file diff --git a/nsot/templates/ui/menu.html b/nsot/templates/ui/menu.html index 6fa18d5..8c27bd0 100644 --- a/nsot/templates/ui/menu.html +++ b/nsot/templates/ui/menu.html @@ -43,6 +43,13 @@ Attributes +
  • + + Iterables + +
  • diff --git a/tests/api_tests/test_iterables.py b/tests/api_tests/test_iterables.py new file mode 100644 index 0000000..3474463 --- /dev/null +++ b/tests/api_tests/test_iterables.py @@ -0,0 +1,78 @@ +# -*- 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_iterables, get_result +) + +log = logging.getLogger(__name__) + +def test_creation(live_server, user, site): + """ Test Iterable Creation""" + admin_client = Client(live_server, 'admin') + user_client = Client(live_server, 'user') + + # URIs + site_uri = site.list_uri() + attr_uri = site.list_uri('attribute') + itr_uri = site.list_uri('iterable') + + admin_client.create(attr_uri, resource_name='Iterable', name='test1') + + + # Good Creation + itr_resp = admin_client.create( + itr_uri, name='test_iterable', + attributes={'test1': 'foo'}, + min_val=100, max_val=200, + increment=1, + ) + + itr = get_result(itr_resp) + itr_obj_url = site.detail_uri('iterable', id=itr['id']) + + assert_created(itr_resp, itr_obj_url) + + # Verify GET all() + + payload = get_result(itr_resp) + expected = [payload] + + assert_success(admin_client.get(itr_uri), expected) + + # Verify Single Iterable + assert_success(admin_client.get(itr_obj_url), itr) + + #### Errors #### + + # Permission Error + assert_error( + user_client.create( + itr_uri, name='i_will_fail', + min_val=100, max_val=101, + ), + status.HTTP_403_FORBIDDEN + ) + + # Bad Attr + assert_error( + admin_client.create( + itr_uri, name='i_will_fail', + min_val=100, max_val=101, + attributes={'test2': 'foo'} + ), + status.HTTP_400_BAD_REQUEST + ) \ No newline at end of file diff --git a/tests/api_tests/util.py b/tests/api_tests/util.py index 587d45b..c61f3b3 100644 --- a/tests/api_tests/util.py +++ b/tests/api_tests/util.py @@ -243,6 +243,19 @@ def filter_circuits(circuits, wanted): return [c for c in circuits if c in wanted] +def filter_iterables(iterables, wanted): + """ + Return a list of desired Iterable objects. + + :param iterables: + list of iterable dicts + + :param name: + list of iterable objects you want + """ + return [i for i in iterables if i in wanted] + + def filter_networks(networks, wanted): """ Return a list of desired Network objects. diff --git a/tests/model_tests/test_iterables.py b/tests/model_tests/test_iterables.py new file mode 100644 index 0000000..77ed345 --- /dev/null +++ b/tests/model_tests/test_iterables.py @@ -0,0 +1,238 @@ +# -*- 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 +import ipaddress +import logging + +from nsot import exc, models + +from .fixtures import admin_user, user, site, transactional_db + + +def test_create_basic_iterable(site): + vrf = models.Iterable.objects.create( + site=site, + name='my_vrf', + description='Dummy Test VRF', + min_val=5500, + max_val=6000, + increment=1 + ) + + basic = models.Iterable.objects.all() + + assert basic.count() == 1 + assert basic[0].id == vrf.id + assert basic[0].name == vrf.name + assert basic[0].min_val == vrf.min_val + assert basic[0].max_val == vrf.max_val + assert basic[0].increment == vrf.increment + assert basic[0].parent is None + assert basic[0].site_id == site.id + + +def test_sequential_creation(site): + vlan1 = models.Iterable.objects.create( + site=site, + name='vlan1', + description='New Test Vlan', + min_val=50, + max_val=70, + increment=1 + ) + + vlan1.refresh_from_db() + + vlan2 = models.Iterable.objects.create( + site=site, + name='vlan2', + description='Test Vlan2', + parent=vlan1, + value=50 + ) + + iterables = models.Iterable.objects.all() + + assert iterables.count() == 2 + assert iterables[0].name == vlan1.name + assert vlan2.parent_id == vlan1.id + assert vlan1.parent_id is None + assert vlan2.value == 50 + assert vlan1.value is None + + +def test_next_value(site): + iterable_1 = models.Iterable.objects.create( + site=site, + name='iterable1', + min_val=100, + max_val=1000, + increment=1, + description='My favorite iterable' + ) + + iterables = models.Iterable.objects.all() + + next_itr = iterables[0].get_next_value() + + assert iterables.count() == 1 + assert next_itr[0] == 100 + + +def test_next_value_non_incrementing(site): + iterable_1 = models.Iterable.objects.create( + site=site, + name='i_dont_increment', + min_val=100, + max_val=100, + increment=0, + description='My favorite iterable' + ) + + itrs = models.Iterable.objects.all() + + next_itr = itrs[0].get_next_value() + + assert itrs.count() == 1 + assert next_itr[0] == 100 + +''' test currently fails - need to add logic to account for + out-of-sequence numbering to prevent uneccesary exhaustion + +def test_next_value_out_of_sequence(site): + iterable_1 = models.Iterable.objects.create( + site=site, + name='iterable1', + min_val=100, + max_val=1000, + increment=1, + description='My favorite iterable' + ) + + iterable_2 = models.Iterable.objects.create( + site=site, + name='iterable2', + min_val=100, + max_val=1000, + value=104, + parent=iterable_1, + increment=1, + description='My favorite iterable' + ) + + iterable_3 = models.Iterable.objects.create( + site=site, + name='iterable3', + min_val=100, + max_val=1000, + value=100, + parent=iterable_1, + increment=1, + description='My favorite iterable' + ) + + itrs = models.Iterable.objects.all() + + next_itr = itrs[0].get_next_value() + + assert next_itr[0] == 101 +''' + +def test_delete_iterable(site): + vrf_1 = models.Iterable.objects.create( + site=site, + name='test_to_delete', + min_val=100, + max_val=1000, + increment=1, + description='test' + ) + + itrs = models.Iterable.objects.all() + + assert itrs.count() == 1 + assert itrs[0].name == vrf_1.name + + vrf_1.delete() + itrs_2 = models.Iterable.objects.all() + + assert itrs_2.count() == 0 + +def test_lookup_iterable(site): + attrs_to_create = ['service_type', 'type', 'device_segment', 'network_segment'] + for attr in attrs_to_create: + models.Attribute.objects.create( + site=site, + resource_name='Iterable', name=attr + ) + + vrf_1 = models.Iterable.objects.create( + site=site, + name='vrf_test', + min_val=100, + max_val=1000, + increment=1, + attributes={ + 'service_type': 'vrf', + 'type': 'incrementing', + 'device_segment': 'routing', + 'network_segment': 'cloud' + } + ) + + vlan_1 = models.Iterable.objects.create( + site=site, + name='vlan_test', + min_val=100, + max_val=1000, + increment=1, + attributes={ + 'service_type': 'vlan', + 'type': 'incrementing', + 'device_segment': 'switching', + 'network_segment': 'wan' + } + ) + + asset_tag_1 = models.Iterable.objects.create( + site=site, + name='asset_tag_test', + min_val=10, + max_val=9999, + increment=1, + attributes={ + 'service_type': 'cmdb', + 'type': 'incrementing', + 'device_segment': 'user', + 'network_segment': 'lan' + } + ) + + vlan_2 = models.Iterable.objects.create( + site=site, + name='vlan_test', + min_val=100, + max_val=1000, + value=100, + parent=vlan_1, + increment=1, + attributes={ + 'service_type': 'vlan', + 'type': 'incrementing', + 'device_segment': 'switching', + 'network_segment': 'wan' + } + ) + + # assert list(site.iterable.filter(parent_id=None)) == [vrf_1, vlan_1, asset_tag_1] + assert list(site.iterable.filter(parent_id=vlan_1)) == [vlan_2] + assert list(site.iterable.by_attribute('service_type', 'vlan')) == [vlan_1, vlan_2] + assert list(site.iterable.by_attribute('type', 'incrementing')) == [vrf_1, vlan_1, asset_tag_1, vlan_2] + assert vlan_1.get_next_value()[0] == 101 \ No newline at end of file