← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~rvb/maas/atomic-modif into lp:maas

 

Raphaël Badin has proposed merging lp:~rvb/maas/atomic-modif into lp:maas with lp:~rvb/maas/dns-config-tasks as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~rvb/maas/atomic-modif/+merge/113601

This branch adds a tiny utility to write a file in an atomic fashion: writing to a temp file and then moving that temp file to the destination path (os.rename is atomic [1]).  This will be useful when writing configuration files as we don't want a server to reload half-written files.

I admit the testing is pretty minimal but it tests that the file gets written (and overrides an existing file) which is all we want from this method.

[1] http://docs.python.org/library/os.html#os.rename
"""If successful, the renaming will be an atomic operation (this is a POSIX requirement)."""

Note that: """On Windows, if dst already exists, OSError will be raised even if it is a file; there may be no way to implement an atomic rename when dst names an existing file.""" but I don't think this is something we should care about here.
-- 
https://code.launchpad.net/~rvb/maas/atomic-modif/+merge/113601
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rvb/maas/atomic-modif into lp:maas.
=== modified file 'src/provisioningserver/dns/config.py'
--- src/provisioningserver/dns/config.py	2012-07-05 15:59:19 +0000
+++ src/provisioningserver/dns/config.py	2012-07-05 15:59:19 +0000
@@ -29,6 +29,7 @@
     )
 
 from celery.conf import conf
+from provisioningserver.utils import atomic_write
 import tempita
 
 
@@ -127,8 +128,7 @@
         template = self.get_template()
         kwargs.update(self.get_extra_context())
         rendered = self.render_template(template, **kwargs)
-        with open(self.target_path, "wb") as f:
-            f.write(rendered)
+        atomic_write(rendered, self.target_path)
 
 
 class DNSConfig(DNSConfigBase):

=== modified file 'src/provisioningserver/tests/test_utils.py'
--- src/provisioningserver/tests/test_utils.py	2012-06-15 14:41:42 +0000
+++ src/provisioningserver/tests/test_utils.py	2012-07-05 15:59:19 +0000
@@ -26,9 +26,11 @@
 from maastesting.testcase import TestCase
 from provisioningserver.utils import (
     ActionScript,
+    atomic_write,
     Safe,
     ShellTemplate,
     )
+from testtools.matchers import FileContains
 
 
 class TestSafe(TestCase):
@@ -45,6 +47,16 @@
         self.assertEqual("<Safe %r>" % string, repr(safe))
 
 
+class TestWriteAtomic(TestCase):
+    """Test `atomic_write`."""
+
+    def test_atomic_write_overwrites_dest_file(self):
+        content = factory.getRandomString()
+        filename = self.make_file(contents=factory.getRandomString())
+        atomic_write(content, filename)
+        self.assertThat(filename, FileContains(content))
+
+
 class TestShellTemplate(TestCase):
     """Test `ShellTemplate`."""
 

=== modified file 'src/provisioningserver/utils.py'
--- src/provisioningserver/utils.py	2012-06-15 14:41:42 +0000
+++ src/provisioningserver/utils.py	2012-07-05 15:59:19 +0000
@@ -14,16 +14,19 @@
     "ActionScript",
     "deferred",
     "ShellTemplate",
+    "atomic_write",
     "xmlrpc_export",
     ]
 
 from argparse import ArgumentParser
 from functools import wraps
+import os
 from os import fdopen
 from pipes import quote
 import signal
 from subprocess import CalledProcessError
 import sys
+import tempfile
 
 import tempita
 from twisted.internet.defer import maybeDeferred
@@ -64,6 +67,23 @@
     return decorate
 
 
+def atomic_write(content, filename):
+    """Write the given `content` into the file `filename` in an atomic
+    fashion.
+    """
+    # Write the file to a temporary place (next to the target destination,
+    # to ensure that it is on the same filesystem).
+    directory = os.path.dirname(filename)
+    _, temp_file = tempfile.mkstemp(dir=directory)
+    with open(temp_file, "wb") as f:
+        f.write(content)
+        f.flush()
+        os.fsync(f.fileno())
+    # Rename the temporary file to `filename`, that operation is atomic on
+    # POSIX systems.
+    os.rename(temp_file, filename)
+
+
 class Safe:
     """An object that is safe to render as-is."""