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
1=== modified file 'src/maasserver/api/subnets.py'
2--- src/maasserver/api/subnets.py 2016-04-27 20:40:24 +0000
3+++ src/maasserver/api/subnets.py 2016-06-06 20:00:55 +0000
4@@ -13,7 +13,11 @@
5 from maasserver.enum import NODE_PERMISSION
6 from maasserver.exceptions import MAASAPIValidationError
7 from maasserver.forms_subnet import SubnetForm
8-from maasserver.models import Subnet
9+from maasserver.models import (
10+ Interface,
11+ Node,
12+ Subnet,
13+)
14 from piston3.utils import rc
15 from provisioningserver.utils.network import IPRangeStatistics
16
17@@ -224,3 +228,43 @@
18 request.GET, 'with_node_summary', True, validator=StringBool)
19 return subnet.render_json_for_related_ips(
20 with_username=with_username, with_node_summary=with_node_summary)
21+
22+ @operation(idempotent=True)
23+ def list_connected_macs(self, request, subnet_id):
24+ """Returns the list of MAC addresses connected to this network.
25+
26+ Only MAC addresses for nodes visible to the requesting user are
27+ returned.
28+
29+ :param by_vlan: Optional boolean to indicate that the MACs should be
30+ found via the VLAN attached to the subnet, not using assigned IP
31+ addresses on the subnet. (Finding the MACs using assigned IP
32+ addresses is the default.)
33+ :type by_vlan: bool
34+ """
35+ subnet = Subnet.objects.get_object_by_specifiers_or_raise(subnet_id)
36+ by_vlan = get_optional_param(
37+ request.GET, 'by_vlan', default=False, validator=StringBool)
38+ visible_nodes = Node.objects.get_nodes(
39+ request.user, NODE_PERMISSION.VIEW,
40+ from_nodes=Node.objects.all())
41+ if by_vlan:
42+ interfaces = Interface.objects.filter(
43+ node__in=visible_nodes, vlan__subnet=subnet)
44+ else:
45+ interfaces = Interface.objects.filter(
46+ node__in=visible_nodes, ip_addresses__subnet=subnet)
47+ existing_macs = set()
48+ unique_interfaces_by_mac = [
49+ interface
50+ for interface in interfaces
51+ if (interface.mac_address not in existing_macs and
52+ not existing_macs.add(interface.mac_address))
53+ ]
54+ unique_interfaces_by_mac = sorted(
55+ unique_interfaces_by_mac,
56+ key=lambda x: (x.node.hostname.lower(), x.mac_address.get_raw()))
57+ return [
58+ {"mac_address": str(interface.mac_address)}
59+ for interface in unique_interfaces_by_mac
60+ ]
61
62=== modified file 'src/maasserver/api/tests/test_subnets.py'
63--- src/maasserver/api/tests/test_subnets.py 2016-05-24 21:29:53 +0000
64+++ src/maasserver/api/tests/test_subnets.py 2016-06-06 20:00:55 +0000
65@@ -12,6 +12,7 @@
66 from django.conf import settings
67 from django.core.urlresolvers import reverse
68 from maasserver.enum import (
69+ INTERFACE_TYPE,
70 IPADDRESS_TYPE,
71 IPRANGE_TYPE,
72 NODE_STATUS,
73@@ -620,3 +621,123 @@
74 expected_result = subnet.render_json_for_related_ips(
75 with_username=True, with_node_summary=False)
76 self.assertThat(result, Equals(expected_result))
77+
78+
79+class TestListConnectedMACs(APITestCase.ForUser):
80+
81+ def make_interface(
82+ self, subnets=None, owner=None, node=None, with_ip=True):
83+ """Create a Interface.
84+
85+ :param subnets: Optional list of `Subnet` objects to connect the
86+ interface to. If omitted, the interface will not be connected to
87+ any subnets.
88+ :param node: Optional node that will have this interface.
89+ If omitted, one will be created.
90+ :param owner: Optional owner for the node that will have this MAC
91+ address. If omitted, one will be created. The node will be in
92+ the "allocated" state. This parameter is ignored if a node is
93+ provided.
94+ :param with_ip: if True, creates an IP address on the interface. If
95+ False, simply associates the Interface with the VLAN.
96+ """
97+ if subnets is None:
98+ subnets = []
99+ if owner is None:
100+ owner = factory.make_User()
101+ if node is None:
102+ node = factory.make_Node(status=NODE_STATUS.ALLOCATED, owner=owner)
103+ interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
104+ if with_ip:
105+ for subnet in subnets:
106+ factory.make_StaticIPAddress(
107+ alloc_type=IPADDRESS_TYPE.DHCP, ip="",
108+ subnet=subnet, interface=interface)
109+ else:
110+ interface.vlan = subnets[0].vlan
111+ interface.save()
112+ return interface
113+
114+ def request_connected_macs(self, subnet, by_vlan=False):
115+ """Request and return the MAC addresses attached to `subnet`.
116+
117+ :param by_vlan: If True, finds all MACs associated with the VLAN,
118+ not just those with an IP address on the specified subnet.
119+ """
120+ url = reverse(
121+ 'subnet_handler', args=[str(subnet.cidr)])
122+ response = self.client.get(url, {
123+ 'op': 'list_connected_macs',
124+ 'by_vlan': str(by_vlan),
125+ })
126+ self.assertEqual(http.client.OK, response.status_code)
127+ return json.loads(response.content.decode(settings.DEFAULT_CHARSET))
128+
129+ def extract_macs(self, returned_macs):
130+ """Extract the textual MAC addresses from an API response."""
131+ return [item['mac_address'] for item in returned_macs]
132+
133+ def test_returns_connected_macs(self):
134+ subnet = factory.make_Subnet()
135+ interfaces = [
136+ self.make_interface(subnets=[subnet], owner=self.user)
137+ for _ in range(3)
138+ ]
139+ self.assertEqual(
140+ {interface.mac_address for interface in interfaces},
141+ set(self.extract_macs(self.request_connected_macs(subnet))))
142+
143+ def test_returns_connected_macs_by_vlan(self):
144+ subnet = factory.make_Subnet()
145+ interfaces = [
146+ self.make_interface(
147+ subnets=[subnet], owner=self.user, with_ip=False)
148+ for _ in range(3)
149+ ]
150+ self.assertEqual(
151+ {interface.mac_address for interface in interfaces},
152+ set(self.extract_macs(self.request_connected_macs(
153+ subnet, by_vlan=True))))
154+
155+ def test_ignores_unconnected_macs(self):
156+ self.make_interface(
157+ subnets=[factory.make_Subnet()], owner=self.user)
158+ self.make_interface(subnets=[], owner=self.user)
159+ self.assertEqual(
160+ [],
161+ self.request_connected_macs(factory.make_Subnet()))
162+
163+ def test_includes_MACs_for_nodes_visible_to_user(self):
164+ subnet = factory.make_Subnet()
165+ interface = self.make_interface(
166+ subnets=[subnet], owner=self.user)
167+ self.assertEqual(
168+ [interface.mac_address],
169+ self.extract_macs(self.request_connected_macs(subnet)))
170+
171+ def test_excludes_MACs_for_nodes_not_visible_to_user(self):
172+ subnet = factory.make_Subnet()
173+ self.make_interface(subnets=[subnet])
174+ self.assertEqual([], self.request_connected_macs(subnet))
175+
176+ def test_returns_sorted_MACs(self):
177+ subnet = factory.make_Subnet()
178+ interfaces = [
179+ self.make_interface(
180+ subnets=[subnet], node=factory.make_Node(sortable_name=True),
181+ owner=self.user)
182+ for _ in range(4)
183+ ]
184+ # Create MACs connected to the same node.
185+ interfaces = interfaces + [
186+ self.make_interface(
187+ subnets=[subnet], owner=self.user,
188+ node=interfaces[0].node)
189+ for _ in range(3)
190+ ]
191+ sorted_interfaces = sorted(
192+ interfaces,
193+ key=lambda x: (x.node.hostname.lower(), x.mac_address.get_raw()))
194+ self.assertEqual(
195+ [nic.mac_address.get_raw() for nic in sorted_interfaces],
196+ self.extract_macs(self.request_connected_macs(subnet)))