def Grow(self, amount, dryrun, backingstore, excl_stor): """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(): base.ThrowError("Can't attach to rbd device during Grow()") if dryrun: # the rbd tool does not support dry runs of resize operations. # Since rbd volumes are thinly provisioned, we assume # there is always enough free space for the operation. return rbd_pool = self.params[constants.LDP_POOL] rbd_name = self.unique_id[1] new_size = self.size + amount # Resize the rbd volume (Image) inside the RADOS cluster. cmd = self.__class__.MakeRbdCmd(self.params, ["resize", "-p", rbd_pool, rbd_name, "--size", "%s" % new_size]) result = utils.RunCmd(cmd) if result.failed: base.ThrowError("rbd resize failed (%s): %s", result.fail_reason, result.output)
def _AssembleLocal(self, minor, backend, meta, size): """Configure the local part of a DRBD device. @type minor: int @param minor: the minor to assemble locally @type backend: string @param backend: path to the data device to use @type meta: string @param meta: path to the meta device to use @type size: int @param size: size in MiB """ cmds = self._cmd_gen.GenLocalInitCmds(minor, backend, meta, size, self.params) for cmd in cmds: result = utils.RunCmd(cmd) if result.failed: base.ThrowError("drbd%d: can't attach local disk: %s", minor, result.output) # syncer init only for drbd => 8.4 - in 8.4 it must be set after local, not in net info = DRBD8.GetProcInfo() version = info.GetVersion() if version["k_minor"] >= 4: sync_errors = self._SetMinorSyncParams(minor, self.params) # try second time because disk config for drbd resource may be still uninitialized if sync_errors: time.sleep(1) sync_errors = self._SetMinorSyncParams(minor, self.params) if sync_errors: base.ThrowError("drbd%d: can't set the synchronization parameters: %s" % (minor, utils.CommaJoin(sync_errors)))
def AttachNet(self, multimaster): """Reconnects the network. This method connects the network side of the device with a specified multi-master flag. The device needs to be 'Standalone' but have valid network configuration data. @type multimaster: boolean @param multimaster: init the network in dual-primary mode """ if self.minor is None: base.ThrowError("drbd%d: device not attached in AttachNet", self._aminor) if None in (self._lhost, self._lport, self._rhost, self._rport): base.ThrowError("drbd%d: missing network info in AttachNet()", self.minor) status = self.GetProcStatus() if not status.is_standalone: base.ThrowError("drbd%d: device is not standalone in AttachNet", self.minor) self._AssembleNet(self.minor, (self._lhost, self._lport, self._rhost, self._rport), dual_pri=multimaster, hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
def _ParseLvInfoLine(cls, line, sep): """Parse one line of the lvs output used in L{_GetLvInfo}. """ elems = line.strip().split(sep) # The previous iteration of code here assumed that LVM might put another # separator to the right of the output. The PV info might be empty for # thin volumes, so stripping off the separators might cut off the last # empty element - do this instead. if len(elems) == 7 and elems[-1] == "": elems.pop() if len(elems) != 6: base.ThrowError("Can't parse LVS output, len(%s) != 6", str(elems)) (status, major, minor, pe_size, stripes, pvs) = elems if len(status) < 6: base.ThrowError("lvs lv_attr is not at least 6 characters (%s)", status) try: major = int(major) minor = int(minor) except (TypeError, ValueError), err: base.ThrowError("lvs major/minor cannot be parsed: %s", str(err))
def Snapshot(self, snap_name=None, snap_size=None): """Create a snapshot copy of an lvm block device. @returns: tuple (vg, lv) """ if not snap_name: snap_name = self._lv_name + ".snap" if not snap_size: # FIXME: choose a saner value for the snapshot size # let's stay on the safe side and ask for the full size, for now snap_size = self.size # remove existing snapshot if found snap = LogicalVolume((self._vg_name, snap_name), None, snap_size, self.params, self.dyn_params) base.IgnoreError(snap.Remove) vg_info = self.GetVGInfo([self._vg_name], False) if not vg_info: base.ThrowError("Can't compute VG info for vg %s", self._vg_name) free_size, _, _ = vg_info[0] if free_size < snap_size: base.ThrowError("Not enough free space: required %s," " available %s", snap_size, free_size) _CheckResult(utils.RunCmd(["lvcreate", "-L%dm" % snap_size, "-s", "-n%s" % snap_name, self.dev_path])) return (self._vg_name, snap_name)
def _AssembleLocal(self, minor, backend, meta, size): """Configure the local part of a DRBD device. @type minor: int @param minor: the minor to assemble locally @type backend: string @param backend: path to the data device to use @type meta: string @param meta: path to the meta device to use @type size: int @param size: size in MiB """ cmds = self._cmd_gen.GenLocalInitCmds(minor, backend, meta, size, self.params) for cmd in cmds: result = utils.RunCmd(cmd) if result.failed: base.ThrowError("drbd%d: can't attach local disk: %s", minor, result.output) def _WaitForMinorSyncParams(): """Call _SetMinorSyncParams and raise RetryAgain on errors. """ if self._SetMinorSyncParams(minor, self.params): raise utils.RetryAgain() if self._NeedsLocalSyncerParams(): # Retry because disk config for DRBD resource may be still uninitialized. try: utils.Retry(_WaitForMinorSyncParams, 1.0, 5.0) except utils.RetryTimeout as e: base.ThrowError("drbd%d: can't set the synchronization parameters: %s" % (minor, utils.CommaJoin(e.args[0])))
def _MapVolumeToBlockdev(self, unique_id): """Maps existing rbd volumes to block devices. This method should be idempotent if the mapping already exists. @rtype: string @return: the block device path that corresponds to the volume """ pool = self.params[constants.LDP_POOL] name = unique_id[1] # Check if the mapping already exists. rbd_dev = self._VolumeToBlockdev(pool, name) if rbd_dev: # The mapping exists. Return it. return rbd_dev # The mapping doesn't exist. Create it. map_cmd = self.__class__.MakeRbdCmd(self.params, ["map", "-p", pool, name]) result = utils.RunCmd(map_cmd) if result.failed: base.ThrowError("rbd map failed (%s): %s", result.fail_reason, result.output) # Find the corresponding rbd device. rbd_dev = self._VolumeToBlockdev(pool, name) if not rbd_dev: base.ThrowError("rbd map succeeded, but could not find the rbd block" " device in output of showmapped, for volume: %s", name) # The device was successfully mapped. Return it. return rbd_dev
def RemoveChildren(self, devices): """Detach the drbd device from local storage. @type devices: list of L{BlockDev} @param devices: a list of exactly two L{BlockDev} objects; the first denotes the data device, the second the meta device for this DRBD device """ if self.minor is None: base.ThrowError( "drbd%d: can't attach to drbd8 during RemoveChildren", self._aminor) # early return if we don't actually have backing storage info = self._GetShowInfo(self.minor) if "local_dev" not in info: return if len(self._children) != 2: base.ThrowError("drbd%d: we don't have two children: %s", self.minor, self._children) if self._children.count( None) == 2: # we don't actually have children :) logging.warning("drbd%d: requested detach while detached", self.minor) return if len(devices) != 2: base.ThrowError("drbd%d: we need two children in RemoveChildren", self.minor) for child, dev in zip(self._children, devices): if dev != child.dev_path: base.ThrowError( "drbd%d: mismatch in local storage (%s != %s) in" " RemoveChildren", self.minor, dev, child.dev_path) self._ShutdownLocal(self.minor) self._children = []
def Grow(self, amount, dryrun, backingstore, _excl_stor): """Grow the file @param amount: the amount (in mebibytes) to grow by. """ # Check that the file exists self.Exists(assert_exists=True) if amount < 0: base.ThrowError("%s: can't grow by negative amount", self.path) if dryrun: return if not backingstore: return current_size = self.Size() new_size = current_size + amount * 1024 * 1024 try: f = open(self.path, "a+") f.truncate(new_size) f.close() except EnvironmentError, err: base.ThrowError("%s: can't grow: ", self.path, str(err))
def _InitMeta(cls, minor, dev_path): """Initialize a meta device. This will not work if the given minor is in use. @type minor: int @param minor: the DRBD minor whose (future) meta device should be initialized @type dev_path: string @param dev_path: path to the meta device to initialize """ # Zero the metadata first, in order to make sure drbdmeta doesn't # try to auto-detect existing filesystems or similar (see # http://code.google.com/p/ganeti/issues/detail?id=182); we only # care about the first 128MB of data in the device, even though it # can be bigger result = utils.RunCmd([ constants.DD_CMD, "if=/dev/zero", "of=%s" % dev_path, "bs=%s" % constants.DD_BLOCK_SIZE, "count=128", "oflag=direct" ]) if result.failed: base.ThrowError("Can't wipe the meta device: %s", result.output) info = DRBD8.GetProcInfo() cmd_gen = DRBD8.GetCmdGenerator(info) cmd = cmd_gen.GenInitMetaCmd(minor, dev_path) result = utils.RunCmd(cmd) if result.failed: base.ThrowError("Can't initialize meta device: %s", result.output)
def _CheckMetaSize(meta_device): """Check if the given meta device looks like a valid one. This currently only checks the size, which must be around 128MiB. @type meta_device: string @param meta_device: the path to the device to check """ result = utils.RunCmd(["blockdev", "--getsize", meta_device]) if result.failed: base.ThrowError("Failed to get device size: %s - %s", result.fail_reason, result.output) try: sectors = int(result.stdout) except (TypeError, ValueError): base.ThrowError("Invalid output from blockdev: '%s'", result.stdout) num_bytes = sectors * 512 if num_bytes < 128 * 1024 * 1024: # less than 128MiB base.ThrowError("Meta device too small (%.2fMib)", (num_bytes / 1024 / 1024)) # the maximum *valid* size of the meta device when living on top # of LVM is hard to compute: it depends on the number of stripes # and the PE size; e.g. a 2-stripe, 64MB PE will result in a 128MB # (normal size), but an eight-stripe 128MB PE will result in a 1GB # size meta device; as such, we restrict it to 1GB (a little bit # too generous, but making assumptions about PE size is hard) if num_bytes > 1024 * 1024 * 1024: base.ThrowError("Meta device too big (%.2fMiB)", (num_bytes / 1024 / 1024))
def AddChildren(self, devices): """Add a disk to the DRBD device. @type devices: list of L{BlockDev} @param devices: a list of exactly two L{BlockDev} objects; the first denotes the data device, the second the meta device for this DRBD device """ if self.minor is None: base.ThrowError("drbd%d: can't attach to dbrd8 during AddChildren", self._aminor) if len(devices) != 2: base.ThrowError("drbd%d: need two devices for AddChildren", self.minor) info = self._GetShowInfo(self.minor) if "local_dev" in info: base.ThrowError("drbd%d: already attached to a local disk", self.minor) backend, meta = devices if backend.dev_path is None or meta.dev_path is None: base.ThrowError("drbd%d: children not ready during AddChildren", self.minor) backend.Open() meta.Open() self._CheckMetaSize(meta.dev_path) self._InitMeta(DRBD8.FindUnusedMinor(), meta.dev_path) self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size) self._children = devices
def _GetLvInfo(cls, dev_path, _run_cmd=utils.RunCmd): """Get info about the given existing LV to be used. """ sep = "|" result = _run_cmd(["lvs", "--noheadings", "--separator=%s" % sep, "--units=k", "--nosuffix", "-olv_attr,lv_kernel_major,lv_kernel_minor," "vg_extent_size,stripes,devices", dev_path]) if result.failed: base.ThrowError("Can't find LV %s: %s, %s", dev_path, result.fail_reason, result.output) # the output can (and will) have multiple lines for multi-segment # LVs, as the 'stripes' parameter is a segment one, so we take # only the last entry, which is the one we're interested in; note # that with LVM2 anyway the 'stripes' value must be constant # across segments, so this is a no-op actually out = result.stdout.splitlines() if not out: # totally empty result? splitlines() returns at least # one line for any non-empty string base.ThrowError("Can't parse LVS output, no lines? Got '%s'", str(out)) pv_names = set() for line in out: (status, major, minor, pe_size, stripes, more_pvs) = \ cls._ParseLvInfoLine(line, sep) pv_names.update(more_pvs) return (status, major, minor, pe_size, stripes, pv_names)
def CreateFromFile(filename=constants.DRBD_STATUS_FILE): try: lines = utils.ReadFile(filename).splitlines() except EnvironmentError, err: if err.errno == errno.ENOENT: base.ThrowError("The file %s cannot be opened, check if the module" " is loaded (%s)", filename, str(err)) else: base.ThrowError("Can't read the DRBD proc file %s: %s", filename, str(err))
def _ParseRbdShowmappedPlain(output, volume_name): """Parse the (plain / text) output of `rbd showmapped'. This method parses the output of `rbd showmapped' and returns the rbd block device path (e.g. /dev/rbd0) that matches the given rbd volume. @type output: string @param output: the plain text output of `rbd showmapped' @type volume_name: string @param volume_name: the name of the volume whose device we search for @rtype: string or None @return: block device path if the volume is mapped, else None """ allfields = 5 volumefield = 2 devicefield = 4 lines = output.splitlines() # Try parsing the new output format (ceph >= 0.55). splitted_lines = map(lambda l: l.split(), lines) # Check for empty output. if not splitted_lines: return None # Check showmapped output, to determine number of fields. field_cnt = len(splitted_lines[0]) if field_cnt != allfields: # Parsing the new format failed. Fallback to parsing the old output # format (< 0.55). splitted_lines = map(lambda l: l.split("\t"), lines) if field_cnt != allfields: base.ThrowError( "Cannot parse rbd showmapped output expected %s fields," " found %s", allfields, field_cnt) matched_lines = \ filter(lambda l: len(l) == allfields and l[volumefield] == volume_name, splitted_lines) if len(matched_lines) > 1: base.ThrowError("rbd volume %s mapped more than once", volume_name) if matched_lines: # rbd block device found. Return it. rbd_dev = matched_lines[0][devicefield] return rbd_dev # The given volume is not mapped. return None
def _GetNetFamily(minor, lhost, rhost): if netutils.IP6Address.IsValid(lhost): if not netutils.IP6Address.IsValid(rhost): base.ThrowError("drbd%d: can't connect ip %s to ip %s" % (minor, lhost, rhost)) return "ipv6" elif netutils.IP4Address.IsValid(lhost): if not netutils.IP4Address.IsValid(rhost): base.ThrowError("drbd%d: can't connect ip %s to ip %s" % (minor, lhost, rhost)) return "ipv4" else: base.ThrowError("drbd%d: Invalid ip %s" % (minor, lhost))
def Close(self): """Make the local state secondary. This will, of course, fail if the device is in use. """ if self.minor is None and not self.Attach(): base.ThrowError("drbd%d: can't Attach() in Close()", self._aminor) cmd = self._cmd_gen.GenSecondaryCmd(self.minor) result = utils.RunCmd(cmd) if result.failed: base.ThrowError("drbd%d: can't switch drbd device to secondary: %s", self.minor, result.output)
def GetProcStatus(self): """Return the current status data from /proc/drbd for this device. @rtype: DRBD8Status """ if self.minor is None: base.ThrowError("drbd%d: GetStats() called while not attached", self._aminor) info = DRBD8.GetProcInfo() if not info.HasMinorStatus(self.minor): base.ThrowError("drbd%d: can't find myself in /proc", self.minor) return info.GetMinorStatus(self.minor)
class DRBD8(object): """Various methods to deals with the DRBD system as a whole. This class provides a set of methods to deal with the DRBD installation on the node or with uninitialized devices as opposed to a DRBD device. """ _USERMODE_HELPER_FILE = "/sys/module/drbd/parameters/usermode_helper" _MAX_MINORS = 255 @staticmethod def GetUsermodeHelper(filename=_USERMODE_HELPER_FILE): """Returns DRBD usermode_helper currently set. @type filename: string @param filename: the filename to read the usermode helper from @rtype: string @return: the currently configured DRBD usermode helper """ try: helper = utils.ReadFile(filename).splitlines()[0] except EnvironmentError, err: if err.errno == errno.ENOENT: base.ThrowError( "The file %s cannot be opened, check if the module" " is loaded (%s)", filename, str(err)) else: base.ThrowError("Can't read DRBD helper file %s: %s", filename, str(err)) if not helper: base.ThrowError("Can't read any data from %s", filename) return helper
def __init__(self, unique_id, children, size, params, dyn_params, *args): """Initalizes a file device backend. """ if children: base.ThrowError("Invalid setup for file device") try: driver, path = unique_id except ValueError: # wrong number of arguments raise ValueError("Invalid configuration data %s" % repr(unique_id)) server_addr = params[constants.GLUSTER_HOST] port = params[constants.GLUSTER_PORT] volume = params[constants.GLUSTER_VOLUME] self.volume = GlusterVolume(server_addr, port, volume) self.path = path self.driver = driver self.full_path = io.PathJoin(self.volume.mount_point, self.path) self.file = None super(GlusterStorage, self).__init__(unique_id, children, size, params, dyn_params, *args) self.Attach()
def CreateFile(cls, path, size, create_folders=False, _file_path_acceptance_fn=None): """Create a new file and its file device helper. @param size: the size in MiBs the file should be truncated to. @param create_folders: create the directories for the path if necessary (using L{ganeti.utils.io.Makedirs}) @rtype: FileDeviceHelper @return: The FileDeviceHelper object representing the object. @raise errors.FileStoragePathError: if the file path is disallowed by policy """ if not _file_path_acceptance_fn: _file_path_acceptance_fn = CheckFileStoragePathAcceptance _file_path_acceptance_fn(path) if create_folders: folder = os.path.dirname(path) io.Makedirs(folder) try: fd = os.open(path, os.O_RDWR | os.O_CREAT | os.O_EXCL) f = os.fdopen(fd, "w") f.truncate(size * 1024 * 1024) f.close() except EnvironmentError as err: base.ThrowError("%s: can't create: %s", path, str(err)) return FileDeviceHelper(path, _file_path_acceptance_fn=_file_path_acceptance_fn)
def Create(cls, unique_id, children, size, spindles, params, excl_stor, dyn_params, **kwargs): """Create a new rbd device. Provision a new rbd volume inside a RADOS pool. """ if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2: raise errors.ProgrammerError("Invalid configuration data %s" % str(unique_id)) if excl_stor: raise errors.ProgrammerError("RBD device requested with" " exclusive_storage") rbd_pool = params[constants.LDP_POOL] rbd_name = unique_id[1] # Provision a new rbd volume (Image) inside the RADOS cluster. cmd = cls.MakeRbdCmd(params, ["create", "-p", rbd_pool, rbd_name, "--size", str(size)]) result = utils.RunCmd(cmd) if result.failed: base.ThrowError("rbd creation failed (%s): %s", result.fail_reason, result.output) return RADOSBlockDevice(unique_id, children, size, params, dyn_params, **kwargs)
def Grow(self, amount, dryrun, backingstore, excl_stor): """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(): base.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, self.ext_params, size=self.size, grow=new_size, name=self.name, uuid=self.uuid)
def _CheckExtStorageFile(base_dir, filename, required): """Check prereqs for an ExtStorage file. Check if file exists, if it is a regular file and in case it is one of extstorage scripts if it is executable. @type base_dir: string @param base_dir: Base directory containing ExtStorage installations. @type filename: string @param filename: The basename of the ExtStorage file. @type required: bool @param required: Whether the file is required or not. @rtype: String @return: The file path if the file is found and is valid, None if the file is not found and not required. @raises BlockDeviceError: In case prereqs are not met (found and not valid/executable, not found and required) """ file_path = utils.PathJoin(base_dir, filename) try: st = os.stat(file_path) except EnvironmentError, err: if not required: logging.info("Optional file '%s' under path '%s' is missing", filename, base_dir) return None base.ThrowError("File '%s' under path '%s' is missing (%s)" % (filename, base_dir, utils.ErrnoOrStr(err)))
def _VolumeToBlockdev(cls, pool, volume_name): """Do the 'volume name'-to-'rbd block device' resolving. @type pool: string @param pool: RADOS pool to use @type volume_name: string @param volume_name: the name of the volume whose device we search for @rtype: string or None @return: block device path if the volume is mapped, else None """ try: # Newer versions of the rbd tool support json output formatting. Use it # if available. showmap_cmd = cls.MakeRbdCmd({}, ["showmapped", "--format", "json"]) result = utils.RunCmd(showmap_cmd) if result.failed: logging.error("rbd JSON output formatting returned error (%s): %s," "falling back to plain output parsing", result.fail_reason, result.output) raise RbdShowmappedJsonError return cls._ParseRbdShowmappedJson(result.output, pool, volume_name) except RbdShowmappedJsonError: # For older versions of rbd, we have to parse the plain / text output # manually. showmap_cmd = cls.MakeRbdCmd({}, ["showmapped", "-p", pool]) result = utils.RunCmd(showmap_cmd) if result.failed: base.ThrowError("rbd showmapped failed (%s): %s", result.fail_reason, result.output) return cls._ParseRbdShowmappedPlain(result.output, volume_name)
def GetDevInfo(cls, show_data): """Parse details about a given DRBD minor. This returns, if available, the local backing device (as a path) and the local and remote (ip, port) information from a string containing the output of the `drbdsetup show` command as returned by DRBD8Dev._GetShowData. This will return a dict with keys: - local_dev - meta_dev - meta_index - local_addr - remote_addr """ if not show_data: return {} try: # run pyparse results = (cls._GetShowParser()).parseString(show_data) except pyp.ParseException as err: base.ThrowError("Can't parse drbdsetup show output: %s", str(err)) return cls._TransformParseResult(results)
def Import(self): """Builds the shell command for importing data to device. @see: L{BlockDev.Import} for details """ base.ThrowError("Importing data is not supported for the" " PersistentBlockDevice template")
def _CheckResult(result): """Throws an error if the given result is a failed one. @param result: result from RunCmd """ if result.failed: base.ThrowError("Command: %s error: %s - %s", result.cmd, result.fail_reason, result.output)
def _ParseLvInfoLine(cls, line, sep): """Parse one line of the lvs output used in L{_GetLvInfo}. """ elems = line.strip().rstrip(sep).split(sep) if len(elems) != 6: base.ThrowError("Can't parse LVS output, len(%s) != 6", str(elems)) (status, major, minor, pe_size, stripes, pvs) = elems if len(status) < 6: base.ThrowError("lvs lv_attr is not at least 6 characters (%s)", status) try: major = int(major) minor = int(minor) except (TypeError, ValueError), err: base.ThrowError("lvs major/minor cannot be parsed: %s", str(err))
def GetUsermodeHelper(filename=_USERMODE_HELPER_FILE): """Returns DRBD usermode_helper currently set. @type filename: string @param filename: the filename to read the usermode helper from @rtype: string @return: the currently configured DRBD usermode helper """ try: helper = utils.ReadFile(filename).splitlines()[0] except EnvironmentError, err: if err.errno == errno.ENOENT: base.ThrowError("The file %s cannot be opened, check if the module" " is loaded (%s)", filename, str(err)) else: base.ThrowError("Can't read DRBD helper file %s: %s", filename, str(err))