X-Git-Url: https://code.grnet.gr/git/ganeti-local/blobdiff_plain/5c755a4dae09d5bb1bce61930d93ad817e71ea95..376631d15d3672cfba323bdf384089fd65c08c9a:/lib/bdev.py diff --git a/lib/bdev.py b/lib/bdev.py index 1d85867..cff63a3 100644 --- a/lib/bdev.py +++ b/lib/bdev.py @@ -36,6 +36,7 @@ from ganeti import constants from ganeti import objects from ganeti import compat from ganeti import netutils +from ganeti import pathutils # Size of reads in _CanReadDevice @@ -74,6 +75,17 @@ def _ThrowError(msg, *args): raise errors.BlockDeviceError(msg) +def _CheckResult(result): + """Throws an error if the given result is a failed one. + + @param result: result from RunCmd + + """ + if result.failed: + _ThrowError("Command: %s error: %s - %s", result.cmd, result.fail_reason, + result.output) + + def _CanReadDevice(path): """Check if we can read from the given device. @@ -88,6 +100,116 @@ def _CanReadDevice(path): return False +def _GetForbiddenFileStoragePaths(): + """Builds a list of path prefixes which shouldn't be used for file storage. + + @rtype: frozenset + + """ + paths = set([ + "/boot", + "/dev", + "/etc", + "/home", + "/proc", + "/root", + "/sys", + ]) + + for prefix in ["", "/usr", "/usr/local"]: + paths.update(map(lambda s: "%s/%s" % (prefix, s), + ["bin", "lib", "lib32", "lib64", "sbin"])) + + return compat.UniqueFrozenset(map(os.path.normpath, paths)) + + +def _ComputeWrongFileStoragePaths(paths, + _forbidden=_GetForbiddenFileStoragePaths()): + """Cross-checks a list of paths for prefixes considered bad. + + Some paths, e.g. "/bin", should not be used for file storage. + + @type paths: list + @param paths: List of paths to be checked + @rtype: list + @return: Sorted list of paths for which the user should be warned + + """ + def _Check(path): + return (not os.path.isabs(path) or + path in _forbidden or + filter(lambda p: utils.IsBelowDir(p, path), _forbidden)) + + return utils.NiceSort(filter(_Check, map(os.path.normpath, paths))) + + +def ComputeWrongFileStoragePaths(_filename=pathutils.FILE_STORAGE_PATHS_FILE): + """Returns a list of file storage paths whose prefix is considered bad. + + See L{_ComputeWrongFileStoragePaths}. + + """ + return _ComputeWrongFileStoragePaths(_LoadAllowedFileStoragePaths(_filename)) + + +def _CheckFileStoragePath(path, allowed): + """Checks if a path is in a list of allowed paths for file storage. + + @type path: string + @param path: Path to check + @type allowed: list + @param allowed: List of allowed paths + @raise errors.FileStoragePathError: If the path is not allowed + + """ + if not os.path.isabs(path): + raise errors.FileStoragePathError("File storage path must be absolute," + " got '%s'" % path) + + for i in allowed: + if not os.path.isabs(i): + logging.info("Ignoring relative path '%s' for file storage", i) + continue + + if utils.IsBelowDir(i, path): + break + else: + raise errors.FileStoragePathError("Path '%s' is not acceptable for file" + " storage" % path) + + +def _LoadAllowedFileStoragePaths(filename): + """Loads file containing allowed file storage paths. + + @rtype: list + @return: List of allowed paths (can be an empty list) + + """ + try: + contents = utils.ReadFile(filename) + except EnvironmentError: + return [] + else: + return utils.FilterEmptyLinesAndComments(contents) + + +def CheckFileStoragePath(path, _filename=pathutils.FILE_STORAGE_PATHS_FILE): + """Checks if a path is allowed for file storage. + + @type path: string + @param path: Path to check + @raise errors.FileStoragePathError: If the path is not allowed + + """ + allowed = _LoadAllowedFileStoragePaths(_filename) + + if _ComputeWrongFileStoragePaths([path]): + raise errors.FileStoragePathError("Path '%s' uses a forbidden prefix" % + path) + + _CheckFileStoragePath(path, allowed) + + class BlockDev(object): """Block device abstract class. @@ -338,7 +460,7 @@ class BlockDev(object): for child in self._children: child.SetInfo(text) - def Grow(self, amount, dryrun): + def Grow(self, amount, dryrun, backingstore): """Grow the block device. @type amount: integer @@ -346,6 +468,9 @@ class BlockDev(object): @type dryrun: boolean @param dryrun: whether to execute the operation in simulation mode only, without actually increasing the size + @param backingstore: whether to execute the operation on backing storage + only, or on "logical" storage only; e.g. DRBD is logical storage, + whereas LVM, file, RBD are backing storage """ raise NotImplementedError @@ -378,8 +503,8 @@ class LogicalVolume(BlockDev): """ _VALID_NAME_RE = re.compile("^[a-zA-Z0-9+_.-]*$") - _INVALID_NAMES = frozenset([".", "..", "snapshot", "pvmove"]) - _INVALID_SUBSTRINGS = frozenset(["_mlog", "_mimage"]) + _INVALID_NAMES = compat.UniqueFrozenset([".", "..", "snapshot", "pvmove"]) + _INVALID_SUBSTRINGS = compat.UniqueFrozenset(["_mlog", "_mimage"]) def __init__(self, unique_id, children, size, params): """Attaches to a LV device. @@ -741,20 +866,33 @@ class LogicalVolume(BlockDev): _ThrowError("Not enough free space: required %s," " available %s", size, free_size) - result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-s", - "-n%s" % snap_name, self.dev_path]) - if result.failed: - _ThrowError("command: %s error: %s - %s", - result.cmd, result.fail_reason, result.output) + _CheckResult(utils.RunCmd(["lvcreate", "-L%dm" % size, "-s", + "-n%s" % snap_name, self.dev_path])) return (self._vg_name, snap_name) + def _RemoveOldInfo(self): + """Try to remove old tags from the lv. + + """ + result = utils.RunCmd(["lvs", "-o", "tags", "--noheadings", "--nosuffix", + self.dev_path]) + _CheckResult(result) + + raw_tags = result.stdout.strip() + if raw_tags: + for tag in raw_tags.split(","): + _CheckResult(utils.RunCmd(["lvchange", "--deltag", + tag.strip(), self.dev_path])) + def SetInfo(self, text): """Update metadata with info text. """ BlockDev.SetInfo(self, text) + self._RemoveOldInfo() + # Replace invalid characters text = re.sub("^[^A-Za-z0-9_+.]", "_", text) text = re.sub("[^-A-Za-z0-9_+.]", "_", text) @@ -762,16 +900,14 @@ class LogicalVolume(BlockDev): # Only up to 128 characters are allowed text = text[:128] - result = utils.RunCmd(["lvchange", "--addtag", text, - self.dev_path]) - if result.failed: - _ThrowError("Command: %s error: %s - %s", result.cmd, result.fail_reason, - result.output) + _CheckResult(utils.RunCmd(["lvchange", "--addtag", text, self.dev_path])) - def Grow(self, amount, dryrun): + def Grow(self, amount, dryrun, backingstore): """Grow the logical volume. """ + if not backingstore: + return if self.pe_size is None or self.stripe_count is None: if not self.Attach(): _ThrowError("Can't attach to LV during Grow()") @@ -822,7 +958,7 @@ class DRBD8Status(object): CS_SYNCTARGET = "SyncTarget" CS_PAUSEDSYNCS = "PausedSyncS" CS_PAUSEDSYNCT = "PausedSyncT" - CSET_SYNC = frozenset([ + CSET_SYNC = compat.UniqueFrozenset([ CS_WFREPORTPARAMS, CS_STARTINGSYNCS, CS_STARTINGSYNCT, @@ -918,7 +1054,7 @@ class BaseDRBD(BlockDev): # pylint: disable=W0223 _ST_WFCONNECTION = "WFConnection" _ST_CONNECTED = "Connected" - _STATUS_FILE = "/proc/drbd" + _STATUS_FILE = constants.DRBD_STATUS_FILE _USERMODE_HELPER_FILE = "/sys/module/drbd/parameters/usermode_helper" @staticmethod @@ -985,12 +1121,13 @@ class BaseDRBD(BlockDev): # pylint: disable=W0223 first_line) values = version.groups() - retval = {"k_major": int(values[0]), - "k_minor": int(values[1]), - "k_point": int(values[2]), - "api": int(values[3]), - "proto": int(values[4]), - } + retval = { + "k_major": int(values[0]), + "k_minor": int(values[1]), + "k_point": int(values[2]), + "api": int(values[3]), + "proto": int(values[4]), + } if values[5] is not None: retval["proto2"] = values[5] @@ -1388,7 +1525,7 @@ class DRBD8(BaseDRBD): @classmethod def _ComputeDiskBarrierArgs(cls, vmaj, vmin, vrel, disabled_barriers, - disable_meta_flush): + disable_meta_flush): """Compute the DRBD command line parameters for disk barriers Returns a list of the disk barrier parameters as requested via the @@ -1622,7 +1759,7 @@ class DRBD8(BaseDRBD): "--c-delay-target", params[constants.LDP_DELAY_TARGET], "--c-max-rate", params[constants.LDP_MAX_RATE], "--c-min-rate", params[constants.LDP_MIN_RATE], - ]) + ]) else: args.extend(["-r", "%d" % params[constants.LDP_RESYNC_RATE]]) @@ -2070,7 +2207,7 @@ class DRBD8(BaseDRBD): cls._InitMeta(aminor, meta.dev_path) return cls(unique_id, children, size, params) - def Grow(self, amount, dryrun): + def Grow(self, amount, dryrun, backingstore): """Resize the DRBD device and its backing storage. """ @@ -2078,9 +2215,10 @@ class DRBD8(BaseDRBD): _ThrowError("drbd%d: Grow called while not attached", self._aminor) if len(self._children) != 2 or None in self._children: _ThrowError("drbd%d: cannot grow diskless device", self.minor) - self._children[0].Grow(amount, dryrun) - if dryrun: - # DRBD does not support dry-run mode, so we'll return here + self._children[0].Grow(amount, dryrun, backingstore) + if dryrun or backingstore: + # DRBD does not support dry-run mode and is not backing storage, + # so we'll return here return result = utils.RunCmd(["drbdsetup", self.dev_path, "resize", "-s", "%dm" % (self.size + amount)]) @@ -2107,6 +2245,9 @@ class FileStorage(BlockDev): raise ValueError("Invalid configuration data %s" % str(unique_id)) self.driver = unique_id[0] self.dev_path = unique_id[1] + + CheckFileStoragePath(self.dev_path) + self.Attach() def Assemble(self): @@ -2163,12 +2304,14 @@ class FileStorage(BlockDev): # TODO: implement rename for file-based storage _ThrowError("Rename is not supported for file-based storage") - def Grow(self, amount, dryrun): + def Grow(self, amount, dryrun, backingstore): """Grow the file @param amount: the amount (in mebibytes) to grow with """ + if not backingstore: + return # Check that the file exists self.Assemble() current_size = self.GetActualSize() @@ -2221,7 +2364,11 @@ class FileStorage(BlockDev): """ if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2: raise ValueError("Invalid configuration data %s" % str(unique_id)) + dev_path = unique_id[1] + + CheckFileStoragePath(dev_path) + try: fd = os.open(dev_path, os.O_RDWR | os.O_CREAT | os.O_EXCL) f = os.fdopen(fd, "w") @@ -2339,7 +2486,7 @@ class PersistentBlockDevice(BlockDev): """ pass - def Grow(self, amount, dryrun): + def Grow(self, amount, dryrun, backingstore): """Grow the logical volume. """ @@ -2605,7 +2752,7 @@ class RADOSBlockDevice(BlockDev): """ pass - def Grow(self, amount, dryrun): + def Grow(self, amount, dryrun, backingstore): """Grow the Volume. @type amount: integer @@ -2615,6 +2762,8 @@ class RADOSBlockDevice(BlockDev): only, without actually increasing the size """ + if not backingstore: + return if not self.Attach(): _ThrowError("Can't attach to rbd device during Grow()") @@ -2637,11 +2786,361 @@ class RADOSBlockDevice(BlockDev): result.fail_reason, result.output) +class ExtStorageDevice(BlockDev): + """A block device provided by an ExtStorage Provider. + + This class implements the External Storage Interface, which means + handling of the externally provided block devices. + + """ + def __init__(self, unique_id, children, size, params): + """Attaches to an extstorage block device. + + """ + super(ExtStorageDevice, self).__init__(unique_id, children, size, params) + if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2: + raise ValueError("Invalid configuration data %s" % str(unique_id)) + + self.driver, self.vol_name = unique_id + + self.major = self.minor = None + self.Attach() + + @classmethod + def Create(cls, unique_id, children, size, params): + """Create a new extstorage device. + + Provision a new volume using an extstorage provider, which will + then be mapped to a block device. + + """ + if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2: + raise errors.ProgrammerError("Invalid configuration data %s" % + str(unique_id)) + + # Call the External Storage's create script, + # to provision a new Volume inside the External Storage + _ExtStorageAction(constants.ES_ACTION_CREATE, unique_id, str(size)) + + return ExtStorageDevice(unique_id, children, size, params) + + def Remove(self): + """Remove the extstorage device. + + """ + if not self.minor and not self.Attach(): + # The extstorage device doesn't exist. + return + + # First shutdown the device (remove mappings). + self.Shutdown() + + # Call the External Storage's remove script, + # to remove the Volume from the External Storage + _ExtStorageAction(constants.ES_ACTION_REMOVE, self.unique_id) + + def Rename(self, new_id): + """Rename this device. + + """ + pass + + def Attach(self): + """Attach to an existing extstorage device. + + This method maps the extstorage volume that matches our name with + a corresponding block device and then attaches to this device. + + """ + self.attached = False + + # Call the External Storage's attach script, + # to attach an existing Volume to a block device under /dev + self.dev_path = _ExtStorageAction(constants.ES_ACTION_ATTACH, + self.unique_id) + + try: + st = os.stat(self.dev_path) + except OSError, err: + logging.error("Error stat()'ing %s: %s", self.dev_path, str(err)) + return False + + if not stat.S_ISBLK(st.st_mode): + logging.error("%s is not a block device", self.dev_path) + return False + + self.major = os.major(st.st_rdev) + self.minor = os.minor(st.st_rdev) + self.attached = True + + return True + + def Assemble(self): + """Assemble the device. + + """ + pass + + def Shutdown(self): + """Shutdown the device. + + """ + if not self.minor and not self.Attach(): + # The extstorage device doesn't exist. + return + + # Call the External Storage's detach script, + # to detach an existing Volume from it's block device under /dev + _ExtStorageAction(constants.ES_ACTION_DETACH, self.unique_id) + + self.minor = None + self.dev_path = None + + def Open(self, force=False): + """Make the device ready for I/O. + + """ + pass + + def Close(self): + """Notifies that the device will no longer be used for I/O. + + """ + pass + + def Grow(self, amount, dryrun, backingstore): + """Grow the Volume. + + @type amount: integer + @param amount: the amount (in mebibytes) to grow with + @type dryrun: boolean + @param dryrun: whether to execute the operation in simulation mode + only, without actually increasing the size + + """ + if not backingstore: + return + if not self.Attach(): + _ThrowError("Can't attach to extstorage device during Grow()") + + if dryrun: + # we do not support dry runs of resize operations for now. + return + + new_size = self.size + amount + + # Call the External Storage's grow script, + # to grow an existing Volume inside the External Storage + _ExtStorageAction(constants.ES_ACTION_GROW, self.unique_id, + str(self.size), grow=str(new_size)) + + def SetInfo(self, text): + """Update metadata with info text. + + """ + # Replace invalid characters + text = re.sub("^[^A-Za-z0-9_+.]", "_", text) + text = re.sub("[^-A-Za-z0-9_+.]", "_", text) + + # Only up to 128 characters are allowed + text = text[:128] + + # Call the External Storage's setinfo script, + # to set metadata for an existing Volume inside the External Storage + _ExtStorageAction(constants.ES_ACTION_SETINFO, self.unique_id, + metadata=text) + + +def _ExtStorageAction(action, unique_id, size=None, grow=None, metadata=None): + """Take an External Storage action. + + Take an External Storage action concerning or affecting + a specific Volume inside the External Storage. + + @type action: string + @param action: which action to perform. One of: + create / remove / grow / attach / detach + @type unique_id: tuple (driver, vol_name) + @param unique_id: a tuple containing the type of ExtStorage (driver) + and the Volume name + @type size: integer + @param size: the size of the Volume in mebibytes + @type grow: integer + @param grow: the new size in mebibytes (after grow) + @type metadata: string + @param metadata: metadata info of the Volume, for use by the provider + @rtype: None or a block device path (during attach) + + """ + driver, vol_name = unique_id + + # Create an External Storage instance of type `driver' + status, inst_es = ExtStorageFromDisk(driver) + if not status: + _ThrowError("%s" % inst_es) + + # Create the basic environment for the driver's scripts + create_env = _ExtStorageEnvironment(unique_id, size, grow, metadata) + + # Do not use log file for action `attach' as we need + # to get the output from RunResult + # TODO: find a way to have a log file for attach too + logfile = None + if action is not constants.ES_ACTION_ATTACH: + logfile = _VolumeLogName(action, driver, vol_name) + + # Make sure the given action results in a valid script + if action not in constants.ES_SCRIPTS: + _ThrowError("Action '%s' doesn't result in a valid ExtStorage script" % + action) + + # Find out which external script to run according the given action + script_name = action + "_script" + script = getattr(inst_es, script_name) + + # Run the external script + result = utils.RunCmd([script], env=create_env, + cwd=inst_es.path, output=logfile,) + if result.failed: + logging.error("External storage's %s command '%s' returned" + " error: %s, logfile: %s, output: %s", + action, result.cmd, result.fail_reason, + logfile, result.output) + + # If logfile is 'None' (during attach), it breaks TailFile + # TODO: have a log file for attach too + if action is not constants.ES_ACTION_ATTACH: + lines = [utils.SafeEncode(val) + for val in utils.TailFile(logfile, lines=20)] + else: + lines = result.output[-20:] + + _ThrowError("External storage's %s script failed (%s), last" + " lines of output:\n%s", + action, result.fail_reason, "\n".join(lines)) + + if action == constants.ES_ACTION_ATTACH: + return result.stdout + + +def ExtStorageFromDisk(name, base_dir=None): + """Create an ExtStorage instance from disk. + + This function will return an ExtStorage instance + if the given name is a valid ExtStorage name. + + @type base_dir: string + @keyword base_dir: Base directory containing ExtStorage installations. + Defaults to a search in all the ES_SEARCH_PATH dirs. + @rtype: tuple + @return: True and the ExtStorage instance if we find a valid one, or + False and the diagnose message on error + + """ + if base_dir is None: + es_base_dir = pathutils.ES_SEARCH_PATH + else: + es_base_dir = [base_dir] + + es_dir = utils.FindFile(name, es_base_dir, os.path.isdir) + + if es_dir is None: + return False, ("Directory for External Storage Provider %s not" + " found in search path" % name) + + # ES Files dictionary, we will populate it with the absolute path + # names; if the value is True, then it is a required file, otherwise + # an optional one + es_files = dict.fromkeys(constants.ES_SCRIPTS, True) + + for filename in es_files: + es_files[filename] = utils.PathJoin(es_dir, filename) + + try: + st = os.stat(es_files[filename]) + except EnvironmentError, err: + return False, ("File '%s' under path '%s' is missing (%s)" % + (filename, es_dir, utils.ErrnoOrStr(err))) + + if not stat.S_ISREG(stat.S_IFMT(st.st_mode)): + return False, ("File '%s' under path '%s' is not a regular file" % + (filename, es_dir)) + + if filename in constants.ES_SCRIPTS: + if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR: + return False, ("File '%s' under path '%s' is not executable" % + (filename, es_dir)) + + es_obj = \ + objects.ExtStorage(name=name, path=es_dir, + create_script=es_files[constants.ES_SCRIPT_CREATE], + remove_script=es_files[constants.ES_SCRIPT_REMOVE], + grow_script=es_files[constants.ES_SCRIPT_GROW], + attach_script=es_files[constants.ES_SCRIPT_ATTACH], + detach_script=es_files[constants.ES_SCRIPT_DETACH], + setinfo_script=es_files[constants.ES_SCRIPT_SETINFO]) + return True, es_obj + + +def _ExtStorageEnvironment(unique_id, size=None, grow=None, metadata=None): + """Calculate the environment for an External Storage script. + + @type unique_id: tuple (driver, vol_name) + @param unique_id: ExtStorage pool and name of the Volume + @type size: string + @param size: size of the Volume (in mebibytes) + @type grow: string + @param grow: new size of Volume after grow (in mebibytes) + @type metadata: string + @param metadata: metadata info of the Volume + @rtype: dict + @return: dict of environment variables + + """ + vol_name = unique_id[1] + + result = {} + result["VOL_NAME"] = vol_name + + if size is not None: + result["VOL_SIZE"] = size + + if grow is not None: + result["VOL_NEW_SIZE"] = grow + + if metadata is not None: + result["VOL_METADATA"] = metadata + + return result + + +def _VolumeLogName(kind, es_name, volume): + """Compute the ExtStorage log filename for a given Volume and operation. + + @type kind: string + @param kind: the operation type (e.g. create, remove etc.) + @type es_name: string + @param es_name: the ExtStorage name + @type volume: string + @param volume: the name of the Volume inside the External Storage + + """ + # Check if the extstorage log dir is a valid dir + if not os.path.isdir(pathutils.LOG_ES_DIR): + _ThrowError("Cannot find log directory: %s", pathutils.LOG_ES_DIR) + + # TODO: Use tempfile.mkstemp to create unique filename + base = ("%s-%s-%s-%s.log" % + (kind, es_name, volume, utils.TimestampForFilename())) + return utils.PathJoin(pathutils.LOG_ES_DIR, base) + + DEV_MAP = { constants.LD_LV: LogicalVolume, constants.LD_DRBD8: DRBD8, constants.LD_BLOCKDEV: PersistentBlockDevice, constants.LD_RBD: RADOSBlockDevice, + constants.LD_EXT: ExtStorageDevice, } if constants.ENABLE_FILE_STORAGE or constants.ENABLE_SHARED_FILE_STORAGE: