Merge lp:~mpontillo/maas/list-connected-macs-2.0 into lp:~maas-committers/maas/trunk

Proposed by Mike Pontillo
Status: Rejected
Rejected by: MAAS Lander
Proposed branch: lp:~mpontillo/maas/list-connected-macs-2.0
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 196 lines (+166/-1)
2 files modified
src/maasserver/api/subnets.py (+45/-1)
src/maasserver/api/tests/test_subnets.py (+121/-0)
To merge this branch: bzr merge lp:~mpontillo/maas/list-connected-macs-2.0
Reviewer Review Type Date Requested Status
MAAS Maintainers Pending
Review via email: mp+296601@code.launchpad.net

Commit message

Add backward compatability API to list the connected MACs on a subnet (optionally by VLAN).

Description of the change

Note: this branch is pretty much the 1.9 version of list_connected_macs, ported to Python 3 and adjusted for the changes in the API test case.

The only difference is, the new API allows the user to select whether they want to list connected MACs by IP addresses assigned to the subnet (the default) or by checking which IP addresses are assigned to subnet's associated VLAN. (the MAAS 1.8 behavior was a little like both options, and this API is just a convenience for backward compatibility; the new API offers the same data, but it would take several steps to obtain and filter.)

To post a comment you must log in.
Revision history for this message
MAAS Lander (maas-lander) wrote :

Transitioned to Git.

lp:maas has now moved from Bzr to Git.
Please propose your branches with Launchpad using Git.

git clone https://git.launchpad.net/maas

Unmerged revisions

5078. By Mike Pontillo

