From: Apollon Oikonomopoulos <[email protected]> This patch introduces basic shared block storage support.
It introduces a new storage backend, bdev.PersistentBlockDevice, to use as a backend for shared block storage. A new disk template, DT_BLOCK is introduced as well and added to DTS_EXT_MIRROR and DTS_MAY_ADOPT. This is very basic support and includes no storage manipulation (provisioning, resizing, renaming) which will have to be implemented through a "driver" framework. Signed-off-by: Apollon Oikonomopoulos <[email protected]> --- lib/bdev.py | 137 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ lib/cmdlib.py | 102 +++++++++++++++++++++++++--------------- lib/constants.py | 14 +++-- lib/objects.py | 11 +++- 4 files changed, 217 insertions(+), 47 deletions(-) diff --git a/lib/bdev.py b/lib/bdev.py index 3d68aea..728b51f 100644 --- a/lib/bdev.py +++ b/lib/bdev.py @@ -1937,10 +1937,147 @@ class FileStorage(BlockDev): return FileStorage(unique_id, children, size) +class PersistentBlockDevice(BlockDev): + """A block device with persistent node + + May be either directly attached, or exposed through DM (e.g. dm-multipath). + udev helpers are probably required to give persistent, human-friendly + names. + + For the time being, pathnames are required to lie under /dev. + + """ + def __init__(self, unique_id, children, size): + """Attaches to a static block device. + + The unique_id is a path under /dev. + + """ + super(PersistentBlockDevice, self).__init__(unique_id, children, size) + if not isinstance(unique_id, (tuple, list)): + raise ValueError("Invalid configuration data %s" % str(unique_id)) + self.dev_path = unique_id[1] + if not os.path.realpath(self.dev_path).startswith('/dev/'): + raise ValueError("Full path '%s' lies outside /dev" % + os.path.realpath(self.dev_path)) + self.major = self.minor = None + self.Attach() + + @classmethod + def Create(cls, unique_id, children, size): + """Create a new device + + This is a noop, we only return a PersistentBlockDevice instance + + """ + return PersistentBlockDevice(unique_id, children, size=0) + + def Remove(self): + """Remove a device + + This is a noop + + """ + pass + + def Rename(self, new_id): + """Rename this device. + + """ + raise NotImplementedError + + def Attach(self): + """Attach to an existing block device. + + + """ + self.attached = False + 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[stat.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. + + """ + pass + + 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): + """Grow the logical volume. + + """ + raise NotImplementedError + + def GetSyncStatus(self): + """Returns the sync status of the device. + + If this device is a mirroring device, this function returns the + status of the mirror. + + If the persistent block device name exists, it is assumed to be in + sync. + + The status was already read in Attach, so we just return it. + + @rtype: objects.BlockDevStatus + + """ + + if self.attached: + ldisk_status = constants.LDS_OKAY + else: + ldisk_status = constants.LDS_FAULTY + + return objects.BlockDevStatus(dev_path=self.dev_path, + major=self.major, + minor=self.minor, + sync_percent=None, + estimated_time=None, + is_degraded=False, + ldisk_status=ldisk_status) + + + def DisconnectNet(self): + pass + + def AttachNet(self, multimaster): + pass + DEV_MAP = { constants.LD_LV: LogicalVolume, constants.LD_DRBD8: DRBD8, + constants.LD_BLOCK: PersistentBlockDevice, } if constants.ENABLE_FILE_STORAGE or constants.ENABLE_SHARED_FILE_STORAGE: diff --git a/lib/cmdlib.py b/lib/cmdlib.py index fec6c75..9e49e8d 100644 --- a/lib/cmdlib.py +++ b/lib/cmdlib.py @@ -5963,6 +5963,18 @@ def _GenerateDiskTemplate(lu, template_name, disk_index)), mode=disk["mode"]) disks.append(disk_dev) + elif template_name == constants.DT_BLOCK: + if len(secondary_nodes) != 0: + raise errors.ProgrammerError("Wrong template configuration") + + for idx, disk in enumerate(disk_info): + disk_index = idx + base_index + disk_dev = objects.Disk(dev_type=constants.LD_BLOCK, size=disk["size"], + logical_id=('dummy', disk["adopt"]), + iv_name="disk/%d" % disk_index, + mode=disk["mode"]) + disks.append(disk_dev) + else: raise errors.ProgrammerError("Invalid disk template '%s'" % template_name) return disks @@ -6081,6 +6093,7 @@ def _ComputeDiskSize(disk_template, disks): constants.DT_DRBD8: sum(d["size"] + 128 for d in disks), constants.DT_FILE: None, constants.DT_SHARED_FILE: 0, + constants.DT_BLOCK: 0, } if disk_template not in req_size_dict: @@ -6772,34 +6785,46 @@ class LUCreateInstance(LogicalUnit): _CheckNodesFreeDisk(self, nodenames, req_size) if self.adopt_disks: # instead, we must check the adoption data - all_lvs = set([i["adopt"] for i in self.disks]) - if len(all_lvs) != len(self.disks): - raise errors.OpPrereqError("Duplicate volume names given for adoption", + all_disks = set([i["adopt"] for i in self.disks]) + if len(all_disks) != len(self.disks): + raise errors.OpPrereqError("Duplicate disk names given for adoption", errors.ECODE_INVAL) - for lv_name in all_lvs: - try: - self.cfg.ReserveLV(lv_name, self.proc.GetECId()) - except errors.ReservationError: - raise errors.OpPrereqError("LV named %s used by another instance" % - lv_name, errors.ECODE_NOTUNIQUE) - - node_lvs = self.rpc.call_lv_list([pnode.name], - self.cfg.GetVGName())[pnode.name] - node_lvs.Raise("Cannot get LV information from node %s" % pnode.name) - node_lvs = node_lvs.payload - delta = all_lvs.difference(node_lvs.keys()) - if delta: - raise errors.OpPrereqError("Missing logical volume(s): %s" % - utils.CommaJoin(delta), - errors.ECODE_INVAL) - online_lvs = [lv for lv in all_lvs if node_lvs[lv][2]] - if online_lvs: - raise errors.OpPrereqError("Online logical volumes found, cannot" - " adopt: %s" % utils.CommaJoin(online_lvs), - errors.ECODE_STATE) - # update the size of disk based on what is found - for dsk in self.disks: - dsk["size"] = int(float(node_lvs[dsk["adopt"]][0])) + if self.op.disk_template == constants.DT_PLAIN: + for lv_name in all_disks: + try: + self.cfg.ReserveLV(lv_name, self.proc.GetECId()) + except errors.ReservationError: + raise errors.OpPrereqError("LV named %s used by another instance" % + lv_name, errors.ECODE_NOTUNIQUE) + + node_lvs = self.rpc.call_lv_list([pnode.name], + self.cfg.GetVGName())[pnode.name] + node_lvs.Raise("Cannot get LV information from node %s" % pnode.name) + node_lvs = node_lvs.payload + delta = all_disks.difference(node_lvs.keys()) + if delta: + raise errors.OpPrereqError("Missing logical volume(s): %s" % + utils.CommaJoin(delta), + errors.ECODE_INVAL) + online_lvs = [lv for lv in all_disks if node_lvs[lv][2]] + if online_lvs: + raise errors.OpPrereqError("Online logical volumes found, cannot" + " adopt: %s" % utils.CommaJoin(online_lvs), + errors.ECODE_STATE) + # update the size of disk based on what is found + for dsk in self.disks: + dsk["size"] = int(float(node_lvs[dsk["adopt"]][0])) + elif self.op.disk_template == constants.DT_BLOCK: + node_disks = self.rpc.call_bdev_list([pnode.name])[pnode.name] + node_disks = node_disks.payload + logging.info("node_disks: %s", node_disks) + delta = all_disks.difference(node_disks.keys()) + if delta: + raise errors.OpPrereqError("Missing block device(s): %s" % + utils.CommaJoin(delta), + errors.ECODE_INVAL) + for dsk in self.disks: + dsk["size"] = int(float(node_disks[dsk["adopt"]])) _CheckHVParams(self, nodenames, self.op.hypervisor, self.op.hvparams) @@ -6868,17 +6893,18 @@ class LUCreateInstance(LogicalUnit): ) if self.adopt_disks: - # rename LVs to the newly-generated names; we need to construct - # 'fake' LV disks with the old data, plus the new unique_id - tmp_disks = [objects.Disk.FromDict(v.ToDict()) for v in disks] - rename_to = [] - for t_dsk, a_dsk in zip (tmp_disks, self.disks): - rename_to.append(t_dsk.logical_id) - t_dsk.logical_id = (t_dsk.logical_id[0], a_dsk["adopt"]) - self.cfg.SetDiskID(t_dsk, pnode_name) - result = self.rpc.call_blockdev_rename(pnode_name, - zip(tmp_disks, rename_to)) - result.Raise("Failed to rename adoped LVs") + if self.op.disk_template == constants.DT_PLAIN: + # rename LVs to the newly-generated names; we need to construct + # 'fake' LV disks with the old data, plus the new unique_id + tmp_disks = [objects.Disk.FromDict(v.ToDict()) for v in disks] + rename_to = [] + for t_dsk, a_dsk in zip (tmp_disks, self.disks): + rename_to.append(t_dsk.logical_id) + t_dsk.logical_id = (t_dsk.logical_id[0], a_dsk["adopt"]) + self.cfg.SetDiskID(t_dsk, pnode_name) + result = self.rpc.call_blockdev_rename(pnode_name, + zip(tmp_disks, rename_to)) + result.Raise("Failed to rename adoped LVs") else: feedback_fn("* creating instance disks...") try: diff --git a/lib/constants.py b/lib/constants.py index 9311dcb..f5f548a 100644 --- a/lib/constants.py +++ b/lib/constants.py @@ -318,15 +318,16 @@ DT_PLAIN = "plain" DT_DRBD8 = "drbd" DT_FILE = "file" DT_SHARED_FILE = "sharedfile" +DT_BLOCK = "blockdev" # the set of network-mirrored disk templates DTS_NET_MIRROR = frozenset([DT_DRBD8]) -# the set of externally mirrored disk templates -DTS_EXT_MIRROR = frozenset([DT_SHARED_FILE]) +# the set of externally-mirrored disk templates (e.g. SAN, NAS) +DTS_EXT_MIRROR = frozenset([DT_SHARED_FILE, DT_BLOCK]) # the set of non-lvm-based disk templates -DTS_NOT_LVM = frozenset([DT_DISKLESS, DT_FILE, DT_SHARED_FILE]) +DTS_NOT_LVM = frozenset([DT_DISKLESS, DT_FILE, DT_SHARED_FILE, DT_BLOCK]) # the set of disk templates which can be grown DTS_GROWABLE = frozenset([DT_PLAIN, DT_DRBD8, DT_FILE, DT_SHARED_FILE]) @@ -335,13 +336,14 @@ DTS_GROWABLE = frozenset([DT_PLAIN, DT_DRBD8, DT_FILE, DT_SHARED_FILE]) DTS_MIRRORED = frozenset.union(DTS_NET_MIRROR, DTS_EXT_MIRROR) # the set of disk templates that allow adoption -DTS_MAY_ADOPT = frozenset([DT_PLAIN]) +DTS_MAY_ADOPT = frozenset([DT_PLAIN, DT_BLOCK]) # logical disk types LD_LV = "lvm" LD_DRBD8 = "drbd8" LD_FILE = "file" -LDS_BLOCK = frozenset([LD_LV, LD_DRBD8]) +LD_BLOCK = "blockdev" +LDS_BLOCK = frozenset([LD_LV, LD_DRBD8, LD_BLOCK]) # drbd constants DRBD_HMAC_ALG = "md5" @@ -399,7 +401,7 @@ RIE_CERT_VALIDITY = 24 * 60 * 60 RIE_CONNECT_TIMEOUT = 60 DISK_TEMPLATES = frozenset([DT_DISKLESS, DT_PLAIN, DT_DRBD8, - DT_FILE, DT_SHARED_FILE]) + DT_FILE, DT_SHARED_FILE, DT_BLOCK]) FILE_DRIVER = frozenset([FD_LOOP, FD_BLKTAP]) diff --git a/lib/objects.py b/lib/objects.py index 0e1fbfe..392d49b 100644 --- a/lib/objects.py +++ b/lib/objects.py @@ -395,11 +395,13 @@ class Disk(ConfigObject): def CreateOnSecondary(self): """Test if this device needs to be created on a secondary node.""" - return self.dev_type in (constants.LD_DRBD8, constants.LD_LV) + return self.dev_type in (constants.LD_DRBD8, constants.LD_LV, + constants.LD_BLOCK) def AssembleOnSecondary(self): """Test if this device needs to be assembled on a secondary node.""" - return self.dev_type in (constants.LD_DRBD8, constants.LD_LV) + return self.dev_type in (constants.LD_DRBD8, constants.LD_LV, + constants.LD_BLOCK) def OpenOnSecondary(self): """Test if this device needs to be opened on a secondary node.""" @@ -418,6 +420,8 @@ class Disk(ConfigObject): """ if self.dev_type == constants.LD_LV: return "/dev/%s/%s" % (self.logical_id[0], self.logical_id[1]) + elif self.dev_type == constants.LD_BLOCK: + return self.logical_id[1] return None def ChildrenNeeded(self): @@ -445,7 +449,8 @@ class Disk(ConfigObject): devices needs to (or can) be assembled. """ - if self.dev_type in [constants.LD_LV, constants.LD_FILE] + if self.dev_type in [constants.LD_LV, constants.LD_FILE, + constants.LD_BLOCK]: result = [node] elif self.dev_type in constants.LDS_DRBD: result = [self.logical_id[0], self.logical_id[1]] -- 1.7.1
