Merge lp:~allenap/maas/ntp-prefer-nearby-servers--bug-1675095 into lp:~maas-committers/maas/trunk

Proposed by Gavin Panella
Status: Merged
Approved by: Gavin Panella
Approved revision: no longer in the source branch.
Merged at revision: 5898
Proposed branch: lp:~allenap/maas/ntp-prefer-nearby-servers--bug-1675095
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 683 lines (+355/-72)
9 files modified
src/maasserver/dbviews.py (+19/-2)
src/maasserver/ntp.py (+5/-4)
src/maasserver/regiondservices/tests/test_ntp.py (+15/-11)
src/maasserver/routablepairs.py (+12/-0)
src/maasserver/tests/test_dbviews.py (+91/-22)
src/maasserver/tests/test_ntp.py (+54/-32)
src/maasserver/tests/test_routablepairs.py (+130/-1)
src/maastesting/matchers.py (+18/-0)
src/maastesting/tests/test_matchers.py (+11/-0)
To merge this branch: bzr merge lp:~allenap/maas/ntp-prefer-nearby-servers--bug-1675095
Reviewer Review Type Date Requested Status
Andres Rodriguez (community) Approve
Review via email: mp+321319@code.launchpad.net

Commit message

Calculated routes between nodes are now prioritised.

Description of the change

This is necessarily hand-wavy because it's nearly impossible to anticipate the cost exactly. Instead a cost is calculated based on a few expectations:

- Sending traffic within a single host is fastest.
- Sending traffic within the same subnet is next.
- Then, sending traffic within the same VLAN.
- Then, sending traffic within the same space.
- Then, sending traffic within the null space.

These costs are relative to one another only, and they do not convey relative _magnitudes_ of cost. They are useful for ordering only.

To post a comment you must log in.
Revision history for this message
Andres Rodriguez (andreserl) wrote :

So, I've tested this and seems to work as expected. I only, however, tested for commissioning and deployment and didn't test for enlistment.

IIRC, for enlistment, we would use the Region IP address the rack is connected to instead of the rack controller IP address.

This doesn't seem to change that, but I just want to make sure that is the case.

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

Where we have no prior knowledge of which racks a machine has booted from — i.e. during enlistment — we configure NTP to talk to all _rack_ controllers. One address per rack is chosen according to the new metric calculation that this branch introduces. Only rack controllers talk NTP to region controllers, never machines or devices.

Thanks for testing this out. I'll give it a run through Jenkins anyway before I land.

Revision history for this message
Andres Rodriguez (andreserl) wrote :

I already did run it through the CI :). I'm gonna go ahead and land.

Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (1.8 MiB)

The attempt to merge lp:~allenap/maas/ntp-prefer-nearby-servers--bug-1675095 into lp:maas failed. Below is the output from the failed tests.