Add backward compatability API to list the connected MACs on a subnet (optionally by VLAN).

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/maasserver/api/subnets.py'
--- src/maasserver/api/subnets.py 2016-04-27 20:40:24 +0000
+++ src/maasserver/api/subnets.py 2016-06-06 20:00:55 +0000
@@ -13,7 +13,11 @@
13from maasserver.enum import NODE_PERMISSION13from maasserver.enum import NODE_PERMISSION
14from maasserver.exceptions import MAASAPIValidationError14from maasserver.exceptions import MAASAPIValidationError
15from maasserver.forms_subnet import SubnetForm15from maasserver.forms_subnet import SubnetForm
16from maasserver.models import Subnet16from maasserver.models import (
17 Interface,
18 Node,
19 Subnet,
20)
17from piston3.utils import rc21from piston3.utils import rc
18from provisioningserver.utils.network import IPRangeStatistics22from provisioningserver.utils.network import IPRangeStatistics
1923
@@ -224,3 +228,43 @@
224 request.GET, 'with_node_summary', True, validator=StringBool)228 request.GET, 'with_node_summary', True, validator=StringBool)
225 return subnet.render_json_for_related_ips(229 return subnet.render_json_for_related_ips(
226 with_username=with_username, with_node_summary=with_node_summary)230 with_username=with_username, with_node_summary=with_node_summary)
231
232 @operation(idempotent=True)
233 def list_connected_macs(self, request, subnet_id):
234 """Returns the list of MAC addresses connected to this network.
235
236 Only MAC addresses for nodes visible to the requesting user are
237 returned.
238
239 :param by_vlan: Optional boolean to indicate that the MACs should be
240 found via the VLAN attached to the subnet, not using assigned IP
241 addresses on the subnet. (Finding the MACs using assigned IP
242 addresses is the default.)
243 :type by_vlan: bool
244 """
245 subnet = Subnet.objects.get_object_by_specifiers_or_raise(subnet_id)
246 by_vlan = get_optional_param(
247 request.GET, 'by_vlan', default=False, validator=StringBool)
248 visible_nodes = Node.objects.get_nodes(
249 request.user, NODE_PERMISSION.VIEW,
250 from_nodes=Node.objects.all())
251 if by_vlan:
252 interfaces = Interface.objects.filter(
253 node__in=visible_nodes, vlan__subnet=subnet)
254 else:
255 interfaces = Interface.objects.filter(
256 node__in=visible_nodes, ip_addresses__subnet=subnet)
257 existing_macs = set()
258 unique_interfaces_by_mac = [
259 interface
260 for interface in interfaces
261 if (interface.mac_address not in existing_macs and
262 not existing_macs.add(interface.mac_address))
263 ]
264 unique_interfaces_by_mac = sorted(
265 unique_interfaces_by_mac,
266 key=lambda x: (x.node.hostname.lower(), x.mac_address.get_raw()))
267 return [
268 {"mac_address": str(interface.mac_address)}
269 for interface in unique_interfaces_by_mac
270 ]
227271
=== modified file 'src/maasserver/api/tests/test_subnets.py'
--- src/maasserver/api/tests/test_subnets.py 2016-05-24 21:29:53 +0000
+++ src/maasserver/api/tests/test_subnets.py 2016-06-06 20:00:55 +0000
@@ -12,6 +12,7 @@
12from django.conf import settings12from django.conf import settings
13from django.core.urlresolvers import reverse13from django.core.urlresolvers import reverse
14from maasserver.enum import (14from maasserver.enum import (
15 INTERFACE_TYPE,
15 IPADDRESS_TYPE,16 IPADDRESS_TYPE,
16 IPRANGE_TYPE,17 IPRANGE_TYPE,
17 NODE_STATUS,18 NODE_STATUS,
@@ -620,3 +621,123 @@
620 expected_result = subnet.render_json_for_related_ips(621 expected_result = subnet.render_json_for_related_ips(
621 with_username=True, with_node_summary=False)622 with_username=True, with_node_summary=False)
622 self.assertThat(result, Equals(expected_result))623 self.assertThat(result, Equals(expected_result))
624
625
626class TestListConnectedMACs(APITestCase.ForUser):
627
628 def make_interface(
629 self, subnets=None, owner=None, node=None, with_ip=True):
630 """Create a Interface.
631
632 :param subnets: Optional list of `Subnet` objects to connect the
633 interface to. If omitted, the interface will not be connected to
634 any subnets.
635 :param node: Optional node that will have this interface.
636 If omitted, one will be created.
637 :param owner: Optional owner for the node that will have this MAC
638 address. If omitted, one will be created. The node will be in
639 the "allocated" state. This parameter is ignored if a node is
640 provided.
641 :param with_ip: if True, creates an IP address on the interface. If
642 False, simply associates the Interface with the VLAN.
643 """
644 if subnets is None:
645 subnets = []
646 if owner is None:
647 owner = factory.make_User()
648 if node is None:
649 node = factory.make_Node(status=NODE_STATUS.ALLOCATED, owner=owner)
650 interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
651 if with_ip:
652 for subnet in subnets:
653 factory.make_StaticIPAddress(
654 alloc_type=IPADDRESS_TYPE.DHCP, ip="",
655 subnet=subnet, interface=interface)
656 else:
657 interface.vlan = subnets[0].vlan
658 interface.save()
659 return interface
660
661 def request_connected_macs(self, subnet, by_vlan=False):
662 """Request and return the MAC addresses attached to `subnet`.
663
664 :param by_vlan: If True, finds all MACs associated with the VLAN,
665 not just those with an IP address on the specified subnet.
666 """
667 url = reverse(
668 'subnet_handler', args=[str(subnet.cidr)])
669 response = self.client.get(url, {
670 'op': 'list_connected_macs',
671 'by_vlan': str(by_vlan),
672 })
673 self.assertEqual(http.client.OK, response.status_code)
674 return json.loads(response.content.decode(settings.DEFAULT_CHARSET))
675
676 def extract_macs(self, returned_macs):
677 """Extract the textual MAC addresses from an API response."""
678 return [item['mac_address'] for item in returned_macs]
679
680 def test_returns_connected_macs(self):
681 subnet = factory.make_Subnet()
682 interfaces = [
683 self.make_interface(subnets=[subnet], owner=self.user)
684 for _ in range(3)
685 ]
686 self.assertEqual(
687 {interface.mac_address for interface in interfaces},
688 set(self.extract_macs(self.request_connected_macs(subnet))))
689
690 def test_returns_connected_macs_by_vlan(self):
691 subnet = factory.make_Subnet()
692 interfaces = [
693 self.make_interface(
694 subnets=[subnet], owner=self.user, with_ip=False)
695 for _ in range(3)
696 ]
697 self.assertEqual(
698 {interface.mac_address for interface in interfaces},
699 set(self.extract_macs(self.request_connected_macs(
700 subnet, by_vlan=True))))
701
702 def test_ignores_unconnected_macs(self):
703 self.make_interface(
704 subnets=[factory.make_Subnet()], owner=self.user)
705 self.make_interface(subnets=[], owner=self.user)
706 self.assertEqual(
707 [],
708 self.request_connected_macs(factory.make_Subnet()))
709
710 def test_includes_MACs_for_nodes_visible_to_user(self):
711 subnet = factory.make_Subnet()
712 interface = self.make_interface(
713 subnets=[subnet], owner=self.user)
714 self.assertEqual(
715 [interface.mac_address],
716 self.extract_macs(self.request_connected_macs(subnet)))
717
718 def test_excludes_MACs_for_nodes_not_visible_to_user(self):
719 subnet = factory.make_Subnet()
720 self.make_interface(subnets=[subnet])
721 self.assertEqual([], self.request_connected_macs(subnet))
722
723 def test_returns_sorted_MACs(self):
724 subnet = factory.make_Subnet()
725 interfaces = [
726 self.make_interface(
727 subnets=[subnet], node=factory.make_Node(sortable_name=True),
728 owner=self.user)
729 for _ in range(4)
730 ]
731 # Create MACs connected to the same node.
732 interfaces = interfaces + [
733 self.make_interface(
734 subnets=[subnet], owner=self.user,
735 node=interfaces[0].node)
736 for _ in range(3)
737 ]
738 sorted_interfaces = sorted(
739 interfaces,
740 key=lambda x: (x.node.hostname.lower(), x.mac_address.get_raw()))
741 self.assertEqual(
742 [nic.mac_address.get_raw() for nic in sorted_interfaces],
743 self.extract_macs(self.request_connected_macs(subnet)))