Merge lp:~allenap/maas/pass-addresses-to-tftp-backend 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: 1315
Proposed branch: lp:~allenap/maas/pass-addresses-to-tftp-backend
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 313 lines (+142/-14)
6 files modified
contrib/python-tx-tftp/tftp/protocol.py (+14/-2)
contrib/python-tx-tftp/tftp/test/test_protocol.py (+73/-2)
src/maasserver/api.py (+4/-1)
src/maasserver/tests/test_api.py (+25/-7)
src/provisioningserver/tests/test_tftp.py (+20/-2)
src/provisioningserver/tftp.py (+6/-0)
To merge this branch: bzr merge lp:~allenap/maas/pass-addresses-to-tftp-backend
Reviewer Review Type Date Requested Status
Julian Edwards (community) Approve
Review via email: mp+132015@code.launchpad.net

Commit message

Use the node-facing address of the cluster controller as the iSCSI host.

The cluster and node addresses are passed over to the pxeconfig view.

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

306 + # Send the local and remote endpoint addresses.
307 + local_host, local_port = get("local", (None, None))
308 + if local_host is not None:
309 + params["local"] = local_host
310 + remote_host, remote_port = get("remote", (None, None))
311 + if remote_host is not None:
312 + params["remote"] = remote_host

As discussed, get rid of the conditionals here, just try to poke in the IP data since it's mandatory in the API. If it fails, we want to fail early.

