From ddbd7b570d0a9f599b417a499c912c5b13a003c9 Mon Sep 17 00:00:00 2001 From: Prashanth Pai Date: Sun, 14 Jun 2015 23:57:42 +0530 Subject: Add missing Path based operation APIs * Added commonly used path based operations that were missing from the Volume class. * Fixed function prototypes at places where it should be ssize_t and not size_t. This caused overflow error at times. * Added doc strings wherever they were missing. Change-Id: I6ce28038da3cd0f89ab075045bb4092dd95e21c4 Signed-off-by: Prashanth Pai --- gluster/api.py | 27 ++++- gluster/gfapi.py | 182 ++++++++++++++++++++++++++++--- test/functional/libgfapi-python-tests.py | 129 +++++++++++++++++++--- test/unit/gluster/test_gfapi.py | 11 +- 4 files changed, 307 insertions(+), 42 deletions(-) diff --git a/gluster/api.py b/gluster/api.py index 560ede8..62135c5 100755 --- a/gluster/api.py +++ b/gluster/api.py @@ -320,26 +320,26 @@ glfs_fsync = ctypes.CFUNCTYPE( glfs_lseek = ctypes.CFUNCTYPE(ctypes.c_ulong, ctypes.c_void_p, ctypes.c_ulong, ctypes.c_int)(('glfs_lseek', client)) -glfs_read = ctypes.CFUNCTYPE(ctypes.c_size_t, +glfs_read = ctypes.CFUNCTYPE(ctypes.c_ssize_t, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_int)(('glfs_read', client)) -glfs_write = ctypes.CFUNCTYPE(ctypes.c_size_t, +glfs_write = ctypes.CFUNCTYPE(ctypes.c_ssize_t, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_int)(('glfs_write', client)) -glfs_getxattr = ctypes.CFUNCTYPE(ctypes.c_size_t, +glfs_getxattr = ctypes.CFUNCTYPE(ctypes.c_ssize_t, ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_void_p, ctypes.c_size_t)(('glfs_getxattr', client)) -glfs_listxattr = ctypes.CFUNCTYPE(ctypes.c_size_t, +glfs_listxattr = ctypes.CFUNCTYPE(ctypes.c_ssize_t, ctypes.c_void_p, ctypes.c_char_p, ctypes.c_void_p, @@ -423,6 +423,25 @@ glfs_flistxattr = ctypes.CFUNCTYPE(ctypes.c_ssize_t, ctypes.c_size_t)(('glfs_flistxattr', client)) +glfs_access = ctypes.CFUNCTYPE(ctypes.c_int, + ctypes.c_void_p, + ctypes.c_char_p, + ctypes.c_int)(('glfs_access', client)) + +glfs_readlink = ctypes.CFUNCTYPE(ctypes.c_int, + ctypes.c_void_p, + ctypes.c_char_p, + ctypes.c_char_p, + ctypes.c_size_t)(('glfs_readlink', client)) + +glfs_chdir = ctypes.CFUNCTYPE(ctypes.c_int, + ctypes.c_void_p, + ctypes.c_char_p)(('glfs_chdir', client)) + +glfs_getcwd = ctypes.CFUNCTYPE(ctypes.c_char_p, + ctypes.c_void_p, + ctypes.c_char_p, + ctypes.c_size_t)(('glfs_getcwd', client)) # TODO: creat and open fails on test_create_file_already_exists & test_open_file_not_exist functional testing, # noqa diff --git a/gluster/gfapi.py b/gluster/gfapi.py index 8ba7251..96088ff 100755 --- a/gluster/gfapi.py +++ b/gluster/gfapi.py @@ -499,6 +499,33 @@ class Volume(object): self.log_file = log_file self.log_level = log_level + def access(self, path, mode): + """ + Use the real uid/gid to test for access to path. + + :param path: Path to be checked. + :param mode: mode should be F_OK to test the existence of path, or + it can be the inclusive OR of one or more of R_OK, W_OK, + and X_OK to test permissions + :returns: True if access is allowed, False if not + """ + ret = api.glfs_access(self.fs, path, mode) + if ret == 0: + return True + else: + return False + + def chdir(self, path): + """ + Change the current working directory to the given path. + + :param path: Path to change current working directory to + """ + ret = api.glfs_chdir(self.fs, path) + if ret < 0: + err = ctypes.get_errno() + raise OSError(err, os.strerror(err)) + def chmod(self, path, mode): """ Change mode of path @@ -551,6 +578,18 @@ class Volume(object): """ return self.stat(path).st_ctime + def getcwd(self): + """ + Returns current working directory. + """ + PATH_MAX = 4096 + buf = ctypes.create_string_buffer(PATH_MAX) + ret = api.glfs_getcwd(self.fs, buf, PATH_MAX) + if ret < 0: + err = ctypes.get_errno() + raise OSError(err, os.strerror(err)) + return buf.value + def getmtime(self, path): """ Returns the time when changes were made to the content of the path @@ -564,12 +603,30 @@ class Volume(object): """ return self.stat(filename).st_size - def getxattr(self, path, key, maxlen): - buf = ctypes.create_string_buffer(maxlen) - rc = api.glfs_getxattr(self.fs, path, key, buf, maxlen) + def getxattr(self, path, key, size=0): + """ + Retrieve the value of the extended attribute identified by key + for path specified. + + :param path: Path to file or directory + :param key: Key of extended attribute + :param size: If size is specified as zero, we first determine the + size of xattr and then allocate a buffer accordingly. + If size is non-zero, it is assumed the caller knows + the size of xattr. + :returns: Value of extended attribute corresponding to key specified. + """ + if size == 0: + size = api.glfs_getxattr(self.fs, path, key, None, 0) + if size < 0: + err = ctypes.get_errno() + raise OSError(err, os.strerror(err)) + + buf = ctypes.create_string_buffer(size) + rc = api.glfs_getxattr(self.fs, path, key, buf, size) if rc < 0: err = ctypes.get_errno() - raise IOError(err, os.strerror(err)) + raise OSError(err, os.strerror(err)) return buf.value[:rc] def isdir(self, path): @@ -618,12 +675,28 @@ class Volume(object): dir_list.append(name) return dir_list - def listxattr(self, path): - buf = ctypes.create_string_buffer(512) - rc = api.glfs_listxattr(self.fs, path, buf, 512) + def listxattr(self, path, size=0): + """ + Retrieve list of extended attribute keys for the specified path. + + :param path: Path to file or directory. + :param size: If size is specified as zero, we first determine the + size of list and then allocate a buffer accordingly. + If size is non-zero, it is assumed the caller knows + the size of the list. + :returns: List of extended attribute keys. + """ + if size == 0: + size = api.glfs_listxattr(self.fs, path, None, 0) + if size < 0: + err = ctypes.get_errno() + raise OSError(err, os.strerror(err)) + + buf = ctypes.create_string_buffer(size) + rc = api.glfs_listxattr(self.fs, path, buf, size) if rc < 0: err = ctypes.get_errno() - raise IOError(err, os.strerror(err)) + raise OSError(err, os.strerror(err)) xattrs = [] # Parsing character by character is ugly, but it seems like the # easiest way to deal with the "strings separated by NUL in one @@ -643,6 +716,11 @@ class Volume(object): return xattrs def lstat(self, path): + """ + Return stat information of path. If path is a symbolic link, then it + returns information about the link itself, not the file that it refers + to. + """ s = api.Stat() rc = api.glfs_lstat(self.fs, path, ctypes.byref(s)) if rc < 0: @@ -668,6 +746,9 @@ class Volume(object): self.mkdir(name, mode) def mkdir(self, path, mode=0777): + """ + Create a directory + """ ret = api.glfs_mkdir(self.fs, path, mode) if ret < 0: err = ctypes.get_errno() @@ -732,25 +813,67 @@ class Volume(object): return fd def opendir(self, path): + """ + Open a directory. + + :param path: Path to the directory + :returns: Returns a instance of Dir class + """ fd = api.glfs_opendir(self.fs, path) if not fd: err = ctypes.get_errno() raise OSError(err, os.strerror(err)) return Dir(fd) + def readlink(self, path): + """ + Read contents of symbolic link path. + + :param path: Path of symbolic link + :returns: Contents of symlink + """ + PATH_MAX = 4096 + buf = ctypes.create_string_buffer(PATH_MAX) + ret = api.glfs_readlink(self.fs, path, buf, PATH_MAX) + if ret < 0: + err = ctypes.get_errno() + raise OSError(err, os.strerror(err)) + return buf.value[:ret] + + def remove(self, path): + """ + Remove (delete) the file path. If path is a directory, + OSError is raised. + """ + return self.unlink(path) + def removexattr(self, path, key): + """ + Remove a extended attribute of the file. + + :param path: Path to the file or directory. + :param key: The key of extended attribute. + """ ret = api.glfs_removexattr(self.fs, path, key) if ret < 0: err = ctypes.get_errno() - raise IOError(err, os.strerror(err)) + raise OSError(err, os.strerror(err)) - def rename(self, opath, npath): - ret = api.glfs_rename(self.fs, opath, npath) + def rename(self, src, dst): + """ + Rename the file or directory from src to dst. + """ + ret = api.glfs_rename(self.fs, src, dst) if ret < 0: err = ctypes.get_errno() raise OSError(err, os.strerror(err)) def rmdir(self, path): + """ + Remove (delete) the directory path. Only works when the directory is + empty, otherwise, OSError is raised. In order to remove whole + directory trees, rmtree() can be used. + """ ret = api.glfs_rmdir(self.fs, path) if ret < 0: err = ctypes.get_errno() @@ -798,24 +921,51 @@ class Volume(object): onerror(self.rmdir, path, e) def setfsuid(self, uid): + """ + setfsuid() changes the value of the caller's filesystem user ID-the + user ID that the Linux kernel uses to check for all accesses to the + filesystem. + """ ret = api.glfs_setfsuid(uid) if ret < 0: err = ctypes.get_errno() raise OSError(err, os.strerror(err)) def setfsgid(self, gid): + """ + setfsgid() changes the value of the caller's filesystem group ID-the + group ID that the Linux kernel uses to check for all accesses to the + filesystem. + """ ret = api.glfs_setfsgid(gid) if ret < 0: err = ctypes.get_errno() raise OSError(err, os.strerror(err)) - def setxattr(self, path, key, value, vlen): - ret = api.glfs_setxattr(self.fs, path, key, value, vlen, 0) + def setxattr(self, path, key, value, flags=0): + """ + Set extended attribute of the path. + + :param path: Path to file or directory. + :param key: The key of extended attribute. + :param value: The valiue of extended attribute. + :param flags: Possible values are 0 (default), 1 and 2 + 0: xattr will be created if it does not exist, or the + value will be replaced if the xattr exists. + 1: Perform a pure create, which fails if the named + attribute already exists. + 2: Perform a pure replace operation, which fails if the + named attribute does not already exist. + """ + ret = api.glfs_setxattr(self.fs, path, key, value, len(value), flags) if ret < 0: err = ctypes.get_errno() - raise IOError(err, os.strerror(err)) + raise OSError(err, os.strerror(err)) def stat(self, path): + """ + Returns stat information of path. + """ s = api.Stat() rc = api.glfs_stat(self.fs, path, ctypes.byref(s)) if rc < 0: @@ -825,8 +975,8 @@ class Volume(object): def statvfs(self, path): """ - To get status information about the file system that contains the file - named by the path argument. + Returns information about a mounted glusterfs volume. path is the + pathname of any file within the mounted filesystem. """ s = api.Statvfs() rc = api.glfs_statvfs(self.fs, path, ctypes.byref(s)) diff --git a/test/functional/libgfapi-python-tests.py b/test/functional/libgfapi-python-tests.py index 1244fd3..c4a9797 100644 --- a/test/functional/libgfapi-python-tests.py +++ b/test/functional/libgfapi-python-tests.py @@ -289,24 +289,72 @@ class FileOpsTest(unittest.TestCase): self.vol.unlink(self.path) self.assertRaises(OSError, self.vol.lstat, self.path) - def test_xattr(self): - key1, key2 = "hello", "world" - self.vol.setxattr(self.path, "trusted.key1", key1, len(key1)) - self.vol.setxattr(self.path, "trusted.key2", key2, len(key2)) - - xattrs = self.vol.listxattr(self.path) - self.assertFalse(isinstance(xattrs, types.IntType)) - self.assertTrue(set(["trusted.key1", "trusted.key2"]) <= set(xattrs)) - - buf = self.vol.getxattr(self.path, "trusted.key1", 32) - self.assertFalse(isinstance(buf, types.IntType)) - self.assertEqual(buf, "hello") - - self.vol.removexattr(self.path, "trusted.key1") - + def test_setxattr(self): + value = "hello world" + self.vol.setxattr(self.path, "trusted.key1", value) + self.assertEqual(self.vol.getxattr(self.path, "trusted.key1"), + value) + + # flag = 1 behavior: fail if xattr exists + self.assertRaises(OSError, self.vol.setxattr, self.path, + "trusted.key1", "whatever", flags=1) + # flag = 1 behavior: pass if xattr does not exist + self.vol.setxattr(self.path, "trusted.key2", "awesome", flags=1) + self.assertEqual(self.vol.getxattr(self.path, "trusted.key2"), + "awesome") + + # flag = 2 behavior: fail if xattr does not exist + self.assertRaises(OSError, self.vol.setxattr, self.path, + "trusted.key3", "whatever", flags=2) + # flag = 2 behavior: pass if xattr exists + self.vol.setxattr(self.path, "trusted.key2", + "more awesome", flags=2) + self.assertEqual(self.vol.getxattr(self.path, "trusted.key2"), + "more awesome") + + def test_getxattr(self): + self.vol.setxattr(self.path, "user.gluster", "awesome") + # user does not know the size of value beforehand + self.assertEqual(self.vol.getxattr(self.path, "user.gluster"), + "awesome") + # user knows the size of value beforehand + self.assertEqual(self.vol.getxattr(self.path, "user.gluster", size=7), + "awesome") + # size is larger + self.assertEqual(self.vol.getxattr(self.path, "user.gluster", size=20), + "awesome") + # size is smaller + self.assertRaises(OSError, self.vol.getxattr, self.path, + "user.gluster", size=1) + # size is negative + self.assertRaises(ValueError, self.vol.getxattr, self.path, + "user.gluster", size=-7) + + def test_listxattr(self): + self.vol.setxattr(self.path, "user.gluster", "awesome") + self.vol.setxattr(self.path, "user.gluster2", "awesome2") xattrs = self.vol.listxattr(self.path) - self.assertFalse(isinstance(xattrs, types.IntType)) - self.assertTrue(["trusted.key1"] not in xattrs) + self.assertTrue("user.gluster" in xattrs) + self.assertTrue("user.gluster2" in xattrs) + # Test passing of size + # larger size - should pass + xattrs = self.vol.listxattr(self.path, size=512) + self.assertTrue("user.gluster" in xattrs) + self.assertTrue("user.gluster2" in xattrs) + # smaller size - should fail + self.assertRaises(OSError, self.vol.listxattr, self.path, size=1) + # invalid size - should fail + self.assertRaises(ValueError, self.vol.listxattr, self.path, size=-1) + + def test_removexattr(self): + self.vol.setxattr(self.path, "user.gluster", "awesome") + self.vol.removexattr(self.path, "user.gluster") + # The xattr now shouldn't exist + self.assertRaises(OSError, self.vol.getxattr, + self.path, "user.gluster") + # Removing an xattr that does not exist + self.assertRaises(OSError, self.vol.removexattr, + self.path, "user.gluster") def test_fsetxattr(self): name = uuid4().hex @@ -344,6 +392,14 @@ class FileOpsTest(unittest.TestCase): self.assertEqual(f.fgetxattr("user.gluster"), "awesome") # user knows the size of value beforehand self.assertEqual(f.fgetxattr("user.gluster", 7), "awesome") + # size is larger + self.assertEqual(f.fgetxattr("user.gluster", 70), "awesome") + # size is smaller + self.assertRaises(OSError, f.fgetxattr, + "user.gluster", size=1) + # size is negative + self.assertRaises(ValueError, f.fgetxattr, + "user.gluster", size=-7) def test_ftruncate(self): name = uuid4().hex @@ -375,6 +431,45 @@ class FileOpsTest(unittest.TestCase): # invalid size - should fail self.assertRaises(ValueError, f.flistxattr, size=-1) + def test_access(self): + file_name = uuid4().hex + with File(self.vol.open(file_name, os.O_WRONLY | os.O_CREAT)) as f: + f.write("I'm whatever Gotham needs me to be") + f.fsync() + # Check that file exists + self.assertTrue(self.vol.access(file_name, os.F_OK)) + # Check that file does not exist + self.assertFalse(self.vol.access("nonexistentfile", os.F_OK)) + dir_name = uuid4().hex + self.vol.mkdir(dir_name) + # Check that directory exists + self.assertTrue(self.vol.access(dir_name, os.F_OK)) + # Check if there is execute and write permission + self.assertTrue(self.vol.access(file_name, os.W_OK | os.X_OK)) + + def test_getcwd_and_chdir(self): + # CWD should be root at first + self.assertEqual(self.vol.getcwd(), '/') + dir_structure = "/%s/%s" % (uuid4().hex, uuid4().hex) + self.vol.makedirs(dir_structure) + # Change directory + self.vol.chdir(dir_structure) + # The changed directory should now be CWD + self.assertEqual(self.vol.getcwd(), dir_structure) + self.vol.chdir("../..") + self.assertEqual(self.vol.getcwd(), '/') + + def test_readlink(self): + file_name = uuid4().hex + with File(self.vol.open(file_name, os.O_WRONLY | os.O_CREAT)) as f: + f.write("It's not who I am underneath," + "but what I do that defines me.") + f.fsync() + # Create a symlink + link_name = uuid4().hex + self.vol.symlink(file_name, link_name) + self.assertEqual(self.vol.readlink(link_name), file_name) + class DirOpsTest(unittest.TestCase): diff --git a/test/unit/gluster/test_gfapi.py b/test/unit/gluster/test_gfapi.py index 9fcfbf5..1463f76 100644 --- a/test/unit/gluster/test_gfapi.py +++ b/test/unit/gluster/test_gfapi.py @@ -562,7 +562,7 @@ class TestVolume(unittest.TestCase): mock_glfs_getxattr.return_value = -1 with patch("gluster.gfapi.api.glfs_getxattr", mock_glfs_getxattr): - self.assertRaises(IOError, self.vol.getxattr, "file.txt", + self.assertRaises(OSError, self.vol.getxattr, "file.txt", "key1", 32) def test_listdir_success(self): @@ -597,7 +597,8 @@ class TestVolume(unittest.TestCase): def test_listxattr_success(self): def mock_glfs_listxattr(fs, path, buf, buflen): - buf.raw = "key1\0key2\0" + if buf: + buf.raw = "key1\0key2\0" return 10 with patch("gluster.gfapi.api.glfs_listxattr", mock_glfs_listxattr): @@ -610,7 +611,7 @@ class TestVolume(unittest.TestCase): mock_glfs_listxattr.return_value = -1 with patch("gluster.gfapi.api.glfs_listxattr", mock_glfs_listxattr): - self.assertRaises(IOError, self.vol.listxattr, "file.txt") + self.assertRaises(OSError, self.vol.listxattr, "file.txt") def test_lstat_success(self): mock_glfs_lstat = Mock() @@ -823,7 +824,7 @@ class TestVolume(unittest.TestCase): with patch("gluster.gfapi.api.glfs_removexattr", mock_glfs_removexattr): - self.assertRaises(IOError, self.vol.removexattr, "file.txt", + self.assertRaises(OSError, self.vol.removexattr, "file.txt", "key1") def test_rmtree_success(self): @@ -940,7 +941,7 @@ class TestVolume(unittest.TestCase): mock_glfs_setxattr.return_value = -1 with patch("gluster.gfapi.api.glfs_setxattr", mock_glfs_setxattr): - self.assertRaises(IOError, self.vol.setxattr, "file.txt", + self.assertRaises(OSError, self.vol.setxattr, "file.txt", "key1", "hello", 5) def test_symlink_success(self): -- cgit