Merge ~blake-rouse/maas:fix-1724677 into maas:master

Proposed by Blake Rouse
Status: Merged
Approved by: Blake Rouse
Approved revision: 46e07bfddc1eeee720cfe4ea7cd96cb3ae35b76a
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~blake-rouse/maas:fix-1724677
Merge into: maas:master
Diff against target: 303 lines (+264/-1)
2 files modified
src/provisioningserver/rackdservices/tests/test_tftp.py (+223/-0)
src/provisioningserver/rackdservices/tftp.py (+41/-1)
Reviewer Review Type Date Requested Status
Mike Pontillo (community) Approve
Review via email: mp+332727@code.launchpad.net

Commit message

Always use the same RPC client for the TFTP remote client request.

This ensures that when a TFTP request is made from a client the same RPC connection will be used for the life of that client. This prevents concurrent updates from occurring across regiond processes since the same regiond reactor will be used for the life of that client.

To post a comment you must log in.
Revision history for this message
Blake Rouse (blake-rouse) wrote :

I have test this in the CI and it passed:

http://162.213.35.104:8080/job/maas-git-manual/75/console

Revision history for this message
Mike Pontillo (mpontillo) wrote :

Looks good!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/src/provisioningserver/rackdservices/tests/test_tftp.py b/src/provisioningserver/rackdservices/tests/test_tftp.py
index fe9df09..925f98c 100644
--- a/src/provisioningserver/rackdservices/tests/test_tftp.py
+++ b/src/provisioningserver/rackdservices/tests/test_tftp.py
@@ -385,6 +385,229 @@ class TestTFTPBackend(MAASTestCase):
385 )385 )
386386
387 @inlineCallbacks387 @inlineCallbacks
388 def test_get_boot_method_reader_uses_same_client(self):
389 # Fake configuration parameters, as discovered from the file path.
390 fake_params = {"mac": factory.make_mac_address("-")}
391 # Fake kernel configuration parameters, as returned from the RPC call.
392 fake_kernel_params = make_kernel_parameters()
393 fake_params = fake_kernel_params._asdict()
394
395 # Stub the output of list_boot_images so the label is set in the
396 # kernel parameters.
397 boot_image = {
398 "osystem": fake_params["osystem"],
399 "release": fake_params["release"],
400 "architecture": fake_params["arch"],
401 "subarchitecture": fake_params["subarch"],
402 "purpose": fake_params["purpose"],
403 "supported_subarches": "",
404 "label": fake_params["label"],
405 }
406 self.patch(tftp_module, "list_boot_images").return_value = [boot_image]
407 del fake_params["label"]
408
409 # Stub RPC call to return the fake configuration parameters.
410 clients = []
411 for _ in range(10):
412 client = Mock()
413 client.localIdent = factory.make_name("system_id")
414 client.side_effect = lambda *args, **kwargs: (
415 succeed(dict(fake_params)))
416 clients.append(client)
417 client_service = Mock()
418 client_service.getClientNow.side_effect = [
419 succeed(client)
420 for client in clients
421 ]
422 client_service.getAllClients.return_value = clients
423
424 # get_boot_method_reader() takes a dict() of parameters and returns an
425 # `IReader` of a PXE configuration, rendered by
426 # `PXEBootMethod.get_reader`.
427 backend = TFTPBackend(
428 self.make_dir(), client_service)
429
430 # Stub get_reader to return the render parameters.
431 method = PXEBootMethod()
432 fake_render_result = factory.make_name("render").encode("utf-8")
433 render_patch = self.patch(method, "get_reader")
434 render_patch.return_value = BytesReader(fake_render_result)
435
436 # Get the reader once.
437 remote_ip = factory.make_ipv4_address()
438 params_with_ip = dict(fake_params)
439 params_with_ip['remote_ip'] = remote_ip
440 reader = yield backend.get_boot_method_reader(method, params_with_ip)
441 self.addCleanup(reader.finish)
442
443 # Get the reader twice.
444 params_with_ip = dict(fake_params)
445 params_with_ip['remote_ip'] = remote_ip
446 reader = yield backend.get_boot_method_reader(method, params_with_ip)
447 self.addCleanup(reader.finish)
448
449 # Only one client is saved.
450 self.assertEquals(clients[0], backend.client_to_remote[remote_ip])
451
452 # Only the first client should have been called twice, and all the
453 # other clients should not have been called.
454 self.assertEquals(2, clients[0].call_count)
455 for idx in range(1, 10):
456 self.assertThat(clients[idx], MockNotCalled())
457
458 @inlineCallbacks
459 def test_get_boot_method_reader_uses_different_clients(self):
460 # Fake configuration parameters, as discovered from the file path.
461 fake_params = {"mac": factory.make_mac_address("-")}
462 # Fake kernel configuration parameters, as returned from the RPC call.
463 fake_kernel_params = make_kernel_parameters()
464 fake_params = fake_kernel_params._asdict()
465
466 # Stub the output of list_boot_images so the label is set in the
467 # kernel parameters.
468 boot_image = {
469 "osystem": fake_params["osystem"],
470 "release": fake_params["release"],
471 "architecture": fake_params["arch"],
472 "subarchitecture": fake_params["subarch"],
473 "purpose": fake_params["purpose"],
474 "supported_subarches": "",
475 "label": fake_params["label"],
476 }
477 self.patch(tftp_module, "list_boot_images").return_value = [boot_image]
478 del fake_params["label"]
479
480 # Stub RPC call to return the fake configuration parameters.
481 clients = []
482 for _ in range(10):
483 client = Mock()
484 client.localIdent = factory.make_name("system_id")
485 client.side_effect = lambda *args, **kwargs: (
486 succeed(dict(fake_params)))
487 clients.append(client)
488 client_service = Mock()
489 client_service.getClientNow.side_effect = [
490 succeed(client)
491 for client in clients
492 ]
493 client_service.getAllClients.return_value = clients
494
495 # get_boot_method_reader() takes a dict() of parameters and returns an
496 # `IReader` of a PXE configuration, rendered by
497 # `PXEBootMethod.get_reader`.
498 backend = TFTPBackend(
499 self.make_dir(), client_service)
500
501 # Stub get_reader to return the render parameters.
502 method = PXEBootMethod()
503 fake_render_result = factory.make_name("render").encode("utf-8")
504 render_patch = self.patch(method, "get_reader")
505 render_patch.return_value = BytesReader(fake_render_result)
506
507 # Get the reader once.
508 remote_ip_one = factory.make_ipv4_address()
509 params_with_ip = dict(fake_params)
510 params_with_ip['remote_ip'] = remote_ip_one
511 reader = yield backend.get_boot_method_reader(method, params_with_ip)
512 self.addCleanup(reader.finish)
513
514 # Get the reader twice.
515 remote_ip_two = factory.make_ipv4_address()
516 params_with_ip = dict(fake_params)
517 params_with_ip['remote_ip'] = remote_ip_two
518 reader = yield backend.get_boot_method_reader(method, params_with_ip)
519 self.addCleanup(reader.finish)
520
521 # The both clients are saved.
522 self.assertEquals(clients[0], backend.client_to_remote[remote_ip_one])
523 self.assertEquals(clients[1], backend.client_to_remote[remote_ip_two])
524
525 # Only the first and second client should have been called once, and
526 # all the other clients should not have been called.
527 self.assertEquals(1, clients[0].call_count)
528 self.assertEquals(1, clients[1].call_count)
529 for idx in range(2, 10):
530 self.assertThat(clients[idx], MockNotCalled())
531
532 @inlineCallbacks
533 def test_get_boot_method_reader_grabs_new_client_on_lost_conn(self):
534 # Fake configuration parameters, as discovered from the file path.
535 fake_params = {"mac": factory.make_mac_address("-")}
536 # Fake kernel configuration parameters, as returned from the RPC call.
537 fake_kernel_params = make_kernel_parameters()
538 fake_params = fake_kernel_params._asdict()
539
540 # Stub the output of list_boot_images so the label is set in the
541 # kernel parameters.
542 boot_image = {
543 "osystem": fake_params["osystem"],
544 "release": fake_params["release"],
545 "architecture": fake_params["arch"],
546 "subarchitecture": fake_params["subarch"],
547 "purpose": fake_params["purpose"],
548 "supported_subarches": "",
549 "label": fake_params["label"],
550 }
551 self.patch(tftp_module, "list_boot_images").return_value = [boot_image]
552 del fake_params["label"]
553
554 # Stub RPC call to return the fake configuration parameters.
555 clients = []
556 for _ in range(10):
557 client = Mock()
558 client.localIdent = factory.make_name("system_id")
559 client.side_effect = lambda *args, **kwargs: (
560 succeed(dict(fake_params)))
561 clients.append(client)
562 client_service = Mock()
563 client_service.getClientNow.side_effect = [
564 succeed(client)
565 for client in clients
566 ]
567 client_service.getAllClients.side_effect = [
568 clients[1:],
569 clients[2:],
570 ]
571
572 # get_boot_method_reader() takes a dict() of parameters and returns an
573 # `IReader` of a PXE configuration, rendered by
574 # `PXEBootMethod.get_reader`.
575 backend = TFTPBackend(
576 self.make_dir(), client_service)
577
578 # Stub get_reader to return the render parameters.
579 method = PXEBootMethod()
580 fake_render_result = factory.make_name("render").encode("utf-8")
581 render_patch = self.patch(method, "get_reader")
582 render_patch.return_value = BytesReader(fake_render_result)
583
584 # Get the reader once.
585 remote_ip = factory.make_ipv4_address()
586 params_with_ip = dict(fake_params)
587 params_with_ip['remote_ip'] = remote_ip
588 reader = yield backend.get_boot_method_reader(method, params_with_ip)
589 self.addCleanup(reader.finish)
590
591 # The first client is now saved.
592 self.assertEquals(clients[0], backend.client_to_remote[remote_ip])
593
594 # Get the reader twice.
595 params_with_ip = dict(fake_params)
596 params_with_ip['remote_ip'] = remote_ip
597 reader = yield backend.get_boot_method_reader(method, params_with_ip)
598 self.addCleanup(reader.finish)
599
600 # The second client is now saved.
601 self.assertEquals(clients[1], backend.client_to_remote[remote_ip])
602
603 # Only the first and second client should have been called once, and
604 # all the other clients should not have been called.
605 self.assertEquals(1, clients[0].call_count)
606 self.assertEquals(1, clients[1].call_count)
607 for idx in range(2, 10):
608 self.assertThat(clients[idx], MockNotCalled())
609
610 @inlineCallbacks
388 def test_get_boot_method_reader_returns_rendered_params(self):611 def test_get_boot_method_reader_returns_rendered_params(self):
389 # Fake configuration parameters, as discovered from the file path.612 # Fake configuration parameters, as discovered from the file path.
390 fake_params = {"mac": factory.make_mac_address("-")}613 fake_params = {"mac": factory.make_mac_address("-")}
diff --git a/src/provisioningserver/rackdservices/tftp.py b/src/provisioningserver/rackdservices/tftp.py
index 5da8a60..eaea735 100644
--- a/src/provisioningserver/rackdservices/tftp.py
+++ b/src/provisioningserver/rackdservices/tftp.py
@@ -67,6 +67,7 @@ from twisted.internet.defer import (
67 inlineCallbacks,67 inlineCallbacks,
68 maybeDeferred,68 maybeDeferred,
69 returnValue,69 returnValue,
70 succeed,
70)71)
71from twisted.internet.task import deferLater72from twisted.internet.task import deferLater
72from twisted.python.filepath import FilePath73from twisted.python.filepath import FilePath
@@ -164,9 +165,48 @@ class TFTPBackend(FilesystemSynchronousBackend):
164 base_path = FilePath(base_path)165 base_path = FilePath(base_path)
165 super(TFTPBackend, self).__init__(166 super(TFTPBackend, self).__init__(
166 base_path, can_read=True, can_write=False)167 base_path, can_read=True, can_write=False)
168 self.client_to_remote = {}
167 self.client_service = client_service169 self.client_service = client_service
168 self.fetcher = RPCFetcher()170 self.fetcher = RPCFetcher()
169171
172 def _get_new_client_for_remote(self, remote_ip):
173 """Return a new client for the `remote_ip`.
174
175 Don't use directly called from `get_client_for`.
176 """
177 def store_client(client):
178 self.client_to_remote[remote_ip] = client
179 return client
180
181 d = self.client_service.getClientNow()
182 d.addCallback(store_client)
183 return d
184
185 def get_client_for(self, params):
186 """Always gets the same client based on `params`.
187
188 This is done so that all TFTP requests from the same remote client go
189 to the same regiond process. `RPCFetcher` only duplciate on the client
190 and arguments, so if the client is not the same the duplicate effort
191 is not consolidated.
192 """
193 remote_ip = params.get('remote_ip')
194 if remote_ip:
195 client = self.client_to_remote.get(remote_ip, None)
196 if client is None:
197 # Get a new client for the remote_ip.
198 return self._get_new_client_for_remote(remote_ip)
199 else:
200 # Check that the existing client is still valid.
201 clients = self.client_service.getAllClients()
202 if client in clients:
203 return succeed(client)
204 else:
205 del self.client_to_remote[remote_ip]
206 return self._get_new_client_for_remote(remote_ip)
207 else:
208 return self.client_service.getClientNow()
209
170 @inlineCallbacks210 @inlineCallbacks
171 @typed211 @typed
172 def get_boot_method(self, file_name: TFTPPath):212 def get_boot_method(self, file_name: TFTPPath):
@@ -259,7 +299,7 @@ class TFTPBackend(FilesystemSynchronousBackend):
259 d.addCallback(lambda data: KernelParameters(**data))299 d.addCallback(lambda data: KernelParameters(**data))
260 return d300 return d
261301
262 d = self.client_service.getClientNow()302 d = self.get_client_for(params)
263 d.addCallback(fetch, params)303 d.addCallback(fetch, params)
264 return d304 return d
265305

Subscribers

People subscribed via source and target branches