Everything else looks tip top!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'contrib/python-tx-tftp/tftp/protocol.py'
--- contrib/python-tx-tftp/tftp/protocol.py 2012-07-05 14:36:27 +0000
+++ contrib/python-tx-tftp/tftp/protocol.py 2012-10-30 09:11:22 +0000
@@ -12,6 +12,7 @@
12from twisted.internet.defer import inlineCallbacks, returnValue12from twisted.internet.defer import inlineCallbacks, returnValue
13from twisted.internet.protocol import DatagramProtocol13from twisted.internet.protocol import DatagramProtocol
14from twisted.python import log14from twisted.python import log
15from twisted.python.context import call
1516
1617
17class TFTP(DatagramProtocol):18class TFTP(DatagramProtocol):
@@ -48,11 +49,22 @@
4849
49 @inlineCallbacks50 @inlineCallbacks
50 def _startSession(self, datagram, addr, mode):51 def _startSession(self, datagram, addr, mode):
52 # Set up a call context so that we can pass extra arbitrary
53 # information to interested backends without adding extra call
54 # arguments, or switching to using a request object, for example.
55 context = {}
56 if self.transport is not None:
57 # Add the local and remote addresses to the call context.
58 local = self.transport.getHost()
59 context["local"] = local.host, local.port
60 context["remote"] = addr
51 try:61 try:
52 if datagram.opcode == OP_WRQ:62 if datagram.opcode == OP_WRQ:
53 fs_interface = yield self.backend.get_writer(datagram.filename)63 fs_interface = yield call(
64 context, self.backend.get_writer, datagram.filename)
54 elif datagram.opcode == OP_RRQ:65 elif datagram.opcode == OP_RRQ:
55 fs_interface = yield self.backend.get_reader(datagram.filename)66 fs_interface = yield call(
67 context, self.backend.get_reader, datagram.filename)
56 except Unsupported, e:68 except Unsupported, e:
57 self.transport.write(ERRORDatagram.from_code(ERR_ILLEGAL_OP,69 self.transport.write(ERRORDatagram.from_code(ERR_ILLEGAL_OP,
58 str(e)).to_wire(), addr)70 str(e)).to_wire(), addr)
5971
=== modified file 'contrib/python-tx-tftp/tftp/test/test_protocol.py'
--- contrib/python-tx-tftp/tftp/test/test_protocol.py 2012-07-25 19:59:37 +0000
+++ contrib/python-tx-tftp/tftp/test/test_protocol.py 2012-10-30 09:11:22 +0000
@@ -11,9 +11,11 @@
11from tftp.netascii import NetasciiReceiverProxy, NetasciiSenderProxy11from tftp.netascii import NetasciiReceiverProxy, NetasciiSenderProxy
12from tftp.protocol import TFTP12from tftp.protocol import TFTP
13from twisted.internet import reactor13from twisted.internet import reactor
14from twisted.internet.defer import Deferred14from twisted.internet.address import IPv4Address
15from twisted.internet.defer import Deferred, inlineCallbacks
15from twisted.internet.protocol import DatagramProtocol16from twisted.internet.protocol import DatagramProtocol
16from twisted.internet.task import Clock17from twisted.internet.task import Clock
18from twisted.python import context
17from twisted.python.filepath import FilePath19from twisted.python.filepath import FilePath
18from twisted.test.proto_helpers import StringTransport20from twisted.test.proto_helpers import StringTransport
19from twisted.trial import unittest21from twisted.trial import unittest
@@ -50,7 +52,8 @@
5052
51 def setUp(self):53 def setUp(self):
52 self.clock = Clock()54 self.clock = Clock()
53 self.transport = FakeTransport(hostAddress=('127.0.0.1', self.port))55 self.transport = FakeTransport(
56 hostAddress=IPv4Address('UDP', '127.0.0.1', self.port))
5457
55 def test_malformed_datagram(self):58 def test_malformed_datagram(self):
56 tftp = TFTP(BackendFactory(), _clock=self.clock)59 tftp = TFTP(BackendFactory(), _clock=self.clock)
@@ -247,3 +250,71 @@
247 self.clock.advance(1)250 self.clock.advance(1)
248 self.assertTrue(d.called)251 self.assertTrue(d.called)
249 self.assertTrue(IWriter.providedBy(d.result.backend))252 self.assertTrue(IWriter.providedBy(d.result.backend))
253
254
255class CapturedContext(Exception):
256 """A donkey, to carry the call context back up the stack."""
257
258 def __init__(self, args, names):
259 super(CapturedContext, self).__init__(*args)
260 self.context = {name: context.get(name) for name in names}
261
262
263class ContextCapturingBackend(object):
264 """A fake `IBackend` that raises `CapturedContext`.
265
266 Calling `get_reader` or `get_writer` raises a `CapturedContext` exception,
267 which captures the values of the call context for the given `names`.
268 """
269
270 def __init__(self, *names):
271 self.names = names
272
273 def get_reader(self, file_name):
274 raise CapturedContext(("get_reader", file_name), self.names)
275
276 def get_writer(self, file_name):
277 raise CapturedContext(("get_writer", file_name), self.names)
278
279
280class HostTransport(object):
281 """A fake `ITransport` that only responds to `getHost`."""
282
283 def __init__(self, host):
284 self.host = host
285
286 def getHost(self):
287 return IPv4Address("UDP", *self.host)
288
289
290class BackendCallingContext(unittest.TestCase):
291
292 def setUp(self):
293 super(BackendCallingContext, self).setUp()
294 self.backend = ContextCapturingBackend("local", "remote")
295 self.tftp = TFTP(self.backend)
296 self.tftp.transport = HostTransport(("12.34.56.78", 1234))
297
298 @inlineCallbacks
299 def test_context_rrq(self):
300 rrq_datagram = RRQDatagram('nonempty', 'NetASCiI', {})
301 rrq_addr = ('127.0.0.1', 1069)
302 error = yield self.assertFailure(
303 self.tftp._startSession(rrq_datagram, rrq_addr, "octet"),
304 CapturedContext)
305 self.assertEqual(("get_reader", rrq_datagram.filename), error.args)
306 self.assertEqual(
307 {"local": self.tftp.transport.host, "remote": rrq_addr},
308 error.context)
309
310 @inlineCallbacks
311 def test_context_wrq(self):
312 wrq_datagram = WRQDatagram('nonempty', 'NetASCiI', {})
313 wrq_addr = ('127.0.0.1', 1069)
314 error = yield self.assertFailure(
315 self.tftp._startSession(wrq_datagram, wrq_addr, "octet"),
316 CapturedContext)
317 self.assertEqual(("get_writer", wrq_datagram.filename), error.args)
318 self.assertEqual(
319 {"local": self.tftp.transport.host, "remote": wrq_addr},
320 error.context)
250321
=== modified file 'src/maasserver/api.py'
--- src/maasserver/api.py 2012-10-26 10:56:06 +0000
+++ src/maasserver/api.py 2012-10-30 09:11:22 +0000
@@ -1792,6 +1792,8 @@
1792 :param arch: Architecture name (in the pxelinux namespace, eg. 'arm' not1792 :param arch: Architecture name (in the pxelinux namespace, eg. 'arm' not
1793 'armhf').1793 'armhf').
1794 :param subarch: Subarchitecture name (in the pxelinux namespace).1794 :param subarch: Subarchitecture name (in the pxelinux namespace).
1795 :param local: The IP address of the cluster controller.
1796 :param remote: The IP address of the booting node.
1795 """1797 """
1796 node = get_node_from_mac_string(request.GET.get('mac', None))1798 node = get_node_from_mac_string(request.GET.get('mac', None))
17971799
@@ -1846,11 +1848,12 @@
18461848
1847 purpose = get_boot_purpose(node)1849 purpose = get_boot_purpose(node)
1848 server_address = get_maas_facing_server_address()1850 server_address = get_maas_facing_server_address()
1851 cluster_address = get_mandatory_param(request.GET, "local")
18491852
1850 params = KernelParameters(1853 params = KernelParameters(
1851 arch=arch, subarch=subarch, release=series, purpose=purpose,1854 arch=arch, subarch=subarch, release=series, purpose=purpose,
1852 hostname=hostname, domain=domain, preseed_url=preseed_url,1855 hostname=hostname, domain=domain, preseed_url=preseed_url,
1853 log_host=server_address, fs_host=server_address)1856 log_host=server_address, fs_host=cluster_address)
18541857
1855 return HttpResponse(1858 return HttpResponse(
1856 json.dumps(params._asdict()),1859 json.dumps(params._asdict()),
18571860
=== modified file 'src/maasserver/tests/test_api.py'
--- src/maasserver/tests/test_api.py 2012-10-26 10:56:06 +0000
+++ src/maasserver/tests/test_api.py 2012-10-30 09:11:22 +0000
@@ -3202,11 +3202,16 @@
32023202
3203class TestPXEConfigAPI(AnonAPITestCase):3203class TestPXEConfigAPI(AnonAPITestCase):
32043204
3205 def get_default_params(self):
3206 return {
3207 "local": factory.getRandomIPAddress(),
3208 "remote": factory.getRandomIPAddress(),
3209 }
3210
3205 def get_mac_params(self):3211 def get_mac_params(self):
3206 return {'mac': factory.make_mac_address().mac_address}3212 params = self.get_default_params()
32073213 params['mac'] = factory.make_mac_address().mac_address
3208 def get_default_params(self):3214 return params
3209 return dict()
32103215
3211 def get_pxeconfig(self, params=None):3216 def get_pxeconfig(self, params=None):
3212 """Make a request to `pxeconfig`, and return its response dict."""3217 """Make a request to `pxeconfig`, and return its response dict."""
@@ -3240,7 +3245,8 @@
3240 ContainsAll(KernelParameters._fields))3245 ContainsAll(KernelParameters._fields))
32413246
3242 def test_pxeconfig_returns_data_for_known_node(self):3247 def test_pxeconfig_returns_data_for_known_node(self):
3243 response = self.client.get(reverse('pxeconfig'), self.get_mac_params())3248 params = self.get_mac_params()
3249 response = self.client.get(reverse('pxeconfig'), params)
3244 self.assertEqual(httplib.OK, response.status_code)3250 self.assertEqual(httplib.OK, response.status_code)
32453251
3246 def test_pxeconfig_returns_no_content_for_unknown_node(self):3252 def test_pxeconfig_returns_no_content_for_unknown_node(self):
@@ -3252,6 +3258,7 @@
3252 architecture = factory.getRandomEnum(ARCHITECTURE)3258 architecture = factory.getRandomEnum(ARCHITECTURE)
3253 arch, subarch = architecture.split('/')3259 arch, subarch = architecture.split('/')
3254 params = dict(3260 params = dict(
3261 self.get_default_params(),
3255 mac=factory.getRandomMACAddress(delimiter=b'-'),3262 mac=factory.getRandomMACAddress(delimiter=b'-'),
3256 arch=arch,3263 arch=arch,
3257 subarch=subarch)3264 subarch=subarch)
@@ -3280,15 +3287,19 @@
3280 full_hostname = '.'.join([host, domain])3287 full_hostname = '.'.join([host, domain])
3281 node = factory.make_node(hostname=full_hostname)3288 node = factory.make_node(hostname=full_hostname)
3282 mac = factory.make_mac_address(node=node)3289 mac = factory.make_mac_address(node=node)
3283 pxe_config = self.get_pxeconfig(params={'mac': mac.mac_address})3290 params = self.get_default_params()
3291 params['mac'] = mac.mac_address
3292 pxe_config = self.get_pxeconfig(params)
3284 self.assertEqual(host, pxe_config.get('hostname'))3293 self.assertEqual(host, pxe_config.get('hostname'))
3285 self.assertNotIn(domain, pxe_config.values())3294 self.assertNotIn(domain, pxe_config.values())
32863295
3287 def test_pxeconfig_uses_nodegroup_domain_for_node(self):3296 def test_pxeconfig_uses_nodegroup_domain_for_node(self):
3288 mac = factory.make_mac_address()3297 mac = factory.make_mac_address()
3298 params = self.get_default_params()
3299 params['mac'] = mac
3289 self.assertEqual(3300 self.assertEqual(
3290 mac.node.nodegroup.name,3301 mac.node.nodegroup.name,
3291 self.get_pxeconfig({'mac': mac.mac_address}).get('domain'))3302 self.get_pxeconfig(params).get('domain'))
32923303
3293 def get_without_param(self, param):3304 def get_without_param(self, param):
3294 """Request a `pxeconfig()` response, but omit `param` from request."""3305 """Request a `pxeconfig()` response, but omit `param` from request."""
@@ -3353,6 +3364,13 @@
3353 fake_boot_purpose,3364 fake_boot_purpose,
3354 json.loads(response.content)["purpose"])3365 json.loads(response.content)["purpose"])
33553366
3367 def test_pxeconfig_returns_fs_host_as_cluster_controller(self):
3368 # The kernel parameter `fs_host` points to the cluster controller
3369 # address, which is passed over within the `local` parameter.
3370 params = self.get_default_params()
3371 kernel_params = KernelParameters(**self.get_pxeconfig(params))
3372 self.assertEqual(params["local"], kernel_params.fs_host)
3373
33563374
3357class TestNodeGroupsAPI(APIv10TestMixin, MultipleUsersScenarios, TestCase):3375class TestNodeGroupsAPI(APIv10TestMixin, MultipleUsersScenarios, TestCase):
3358 scenarios = [3376 scenarios = [
33593377
=== modified file 'src/provisioningserver/tests/test_tftp.py'
--- src/provisioningserver/tests/test_tftp.py 2012-10-02 10:53:22 +0000
+++ src/provisioningserver/tests/test_tftp.py 2012-10-30 09:11:22 +0000
@@ -35,6 +35,7 @@
35 inlineCallbacks,35 inlineCallbacks,
36 succeed,36 succeed,
37 )37 )
38from twisted.python import context
38from zope.interface.verify import verifyObject39from zope.interface.verify import verifyObject
3940
4041
@@ -213,6 +214,16 @@
213 mac = factory.getRandomMACAddress(b"-")214 mac = factory.getRandomMACAddress(b"-")
214 config_path = compose_config_path(mac)215 config_path = compose_config_path(mac)
215 backend = TFTPBackend(self.make_dir(), b"http://example.com/")216 backend = TFTPBackend(self.make_dir(), b"http://example.com/")
217 # python-tx-tftp sets up call context so that backends can discover
218 # more about the environment in which they're running.
219 call_context = {
220 "local": (
221 factory.getRandomIPAddress(),
222 factory.getRandomPort()),
223 "remote": (
224 factory.getRandomIPAddress(),
225 factory.getRandomPort()),
226 }
216227
217 @partial(self.patch, backend, "get_config_reader")228 @partial(self.patch, backend, "get_config_reader")
218 def get_config_reader(params):229 def get_config_reader(params):
@@ -220,9 +231,16 @@
220 params_json_reader = BytesReader(params_json)231 params_json_reader = BytesReader(params_json)
221 return succeed(params_json_reader)232 return succeed(params_json_reader)
222233
223 reader = yield backend.get_reader(config_path)234 reader = yield context.call(
235 call_context, backend.get_reader, config_path)
224 output = reader.read(10000)236 output = reader.read(10000)
225 expected_params = dict(mac=mac)237 # The addresses provided by python-tx-tftp in the call context are
238 # passed over the wire as address:port strings.
239 expected_params = {
240 "mac": mac,
241 "local": call_context["local"][0], # address only.
242 "remote": call_context["remote"][0], # address only.
243 }
226 observed_params = json.loads(output)244 observed_params = json.loads(output)
227 self.assertEqual(expected_params, observed_params)245 self.assertEqual(expected_params, observed_params)
228246
229247
=== modified file 'src/provisioningserver/tftp.py'
--- src/provisioningserver/tftp.py 2012-10-03 08:50:17 +0000
+++ src/provisioningserver/tftp.py 2012-10-30 09:11:22 +0000
@@ -34,6 +34,7 @@
34 IReader,34 IReader,
35 )35 )
36from tftp.errors import FileNotFound36from tftp.errors import FileNotFound
37from twisted.python.context import get
37from twisted.web.client import getPage38from twisted.web.client import getPage
38import twisted.web.error39import twisted.web.error
39from zope.interface import implementer40from zope.interface import implementer
@@ -211,6 +212,11 @@
211 for key, value in config_file_match.groupdict().items()212 for key, value in config_file_match.groupdict().items()
212 if value is not None213 if value is not None
213 }214 }
215 # Send the local and remote endpoint addresses.
216 local_host, local_port = get("local", (None, None))
217 params["local"] = local_host
218 remote_host, remote_port = get("remote", (None, None))
219 params["remote"] = remote_host
214 d = self.get_config_reader(params)220 d = self.get_config_reader(params)
215 d.addErrback(self.get_page_errback, file_name)221 d.addErrback(self.get_page_errback, file_name)
216 return d222 return d