833c5b8ceb
This change extends the block device lvs attributes to allow creating a volume which represents a thin pool, and to create volumes which are allocated from this pool. Change-Id: Ic58f55c36236cc8c6279fbcb708e27dc2982f2d5
430 lines
14 KiB
Python
430 lines
14 KiB
Python
# Copyright 2017 Red Hat, Inc.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
import logging
|
|
|
|
from diskimage_builder.block_device.exception \
|
|
import BlockDeviceSetupException
|
|
from diskimage_builder.block_device.plugin import NodeBase
|
|
from diskimage_builder.block_device.plugin import PluginBase
|
|
from diskimage_builder.block_device.utils import exec_sudo
|
|
from diskimage_builder.block_device.utils import parse_abs_size_spec
|
|
from diskimage_builder.block_device.utils import remove_device
|
|
|
|
PHYSICAL_EXTENT_BYTES = parse_abs_size_spec('4MiB')
|
|
LVS_TYPES = ['thin', 'thin-pool']
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
#
|
|
# LVM
|
|
# ---
|
|
#
|
|
# The LVM config has three required keys; pvs, vgs and lvs
|
|
#
|
|
# lvm: -> LVSNode
|
|
# pvs: -> PvsNode
|
|
# lvs: -> LvsNode
|
|
# vgs: -> VgsNode
|
|
#
|
|
# The LVMPlugin will verify this and build nodes into the
|
|
# configuration graph.
|
|
#
|
|
# As described below, a LVSNode is created for synchronisation
|
|
# purposes. Thus if you had something like two partitions that became
|
|
# two physical-volumes (pv1 & pv2), that you then combine into a
|
|
# single volume group (vg) and then create several logical volumes
|
|
# (lv1, lv2, lv3) your graph would end up looking like:
|
|
#
|
|
# partition1 partition2
|
|
# | |
|
|
# ---> LVSNode <--+
|
|
# |
|
|
# +------+------+
|
|
# v v
|
|
# pv1 pv2
|
|
# | |
|
|
# +--> vg <---+
|
|
# |
|
|
# +-----+-----+
|
|
# v v v
|
|
# lv1 lv2 lv3
|
|
#
|
|
# After the create() call on the LVSNode object, the entire LVM setup
|
|
# would actually be complete. The other nodes are all just
|
|
# place-holders, and are used for further ordering (for example, the
|
|
# fs creation & mounting should depend on the logical volume nodes).
|
|
# For this reason, their create() calls are blank. However, for code
|
|
# organisational purposes they have a private _create() and _cleanup()
|
|
# call that is driven by the LVSNode object.
|
|
|
|
|
|
class PvsNode(NodeBase):
|
|
def __init__(self, name, state, base, options):
|
|
"""Physical volume
|
|
|
|
This is a placeholder node for the LVM physical volumes.
|
|
|
|
Arguments:
|
|
:param name: Name of this node
|
|
:param state: global state pointer
|
|
:param base: Parent partition
|
|
:param options: config options
|
|
"""
|
|
super(PvsNode, self).__init__(name, state)
|
|
self.base = base
|
|
self.options = options
|
|
|
|
def _create(self):
|
|
# the underlying device path of our parent was previously
|
|
# recorded into the state during blockdev creation; look it
|
|
# up.
|
|
phys_dev = self.state['blockdev'][self.base]['device']
|
|
|
|
cmd = ["pvcreate"]
|
|
cmd.append(phys_dev)
|
|
if self.options:
|
|
cmd.extend(self.options)
|
|
logger.debug("Creating pv command [%s]", cmd)
|
|
exec_sudo(cmd)
|
|
|
|
# save state
|
|
if 'pvs' not in self.state:
|
|
self.state['pvs'] = {}
|
|
self.state['pvs'][self.name] = {
|
|
'opts': self.options,
|
|
'device': phys_dev
|
|
}
|
|
|
|
def get_edges(self):
|
|
# See LVMNode.get_edges() for how this gets connected
|
|
return ([], [])
|
|
|
|
def create(self):
|
|
# see notes in LVMNode object
|
|
pass
|
|
|
|
|
|
class VgsNode(NodeBase):
|
|
def __init__(self, name, state, base, options):
|
|
"""Volume Group
|
|
|
|
This is a placeholder node for a volume group
|
|
|
|
Arguments:
|
|
:param name: Name of this node
|
|
:param state: global state pointer
|
|
:param base: Parent :class:`PvsNodes` this volume group exists on
|
|
:param options: extra options passed to the `vgcreate` command
|
|
"""
|
|
super(VgsNode, self).__init__(name, state)
|
|
self.base = base
|
|
self.options = options
|
|
|
|
def _create(self):
|
|
# The PV's have saved their actual device name into the state
|
|
# during their _create(). Look at our base elements and thus
|
|
# find the underlying device paths in the state.
|
|
pvs_devs = []
|
|
for pv in self.base:
|
|
pvs_dev = self.state['pvs'][pv]['device']
|
|
pvs_devs.append(pvs_dev)
|
|
|
|
cmd = ["vgcreate", ]
|
|
cmd.append(self.name)
|
|
cmd.extend(pvs_devs)
|
|
if self.options:
|
|
cmd.extend(self.options)
|
|
|
|
logger.debug("Creating vg command [%s]", cmd)
|
|
exec_sudo(cmd)
|
|
|
|
# save state
|
|
if 'vgs' not in self.state:
|
|
self.state['vgs'] = {}
|
|
self.state['vgs'][self.name] = {
|
|
'opts': self.options,
|
|
'devices': self.base,
|
|
}
|
|
|
|
def _umount(self):
|
|
exec_sudo(['vgchange', '-an', self.name])
|
|
|
|
def get_edges(self):
|
|
# self.base is already a list, per the config. There might be
|
|
# multiple pv parents here.
|
|
edge_from = self.base
|
|
edge_to = []
|
|
return (edge_from, edge_to)
|
|
|
|
def create(self):
|
|
# see notes in LVMNode object
|
|
pass
|
|
|
|
|
|
class LvsNode(NodeBase):
|
|
def __init__(self, name, state, base, options, size, extents, segtype,
|
|
thin_pool):
|
|
"""Logical Volume
|
|
|
|
This is a placeholder node for a logical volume
|
|
|
|
Arguments:
|
|
:param name: Name of this node
|
|
:param state: global state pointer
|
|
:param base: the parent volume group
|
|
:param options: options passed to lvcreate
|
|
:param size: size of the LV, using the supported unit types
|
|
(MB, MiB, etc)
|
|
:param extents: size of the LV in extents
|
|
:param segtype: value passed to segment type, supports thin and
|
|
thin-pool
|
|
:param thin_pool: name of the thin pool to create this volume from
|
|
"""
|
|
super(LvsNode, self).__init__(name, state)
|
|
self.base = base
|
|
self.options = options
|
|
self.size = size
|
|
self.extents = extents
|
|
self.type = segtype
|
|
self.thin_pool = thin_pool
|
|
|
|
def _create(self):
|
|
cmd = ["lvcreate", ]
|
|
cmd.extend(['--name', self.name])
|
|
if self.type:
|
|
cmd.extend(['--type', self.type])
|
|
if self.thin_pool:
|
|
cmd.extend(['--thin-pool', self.thin_pool])
|
|
if self.size:
|
|
size = parse_abs_size_spec(self.size)
|
|
# ensuire size aligns with physical extents
|
|
size = size - size % PHYSICAL_EXTENT_BYTES
|
|
size_arg = '-V' if self.type == 'thin' else '-L'
|
|
cmd.extend([size_arg, '%dB' % size])
|
|
elif self.extents:
|
|
cmd.extend(['-l', self.extents])
|
|
if self.options:
|
|
cmd.extend(self.options)
|
|
|
|
cmd.append(self.base)
|
|
|
|
logger.debug("Creating lv command [%s]", cmd)
|
|
exec_sudo(cmd)
|
|
|
|
# save state
|
|
device_name = '%s-%s' % (self.base, self.name)
|
|
self.state['blockdev'][self.name] = {
|
|
'vgs': self.base,
|
|
'size': self.size,
|
|
'extents': self.extents,
|
|
'opts': self.options,
|
|
'device': '/dev/mapper/%s' % device_name
|
|
}
|
|
self.add_rollback(remove_device, device_name)
|
|
|
|
def _umount(self):
|
|
exec_sudo(['lvchange', '-an',
|
|
'/dev/%s/%s' % (self.base, self.name)])
|
|
|
|
def get_edges(self):
|
|
edge_from = [self.base]
|
|
edge_to = []
|
|
return (edge_from, edge_to)
|
|
|
|
def create(self):
|
|
# see notes in LVMNode object
|
|
pass
|
|
|
|
|
|
class LVMNode(NodeBase):
|
|
def __init__(self, name, state, pvs, lvs, vgs):
|
|
"""LVM Driver Node
|
|
|
|
This is the "global" node where all LVM operations are driven
|
|
from. In the node graph, the LVM physical-volumes depend on
|
|
this node. This node then depends on the devices that the
|
|
PV's require. This node incorporates *all* LVM setup;
|
|
i.e. after the create() call here we have created all pv's,
|
|
lv's and vg. The <Pvs|Lvs|Vgs>Node objects in the graph are
|
|
therefore just dependency place holders whose create() call
|
|
does nothing.
|
|
|
|
Arguments:
|
|
:param name: name of this node
|
|
:param state: global state pointer
|
|
:param pvs: A list of :class:`PvsNode` objects
|
|
:param lvs: A list of :class:`LvsNode` objects
|
|
:param vgs: A list of :class:`VgsNode` objects
|
|
|
|
"""
|
|
super(LVMNode, self).__init__(name, state)
|
|
self.pvs = pvs
|
|
self.lvs = lvs
|
|
self.vgs = vgs
|
|
|
|
def get_edges(self):
|
|
# This node requires the physical device(s), which is
|
|
# recorded in the "base" argument of the PV nodes.
|
|
pvs = []
|
|
for pv in self.pvs:
|
|
pvs.append(pv.base)
|
|
edge_from = set(pvs)
|
|
|
|
# The PV nodes should then depend on us. i.e., we just made
|
|
# this node a synchronisation point
|
|
edge_to = [pv.name for pv in self.pvs]
|
|
|
|
return (edge_from, edge_to)
|
|
|
|
def create(self):
|
|
# Run through pvs->vgs->lvs and create them
|
|
# XXX: we could theoretically get this same info from walking
|
|
# the graph of our children nodes? Would that be helpful in
|
|
# any way?
|
|
for pvs in self.pvs:
|
|
pvs._create()
|
|
for vgs in self.vgs:
|
|
vgs._create()
|
|
for lvs in self.lvs:
|
|
lvs._create()
|
|
|
|
def umount(self):
|
|
for lvs in self.lvs:
|
|
lvs._umount()
|
|
|
|
for vgs in self.vgs:
|
|
vgs._umount()
|
|
|
|
exec_sudo(['udevadm', 'settle'])
|
|
|
|
def cleanup(self):
|
|
# Information about the PV, VG and LV is typically
|
|
# cached in lvmetad. Even after removing PV device and
|
|
# partitions this data is not automatically updated
|
|
# which leads to a couple of problems.
|
|
# the 'pvscan --cache' scans the available disks
|
|
# and updates the cache.
|
|
# This is in cleanup because it must be called after the
|
|
# umount of the containing block device is done, (which should
|
|
# all be done in umount phase).
|
|
try:
|
|
exec_sudo(['pvscan', '--cache'])
|
|
except BlockDeviceSetupException as e:
|
|
logger.info("pvscan call failed [%s]", e.returncode)
|
|
|
|
|
|
class LVMPlugin(PluginBase):
|
|
|
|
def _config_error(self, msg):
|
|
raise BlockDeviceSetupException(msg)
|
|
|
|
def __init__(self, config, defaults, state):
|
|
"""Build LVM nodes
|
|
|
|
This reads the "lvm:" config stanza, validates it and produces
|
|
the PV, VG and LV nodes. These are all synchronised via a
|
|
LVMNode as described above.
|
|
|
|
Arguments:
|
|
:param config: "lvm" configuration dictionary
|
|
:param defaults: global defaults dictionary
|
|
:param state: global state reference
|
|
"""
|
|
|
|
super(LVMPlugin, self).__init__()
|
|
|
|
# note lvm: doesn't require a base ... the base is the
|
|
# physical devices the "pvs" nodes are made on.
|
|
if 'name' not in config:
|
|
self._config_error("Lvm config requires 'name'")
|
|
if 'pvs' not in config:
|
|
self._config_error("Lvm config requires a 'pvs'")
|
|
if 'vgs' not in config:
|
|
self._config_error("Lvm config needs 'vgs'")
|
|
if 'lvs' not in config:
|
|
self._config_error("Lvm config needs 'lvs'")
|
|
|
|
# create physical volume nodes
|
|
self.pvs = []
|
|
self.pvs_keys = []
|
|
for pvs_cfg in config['pvs']:
|
|
if 'name' not in pvs_cfg:
|
|
self._config_error("Missing 'name' in pvs config")
|
|
if 'base' not in pvs_cfg:
|
|
self._config_error("Missing 'base' in pvs_config")
|
|
|
|
pvs_item = PvsNode(pvs_cfg['name'], state,
|
|
pvs_cfg['base'],
|
|
pvs_cfg.get('options'))
|
|
self.pvs.append(pvs_item)
|
|
|
|
# create volume group nodes
|
|
self.vgs = []
|
|
self.vgs_keys = []
|
|
for vgs_cfg in config['vgs']:
|
|
if 'name' not in vgs_cfg:
|
|
self._config_error("Missing 'name' in vgs config")
|
|
if 'base' not in vgs_cfg:
|
|
self._config_error("Missing 'base' in vgs config")
|
|
|
|
# Ensure we have a valid PVs backing this VG
|
|
for pvs in vgs_cfg['base']:
|
|
if not any(pv.name == pvs for pv in self.pvs):
|
|
self._config_error("base:%s in vgs does not "
|
|
"match a valid pvs" % pvs)
|
|
vgs_item = VgsNode(vgs_cfg['name'], state, vgs_cfg['base'],
|
|
vgs_cfg.get('options', None))
|
|
self.vgs.append(vgs_item)
|
|
|
|
# create logical volume nodes
|
|
self.lvs = []
|
|
for lvs_cfg in config['lvs']:
|
|
if 'name' not in lvs_cfg:
|
|
self._config_error("Missing 'name' in lvs config")
|
|
if 'base' not in lvs_cfg:
|
|
self._config_error("Missing 'base' in lvs config")
|
|
if 'size' not in lvs_cfg and 'extents' not in lvs_cfg:
|
|
self._config_error("Missing 'size' or 'extents' in lvs config")
|
|
|
|
# ensure this logical volume has a valid volume group base
|
|
if not any(vg.name == lvs_cfg['base'] for vg in self.vgs):
|
|
self._config_error("base:%s in lvs does not match a valid vg" %
|
|
lvs_cfg['base'])
|
|
|
|
if 'type' in lvs_cfg:
|
|
if lvs_cfg['type'] not in LVS_TYPES:
|
|
self._config_error(
|
|
"Unsupported type:%s, supported types: %s" %
|
|
(lvs_cfg['type'], ', '.join(LVS_TYPES)))
|
|
|
|
lvs_item = LvsNode(lvs_cfg['name'], state, lvs_cfg['base'],
|
|
lvs_cfg.get('options', None),
|
|
lvs_cfg.get('size', None),
|
|
lvs_cfg.get('extents', None),
|
|
lvs_cfg.get('type', None),
|
|
lvs_cfg.get('thin-pool', None))
|
|
self.lvs.append(lvs_item)
|
|
|
|
# create the "driver" node
|
|
self.lvm_node = LVMNode(config['name'], state,
|
|
self.pvs, self.lvs, self.vgs)
|
|
|
|
def get_nodes(self):
|
|
# the nodes for insertion into the graph are all of the pvs,
|
|
# vgs and lvs nodes we have created above and the root node and
|
|
# the cleanup node.
|
|
return self.pvs + self.vgs + self.lvs \
|
|
+ [self.lvm_node]
|