launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #13981
[Merge] lp:~allenap/maas/one-tftp-server-per-interface-for-1.2 into lp:maas/1.2
Gavin Panella has proposed merging lp:~allenap/maas/one-tftp-server-per-interface-for-1.2 into lp:maas/1.2.
Commit message:
Backport lp:maas revision 1322:
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:
Gavin Panella (allenap)
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-for-1.2/+merge/132673
--
https://code.launchpad.net/~allenap/maas/one-tftp-server-per-interface-for-1.2/+merge/132673
Your team MAAS Maintainers is subscribed to branch lp:maas/1.2.
=== modified file 'src/provisioningserver/plugin.py'
--- src/provisioningserver/plugin.py 2012-08-16 08:07:06 +0000
+++ src/provisioningserver/plugin.py 2012-11-02 10:41:46 +0000
@@ -19,6 +19,7 @@
OOPSService,
)
from provisioningserver.tftp import TFTPBackend
+from provisioningserver.utils import get_all_interface_addresses
from tftp.protocol import TFTP
from twisted.application import internet
from twisted.application.internet import (
@@ -150,10 +151,17 @@
def _makeTFTPService(self, tftp_config):
"""Create the dynamic TFTP service."""
backend = TFTPBackend(tftp_config["root"], tftp_config["generator"])
- factory = TFTP(backend)
- tftp_service = internet.UDPServer(tftp_config["port"], factory)
- tftp_service.setName("tftp")
- return tftp_service
+ # Create a UDP server individually for each discovered network
+ # interface, so that we can detect the interface via which we have
+ # received a datagram.
+ tftp_services = MultiService()
+ tftp_services.setName("tftp")
+ for address in get_all_interface_addresses():
+ tftp_service = internet.UDPServer(
+ tftp_config["port"], TFTP(backend), interface=address)
+ tftp_service.setName(address)
+ tftp_service.setServiceParent(tftp_services)
+ return tftp_services
def makeService(self, options):
"""Construct a service."""
=== 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-11-02 10:41:46 +0000
@@ -17,6 +17,7 @@
from maastesting.factory import factory
from maastesting.testcase import TestCase
+from provisioningserver import plugin
from provisioningserver.plugin import (
Options,
ProvisioningRealm,
@@ -29,6 +30,11 @@
AsynchronousDeferredRunTest,
)
from testtools.matchers import (
+ AfterPreprocessing,
+ AllMatch,
+ Equals,
+ IsInstance,
+ MatchesAll,
MatchesException,
Raises,
)
@@ -120,6 +126,13 @@
def test_tftp_service(self):
# A TFTP service is configured and added to the top-level service.
+ interfaces = [
+ factory.getRandomIPAddress(),
+ factory.getRandomIPAddress(),
+ ]
+ self.patch(
+ plugin, "get_all_interface_addresses",
+ lambda: interfaces)
config = {
"tftp": {
"generator": "http://candlemass/solitude",
@@ -131,17 +144,43 @@
options["config-file"] = self.write_config(config)
service_maker = ProvisioningServiceMaker("Harry", "Hill")
service = service_maker.makeService(options)
- tftp_service = service.getServiceNamed("tftp")
- self.assertIsInstance(tftp_service, UDPServer)
- port, protocol = tftp_service.args
- self.assertEqual(config["tftp"]["port"], port)
- self.assertIsInstance(protocol, TFTP)
- self.assertIsInstance(protocol.backend, TFTPBackend)
+ tftp_services = service.getServiceNamed("tftp")
+ # The "tftp" service is a multi-service containing UDP servers for
+ # each interface defined by get_all_interface_addresses().
+ self.assertIsInstance(tftp_services, MultiService)
+ services = [
+ tftp_services.getServiceNamed(interface)
+ for interface in interfaces
+ ]
+ expected_backend = MatchesAll(
+ IsInstance(TFTPBackend),
+ AfterPreprocessing(
+ lambda backend: backend.base.path,
+ Equals(config["tftp"]["root"])),
+ AfterPreprocessing(
+ lambda backend: backend.generator_url.geturl(),
+ Equals(config["tftp"]["generator"])))
+ expected_protocol = MatchesAll(
+ IsInstance(TFTP),
+ AfterPreprocessing(
+ lambda protocol: protocol.backend,
+ expected_backend))
+ expected_service = MatchesAll(
+ IsInstance(UDPServer),
+ AfterPreprocessing(
+ lambda service: len(service.args),
+ Equals(2)),
+ AfterPreprocessing(
+ lambda service: service.args[0], # port
+ Equals(config["tftp"]["port"])),
+ AfterPreprocessing(
+ lambda service: service.args[1], # protocol
+ expected_protocol))
+ self.assertThat(services, AllMatch(expected_service))
+ # Only the interface used for each service differs.
self.assertEqual(
- (config["tftp"]["root"],
- config["tftp"]["generator"]),
- (protocol.backend.base.path,
- protocol.backend.generator_url.geturl()))
+ [service.kwargs for service in services],
+ [{"interface": interface} for interface in interfaces])
class TestSingleUsernamePasswordChecker(TestCase):
=== 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-11-02 10:41:46 +0000
@@ -35,11 +35,18 @@
from maastesting.fakemethod import FakeMethod
from maastesting.testcase import TestCase
from mock import Mock
+import netifaces
+from netifaces import (
+ AF_LINK,
+ AF_INET,
+ AF_INET6,
+ )
import provisioningserver
from provisioningserver.utils import (
ActionScript,
atomic_write,
AtomicWriteScript,
+ get_all_interface_addresses,
get_mtime,
incremental_write,
maas_custom_config_markers,
@@ -59,6 +66,58 @@
from testtools.testcase import ExpectedException
+class TestInterfaceFunctions(TestCase):
+ """Tests for functions relating to network interfaces."""
+
+ example_interfaces = {
+ 'eth0': {
+ AF_LINK: [{'addr': '00:1d:ba:86:aa:fe',
+ 'broadcast': 'ff:ff:ff:ff:ff:ff'}],
+ },
+ 'lo': {
+ AF_INET: [{'addr': '127.0.0.1',
+ 'netmask': '255.0.0.0',
+ 'peer': '127.0.0.1'}],
+ AF_INET6: [{'addr': '::1',
+ 'netmask': 'ff:ff:ff:ff:ff:ff'}],
+ AF_LINK: [{'addr': '00:00:00:00:00:00',
+ 'peer': '00:00:00:00:00:00'}],
+ },
+ 'lxcbr0': {
+ AF_INET: [{'addr': '10.0.3.1',
+ 'broadcast': '10.0.3.255',
+ 'netmask': '255.255.255.0'}],
+ AF_INET6: [{'addr': 'fe80::9894:6fff:fe8b:22%lxcbr0',
+ 'netmask': 'ffff:ffff:ffff:ffff::'}],
+ AF_LINK: [{'addr': '9a:94:6f:8b:00:22',
+ 'broadcast': 'ff:ff:ff:ff:ff:ff'}]},
+ 'tun0': {
+ AF_INET: [{'addr': '10.99.244.250',
+ 'netmask': '255.255.255.255',
+ 'peer': '10.99.244.249'}],
+ },
+ 'wlan0': {
+ AF_INET: [{'addr': '10.155.1.159',
+ 'broadcast': '10.155.31.255',
+ 'netmask': '255.255.224.0'}],
+ AF_INET6: [{'addr': 'fe80::221:5dff:fe85:d2e4%wlan0',
+ 'netmask': 'ffff:ffff:ffff:ffff::'}],
+ AF_LINK: [{'addr': '00:21:5d:85:dAF_INET:e4',
+ 'broadcast': 'ff:ff:ff:ff:ff:ff'}],
+ },
+ }
+
+ def test_get_all_interface_addresses(self):
+ # get_all_interface_addresses() returns the IPv4 addresses associated
+ # with each of the network devices present on the system, as reported
+ # by netifaces. IPv6 is ignored.
+ self.patch(netifaces, "interfaces", self.example_interfaces.keys)
+ self.patch(netifaces, "ifaddresses", self.example_interfaces.get)
+ self.assertEqual(
+ ["127.0.0.1", "10.0.3.1", "10.99.244.250", "10.155.1.159"],
+ list(get_all_interface_addresses()))
+
+
class TestSafe(TestCase):
"""Test `Safe`."""
=== modified file 'src/provisioningserver/utils.py'
--- src/provisioningserver/utils.py 2012-10-11 10:47:57 +0000
+++ src/provisioningserver/utils.py 2012-11-02 10:41:46 +0000
@@ -26,6 +26,7 @@
from argparse import ArgumentParser
import errno
from functools import wraps
+import netifaces
import os
from os import fdopen
from pipes import quote
@@ -460,3 +461,13 @@
atomic_write(
content, args.filename, overwrite=not args.no_overwrite,
mode=mode)
+
+
+def get_all_interface_addresses():
+ """For each network interface, yield its IPv4 address."""
+ for interface in netifaces.interfaces():
+ addresses = netifaces.ifaddresses(interface)
+ if netifaces.AF_INET in addresses:
+ for inet_address in addresses[netifaces.AF_INET]:
+ if "addr" in inet_address:
+ yield inet_address["addr"]
Follow ups