Merge lp:~allenap/maas/node-networking-api-get-mac-addresses-and-interfaces into lp:~maas-committers/maas/trunk

Proposed by Gavin Panella
Status: Rejected
Rejected by: Gavin Panella
Proposed branch: lp:~allenap/maas/node-networking-api-get-mac-addresses-and-interfaces
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 203 lines (+191/-0)
2 files modified
src/maasserver/networking/__init__.py (+62/-0)
src/maasserver/networking/tests/test_networking.py (+129/-0)
To merge this branch: bzr merge lp:~allenap/maas/node-networking-api-get-mac-addresses-and-interfaces
Reviewer Review Type Date Requested Status
Mike Pontillo (community) Needs Fixing
Blake Rouse (community) Needs Information
Review via email: mp+268360@code.launchpad.net

Commit message

The beginnings of maasserver.networking, a new API for working with networking in MAAS.

Right now this is a place to put code that relates to the new networking model in MAAS. The maze of code relating to the existing/old networking model can be left mostly alone, and later removed or refactored to use this API. Right now the only member of this API is NodeNetworking.

To post a comment you must log in.
Revision history for this message
Blake Rouse (blake-rouse) wrote :

I am confused? Why not place this in InterfaceManager? That is new and is only use by the new model code. This really parts from the way we have implemented models.

review: Needs Information
Revision history for this message
Blake Rouse (blake-rouse) wrote :

Speaking of this InterfaceManager already has a get_interfaces_for_node. Which looks like it needs to be improved as your code gets all the nested interfaces. Makes me wonder if you would have placed this in the InterfaceManager you would have seen that it already exists.

Revision history for this message
Mike Pontillo (mpontillo) wrote :

I think we can make a good argument that models/node.py (at ~2300 lines) is too big, and responsible for a too much. (just scanning through, node.py is responsible for start/stop/release of nodes, node storage, node networking, and node DHCP mappings.) So I like the concept of a separate layer for networking, and I think we should consider whether other functionality should be moved out of the model. (Do we need a NodeStorage and NodeStateMachine class as well?)

There are never-ending arguments in software design about whether you want an "anemic" model (containing little functionality other than what is needed to map the object to a database row), so you can whatever objects you want and never worry about side effects during unit testing. Or if you want a rich model, where methods on the Node object could reach far beyond the Node, for example by creating related Interface, MACAddress, or even Subnet objects. (and thus adding side effects for those trying to unit test a Node in isolation!)

However, in this particular case (given a Node object) I think I should be able to say node.get_interfaces() and node.get_mac_addresses() without the need for an extra layer.

I spoke with you on IRC about this, and you said we could have the methods in Node turn around and call this interface. I think that would be fine (and especially useful as we move from the old model to the new model, as we could test the operations we need independently, and then switch over to the new model by changing the shim layer), but let's do that before we land this.

Also, I have some minor comments/questions below.

Sorry for the wall of text. I've been spending way too much time thinking about this MP. ;-)

review: Needs Fixing
Revision history for this message
Gavin Panella (allenap) wrote :

You're quite right, but it's Node.get_interfaces(). I've reimplemented it almost exactly! It has the recursive query too. Serves me right for wandering off on my own.

My original intent was to implement some of the new networking API where it's distinct from the old one. There's so much code in the models' modules, loose and on the managers and model classes themselves: I've long wanted to peel off some of the controller-like code and my temptation got the better of me today.

Part of my problem is that I've not been involved in this feature, during design or implementation, and I'm trying to pick it up in drips between other work. Separation was meant to make the thought process clearer.

Anyway, I'll rescind this and think again.

Unmerged revisions

4205. By Gavin Panella

Docstrings.

4204. By Gavin Panella

get_interfaces() works with nested interfaces.

4203. By Gavin Panella

NodeNetworking.get_mac_addresses() and NodeNetworking.get_interfaces().

4202. By Gavin Panella