Hit:1 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial InRelease
Get:2 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-updates InRelease [102 kB]
Get:3 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-backports InRelease [102 kB]
Get:4 http://security.ubuntu.com/ubuntu xenial-security InRelease [102 kB]
Fetched 306 kB in 0s (667 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
    --no-install-recommends install apache2 archdetect-deb authbind avahi-utils bash bind9 bind9utils build-essential bzr bzr-builddeb chromium-browser chromium-chromedriver curl daemontools debhelper dh-apport dh-systemd distro-info dnsutils firefox freeipmi-tools git gjs ipython isc-dhcp-common isc-dhcp-server libjs-angularjs libjs-jquery libjs-jquery-hotkeys libjs-yui3-full libjs-yui3-min libnss-wrapper libpq-dev make nodejs-legacy npm postgresql psmisc pxelinux python3-all python3-apt python3-attr python3-bson python3-convoy python3-crochet python3-cssselect python3-curtin python3-dev python3-distro-info python3-django python3-django-nose python3-django-piston3 python3-dnspython python3-docutils python3-formencode python3-hivex python3-httplib2 python3-jinja2 python3-jsonschema python3-lxml python3-netaddr python3-netifaces python3-novaclient python3-oauth python3-oauthlib python3-openssl python3-paramiko python3-petname python3-pexpect python3-psycopg2 python3-pyinotify python3-pyparsing python3-pyvmomi python3-requests python3-seamicroclient python3-setuptools python3-simplestreams python3-sphinx python3-tempita python3-twisted python3-txtftp python3-tz python3-yaml python3-zope.interface python-bson python-crochet python-django python-django-piston python-djorm-ext-pgarray python-formencode python-lxml python-netaddr python-netifaces python-pocket-lint python-psycopg2 python-simplejson python-tempita python-twisted python-yaml socat syslinux-common tgt ubuntu-cloudimage-keyring wget xvfb
Reading package lists...
Building dependency tree...
Reading state information...
authbind is already the newest version (2.1.1+nmu1).
avahi-utils is already the newest version (0.6.32~rc+dfsg-1ubuntu2).
build-essential is already the newest version (12.1ubuntu2).
debhelper is already the newest version (9.20160115ubuntu3).
distro-info is already the newest version (0.14build1).
git is already the newest version (1:2.7.4-0ubuntu1).
libjs-angularjs is already the newest version (1.2.28-1ubuntu2).
libjs-jquery is already the newest version (1.11.3+dfsg-4).
libjs-yui3-full is already the newest version (3.5.1-1ubuntu3).
libjs-yui3-min is already the newest version (3.5.1-1ubuntu3).
make is already the newest version (4.1-6).
postgresql is already the newest version (9.5+173).
psmisc is already the newest version (22.21-2.1build1).
pxelinux is already the newest version (3:6.03+dfsg-11ubuntu1).
python-formencode is already the newest version (1.3.0-0ubuntu5).
python-lxml is already the newest version (3.5.0-1build1).
python-netaddr is already the newest version (0.7.18...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/maasserver/dbviews.py'
--- src/maasserver/dbviews.py 2017-03-07 16:26:47 +0000
+++ src/maasserver/dbviews.py 2017-03-31 09:18:27 +0000
@@ -36,6 +36,7 @@
36 with closing(connection.cursor()) as cursor:36 with closing(connection.cursor()) as cursor:
37 cursor.execute(view_sql)37 cursor.execute(view_sql)
3838
39
39# Note that the `Discovery` model object is backed by this view. Any40# Note that the `Discovery` model object is backed by this view. Any
40# changes made to this view should be reflected there.41# changes made to this view should be reflected there.
41maasserver_discovery = dedent("""\42maasserver_discovery = dedent("""\
@@ -102,17 +103,33 @@
102# should not be used without constraining, say, the sets of nodes, to find103# should not be used without constraining, say, the sets of nodes, to find
103# addresses that are mutually routable between region controllers for example.104# addresses that are mutually routable between region controllers for example.
104maasserver_routable_pairs = dedent("""\105maasserver_routable_pairs = dedent("""\
105 SELECT if_left.node_id AS left_node_id,106 SELECT
107 -- "Left" node.
108 if_left.node_id AS left_node_id,
106 if_left.id AS left_interface_id,109 if_left.id AS left_interface_id,
107 subnet_left.id AS left_subnet_id,110 subnet_left.id AS left_subnet_id,
108 vlan_left.id AS left_vlan_id,111 vlan_left.id AS left_vlan_id,
109 sip_left.ip AS left_ip,112 sip_left.ip AS left_ip,
113
114 -- "Right" node.
110 if_right.node_id AS right_node_id,115 if_right.node_id AS right_node_id,
111 if_right.id AS right_interface_id,116 if_right.id AS right_interface_id,
112 subnet_right.id AS right_subnet_id,117 subnet_right.id AS right_subnet_id,
113 vlan_right.id AS right_vlan_id,118 vlan_right.id AS right_vlan_id,
114 sip_right.ip AS right_ip,119 sip_right.ip AS right_ip,
115 vlan_left.space_id AS space_id120
121 -- Space that left and right have in commmon. Can be NULL.
122 vlan_left.space_id AS space_id,
123
124 -- Relative metric; lower is better.
125 CASE
126 WHEN if_left.node_id = if_right.node_id THEN 0
127 WHEN subnet_left.id = subnet_right.id THEN 1
128 WHEN vlan_left.id = vlan_right.id THEN 2
129 WHEN vlan_left.space_id IS NOT NULL THEN 3
130 ELSE 4 -- The NULL space.
131 END AS metric
132
116 FROM maasserver_interface AS if_left133 FROM maasserver_interface AS if_left
117 JOIN maasserver_interface_ip_addresses AS ifia_left134 JOIN maasserver_interface_ip_addresses AS ifia_left
118 ON if_left.id = ifia_left.interface_id135 ON if_left.id = ifia_left.interface_id
119136
=== modified file 'src/maasserver/ntp.py'
--- src/maasserver/ntp.py 2016-10-03 20:10:53 +0000
+++ src/maasserver/ntp.py 2017-03-31 09:18:27 +0000
@@ -124,8 +124,9 @@
124 routable_addrs_map: AddressMap) -> Iterable[IPAddress]:124 routable_addrs_map: AddressMap) -> Iterable[IPAddress]:
125 """Choose one routable address per destination node.125 """Choose one routable address per destination node.
126126
127 The result is stable, preferring IPv6 over IPv4, and then the highest127 The addresses are in preference order (see `find_addresses_between_nodes`
128 address numberically. (In fact it relies on the ordering provided by128 for information on how that's derived) so this simply chooses the first.
129 `netaddr.IPAddress`, which is basically that.)
130 """129 """
131 return map(max, routable_addrs_map.values())130 for addresses in routable_addrs_map.values():
131 assert len(addresses) >= 1
132 yield addresses[0]
132133
=== modified file 'src/maasserver/regiondservices/tests/test_ntp.py'
--- src/maasserver/regiondservices/tests/test_ntp.py 2017-01-28 00:51:47 +0000
+++ src/maasserver/regiondservices/tests/test_ntp.py 2017-03-31 09:18:27 +0000
@@ -18,13 +18,17 @@
18from maasserver.utils.threads import deferToDatabase18from maasserver.utils.threads import deferToDatabase
19from maastesting.fixtures import MAASRootFixture19from maastesting.fixtures import MAASRootFixture
20from maastesting.matchers import (20from maastesting.matchers import (
21 ContainedBy,
21 DocTestMatches,22 DocTestMatches,
23 Matches,
24 MockCalledOnce,
22 MockCalledOnceWith,25 MockCalledOnceWith,
23)26)
24from maastesting.testcase import MAASTestCase27from maastesting.testcase import MAASTestCase
25from maastesting.twisted import TwistedLoggerFixture28from maastesting.twisted import TwistedLoggerFixture
26from provisioningserver.utils.testing import MAASIDFixture29from provisioningserver.utils.testing import MAASIDFixture
27from testtools.matchers import (30from testtools.matchers import (
31 AllMatch,
28 Equals,32 Equals,
29 IsInstance,33 IsInstance,
30 MatchesStructure,34 MatchesStructure,
@@ -78,25 +82,25 @@
78 self.useFixture(MAASIDFixture(region.system_id))82 self.useFixture(MAASIDFixture(region.system_id))
79 peer1, addr1_4, addr1_6 = make_region_with_address(space)83 peer1, addr1_4, addr1_6 = make_region_with_address(space)
80 peer2, addr2_4, addr2_6 = make_region_with_address(space)84 peer2, addr2_4, addr2_6 = make_region_with_address(space)
81 # Create a configuration object.85 # Return the servers and all possible peer IP addresses.
82 return ntp._Configuration(ntp_servers, {addr1_6.ip, addr2_6.ip})86 return ntp_servers, {addr1_4.ip, addr1_6.ip, addr2_4.ip, addr2_6.ip}
8387
84 @wait_for_reactor88 @wait_for_reactor
85 @inlineCallbacks89 @inlineCallbacks
86 def test__tryUpdate_updates_ntp_server(self):90 def test__tryUpdate_updates_ntp_server(self):
87 service = ntp.RegionNetworkTimeProtocolService(reactor)91 service = ntp.RegionNetworkTimeProtocolService(reactor)
88 configuration = yield deferToDatabase(self.make_example_configuration)92 refs, peers = yield deferToDatabase(self.make_example_configuration)
89 configure_region = self.patch_autospec(ntp, "configure_region")93 configure_region = self.patch_autospec(ntp, "configure_region")
90 restartService = self.patch_autospec(service_monitor, "restartService")94 restartService = self.patch_autospec(service_monitor, "restartService")
91 yield service._tryUpdate()95 yield service._tryUpdate()
92 self.assertThat(configure_region, MockCalledOnceWith(96 self.assertThat(
93 configuration.references, configuration.peers))97 configure_region, MockCalledOnceWith(
98 refs, Matches(AllMatch(ContainedBy(peers)))))
94 self.assertThat(restartService, MockCalledOnceWith("ntp_region"))99 self.assertThat(restartService, MockCalledOnceWith("ntp_region"))
95 # If the configuration has not changed then a second call to100 # If the configuration has not changed then a second call to
96 # `_tryUpdate` does not result in another call to `configure_region`.101 # `_tryUpdate` does not result in another call to `configure_region`.
97 yield service._tryUpdate()102 yield service._tryUpdate()
98 self.assertThat(configure_region, MockCalledOnceWith(103 self.assertThat(configure_region, MockCalledOnce())
99 configuration.references, configuration.peers))
100 self.assertThat(restartService, MockCalledOnceWith("ntp_region"))104 self.assertThat(restartService, MockCalledOnceWith("ntp_region"))
101105
102106
@@ -147,15 +151,15 @@
147 # Put all addresses in the same space so they're mutually routable.151 # Put all addresses in the same space so they're mutually routable.
148 space = factory.make_Space()152 space = factory.make_Space()
149 # Populate the database with "this" region and an example peer.153 # Populate the database with "this" region and an example peer.
150 region, addr4, addr6 = make_region_with_address(space)154 region, _, _ = make_region_with_address(space)
151 self.useFixture(MAASIDFixture(region.system_id))155 self.useFixture(MAASIDFixture(region.system_id))
152 peer, addr4, addr6 = make_region_with_address(space)156 peer, addr4, addr6 = make_region_with_address(space)
153157
154 observed = service._getConfiguration()158 observed = service._getConfiguration()
155 self.assertThat(observed, IsInstance(ntp._Configuration))159 self.assertThat(observed, IsInstance(ntp._Configuration))
156160
157 expected_references = frozenset(ntp_servers)161 expected_references = Equals(frozenset(ntp_servers))
158 expected_peers = {addr6.ip} # IPv6 is preferred.162 expected_peers = AllMatch(ContainedBy({addr4.ip, addr6.ip}))
159163
160 self.assertThat(observed, MatchesStructure.byEquality(164 self.assertThat(observed, MatchesStructure(
161 references=expected_references, peers=expected_peers))165 references=expected_references, peers=expected_peers))
162166
=== modified file 'src/maasserver/routablepairs.py'
--- src/maasserver/routablepairs.py 2016-09-19 14:50:27 +0000
+++ src/maasserver/routablepairs.py 2017-03-31 09:18:27 +0000
@@ -24,6 +24,17 @@
24 ``node-left`` and ``node-right`` are one of the nodes passed in and24 ``node-left`` and ``node-right`` are one of the nodes passed in and
25 ``addr-left`` and ``addr-right`` are :class:`netaddr.IPAddress` instances25 ``addr-left`` and ``addr-right`` are :class:`netaddr.IPAddress` instances
26 corresponding to an active address on their respective nodes.26 corresponding to an active address on their respective nodes.
27
28 The results are sorted, lowest metric first, meaning that earlier
29 addresses are likely to have a lower overall communications cost, lower
30 latency, and so on. The order of preference is:
31
32 - Same node
33 - Same subnet
34 - Same VLAN
35 - Same space
36
37 An explicitly defined space is preferred to the default / null space.
27 """38 """
28 nodes_left = {node.id: node for node in nodes_left}39 nodes_left = {node.id: node for node in nodes_left}
29 nodes_right = {node.id: node for node in nodes_right}40 nodes_right = {node.id: node for node in nodes_right}
@@ -48,6 +59,7 @@
48 FROM maasserver_routable_pairs59 FROM maasserver_routable_pairs
49 WHERE left_node_id IN (%s)60 WHERE left_node_id IN (%s)
50 AND right_node_id IN (%s)61 AND right_node_id IN (%s)
62 ORDER BY metric ASC
51""")63""")
5264
5365
5466
=== modified file 'src/maasserver/tests/test_dbviews.py'
--- src/maasserver/tests/test_dbviews.py 2017-01-28 00:51:47 +0000
+++ src/maasserver/tests/test_dbviews.py 2017-03-31 09:18:27 +0000
@@ -10,6 +10,7 @@
10 _ALL_VIEWS,10 _ALL_VIEWS,
11 register_all_views,11 register_all_views,
12)12)
13from maasserver.models.subnet import Subnet
13from maasserver.testing.factory import factory14from maasserver.testing.factory import factory
14from maasserver.testing.testcase import MAASServerTestCase15from maasserver.testing.testcase import MAASServerTestCase
15from testtools.matchers import HasLength16from testtools.matchers import HasLength
@@ -39,29 +40,97 @@
39 cursor.execute("SELECT * from maasserver_routable_pairs")40 cursor.execute("SELECT * from maasserver_routable_pairs")
40 self.assertThat(cursor.fetchall(), HasLength(0))41 self.assertThat(cursor.fetchall(), HasLength(0))
4142
42 def make_node_with_address(self, space, cidr):43 def make_node_with_address(self, cidr, space=None, vlan=None):
43 node = factory.make_Node()44 node = factory.make_Node()
44 iface = factory.make_Interface(node=node)45 iface = factory.make_Interface(node=node)
45 subnet = factory.make_Subnet(space=space, cidr=cidr)46 try:
47 subnet = Subnet.objects.get(cidr=cidr, vlan__space=space)
48 except Subnet.DoesNotExist:
49 subnet = factory.make_Subnet(cidr=cidr, space=space, vlan=vlan)
46 sip = factory.make_StaticIPAddress(interface=iface, subnet=subnet)50 sip = factory.make_StaticIPAddress(interface=iface, subnet=subnet)
47 return node, iface, subnet, sip51 return node, iface, subnet, sip
4852
53 def test__contains_routes_between_nodes_on_same_subnet(self):
54 network = factory.make_ip4_or_6_network()
55 node1, if1, sn1, sip1 = self.make_node_with_address(network)
56 node2, if2, sn2, sip2 = self.make_node_with_address(network)
57
58 # Routes between all addresses are found, even back to themselves.
59 left = node1.id, if1.id, sn1.id, sn1.vlan.id, sip1.ip
60 right = node2.id, if2.id, sn2.id, sn2.vlan.id, sip2.ip
61 row = lambda ent1, ent2, metric: (*ent1, *ent2, None, metric)
62 expected = [
63 row(left, left, 0),
64 row(left, right, 1), # Same space, hence metric of 3.
65 row(right, right, 0),
66 row(right, left, 1), # Same space, hence metric of 3.
67 ]
68
69 with connection.cursor() as cursor:
70 cursor.execute("SELECT * from maasserver_routable_pairs")
71 self.assertItemsEqual(expected, cursor.fetchall())
72
73 def test__contains_routes_between_nodes_on_same_vlan(self):
74 vlan = factory.make_VLAN()
75 network1 = factory.make_ip4_or_6_network()
76 network2 = factory.make_ip4_or_6_network(version=network1.version)
77 node1, if1, sn1, sip1 = self.make_node_with_address(
78 network1, vlan=vlan)
79 node2, if2, sn2, sip2 = self.make_node_with_address(
80 network2, vlan=vlan)
81
82 # Routes between all addresses are found, even back to themselves.
83 left = node1.id, if1.id, sn1.id, sn1.vlan.id, sip1.ip
84 right = node2.id, if2.id, sn2.id, sn2.vlan.id, sip2.ip
85 row = lambda ent1, ent2, metric: (*ent1, *ent2, None, metric)
86 expected = [
87 row(left, left, 0),
88 row(left, right, 2), # Same VLAN, hence metric of 2.
89 row(right, right, 0),
90 row(right, left, 2), # Same VLAN, hence metric of 2.
91 ]
92
93 with connection.cursor() as cursor:
94 cursor.execute("SELECT * from maasserver_routable_pairs")
95 self.assertItemsEqual(expected, cursor.fetchall())
96
49 def test__contains_routes_between_nodes_on_same_space(self):97 def test__contains_routes_between_nodes_on_same_space(self):
50 space = factory.make_Space()98 space = factory.make_Space()
51 network1 = factory.make_ip4_or_6_network()99 network1 = factory.make_ip4_or_6_network()
52 network2 = factory.make_ip4_or_6_network(version=network1.version)100 network2 = factory.make_ip4_or_6_network(version=network1.version)
53 node1, if1, sn1, sip1 = self.make_node_with_address(space, network1)101 node1, if1, sn1, sip1 = self.make_node_with_address(network1, space)
54 node2, if2, sn2, sip2 = self.make_node_with_address(space, network2)102 node2, if2, sn2, sip2 = self.make_node_with_address(network2, space)
55103
56 # Routes between all addresses are found, even back to themselves.104 # Routes between all addresses are found, even back to themselves.
57 left = node1.id, if1.id, sn1.id, sn1.vlan.id, sip1.ip105 left = node1.id, if1.id, sn1.id, sn1.vlan.id, sip1.ip
58 right = node2.id, if2.id, sn2.id, sn2.vlan.id, sip2.ip106 right = node2.id, if2.id, sn2.id, sn2.vlan.id, sip2.ip
59 row = lambda ent1, ent2: ent1 + ent2 + (space.id, )107 row = lambda ent1, ent2, metric: (*ent1, *ent2, space.id, metric)
60 expected = [108 expected = [
61 row(left, left),109 row(left, left, 0),
62 row(left, right),110 row(left, right, 3), # Same space, hence metric of 3.
63 row(right, right),111 row(right, right, 0),
64 row(right, left),112 row(right, left, 3), # Same space, hence metric of 3.
113 ]
114
115 with connection.cursor() as cursor:
116 cursor.execute("SELECT * from maasserver_routable_pairs")
117 self.assertItemsEqual(expected, cursor.fetchall())
118
119 def test__contains_routes_between_nodes_via_null_space(self):
120 network1 = factory.make_ip4_or_6_network()
121 network2 = factory.make_ip4_or_6_network(version=network1.version)
122 node1, if1, sn1, sip1 = self.make_node_with_address(network1)
123 node2, if2, sn2, sip2 = self.make_node_with_address(network2)
124
125 # Routes between all addresses are found, even back to themselves.
126 left = node1.id, if1.id, sn1.id, sn1.vlan.id, sip1.ip
127 right = node2.id, if2.id, sn2.id, sn2.vlan.id, sip2.ip
128 row = lambda ent1, ent2, metric: (*ent1, *ent2, None, metric)
129 expected = [
130 row(left, left, 0),
131 row(left, right, 4), # The NULL space, hence metric of 4.
132 row(right, right, 0),
133 row(right, left, 4), # The NULL space, hence metric of 4.
65 ]134 ]
66135
67 with connection.cursor() as cursor:136 with connection.cursor() as cursor:
@@ -73,8 +142,8 @@
73 space2 = factory.make_Space()142 space2 = factory.make_Space()
74 network1 = factory.make_ip4_or_6_network()143 network1 = factory.make_ip4_or_6_network()
75 network2 = factory.make_ip4_or_6_network(version=network1.version)144 network2 = factory.make_ip4_or_6_network(version=network1.version)
76 node1, if1, sn1, sip1 = self.make_node_with_address(space1, network1)145 node1, if1, sn1, sip1 = self.make_node_with_address(network1, space1)
77 node2, if2, sn2, sip2 = self.make_node_with_address(space2, network2)146 node2, if2, sn2, sip2 = self.make_node_with_address(network2, space2)
78147
79 # Only routes from left to left and right to right are found: right is148 # Only routes from left to left and right to right are found: right is
80 # not routable from left, and left is not routable from right because149 # not routable from left, and left is not routable from right because
@@ -82,8 +151,8 @@
82 left = node1.id, if1.id, sn1.id, sn1.vlan.id, sip1.ip151 left = node1.id, if1.id, sn1.id, sn1.vlan.id, sip1.ip
83 right = node2.id, if2.id, sn2.id, sn2.vlan.id, sip2.ip152 right = node2.id, if2.id, sn2.id, sn2.vlan.id, sip2.ip
84 expected = [153 expected = [
85 left + left + (space1.id,),154 (*left, *left, space1.id, 0),
86 right + right + (space2.id,),155 (*right, *right, space2.id, 0),
87 ]156 ]
88157
89 with connection.cursor() as cursor:158 with connection.cursor() as cursor:
@@ -95,8 +164,8 @@
95 network1 = factory.make_ip4_or_6_network()164 network1 = factory.make_ip4_or_6_network()
96 network2 = factory.make_ip4_or_6_network(165 network2 = factory.make_ip4_or_6_network(
97 version=(4 if network1.version == 6 else 6))166 version=(4 if network1.version == 6 else 6))
98 node1, if1, sn1, sip1 = self.make_node_with_address(space, network1)167 node1, if1, sn1, sip1 = self.make_node_with_address(network1, space)
99 node2, if2, sn2, sip2 = self.make_node_with_address(space, network2)168 node2, if2, sn2, sip2 = self.make_node_with_address(network2, space)
100169
101 # Only routes from left to left and right to right are found: right is170 # Only routes from left to left and right to right are found: right is
102 # not routable from left, and left is not routable from right because171 # not routable from left, and left is not routable from right because
@@ -104,8 +173,8 @@
104 left = node1.id, if1.id, sn1.id, sn1.vlan.id, sip1.ip173 left = node1.id, if1.id, sn1.id, sn1.vlan.id, sip1.ip
105 right = node2.id, if2.id, sn2.id, sn2.vlan.id, sip2.ip174 right = node2.id, if2.id, sn2.id, sn2.vlan.id, sip2.ip
106 expected = [175 expected = [
107 left + left + (space.id,),176 (*left, *left, space.id, 0),
108 right + right + (space.id,),177 (*right, *right, space.id, 0),
109 ]178 ]
110179
111 with connection.cursor() as cursor:180 with connection.cursor() as cursor:
112181
=== modified file 'src/maasserver/tests/test_ntp.py'
--- src/maasserver/tests/test_ntp.py 2017-01-28 00:51:47 +0000
+++ src/maasserver/tests/test_ntp.py 2017-03-31 09:18:27 +0000
@@ -5,8 +5,6 @@
55
6__all__ = []6__all__ = []
77
8from operator import methodcaller
9
10from maasserver.models.config import Config8from maasserver.models.config import Config
11from maasserver.ntp import (9from maasserver.ntp import (
12 get_peers_for,10 get_peers_for,
@@ -14,10 +12,12 @@
14)12)
15from maasserver.testing.factory import factory13from maasserver.testing.factory import factory
16from maasserver.testing.testcase import MAASServerTestCase14from maasserver.testing.testcase import MAASServerTestCase
17from netaddr import IPAddress15from netaddr import (
16 IPAddress,
17 IPSet,
18)
18from testtools.matchers import (19from testtools.matchers import (
19 AfterPreprocessing,20 AfterPreprocessing,
20 AllMatch,
21 ContainsAll,21 ContainsAll,
22 Equals,22 Equals,
23 HasLength,23 HasLength,
@@ -231,7 +231,7 @@
231231
232 For racks, machines, and devices, a selection process takes place to232 For racks, machines, and devices, a selection process takes place to
233 determine which of several candidate addresses per server to choose. This233 determine which of several candidate addresses per server to choose. This
234 result is stable, i.e. it will choose the same address each time.234 result is semi-stable, i.e. it will always prefer "closer" addresses.
235 """235 """
236236
237 scenarios = (237 scenarios = (
@@ -253,23 +253,31 @@
253 super(TestGetServersFor_Selection, self).setUp()253 super(TestGetServersFor_Selection, self).setUp()
254 Config.objects.set_config("ntp_external_only", False)254 Config.objects.set_config("ntp_external_only", False)
255255
256 def test_prefers_ipv6_to_ipv4_peers_then_highest_numerically(self):256 def test_prefers_closest_addresses(self):
257 subnet4 = factory.make_Subnet(version=4)257 subnet4 = factory.make_Subnet(version=4)
258 subnet6a = factory.make_Subnet(version=6)258 subnet6 = factory.make_Subnet(version=6)
259 subnet6b = factory.make_Subnet(version=6)259 # Separate subnets but sharing the VLAN, hence routable.
260 # Ensure that addresses in subnet6a < those in subnet6b.260 subnet4v = factory.make_Subnet(version=4, vlan=subnet4.vlan)
261 subnet6a, subnet6b = sorted(261 subnet6v = factory.make_Subnet(version=6, vlan=subnet6.vlan)
262 {subnet6a, subnet6b}, key=methodcaller("get_ipnetwork"))262
263 # Create a node and server with an address in each subnet.263 # Create a node with an address in the first two subnets...
264 node, server = self.make_node(), self.make_server()264 node = self.make_node()
265 subnets = {subnet4, subnet6a, subnet6b}265 populate_node_with_addresses(node, {subnet4, subnet6})
266 populate_node_with_addresses(node, subnets)266 # ... and a server with an address in every subnet.
267 populate_node_with_addresses(server, subnets)267 server = self.make_server()
268 populate_node_with_addresses(
269 server, {subnet4, subnet6, subnet4v, subnet6v})
270
271 # The NTP server addresses chosen will be those that are "closest" to
272 # the node, and same-subnet wins in this over same-VLAN. No additional
273 # preference is made between IPv4 or IPv6, hence we allow for either.
274 preferred_subnets = subnet4, subnet6
275 preferred_networks = IPSet(
276 subnet.get_ipnetwork() for subnet in preferred_subnets)
268277
269 servers = get_servers_for(node)278 servers = get_servers_for(node)
270 self.assertThat(servers, Not(HasLength(0)))279 self.assertThat(servers, Not(HasLength(0)))
271 self.assertThat(servers, AllMatch(IsIPv6Address))280 self.assertThat(preferred_networks, ContainsAll(servers))
272 self.assertThat(subnet6b.get_ipnetwork(), ContainsAll(servers))
273281
274282
275class TestGetPeersFor_Region_RegionRack(MAASServerTestCase):283class TestGetPeersFor_Region_RegionRack(MAASServerTestCase):
@@ -296,23 +304,37 @@
296 get_peers_for(node2),304 get_peers_for(node2),
297 IsSetOfServers({node1_address.ip}))305 IsSetOfServers({node1_address.ip}))
298306
299 def test_prefers_ipv6_to_ipv4_peers_then_highest_numerically(self):307 def test_prefers_closest_addresses(self):
300 subnet4 = factory.make_Subnet(version=4)308 subnet4 = factory.make_Subnet(version=4)
301 subnet6a = factory.make_Subnet(version=6)309 subnet6 = factory.make_Subnet(version=6)
302 subnet6b = factory.make_Subnet(version=6)310 # Separate subnets but sharing the VLAN, hence routable.
303 # Ensure that addresses in subnet6a < those in subnet6b.311 subnet4v1 = factory.make_Subnet(version=4, vlan=subnet4.vlan)
304 subnet6a, subnet6b = sorted(312 subnet6v1 = factory.make_Subnet(version=6, vlan=subnet6.vlan)
305 {subnet6a, subnet6b}, key=methodcaller("get_ipnetwork"))313 subnet4v2 = factory.make_Subnet(version=4, vlan=subnet4.vlan)
306 # Create some peers, each with an address in each subnet.314 subnet6v2 = factory.make_Subnet(version=6, vlan=subnet6.vlan)
307 nodes = self.make_node(), self.make_node()315
308 subnets = {subnet4, subnet6a, subnet6b}316 # Create a node with an address in the first two subnets and the first
309 for node in nodes:317 # two same-VLAN subnets.
310 populate_node_with_addresses(node, subnets)318 node1 = self.make_node()
311 for node in nodes:319 populate_node_with_addresses(
320 node1, {subnet4, subnet6, subnet4v1, subnet6v1})
321 # Create a node with an address in the first two subnets and the
322 # second two same-VLAN subnets.
323 node2 = self.make_node()
324 populate_node_with_addresses(
325 node2, {subnet4, subnet6, subnet4v2, subnet6v2})
326
327 # The NTP server addresses chosen will be those that are "closest" to
328 # the node, and same-subnet wins in this over same-VLAN. No additional
329 # preference is made between IPv4 or IPv6, hence we allow for either.
330 preferred_subnets = subnet4, subnet6
331 preferred_networks = IPSet(
332 subnet.get_ipnetwork() for subnet in preferred_subnets)
333
334 for node in (node1, node2):
312 peers = get_peers_for(node)335 peers = get_peers_for(node)
313 self.assertThat(peers, Not(HasLength(0)))336 self.assertThat(peers, Not(HasLength(0)))
314 self.assertThat(peers, AllMatch(IsIPv6Address))337 self.assertThat(preferred_networks, ContainsAll(peers))
315 self.assertThat(subnet6b.get_ipnetwork(), ContainsAll(peers))
316338
317339
318class TestGetPeersFor_Other(MAASServerTestCase):340class TestGetPeersFor_Other(MAASServerTestCase):
319341
=== modified file 'src/maasserver/tests/test_routablepairs.py'
--- src/maasserver/tests/test_routablepairs.py 2017-01-28 00:51:47 +0000
+++ src/maasserver/tests/test_routablepairs.py 2017-03-31 09:18:27 +0000
@@ -5,13 +5,21 @@
55
6__all__ = []6__all__ = []
77
8from itertools import product8from itertools import (
9 product,
10 takewhile,
11)
12import random
913
10from maasserver.models.node import Node14from maasserver.models.node import Node
11from maasserver.routablepairs import find_addresses_between_nodes15from maasserver.routablepairs import find_addresses_between_nodes
12from maasserver.testing.factory import factory16from maasserver.testing.factory import factory
13from maasserver.testing.testcase import MAASServerTestCase17from maasserver.testing.testcase import MAASServerTestCase
14from testtools import ExpectedException18from testtools import ExpectedException
19from testtools.matchers import (
20 AfterPreprocessing,
21 Equals,
22)
1523
1624
17class TestFindAddressesBetweenNodes(MAASServerTestCase):25class TestFindAddressesBetweenNodes(MAASServerTestCase):
@@ -102,3 +110,124 @@
102 # No routable addresses are found.110 # No routable addresses are found.
103 self.assertItemsEqual(111 self.assertItemsEqual(
104 expected, find_addresses_between_nodes([node1], [node2]))112 expected, find_addresses_between_nodes([node1], [node2]))
113
114 def gen_disjoint_networks(self):
115 """Generate disjoint networks.
116
117 Can be IPv4 or IPv6, but once generation has begun they'll all be the
118 same family.
119 """
120 make_network = random.choice([
121 factory.make_ipv4_network,
122 factory.make_ipv6_network]
123 )
124 networks = []
125 while True:
126 network = make_network(disjoint_from=networks)
127 networks.append(network)
128 yield network
129
130 def test__yields_routes_with_lowest_metrics_first(self):
131 space = factory.make_Space()
132 # Ensure networks are disjoint but of the same family.
133 networks = self.gen_disjoint_networks()
134
135 # Create the node for the "left" that has two IP addresses, one in the
136 # null space, one in a non-null space.
137 origin = factory.make_Node(hostname="origin")
138 origin_iface = factory.make_Interface(
139 node=origin, disconnected=True)
140 origin_subnet = factory.make_Subnet(
141 space=space, cidr=next(networks))
142 origin_subnet_null_space = factory.make_Subnet(
143 space=None, cidr=next(networks))
144 origin_sip = factory.make_StaticIPAddress(
145 interface=origin_iface, subnet=origin_subnet)
146 origin_sip_null_space = factory.make_StaticIPAddress(
147 interface=origin_iface, subnet=origin_subnet_null_space)
148
149 # Same subnet, different node.
150 node_same_subnet = factory.make_Node(hostname="same-subnet")
151 sip_same_subnet = factory.make_StaticIPAddress(
152 interface=factory.make_Interface(
153 node=node_same_subnet, disconnected=True),
154 subnet=origin_subnet)
155
156 # Same VLAN, different subnet, different node.
157 node_same_vlan = factory.make_Node(hostname="same-vlan")
158 sip_same_vlan = factory.make_StaticIPAddress(
159 interface=factory.make_Interface(
160 node=node_same_vlan, disconnected=True),
161 subnet=factory.make_Subnet(
162 space=space, vlan=origin_subnet.vlan,
163 cidr=next(networks)))
164
165 # Same space, different VLAN, subnet, and node.
166 node_same_space = factory.make_Node(hostname="same-space")
167 sip_same_space = factory.make_StaticIPAddress(
168 interface=factory.make_Interface(
169 node=node_same_space, disconnected=True),
170 subnet=factory.make_Subnet(
171 space=space, cidr=next(networks)))
172
173 # Null space, different VLAN, subnet, and node.
174 node_null_space = factory.make_Node(hostname="null-space")
175 sip_null_space = factory.make_StaticIPAddress(
176 interface=factory.make_Interface(
177 node=node_null_space, disconnected=True),
178 subnet=factory.make_Subnet(
179 space=None, cidr=next(networks)))
180
181 # We'll search for routes between `lefts` and `rights`.
182 lefts = [
183 origin,
184 ]
185 rights = [
186 node_same_subnet,
187 node_same_vlan,
188 node_same_space,
189 node_null_space,
190 ]
191
192 # This is in order, lowest "metric" first.
193 expected = [
194 (origin, origin_sip.get_ipaddress(),
195 node_same_subnet, sip_same_subnet.get_ipaddress()),
196 (origin, origin_sip.get_ipaddress(),
197 node_same_vlan, sip_same_vlan.get_ipaddress()),
198 (origin, origin_sip.get_ipaddress(),
199 node_same_space, sip_same_space.get_ipaddress()),
200 (origin, origin_sip_null_space.get_ipaddress(),
201 node_null_space, sip_null_space.get_ipaddress()),
202 ]
203 self.assertThat(
204 find_addresses_between_nodes(lefts, rights),
205 AfterPreprocessing(list, Equals(expected)))
206
207 # Same node, same space, different VLAN and subnet. We did not add
208 # this earlier because its existence allows for a large number of
209 # additional routes between the origin and the other nodes, which
210 # would have obscured the test.
211 origin_sip_2 = factory.make_StaticIPAddress(
212 interface=factory.make_Interface(
213 node=origin, disconnected=True),
214 subnet=factory.make_Subnet(
215 space=space, cidr=next(networks)))
216
217 # Now the first addresses returned are between those addresses we
218 # created on the same node, in no particular order.
219 origin_ips = origin_sip.get_ipaddress(), origin_sip_2.get_ipaddress()
220 expected_mutual = {
221 (origin, ip1, origin, ip2)
222 for ip1, ip2 in product(origin_ips, origin_ips)
223 }
224 # There's a mutual route for the null-space IP address too.
225 expected_mutual.add(
226 (origin, origin_sip_null_space.get_ipaddress(),
227 origin, origin_sip_null_space.get_ipaddress()))
228 observed_mutual = takewhile(
229 (lambda route: route[0] == route[2]), # Route is mutual.
230 find_addresses_between_nodes(lefts, [origin, *rights]),
231 )
232 self.assertItemsEqual(
233 expected_mutual, observed_mutual)
105234
=== modified file 'src/maastesting/matchers.py'
--- src/maastesting/matchers.py 2016-12-07 12:46:14 +0000
+++ src/maastesting/matchers.py 2017-03-31 09:18:27 +0000
@@ -4,6 +4,7 @@
4"""testtools custom matchers"""4"""testtools custom matchers"""
55
6__all__ = [6__all__ = [
7 'ContainedBy',
7 'DocTestMatches',8 'DocTestMatches',
8 'FileContains',9 'FileContains',
9 'GreaterThanOrEqual',10 'GreaterThanOrEqual',
@@ -481,3 +482,20 @@
481 (lambda observed: not observed.isspace()),482 (lambda observed: not observed.isspace()),
482 "%r is whitespace"),483 "%r is whitespace"),
483 first_only=True)484 first_only=True)
485
486
487class ContainedBy(Matcher):
488 """Test if the matchee is in the given container."""
489
490 def __init__(self, haystack):
491 super(ContainedBy, self).__init__()
492 self.haystack = haystack
493
494 def __str__(self):
495 return "%s(%r)" % (
496 self.__class__.__name__, self.haystack)
497
498 def match(self, needle):
499 if needle not in self.haystack:
500 return Mismatch(
501 "%r not in %r" % (needle, self.haystack))
484502
=== modified file 'src/maastesting/tests/test_matchers.py'
--- src/maastesting/tests/test_matchers.py 2016-12-07 12:46:14 +0000
+++ src/maastesting/tests/test_matchers.py 2017-03-31 09:18:27 +0000
@@ -18,6 +18,7 @@
18from maastesting import matchers18from maastesting import matchers
19from maastesting.factory import factory19from maastesting.factory import factory
20from maastesting.matchers import (20from maastesting.matchers import (
21 ContainedBy,
21 FileContains,22 FileContains,
22 GreaterThanOrEqual,23 GreaterThanOrEqual,
23 HasAttribute,24 HasAttribute,
@@ -588,3 +589,13 @@
588 def test_does_not_match_non_strings(self):589 def test_does_not_match_non_strings(self):
589 self.assertMismatch(590 self.assertMismatch(
590 IsNonEmptyString.match(1234), "1234 is not a string")591 IsNonEmptyString.match(1234), "1234 is not a string")
592
593
594class TestContainedBy(MAASTestCase, MockTestMixin):
595 """Tests for the `ContainedBy` matcher."""
596
597 def test_matches_needle_in_haystack(self):
598 self.assertThat("foo", ContainedBy({"foo", "bar"}))
599
600 def test_does_not_match_needle_not_in_haystack(self):
601 self.assertMismatch(ContainedBy([]).match("foo"), "'foo' not in []")