From 972c24f8b11d5a3e7e6fc341453d9733b2bb47b5 Mon Sep 17 00:00:00 2001 From: Prashanth Pai Date: Mon, 30 May 2016 17:42:15 +0530 Subject: Implement os.utime() like API and zerofill This patch: * Implements Volume.utime() which is very similar to os.utime() present in Python. https://docs.python.org/2/library/os.html#os.utime * Implements File.zerofill() which exposes glfs_zerofill. * Fixes function prototype of fallocate and discard. Adds functional tests for the same. Change-Id: Icb8d3a571998c31d6bf9b139ca253af59f6fc3f4 Signed-off-by: Prashanth Pai --- gluster/api.py | 31 +++++++++++++---- gluster/gfapi.py | 45 ++++++++++++++++++++++-- test/functional/libgfapi-python-tests.py | 59 ++++++++++++++++++++++++++++++++ test/unit/gluster/test_gfapi.py | 34 ++++++++++++++++++ 4 files changed, 160 insertions(+), 9 deletions(-) diff --git a/gluster/api.py b/gluster/api.py index 7163076..3fd9d91 100755 --- a/gluster/api.py +++ b/gluster/api.py @@ -115,6 +115,13 @@ class Dirent (ctypes.Structure): ] +class Timespec (ctypes.Structure): + _fields_ = [ + ('tv_sec', ctypes.c_long), + ('tv_nsec', ctypes.c_long) + ] + + # Here is the reference card of libgfapi library exported # apis with its different versions. # @@ -474,12 +481,22 @@ glfs_getcwd = gfapi_prototype('glfs_getcwd', ctypes.c_char_p, ctypes.c_char_p, ctypes.c_size_t) +glfs_fallocate = gfapi_prototype('glfs_fallocate', ctypes.c_int, + ctypes.c_void_p, + ctypes.c_int, + ctypes.c_size_t) + +glfs_discard = gfapi_prototype('glfs_discard', ctypes.c_int, + ctypes.c_void_p, + ctypes.c_int, + ctypes.c_size_t) -# TODO: # discard and fallocate fails with "AttributeError: /lib64/libgfapi.so.0: undefined symbol: glfs_discard", # noqa -# for time being, using it from api.* # noqa -# glfs_discard = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_ulong, ctypes.c_size_t)(('glfs_discard', client)) # noqa -# _glfs_fallocate = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_int, ctypes.c_ulong, ctypes.c_size_t) # noqa -# (('glfs_fallocate', client)) # noqa +glfs_zerofill = gfapi_prototype('glfs_zerofill', ctypes.c_int, + ctypes.c_void_p, + ctypes.c_int, + ctypes.c_size_t) -# glfs_discard = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_ulong, ctypes.c_size_t)(('glfs_discard', client)) # noqa -# glfs_fallocate = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_int, ctypes.c_ulong, ctypes.c_size_t)(('glfs_fallocate', client)) # noqa +glfs_utimens = gfapi_prototype('glfs_utimens', ctypes.c_int, + ctypes.c_void_p, + ctypes.c_char_p, + ctypes.POINTER(Timespec)) diff --git a/gluster/gfapi.py b/gluster/gfapi.py index c84b262..c409f62 100755 --- a/gluster/gfapi.py +++ b/gluster/gfapi.py @@ -11,6 +11,8 @@ import ctypes import os +import math +import time import stat import errno from gluster import api @@ -87,7 +89,7 @@ class File(object): self._closed = True def discard(self, offset, len): - ret = api.client.glfs_discard(self.fd, offset, len) + ret = api.glfs_discard(self.fd, offset, len) if ret < 0: err = ctypes.get_errno() raise OSError(err, os.strerror(err)) @@ -111,7 +113,7 @@ class File(object): :param offset: Starting offset :param length: Size in bytes, starting at offset """ - ret = api.client.glfs_fallocate(self.fd, mode, offset, length) + ret = api.glfs_fallocate(self.fd, mode, offset, length) if ret < 0: err = ctypes.get_errno() raise OSError(err, os.strerror(err)) @@ -370,6 +372,12 @@ class File(object): raise OSError(err, os.strerror(err)) return ret + def zerofill(self, offset, length): + ret = api.glfs_zerofill(self.fd, offset, length) + if ret < 0: + err = ctypes.get_errno() + raise OSError(err, os.strerror(err)) + class Dir(object): @@ -1036,6 +1044,39 @@ class Volume(object): err = ctypes.get_errno() raise OSError(err, os.strerror(err)) + def utime(self, path, times): + """ + Set the access and modified times of the file specified by path. If + times is None, then the file's access and modified times are set to + the current time. (The effect is similar to running the Unix program + touch on the path.) Otherwise, times must be a 2-tuple of numbers, + of the form (atime, mtime) which is used to set the access and + modified times, respectively. + """ + if times is None: + now = time.time() + times = (now, now) + else: + if type(times) is not tuple or len(times) != 2: + raise TypeError("utime() arg 2 must be a tuple (atime, mtime)") + + timespec_array = (api.Timespec * 2)() + + # Set atime + decimal, whole = math.modf(times[0]) + timespec_array[0].tv_sec = int(whole) + timespec_array[0].tv_nsec = int(decimal * 1e9) + + # Set mtime + decimal, whole = math.modf(times[1]) + timespec_array[1].tv_sec = int(whole) + timespec_array[1].tv_nsec = int(decimal * 1e9) + + ret = api.glfs_utimens(self.fs, path, timespec_array) + if ret < 0: + err = ctypes.get_errno() + raise OSError(err, os.strerror(err)) + def walk(self, top, topdown=True, onerror=None, followlinks=False): """ Directory tree generator. Yields a 3-tuple dirpath, dirnames, filenames diff --git a/test/functional/libgfapi-python-tests.py b/test/functional/libgfapi-python-tests.py index 591fe7b..c29f400 100644 --- a/test/functional/libgfapi-python-tests.py +++ b/test/functional/libgfapi-python-tests.py @@ -443,6 +443,65 @@ class FileOpsTest(unittest.TestCase): # So should be the content. self.assertEqual(f.read(), "12345") + def test_fallocate(self): + name = uuid4().hex + with File(self.vol.open(name, os.O_WRONLY | os.O_CREAT)) as f: + f.fallocate(0, 0, 10) + f.fsync() + # Stat information should now show the allocated size. + self.assertEqual(f.fstat().st_size, 10) + + def test_discard(self): + name = uuid4().hex + with File(self.vol.open(name, os.O_WRONLY | os.O_CREAT)) as f: + f.fallocate(0, 0, 10) + f.fsync() + self.assertEqual(f.fstat().st_size, 10) + # We can't really know if the blocks were actually returned + # to filesystem. This functional test only tests if glfs_discard + # interfacing is proper and that it returns successfully. + f.discard(4, 5) + + def test_zerofill(self): + name = uuid4().hex + with File(self.vol.open(name, os.O_RDWR | os.O_CREAT)) as f: + f.write('0123456789') + f.fsync() + self.assertEqual(f.fstat().st_size, 10) + f.lseek(0, os.SEEK_SET) + self.assertEqual(f.read(), '0123456789') + f.zerofill(3, 6) + f.lseek(0, os.SEEK_SET) + data = f.read() + self.assertEqual(data, '012\x00\x00\x00\x00\x00\x009') + self.assertEqual(len(data), 10) + + def test_utime(self): + # Create a file + name = uuid4().hex + self.vol.fopen(name, 'w').close() + + # Test times arg being invalid + for junk in ('a', 1234.1234, (1, 2, 3), (1)): + self.assertRaises(TypeError, self.vol.utime, name, junk) + + # Test normal success + # Mission Report: December 16th, 1991 + (atime, mtime) = (692884800, 692884800) + self.vol.utime(name, (atime, mtime)) + st = self.vol.stat(name) + self.assertEqual(st.st_atime, atime) + self.assertEqual(st.st_mtime, mtime) + + # Test times = None + self.vol.utime(name, None) + new_st = self.vol.stat(name) + self.assertTrue(new_st.st_atime > st.st_atime) + self.assertTrue(new_st.st_mtime > st.st_mtime) + + # Non-existent file + self.assertRaises(OSError, self.vol.utime, 'non-existent-file', None) + def test_flistxattr(self): name = uuid4().hex with File(self.vol.open(name, os.O_RDWR | os.O_CREAT)) as f: diff --git a/test/unit/gluster/test_gfapi.py b/test/unit/gluster/test_gfapi.py index 84f6261..3934a6f 100644 --- a/test/unit/gluster/test_gfapi.py +++ b/test/unit/gluster/test_gfapi.py @@ -13,6 +13,8 @@ import unittest import gluster import os import stat +import time +import math import errno from gluster.gfapi import File, Dir, Volume @@ -997,3 +999,35 @@ class TestVolume(unittest.TestCase): for (path, dirs, files) in self.vol.walk("dir1", onerror=mock_onerror): pass + + def test_utime(self): + # Test times arg being invalid. + for junk in ('a', 1234.1234, (1, 2, 3), (1)): + self.assertRaises(TypeError, self.vol.utime, '/path', junk) + + # Test times = None + mock_glfs_utimens = Mock(return_value=1) + mock_time = Mock(return_value=12345.6789) + with patch("gluster.gfapi.api.glfs_utimens", mock_glfs_utimens): + with patch("time.time", mock_time): + self.vol.utime('/path', None) + self.assertTrue(mock_glfs_utimens.called) + self.assertTrue(mock_time.called) + + # Test times passed as arg + mock_glfs_utimens.reset_mock() + atime = time.time() + mtime = time.time() + with patch("gluster.gfapi.api.glfs_utimens", mock_glfs_utimens): + self.vol.utime('/path', (atime, mtime)) + self.assertTrue(mock_glfs_utimens.called) + self.assertEqual(mock_glfs_utimens.call_args[0][1], '/path') + # verify atime and mtime + self.assertEqual(mock_glfs_utimens.call_args[0][2][0].tv_sec, + int(atime)) + self.assertEqual(mock_glfs_utimens.call_args[0][2][0].tv_nsec, + int(math.modf(atime)[0] * 1e9)) + self.assertEqual(mock_glfs_utimens.call_args[0][2][1].tv_sec, + int(mtime)) + self.assertEqual(mock_glfs_utimens.call_args[0][2][1].tv_nsec, + int(math.modf(mtime)[0] * 1e9)) -- cgit