Merge lp:~allenap/maas/one-tftp-server-per-interface 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: 1322
Proposed branch: lp:~allenap/maas/one-tftp-server-per-interface
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 233 lines (+131/-14)
4 files modified
src/provisioningserver/plugin.py (+12/-4)
src/provisioningserver/tests/test_plugin.py (+49/-10)
src/provisioningserver/tests/test_utils.py (+59/-0)
src/provisioningserver/utils.py (+11/-0)
To merge this branch: bzr merge lp:~allenap/maas/one-tftp-server-per-interface
Reviewer Review Type Date Requested Status
John A Meinel (community) Approve
Julian Edwards (community) Approve
Review via email: mp+132252@code.launchpad.net

Commit message

Start a TFTP server for each IPv4 address configured on a cluster controller's interfaces.

Previously there was a single server on all interfaces, which made it tough to figure out on which interface a particular datagram was received.

To post a comment you must log in.
Revision history for this message
Julian Edwards (julian-edwards) wrote :

Awesome! Just one comment as usual - can you squish the multiple assertions into a single one?

review: Approve
Revision history for this message
John A Meinel (jameinel) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

On 10/31/2012 09:30 AM, Gavin Panella wrote:
> Gavin Panella has proposed merging
> lp:~allenap/maas/one-tftp-server-per-interface into lp:maas.
>
> Commit message: Start a TFTP server for each IPv4 address
> configured on a cluster controller's interfaces.
>
> Previously there was a single server on all interfaces, which made
> it tough to figure out on which interface a particular datagram
> was received.
>
>
> Requested reviews: Launchpad code reviewers (launchpad-reviewers)
> Related bugs: Bug #1068843 in MAAS: "maas-cluster-controller
> doesn't have images for provisioning"
> https://bugs.launchpad.net/maas/+bug/1068843
>
> For more details, see:
> https://code.launchpad.net/~allenap/maas/one-tftp-server-per-interface/+merge/132252
>
>
>
+ example_interfaces = {
+ 'eth0': {
+ 17: [{'addr': '00:1d:ba:86:aa:fe',
+ 'broadcast': 'ff:ff:ff:ff:ff:ff'}],
+ },
+ 'lo': {
+ 2: [{'addr': '127.0.0.1',
+ 'netmask': '255.0.0.0',
+ 'peer': '127.0.0.1'}],
+ 10: [{'addr': '::1',
+ 'netmask': 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff'}],
+ 17: [{'addr': '00:00:00:00:00:00',
+ 'peer': '00:00:00:00:00:00'}],
+ },

The 2, 10, 17 values seem particularly magical. Is it possible to do
something ilke:

tcpv4 = 2
tcpv6 = 17
...

And then use those constants instead of raw numbers?

 review: approve

John
=:->

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.11 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://www.enigmail.net/

iEYEARECAAYFAlCQ5hUACgkQJdeBCYSNAANfAwCfZ2E2Tz27Iw6ZbtwPTKbaHvyd
wcYAoLe9lehi1shIBcizSrznFA+sXmQE
=Qc3e
-----END PGP SIGNATURE-----

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

Thank you both for the reviews. I've addressed all your points.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/provisioningserver/plugin.py'
--- src/provisioningserver/plugin.py 2012-08-16 08:07:06 +0000
+++ src/provisioningserver/plugin.py 2012-10-31 11:59:23 +0000
@@ -19,6 +19,7 @@
19 OOPSService,19 OOPSService,
20 )20 )
21from provisioningserver.tftp import TFTPBackend21from provisioningserver.tftp import TFTPBackend
22from provisioningserver.utils import get_all_interface_addresses
22from tftp.protocol import TFTP23from tftp.protocol import TFTP
23from twisted.application import internet24from twisted.application import internet
24from twisted.application.internet import (25from twisted.application.internet import (
@@ -150,10 +151,17 @@
150 def _makeTFTPService(self, tftp_config):151 def _makeTFTPService(self, tftp_config):
151 """Create the dynamic TFTP service."""152 """Create the dynamic TFTP service."""
152 backend = TFTPBackend(tftp_config["root"], tftp_config["generator"])153 backend = TFTPBackend(tftp_config["root"], tftp_config["generator"])
153 factory = TFTP(backend)154 # Create a UDP server individually for each discovered network
154 tftp_service = internet.UDPServer(tftp_config["port"], factory)155 # interface, so that we can detect the interface via which we have
155 tftp_service.setName("tftp")156 # received a datagram.
156 return tftp_service157 tftp_services = MultiService()
158 tftp_services.setName("tftp")
159 for address in get_all_interface_addresses():
160 tftp_service = internet.UDPServer(
161 tftp_config["port"], TFTP(backend), interface=address)
162 tftp_service.setName(address)
163 tftp_service.setServiceParent(tftp_services)
164 return tftp_services
157165
158 def makeService(self, options):166 def makeService(self, options):
159 """Construct a service."""167 """Construct a service."""
160168
=== modified file 'src/provisioningserver/tests/test_plugin.py'
--- src/provisioningserver/tests/test_plugin.py 2012-08-16 10:50:16 +0000
+++ src/provisioningserver/tests/test_plugin.py 2012-10-31 11:59:23 +0000
@@ -17,6 +17,7 @@
1717
18from maastesting.factory import factory18from maastesting.factory import factory
19from maastesting.testcase import TestCase19from maastesting.testcase import TestCase
20from provisioningserver import plugin
20from provisioningserver.plugin import (21from provisioningserver.plugin import (
21 Options,22 Options,
22 ProvisioningRealm,23 ProvisioningRealm,
@@ -29,6 +30,11 @@
29 AsynchronousDeferredRunTest,30 AsynchronousDeferredRunTest,
30 )31 )
31from testtools.matchers import (32from testtools.matchers import (
33 AfterPreprocessing,
34 AllMatch,
35 Equals,
36 IsInstance,
37 MatchesAll,
32 MatchesException,38 MatchesException,
33 Raises,39 Raises,
34 )40 )
@@ -120,6 +126,13 @@
120126
121 def test_tftp_service(self):127 def test_tftp_service(self):
122 # A TFTP service is configured and added to the top-level service.128 # A TFTP service is configured and added to the top-level service.
129 interfaces = [
130 factory.getRandomIPAddress(),
131 factory.getRandomIPAddress(),
132 ]
133 self.patch(
134 plugin, "get_all_interface_addresses",
135 lambda: interfaces)
123 config = {136 config = {
124 "tftp": {137 "tftp": {
125 "generator": "http://candlemass/solitude",138 "generator": "http://candlemass/solitude",
@@ -131,17 +144,43 @@
131 options["config-file"] = self.write_config(config)144 options["config-file"] = self.write_config(config)
132 service_maker = ProvisioningServiceMaker("Harry", "Hill")145 service_maker = ProvisioningServiceMaker("Harry", "Hill")
133 service = service_maker.makeService(options)146 service = service_maker.makeService(options)
134 tftp_service = service.getServiceNamed("tftp")147 tftp_services = service.getServiceNamed("tftp")
135 self.assertIsInstance(tftp_service, UDPServer)148 # The "tftp" service is a multi-service containing UDP servers for
136 port, protocol = tftp_service.args149 # each interface defined by get_all_interface_addresses().
137 self.assertEqual(config["tftp"]["port"], port)150 self.assertIsInstance(tftp_services, MultiService)
138 self.assertIsInstance(protocol, TFTP)151 services = [
139 self.assertIsInstance(protocol.backend, TFTPBackend)152 tftp_services.getServiceNamed(interface)
153 for interface in interfaces
154 ]
155 expected_backend = MatchesAll(
156 IsInstance(TFTPBackend),
157 AfterPreprocessing(
158 lambda backend: backend.base.path,
159 Equals(config["tftp"]["root"])),
160 AfterPreprocessing(
161 lambda backend: backend.generator_url.geturl(),
162 Equals(config["tftp"]["generator"])))
163 expected_protocol = MatchesAll(
164 IsInstance(TFTP),
165 AfterPreprocessing(
166 lambda protocol: protocol.backend,
167 expected_backend))
168 expected_service = MatchesAll(
169 IsInstance(UDPServer),
170 AfterPreprocessing(
171 lambda service: len(service.args),
172 Equals(2)),
173 AfterPreprocessing(
174 lambda service: service.args[0], # port
175 Equals(config["tftp"]["port"])),
176 AfterPreprocessing(
177 lambda service: service.args[1], # protocol
178 expected_protocol))
179 self.assertThat(services, AllMatch(expected_service))
180 # Only the interface used for each service differs.
140 self.assertEqual(181 self.assertEqual(
141 (config["tftp"]["root"],182 [service.kwargs for service in services],
142 config["tftp"]["generator"]),183 [{"interface": interface} for interface in interfaces])
143 (protocol.backend.base.path,
144 protocol.backend.generator_url.geturl()))
145184
146185
147class TestSingleUsernamePasswordChecker(TestCase):186class TestSingleUsernamePasswordChecker(TestCase):
148187
=== modified file 'src/provisioningserver/tests/test_utils.py'
--- src/provisioningserver/tests/test_utils.py 2012-09-25 05:21:24 +0000
+++ src/provisioningserver/tests/test_utils.py 2012-10-31 11:59:23 +0000
@@ -35,11 +35,18 @@
35from maastesting.fakemethod import FakeMethod35from maastesting.fakemethod import FakeMethod
36from maastesting.testcase import TestCase36from maastesting.testcase import TestCase
37from mock import Mock37from mock import Mock
38import netifaces
39from netifaces import (
40 AF_LINK,
41 AF_INET,
42 AF_INET6,
43 )
38import provisioningserver44import provisioningserver
39from provisioningserver.utils import (45from provisioningserver.utils import (
40 ActionScript,46 ActionScript,
41 atomic_write,47 atomic_write,
42 AtomicWriteScript,48 AtomicWriteScript,
49 get_all_interface_addresses,
43 get_mtime,50 get_mtime,
44 incremental_write,51 incremental_write,
45 maas_custom_config_markers,52 maas_custom_config_markers,
@@ -59,6 +66,58 @@
59from testtools.testcase import ExpectedException66from testtools.testcase import ExpectedException
6067
6168
69class TestInterfaceFunctions(TestCase):
70 """Tests for functions relating to network interfaces."""
71
72 example_interfaces = {
73 'eth0': {
74 AF_LINK: [{'addr': '00:1d:ba:86:aa:fe',
75 'broadcast': 'ff:ff:ff:ff:ff:ff'}],
76 },
77 'lo': {
78 AF_INET: [{'addr': '127.0.0.1',
79 'netmask': '255.0.0.0',
80 'peer': '127.0.0.1'}],
81 AF_INET6: [{'addr': '::1',
82 'netmask': 'ff:ff:ff:ff:ff:ff'}],
83 AF_LINK: [{'addr': '00:00:00:00:00:00',
84 'peer': '00:00:00:00:00:00'}],
85 },
86 'lxcbr0': {
87 AF_INET: [{'addr': '10.0.3.1',
88 'broadcast': '10.0.3.255',
89 'netmask': '255.255.255.0'}],
90 AF_INET6: [{'addr': 'fe80::9894:6fff:fe8b:22%lxcbr0',
91 'netmask': 'ffff:ffff:ffff:ffff::'}],
92 AF_LINK: [{'addr': '9a:94:6f:8b:00:22',
93 'broadcast': 'ff:ff:ff:ff:ff:ff'}]},
94 'tun0': {
95 AF_INET: [{'addr': '10.99.244.250',
96 'netmask': '255.255.255.255',
97 'peer': '10.99.244.249'}],
98 },
99 'wlan0': {
100 AF_INET: [{'addr': '10.155.1.159',
101 'broadcast': '10.155.31.255',
102 'netmask': '255.255.224.0'}],
103 AF_INET6: [{'addr': 'fe80::221:5dff:fe85:d2e4%wlan0',
104 'netmask': 'ffff:ffff:ffff:ffff::'}],
105 AF_LINK: [{'addr': '00:21:5d:85:dAF_INET:e4',
106 'broadcast': 'ff:ff:ff:ff:ff:ff'}],
107 },
108 }
109
110 def test_get_all_interface_addresses(self):
111 # get_all_interface_addresses() returns the IPv4 addresses associated
112 # with each of the network devices present on the system, as reported
113 # by netifaces. IPv6 is ignored.
114 self.patch(netifaces, "interfaces", self.example_interfaces.keys)
115 self.patch(netifaces, "ifaddresses", self.example_interfaces.get)
116 self.assertEqual(
117 ["127.0.0.1", "10.0.3.1", "10.99.244.250", "10.155.1.159"],
118 list(get_all_interface_addresses()))
119
120
62class TestSafe(TestCase):121class TestSafe(TestCase):
63 """Test `Safe`."""122 """Test `Safe`."""
64123
65124
=== modified file 'src/provisioningserver/utils.py'
--- src/provisioningserver/utils.py 2012-10-11 10:47:57 +0000
+++ src/provisioningserver/utils.py 2012-10-31 11:59:23 +0000
@@ -26,6 +26,7 @@
26from argparse import ArgumentParser26from argparse import ArgumentParser
27import errno27import errno
28from functools import wraps28from functools import wraps
29import netifaces
29import os30import os
30from os import fdopen31from os import fdopen
31from pipes import quote32from pipes import quote
@@ -460,3 +461,13 @@
460 atomic_write(461 atomic_write(
461 content, args.filename, overwrite=not args.no_overwrite,462 content, args.filename, overwrite=not args.no_overwrite,
462 mode=mode)463 mode=mode)
464
465
466def get_all_interface_addresses():
467 """For each network interface, yield its IPv4 address."""
468 for interface in netifaces.interfaces():
469 addresses = netifaces.ifaddresses(interface)
470 if netifaces.AF_INET in addresses:
471 for inet_address in addresses[netifaces.AF_INET]:
472 if "addr" in inet_address:
473 yield inet_address["addr"]