Start of a maasserver/networking package.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added directory 'src/maasserver/networking'
2=== added file 'src/maasserver/networking/__init__.py'
3--- src/maasserver/networking/__init__.py 1970-01-01 00:00:00 +0000
4+++ src/maasserver/networking/__init__.py 2015-08-18 17:11:19 +0000
5@@ -0,0 +1,62 @@
6+# Copyright 2012-2015 Canonical Ltd. This software is licensed under the
7+# GNU Affero General Public License version 3 (see the file LICENSE).
8+
9+"""MAAS Networking."""
10+
11+from __future__ import (
12+ absolute_import,
13+ print_function,
14+ unicode_literals,
15+ )
16+
17+str = None
18+
19+__metaclass__ = type
20+__all__ = [
21+ "NodeNetworking",
22+]
23+
24+from textwrap import dedent
25+
26+from maasserver.models.interface import Interface
27+
28+
29+class NodeNetworking:
30+ """Query and modify a node's network configuration."""
31+
32+ def __init__(self, node):
33+ super(NodeNetworking, self).__init__()
34+ self.node = node
35+
36+ def get_mac_addresses(self):
37+ """Obtain the set of MAC addresses for this node.
38+
39+ :return: A `QuerySet` for `MACAddress`.
40+ """
41+ return self.node.macaddress_set.all()
42+
43+ _get_interfaces_query = dedent("""\
44+ WITH RECURSIVE interfaces(id) AS (
45+ SELECT iface.id
46+ FROM maasserver_interface iface, maasserver_macaddress mac
47+ WHERE iface.mac_id = mac.id
48+ AND mac.node_id = %s
49+ UNION ALL
50+ SELECT iface_rel.child_id
51+ FROM maasserver_interfacerelationship iface_rel, interfaces
52+ WHERE iface_rel.parent_id = interfaces.id
53+ )
54+ SELECT *
55+ FROM maasserver_interface
56+ WHERE id in (SELECT id FROM interfaces)
57+ """)
58+
59+ def get_interfaces(self):
60+ """Obtain the set of interfaces for this node.
61+
62+ This will (efficiently) recurse into all nested interfaces.
63+
64+ :return: A `QuerySet` for `Interface`.
65+ """
66+ return Interface.objects.raw(
67+ self._get_interfaces_query, params=[self.node.id])
68
69=== added directory 'src/maasserver/networking/tests'
70=== added file 'src/maasserver/networking/tests/__init__.py'
71=== added file 'src/maasserver/networking/tests/test_networking.py'
72--- src/maasserver/networking/tests/test_networking.py 1970-01-01 00:00:00 +0000
73+++ src/maasserver/networking/tests/test_networking.py 2015-08-18 17:11:19 +0000
74@@ -0,0 +1,129 @@
75+# Copyright 2012-2015 Canonical Ltd. This software is licensed under the
76+# GNU Affero General Public License version 3 (see the file LICENSE).
77+
78+"""Tests for the `maasserver.networking` module itself."""
79+
80+from __future__ import (
81+ absolute_import,
82+ print_function,
83+ unicode_literals,
84+ )
85+
86+str = None
87+
88+__metaclass__ = type
89+__all__ = []
90+
91+from functools import partial
92+
93+from django.db.models.query import (
94+ QuerySet,
95+ RawQuerySet,
96+)
97+from maasserver.enum import INTERFACE_TYPE
98+from maasserver.networking import NodeNetworking
99+from maasserver.testing.factory import factory
100+from maasserver.testing.testcase import MAASServerTestCase
101+from testtools.matchers import (
102+ Is,
103+ IsInstance,
104+)
105+
106+# Modify make_MACAddress() to not create an interface by default.
107+make_MACAddress = partial(factory.make_MACAddress, iftype=None)
108+make_Interface = factory.make_Interface
109+
110+
111+class TestNodeNetworking(MAASServerTestCase):
112+ """Tests for `NodeNetworking` class."""
113+
114+ def test__init(self):
115+ node = factory.make_Node()
116+ node_net = NodeNetworking(node)
117+ self.assertThat(node_net.node, Is(node))
118+
119+ # MAC addresses.
120+
121+ def test__get_mac_addresses_returns_empty_when_no_mac_addrs(self):
122+ node = factory.make_Node()
123+ node_net = NodeNetworking(node)
124+ self.assertItemsEqual(node_net.get_mac_addresses(), [])
125+
126+ def test__get_mac_addresses_returns_empty_when_no_mac_addrs_for_node(self):
127+ node = factory.make_Node()
128+ node_net = NodeNetworking(node)
129+ mac_for_other_node = make_MACAddress() # noqa
130+ self.assertItemsEqual(node_net.get_mac_addresses(), [])
131+
132+ def test__get_mac_addresses_returns_mac_addrs_belonging_to_node(self):
133+ node = factory.make_Node()
134+ node_net = NodeNetworking(node)
135+ mac_for_node_1 = make_MACAddress(node=node)
136+ mac_for_node_2 = make_MACAddress(node=node)
137+ mac_for_other_node = make_MACAddress() # noqa
138+ self.assertItemsEqual(
139+ [mac_for_node_1, mac_for_node_2],
140+ node_net.get_mac_addresses())
141+
142+ def test__get_mac_addresses_returns_query_set(self):
143+ node = factory.make_Node()
144+ node_net = NodeNetworking(node)
145+ self.assertThat(node_net.get_mac_addresses(), IsInstance(QuerySet))
146+
147+ # Interfaces.
148+
149+ def test__get_interfaces_returns_empty_when_no_interfaces(self):
150+ node = factory.make_Node()
151+ node_net = NodeNetworking(node)
152+ self.assertItemsEqual(node_net.get_interfaces(), [])
153+
154+ def test__get_interfaces_returns_empty_when_no_interfaces_for_node(self):
155+ node = factory.make_Node()
156+ node_net = NodeNetworking(node)
157+ iface_for_other_node = ( # noqa
158+ make_Interface(INTERFACE_TYPE.PHYSICAL))
159+ self.assertItemsEqual(node_net.get_interfaces(), [])
160+
161+ def test__get_interfaces_returns_ifaces_belonging_to_node(self):
162+ node = factory.make_Node()
163+ node_net = NodeNetworking(node)
164+
165+ mac1 = make_MACAddress(node=node, iftype=None)
166+ mac2 = make_MACAddress(node=node, iftype=None)
167+
168+ iface1 = make_Interface(INTERFACE_TYPE.PHYSICAL, mac=mac1)
169+ iface2 = make_Interface(INTERFACE_TYPE.PHYSICAL, mac=mac2)
170+
171+ mac_for_other_node = make_MACAddress() # noqa
172+ iface_for_other_node = make_Interface( # noqa
173+ INTERFACE_TYPE.PHYSICAL, mac=mac_for_other_node)
174+
175+ self.assertItemsEqual([iface1, iface2], node_net.get_interfaces())
176+
177+ def test__get_interfaces_returns_nested_ifaces_belonging_to_node(self):
178+ node = factory.make_Node()
179+ node_net = NodeNetworking(node)
180+
181+ mac1 = make_MACAddress(node=node)
182+ mac2 = make_MACAddress(node=node)
183+ mac3 = make_MACAddress(node=node)
184+
185+ iface1 = make_Interface(INTERFACE_TYPE.PHYSICAL, mac=mac1)
186+ iface2 = make_Interface(INTERFACE_TYPE.PHYSICAL, mac=mac2)
187+ iface3 = make_Interface(INTERFACE_TYPE.PHYSICAL, mac=mac3)
188+
189+ bond_iface_for_node = make_Interface(
190+ INTERFACE_TYPE.BOND, mac1, parents=[iface1, iface2])
191+ vlan_iface_for_node = make_Interface(
192+ INTERFACE_TYPE.VLAN, mac=None, parents=[iface3])
193+ # INTERFACE_TYPE.ALIAS is not yet supported.
194+
195+ self.assertItemsEqual(
196+ [iface1, iface2, iface3, bond_iface_for_node, vlan_iface_for_node],
197+ node_net.get_interfaces())
198+
199+ def test__get_interfaces_returns_raw_query_set(self):
200+ node = factory.make_Node()
201+ node_net = NodeNetworking(node)
202+ self.assertThat(node_net.get_interfaces(), IsInstance(RawQuerySet))
203+ node_net.get_interfaces()