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
=== added directory 'src/maasserver/networking'
=== added file 'src/maasserver/networking/__init__.py'
--- src/maasserver/networking/__init__.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/networking/__init__.py 2015-08-18 17:11:19 +0000
@@ -0,0 +1,62 @@
1# Copyright 2012-2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""MAAS Networking."""
5
6from __future__ import (
7 absolute_import,
8 print_function,
9 unicode_literals,
10 )
11
12str = None
13
14__metaclass__ = type
15__all__ = [
16 "NodeNetworking",
17]
18
19from textwrap import dedent
20
21from maasserver.models.interface import Interface
22
23
24class NodeNetworking:
25 """Query and modify a node's network configuration."""
26
27 def __init__(self, node):
28 super(NodeNetworking, self).__init__()
29 self.node = node
30
31 def get_mac_addresses(self):
32 """Obtain the set of MAC addresses for this node.
33
34 :return: A `QuerySet` for `MACAddress`.
35 """
36 return self.node.macaddress_set.all()
37
38 _get_interfaces_query = dedent("""\
39 WITH RECURSIVE interfaces(id) AS (
40 SELECT iface.id
41 FROM maasserver_interface iface, maasserver_macaddress mac
42 WHERE iface.mac_id = mac.id
43 AND mac.node_id = %s
44 UNION ALL
45 SELECT iface_rel.child_id
46 FROM maasserver_interfacerelationship iface_rel, interfaces
47 WHERE iface_rel.parent_id = interfaces.id
48 )
49 SELECT *
50 FROM maasserver_interface
51 WHERE id in (SELECT id FROM interfaces)
52 """)
53
54 def get_interfaces(self):
55 """Obtain the set of interfaces for this node.
56
57 This will (efficiently) recurse into all nested interfaces.
58
59 :return: A `QuerySet` for `Interface`.
60 """
61 return Interface.objects.raw(
62 self._get_interfaces_query, params=[self.node.id])
063
=== added directory 'src/maasserver/networking/tests'
=== added file 'src/maasserver/networking/tests/__init__.py'
=== added file 'src/maasserver/networking/tests/test_networking.py'
--- src/maasserver/networking/tests/test_networking.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/networking/tests/test_networking.py 2015-08-18 17:11:19 +0000
@@ -0,0 +1,129 @@
1# Copyright 2012-2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for the `maasserver.networking` module itself."""
5
6from __future__ import (
7 absolute_import,
8 print_function,
9 unicode_literals,
10 )
11
12str = None
13
14__metaclass__ = type
15__all__ = []
16
17from functools import partial
18
19from django.db.models.query import (
20 QuerySet,
21 RawQuerySet,
22)
23from maasserver.enum import INTERFACE_TYPE
24from maasserver.networking import NodeNetworking
25from maasserver.testing.factory import factory
26from maasserver.testing.testcase import MAASServerTestCase
27from testtools.matchers import (
28 Is,
29 IsInstance,
30)
31
32# Modify make_MACAddress() to not create an interface by default.
33make_MACAddress = partial(factory.make_MACAddress, iftype=None)
34make_Interface = factory.make_Interface
35
36
37class TestNodeNetworking(MAASServerTestCase):
38 """Tests for `NodeNetworking` class."""
39
40 def test__init(self):
41 node = factory.make_Node()
42 node_net = NodeNetworking(node)
43 self.assertThat(node_net.node, Is(node))
44
45 # MAC addresses.
46
47 def test__get_mac_addresses_returns_empty_when_no_mac_addrs(self):
48 node = factory.make_Node()
49 node_net = NodeNetworking(node)
50 self.assertItemsEqual(node_net.get_mac_addresses(), [])
51
52 def test__get_mac_addresses_returns_empty_when_no_mac_addrs_for_node(self):
53 node = factory.make_Node()
54 node_net = NodeNetworking(node)
55 mac_for_other_node = make_MACAddress() # noqa
56 self.assertItemsEqual(node_net.get_mac_addresses(), [])
57
58 def test__get_mac_addresses_returns_mac_addrs_belonging_to_node(self):
59 node = factory.make_Node()
60 node_net = NodeNetworking(node)
61 mac_for_node_1 = make_MACAddress(node=node)
62 mac_for_node_2 = make_MACAddress(node=node)
63 mac_for_other_node = make_MACAddress() # noqa
64 self.assertItemsEqual(
65 [mac_for_node_1, mac_for_node_2],
66 node_net.get_mac_addresses())
67
68 def test__get_mac_addresses_returns_query_set(self):
69 node = factory.make_Node()
70 node_net = NodeNetworking(node)
71 self.assertThat(node_net.get_mac_addresses(), IsInstance(QuerySet))
72
73 # Interfaces.
74
75 def test__get_interfaces_returns_empty_when_no_interfaces(self):
76 node = factory.make_Node()
77 node_net = NodeNetworking(node)
78 self.assertItemsEqual(node_net.get_interfaces(), [])
79
80 def test__get_interfaces_returns_empty_when_no_interfaces_for_node(self):
81 node = factory.make_Node()
82 node_net = NodeNetworking(node)
83 iface_for_other_node = ( # noqa
84 make_Interface(INTERFACE_TYPE.PHYSICAL))
85 self.assertItemsEqual(node_net.get_interfaces(), [])
86
87 def test__get_interfaces_returns_ifaces_belonging_to_node(self):
88 node = factory.make_Node()
89 node_net = NodeNetworking(node)
90
91 mac1 = make_MACAddress(node=node, iftype=None)
92 mac2 = make_MACAddress(node=node, iftype=None)
93
94 iface1 = make_Interface(INTERFACE_TYPE.PHYSICAL, mac=mac1)
95 iface2 = make_Interface(INTERFACE_TYPE.PHYSICAL, mac=mac2)
96
97 mac_for_other_node = make_MACAddress() # noqa
98 iface_for_other_node = make_Interface( # noqa
99 INTERFACE_TYPE.PHYSICAL, mac=mac_for_other_node)
100
101 self.assertItemsEqual([iface1, iface2], node_net.get_interfaces())
102
103 def test__get_interfaces_returns_nested_ifaces_belonging_to_node(self):
104 node = factory.make_Node()
105 node_net = NodeNetworking(node)
106
107 mac1 = make_MACAddress(node=node)
108 mac2 = make_MACAddress(node=node)
109 mac3 = make_MACAddress(node=node)
110
111 iface1 = make_Interface(INTERFACE_TYPE.PHYSICAL, mac=mac1)
112 iface2 = make_Interface(INTERFACE_TYPE.PHYSICAL, mac=mac2)
113 iface3 = make_Interface(INTERFACE_TYPE.PHYSICAL, mac=mac3)
114
115 bond_iface_for_node = make_Interface(
116 INTERFACE_TYPE.BOND, mac1, parents=[iface1, iface2])
117 vlan_iface_for_node = make_Interface(
118 INTERFACE_TYPE.VLAN, mac=None, parents=[iface3])
119 # INTERFACE_TYPE.ALIAS is not yet supported.
120
121 self.assertItemsEqual(
122 [iface1, iface2, iface3, bond_iface_for_node, vlan_iface_for_node],
123 node_net.get_interfaces())
124
125 def test__get_interfaces_returns_raw_query_set(self):
126 node = factory.make_Node()
127 node_net = NodeNetworking(node)
128 self.assertThat(node_net.get_interfaces(), IsInstance(RawQuerySet))
129 node_net.get_interfaces()