summaryrefslogtreecommitdiffstats
path: root/glustolibs-gluster/glustolibs/gluster
diff options
context:
space:
mode:
authorJonathan Holloway <jholloway@redhat.com>2018-01-23 02:02:38 +0100
committerNigel Babu <nigelb@redhat.com>2018-02-26 10:42:31 +0000
commitab7a71cc4b2862d267c3e6fae67c711e51abca77 (patch)
tree42060d76949e5c8bb4736c2ecdf1ecf68eed6df5 /glustolibs-gluster/glustolibs/gluster
parent1374783418db58f6f4fe153d83082549bf2d000f (diff)
initial dht libs
* glusterfile.py - helper for gluster client and backend files. * glusterdir.py - helper for gluster client and backend dirs. * brickdir.py - helper for collection and hashing of brickdirs (from pathinfo data). * layout.py - base class for simple DHT layout validation. * dht_test_util.py - utility module to walk a directory tree and run tests against files. * constants.py - definitions for constants used in DHT libraries. * exceptions.py - definitions for exceptions raised in DHT libraries. Change-Id: I44770a822e0ec79561b3aa048e555320f622116a Signed-off-by: Jonathan Holloway <jholloway@redhat.com>
Diffstat (limited to 'glustolibs-gluster/glustolibs/gluster')
-rw-r--r--glustolibs-gluster/glustolibs/gluster/brickdir.py184
-rw-r--r--glustolibs-gluster/glustolibs/gluster/constants.py33
-rw-r--r--glustolibs-gluster/glustolibs/gluster/dht_test_utils.py148
-rw-r--r--glustolibs-gluster/glustolibs/gluster/exceptions.py83
-rw-r--r--glustolibs-gluster/glustolibs/gluster/glusterdir.py117
-rw-r--r--glustolibs-gluster/glustolibs/gluster/glusterfile.py710
-rw-r--r--glustolibs-gluster/glustolibs/gluster/layout.py149
7 files changed, 1424 insertions, 0 deletions
diff --git a/glustolibs-gluster/glustolibs/gluster/brickdir.py b/glustolibs-gluster/glustolibs/gluster/brickdir.py
new file mode 100644
index 000000000..564f1421b
--- /dev/null
+++ b/glustolibs-gluster/glustolibs/gluster/brickdir.py
@@ -0,0 +1,184 @@
+#!/usr/bin/env python
+# Copyright (C) 2018 Red Hat, Inc. <http://www.redhat.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+"""Module for library brick class and related functions"""
+
+import os
+
+from glusto.core import Glusto as g
+
+
+def get_hashrange(brickdir_path):
+ """Get the int hash range for a brick
+
+ Args:
+ brickdir_url (str): path of the directory as returned from pathinfo
+ (e.g., server1.example.com:/bricks/brick1/testdir1)
+
+ Returns:
+ list containing the low and high hash for the brickdir. None on fail.
+ """
+ (host, fqpath) = brickdir_path.split(':')
+
+ command = ("getfattr -n trusted.glusterfs.dht -e hex %s "
+ "2> /dev/null | grep -i trusted.glusterfs.dht | "
+ "cut -d= -f2" % fqpath)
+ rcode, rout, rerr = g.run(host, command)
+ full_hash_hex = rout.strip()
+
+ if rcode == 0:
+ # Grab the trailing 16 hex bytes
+ trailing_hash_hex = full_hash_hex[-16:]
+ # Split the full hash into low and high
+ hash_range_low = int(trailing_hash_hex[0:8], 16)
+ hash_range_high = int(trailing_hash_hex[-8:], 16)
+
+ return (hash_range_low, hash_range_high)
+
+ g.log.error('Could not get hashrange: %s' % rerr)
+ return None
+
+
+def file_exists(host, filename):
+ """Check if file exists at path on host
+
+ Args:
+ host (str): hostname or ip of system
+ filename (str): fully qualified path of file
+
+ Returns:
+ True if file exists. False if file does not exist
+ """
+ command = "ls -ld %s" % filename
+ rcode, _, _ = g.run(host, command)
+ if rcode == 0:
+ return True
+
+ return False
+
+
+class BrickDir(object):
+ """Directory on a brick"""
+ def __init__(self, path):
+ self._path = path
+ (self._host, self._fqpath) = self._path.split(':')
+ self._hashrange = None
+ self._hashrange_low = None
+ self._hashrange_high = None
+
+ def _get_hashrange(self):
+ """get the hash range for a brick from a remote system"""
+ self._hashrange = get_hashrange(self._path)
+ self._hashrange_low = self._hashrange[0]
+ self._hashrange_high = self._hashrange[1]
+
+ @property
+ def path(self):
+ """The brick url
+ Example:
+ server1.example.com:/bricks/brick1
+ """
+ return self._path
+
+ @property
+ def host(self):
+ """The hostname/ip of the system hosting the brick"""
+ return self._host
+
+ @property
+ def fqpath(self):
+ """The fully qualified path of the brick directory"""
+ return self._fqpath
+
+ @property
+ def hashrange(self):
+ """A list containing the low and high hash of the brick hashrange"""
+ if self._hashrange is None:
+ g.log.info("Retrieving hash range for %s" % self._path)
+ self._get_hashrange()
+
+ return (self._hashrange_low, self._hashrange_high)
+
+ @property
+ def hashrange_low(self):
+ """The low hash of the brick hashrange"""
+ if self.hashrange is None or self._hashrange_low is None:
+ self._get_hashrange()
+
+ return self._hashrange_low
+
+ @property
+ def hashrange_high(self):
+ """The high hash of the brick hashrange"""
+ if self.hashrange is None or self._hashrange_high is None:
+ self._get_hashrange()
+
+ return self._hashrange_high
+
+ def hashrange_contains_hash(self, filehash):
+ """Check if a hash number falls between the brick hashrange
+
+ Args:
+ filehash (int): hash being checked against range
+
+ Returns:
+ True if hash is in range. False if hash is out of range
+ """
+ if self._hashrange is None:
+ self._get_hashrange()
+
+ if self._hashrange_low <= filehash <= self._hashrange_high:
+ return True
+
+ return False
+
+ def has_zero_hashrange(self):
+ """figure out if the brickdir has a low and high zero value hash"""
+ if self.hashrange_low == 0 and self.hashrange_high == 0:
+ return True
+
+ return False
+
+ def resync_hashrange(self):
+ """Reset the hashrange attributes and update hashrange from brick
+ Args:
+ None
+
+ Returns:
+ None
+ """
+ self._hashrange = None
+ self._hashrange_low = None
+ self._hashrange_high = None
+ self._get_hashrange()
+
+ def file_exists(self, filename):
+ """Check if the file exists on the brick
+
+ Args:
+ filename (int): relative path of the file
+
+ Returns:
+ True if the file exists on the brick
+ False if the file does not exist on the brick
+ """
+ fqfilepath = os.path.join(self._fqpath, filename)
+
+ if file_exists(self._host, fqfilepath):
+ return True
+
+ return False
diff --git a/glustolibs-gluster/glustolibs/gluster/constants.py b/glustolibs-gluster/glustolibs/gluster/constants.py
new file mode 100644
index 000000000..3573a40a0
--- /dev/null
+++ b/glustolibs-gluster/glustolibs/gluster/constants.py
@@ -0,0 +1,33 @@
+#!/usr/bin/env python
+# Copyright (C) 2018 Red Hat, Inc. <http://www.redhat.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+"""Constants for DHT library functions"""
+
+# FILE TYPES
+FILETYPE_DIR = 1
+FILETYPE_DIRS = 1
+FILETYPE_FILE = 2
+FILETYPE_FILES = 2
+FILETYPE_LINK = 4
+FILETYPE_LINKS = 4
+FILETYPE_ALL = 255
+
+# TEST TYPES
+TEST_LAYOUT_IS_COMPLETE = 1
+TEST_LAYOUT_IS_BALANCED = 2
+TEST_FILE_EXISTS_ON_HASHED_BRICKS = 4
+TEST_ALL = 255
diff --git a/glustolibs-gluster/glustolibs/gluster/dht_test_utils.py b/glustolibs-gluster/glustolibs/gluster/dht_test_utils.py
new file mode 100644
index 000000000..3e1cd7c23
--- /dev/null
+++ b/glustolibs-gluster/glustolibs/gluster/dht_test_utils.py
@@ -0,0 +1,148 @@
+#!/usr/bin/env python
+# Copyright (C) 2018 Red Hat, Inc. <http://www.redhat.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+"""Module for library DHT test utility functions"""
+
+import os
+
+from glusto.core import Glusto as g
+
+from glustolibs.gluster.glusterfile import GlusterFile
+from glustolibs.gluster.glusterdir import GlusterDir
+from glustolibs.gluster.layout import Layout
+import glustolibs.gluster.constants as k
+import glustolibs.gluster.exceptions as gex
+
+
+def run_layout_tests(fqpath, layout, test_type):
+ """run the is_complete and/or is_balanced tests"""
+ if test_type & k.TEST_LAYOUT_IS_COMPLETE:
+ g.log.info("Testing layout complete for %s" % fqpath)
+ if not layout.is_complete:
+ msg = ("Layout for %s IS NOT COMPLETE" % fqpath)
+ g.log.error(msg)
+ raise gex.LayoutIsNotCompleteError(msg)
+ if test_type & k.TEST_LAYOUT_IS_BALANCED:
+ g.log.info("Testing layout balance for %s" % fqpath)
+ if not layout.is_balanced:
+ msg = ("Layout for %s IS NOT BALANCED" % fqpath)
+ g.log.error(msg)
+ raise gex.LayoutIsNotBalancedError(msg)
+
+ # returning True until logic requires non-exception error check(s)
+ return True
+
+
+def run_hashed_bricks_test(gfile):
+ """run check for file/dir existence on brick based on calculated hash"""
+ g.log.info("Testing file/dir %s existence on hashed brick(s)." %
+ gfile.fqpath)
+ if not gfile.exists_on_hashed_bricks:
+ msg = ("File/Dir %s DOES NOT EXIST on hashed bricks." %
+ gfile.fqpath)
+ g.log.error(msg)
+ raise gex.FileDoesNotExistOnHashedBricksError(msg)
+
+ return True
+
+
+def validate_files_in_dir(host, rootdir,
+ file_type=k.FILETYPE_ALL,
+ test_type=k.TEST_ALL):
+ """walk a directory tree and check if layout is_complete.
+
+ Args:
+ host (str): The host of the directory being traversed.
+ rootdir (str): The fully qualified path of the dir being traversed.
+ file_type (int): An or'd set of constants defining the file types
+ to test.
+ FILETYPE_DIR
+ FILETYPE_DIRS
+ FILETYPE_FILE
+ FILETYPE_FILES
+ FILETYPE_ALL
+
+ test_type (int): An or'd set of constants defining the test types
+ to run.
+ TEST_LAYOUT_IS_COMPLETE
+ TEST_LAYOUT_IS_BALANCED
+ TEST_FILE_EXISTS_ON_HASHED_BRICKS
+ TEST_ALL
+
+ Examples:
+ # TEST LAYOUTS FOR FILES IN A DIRECTORY
+
+ validate_files_in_dir(clients[0], '/mnt/glusterfs')
+ validate_files_in_dir(clients[0], '/mnt/glusterfs',
+ file_type=k.FILETYPE_DIRS)
+ validate_files_in_dir(clients[0], '/mnt/glusterfs',
+ file_type=k.FILETYPE_FILES)
+ validate_files_in_dir(clients[0], '/mnt/glusterfs',
+ test_type=k.TEST_LAYOUT_IS_COMPLETE,
+ file_type=(k.FILETYPE_DIRS | k.FILETYPE_FILES))
+ validate_files_in_dir(clients[0], '/mnt/glusterfs',
+ test_type=k.TEST_LAYOUT_IS_BALANCED)
+ validate_files_in_dir(clients[0], '/mnt/glusterfs',
+ test_type=k.TEST_LAYOUT_IS_BALANCED,
+ file_type=k.FILETYPE_FILES)
+
+ # TEST FILES IN DIRECTORY EXIST ON HASHED BRICKS
+ validate_files_in_dir(clients[0], '/mnt/glusterfs',
+ test_type=k.TEST_FILE_EXISTS_ON_HASHED_BRICKS)
+ """
+ layout_cache = {}
+
+ conn = g.rpyc_get_connection(host)
+
+ for walkies in conn.modules.os.walk(rootdir):
+ g.log.info("TESTING DIRECTORY %s..." % walkies[0])
+
+ # check directories
+ if file_type & k.FILETYPE_DIR:
+ for testdir in walkies[1]:
+ fqpath = os.path.join(walkies[0], testdir)
+ gdir = GlusterDir(host, fqpath)
+
+ if gdir.parent_dir in layout_cache:
+ layout = layout_cache[gdir.parent_dir]
+ else:
+ layout = Layout(gdir.parent_dir_pathinfo)
+ layout_cache[gdir.parent_dir] = layout
+
+ run_layout_tests(gdir.parent_dir, layout, test_type)
+
+ if test_type & k.TEST_FILE_EXISTS_ON_HASHED_BRICKS:
+ run_hashed_bricks_test(gdir)
+
+ # check files
+ if file_type & k.FILETYPE_FILE:
+ for file in walkies[2]:
+ fqpath = os.path.join(walkies[0], file)
+ gfile = GlusterFile(host, fqpath)
+
+ if gfile.parent_dir in layout_cache:
+ layout = layout_cache[gfile.parent_dir]
+ else:
+ layout = Layout(gfile.parent_dir_pathinfo)
+ layout_cache[gfile.parent_dir] = layout
+
+ run_layout_tests(gfile.parent_dir, layout, test_type)
+
+ if test_type & k.TEST_FILE_EXISTS_ON_HASHED_BRICKS:
+ run_hashed_bricks_test(gfile)
+
+ return True
diff --git a/glustolibs-gluster/glustolibs/gluster/exceptions.py b/glustolibs-gluster/glustolibs/gluster/exceptions.py
index 42ee48441..c52dcee59 100644
--- a/glustolibs-gluster/glustolibs/gluster/exceptions.py
+++ b/glustolibs-gluster/glustolibs/gluster/exceptions.py
@@ -1,3 +1,86 @@
+#!/usr/bin/env python
+# Copyright (C) 2018 Red Hat, Inc. <http://www.redhat.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+"""Exceptions for Gluster libraries and tests"""
+
+
+class GlusterError(Exception):
+ """Base Gluster exception class."""
+ def __init__(self, arg):
+ Exception.__init__(self, arg)
+ self.msg = arg
+
+
+class TransportEndpointNotConnectedError(GlusterError):
+ """Exception for transport endpoint not connected error."""
+ def __init__(self, arg):
+ GlusterError.__init__(self, arg)
+ self.msg = arg
+
+
+class NoSuchFileOrDirectoryError(GlusterError):
+ """Exception for no such file or directory error."""
+ def __init__(self, arg):
+ GlusterError.__init__(self, arg)
+ self.msg = arg
+
+
+class AttributesDoNotMatchError(GlusterError):
+ """Attributes do not match exception."""
+ def __init__(self, arg):
+ GlusterError.__init__(self, arg)
+ self.msg = arg
+
+
+class LayoutIsNotCompleteError(GlusterError):
+ """Exception raised when the layout of a file is not complete."""
+ def __init__(self, arg):
+ GlusterError.__init__(self, arg)
+ self.msg = arg
+
+
+class LayoutIsNotBalancedError(GlusterError):
+ """Exception raised when the layout of a file is not balanced."""
+ def __init__(self, arg):
+ GlusterError.__init__(self, arg)
+ self.msg = arg
+
+
+class LayoutHasHolesError(GlusterError):
+ """Exception raised when the layout of a file has holes."""
+ def __init__(self, arg):
+ GlusterError.__init__(self, arg)
+ self.msg = arg
+
+
+class LayoutHasOverlapsError(GlusterError):
+ """Exception raised when the layout of a file has overlaps."""
+ def __init__(self, arg):
+ GlusterError.__init__(self, arg)
+ self.msg = arg
+
+
+class FileDoesNotExistOnHashedBricksError(GlusterError):
+ """Exception raised when a file/dir does not exist where it is hashed."""
+ def __init__(self, arg):
+ GlusterError.__init__(self, arg)
+ self.msg = arg
+
+
class ConfigError(Exception):
'''
Custom exception thrown when there is an unrecoverable configuration error.
diff --git a/glustolibs-gluster/glustolibs/gluster/glusterdir.py b/glustolibs-gluster/glustolibs/gluster/glusterdir.py
new file mode 100644
index 000000000..50c37c715
--- /dev/null
+++ b/glustolibs-gluster/glustolibs/gluster/glusterdir.py
@@ -0,0 +1,117 @@
+#!/usr/bin/env python
+# Copyright (C) 2018 Red Hat, Inc. <http://www.redhat.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+"""Description: Module for library gluster dir class and related functions
+
+A GlusterDir is a file object that exists on the client and backend brick.
+This module provides low-level functions and a GlusterDir class to maintain
+state and manage properties of a file in both locations.
+
+GlusterDir inherits from GlusterFile.
+"""
+
+from glusto.core import Glusto as g
+
+from glustolibs.gluster.glusterfile import GlusterFile
+
+
+def mkdir(host, fqpath, parents=False, mode=None):
+ """Create a directory or path of directories.
+
+ Args:
+ host (str): The hostname/ip of the remote system.
+ fqpath (str): The fully-qualified path to the file.
+ parents (Bool, optional): create parent directories if do not exist.
+ mode (str, optional): the initial mode of the directory.
+
+ Returns:
+ True on success.
+ False on failure.
+ """
+ command_list = ['mkdir']
+ if parents:
+ command_list.append('-p')
+ if mode is not None:
+ command_list.append('-m %s' % mode)
+ command_list.append(fqpath)
+ rcode, _, rerr = g.run(host, ' '.join(command_list))
+
+ if rcode == 0:
+ return True
+
+ g.log.error("Directory mkdir failed: %s" % rerr)
+ return False
+
+
+def rmdir(host, fqpath, force=False):
+ """Remove a directory.
+
+ Args:
+ host (str): The hostname/ip of the remote system.
+ fqpath (str): The fully-qualified path to the file.
+ force (bool, optional): Remove directory with recursive file delete.
+
+ Returns:
+ True on success. False on failure.
+ """
+ command_list = ['rmdir']
+ if force:
+ command_list = ["rm"]
+ command_list.append('-rf')
+ command_list.append(fqpath)
+ rcode, _, rerr = g.run(host, ' '.join(command_list))
+
+ if rcode == 0:
+ return True
+
+ g.log.error("Directory remove failed: %s" % rerr)
+ return False
+
+
+class GlusterDir(GlusterFile):
+ """Class to handle directories specific to Gluster (client and backend)"""
+ def mkdir(self, parents=False, mode=None):
+ """mkdir the instance fqpath on the remote host.
+
+ Args:
+ parents (Bool, optional): create parent directories
+ if they do not exist.
+ mode (str, optional): the initial mode of the directory.
+
+ Returns:
+ True on success.
+ False on failure.
+ """
+ if not self.exists_on_client:
+ ret = mkdir(self._host, self._fqpath, parents, mode)
+ if ret:
+ return True
+
+ return False
+
+ def create(self):
+ """Creates the directory and parent directories if they do not exist.
+ Overrides GlusterFile.create() to handle directories.
+
+ Args:
+ None
+
+ Returns:
+ True on success. False on failure.
+ """
+ # TODO: extend with additional methods to create directories as needed
+ return self.mkdir(parents=True, mode=None)
diff --git a/glustolibs-gluster/glustolibs/gluster/glusterfile.py b/glustolibs-gluster/glustolibs/gluster/glusterfile.py
new file mode 100644
index 000000000..fcc10f25f
--- /dev/null
+++ b/glustolibs-gluster/glustolibs/gluster/glusterfile.py
@@ -0,0 +1,710 @@
+#!/usr/bin/env python
+# Copyright (C) 2018 Red Hat, Inc. <http://www.redhat.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+"""Description: Module for library gluster file class and related functions.
+
+A GlusterFile is a file object that exists on the client and backend brick.
+This module provides low-level functions and a GlusterFile class to maintain
+state and manage properties of a file in both locations.
+"""
+
+import ctypes
+import os
+import re
+
+from glusto.core import Glusto as g
+
+from glustolibs.gluster.layout import Layout
+
+
+def calculate_hash(host, filename):
+ """ Function to import DHT Hash library.
+
+ Args:
+ filename (str): the name of the file
+
+ Returns:
+ An integer representation of the hash
+ """
+ # TODO: For testcases specifically testing hashing routine
+ # consider using a baseline external Davies-Meyer hash_value.c
+ # Creating comparison hash from same library we are testing
+ # may not be best practice here. (Holloway)
+ try:
+ # Check if libglusterfs.so.0 is available locally
+ glusterfs = ctypes.cdll.LoadLibrary("libglusterfs.so.0")
+ g.log.debug("Library libglusterfs.so.0 loaded locally")
+ except OSError:
+ conn = g.get_connection(host)
+ glusterfs = \
+ conn.modules.ctypes.cdll.LoadLibrary("libglusterfs.so.0")
+ g.log.debug("Library libglusterfs.so.0 loaded via rpyc")
+
+ computed_hash = \
+ ctypes.c_uint32(glusterfs.gf_dm_hashfn(filename, len(filename)))
+ # conn.close()
+
+ return int(computed_hash.value)
+
+
+def get_mountpoint(host, fqpath):
+ """Retrieve the mountpoint under a file
+
+ Args:
+ host (str): The hostname/ip of the remote system.
+ fqpath (str): The fully-qualified path to the file.
+
+ Returns:
+ The mountpoint on success. None on fail.
+ """
+ command = "df -P %s | awk 'END{print $NF}'" % fqpath
+ rcode, rout, rerr = g.run(host, command)
+ if rcode == 0:
+ return rout.strip()
+
+ g.log.error("Get mountpoint failed: %s" % rerr)
+ return None
+
+
+def get_fattr(host, fqpath, fattr):
+ """getfattr for filepath on remote system
+
+ Args:
+ host (str): The hostname/ip of the remote system.
+ fqpath (str): The fully-qualified path to the file.
+ fattr (str): name of the fattr to retrieve
+
+ Returns:
+ getfattr result on success. None on fail.
+ """
+ command = ("getfattr --absolute-names --only-values -n '%s' %s" %
+ (fattr, fqpath))
+ rcode, rout, rerr = g.run(host, command)
+
+ if rcode == 0:
+ return rout.strip()
+
+ g.log.error('getfattr failed: %s' % rerr)
+ return None
+
+
+def get_fattr_list(host, fqpath):
+ """List of xattr for filepath on remote system.
+
+ Args:
+ host (str): The hostname/ip of the remote system.
+ fqpath (str): The fully-qualified path to the file.
+
+ Returns:
+ Dictionary of xattrs on success. None on fail.
+ """
+ command = "getfattr --absolute-names -d -m - %s" % fqpath
+ rcode, rout, rerr = g.run(host, command)
+
+ if rcode == 0:
+ xattr_list = {}
+ for xattr_string in rout.strip().split('\n'):
+ xattr = xattr_string.split('=', 1)
+ if len(xattr) > 1:
+ key, value = xattr
+ xattr_list[key] = value
+
+ return xattr_list
+
+ g.log.error('getfattr failed: %s' % rerr)
+ return None
+
+
+def set_fattr(host, fqpath, fattr, value):
+ """setfattr for filepath on remote system
+
+ Args:
+ host (str): The hostname/ip of the remote system.
+ fqpath (str): The fully-qualified path to the file.
+ fattr (str): The name of the fattr to retrieve.
+
+ Returns:
+ setfattr result on success. errorcode on fail
+ """
+ command = 'setfattr -n %s -v %s %s' % (fattr, value, fqpath)
+ rcode, _, rerr = g.run(host, command)
+
+ if rcode == 0:
+ return True
+
+ g.log.error('setfattr failed: %s', rerr)
+ return False
+
+
+def delete_fattr(host, fqpath, fattr):
+ """remove fattr for filepath on remote system
+
+ Args:
+ host (str): hostname/ip of remote system
+ fqpath (str): the fully qualified path of the file
+ fattr (str): name of the fattr to delete
+
+ Returns:
+ setfattr result on success. errorcode on fail
+ """
+ command = 'setfattr -x %s %s' % (fattr, fqpath)
+ rcode, _, rerr = g.run(host, command)
+
+ if rcode == 0:
+ return True
+
+ g.log.error('setfattr -x failed: %s' % rerr)
+ return False
+
+
+def file_exists(host, fqpath):
+ """Check if file exists at path on host
+
+ Args:
+ host (str): hostname or ip of system
+ filename (str): fully qualified path of file
+
+ Returns:
+ True if file exists. False if file does not exist
+ """
+ command = "ls -ld %s" % fqpath
+ rcode, _, rerr = g.run(host, command)
+ if rcode == 0:
+ return True
+
+ g.log.error('File does not exist: %s', rerr)
+ return False
+
+
+def get_md5sum(host, fqpath):
+ """Get the md5 checksum for the file.
+
+ Args:
+ host (str): The hostname/ip of the remote system.
+ fqpath (str): The fully-qualified path to the file.
+
+ Returns:
+ The md5sum of the file on success. None on fail.
+ """
+ command = "md5sum %s" % fqpath
+ rcode, rout, rerr = g.run(host, command)
+
+ if rcode == 0:
+ return rout.strip()
+
+ g.log.error('md5sum failed: %s' % rerr)
+ return None
+
+
+def get_file_stat(host, fqpath):
+ """Get file stat information about a file.
+
+ Args:
+ host (str): The hostname/ip of the remote system.
+ fqpath (str): The fully-qualified path to the file.
+
+ Returns:
+ A dictionary of file stat data. None on fail.
+ """
+ statformat = '%F:%n:%i:%a:%s:%h:%u:%g:%U:%G'
+ command = "stat -c '%s' %s" % (statformat, fqpath)
+ rcode, rout, rerr = g.run(host, command)
+ if rcode == 0:
+ stat_data = {}
+ stat_string = rout.strip()
+ (filetype, filename, inode,
+ access, size, links,
+ uid, gid, username, groupname) = stat_string.split(":")
+
+ stat_data['filetype'] = filetype
+ stat_data['filename'] = filename
+ stat_data["inode"] = inode
+ stat_data["access"] = access
+ stat_data["size"] = size
+ stat_data["links"] = links
+ stat_data["username"] = username
+ stat_data["groupname"] = groupname
+ stat_data["uid"] = uid
+ stat_data["gid"] = gid
+
+ return stat_data
+
+ g.log.error("Could not stat file %s: %s" % (fqpath, rerr))
+ return None
+
+
+def set_file_permissions(host, fqpath, perms):
+ """Set permissions on a remote file.
+
+ Args:
+ host (str): The hostname/ip of the remote system.
+ fqpath (str): The fully-qualified path to the file.
+ perms (str): A permissions string as passed to chmod.
+
+ Returns:
+ True on success. False on fail.
+ """
+ command = "chmod %s %s" % (perms, fqpath)
+ rcode, _, rerr = g.run(host, command)
+
+ if rcode == 0:
+ return True
+
+ g.log.error('chmod failed: %s' % rerr)
+ return False
+
+
+def set_file_owner(host, fqpath, user):
+ """Set file owner for a remote file.
+
+ Args:
+ host (str): The hostname/ip of the remote system.
+ fqpath (str): The fully-qualified path to the file.
+ user (str): The user owning the file.
+
+ Returns:
+ True on success. False on fail.
+ """
+ command = "chown %s %s" % (user, fqpath)
+ rcode, _, rerr = g.run(host, command)
+
+ if rcode == 0:
+ return True
+
+ g.log.error('chown failed: %s' % rerr)
+ return False
+
+
+def set_file_group(host, fqpath, group):
+ """Set file group for a remote file.
+
+ Args:
+ host (str): The hostname/ip of the remote system.
+ fqpath (str): The fully-qualified path to the file.
+ group (str): The group owning the file.
+
+ Returns:
+ True on success. False on fail.
+ """
+ command = "chgrp %s %s" % (group, fqpath)
+ rcode, _, rerr = g.run(host, command)
+
+ if rcode == 0:
+ return True
+
+ g.log.error('chgrp failed: %s' % rerr)
+ return False
+
+
+def move_file(host, source_fqpath, dest_fqpath):
+ """Move a remote file.
+
+ Args:
+ host (str): The hostname/ip of the remote system.
+ source_fqpath (str): The fully-qualified path to the file to move.
+ dest_fqpath (str): The fully-qualified path to the new file location.
+
+ Returns:
+ True on success. False on fail.
+ """
+ command = "mv %s %s" % (source_fqpath, dest_fqpath)
+ rcode, _, rerr = g.run(host, command)
+
+ if rcode == 0:
+ return True
+
+ g.log.error('mv failed: %s' % rerr)
+ return False
+
+
+def remove_file(host, fqpath, force=False):
+ """Removes a remote file.
+
+ Args:
+ host (str): The hostname/ip of the remote system.
+ fqpath (str): The fully-qualified path to the file.
+
+ Returns:
+ True on success. False on fail.
+ """
+ command_list = ['rm']
+ if force:
+ command_list.append('-f')
+ command_list.append(fqpath)
+ rcode, _, rerr = g.run(host, ' '.join(command_list))
+
+ if rcode == 0:
+ return True
+
+ g.log.error('Remove file failed: %s' % rerr)
+ return False
+
+
+def get_pathinfo(host, fqpath):
+ """Get pathinfo for a remote file.
+
+ Args:
+ host (str): The hostname/ip of the remote system.
+ fqpath (str): The fully-qualified path to the file.
+
+ Returns:
+ A dictionary of pathinfo data for a remote file. None on fail.
+ """
+ pathinfo = {}
+ pathinfo['raw'] = get_fattr(host, fqpath, 'trusted.glusterfs.pathinfo')
+ pathinfo['brickdir_paths'] = re.findall(".*?POSIX.*?:(\S+)\>",
+ pathinfo['raw'])
+
+ return pathinfo
+
+
+def is_linkto_file(host, fqpath):
+ """Test if file is a dht linkto file.
+ To return True, file must...
+ 1. be of file type 'sticky empty'
+ 2. have size of 0
+ 3. have the glusterfs.dht.linkto xattr set.
+
+ Args:
+ host (str): The hostname/ip of the remote system.
+ fqpath (str): The fully-qualified path to the file.
+
+ Returns:
+ True or False
+ """
+ command = 'file %s' % fqpath
+ rcode, rout, _ = g.run(host, command)
+
+ if rcode == 0:
+ if 'sticky empty' in rout.strip():
+ stat = get_file_stat(host, fqpath)
+ if int(stat['size']) == 0:
+ # xattr = get_fattr(host, fqpath,
+ # 'trusted.glusterfs.dht.linkto')
+ xattr = get_dht_linkto_xattr(host, fqpath)
+ if xattr is not None:
+ return True
+
+ return False
+
+
+def get_dht_linkto_xattr(host, fqpath):
+ """Get the glusterfs.dht.linkto xattr for a file on a brick.
+
+ Args:
+ host (str): The hostname/ip of the remote system.
+ fqpath (str): The fully-qualified path to the file.
+
+ Returns:
+ Return value of get_fattr trusted.glusterfs.dht.linkto call.
+ """
+ linkto_xattr = get_fattr(host, fqpath, 'trusted.glusterfs.dht.linkto')
+
+ return linkto_xattr
+
+
+class GlusterFile(object):
+ """Class to handle files specific to Gluster (client and backend)"""
+ def __init__(self, host, fqpath):
+ self._host = host
+ self._fqpath = fqpath
+
+ self._mountpoint = None
+ self._calculated_hash = None
+ self._pathinfo = None
+ self._parent_dir_pathinfo = None
+ self._parent_dir_layout = None
+
+ self._previous_fqpath = None
+
+ @property
+ def host(self):
+ """str: the hostname/ip of the client system hosting the file."""
+ return self._host
+
+ @property
+ def fqpath(self):
+ """str: the fully-qualified path of the file on the client system."""
+ return self._fqpath
+
+ @property
+ def relative_path(self):
+ """str: the relative path from the mountpoint of the file."""
+ return os.path.relpath(self._fqpath, self.mountpoint)
+
+ @property
+ def basename(self):
+ """str: the name of the file with directories stripped from string."""
+ return os.path.basename(self._fqpath)
+
+ @property
+ def parent_dir(self):
+ """str: the full-qualified path of the file's parent directory."""
+ return os.path.dirname(self._fqpath)
+
+ @property
+ def mountpoint(self):
+ """str: the fully-qualified path of the mountpoint under the file."""
+ if self._mountpoint is None:
+ self._mountpoint = get_mountpoint(self._host, self._fqpath)
+
+ return self._mountpoint
+
+ @property
+ def pathinfo(self):
+ """dict: a dictionary of path_info-related values"""
+ self._pathinfo = get_pathinfo(self._host, self._fqpath)
+
+ return self._pathinfo
+
+ @property
+ def parent_dir_pathinfo(self):
+ """"dict: a dictionary of path_info-related values for the parent dir
+ of the file's fqpath.
+ """
+ parent_dir_pathinfo = get_pathinfo(self._host, self.parent_dir)
+
+ return parent_dir_pathinfo
+
+ @property
+ def exists_on_client(self):
+ """bool: Does the file exists on the client?"""
+ ret = file_exists(self._host, self._fqpath)
+
+ if ret:
+ return True
+
+ return False
+
+ @property
+ def exists_on_bricks(self):
+ """bool: Does the file exist on the backend bricks?"""
+ flag = 0
+ for brickdir_path in self.pathinfo['brickdir_paths']:
+ (host, fqpath) = brickdir_path.split(':')
+ if not file_exists(host, fqpath):
+ flag = flag | 1
+
+ if flag == 0:
+ return True
+
+ return False
+
+ @property
+ def exists_on_hashed_bricks(self):
+ """bool: Does the file exist on the hashed bricks as expected?"""
+ # TODO: inject check for linkto and data files
+ flag = 0
+ for brickdir_path in self.hashed_bricks:
+ (host, fqpath) = brickdir_path.split(':')
+ if not file_exists(host, fqpath):
+ flag = flag | 1
+
+ if flag == 0:
+ return True
+
+ return False
+
+ @property
+ def exists_on_cached_bricks(self):
+ """bool: Does the file exist on the cached bricks as expected?
+
+ This currently is redundant as the cache list is currently
+ created by searching bricks for the file. This will be more
+ useful when the cached brick list is compiled by following the
+ subvolume info provided in the linkto xattr.
+ """
+ flag = 0
+ for brickdir_path in self.cached_bricks:
+ (host, fqpath) = brickdir_path.split(':')
+ if not file_exists(host, fqpath):
+ flag = flag | 1
+
+ if flag == 0:
+ return True
+
+ return False
+
+ @property
+ def exists(self):
+ """bool: does the file exist on both client and backend bricks"""
+ return (self.exists_on_client, self.exists_on_bricks)
+
+ @property
+ def stat_on_client(self):
+ """dict: a dictionary of stat data"""
+ return get_file_stat(self._host, self._fqpath)
+
+ @property
+ def stat_on_bricks(self):
+ """dict: a dictionary of stat dictionaries for the file on bricks"""
+ file_stats = {}
+ for brickdir_path in self.pathinfo['brickdir_paths']:
+ (host, fqpath) = brickdir_path.split(':')
+ file_stats[brickdir_path] = get_file_stat(host, fqpath)
+
+ return file_stats
+
+ @property
+ def stat(self):
+ """list: a list of the stat dictionary data for client and bricks."""
+ return (self.stat_on_client, self.stat_on_bricks)
+
+ @property
+ def md5sum_on_client(self):
+ """str: the md5sum for the file on the client"""
+ return get_md5sum(self._host, self._fqpath)
+
+ @property
+ def md5sum_on_bricks(self):
+ """dict: a dictionary of md5sums for the file on bricks"""
+ # TODO: handle dispersed ???
+ file_md5s = {}
+ for brickdir_path in self.pathinfo['brickdir_paths']:
+ (host, fqpath) = brickdir_path.split(':')
+ file_md5s[brickdir_path] = get_md5sum(host, fqpath)
+
+ return file_md5s
+
+ @property
+ def md5sum(self):
+ """list: a list of client and brick md5sum data"""
+ return (self.md5sum_on_client, self.md5sum_on_bricks)
+
+ @property
+ def calculated_hash(self):
+ """str: the computed hash of the file using libglusterfs"""
+ if self._calculated_hash is None:
+ self._calculated_hash = calculate_hash(self._host, self.basename)
+
+ return self._calculated_hash
+
+ @property
+ def parent_dir_layout(self):
+ """obj: Layout instance of the file's parent directory"""
+ if self._parent_dir_layout is None:
+ layout = Layout(self.parent_dir_pathinfo)
+ self._parent_dir_layout = layout
+ else:
+ layout = self._parent_dir_layout
+
+ return layout
+
+ @property
+ def hashed_bricks(self):
+ """list: the list of bricks matching with hashrange surrounding hash"""
+ brickpaths = []
+ for brickdir in self.parent_dir_layout.brickdirs:
+ low = brickdir.hashrange_low
+ high = brickdir.hashrange_high
+ if low < self.calculated_hash < high:
+ brickpaths.append(brickdir.path)
+ g.log.debug("%s: %d - %d - %d" % (brickdir.path,
+ brickdir.hashrange_low,
+ self.calculated_hash,
+ brickdir.hashrange_high))
+
+ return brickpaths
+
+ @property
+ def cached_bricks(self):
+ """list: the list of bricks with the cached file(s)"""
+ # TODO: build list from subvolume in glusterfs.dht.linkto xattr
+ brickpaths = []
+ for brickdir in self.parent_dir_layout.brickdirs:
+ fqpath = os.path.join(brickdir.fqpath, self.basename)
+ if file_exists(brickdir.host, fqpath):
+ if not is_linkto_file(brickdir.host, fqpath):
+ brickpaths.append(brickdir.path)
+
+ return brickpaths
+
+ def move(self, dest_fqpath):
+ """Move the file to a new location and store previous fqpath.
+
+ Args:
+ dest_fqpath (str): The fully-qualified destination path.
+
+ Returns:
+ True on success. False on fail.
+ """
+ ret = move_file(self._host, self._fqpath, dest_fqpath)
+
+ if ret:
+ # TODO: change this to use a setter/getter for heavy lifting once
+ # and can reset everything from one place
+ self._previous_fqpath = self._fqpath
+ self._fqpath = dest_fqpath
+
+ return True
+
+ return False
+
+ def create(self):
+ """Creates a simple file via copy for testing purposes.
+ Also creates parent directories if they don't exist.
+ Args:
+ None
+
+ Returns:
+ True on success. False on failure.
+ """
+ if not self.exists_on_client:
+ command = "mkdir -p %s" % self.parent_dir
+ rcode, _, _ = g.run(self._host, command)
+ if rcode != 0:
+ return False
+ command = "cp /etc/inittab %s" % self._fqpath
+ rcode, _, _ = g.run(self._host, command)
+ if rcode == 0:
+ return True
+
+ return False
+
+ def get_xattr(self, xattr):
+ """Get the xattr for the file instance.
+
+ Args:
+ xattr (str): The file attribute to get from file.
+
+ Returns:
+ Result of get_fattr function.
+ """
+ return get_fattr(self._host, self._fqpath, xattr)
+
+ def set_xattr(self, xattr, value):
+ """Set the specified xattr for the file instance.
+
+ Args:
+ xattr (str): The attribute to set on the file.
+ value (str): the value for the attribute.
+
+ Returns:
+ Return of set_fattr function.
+ """
+ return set_fattr(self._host, self._fqpath, xattr, value)
+
+ def delete_xattr(self, xattr):
+ """Delete the specified xattr for the file instance.
+
+ Args:
+ xattr (str): The attribute to delete.
+
+ Returns:
+ Return of delete_fattr function.
+ """
+ return delete_fattr(self._host, self._fqpath, xattr)
diff --git a/glustolibs-gluster/glustolibs/gluster/layout.py b/glustolibs-gluster/glustolibs/gluster/layout.py
new file mode 100644
index 000000000..c1ddb40f8
--- /dev/null
+++ b/glustolibs-gluster/glustolibs/gluster/layout.py
@@ -0,0 +1,149 @@
+#!/usr/bin/env python
+# Copyright (C) 2018 Red Hat, Inc. <http://www.redhat.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+"""Module for library DHT layout class and related functions"""
+
+from glusto.core import Glusto as g
+from glustolibs.gluster.brickdir import BrickDir
+
+
+class Layout(object):
+ """Default layout class for equal-sized bricks.
+ Other layouts should inherit from this class
+ and override/add where needed.
+ """
+ def _get_layout(self):
+ """Discover brickdir data and cache in instance for further use"""
+ self._brickdirs = []
+ for brickdir_path in self._pathinfo['brickdir_paths']:
+ brickdir = BrickDir(brickdir_path)
+ g.log.debug("%s: %s" % (brickdir.path, brickdir.hashrange))
+ self._brickdirs.append(brickdir)
+
+ def __init__(self, pathinfo):
+ """Init the layout class
+
+ Args:
+ pathinfo (dict): pathinfo collected from client directory
+ """
+ self._pathinfo = pathinfo
+ self._get_layout()
+ self._zero_hashrange_brickdirs = None
+ self._brickdirs = None
+
+ @property
+ def brickdirs(self):
+ """list: a list of brickdirs associated with this layout"""
+ if self._brickdirs is None:
+ self._get_layout()
+
+ return self._brickdirs
+
+ @property
+ def is_complete(self):
+ """Layout starts at zero,
+ ends at 32-bits high,
+ and has no holes or overlaps
+ """
+ joined_hashranges = []
+ for brickdir in self.brickdirs:
+ # join all of the hashranges into a single list
+ joined_hashranges += brickdir.hashrange
+ g.log.debug("joined range list: %s" % joined_hashranges)
+ # remove duplicate hashes
+ collapsed_ranges = list(set(joined_hashranges))
+ # sort the range list for good measure
+ collapsed_ranges.sort()
+
+ # first hash in the list is 0?
+ if collapsed_ranges[0] != 0:
+ g.log.error('First hash in range (%d) is not zero' %
+ collapsed_ranges[0])
+ return False
+
+ # last hash in the list is 32-bits high?
+ if collapsed_ranges[-1] != int(0xffffffff):
+ g.log.error('Last hash in ranges (%s) is not 0xffffffff' %
+ hex(collapsed_ranges[-1]))
+ return False
+
+ # remove the first and last hashes
+ clipped_ranges = collapsed_ranges[1:-1]
+ g.log.debug('clipped: %s' % clipped_ranges)
+
+ # walk through the list in pairs and look for diff == 1
+ iter_ranges = iter(clipped_ranges)
+ for first in iter_ranges:
+ second = next(iter_ranges)
+ hash_difference = second - first
+ g.log.debug('%d - %d = %d' % (second, first, hash_difference))
+ if hash_difference > 1:
+ g.log.error("Layout has holes")
+
+ return False
+ elif hash_difference < 1:
+ g.log.error("Layout has overlaps")
+
+ return False
+
+ return True
+
+ @property
+ def has_zero_hashranges(self):
+ """Check brickdirs for zero hashrange"""
+ # TODO: change this to use self.zero_hashrange_brickdirs and set bool
+ low_and_high_zero = False
+ for brickdir in self._brickdirs:
+ if brickdir.has_zero_hashrange:
+ low_and_high_zero = True
+
+ return low_and_high_zero
+
+ @property
+ def zero_hashrange_brickdirs(self):
+ """list: the list of zero_hashrange_brickdirs"""
+ if self._zero_hashrange_brickdirs is None:
+ zero_hashrange_brickdirs = []
+ for brickdir in self._brickdirs:
+ if brickdir.has_zero_hashrange():
+ zero_hashrange_brickdirs.append(brickdir)
+ self._zero_hashrange_brickdirs = zero_hashrange_brickdirs
+
+ return self._zero_hashrange_brickdirs
+
+ @property
+ def is_balanced(self):
+ """Checks for balanced distribution in equal-sized bricks"""
+ baseline_size = None
+ for brickdir in self._brickdirs:
+ hashrange_low = brickdir.hashrange_low
+ hashrange_high = brickdir.hashrange_high
+
+ if baseline_size is None:
+ baseline_size = int(hashrange_high) - int(hashrange_low)
+ g.log.debug('Baseline size: %d' % baseline_size)
+ continue
+ else:
+ size = int(hashrange_high) - int(hashrange_low)
+ g.log.debug('Hashrange size: %d' % size)
+
+ # if any of the range diffs differ, exit immediately False
+ if int(size) != int(baseline_size):
+ g.log.error('Brick distribution is not balanced.')
+ return False
+
+ return True