From ab7a71cc4b2862d267c3e6fae67c711e51abca77 Mon Sep 17 00:00:00 2001 From: Jonathan Holloway Date: Tue, 23 Jan 2018 02:02:38 +0100 Subject: 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 --- glustolibs-gluster/glustolibs/gluster/brickdir.py | 184 ++++++ glustolibs-gluster/glustolibs/gluster/constants.py | 33 + .../glustolibs/gluster/dht_test_utils.py | 148 +++++ .../glustolibs/gluster/exceptions.py | 83 +++ .../glustolibs/gluster/glusterdir.py | 117 ++++ .../glustolibs/gluster/glusterfile.py | 710 +++++++++++++++++++++ glustolibs-gluster/glustolibs/gluster/layout.py | 149 +++++ 7 files changed, 1424 insertions(+) create mode 100644 glustolibs-gluster/glustolibs/gluster/brickdir.py create mode 100644 glustolibs-gluster/glustolibs/gluster/constants.py create mode 100644 glustolibs-gluster/glustolibs/gluster/dht_test_utils.py create mode 100644 glustolibs-gluster/glustolibs/gluster/glusterdir.py create mode 100644 glustolibs-gluster/glustolibs/gluster/glusterfile.py create mode 100644 glustolibs-gluster/glustolibs/gluster/layout.py (limited to 'glustolibs-gluster/glustolibs') 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. +# +# 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. +# +# 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. +# +# 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. +# +# 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. +# +# 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. +# +# 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. +# +# 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 -- cgit