From 3f2233ba01c74681ef565285a32fa8bcea8357e9 Mon Sep 17 00:00:00 2001 From: Thiago da Silva Date: Wed, 30 Oct 2013 15:43:27 -0400 Subject: initial repo tree with gfapi.py This initial commit is a copy from gluster/api/examples/gfapi.py from the glusterfs repo Signed-off-by: Thiago da Silva Change-Id: I4442528d5bc2ec52ed2970b818d8cb9327e36168 --- gluster/__init__.py | 49 ++++++ gluster/gfapi.py | 422 ++++++++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 50 +++++++ test/__init__.py | 0 tox.ini | 37 +++++ 5 files changed, 558 insertions(+) create mode 100644 gluster/__init__.py create mode 100644 gluster/gfapi.py create mode 100644 setup.py create mode 100644 test/__init__.py create mode 100644 tox.ini diff --git a/gluster/__init__.py b/gluster/__init__.py new file mode 100644 index 0000000..ca16f94 --- /dev/null +++ b/gluster/__init__.py @@ -0,0 +1,49 @@ +# Copyright (c) 2012-2013 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" Python bindings for GlusterFS libgfapi """ + + +class PkgInfo(object): + def __init__(self, canonical_version, release, name, final): + self.canonical_version = canonical_version + self.release = release + self.name = name + self.final = final + + def save_config(self, filename): + """ + Creates a file with the package configuration which can be sourced by + a bash script. + """ + with open(filename, 'w') as fd: + fd.write("NAME=%s\n" % self.name) + fd.write("VERSION=%s\n" % self.canonical_version) + fd.write("RELEASE=%s\n" % self.release) + + @property + def pretty_version(self): + if self.final: + return self.canonical_version + else: + return '%s-dev' % (self.canonical_version,) + + +### +### Change the Package version here +### +_pkginfo = PkgInfo('0.0.1', '0', 'python-libgfapi', False) +__version__ = _pkginfo.pretty_version +__canonical_version__ = _pkginfo.canonical_version diff --git a/gluster/gfapi.py b/gluster/gfapi.py new file mode 100644 index 0000000..3ac67f4 --- /dev/null +++ b/gluster/gfapi.py @@ -0,0 +1,422 @@ +#!/usr/bin/python + +from ctypes import * +from ctypes.util import find_library +import os +import sys +import time +import types + +# Looks like ctypes is having trouble with dependencies, so just force them to +# load with RTLD_GLOBAL until I figure that out. +glfs = CDLL(find_library("glusterfs"),RTLD_GLOBAL) +xdr = CDLL(find_library("gfxdr"),RTLD_GLOBAL) +api = CDLL(find_library("gfapi"),RTLD_GLOBAL) + +# Wow, the Linux kernel folks really play nasty games with this structure. If +# you look at the man page for stat(2) and then at this definition you'll note +# two discrepancies. First, we seem to have st_nlink and st_mode reversed. In +# fact that's exactly how they're defined *for 64-bit systems*; for 32-bit +# they're in the man-page order. Even uglier, the man page makes no mention of +# the *nsec fields, but they are very much present and if they're not included +# then we get memory corruption because libgfapi has a structure definition +# that's longer than ours and they overwrite some random bit of memory after +# the space we allocated. Yes, that's all very disgusting, and I'm still not +# sure this will really work on 32-bit because all of the field types are so +# obfuscated behind macros and feature checks. +class Stat (Structure): + _fields_ = [ + ("st_dev", c_ulong), + ("st_ino", c_ulong), + ("st_nlink", c_ulong), + ("st_mode", c_uint), + ("st_uid", c_uint), + ("st_gid", c_uint), + ("st_rdev", c_ulong), + ("st_size", c_ulong), + ("st_blksize", c_ulong), + ("st_blocks", c_ulong), + ("st_atime", c_ulong), + ("st_atimensec", c_ulong), + ("st_mtime", c_ulong), + ("st_mtimensec", c_ulong), + ("st_ctime", c_ulong), + ("st_ctimensec", c_ulong), + ] +api.glfs_creat.restype = c_void_p +api.glfs_open.restype = c_void_p +api.glfs_lstat.restype = c_int +api.glfs_lstat.argtypes = [c_void_p, c_char_p, POINTER(Stat)] + +class Dirent (Structure): + _fields_ = [ + ("d_ino", c_ulong), + ("d_off", c_ulong), + ("d_reclen", c_ushort), + ("d_type", c_char), + ("d_name", c_char * 256), + ] +api.glfs_opendir.restype = c_void_p +api.glfs_readdir_r.restype = c_int +api.glfs_readdir_r.argtypes = [c_void_p, POINTER(Dirent), + POINTER(POINTER(Dirent))] + +# There's a bit of ctypes glitchiness around __del__ functions and module-level +# variables. If we unload the module while we still have references to File or +# Volume objects, the module-level variables might have disappeared by the time +# __del__ gets called. Therefore the objects hold references which they +# release when __del__ is done. We only actually use the object-local values +# in __del__; for clarity, we just use the simpler module-level form elsewhere. + +class File(object): + + def __init__ (self, fd): + # Add a reference so the module-level variable "api" doesn't + # get yanked out from under us (see comment above File def'n). + self._api = api + self.fd = fd + + def __del__ (self): + self._api.glfs_close(self.fd) + self._api = None + + # File operations, in alphabetical order. + + def fsync (self): + return api.glfs_fsync(self.fd) + + def read (self, buflen, flags=0): + rbuf = create_string_buffer(buflen) + rc = api.glfs_read(self.fd,rbuf,buflen,flags) + if rc > 0: + return rbuf.value[:rc] + else: + return rc + + def read_buffer (self, buf, flags=0): + return api.glfs_read(self.fd,buf,len(buf),flags) + + def write (self, data, flags=0): + return api.glfs_write(self.fd,data,len(data),flags) + + def fallocate (self, mode, offset, len): + return api.glfs_fallocate(self.fd, mode, offset, len) + + def discard (self, offset, len): + return api.glfs_discard(self.fd, offset, len) + + +class Dir(object): + + def __init__ (self, fd): + # Add a reference so the module-level variable "api" doesn't + # get yanked out from under us (see comment above File def'n). + self._api = api + self.fd = fd + self.cursor = POINTER(Dirent)() + + def __del__ (self): + self._api.glfs_closedir(self.fd) + self._api = None + + def next (self): + entry = Dirent() + entry.d_reclen = 256 + rc = api.glfs_readdir_r(self.fd,byref(entry),byref(self.cursor)) + if (rc < 0) or (not self.cursor) or (not self.cursor.contents): + return rc + return entry + +class Volume(object): + + # Housekeeping functions. + + def __init__ (self, host, volid, proto="tcp", port=24007): + # Add a reference so the module-level variable "api" doesn't + # get yanked out from under us (see comment above File def'n). + self._api = api + self.fs = api.glfs_new(volid) + api.glfs_set_volfile_server(self.fs,proto,host,port) + + def __del__ (self): + self._api.glfs_fini(self.fs) + self._api = None + + def set_logging (self, path, level): + api.glfs_set_logging(self.fs,path,level) + + def mount (self): + api.glfs_init(self.fs) + + # File operations, in alphabetical order. + + def creat (self, path, flags, mode): + fd = api.glfs_creat(self.fs,path,flags,mode) + if not fd: + return fd + return File(fd) + + def getxattr (self, path, key, maxlen): + buf = create_string_buffer(maxlen) + rc = api.glfs_getxattr(self.fs,path,key,buf,maxlen) + if rc < 0: + return rc + return buf.value[:rc] + + def listxattr (self, path): + buf = create_string_buffer(512) + rc = api.glfs_listxattr(self.fs,path,buf,512) + if rc < 0: + return rc + xattrs = [] + # Parsing character by character is ugly, but it seems like the + # easiest way to deal with the "strings separated by NUL in one + # buffer" format. + i = 0 + while i < rc: + new_xa = buf.raw[i] + i += 1 + while i < rc: + next_char = buf.raw[i] + i += 1 + if next_char == '\0': + xattrs.append(new_xa) + break + new_xa += next_char + xattrs.sort() + return xattrs + + def lstat (self, path): + x = Stat() + rc = api.glfs_lstat(self.fs,path,byref(x)) + if rc >= 0: + return x + else: + return rc + + def mkdir (self, path): + return api.glfs_mkdir(self.fs,path) + + def open (self, path, flags): + fd = api.glfs_open(self.fs,path,flags) + if not fd: + return fd + return File(fd) + + def opendir (self, path): + fd = api.glfs_opendir(self.fs,path) + if not fd: + return fd + return Dir(fd) + + def rename (self, opath, npath): + return api.glfs_rename(self.fs,opath,npath) + + def rmdir (self, path): + return api.glfs_rmdir(self.fs,path) + + def setxattr (self, path, key, value, vlen): + return api.glfs_setxattr(self.fs,path,key,value,vlen,0) + + def unlink (self, path): + return api.glfs_unlink(self.fs,path) + +if __name__ == "__main__": + def test_create_write (vol, path, data): + mypath = path + ".io" + fd = vol.creat(mypath,os.O_WRONLY|os.O_EXCL,0644) + if not fd: + return False, "creat error" + rc = fd.write(data) + if rc != len(data): + return False, "wrote %d/%d bytes" % (rc, len(data)) + return True, "wrote %d bytes" % rc + + # TBD: this test fails if we do create, open, write, read + def test_open_read (vol, path, data): + mypath = path + ".io" + fd = vol.open(mypath,os.O_RDONLY) + if not fd: + return False, "open error" + dlen = len(data) * 2 + buf = fd.read(dlen) + if type(buf) == types.IntType: + return False, "read error %d" % buf + if len(buf) != len(data): + return False, "read %d/%d bytes" % (len(buf), len(data)) + return True, "read '%s'" % buf + + def test_lstat (vol, path, data): + mypath = path + ".io" + sb = vol.lstat(mypath) + if type(sb) == types.IntType: + return False, "lstat error %d" % sb + if sb.st_size != len(data): + return False, "lstat size is %d, expected %d" % ( + sb.st_size, len(data)) + return True, "lstat got correct size %d" % sb.st_size + + def test_rename (vol, path, data): + opath = path + ".io" + npath = path + ".tmp" + rc = vol.rename(opath,npath) + if rc < 0: + return False, "rename error %d" % rc + ofd = vol.open(opath,os.O_RDWR) + if isinstance(ofd,File): + return False, "old path working after rename" + nfd = vol.open(npath,os.O_RDWR) + if isinstance(nfd,File): + return False, "new path not working after rename" + return True, "rename worked" + + def test_unlink (vol, path, data): + mypath = path + ".tmp" + rc = vol.unlink(mypath) + if rc < 0: + return False, "unlink error %d" % fd + fd = vol.open(mypath,os.O_RDWR) + if isinstance(fd,File): + return False, "path still usable after unlink" + return True, "unlink worked" + + def test_mkdir (vol, path, data): + mypath = path + ".dir" + rc = vol.mkdir(mypath) + if rc < 0: + return False, "mkdir error %d" % rc + return True, "mkdir worked" + + def test_create_in_dir (vol, path, data): + mypath = path + ".dir/probe" + fd = vol.creat(mypath,os.O_RDWR,0644) + if not isinstance(fd,File): + return False, "create (in dir) error" + return True, "create (in dir) worked" + + def test_dir_listing (vol, path, data): + mypath = path + ".dir" + fd = vol.opendir(mypath) + if not isinstance(fd,Dir): + return False, "opendir error %d" % fd + files = [] + while True: + ent = fd.next() + if not isinstance(ent,Dirent): + break + name = ent.d_name[:ent.d_reclen] + files.append(name) + if files != [".", "..", "probe"]: + return False, "wrong directory contents" + return True, "directory listing worked" + + def test_unlink_in_dir (vol, path, data): + mypath = path + ".dir/probe" + rc = vol.unlink(mypath) + if rc < 0: + return False, "unlink (in dir) error %d" % rc + return True, "unlink (in dir) worked" + + def test_rmdir (vol, path, data): + mypath = path + ".dir" + rc = vol.rmdir(mypath) + if rc < 0: + return False, "rmdir error %d" % rc + sb = vol.lstat(mypath) + if not isinstance(sb,Stat): + return False, "dir still there after rmdir" + return True, "rmdir worked" + + def test_setxattr (vol, path, data): + mypath = path + ".xa" + fd = vol.creat(mypath,os.O_RDWR|os.O_EXCL,0644) + if not fd: + return False, "creat (xattr test) error" + key1, key2 = "hello", "goodbye" + if vol.setxattr(mypath,"trusted.key1",key1,len(key1)) < 0: + return False, "setxattr (key1) error" + if vol.setxattr(mypath,"trusted.key2",key2,len(key2)) < 0: + return False, "setxattr (key2) error" + return True, "setxattr worked" + + def test_getxattr (vol, path, data): + mypath = path + ".xa" + buf = vol.getxattr(mypath,"trusted.key1",32) + if type(buf) == types.IntType: + return False, "getxattr error" + if buf != "hello": + return False, "wrong getxattr value %s" % buf + return True, "getxattr worked" + + def test_listxattr (vol, path, data): + mypath = path + ".xa" + xattrs = vol.listxattr(mypath) + if type(xattrs) == types.IntType: + return False, "listxattr error" + if xattrs != ["trusted.key1","trusted.key2"]: + return False, "wrong listxattr value %s" % repr(xattrs) + return True, "listxattr worked" + + def test_fallocate (vol, path, data): + mypath = path + ".io" + fd = vol.creat(mypath,os.O_WRONLY|os.O_EXCL,0644) + if not fd: + return False, "creat error" + rc = fd.fallocate(0, 0, 1024*1024) + if rc != 0: + return False, "fallocate error" + rc = fd.discard(4096, 4096) + if rc != 0: + return False, "discard error" + return True, "fallocate/discard worked" + + test_list = ( + test_create_write, + test_open_read, + test_lstat, + test_rename, + test_unlink, + test_mkdir, + test_create_in_dir, + test_dir_listing, + test_unlink_in_dir, + test_rmdir, + test_setxattr, + test_getxattr, + test_listxattr, + test_fallocate, + ) + + ok_to_fail = ( + # TBD: this fails opening the new file, even though the file + # did get renamed. Looks like a gfapi bug, not ours. + (test_rename, "new path not working after rename"), + # TBD: similar, call returns error even though it worked + (test_rmdir, "dir still there after rmdir"), + ) + + volid, path = sys.argv[1:3] + data = "fubar" + vol = Volume("localhost",volid) + vol.set_logging("/dev/null",7) + #vol.set_logging("/dev/stderr",7) + vol.mount() + + failures = 0 + expected = 0 + for t in test_list: + rc, msg = t(vol,path,data) + if rc: + print "PASS: %s" % msg + else: + print "FAIL: %s" % msg + failures += 1 + for otf in ok_to_fail: + if (t == otf[0]) and (msg == otf[1]): + print " (skipping known failure)" + expected += 1 + break # from the *inner* for loop + else: + break # from the *outer* for loop + + print "%d failures (%d expected)" % (failures, expected) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..fc396d0 --- /dev/null +++ b/setup.py @@ -0,0 +1,50 @@ +#!/usr/bin/python +# Copyright (c) 2013 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from setuptools import setup, find_packages + +from gluster import __canonical_version__ as version + + +name = 'gfapi' + + +setup( + name=name, + version=version, + description='Python bindings for GlusterFS libgfapi', + license='Apache License (2.0)', + author='Red Hat, Inc.', + author_email='gluster-users@gluster.org', + url='http://www.gluster.org', + packages=find_packages(exclude=['test']), + test_suite='nose.collector', + classifiers=[ + 'Development Status :: 5 - Production/Stable' + 'Environment :: OpenStack' + 'Intended Audience :: Information Technology' + 'Intended Audience :: System Administrators' + 'License :: OSI Approved :: Apache Software License' + 'Operating System :: POSIX :: Linux' + 'Programming Language :: Python' + 'Programming Language :: Python :: 2' + 'Programming Language :: Python :: 2.6' + 'Programming Language :: Python :: 2.7' + ], + install_requires=[], + scripts=[], + entry_points={}, +) diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..657412f --- /dev/null +++ b/tox.ini @@ -0,0 +1,37 @@ +[tox] +envlist = py26,py27,pep8 + +[testenv] +setenv = VIRTUAL_ENV={envdir} + NOSE_WITH_OPENSTACK=1 + NOSE_OPENSTACK_COLOR=1 + NOSE_OPENSTACK_RED=0.05 + NOSE_OPENSTACK_YELLOW=0.025 + NOSE_OPENSTACK_SHOW_ELAPSED=1 + NOSE_OPENSTACK_STDOUT=1 +changedir = {toxinidir}/test/unit +commands = nosetests -v --exe --with-xunit --with-coverage --cover-package gluster --cover-erase --cover-xml --cover-html --cover-branches --with-html-output {posargs} + +[tox:jenkins] +downloadcache = ~/cache/pip + +[testenv:pep8] +deps = + --download-cache={homedir}/.pipcache + -r{toxinidir}/tools/test-requires +changedir = {toxinidir} +commands = + flake8 + flake8 gluster test + +[testenv:cover] +setenv = NOSE_WITH_COVERAGE=1 + +[testenv:venv] +commands = {posargs} + +[flake8] +ignore = H +builtins = _ +exclude = .venv,.tox,dist,doc,test,*egg +show-source = True -- cgit