Petr Horáček has uploaded a new change for review.
Change subject: utils: atomic write ......................................................................
utils: atomic write
Atomic writes are needed for safe file editation. This function will be used in following network patch 'acquire external interfaces'.
Change-Id: Icbb5a2d3ac439a334db2c9075376f219c356762c Bug-Url: https://bugzilla.redhat.com/1195208 Signed-off-by: Petr Horáček phoracek@redhat.com --- M lib/vdsm/utils.py M tests/utilsTests.py 2 files changed, 73 insertions(+), 3 deletions(-)
git pull ssh://gerrit.ovirt.org:29418/vdsm refs/changes/82/61482/1
diff --git a/lib/vdsm/utils.py b/lib/vdsm/utils.py index 0beac90..10888bb 100644 --- a/lib/vdsm/utils.py +++ b/lib/vdsm/utils.py @@ -834,8 +834,7 @@ suffix, e.g. dummy_ilXaYiSn7. The name is bound to IFNAMSIZ of 16-1 chars. """ suffix_len = max_length - len(prefix) - suffix = ''.join(random.choice(string.ascii_letters + string.digits) - for _ in range(suffix_len)) + suffix = _random_alnum_string(suffix_len) return prefix + suffix
@@ -929,3 +928,43 @@ elif len(keys) == 0: return dict return rget(dict.get(keys[0]), keys[1:], default) + + +@contextmanager +def atomic_write(path, flag): + """Atomically write into a file. + + Usage: + + with atomic_write('foo.txt', 'w') as f: + f.write('shrubery') + # there are no changes on foo.txt yet + # now it is changed + + Temporary copy of the original is created in the same folder == same file + system. + """ + tmp_path = _generate_tmp_path(path) + if os.path.exists(path): + shutil.copyfile(path, tmp_path) + try: + with open(tmp_path, flag) as f: + yield f + except: + raise + else: + shutil.copyfile(tmp_path, path) + finally: + os.remove(tmp_path) + + +def _generate_tmp_path(path): + id = _random_alnum_string(8) + tmp_path = '{}.{}.tmp'.format(path, id) + return tmp_path + + +def _random_alnum_string(length): + return ''.join( + random.choice(string.ascii_letters + string.digits) + for _ in range(length)) diff --git a/tests/utilsTests.py b/tests/utilsTests.py index 975cb0b..1fc4f95 100644 --- a/tests/utilsTests.py +++ b/tests/utilsTests.py @@ -47,7 +47,7 @@ from monkeypatch import MonkeyPatch, MonkeyPatchScope from vmTestsData import VM_STATUS_DUMP from monkeypatch import Patch -from testlib import forked, online_cpus +from testlib import forked, online_cpus, namedTemporaryDir from testlib import permutations, expandPermutations from testlib import VdsmTestCase as TestCaseBase from testValidation import brokentest @@ -1059,3 +1059,34 @@ proc.start() self._noIntrWatchFd(myPipe, isEpoll=False, mask=select.POLLIN) proc.join() + + +class AtomicWriteTest(TestCaseBase): + + def test_create_a_new_file(self): + TEST_TEXT = 'shrubery' + + with namedTemporaryDir() as tmp_dir: + test_file_path = os.path.join(tmp_dir, 'foo.txt') + with utils.atomic_write(test_file_path, 'w') as f: + f.write(TEST_TEXT) + self.assertFalse(os.path.exists(test_file_path)) + self._assert_file_contains(test_file_path, TEST_TEXT) + + def test_edit_file(self): + TEST_TEXT_1 = 'foo' + TEST_TEXT_2 = 'bar' + + with namedTemporaryDir() as tmp_dir: + test_file_path = os.path.join(tmp_dir, 'foo.txt') + with open(test_file_path, 'w') as f: + f.write(TEST_TEXT_1) + with utils.atomic_write(test_file_path, 'w') as f: + f.write(TEST_TEXT_2) + self._assert_file_contains(test_file_path, TEST_TEXT_1) + self._assert_file_contains(test_file_path, TEST_TEXT_2) + + def _assert_file_contains(self, path, expected_content): + with open(path) as f: + content = f.read() + self.assertEqual(content, expected_content)