diff --git a/nsot/api/views.py b/nsot/api/views.py index 5aec8c4d..499fe8ca 100644 --- a/nsot/api/views.py +++ b/nsot/api/views.py @@ -662,7 +662,6 @@ def get_serializer_class(self): return serializers.InterfaceUpdateSerializer if self.request.method == 'PATCH': return serializers.InterfacePartialUpdateSerializer - return self.serializer_class @detail_route(methods=['get']) @@ -670,7 +669,6 @@ def addresses(self, request, pk=None, site_pk=None, *args, **kwargs): """Return a list of addresses for this Interface.""" interface = self.get_resource_object(pk, site_pk) addresses = interface.addresses.all() - return self.list(request, queryset=addresses, *args, **kwargs) @detail_route(methods=['get']) @@ -678,16 +676,57 @@ def assignments(self, request, pk=None, site_pk=None, *args, **kwargs): """Return a list of information about my assigned addresses.""" interface = self.get_resource_object(pk, site_pk) assignments = interface.assignments.all() - return self.list(request, queryset=assignments, *args, **kwargs) @detail_route(methods=['get']) def networks(self, request, pk=None, site_pk=None, *args, **kwargs): """Return all the containing Networks for my assigned addresses.""" interface = self.get_resource_object(pk, site_pk) - return self.list(request, queryset=interface.networks, *args, **kwargs) + @detail_route(methods=['get']) + def parent(self, request, pk=None, site_pk=None, *args, **kwargs): + """Return the parent of this Interface.""" + interface = self.get_resource_object(pk, site_pk) + parent = interface.parent + if parent is not None: + pk = interface.parent_id + else: + pk = None + return self.retrieve(request, pk, site_pk, *args, **kwargs) + + @detail_route(methods=['get']) + def ancestors(self, request, pk=None, site_pk=None, *args, **kwargs): + """Return all the ancestors of this Interface.""" + interface = self.get_resource_object(pk, site_pk) + return self.list(request, queryset=interface.get_ancestors(), *args, **kwargs) + + @detail_route(methods=['get']) + def children(self, request, pk=None, site_pk=None, *args, **kwargs): + """Return all the immediate children of this Interface.""" + interface = self.get_resource_object(pk, site_pk) + return self.list(request, queryset=interface.get_children(), *args, **kwargs) + + @detail_route(methods=['get']) + def descendants(self, request, pk=None, site_pk=None, *args, **kwargs): + """Return all the descendants of this Interface.""" + interface = self.get_resource_object(pk, site_pk) + return self.list(request, queryset=interface.get_descendants(), *args, **kwargs) + + @detail_route(methods=['get']) + def siblings(self, request, pk=None, site_pk=None, *args, **kwargs): + """Return all the siblings of this Interface.""" + interface = self.get_resource_object(pk, site_pk) + return self.list(request, queryset=interface.get_siblings(), *args, **kwargs) + + @detail_route(methods=['get']) + def root(self, request, pk=None, site_pk=None, *args, **kwargs): + """Return the root of the tree this Interface is part of.""" + interface = self.get_resource_object(pk, site_pk) + root = interface.get_root() + pk = root.id + return self.retrieve(request, pk, site_pk, *args, **kwargs) + @detail_route(methods=['get']) def circuit(self, request, pk=None, site_pk=None, *args, **kwargs): """Return the Circuit I am associated with""" diff --git a/nsot/models.py b/nsot/models.py index 8f10e5a6..18fd6e48 100644 --- a/nsot/models.py +++ b/nsot/models.py @@ -1239,7 +1239,7 @@ class Interface(Resource): ) parent = fields.ChainedForeignKey( - 'nsot.Interface', blank=True, null=True, related_name='children', + 'self', blank=True, null=True, related_name='children', default=None, db_index=True, on_delete=models.PROTECT, chained_field='device', chained_model_field='device', verbose_name='Parent', help_text='Unique ID of the parent Interface.', @@ -1396,8 +1396,45 @@ def set_addresses(self, addresses, overwrite=False, partial=False): self.clean_addresses() + def get_ancestors(self): + """Return all ancestors of an Interface.""" + p = self.parent + ancestors = [] + while p is not None: + ancestors.append(p) + p = p.parent + ancestor_ids = [a.id for a in ancestors] + return Interface.objects.filter(id__in=ancestor_ids) + + def get_children(self): + """Return the immediate children of an Interface.""" + return Interface.objects.filter(parent=self) + + def get_descendants(self): + """Return all the descendants of an Interface.""" + s = list(self.get_children()) + descendants = [] + while len(s) > 0: + top = s.pop() + descendants.append(top) + for c in top.get_children(): + s.append(c) + descendant_ids = [c.id for c in descendants] + return Interface.objects.filter(id__in=descendant_ids) + + def get_root(self): + """Return the parent of all ancestors of an Interface.""" + root = self + while root.parent is not None: + root = root.parent + return root + + def get_siblings(self): + """Return Interfaces with the same parent and device id as an Interface.""" + return Interface.objects.filter(parent=self.parent, device=self.device).exclude(id=self.id) + def get_assignments(self): - """Return a list of informatoin about my assigned addresses.""" + """Return a list of information about my assigned addresses.""" return [a.to_dict() for a in self.assignments.all()] def get_addresses(self): @@ -1473,6 +1510,16 @@ def clean_device_hostname(self, device): """Extract hostname from device""" return device.hostname + def clean_parent(self, parent): + if parent is None: + return parent + if parent.device_hostname != self.device_hostname: + raise exc.ValidationError({ + 'parent': "Parent's device does not match device with host name %r"%self.device_hostname + }) + return parent + + def clean_fields(self, exclude=None): self.site_id = self.clean_site(self.site_id) self.name = self.clean_name(self.name) @@ -1480,6 +1527,8 @@ def clean_fields(self, exclude=None): self.speed = self.clean_speed(self.speed) self.mac_address = self.clean_mac_address(self.mac_address) self.device_hostname = self.clean_device_hostname(self.device) + self.parent = self.clean_parent(self.parent) + def save(self, *args, **kwargs): # We don't want to validate unique because we want the IntegrityError diff --git a/tests/api_tests/test_interfaces.py b/tests/api_tests/test_interfaces.py index 5b28a454..cffb645a 100644 --- a/tests/api_tests/test_interfaces.py +++ b/tests/api_tests/test_interfaces.py @@ -31,6 +31,9 @@ def test_creation(site, client): dev_resp = client.create(dev_uri, hostname='foo-bar1') dev = get_result(dev_resp) + dev_resp1 = client.create(dev_uri, hostname='foo-bar2') + dev1 = get_result(dev_resp1) + net_resp = client.create(net_uri, cidr='10.1.1.0/24') net = get_result(net_resp) @@ -56,6 +59,13 @@ def test_creation(site, client): assert_created(ifc1_resp, ifc1_obj_uri) + # Verify that creating a device with parent as + # ifc1 but device as foo-bar2 will cause error + assert_error( + client.create(ifc_uri, device=dev1['id'], name='eth0.1', parent_id=ifc1['id']), + status.HTTP_400_BAD_REQUEST + ) + # Make sure MAC is None assert ifc1['mac_address'] is None @@ -83,6 +93,128 @@ def test_creation(site, client): assert_success(client.get(ifc_uri), expected) +def test_tree_traversal(site, client): + """Test basic creation of an Interface.""" + ifc_uri = site.list_uri('interface') + dev_uri = site.list_uri('device') + + dev_resp = client.create(dev_uri, hostname='foo-bar1') + dev = get_result(dev_resp) + + dev_resp1 = client.create(dev_uri, hostname='foo-bar2') + dev1 = get_result(dev_resp1) + + ifc1_resp = client.create( + ifc_uri, device=dev['id'], name='eth0', parent_id=None, + mac_address=None, + ) + ifc1 = get_result(ifc1_resp) + ifc1_obj_uri = site.detail_uri('interface', id=ifc1['id']) + + assert_created(ifc1_resp, ifc1_obj_uri) + + # Create another interface with ifc1 as parent + ifc2_resp = client.create( + ifc_uri, device=dev['id'], name='eth0.0', parent_id=ifc1['id'], + mac_address=None + ) + ifc2 = get_result(ifc2_resp) + ifc2_obj_uri = site.detail_uri('interface', id=ifc2['id']) + + assert_created(ifc2_resp, ifc2_obj_uri) + + # Create another interface with ifc2 as parent + ifc3_resp = client.create( + ifc_uri, device = dev['id'], name='eth0.1', parent_id=ifc2['id'], + mac_address = None + ) + ifc3 = get_result(ifc3_resp) + ifc3_obj_uri = site.detail_uri('interface', id = ifc3['id']) + + assert_created(ifc3_resp, ifc3_obj_uri) + + # Create another interface with ifc2 as parent + ifc4_resp = client.create( + ifc_uri, device = dev['id'], name='eth0.2', parent_id=ifc2['id'], + mac_address = None + ) + ifc4 = get_result(ifc4_resp) + ifc4_obj_uri = site.detail_uri('interface', id = ifc4['id']) + + assert_created(ifc4_resp, ifc4_obj_uri) + + # Create another interface with ifc2 as parent + ifc5_resp = client.create( + ifc_uri, device = dev['id'], name='eth0.3', parent_id=ifc2['id'], + mac_address = None + ) + + ifc5 = get_result(ifc5_resp) + ifc5_obj_uri = site.detail_uri('interface', id = ifc5['id']) + + assert_created(ifc5_resp, ifc5_obj_uri) + + ifc6_resp = client.create( + ifc_uri, device = dev1['id'], name='eth0.4', parent_id=None, + mac_address = None + ) + + ifc6 = get_result(ifc6_resp) + ifc6_obj_uri = site.detail_uri('interface', id = ifc6['id']) + + assert_created(ifc6_resp, ifc6_obj_uri) + + ifc7_resp = client.create( + ifc_uri, device = dev1['id'], name='eth0.5', parent_id=None, + mac_address = None + ) + + ifc7 = get_result(ifc7_resp) + ifc7_obj_uri = site.detail_uri('interface', id = ifc7['id']) + + assert_created(ifc7_resp, ifc7_obj_uri) + + # test Ancestors by calling it on ifc3 + expected = [ifc1, ifc2] + uri = reverse('interface-ancestors', args = (site.id, ifc3['id'])) + assert_success(client.retrieve(uri), expected) + + # test children by calling it on ifc2 + expected = [ifc3, ifc4, ifc5] + uri = reverse('interface-children', args = (site.id, ifc2['id'])) + assert_success(client.retrieve(uri), expected) + + # test descendants by calling it on ifc1 + expected = [ifc2, ifc3, ifc4, ifc5] + uri = reverse('interface-descendants', args = (site.id, ifc1['id'])) + assert_success(client.retrieve(uri), expected) + + # test siblings by calling it on ifc3 + expected = [ifc4, ifc5] + uri = reverse('interface-siblings', args = (site.id, ifc3['id'])) + assert_success(client.retrieve(uri), expected) + + # test root by calling it on ifc4 + expected = ifc1 + uri = reverse('interface-root', args=(site.id, ifc4['id'])) + assert_success(client.retrieve(uri), expected) + + # test that root of ifc1 is ifc1 + expected = ifc1 + uri = reverse('interface-root', args=(site.id, ifc1['id'])) + assert_success(client.retrieve(uri), expected) + + # test parent by calling it on ifc5 + expected = ifc2 + uri = reverse('interface-parent', args=(site.id, ifc5['id'])) + assert_success(client.retrieve(uri), expected) + + # test sibling for interfaces with None as parent and attached to different devices + expected = [ifc7] + uri = reverse('interface-siblings', args=(site.id, ifc6['id'])) + assert_success(client.retrieve(uri), expected) + + def test_creation_with_addresses(site, client): """Test creating an Interface w/ addresses.""" ifc_uri = site.list_uri('interface') diff --git a/tests/model_tests/test_interfaces.py b/tests/model_tests/test_interfaces.py index dac4972c..440cec1f 100644 --- a/tests/model_tests/test_interfaces.py +++ b/tests/model_tests/test_interfaces.py @@ -32,6 +32,50 @@ def test_creation(device): iface.save() +def test_tree_methods(device): + iface = models.Interface.objects.create( + device = device, name = 'eth0' + ) + iface1 = models.Interface.objects.create( + device = device, name = 'eth0.0', parent = iface + ) + iface2 = models.Interface.objects.create( + device = device, name = 'eth0.1', parent = iface + ) + iface3 = models.Interface.objects.create( + device = device, name = 'eth0.2', parent = iface1 + ) + iface4 = models.Interface.objects.create( + device = device, name = 'eth0.3', parent = iface + ) + assert iface1.parent.id is iface.id + assert iface3.get_root().id is iface.id + + children = [x.id for x in iface.get_children()] + expected = [iface1.id, iface2.id, iface4.id] + children.sort() + expected.sort() + assert children == expected + + descendants = [x.id for x in iface.get_descendants()] + expected = [iface1.id, iface2.id, iface3.id, iface4.id] + expected.sort() + descendants.sort() + assert descendants == expected + + ancestors = [x.id for x in iface3.get_ancestors()] + expected = [iface1.id, iface.id] + expected.sort() + ancestors.sort() + assert ancestors == expected + + siblings = [x.id for x in iface4.get_siblings()] + expected = [iface1.id, iface2.id] + siblings.sort() + expected.sort() + assert siblings == expected + + def test_speed(device): """Test interface speed.""" iface = models.Interface.objects.create(device=device, name='eth0')