← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~jtv/maas/register-new-leases-in-omshell into lp:maas

 

Jeroen T. Vermeulen has proposed merging lp:~jtv/maas/register-new-leases-in-omshell into lp:maas.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~jtv/maas/register-new-leases-in-omshell/+merge/118714

Pre-imped with Julian.  Originally the plan was to have the MAAS server tell the worker to do this, whenever the worker made an update_leases request to the API.  But there's really no point to the round-trip.  Better to kick off the Omshell update and the update_leases from the same place.  I had to build some worker-side retention logic for the omshell credentials (in another branch, already landed), but we need that logic for API credentials anyway.  The Omshell registration is added in such a way that if it fails, the worker more or less forgets that it ever made the attempt.  It will simply retry next time.  This is different for the update_leases call, since that call is non-incremental — a call to update_leases does not care at all whether any previous calls failed or succeeded.

A lot of the work on this branch was actually the fight to get the fakes and such right in the tests: making sure that the Omshell doesn't really fork any processes, getting the right fake leases file back after simulating an update, and so on.  I also unified the small bit of code that was repeated between update_leases and upload_leases into a new function process_leases, which affected several tests.

I found it a bit hard to get granularities right for all these interacting functions.  Generally I wanted at least some integration testing across multiple levels of call, and most of these functions don't have much control flow to unit-test.  So you'll see that in many tests, the call tree goes about as deep as it can before hitting a test double.

You'll also notice that send_leases is still empty.  That's a separate card on the board.

Something that's clearly inefficient here is that on startup, the worker will re-register all its existing DHCP leases with omshell.  If that turns out to be a problem we may have to get clever.  But for now it's comforting to know that the algorithm is too stupid to be fooled.


Jeroen
-- 
https://code.launchpad.net/~jtv/maas/register-new-leases-in-omshell/+merge/118714
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~jtv/maas/register-new-leases-in-omshell into lp:maas.
=== modified file 'src/provisioningserver/dhcp/leases.py'
--- src/provisioningserver/dhcp/leases.py	2012-08-08 02:55:48 +0000
+++ src/provisioningserver/dhcp/leases.py	2012-08-08 11:10:25 +0000
@@ -101,9 +101,47 @@
     recorded_leases = leases
 
 
+def identify_new_leases(current_leases):
+    """Return a dict of those leases that weren't previousl recorded.
+
+    :param current_leases: A dict mapping IP addresses to the respective
+        MAC addresses that own them.
+    """
+    if recorded_leases is None:
+        return current_leases
+    else:
+        return {
+            ip: current_leases[ip]
+            for ip in set(current_leases).difference(recorded_leases)}
+
+
+def register_new_leases(current_leases):
+    """Register new DHCP leases with the OMAPI.
+
+    :param current_leases: A dict mapping IP addresses to the respective
+        MAC addresses that own them.
+    """
+    # Avoid circular imports.
+    from provisioningserver.tasks import add_new_dhcp_host_map
+
+    omapi_key = recorded_omapi_shared_key
+    if omapi_key is not None:
+        new_leases = identify_new_leases(current_leases)
+        add_new_dhcp_host_map(new_leases, 'localhost', omapi_key)
+
+
 def send_leases(leases):
-    """Send snapshot of current leases to the MAAS server."""
-    # TODO: Implement API call for uploading leases.
+    """Send lease updates to the server API."""
+
+
+def process_leases(timestamp, leases):
+    """Register leases with the DHCP server, and send to the MAAS server."""
+    # Register new leases before recording them.  That way, any
+    # failure to register a lease will cause it to be retried at the
+    # next opportunity.
+    register_new_leases(leases)
+    record_lease_state(timestamp, leases)
+    send_leases(leases)
 
 
 def upload_leases():
