X-Git-Url: https://code.grnet.gr/git/ganeti-local/blobdiff_plain/c7c6606da65e53a0b0c4b1901ca4774dc016c949..b3c728dc6f916757627f3da6422a43318dfecf8b:/lib/bdev.py diff --git a/lib/bdev.py b/lib/bdev.py index 452e510..6c20b4b 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 frozenset(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 @@ -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()") @@ -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] @@ -1058,7 +1195,7 @@ class BaseDRBD(BlockDev): # pylint: disable=W0223 def _CheckMetaSize(meta_device): """Check if the given meta device looks like a valid one. - This currently only check the size, which must be around + This currently only checks the size, which must be around 128MiB. """ @@ -1098,10 +1235,10 @@ class DRBD8(BaseDRBD): doesn't do anything to the supposed peer. If you need a fully connected DRBD pair, you need to use this class on both hosts. - The unique_id for the drbd device is the (local_ip, local_port, - remote_ip, remote_port) tuple, and it must have two children: the - data device and the meta_device. The meta device is checked for - valid size and is zeroed on create. + The unique_id for the drbd device is a (local_ip, local_port, + remote_ip, remote_port, local_minor, secret) tuple, and it must have + two children: the data device and the meta_device. The meta device + is checked for valid size and is zeroed on create. """ _MAX_MINORS = 255 @@ -1197,7 +1334,7 @@ class DRBD8(BaseDRBD): def _GetShowParser(cls): """Return a parser for `drbd show` output. - This will either create or return an already-create parser for the + This will either create or return an already-created parser for the output of the command `drbd show`. """ @@ -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]]) @@ -1633,7 +1770,7 @@ class DRBD8(BaseDRBD): msg = ("Can't change syncer rate: %s - %s" % (result.fail_reason, result.output)) logging.error(msg) - return msg + return [msg] return [] @@ -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()") @@ -2653,6 +2802,16 @@ def _VerifyDiskType(dev_type): raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type) +def _VerifyDiskParams(disk): + """Verifies if all disk parameters are set. + + """ + missing = set(constants.DISK_LD_DEFAULTS[disk.dev_type]) - set(disk.params) + if missing: + raise errors.ProgrammerError("Block device is missing disk parameters: %s" % + missing) + + def FindDevice(disk, children): """Search for an existing, assembled device. @@ -2688,6 +2847,7 @@ def Assemble(disk, children): """ _VerifyDiskType(disk.dev_type) + _VerifyDiskParams(disk) device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size, disk.params) device.Assemble() @@ -2705,6 +2865,7 @@ def Create(disk, children): """ _VerifyDiskType(disk.dev_type) + _VerifyDiskParams(disk) device = DEV_MAP[disk.dev_type].Create(disk.physical_id, children, disk.size, disk.params) return device