commit 4b61ee6328bb05f339c8420f763b66b9e8e23c7c
Author: Ilias Tsitsimpis <iliastsi@grnet.gr>
Date:   Mon Aug 26 15:34:43 2013 +0300

    ci: Implement file lock mechanism for temp_config
    
    Before writing to temp config file, acquire a lock
    and re-read it's contents to make sure we don't
    overwrite anything.

diff --git a/ci/filelocker.py b/ci/filelocker.py
new file mode 100644
index 0000000..98580f9
--- /dev/null
+++ b/ci/filelocker.py
@@ -0,0 +1,125 @@
+# filelocker.py - Cross-platform (posix/nt) API for flock-style file locking.
+#                 Requires python 1.5.2 or better.
+"""Cross-platform (posix/nt) API for flock-style file locking.
+
+
+Synopsis:
+
+    import filelocker
+    with filelocker.lock("lockfile", filelocker.LOCK_EX):
+        print "Got it"
+
+
+Methods:
+
+   lock ( file, flags, tries=10 )
+
+
+Constants:
+
+   LOCK_EX
+   LOCK_SH
+   LOCK_NB
+
+
+Exceptions:
+
+    LockException
+
+
+Notes:
+
+For the 'nt' platform, this module requires the Python Extensions for Windows.
+Be aware that this may not work as expected on Windows 95/98/ME.
+
+
+History:
+
+I learned the win32 technique for locking files from sample code
+provided by John Nielsen <nielsenjf@my-deja.com> in the documentation
+that accompanies the win32 modules.
+
+
+Author: Jonathan Feinberg <jdf@pobox.com>,
+        Lowell Alleman <lalleman@mfps.com>
+Version: $Id: filelocker.py 5474 2008-05-16 20:53:50Z lowell $
+
+
+Modified to work as a contextmanager
+
+"""
+
+import os
+from contextlib import contextmanager
+
+
+class LockException(Exception):
+    # Error codes:
+    LOCK_FAILED = 1
+
+
+# Import modules for each supported platform
+if os.name == 'nt':
+    import win32con
+    import win32file
+    import pywintypes
+    LOCK_EX = win32con.LOCKFILE_EXCLUSIVE_LOCK
+    LOCK_SH = 0  # the default
+    LOCK_NB = win32con.LOCKFILE_FAIL_IMMEDIATELY
+    # is there any reason not to reuse the following structure?
+    __overlapped = pywintypes.OVERLAPPED()
+elif os.name == 'posix':
+    import fcntl
+    LOCK_EX = fcntl.LOCK_EX
+    LOCK_SH = fcntl.LOCK_SH
+    LOCK_NB = fcntl.LOCK_NB
+else:
+    raise RuntimeError("FileLocker only defined for nt and posix platforms")
+
+
+# --------------------------------------
+# Implementation for NT
+if os.name == 'nt':
+    @contextmanager
+    def lock(filename, flags):
+        file = open(filename, "w+")
+        hfile = win32file._get_osfhandle(file.fileno())
+
+        try:
+            win32file.LockFileEx(hfile, flags, 0, -0x10000, __overlapped)
+            try:
+                yield
+            finally:
+                file.close()
+        except pywintypes.error, exc_value:
+            # error: (33, 'LockFileEx',
+            #         'The process cannot access the file because another
+            #          process has locked a portion of the file.')
+            file.close()
+            if exc_value[0] == 33:
+                raise LockException(LockException.LOCK_FAILED, exc_value[2])
+            else:
+                # Q:  Are there exceptions/codes we should be dealing with?
+                raise
+
+
+# --------------------------------------
+# Implementation for Posix
+elif os.name == 'posix':
+    @contextmanager
+    def lock(filename, flags):
+        file = open(filename, "w+")
+
+        try:
+            fcntl.flock(file.fileno(), flags)
+            try:
+                yield
+            finally:
+                file.close()
+        except IOError, exc_value:
+            #  IOError: [Errno 11] Resource temporarily unavailable
+            file.close()
+            if exc_value[0] == 11:
+                raise LockException(LockException.LOCK_FAILED, exc_value[1])
+            else:
+                raise
diff --git a/ci/utils.py b/ci/utils.py
index 693ea31..868fd79 100644
--- a/ci/utils.py
+++ b/ci/utils.py
@@ -19,6 +19,7 @@ from kamaki.clients.astakos import AstakosClient
 from kamaki.clients.cyclades import CycladesClient
 from kamaki.clients.image import ImageClient
 from kamaki.clients.compute import ComputeClient
+import filelocker
 
 DEFAULT_CONFIG_FILE = "new_config"
 # UUID of owner of system images
@@ -147,10 +148,11 @@ class SynnefoCI(object):
         self.config.read(config_file)
 
         # Read temporary_config file
-        temp_config = self.config.get('Global', 'temporary_config')
+        self.temp_config_file = \
+            os.path.expanduser(self.config.get('Global', 'temporary_config'))
         self.temp_config = ConfigParser()
         self.temp_config.optionxform = str
-        self.temp_config.read(os.path.expanduser(temp_config))
+        self.temp_config.read(self.temp_config_file)
         self.build_id = build_id
         self.logger.info("Will use \"%s\" as build id" % _green(self.build_id))
 
@@ -453,19 +455,27 @@ class SynnefoCI(object):
 
     def write_temp_config(self, option, value):
         """Write changes back to config file"""
-        # If build_id section doesn't exist create a new one
-        try:
-            self.temp_config.add_section(str(self.build_id))
-            creation_time = time.strftime("%a, %d %b %Y %X", time.localtime())
-            self.write_temp_config("created", creation_time)
-        except DuplicateSectionError:
-            pass
-        self.temp_config.set(str(self.build_id), option, str(value))
-        curr_time = time.strftime("%a, %d %b %Y %X", time.localtime())
-        self.temp_config.set(str(self.build_id), "modified", curr_time)
-        temp_conf_file = self.config.get('Global', 'temporary_config')
-        with open(temp_conf_file, 'wb') as tcf:
-            self.temp_config.write(tcf)
+        # Acquire the lock to write to temp_config_file
+        with filelocker.lock("%s.lock" % self.temp_config_file,
+                             filelocker.LOCK_EX):
+
+            # Read temp_config again to get any new entries
+            self.temp_config.read(self.temp_config_file)
+
+            # If build_id section doesn't exist create a new one
+            try:
+                self.temp_config.add_section(str(self.build_id))
+                creation_time = \
+                    time.strftime("%a, %d %b %Y %X", time.localtime())
+                self.temp_config.set(str(self.build_id),
+                                     "created", str(creation_time))
+            except DuplicateSectionError:
+                pass
+            self.temp_config.set(str(self.build_id), option, str(value))
+            curr_time = time.strftime("%a, %d %b %Y %X", time.localtime())
+            self.temp_config.set(str(self.build_id), "modified", curr_time)
+            with open(self.temp_config_file, 'wb') as tcf:
+                self.temp_config.write(tcf)
 
     def read_temp_config(self, option):
         """Read from temporary_config file"""
@@ -545,6 +555,7 @@ class SynnefoCI(object):
             # namely to disable host checking.
             (temp_ssh_file_handle, temp_ssh_file) = tempfile.mkstemp()
             os.close(temp_ssh_file_handle)
+            # XXX: git push doesn't read the password
             cmd = """
             echo 'exec ssh -o "StrictHostKeyChecking no" \
                            -o "UserKnownHostsFile /dev/null" \