@@ -115,8 +153,7 @@
     way to the DNS server.
     """
     timestamp, leases = parse_leases_file()
-    record_lease_state(timestamp, leases)
-    send_leases(leases)
+    process_leases(timestamp, leases)
 
 
 def update_leases():
@@ -129,5 +166,4 @@
     updated_lease_info = check_lease_changes()
     if updated_lease_info is not None:
         timestamp, leases = updated_lease_info
-        record_lease_state(timestamp, leases)
-        send_leases(leases)
+        process_leases(timestamp, leases)

=== modified file 'src/provisioningserver/dhcp/tests/test_leases.py'
--- src/provisioningserver/dhcp/tests/test_leases.py	2012-08-08 03:14:10 +0000
+++ src/provisioningserver/dhcp/tests/test_leases.py	2012-08-08 11:10:25 +0000
@@ -28,12 +28,17 @@
 from provisioningserver.dhcp import leases as leases_module
 from provisioningserver.dhcp.leases import (
     check_lease_changes,
+    identify_new_leases,
     parse_leases_file,
+    process_leases,
     record_lease_state,
     record_omapi_shared_key,
+    register_new_leases,
     update_leases,
     upload_leases,
     )
+from provisioningserver.omshell import Omshell
+from testtools.testcase import ExpectedException
 
 
 class TestHelpers(TestCase):
@@ -156,12 +161,13 @@
         record_lease_state(datetime.utcnow(), leases.copy())
         self.assertIsNone(check_lease_changes())
 
-    def test_update_leases_sends_leases_if_changed(self):
+    def test_update_leases_processes_leases_if_changed(self):
         record_lease_state(None, None)
         send_leases = FakeMethod()
         self.patch(leases_module, 'send_leases', send_leases)
         leases = self.make_lease()
         self.fake_leases_file(leases)
+        self.patch(Omshell, 'create', FakeMethod())
         update_leases()
         self.assertSequenceEqual([(leases, )], send_leases.extract_args())
 
@@ -173,25 +179,60 @@
         record_lease_state(get_write_time(leases_file), leases.copy())
         self.assertSequenceEqual([], send_leases.calls)
 
-    def test_update_leases_records_update(self):
+    def test_process_leases_records_update(self):
         record_lease_state(None, None)
-        self.fake_leases_file()
+        self.patch(leases_module, 'recorded_omapi_shared_key', None)
+        self.fake_leases_file({})
         self.patch(leases_module, 'send_leases', FakeMethod())
-        update_leases()
+        new_leases = {
+            factory.getRandomIPAddress(): factory.getRandomMACAddress(),
+        }
+        self.fake_leases_file(new_leases)
+        process_leases(datetime.utcnow(), new_leases)
         self.assertIsNone(check_lease_changes())
 
-    def test_update_leases_records_state_before_sending(self):
+    def test_process_leases_records_state_before_sending(self):
         record_lease_state(None, None)
-        self.fake_leases_file()
+        self.patch(Omshell, 'create', FakeMethod())
+        self.fake_leases_file({})
         self.patch(
             leases_module, 'send_leases', FakeMethod(failure=StopExecuting()))
+        new_leases = {
+            factory.getRandomIPAddress(): factory.getRandomMACAddress(),
+        }
         try:
-            update_leases()
+            process_leases(datetime.utcnow(), new_leases)
         except StopExecuting:
             pass
+        self.fake_leases_file(new_leases)
         self.assertIsNone(check_lease_changes())
 
-    def test_upload_leases_sends_leases_unconditionally(self):
+    def test_process_leases_registers_new_leases(self):
+        record_lease_state(None, None)
+        self.patch(Omshell, 'create', FakeMethod())
+        ip = factory.getRandomIPAddress()
+        mac = factory.getRandomMACAddress()
+        process_leases(datetime.utcnow(), {ip: mac})
+        self.assertEqual([(ip, mac)], Omshell.create.extract_args())
+
+    def test_process_leases_retries_failed_registrations(self):
+
+        class OmshellFailure(Exception):
+            pass
+
+        record_lease_state(None, None)
+        self.patch(Omshell, 'create', FakeMethod(failure=OmshellFailure()))
+        ip = factory.getRandomIPAddress()
+        mac = factory.getRandomMACAddress()
+        with ExpectedException(OmshellFailure):
+            process_leases(datetime.utcnow(), {ip: mac})
+        # At this point {ip: mac} has not been successfully registered.
+        # But if we re-run process_leases later, it will retry.
+        self.patch(Omshell, 'create', FakeMethod())
+        process_leases(datetime.utcnow(), {ip: mac})
+        self.assertEqual([(ip, mac)], Omshell.create.extract_args())
+
+    def test_upload_leases_processes_leases_unconditionally(self):
         send_leases = FakeMethod()
         leases = self.make_lease()
         leases_file = self.fake_leases_file(leases)
@@ -200,24 +241,6 @@
         upload_leases()
         self.assertSequenceEqual([(leases, )], send_leases.extract_args())
 
-    def test_upload_leases_records_update(self):
-        record_lease_state(None, None)
-        self.fake_leases_file()
-        self.patch(leases_module, 'send_leases', FakeMethod())
-        upload_leases()
-        self.assertIsNone(check_lease_changes())
-
-    def test_upload_leases_records_state_before_sending(self):
-        record_lease_state(None, None)
-        self.fake_leases_file()
-        self.patch(
-            leases_module, 'send_leases', FakeMethod(failure=StopExecuting()))
-        try:
-            upload_leases()
-        except StopExecuting:
-            pass
-        self.assertIsNone(check_lease_changes())
-
     def test_parse_leases_file_parses_leases(self):
         params = {
             'ip': factory.getRandomIPAddress(),
@@ -236,3 +259,60 @@
         self.assertEqual(
             (get_write_time(leases_file), {params['ip']: params['mac']}),
             parse_leases_file())
+
+    def test_identify_new_leases_identifies_everything_first_time(self):
+        self.patch(leases_module, 'recorded_leases', None)
+        current_leases = {
+            factory.getRandomIPAddress(): factory.getRandomMACAddress(),
+            factory.getRandomIPAddress(): factory.getRandomMACAddress(),
+        }
+        self.assertEqual(current_leases, identify_new_leases(current_leases))
+
+    def test_identify_new_leases_identifies_previously_unknown_leases(self):
+        self.patch(leases_module, 'recorded_leases', {})
+        current_leases = {
+            factory.getRandomIPAddress(): factory.getRandomMACAddress(),
+        }
+        self.assertEqual(current_leases, identify_new_leases(current_leases))
+
+    def test_identify_new_leases_leaves_out_previously_known_leases(self):
+        old_leases = {
+            factory.getRandomIPAddress(): factory.getRandomMACAddress(),
+        }
+        self.patch(leases_module, 'recorded_leases', old_leases)
+        new_leases = {
+            factory.getRandomIPAddress(): factory.getRandomMACAddress(),
+        }
+        current_leases = old_leases.copy()
+        current_leases.update(new_leases)
+        self.assertEqual(new_leases, identify_new_leases(current_leases))
+
+    def test_register_new_leases_registers_new_leases(self):
+        self.patch(Omshell, 'create', FakeMethod())
+        self.patch(leases_module, 'recorded_leases', None)
+        self.patch(leases_module, 'recorded_omapi_shared_key', 'omapi-key')
+        ip = factory.getRandomIPAddress()
+        mac = factory.getRandomMACAddress()
+        register_new_leases({ip: mac})
+        [create_args] = Omshell.create.extract_args()
+        self.assertEqual((ip, mac), create_args)
+
+    def test_register_new_leases_ignores_known_leases(self):
+        self.patch(Omshell, 'create', FakeMethod())
+        self.patch(leases_module, 'recorded_omapi_shared_key', 'omapi-key')
+        old_leases = {
+            factory.getRandomIPAddress(): factory.getRandomMACAddress(),
+        }
+        self.patch(leases_module, 'recorded_leases', old_leases)
+        register_new_leases(old_leases)
+        self.assertEqual([], Omshell.create.calls)
+
+    def test_register_new_leases_does_nothing_without_omapi_key(self):
+        self.patch(Omshell, 'create', FakeMethod())
+        self.patch(leases_module, 'recorded_leases', None)
+        self.patch(leases_module, 'recorded_omapi_shared_key', None)
+        new_leases = {
+            factory.getRandomIPAddress(): factory.getRandomMACAddress(),
+        }
+        register_new_leases(new_leases)
+        self.assertEqual([], Omshell.create.calls)

=== modified file 'src/provisioningserver/tasks.py'
--- src/provisioningserver/tasks.py	2012-08-08 05:54:08 +0000
+++ src/provisioningserver/tasks.py	2012-08-08 11:10:25 +0000
@@ -29,8 +29,10 @@
 
 from celery.task import task
 from celeryconfig import DHCP_CONFIG_FILE
-from provisioningserver.dhcp import config
-from provisioningserver.dhcp.leases import record_omapi_shared_key
+from provisioningserver.dhcp import (
+    config,
+    leases,
+    )
 from provisioningserver.dns.config import (
     DNSConfig,
     execute_rndc_command,
@@ -45,7 +47,7 @@
 
 # For each item passed to refresh_secrets, a refresh function to give it to.
 refresh_functions = {
-    'omapi_shared_key': record_omapi_shared_key,
+    'omapi_shared_key': leases.record_omapi_shared_key,
 }