os-brick 1.1.0 release

meta:version: 1.1.0
 meta:series: mitaka
 meta:release-type: release
 meta:announce: openstack-dev@lists.openstack.org
 meta:pypi: yes
 -----BEGIN PGP SIGNATURE-----
 Comment: GPGTools - http://gpgtools.org
 
 iQEcBAABAgAGBQJWzgK5AAoJEDttBqDEKEN6MZ8H/RuI5QfE6XjmU8fxjiuPEFnx
 Ht05ne0bMUEOggCzdoISRPQdZEpAXZaIZktHmxFNhfzKTJhkAy5wDvjBsLaABLfS
 2D+4RtE/ox26TaeiDOFbocKSE/FC+EBfxIn0bgtsdyfMcfRZIG/RdJNhOIlk+fXm
 iOrHGXYw8IGspnvf409ivc3qpEEiUad1OvooYewM4G27Mh3J00SVOqcVmSb8hawE
 NXV/1QkvcXjbeFakZx4uA5XRpvQSMGt8CErUa6KsuibYTf7vwxicSmqUgINMcfJs
 tA1b3bDJ/+/5Enxl8uFSZmY2j3MXzdTM7fcx+BOir4SL3dyKjLNOrdYK6fcS0Zg=
 =svDb
 -----END PGP SIGNATURE-----

Merge tag '1.1.0' into debian/mitaka

os-brick 1.1.0 release

meta:version: 1.1.0
meta:series: mitaka
meta:release-type: release
meta:announce: openstack-dev@lists.openstack.org
meta:pypi: yes
This commit is contained in:
Corey Bryant 2016-03-03 10:35:38 -05:00
commit 8a0c84a3ed
25 changed files with 2525 additions and 96 deletions

3
.gitignore vendored
View File

@ -43,6 +43,9 @@ output/*/index.html
# Sphinx
doc/build
# Release notes
releasenotes/build/
# pbr generates these
AUTHORS
ChangeLog

View File

@ -22,8 +22,8 @@ sys.path.insert(0, os.path.abspath('../..'))
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = [
'sphinx.ext.autodoc',
#'sphinx.ext.intersphinx',
'oslosphinx'
'oslosphinx',
'reno.sphinxext',
]
# autodoc generation is a bit aggressive and a nuisance when doing heavy

View File

@ -67,3 +67,39 @@ vgc-cluster: CommandFilter, vgc-cluster, root
# initiator/linuxscsi.py
scsi_id: CommandFilter, /lib/udev/scsi_id, root
# local_dev lvm related commands
# LVM related show commands
pvs: EnvFilter, env, root, LC_ALL=C, pvs
vgs: EnvFilter, env, root, LC_ALL=C, vgs
lvs: EnvFilter, env, root, LC_ALL=C, lvs
lvdisplay: EnvFilter, env, root, LC_ALL=C, lvdisplay
# local_dev/lvm.py: 'vgcreate', vg_name, pv_list
vgcreate: CommandFilter, vgcreate, root
# local_dev/lvm.py: 'lvcreate', '-L', sizestr, '-n', volume_name,..
# local_dev/lvm.py: 'lvcreate', '-L', ...
lvcreate: EnvFilter, env, root, LC_ALL=C, lvcreate
lvcreate_lvmconf: EnvFilter, env, root, LVM_SYSTEM_DIR=, LC_ALL=C, lvcreate
# local_dev/lvm.py: 'lvextend', '-L' '%(new_size)s', '%(lv_name)s' ...
# local_dev/lvm.py: 'lvextend', '-L' '%(new_size)s', '%(thin_pool)s' ...
lvextend: EnvFilter, env, root, LC_ALL=C, lvextend
lvextend_lvmconf: EnvFilter, env, root, LVM_SYSTEM_DIR=, LC_ALL=C, lvextend
# local_dev/lvm.py: 'lvremove', '-f', %s/%s % ...
lvremove: CommandFilter, lvremove, root
# local_dev/lvm.py: 'lvrename', '%(vg)s', '%(orig)s' '(new)s'...
lvrename: CommandFilter, lvrename, root
# local_dev/lvm.py: 'lvchange -a y -K <lv>'
lvchange: CommandFilter, lvchange, root
# local_dev/lvm.py: 'lvconvert', '--merge', snapshot_name
lvconvert: CommandFilter, lvconvert, root
# local_dev/lvm.py: 'udevadm', 'settle'
udevadm: CommandFilter, udevadm, root

View File

@ -98,6 +98,10 @@ class VolumeDeviceNotFound(BrickException):
message = _("Volume device not found at %(device)s.")
class VolumePathsNotFound(BrickException):
message = _("Could not find any paths for the volume.")
class VolumePathNotRemoved(BrickException):
message = _("Volume path %(volume_path)s was not removed in time.")
@ -116,3 +120,15 @@ class FailedISCSITargetPortalLogin(BrickException):
class BlockDeviceReadOnly(BrickException):
message = _("Block device %(device)s is Read-Only.")
class VolumeGroupNotFound(BrickException):
message = _("Unable to find Volume Group: %(vg_name)s")
class VolumeGroupCreationFailed(BrickException):
message = _("Failed to create Volume Group: %(vg_name)s")
class CommandExecutionFailed(BrickException):
message = _("Failed to execute command %(cmd)s")

View File

@ -29,6 +29,7 @@ import platform
import re
import requests
import socket
import struct
import sys
import tempfile
import time
@ -77,6 +78,8 @@ RBD = "RBD"
SCALEIO = "SCALEIO"
SCALITY = "SCALITY"
QUOBYTE = "QUOBYTE"
DISCO = "DISCO"
VZSTORAGE = "VZSTORAGE"
def _check_multipathd_running(root_helper, enforce_multipath):
@ -153,6 +156,7 @@ class InitiatorConnector(executor.Executor):
driver = host_driver.HostDriver()
self.set_driver(driver)
self.device_scan_attempts = device_scan_attempts
self._linuxscsi = linuxscsi.LinuxSCSI(root_helper, execute=execute)
def set_driver(self, driver):
"""The driver is used to find used LUNs."""
@ -209,7 +213,7 @@ class InitiatorConnector(executor.Executor):
execute=execute,
device_scan_attempts=device_scan_attempts,
*args, **kwargs)
elif protocol in (NFS, GLUSTERFS, SCALITY, QUOBYTE):
elif protocol in (NFS, GLUSTERFS, SCALITY, QUOBYTE, VZSTORAGE):
return RemoteFsConnector(mount_type=protocol.lower(),
root_helper=root_helper,
driver=driver,
@ -248,6 +252,13 @@ class InitiatorConnector(executor.Executor):
*args, **kwargs)
elif protocol == SCALEIO:
return ScaleIOConnector(
root_helper=root_helper,
driver=driver,
execute=execute,
device_scan_attempts=device_scan_attempts,
*args, **kwargs)
elif protocol == DISCO:
return DISCOConnector(
root_helper=root_helper,
driver=driver,
execute=execute,
@ -286,6 +297,45 @@ class InitiatorConnector(executor.Executor):
return False
return True
def _discover_mpath_device(self, device_wwn, connection_properties,
device_name):
"""This method discovers a multipath device.
Discover a multipath device based on a defined connection_property
and a device_wwn and return the multipath_id and path of the multipath
enabled device if there is one.
"""
path = self._linuxscsi.find_multipath_device_path(device_wwn)
device_path = None
multipath_id = None
if path is None:
mpath_info = self._linuxscsi.find_multipath_device(
device_name)
if mpath_info:
device_path = mpath_info['device']
multipath_id = device_wwn
else:
# we didn't find a multipath device.
# so we assume the kernel only sees 1 device
device_path = self.host_device
LOG.debug("Unable to find multipath device name for "
"volume. Using path %(device)s for volume.",
{'device': self.host_device})
else:
device_path = path
multipath_id = device_wwn
if connection_properties.get('access_mode', '') != 'ro':
try:
# Sometimes the multipath devices will show up as read only
# initially and need additional time/rescans to get to RW.
self._linuxscsi.wait_for_rw(device_wwn, device_path)
except exception.BlockDeviceReadOnly:
LOG.warning(_LW('Block device %s is still read-only. '
'Continuing anyway.'), device_path)
return device_path, multipath_id
@abc.abstractmethod
def connect_volume(self, connection_properties):
"""Connect to a volume.
@ -376,6 +426,20 @@ class InitiatorConnector(executor.Executor):
"""
pass
@abc.abstractmethod
def extend_volume(self, connection_properties):
"""Update the attached volume's size.
This method will attempt to update the local hosts's
volume after the volume has been extended on the remote
system. The new volume size in bytes will be returned.
If there is a failure to update, then None will be returned.
:param connection_properties: The volume connection properties.
:returns: new size of the volume.
"""
pass
def get_all_available_volumes(self, connection_properties=None):
"""Return all volumes that exist in the search directory.
@ -395,13 +459,10 @@ class InitiatorConnector(executor.Executor):
path = self.get_search_path()
if path:
# now find all entries in the search path
file_list = []
if os.path.isdir(path):
files = os.listdir(path)
for entry in files:
file_list.append(path + entry)
return file_list
path_items = [path, '/*']
file_filter = ''.join(path_items)
return glob.glob(file_filter)
else:
return []
@ -424,6 +485,9 @@ class FakeConnector(InitiatorConnector):
def get_search_path(self):
return '/dev/disk/by-path'
def extend_volume(self, connection_properties):
return None
def get_all_available_volumes(self, connection_properties=None):
return ['/dev/disk/by-path/fake-volume-1',
'/dev/disk/by-path/fake-volume-X']
@ -741,6 +805,25 @@ class ISCSIConnector(InitiatorConnector):
run_as_root=True,
root_helper=self._root_helper)
@synchronized('extend_volume')
def extend_volume(self, connection_properties):
"""Update the local kernel's size information.
Try and update the local kernel's size information
for an iSCSI volume.
"""
LOG.info(_LI("Extend volume for %s"), connection_properties)
volume_paths = self.get_volume_paths(connection_properties)
LOG.info(_LI("Found paths for volume %s"), volume_paths)
if volume_paths:
return self._linuxscsi.extend_volume(volume_paths[0])
else:
LOG.warning(_LW("Couldn't find any volume paths on the host to "
"extend volume for %(props)s"),
{'props': connection_properties})
raise exception.VolumePathsNotFound()
@synchronized('connect_volume')
def connect_volume(self, connection_properties):
"""Attach the volume to instance_name.
@ -797,20 +880,18 @@ class ISCSIConnector(InitiatorConnector):
# Choose an accessible host device
host_device = next(dev for dev in host_devices if os.path.exists(dev))
if self.use_multipath:
# We use the multipath device instead of the single path device
self._rescan_multipath()
multipath_device = self._get_multipath_device_name(host_device)
if multipath_device is not None:
host_device = multipath_device
LOG.debug("Found multipath device name for "
"volume. Using path %(device)s "
"for volume.", {'device': host_device})
# find out the WWN of the device
device_wwn = self._linuxscsi.get_scsi_wwn(host_device)
LOG.debug("Device WWN = '%(wwn)s'", {'wwn': device_wwn})
device_info['scsi_wwn'] = device_wwn
if self.use_multipath:
(host_device, multipath_id) = (super(
ISCSIConnector, self)._discover_mpath_device(
device_wwn, connection_properties, host_device))
if multipath_id:
device_info['multipath_id'] = multipath_id
device_info['path'] = host_device
LOG.debug("connect_volume returning %s", device_info)
@ -900,7 +981,8 @@ class ISCSIConnector(InitiatorConnector):
brackets. Udev code specifically forbids that.
"""
portal, iqn, lun = target
return (portal.replace('[', '').replace(']', ''), iqn, lun)
return (portal.replace('[', '').replace(']', ''), iqn,
self._linuxscsi.process_lun_id(lun))
def _get_device_path(self, connection_properties):
if self._get_transport() == "default":
@ -946,7 +1028,7 @@ class ISCSIConnector(InitiatorConnector):
check_exit_code=check_exit_code,
attempts=attempts,
delay_on_retry=delay_on_retry)
msg = ("iscsiadm %(iscsi_command)s: stdout=%(out)s stderr=%(err)s",
msg = ("iscsiadm %(iscsi_command)s: stdout=%(out)s stderr=%(err)s" %
{'iscsi_command': iscsi_command, 'out': out, 'err': err})
# don't let passwords be shown in log output
LOG.debug(strutils.mask_password(msg))
@ -1170,7 +1252,8 @@ class ISCSIConnector(InitiatorConnector):
mpath_map['/dev/' + m[1].split(" ")[0]] = mpath_dev
if mpath_line and not mpath_map:
LOG.warning(_LW("Failed to parse the output of multipath -ll."))
LOG.warning(_LW("Failed to parse the output of multipath -ll. "
"stdout: %s"), out)
return mpath_map
def _run_iscsi_session(self):
@ -1261,6 +1344,22 @@ class FibreChannelConnector(InitiatorConnector):
return volume_paths
@synchronized('extend_volume')
def extend_volume(self, connection_properties):
"""Update the local kernel's size information.
Try and update the local kernel's size information
for an FC volume.
"""
volume_paths = self.get_volume_paths(connection_properties)
if volume_paths:
return self._linuxscsi.extend_volume(volume_paths[0])
else:
LOG.warning(_LW("Couldn't find any volume paths on the host to "
"extend volume for %(props)s"),
{'props': connection_properties})
raise exception.VolumePathsNotFound()
@synchronized('connect_volume')
def connect_volume(self, connection_properties):
"""Attach the volume to instance_name.
@ -1334,37 +1433,12 @@ class FibreChannelConnector(InitiatorConnector):
# see if the new drive is part of a multipath
# device. If so, we'll use the multipath device.
if self.use_multipath:
path = self._linuxscsi.find_multipath_device_path(device_wwn)
if path is not None:
LOG.debug("Multipath device path discovered %(device)s",
{'device': path})
device_path = path
# for temporary backwards compatibility
device_info['multipath_id'] = device_wwn
else:
mpath_info = self._linuxscsi.find_multipath_device(
self.device_name)
if mpath_info:
device_path = mpath_info['device']
# for temporary backwards compatibility
device_info['multipath_id'] = device_wwn
else:
# we didn't find a multipath device.
# so we assume the kernel only sees 1 device
device_path = self.host_device
LOG.debug("Unable to find multipath device name for "
"volume. Using path %(device)s for volume.",
{'device': self.host_device})
if connection_properties.get('access_mode', '') != 'ro':
try:
# Sometimes the multipath devices will show up as read only
# initially and need additional time/rescans to get to RW.
self._linuxscsi.wait_for_rw(device_wwn, device_path)
except exception.BlockDeviceReadOnly:
LOG.warning(_LW('Block device %s is still read-only. '
'Continuing anyway.'), device_path)
(device_path, multipath_id) = (super(
FibreChannelConnector, self)._discover_mpath_device(
device_wwn, connection_properties, self.device_name))
if multipath_id:
# only set the multipath_id if we found one
device_info['multipath_id'] = multipath_id
else:
device_path = self.host_device
@ -1379,7 +1453,7 @@ class FibreChannelConnector(InitiatorConnector):
host_device = "/dev/disk/by-path/pci-%s-fc-%s-lun-%s" % (
pci_num,
target_wwn,
lun)
self._linuxscsi.process_lun_id(lun))
host_devices.append(host_device)
return host_devices
@ -1674,6 +1748,10 @@ class AoEConnector(InitiatorConnector):
LOG.debug('aoe-flush %(dev)s: stdout=%(out)s stderr%(err)s',
{'dev': aoe_device, 'out': out, 'err': err})
def extend_volume(self, connection_properties):
# TODO(walter-boring): is this possible?
raise NotImplementedError
class RemoteFsConnector(InitiatorConnector):
"""Connector class to attach/detach NFS and GlusterFS volumes."""
@ -1687,7 +1765,8 @@ class RemoteFsConnector(InitiatorConnector):
if conn:
mount_point_base = conn.get('mount_point_base')
mount_type_lower = mount_type.lower()
if mount_type_lower in ('nfs', 'glusterfs', 'scality', 'quobyte'):
if mount_type_lower in ('nfs', 'glusterfs', 'scality',
'quobyte', 'vzstorage'):
kwargs[mount_type_lower + '_mount_point_base'] = (
kwargs.get(mount_type_lower + '_mount_point_base') or
mount_point_base)
@ -1753,6 +1832,10 @@ class RemoteFsConnector(InitiatorConnector):
:type device_info: dict
"""
def extend_volume(self, connection_properties):
# TODO(walter-boring): is this possible?
raise NotImplementedError
class RBDConnector(InitiatorConnector):
""""Connector class to attach/detach RBD volumes."""
@ -1841,6 +1924,10 @@ class RBDConnector(InitiatorConnector):
return True
def extend_volume(self, connection_properties):
# TODO(walter-boring): is this possible?
raise NotImplementedError
class LocalConnector(InitiatorConnector):
""""Connector class to attach/detach File System backed volumes."""
@ -1891,6 +1978,10 @@ class LocalConnector(InitiatorConnector):
"""
pass
def extend_volume(self, connection_properties):
# TODO(walter-boring): is this possible?
raise NotImplementedError
class DRBDConnector(InitiatorConnector):
""""Connector class to attach/detach DRBD resources."""
@ -1968,6 +2059,10 @@ class DRBDConnector(InitiatorConnector):
# TODO(linbit): is it allowed to return "/dev", or is that too broad?
return None
def extend_volume(self, connection_properties):
# TODO(walter-boring): is this possible?
raise NotImplementedError
class HuaweiStorHyperConnector(InitiatorConnector):
""""Connector class to attach/detach SDSHypervisor volumes."""
@ -2123,6 +2218,10 @@ class HuaweiStorHyperConnector(InitiatorConnector):
else:
return None
def extend_volume(self, connection_properties):
# TODO(walter-boring): is this possible?
raise NotImplementedError
class HGSTConnector(InitiatorConnector):
"""Connector class to attach/detach HGST volumes."""
@ -2268,6 +2367,10 @@ class HGSTConnector(InitiatorConnector):
connection_properties['name'])
raise exception.BrickException(message=msg)
def extend_volume(self, connection_properties):
# TODO(walter-boring): is this possible?
raise NotImplementedError
class ScaleIOConnector(InitiatorConnector):
"""Class implements the connector driver for ScaleIO."""
@ -2508,6 +2611,7 @@ class ScaleIOConnector(InitiatorConnector):
'path': self.volume_path}
return device_info
@lockutils.synchronized('scaleio', 'scaleio-')
def connect_volume(self, connection_properties):
"""Connect the volume.
@ -2636,6 +2740,7 @@ class ScaleIOConnector(InitiatorConnector):
return device_info
@lockutils.synchronized('scaleio', 'scaleio-')
def disconnect_volume(self, connection_properties, device_info):
"""Disconnect the ScaleIO volume.
@ -2711,3 +2816,176 @@ class ScaleIOConnector(InitiatorConnector):
'err': response['message']})
LOG.error(msg)
raise exception.BrickException(message=msg)
def extend_volume(self, connection_properties):
# TODO(walter-boring): is this possible?
raise NotImplementedError
class DISCOConnector(InitiatorConnector):
"""Class implements the connector driver for DISCO."""
DISCO_PREFIX = 'dms'
def __init__(self, root_helper, driver=None, execute=putils.execute,
device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT,
*args, **kwargs):
"""Init DISCO connector."""
super(DISCOConnector, self).__init__(
root_helper,
driver=driver,
execute=execute,
device_scan_attempts=device_scan_attempts,
*args, **kwargs
)
LOG.info(_LI("Init DISCO connector"))
self.server_port = None
self.server_ip = None
def get_search_path(self):
"""Get directory path where to get DISCO volumes."""
return "/dev"
def get_volume_paths(self, connection_properties):
"""Get config for DISCO volume driver."""
self.get_config(connection_properties)
volume_paths = []
disco_id = connection_properties['disco_id']
disco_dev = '/dev/dms%s' % (disco_id)
device_paths = [disco_dev]
for path in device_paths:
if os.path.exists(path):
volume_paths.append(path)
return volume_paths
def get_all_available_volumes(self, connection_properties=None):
"""Return all DISCO volumes that exist in the search directory."""
path = self.get_search_path()
if os.path.isdir(path):
path_items = [path, '/', self.DISCO_PREFIX, '*']
file_filter = ''.join(path_items)
return glob.glob(file_filter)
else:
return []
def get_config(self, connection_properties):
"""Get config for DISCO volume driver."""
self.server_port = (
six.text_type(connection_properties['conf']['server_port']))
self.server_ip = (
six.text_type(connection_properties['conf']['server_ip']))
disco_id = connection_properties['disco_id']
disco_dev = '/dev/dms%s' % (disco_id)
device_info = {'type': 'block',
'path': disco_dev}
return device_info
@synchronized('connect_volume')
def connect_volume(self, connection_properties):
"""Connect the volume. Returns xml for libvirt."""
LOG.debug("Enter in DISCO connect_volume")
device_info = self.get_config(connection_properties)
LOG.debug("Device info : %s.", device_info)
disco_id = connection_properties['disco_id']
disco_dev = '/dev/dms%s' % (disco_id)
LOG.debug("Attaching %s", disco_dev)
self._mount_disco_volume(disco_dev, disco_id)
return device_info
@synchronized('connect_volume')
def disconnect_volume(self, connection_properties, device_info):
"""Detach the volume from instance."""
disco_id = connection_properties['disco_id']
disco_dev = '/dev/dms%s' % (disco_id)
LOG.debug("detaching %s", disco_dev)
if os.path.exists(disco_dev):
ret = self._send_disco_vol_cmd(self.server_ip,
self.server_port,
2,
disco_id)
if ret is not None:
msg = _("Detach volume failed")
raise exception.BrickException(message=msg)
else:
LOG.info(_LI("Volume already detached from host"))
def _mount_disco_volume(self, path, volume_id):
"""Send request to mount volume on physical host."""
LOG.debug("Enter in mount disco volume %(port)s "
"and %(ip)s." %
{'port': self.server_port,
'ip': self.server_ip})
if not os.path.exists(path):
ret = self._send_disco_vol_cmd(self.server_ip,
self.server_port,
1,
volume_id)
if ret is not None:
msg = _("Attach volume failed")
raise exception.BrickException(message=msg)
else:
LOG.info(_LI("Volume already attached to host"))
def _connect_tcp_socket(self, client_ip, client_port):
"""Connect to TCP socket."""
sock = None
for res in socket.getaddrinfo(client_ip,
client_port,
socket.AF_UNSPEC,
socket.SOCK_STREAM):
aff, socktype, proto, canonname, saa = res
try:
sock = socket.socket(aff, socktype, proto)
except socket.error:
sock = None
continue
try:
sock.connect(saa)
except socket.error:
sock.close()
sock = None
continue
break
if sock is None:
LOG.error(_LE("Cannot connect TCP socket"))
return sock
def _send_disco_vol_cmd(self, client_ip, client_port, op_code, vol_id):
"""Send DISCO client socket command."""
s = self._connect_tcp_socket(client_ip, int(client_port))
if s is not None:
inst_id = 'DEFAULT-INSTID'
pktlen = 2 + 8 + len(inst_id)
LOG.debug("pktlen=%(plen)s op=%(op)s "
"vol_id=%(vol_id)s, inst_id=%(inst_id)s",
{'plen': pktlen, 'op': op_code,
'vol_id': vol_id, 'inst_id': inst_id})
data = struct.pack("!HHQ14s",
pktlen,
op_code,
int(vol_id),
inst_id)
s.sendall(data)
ret = s.recv(4)
s.close()
LOG.debug("Received ret len=%(lenR)d, ret=%(ret)s",
{'lenR': len(repr(ret)), 'ret': repr(ret)})
ret_val = "".join("%02x" % ord(c) for c in ret)
if ret_val != '00000000':
return 'ERROR'
return None
def extend_volume(self, connection_properties):
raise NotImplementedError

View File

@ -18,6 +18,7 @@
"""
import os
import re
import six
from oslo_concurrency import processutils as putils
from oslo_log import log as logging
@ -265,7 +266,8 @@ class LinuxSCSI(executor.Executor):
except putils.ProcessExecutionError as exc:
LOG.warning(_LW("multipath call failed exit %(code)s"),
{'code': exc.exit_code})
return None
raise exception.CommandExecutionFailed(
cmd='multipath -l %s' % device)
if out:
lines = out.strip()
@ -320,3 +322,107 @@ class LinuxSCSI(executor.Executor):
"devices": devices}
return info
return None
def get_device_size(self, device):
"""Get the size in bytes of a volume."""
(out, _err) = self._execute('blockdev', '--getsize64',
device, run_as_root=True,
root_helper=self._root_helper)
var = six.text_type(out.strip())
if var.isnumeric():
return int(var)
else:
return None
def multipath_reconfigure(self):
"""Issue a multipathd reconfigure.
When attachments come and go, the multipathd seems
to get lost and not see the maps. This causes
resize map to fail 100%. To overcome this we have
to issue a reconfigure prior to resize map.
"""
(out, _err) = self._execute('multipathd', 'reconfigure',
run_as_root=True,
root_helper=self._root_helper)
return out
def multipath_resize_map(self, mpath_id):
"""Issue a multipath resize map on device.
This forces the multipath daemon to update it's
size information a particular multipath device.
"""
(out, _err) = self._execute('multipathd', 'resize', 'map', mpath_id,
run_as_root=True,
root_helper=self._root_helper)
return out
def extend_volume(self, volume_path):
"""Signal the SCSI subsystem to test for volume resize.
This function tries to signal the local system's kernel
that an already attached volume might have been resized.
"""
LOG.debug("extend volume %s", volume_path)
device = self.get_device_info(volume_path)
LOG.debug("Volume device info = %s", device)
device_id = ("%(host)s:%(channel)s:%(id)s:%(lun)s" %
{'host': device['host'],
'channel': device['channel'],
'id': device['id'],
'lun': device['lun']})
scsi_path = ("/sys/bus/scsi/drivers/sd/%(device_id)s" %
{'device_id': device_id})
size = self.get_device_size(volume_path)
LOG.debug("Starting size: %s", size)
# now issue the device rescan
rescan_path = "%(scsi_path)s/rescan" % {'scsi_path': scsi_path}
self.echo_scsi_command(rescan_path, "1")
new_size = self.get_device_size(volume_path)
LOG.debug("volume size after scsi device rescan %s", new_size)
scsi_wwn = self.get_scsi_wwn(volume_path)
mpath_device = self.find_multipath_device_path(scsi_wwn)
if mpath_device:
# Force a reconfigure so that resize works
self.multipath_reconfigure()
size = self.get_device_size(mpath_device)
LOG.info(_LI("mpath(%(device)s) current size %(size)s"),
{'device': mpath_device, 'size': size})
result = self.multipath_resize_map(scsi_wwn)
if 'fail' in result:
msg = (_LI("Multipathd failed to update the size mapping of "
"multipath device %(scsi_wwn)s volume %(volume)s") %
{'scsi_wwn': scsi_wwn, 'volume': volume_path})
LOG.error(msg)
return None
new_size = self.get_device_size(mpath_device)
LOG.info(_LI("mpath(%(device)s) new size %(size)s"),
{'device': mpath_device, 'size': new_size})
return new_size
else:
return new_size
def process_lun_id(self, lun_ids):
if isinstance(lun_ids, list):
processed = []
for x in lun_ids:
x = self._format_lun_id(x)
processed.append(x)
else:
processed = self._format_lun_id(lun_ids)
return processed
def _format_lun_id(self, lun_id):
if lun_id < 256:
return lun_id
else:
return ("0x%04x%04x00000000" %
(lun_id & 0xffff, lun_id >> 16 & 0xffff))

View File

782
os_brick/local_dev/lvm.py Normal file
View File

@ -0,0 +1,782 @@
# Copyright 2013 OpenStack Foundation.
# All Rights Reserved.
#
# 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.
"""
LVM class for performing LVM operations.
"""
import math
import os
import re
from os_brick import exception
from os_brick import executor
from os_brick.i18n import _LE, _LI
from os_brick import utils
from oslo_concurrency import processutils as putils
from oslo_log import log as logging
from oslo_utils import excutils
from six import moves
LOG = logging.getLogger(__name__)
class LVM(executor.Executor):
"""LVM object to enable various LVM related operations."""
LVM_CMD_PREFIX = ['env', 'LC_ALL=C']
def __init__(self, vg_name, root_helper, create_vg=False,
physical_volumes=None, lvm_type='default',
executor=putils.execute, lvm_conf=None):
"""Initialize the LVM object.
The LVM object is based on an LVM VolumeGroup, one instantiation
for each VolumeGroup you have/use.
:param vg_name: Name of existing VG or VG to create
:param root_helper: Execution root_helper method to use
:param create_vg: Indicates the VG doesn't exist
and we want to create it
:param physical_volumes: List of PVs to build VG on
:param lvm_type: VG and Volume type (default, or thin)
:param executor: Execute method to use, None uses common/processutils
"""
super(LVM, self).__init__(execute=executor, root_helper=root_helper)
self.vg_name = vg_name
self.pv_list = []
self.vg_size = 0.0
self.vg_free_space = 0.0
self.vg_lv_count = 0
self.vg_uuid = None
self.vg_thin_pool = None
self.vg_thin_pool_size = 0.0
self.vg_thin_pool_free_space = 0.0
self._supports_snapshot_lv_activation = None
self._supports_lvchange_ignoreskipactivation = None
self.vg_provisioned_capacity = 0.0
# Ensure LVM_SYSTEM_DIR has been added to LVM.LVM_CMD_PREFIX
# before the first LVM command is executed, and use the directory
# where the specified lvm_conf file is located as the value.
if lvm_conf and os.path.isfile(lvm_conf):
lvm_sys_dir = os.path.dirname(lvm_conf)
LVM.LVM_CMD_PREFIX = ['env',
'LC_ALL=C',
'LVM_SYSTEM_DIR=' + lvm_sys_dir]
if create_vg and physical_volumes is not None:
self.pv_list = physical_volumes
try:
self._create_vg(physical_volumes)
except putils.ProcessExecutionError as err:
LOG.exception(_LE('Error creating Volume Group'))
LOG.error(_LE('Cmd :%s'), err.cmd)
LOG.error(_LE('StdOut :%s'), err.stdout)
LOG.error(_LE('StdErr :%s'), err.stderr)
raise exception.VolumeGroupCreationFailed(vg_name=self.vg_name)
if self._vg_exists() is False:
LOG.error(_LE('Unable to locate Volume Group %s'), vg_name)
raise exception.VolumeGroupNotFound(vg_name=vg_name)
# NOTE: we assume that the VG has been activated outside of Cinder
if lvm_type == 'thin':
pool_name = "%s-pool" % self.vg_name
if self.get_volume(pool_name) is None:
try:
self.create_thin_pool(pool_name)
except putils.ProcessExecutionError:
# Maybe we just lost the race against another copy of
# this driver being in init in parallel - e.g.
# cinder-volume and cinder-backup starting in parallel
if self.get_volume(pool_name) is None:
raise
self.vg_thin_pool = pool_name
self.activate_lv(self.vg_thin_pool)
self.pv_list = self.get_all_physical_volumes(root_helper, vg_name)
def _vg_exists(self):
"""Simple check to see if VG exists.
:returns: True if vg specified in object exists, else False
"""
exists = False
cmd = LVM.LVM_CMD_PREFIX + ['vgs', '--noheadings',
'-o', 'name', self.vg_name]
(out, _err) = self._execute(*cmd,
root_helper=self._root_helper,
run_as_root=True)
if out is not None:
volume_groups = out.split()
if self.vg_name in volume_groups:
exists = True
return exists
def _create_vg(self, pv_list):
cmd = ['vgcreate', self.vg_name, ','.join(pv_list)]
self._execute(*cmd, root_helper=self._root_helper, run_as_root=True)
def _get_vg_uuid(self):
cmd = LVM.LVM_CMD_PREFIX + ['vgs', '--noheadings',
'-o', 'uuid', self.vg_name]
(out, _err) = self._execute(*cmd,
root_helper=self._root_helper,
run_as_root=True)
if out is not None:
return out.split()
else:
return []
def _get_thin_pool_free_space(self, vg_name, thin_pool_name):
"""Returns available thin pool free space.
:param vg_name: the vg where the pool is placed
:param thin_pool_name: the thin pool to gather info for
:returns: Free space in GB (float), calculated using data_percent
"""
cmd = LVM.LVM_CMD_PREFIX + ['lvs', '--noheadings', '--unit=g',
'-o', 'size,data_percent', '--separator',
':', '--nosuffix']
# NOTE(gfidente): data_percent only applies to some types of LV so we
# make sure to append the actual thin pool name
cmd.append("/dev/%s/%s" % (vg_name, thin_pool_name))
free_space = 0.0
try:
(out, err) = self._execute(*cmd,
root_helper=self._root_helper,
run_as_root=True)
if out is not None:
out = out.strip()
data = out.split(':')
pool_size = float(data[0])
data_percent = float(data[1])
consumed_space = pool_size / 100 * data_percent
free_space = pool_size - consumed_space
free_space = round(free_space, 2)
except putils.ProcessExecutionError as err:
LOG.exception(_LE('Error querying thin pool about data_percent'))
LOG.error(_LE('Cmd :%s'), err.cmd)
LOG.error(_LE('StdOut :%s'), err.stdout)
LOG.error(_LE('StdErr :%s'), err.stderr)
return free_space
@staticmethod
def get_lvm_version(root_helper):
"""Static method to get LVM version from system.
:param root_helper: root_helper to use for execute
:returns: version 3-tuple
"""
cmd = LVM.LVM_CMD_PREFIX + ['vgs', '--version']
(out, _err) = putils.execute(*cmd,
root_helper=root_helper,
run_as_root=True)
lines = out.split('\n')
for line in lines:
if 'LVM version' in line:
version_list = line.split()
# NOTE(gfidente): version is formatted as follows:
# major.minor.patchlevel(library API version)[-customisation]
version = version_list[2]
version_filter = r"(\d+)\.(\d+)\.(\d+).*"
r = re.search(version_filter, version)
version_tuple = tuple(map(int, r.group(1, 2, 3)))
return version_tuple
@staticmethod
def supports_thin_provisioning(root_helper):
"""Static method to check for thin LVM support on a system.
:param root_helper: root_helper to use for execute
:returns: True if supported, False otherwise
"""
return LVM.get_lvm_version(root_helper) >= (2, 2, 95)
@property
def supports_snapshot_lv_activation(self):
"""Property indicating whether snap activation changes are supported.
Check for LVM version >= 2.02.91.
(LVM2 git: e8a40f6 Allow to activate snapshot)
:returns: True/False indicating support
"""
if self._supports_snapshot_lv_activation is not None:
return self._supports_snapshot_lv_activation
self._supports_snapshot_lv_activation = (
self.get_lvm_version(self._root_helper) >= (2, 2, 91))
return self._supports_snapshot_lv_activation
@property
def supports_lvchange_ignoreskipactivation(self):
"""Property indicating whether lvchange can ignore skip activation.
Check for LVM version >= 2.02.99.
(LVM2 git: ab789c1bc add --ignoreactivationskip to lvchange)
"""
if self._supports_lvchange_ignoreskipactivation is not None:
return self._supports_lvchange_ignoreskipactivation
self._supports_lvchange_ignoreskipactivation = (
self.get_lvm_version(self._root_helper) >= (2, 2, 99))
return self._supports_lvchange_ignoreskipactivation
@staticmethod
def get_lv_info(root_helper, vg_name=None, lv_name=None):
"""Retrieve info about LVs (all, in a VG, or a single LV).
:param root_helper: root_helper to use for execute
:param vg_name: optional, gathers info for only the specified VG
:param lv_name: optional, gathers info for only the specified LV
:returns: List of Dictionaries with LV info
"""
cmd = LVM.LVM_CMD_PREFIX + ['lvs', '--noheadings', '--unit=g',
'-o', 'vg_name,name,size', '--nosuffix']
if lv_name is not None and vg_name is not None:
cmd.append("%s/%s" % (vg_name, lv_name))
elif vg_name is not None:
cmd.append(vg_name)
try:
(out, _err) = putils.execute(*cmd,
root_helper=root_helper,
run_as_root=True)
except putils.ProcessExecutionError as err:
with excutils.save_and_reraise_exception(reraise=True) as ctx:
if "not found" in err.stderr or "Failed to find" in err.stderr:
ctx.reraise = False
LOG.info(_LI("Logical Volume not found when querying "
"LVM info. (vg_name=%(vg)s, lv_name=%(lv)s"),
{'vg': vg_name, 'lv': lv_name})
out = None
lv_list = []
if out is not None:
volumes = out.split()
iterator = moves.zip(*[iter(volumes)] * 3) # pylint: disable=E1101
for vg, name, size in iterator:
lv_list.append({"vg": vg, "name": name, "size": size})
return lv_list
def get_volumes(self, lv_name=None):
"""Get all LV's associated with this instantiation (VG).
:returns: List of Dictionaries with LV info
"""
return self.get_lv_info(self._root_helper,
self.vg_name,
lv_name)
def get_volume(self, name):
"""Get reference object of volume specified by name.
:returns: dict representation of Logical Volume if exists
"""
ref_list = self.get_volumes(name)
for r in ref_list:
if r['name'] == name:
return r
return None
@staticmethod
def get_all_physical_volumes(root_helper, vg_name=None):
"""Static method to get all PVs on a system.
:param root_helper: root_helper to use for execute
:param vg_name: optional, gathers info for only the specified VG
:returns: List of Dictionaries with PV info
"""
field_sep = '|'
cmd = LVM.LVM_CMD_PREFIX + ['pvs', '--noheadings',
'--unit=g',
'-o', 'vg_name,name,size,free',
'--separator', field_sep,
'--nosuffix']
(out, _err) = putils.execute(*cmd,
root_helper=root_helper,
run_as_root=True)
pvs = out.split()
if vg_name is not None:
pvs = [pv for pv in pvs if vg_name == pv.split(field_sep)[0]]
pv_list = []
for pv in pvs:
fields = pv.split(field_sep)
pv_list.append({'vg': fields[0],
'name': fields[1],
'size': float(fields[2]),
'available': float(fields[3])})
return pv_list
def get_physical_volumes(self):
"""Get all PVs associated with this instantiation (VG).
:returns: List of Dictionaries with PV info
"""
self.pv_list = self.get_all_physical_volumes(self._root_helper,
self.vg_name)
return self.pv_list
@staticmethod
def get_all_volume_groups(root_helper, vg_name=None):
"""Static method to get all VGs on a system.
:param root_helper: root_helper to use for execute
:param vg_name: optional, gathers info for only the specified VG
:returns: List of Dictionaries with VG info
"""
cmd = LVM.LVM_CMD_PREFIX + ['vgs', '--noheadings',
'--unit=g', '-o',
'name,size,free,lv_count,uuid',
'--separator', ':',
'--nosuffix']
if vg_name is not None:
cmd.append(vg_name)
(out, _err) = putils.execute(*cmd,
root_helper=root_helper,
run_as_root=True)
vg_list = []
if out is not None:
vgs = out.split()
for vg in vgs:
fields = vg.split(':')
vg_list.append({'name': fields[0],
'size': float(fields[1]),
'available': float(fields[2]),
'lv_count': int(fields[3]),
'uuid': fields[4]})
return vg_list
def update_volume_group_info(self):
"""Update VG info for this instantiation.
Used to update member fields of object and
provide a dict of info for caller.
:returns: Dictionaries of VG info
"""
vg_list = self.get_all_volume_groups(self._root_helper, self.vg_name)
if len(vg_list) != 1:
LOG.error(_LE('Unable to find VG: %s'), self.vg_name)
raise exception.VolumeGroupNotFound(vg_name=self.vg_name)
self.vg_size = float(vg_list[0]['size'])
self.vg_free_space = float(vg_list[0]['available'])
self.vg_lv_count = int(vg_list[0]['lv_count'])
self.vg_uuid = vg_list[0]['uuid']
total_vols_size = 0.0
if self.vg_thin_pool is not None:
# NOTE(xyang): If providing only self.vg_name,
# get_lv_info will output info on the thin pool and all
# individual volumes.
# get_lv_info(self._root_helper, 'stack-vg')
# sudo lvs --noheadings --unit=g -o vg_name,name,size
# --nosuffix stack-vg
# stack-vg stack-pool 9.51
# stack-vg volume-13380d16-54c3-4979-9d22-172082dbc1a1 1.00
# stack-vg volume-629e13ab-7759-46a5-b155-ee1eb20ca892 1.00
# stack-vg volume-e3e6281c-51ee-464c-b1a7-db6c0854622c 1.00
#
# If providing both self.vg_name and self.vg_thin_pool,
# get_lv_info will output only info on the thin pool, but not
# individual volumes.
# get_lv_info(self._root_helper, 'stack-vg', 'stack-pool')
# sudo lvs --noheadings --unit=g -o vg_name,name,size
# --nosuffix stack-vg/stack-pool
# stack-vg stack-pool 9.51
#
# We need info on both the thin pool and the volumes,
# therefore we should provide only self.vg_name, but not
# self.vg_thin_pool here.
for lv in self.get_lv_info(self._root_helper,
self.vg_name):
lvsize = lv['size']
# get_lv_info runs "lvs" command with "--nosuffix".
# This removes "g" from "1.00g" and only outputs "1.00".
# Running "lvs" command without "--nosuffix" will output
# "1.00g" if "g" is the unit.
# Remove the unit if it is in lv['size'].
if not lv['size'][-1].isdigit():
lvsize = lvsize[:-1]
if lv['name'] == self.vg_thin_pool:
self.vg_thin_pool_size = lvsize
tpfs = self._get_thin_pool_free_space(self.vg_name,
self.vg_thin_pool)
self.vg_thin_pool_free_space = tpfs
else:
total_vols_size = total_vols_size + float(lvsize)
total_vols_size = round(total_vols_size, 2)
self.vg_provisioned_capacity = total_vols_size
def _calculate_thin_pool_size(self):
"""Calculates the correct size for a thin pool.
Ideally we would use 100% of the containing volume group and be done.
But the 100%VG notation to lvcreate is not implemented and thus cannot
be used. See https://bugzilla.redhat.com/show_bug.cgi?id=998347
Further, some amount of free space must remain in the volume group for
metadata for the contained logical volumes. The exact amount depends
on how much volume sharing you expect.
:returns: An lvcreate-ready string for the number of calculated bytes.
"""
# make sure volume group information is current
self.update_volume_group_info()
# leave 5% free for metadata
return "%sg" % (self.vg_free_space * 0.95)
def create_thin_pool(self, name=None, size_str=None):
"""Creates a thin provisioning pool for this VG.
The syntax here is slightly different than the default
lvcreate -T, so we'll just write a custom cmd here
and do it.
:param name: Name to use for pool, default is "<vg-name>-pool"
:param size_str: Size to allocate for pool, default is entire VG
:returns: The size string passed to the lvcreate command
"""
if not self.supports_thin_provisioning(self._root_helper):
LOG.error(_LE('Requested to setup thin provisioning, '
'however current LVM version does not '
'support it.'))
return None
if name is None:
name = '%s-pool' % self.vg_name
vg_pool_name = '%s/%s' % (self.vg_name, name)
if not size_str:
size_str = self._calculate_thin_pool_size()
cmd = LVM.LVM_CMD_PREFIX + ['lvcreate', '-T', '-L', size_str,
vg_pool_name]
LOG.debug("Creating thin pool '%(pool)s' with size %(size)s of "
"total %(free)sg", {'pool': vg_pool_name,
'size': size_str,
'free': self.vg_free_space})
self._execute(*cmd,
root_helper=self._root_helper,
run_as_root=True)
self.vg_thin_pool = name
return size_str
def create_volume(self, name, size_str, lv_type='default', mirror_count=0):
"""Creates a logical volume on the object's VG.
:param name: Name to use when creating Logical Volume
:param size_str: Size to use when creating Logical Volume
:param lv_type: Type of Volume (default or thin)
:param mirror_count: Use LVM mirroring with specified count
"""
if lv_type == 'thin':
pool_path = '%s/%s' % (self.vg_name, self.vg_thin_pool)
cmd = LVM.LVM_CMD_PREFIX + ['lvcreate', '-T', '-V', size_str, '-n',
name, pool_path]
else:
cmd = LVM.LVM_CMD_PREFIX + ['lvcreate', '-n', name, self.vg_name,
'-L', size_str]
if mirror_count > 0:
cmd.extend(['-m', mirror_count, '--nosync',
'--mirrorlog', 'mirrored'])
terras = int(size_str[:-1]) / 1024.0
if terras >= 1.5:
rsize = int(2 ** math.ceil(math.log(terras) / math.log(2)))
# NOTE(vish): Next power of two for region size. See:
# http://red.ht/U2BPOD
cmd.extend(['-R', str(rsize)])
try:
self._execute(*cmd,
root_helper=self._root_helper,
run_as_root=True)
except putils.ProcessExecutionError as err:
LOG.exception(_LE('Error creating Volume'))
LOG.error(_LE('Cmd :%s'), err.cmd)
LOG.error(_LE('StdOut :%s'), err.stdout)
LOG.error(_LE('StdErr :%s'), err.stderr)
raise
@utils.retry(putils.ProcessExecutionError)
def create_lv_snapshot(self, name, source_lv_name, lv_type='default'):
"""Creates a snapshot of a logical volume.
:param name: Name to assign to new snapshot
:param source_lv_name: Name of Logical Volume to snapshot
:param lv_type: Type of LV (default or thin)
"""
source_lvref = self.get_volume(source_lv_name)
if source_lvref is None:
LOG.error(_LE("Trying to create snapshot by non-existent LV: %s"),
source_lv_name)
raise exception.VolumeDeviceNotFound(device=source_lv_name)
cmd = LVM.LVM_CMD_PREFIX + ['lvcreate', '--name', name, '--snapshot',
'%s/%s' % (self.vg_name, source_lv_name)]
if lv_type != 'thin':
size = source_lvref['size']
cmd.extend(['-L', '%sg' % (size)])
try:
self._execute(*cmd,
root_helper=self._root_helper,
run_as_root=True)
except putils.ProcessExecutionError as err:
LOG.exception(_LE('Error creating snapshot'))
LOG.error(_LE('Cmd :%s'), err.cmd)
LOG.error(_LE('StdOut :%s'), err.stdout)
LOG.error(_LE('StdErr :%s'), err.stderr)
raise
def _mangle_lv_name(self, name):
# Linux LVM reserves name that starts with snapshot, so that
# such volume name can't be created. Mangle it.
if not name.startswith('snapshot'):
return name
return '_' + name
def deactivate_lv(self, name):
lv_path = self.vg_name + '/' + self._mangle_lv_name(name)
cmd = ['lvchange', '-a', 'n']
cmd.append(lv_path)
try:
self._execute(*cmd,
root_helper=self._root_helper,
run_as_root=True)
except putils.ProcessExecutionError as err:
LOG.exception(_LE('Error deactivating LV'))
LOG.error(_LE('Cmd :%s'), err.cmd)
LOG.error(_LE('StdOut :%s'), err.stdout)
LOG.error(_LE('StdErr :%s'), err.stderr)
raise
def activate_lv(self, name, is_snapshot=False, permanent=False):
"""Ensure that logical volume/snapshot logical volume is activated.
:param name: Name of LV to activate
:param is_snapshot: whether LV is a snapshot
:param permanent: whether we should drop skipactivation flag
:raises: putils.ProcessExecutionError
"""
# This is a no-op if requested for a snapshot on a version
# of LVM that doesn't support snapshot activation.
# (Assume snapshot LV is always active.)
if is_snapshot and not self.supports_snapshot_lv_activation:
return
lv_path = self.vg_name + '/' + self._mangle_lv_name(name)
# Must pass --yes to activate both the snap LV and its origin LV.
# Otherwise lvchange asks if you would like to do this interactively,
# and fails.
cmd = ['lvchange', '-a', 'y', '--yes']
if self.supports_lvchange_ignoreskipactivation:
cmd.append('-K')
# If permanent=True is specified, drop the skipactivation flag in
# order to make this LV automatically activated after next reboot.
if permanent:
cmd += ['-k', 'n']
cmd.append(lv_path)
try:
self._execute(*cmd,
root_helper=self._root_helper,
run_as_root=True)
except putils.ProcessExecutionError as err:
LOG.exception(_LE('Error activating LV'))
LOG.error(_LE('Cmd :%s'), err.cmd)
LOG.error(_LE('StdOut :%s'), err.stdout)
LOG.error(_LE('StdErr :%s'), err.stderr)
raise
@utils.retry(putils.ProcessExecutionError)
def delete(self, name):
"""Delete logical volume or snapshot.
:param name: Name of LV to delete
"""
def run_udevadm_settle():
self._execute('udevadm', 'settle',
root_helper=self._root_helper, run_as_root=True,
check_exit_code=False)
# LV removal seems to be a race with other writers or udev in
# some cases (see LP #1270192), so we enable retry deactivation
LVM_CONFIG = 'activation { retry_deactivation = 1} '
try:
self._execute(
'lvremove',
'--config', LVM_CONFIG,
'-f',
'%s/%s' % (self.vg_name, name),
root_helper=self._root_helper, run_as_root=True)
except putils.ProcessExecutionError as err:
LOG.debug('Error reported running lvremove: CMD: %(command)s, '
'RESPONSE: %(response)s',
{'command': err.cmd, 'response': err.stderr})
LOG.debug('Attempting udev settle and retry of lvremove...')
run_udevadm_settle()
# The previous failing lvremove -f might leave behind
# suspended devices; when lvmetad is not available, any
# further lvm command will block forever.
# Therefore we need to skip suspended devices on retry.
LVM_CONFIG += 'devices { ignore_suspended_devices = 1}'
self._execute(
'lvremove',
'--config', LVM_CONFIG,
'-f',
'%s/%s' % (self.vg_name, name),
root_helper=self._root_helper, run_as_root=True)
LOG.debug('Successfully deleted volume: %s after '
'udev settle.', name)
def revert(self, snapshot_name):
"""Revert an LV from snapshot.
:param snapshot_name: Name of snapshot to revert
"""
self._execute('lvconvert', '--merge',
snapshot_name, root_helper=self._root_helper,
run_as_root=True)
def lv_has_snapshot(self, name):
cmd = LVM.LVM_CMD_PREFIX + ['lvdisplay', '--noheading', '-C', '-o',
'Attr', '%s/%s' % (self.vg_name, name)]
out, _err = self._execute(*cmd,
root_helper=self._root_helper,
run_as_root=True)
if out:
out = out.strip()
if (out[0] == 'o') or (out[0] == 'O'):
return True
return False
def extend_volume(self, lv_name, new_size):
"""Extend the size of an existing volume."""
# Volumes with snaps have attributes 'o' or 'O' and will be
# deactivated, but Thin Volumes with snaps have attribute 'V'
# and won't be deactivated because the lv_has_snapshot method looks
# for 'o' or 'O'
if self.lv_has_snapshot(lv_name):
self.deactivate_lv(lv_name)
try:
cmd = LVM.LVM_CMD_PREFIX + ['lvextend', '-L', new_size,
'%s/%s' % (self.vg_name, lv_name)]
self._execute(*cmd, root_helper=self._root_helper,
run_as_root=True)
except putils.ProcessExecutionError as err:
LOG.exception(_LE('Error extending Volume'))
LOG.error(_LE('Cmd :%s'), err.cmd)
LOG.error(_LE('StdOut :%s'), err.stdout)
LOG.error(_LE('StdErr :%s'), err.stderr)
raise
def vg_mirror_free_space(self, mirror_count):
free_capacity = 0.0
disks = []
for pv in self.pv_list:
disks.append(float(pv['available']))
while True:
disks = sorted([a for a in disks if a > 0.0], reverse=True)
if len(disks) <= mirror_count:
break
# consume the smallest disk
disk = disks[-1]
disks = disks[:-1]
# match extents for each mirror on the largest disks
for index in list(range(mirror_count)):
disks[index] -= disk
free_capacity += disk
return free_capacity
def vg_mirror_size(self, mirror_count):
return (self.vg_free_space / (mirror_count + 1))
def rename_volume(self, lv_name, new_name):
"""Change the name of an existing volume."""
try:
self._execute('lvrename', self.vg_name, lv_name, new_name,
root_helper=self._root_helper,
run_as_root=True)
except putils.ProcessExecutionError as err:
LOG.exception(_LE('Error renaming logical volume'))
LOG.error(_LE('Cmd :%s'), err.cmd)
LOG.error(_LE('StdOut :%s'), err.stdout)
LOG.error(_LE('StdErr :%s'), err.stderr)
raise

View File

@ -175,6 +175,9 @@ class ConnectorTestCase(base.TestCase):
'quobyte', None, quobyte_mount_point_base='/mnt/test')
self.assertEqual(obj.__class__.__name__, "RemoteFsConnector")
obj = connector.InitiatorConnector.factory("disco", None)
self.assertEqual("DISCOConnector", obj.__class__.__name__)
self.assertRaises(ValueError,
connector.InitiatorConnector.factory,
"bogus", None)
@ -334,6 +337,33 @@ class ISCSIConnectorTestCase(ConnectorTestCase):
connection_properties['data'])
self.assertEqual(expected, volume_paths)
@mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device_path')
@mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device')
def test_discover_mpath_device(self, mock_multipath_device,
mock_multipath_device_path):
location1 = '10.0.2.15:3260'
location2 = '[2001:db8::1]:3260'
name1 = 'volume-00000001-1'
name2 = 'volume-00000001-2'
iqn1 = 'iqn.2010-10.org.openstack:%s' % name1
iqn2 = 'iqn.2010-10.org.openstack:%s' % name2
fake_multipath_dev = '/dev/mapper/fake-multipath-dev'
fake_raw_dev = '/dev/disk/by-path/fake-raw-lun'
vol = {'id': 1, 'name': name1}
connection_properties = self.iscsi_connection_multipath(
vol, [location1, location2], [iqn1, iqn2], [1, 2])
mock_multipath_device_path.return_value = fake_multipath_dev
mock_multipath_device.return_value = FAKE_SCSI_WWN
(result_path, result_mpath_id) = (
self.connector_with_multipath._discover_mpath_device(
FAKE_SCSI_WWN,
connection_properties['data'],
fake_raw_dev))
result = {'path': result_path, 'multipath_id': result_mpath_id}
expected_result = {'path': fake_multipath_dev,
'multipath_id': FAKE_SCSI_WWN}
self.assertEqual(expected_result, result)
def _test_connect_volume(self, extra_props, additional_commands,
transport=None, disconnect_mock=None):
# for making sure the /dev/disk/by-path is gone
@ -388,6 +418,7 @@ class ISCSIConnectorTestCase(ConnectorTestCase):
('iscsiadm -m node -T %s -p %s --login' % (iqn, location)),
('iscsiadm -m node -T %s -p %s --op update'
' -n node.startup -v automatic' % (iqn, location)),
('scsi_id --page 0x83 --whitelisted %s' % dev_str),
('blockdev --flushbufs /dev/sdb'),
('tee -a /sys/block/sdb/device/delete'),
('iscsiadm -m node -T %s -p %s --op update'
@ -509,16 +540,19 @@ class ISCSIConnectorTestCase(ConnectorTestCase):
@mock.patch.object(connector.ISCSIConnector, '_rescan_multipath')
@mock.patch.object(connector.ISCSIConnector, '_get_multipath_device_name')
@mock.patch.object(os.path, 'exists', return_value=True)
@mock.patch.object(connector.InitiatorConnector, '_discover_mpath_device')
def test_connect_volume_with_multipath(
self, exists_mock, get_device_mock, rescan_multipath_mock,
rescan_iscsi_mock, connect_to_mock, portals_mock, iscsiadm_mock,
mock_iscsi_wwn):
self, mock_discover_mpath_device, exists_mock, get_device_mock,
rescan_multipath_mock, rescan_iscsi_mock, connect_to_mock,
portals_mock, iscsiadm_mock, mock_iscsi_wwn):
mock_iscsi_wwn.return_value = FAKE_SCSI_WWN
location = '10.0.2.15:3260'
name = 'volume-00000001'
iqn = 'iqn.2010-10.org.openstack:%s' % name
vol = {'id': 1, 'name': name}
connection_properties = self.iscsi_connection(vol, location, iqn)
mock_discover_mpath_device.return_value = (
'iqn.2010-10.org.openstack:%s' % name, FAKE_SCSI_WWN)
self.connector_with_multipath = \
connector.ISCSIConnector(None, use_multipath=True)
@ -528,10 +562,11 @@ class ISCSIConnectorTestCase(ConnectorTestCase):
result = self.connector_with_multipath.connect_volume(
connection_properties['data'])
expected_result = {'path': 'iqn.2010-10.org.openstack:volume-00000001',
expected_result = {'multipath_id': FAKE_SCSI_WWN,
'path': 'iqn.2010-10.org.openstack:volume-00000001',
'type': 'block',
'scsi_wwn': FAKE_SCSI_WWN}
self.assertEqual(result, expected_result)
self.assertEqual(expected_result, result)
@mock.patch.object(connector.ISCSIConnector,
'_run_iscsiadm_update_discoverydb')
@ -585,14 +620,16 @@ class ISCSIConnectorTestCase(ConnectorTestCase):
@mock.patch.object(connector.ISCSIConnector, '_get_multipath_device_map',
return_value={})
@mock.patch.object(connector.ISCSIConnector, '_get_iscsi_devices')
@mock.patch.object(connector.ISCSIConnector, '_rescan_multipath')
@mock.patch.object(connector.ISCSIConnector, '_run_multipath')
@mock.patch.object(connector.ISCSIConnector, '_get_multipath_device_name')
@mock.patch.object(connector.ISCSIConnector, '_get_multipath_iqns')
@mock.patch.object(connector.InitiatorConnector, '_discover_mpath_device')
@mock.patch.object(linuxscsi.LinuxSCSI, 'process_lun_id')
def test_connect_volume_with_multiple_portals(
self, mock_get_iqn, mock_device_name, mock_run_multipath,
mock_rescan_multipath, mock_iscsi_devices,
mock_get_device_map, mock_devices, mock_exists, mock_scsi_wwn):
self, mock_process_lun_id, mock_discover_mpath_device,
mock_get_iqn, mock_device_name, mock_run_multipath,
mock_iscsi_devices, mock_get_device_map, mock_devices,
mock_exists, mock_scsi_wwn):
mock_scsi_wwn.return_value = FAKE_SCSI_WWN
location1 = '10.0.2.15:3260'
location2 = '[2001:db8::1]:3260'
@ -609,12 +646,15 @@ class ISCSIConnectorTestCase(ConnectorTestCase):
'/dev/disk/by-path/ip-%s-iscsi-%s-lun-2' % (dev_loc2, iqn2)]
mock_devices.return_value = devs
mock_iscsi_devices.return_value = devs
mock_device_name.return_value = fake_multipath_dev
mock_get_iqn.return_value = [iqn1, iqn2]
mock_discover_mpath_device.return_value = (fake_multipath_dev,
FAKE_SCSI_WWN)
mock_process_lun_id.return_value = [1, 2]
result = self.connector_with_multipath.connect_volume(
connection_properties['data'])
expected_result = {'path': fake_multipath_dev, 'type': 'block',
expected_result = {'multipath_id': FAKE_SCSI_WWN,
'path': fake_multipath_dev, 'type': 'block',
'scsi_wwn': FAKE_SCSI_WWN}
cmd_format = 'iscsiadm -m node -T %s -p %s --%s'
expected_commands = [cmd_format % (iqn1, location1, 'login'),
@ -622,7 +662,6 @@ class ISCSIConnectorTestCase(ConnectorTestCase):
self.assertEqual(expected_result, result)
for command in expected_commands:
self.assertIn(command, self.cmds)
mock_device_name.assert_called_once_with(devs[0])
self.cmds = []
self.connector_with_multipath.disconnect_volume(
@ -643,9 +682,12 @@ class ISCSIConnectorTestCase(ConnectorTestCase):
@mock.patch.object(connector.ISCSIConnector, '_get_multipath_device_name')
@mock.patch.object(connector.ISCSIConnector, '_get_multipath_iqns')
@mock.patch.object(connector.ISCSIConnector, '_run_iscsiadm')
@mock.patch.object(connector.InitiatorConnector, '_discover_mpath_device')
@mock.patch.object(linuxscsi.LinuxSCSI, 'process_lun_id')
def test_connect_volume_with_multiple_portals_primary_error(
self, mock_iscsiadm, mock_get_iqn, mock_device_name,
mock_run_multipath, mock_rescan_multipath, mock_iscsi_devices,
self, mock_process_lun_id, mock_discover_mpath_device,
mock_iscsiadm, mock_get_iqn, mock_device_name, mock_run_multipath,
mock_rescan_multipath, mock_iscsi_devices,
mock_get_multipath_device_map, mock_devices, mock_exists,
mock_scsi_wwn):
mock_scsi_wwn.return_value = FAKE_SCSI_WWN
@ -676,14 +718,18 @@ class ISCSIConnectorTestCase(ConnectorTestCase):
mock_get_iqn.return_value = [iqn2]
mock_iscsiadm.side_effect = fake_run_iscsiadm
mock_discover_mpath_device.return_value = (fake_multipath_dev,
FAKE_SCSI_WWN)
mock_process_lun_id.return_value = [1, 2]
props = connection_properties['data'].copy()
result = self.connector_with_multipath.connect_volume(
connection_properties['data'])
expected_result = {'path': fake_multipath_dev, 'type': 'block',
expected_result = {'multipath_id': FAKE_SCSI_WWN,
'path': fake_multipath_dev, 'type': 'block',
'scsi_wwn': FAKE_SCSI_WWN}
self.assertEqual(expected_result, result)
mock_device_name.assert_called_once_with(dev2)
props['target_portal'] = location1
props['target_iqn'] = iqn1
mock_iscsiadm.assert_any_call(props, ('--login',),
@ -717,10 +763,12 @@ class ISCSIConnectorTestCase(ConnectorTestCase):
@mock.patch.object(connector.ISCSIConnector, '_rescan_multipath')
@mock.patch.object(connector.ISCSIConnector, '_run_multipath')
@mock.patch.object(connector.ISCSIConnector, '_get_multipath_device_name')
@mock.patch.object(connector.InitiatorConnector, '_discover_mpath_device')
def test_connect_volume_with_multipath_connecting(
self, mock_device_name, mock_run_multipath,
mock_rescan_multipath, mock_iscsi_devices, mock_devices,
mock_connect, mock_portals, mock_exists, mock_scsi_wwn):
self, mock_discover_mpath_device, mock_device_name,
mock_run_multipath, mock_rescan_multipath, mock_iscsi_devices,
mock_devices, mock_connect, mock_portals, mock_exists,
mock_scsi_wwn):
mock_scsi_wwn.return_value = FAKE_SCSI_WWN
location1 = '10.0.2.15:3260'
location2 = '[2001:db8::1]:3260'
@ -739,10 +787,13 @@ class ISCSIConnectorTestCase(ConnectorTestCase):
mock_device_name.return_value = fake_multipath_dev
mock_portals.return_value = [[location1, iqn1], [location2, iqn1],
[location2, iqn2]]
mock_discover_mpath_device.return_value = (fake_multipath_dev,
FAKE_SCSI_WWN)
result = self.connector_with_multipath.connect_volume(
connection_properties['data'])
expected_result = {'path': fake_multipath_dev, 'type': 'block',
expected_result = {'multipath_id': FAKE_SCSI_WWN,
'path': fake_multipath_dev, 'type': 'block',
'scsi_wwn': FAKE_SCSI_WWN}
props1 = connection_properties['data'].copy()
props2 = connection_properties['data'].copy()
@ -1066,6 +1117,31 @@ Setting up iSCSI targets: unused
# our stub method is called which asserts the password is scrubbed
self.assertTrue(debug_mock.called)
@mock.patch.object(connector.ISCSIConnector, 'get_volume_paths')
def test_extend_volume_no_path(self, mock_volume_paths):
mock_volume_paths.return_value = []
volume = {'id': 'fake_uuid'}
connection_info = self.iscsi_connection(volume,
"10.0.2.15:3260",
"fake_iqn")
self.assertRaises(exception.VolumePathsNotFound,
self.connector.extend_volume,
connection_info['data'])
@mock.patch.object(linuxscsi.LinuxSCSI, 'extend_volume')
@mock.patch.object(connector.ISCSIConnector, 'get_volume_paths')
def test_extend_volume(self, mock_volume_paths, mock_scsi_extend):
fake_new_size = 1024
mock_volume_paths.return_value = ['/dev/vdx']
mock_scsi_extend.return_value = fake_new_size
volume = {'id': 'fake_uuid'}
connection_info = self.iscsi_connection(volume,
"10.0.2.15:3260",
"fake_iqn")
new_size = self.connector.extend_volume(connection_info['data'])
self.assertEqual(fake_new_size, new_size)
class FibreChannelConnectorTestCase(ConnectorTestCase):
def setUp(self):
@ -1262,6 +1338,7 @@ class FibreChannelConnectorTestCase(ConnectorTestCase):
self.connector.connect_volume(connection_info['data'])
self.assertEqual(should_wait_for_rw, wait_for_rw_mock.called)
return connection_info
@mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device')
@mock.patch.object(linuxscsi.LinuxSCSI, 'wait_for_rw')
@ -1357,6 +1434,65 @@ class FibreChannelConnectorTestCase(ConnectorTestCase):
'ro',
False)
@mock.patch.object(connector.InitiatorConnector, '_discover_mpath_device')
@mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device')
@mock.patch.object(linuxscsi.LinuxSCSI, 'wait_for_rw')
@mock.patch.object(os.path, 'exists', return_value=True)
@mock.patch.object(os.path, 'realpath', return_value='/dev/sdb')
@mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas')
@mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas_info')
@mock.patch.object(linuxscsi.LinuxSCSI, 'remove_scsi_device')
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn')
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_device_info')
def test_connect_volume_multipath_not_found(self,
get_device_info_mock,
get_scsi_wwn_mock,
remove_device_mock,
get_fc_hbas_info_mock,
get_fc_hbas_mock,
realpath_mock,
exists_mock,
wait_for_rw_mock,
find_mp_dev_mock,
discover_mp_dev_mock):
discover_mp_dev_mock.return_value = ("/dev/disk/by-path/something",
None)
connection_info = self._test_connect_volume_multipath(
get_device_info_mock, get_scsi_wwn_mock, remove_device_mock,
get_fc_hbas_info_mock, get_fc_hbas_mock, realpath_mock,
exists_mock, wait_for_rw_mock, find_mp_dev_mock,
'rw', False)
self.assertNotIn('multipathd_id', connection_info['data'])
@mock.patch.object(connector.FibreChannelConnector, 'get_volume_paths')
def test_extend_volume_no_path(self, mock_volume_paths):
mock_volume_paths.return_value = []
volume = {'id': 'fake_uuid'}
wwn = '1234567890123456'
connection_info = self.fibrechan_connection(volume,
"10.0.2.15:3260",
wwn)
self.assertRaises(exception.VolumePathsNotFound,
self.connector.extend_volume,
connection_info['data'])
@mock.patch.object(linuxscsi.LinuxSCSI, 'extend_volume')
@mock.patch.object(connector.FibreChannelConnector, 'get_volume_paths')
def test_extend_volume(self, mock_volume_paths, mock_scsi_extend):
fake_new_size = 1024
mock_volume_paths.return_value = ['/dev/vdx']
mock_scsi_extend.return_value = fake_new_size
volume = {'id': 'fake_uuid'}
wwn = '1234567890123456'
connection_info = self.fibrechan_connection(volume,
"10.0.2.15:3260",
wwn)
new_size = self.connector.extend_volume(connection_info['data'])
self.assertEqual(fake_new_size, new_size)
class FibreChannelConnectorS390XTestCase(ConnectorTestCase):
@ -1506,6 +1642,11 @@ class AoEConnectorTestCase(ConnectorTestCase):
return_value=["", ""]):
self.connector.disconnect_volume(self.connection_properties, {})
def test_extend_volume(self):
self.assertRaises(NotImplementedError,
self.connector.extend_volume,
self.connection_properties)
class RemoteFsConnectorTestCase(ConnectorTestCase):
"""Test cases for Remote FS initiator class."""
@ -1548,6 +1689,11 @@ class RemoteFsConnectorTestCase(ConnectorTestCase):
"""Nothing should happen here -- make sure it doesn't blow up."""
self.connector.disconnect_volume(self.connection_properties, {})
def test_extend_volume(self):
self.assertRaises(NotImplementedError,
self.connector.extend_volume,
self.connection_properties)
class LocalConnectorTestCase(ConnectorTestCase):
@ -1555,32 +1701,34 @@ class LocalConnectorTestCase(ConnectorTestCase):
super(LocalConnectorTestCase, self).setUp()
self.connection_properties = {'name': 'foo',
'device_path': '/tmp/bar'}
self.connector = connector.LocalConnector(None)
def test_get_search_path(self):
self.connector = connector.LocalConnector(None)
actual = self.connector.get_search_path()
self.assertIsNone(actual)
def test_get_volume_paths(self):
self.connector = connector.LocalConnector(None)
expected = [self.connection_properties['device_path']]
actual = self.connector.get_volume_paths(
self.connection_properties)
self.assertEqual(expected, actual)
def test_connect_volume(self):
self.connector = connector.LocalConnector(None)
cprops = self.connection_properties
dev_info = self.connector.connect_volume(cprops)
self.assertEqual(dev_info['type'], 'local')
self.assertEqual(dev_info['path'], cprops['device_path'])
def test_connect_volume_with_invalid_connection_data(self):
self.connector = connector.LocalConnector(None)
cprops = {}
self.assertRaises(ValueError,
self.connector.connect_volume, cprops)
def test_extend_volume(self):
self.assertRaises(NotImplementedError,
self.connector.extend_volume,
self.connection_properties)
class HuaweiStorHyperConnectorTestCase(ConnectorTestCase):
"""Test cases for StorHyper initiator class."""
@ -1794,6 +1942,11 @@ class HuaweiStorHyperConnectorTestCase(ConnectorTestCase):
LOG.debug("self.cmds = %s." % self.cmds)
LOG.debug("expected = %s." % expected_commands)
def test_extend_volume(self):
self.assertRaises(NotImplementedError,
self.connector.extend_volume,
self.connection_properties)
class HGSTConnectorTestCase(ConnectorTestCase):
"""Test cases for HGST initiator class."""
@ -1978,6 +2131,12 @@ Request Succeeded
self.connector.disconnect_volume,
cprops, None)
def test_extend_volume(self):
cprops = {'name': 'space', 'noremovehost': 'stor1'}
self.assertRaises(NotImplementedError,
self.connector.extend_volume,
cprops)
class RBDConnectorTestCase(ConnectorTestCase):
@ -2044,6 +2203,12 @@ class RBDConnectorTestCase(ConnectorTestCase):
self.assertEqual(1, volume_close.call_count)
def test_extend_volume(self):
rbd = connector.RBDConnector(None)
self.assertRaises(NotImplementedError,
rbd.extend_volume,
self.connection_properties)
class DRBDConnectorTestCase(ConnectorTestCase):
@ -2104,6 +2269,12 @@ class DRBDConnectorTestCase(ConnectorTestCase):
self.assertEqual('down', self.execs[0][1])
def test_extend_volume(self):
cprop = {'name': 'something'}
self.assertRaises(NotImplementedError,
self.connector.extend_volume,
cprop)
class ScaleIOConnectorTestCase(ConnectorTestCase):
"""Test cases for ScaleIO connector"""
@ -2337,3 +2508,137 @@ class ScaleIOConnectorTestCase(ConnectorTestCase):
message='Test error map volume'), 500)
self.test_disconnect_volume()
def test_extend_volume(self):
self.assertRaises(NotImplementedError,
self.connector.extend_volume,
self.fake_connection_properties)
class DISCOConnectorTestCase(ConnectorTestCase):
"""Test cases for DISCO connector."""
# Fake volume information
volume = {
'name': 'a-disco-volume',
'disco_id': '1234567'
}
# Conf for test
conf = {
'ip': MY_IP,
'port': 9898
}
def setUp(self):
super(DISCOConnectorTestCase, self).setUp()
self.fake_connection_properties = {
'name': self.volume['name'],
'disco_id': self.volume['disco_id'],
'conf': {
'server_ip': self.conf['ip'],
'server_port': self.conf['port']}
}
self.fake_volume_status = {'attached': True,
'detached': False}
self.fake_request_status = {'success': None,
'fail': 'ERROR'}
self.volume_status = 'detached'
self.request_status = 'success'
# Patch the request and os calls to fake versions
mock.patch.object(connector.DISCOConnector,
'_send_disco_vol_cmd',
self.perform_disco_request).start()
mock.patch.object(os.path,
'exists', self.is_volume_attached).start()
mock.patch.object(glob,
'glob', self.list_disco_volume).start()
self.addCleanup(mock.patch.stopall)
# The actual DISCO connector
self.connector = connector.DISCOConnector(
'sudo', execute=self.fake_execute)
def perform_disco_request(self, *cmd, **kwargs):
"""Fake the socket call."""
return self.fake_request_status[self.request_status]
def is_volume_attached(self, *cmd, **kwargs):
"""Fake volume detection check."""
return self.fake_volume_status[self.volume_status]
def list_disco_volume(self, *cmd, **kwargs):
"""Fake the glob call."""
path_dir = self.connector.get_search_path()
volume_id = self.volume['disco_id']
volume_items = [path_dir, '/', self.connector.DISCO_PREFIX, volume_id]
volume_path = ''.join(volume_items)
return [volume_path]
def test_get_search_path(self):
"""DISCO volumes should be under /dev."""
expected = "/dev"
actual = self.connector.get_search_path()
self.assertEqual(expected, actual)
def test_get_volume_paths(self):
"""Test to get all the path for a specific volume."""
expected = ['/dev/dms1234567']
self.volume_status = 'attached'
actual = self.connector.get_volume_paths(
self.fake_connection_properties)
self.assertEqual(expected, actual)
def test_connect_volume(self):
"""Attach a volume."""
self.connector.connect_volume(self.fake_connection_properties)
def test_connect_volume_already_attached(self):
"""Make sure that we don't issue the request."""
self.request_status = 'fail'
self.volume_status = 'attached'
self.test_connect_volume()
def test_connect_volume_request_fail(self):
"""Fail the attach request."""
self.volume_status = 'detached'
self.request_status = 'fail'
self.assertRaises(exception.BrickException,
self.test_connect_volume)
def test_disconnect_volume(self):
"""Detach a volume."""
self.connector.disconnect_volume(self.fake_connection_properties, None)
def test_disconnect_volume_attached(self):
"""Detach a volume attached."""
self.request_status = 'success'
self.volume_status = 'attached'
self.test_disconnect_volume()
def test_disconnect_volume_already_detached(self):
"""Ensure that we don't issue the request."""
self.request_status = 'fail'
self.volume_status = 'detached'
self.test_disconnect_volume()
def test_disconnect_volume_request_fail(self):
"""Fail the detach request."""
self.volume_status = 'attached'
self.request_status = 'fail'
self.assertRaises(exception.BrickException,
self.test_disconnect_volume)
def test_get_all_available_volumes(self):
"""Test to get all the available DISCO volumes."""
expected = ['/dev/dms1234567']
actual = self.connector.get_all_available_volumes(None)
self.assertItemsEqual(expected, actual)
def test_extend_volume(self):
self.assertRaises(NotImplementedError,
self.connector.extend_volume,
self.fake_connection_properties)

View File

@ -491,3 +491,165 @@ loop0 0"""
self.assertEqual("1", info['devices'][1]['channel'])
self.assertEqual("0", info['devices'][1]['id'])
self.assertEqual("3", info['devices'][1]['lun'])
def test_get_device_size(self):
mock_execute = mock.Mock()
self.linuxscsi._execute = mock_execute
size = '1024'
mock_execute.return_value = (size, None)
ret_size = self.linuxscsi.get_device_size('/dev/fake')
self.assertEqual(int(size), ret_size)
size = 'junk'
mock_execute.return_value = (size, None)
ret_size = self.linuxscsi.get_device_size('/dev/fake')
self.assertEqual(None, ret_size)
size_bad = '1024\n'
size_good = 1024
mock_execute.return_value = (size_bad, None)
ret_size = self.linuxscsi.get_device_size('/dev/fake')
self.assertEqual(size_good, ret_size)
def test_multipath_reconfigure(self):
self.linuxscsi.multipath_reconfigure()
expected_commands = ['multipathd reconfigure']
self.assertEqual(expected_commands, self.cmds)
def test_multipath_resize_map(self):
wwn = '1234567890123456'
self.linuxscsi.multipath_resize_map(wwn)
expected_commands = ['multipathd resize map %s' % wwn]
self.assertEqual(expected_commands, self.cmds)
@mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device_path')
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn')
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_device_size')
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_device_info')
def test_extend_volume_no_mpath(self, mock_device_info,
mock_device_size,
mock_scsi_wwn,
mock_find_mpath_path):
"""Test extending a volume where there is no multipath device."""
fake_device = {'host': '0',
'channel': '0',
'id': '0',
'lun': '1'}
mock_device_info.return_value = fake_device
first_size = 1024
second_size = 2048
mock_device_size.side_effect = [first_size, second_size]
wwn = '1234567890123456'
mock_scsi_wwn.return_value = wwn
mock_find_mpath_path.return_value = None
ret_size = self.linuxscsi.extend_volume('/dev/fake')
self.assertEqual(second_size, ret_size)
# because we don't mock out the echo_scsi_command
expected_cmds = ['tee -a /sys/bus/scsi/drivers/sd/0:0:0:1/rescan']
self.assertEqual(expected_cmds, self.cmds)
@mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device_path')
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn')
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_device_size')
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_device_info')
def test_extend_volume_with_mpath(self, mock_device_info,
mock_device_size,
mock_scsi_wwn,
mock_find_mpath_path):
"""Test extending a volume where there is a multipath device."""
fake_device = {'host': '0',
'channel': '0',
'id': '0',
'lun': '1'}
mock_device_info.return_value = fake_device
first_size = 1024
second_size = 2048
third_size = 1024
fourth_size = 2048
mock_device_size.side_effect = [first_size, second_size,
third_size, fourth_size]
wwn = '1234567890123456'
mock_scsi_wwn.return_value = wwn
mock_find_mpath_path.return_value = ('/dev/mapper/dm-uuid-mpath-%s' %
wwn)
ret_size = self.linuxscsi.extend_volume('/dev/fake')
self.assertEqual(fourth_size, ret_size)
# because we don't mock out the echo_scsi_command
expected_cmds = ['tee -a /sys/bus/scsi/drivers/sd/0:0:0:1/rescan',
'multipathd reconfigure',
'multipathd resize map %s' % wwn]
self.assertEqual(expected_cmds, self.cmds)
@mock.patch.object(linuxscsi.LinuxSCSI, 'multipath_resize_map')
@mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device_path')
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn')
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_device_size')
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_device_info')
def test_extend_volume_with_mpath_fail(self, mock_device_info,
mock_device_size,
mock_scsi_wwn,
mock_find_mpath_path,
mock_mpath_resize_map):
"""Test extending a volume where there is a multipath device fail."""
fake_device = {'host': '0',
'channel': '0',
'id': '0',
'lun': '1'}
mock_device_info.return_value = fake_device
first_size = 1024
second_size = 2048
third_size = 1024
fourth_size = 2048
mock_device_size.side_effect = [first_size, second_size,
third_size, fourth_size]
wwn = '1234567890123456'
mock_scsi_wwn.return_value = wwn
mock_find_mpath_path.return_value = ('/dev/mapper/dm-uuid-mpath-%s' %
wwn)
mock_mpath_resize_map.return_value = 'fail'
ret_size = self.linuxscsi.extend_volume('/dev/fake')
self.assertIsNone(ret_size)
# because we don't mock out the echo_scsi_command
expected_cmds = ['tee -a /sys/bus/scsi/drivers/sd/0:0:0:1/rescan',
'multipathd reconfigure']
self.assertEqual(expected_cmds, self.cmds)
def test_process_lun_id_list(self):
lun_list = [2, 255, 88, 370, 5, 256]
result = self.linuxscsi.process_lun_id(lun_list)
expected = [2, 255, 88, '0x0172000000000000',
5, '0x0100000000000000']
self.assertEqual(expected, result)
def test_process_lun_id_single_val_make_hex(self):
lun_id = 499
result = self.linuxscsi.process_lun_id(lun_id)
expected = '0x01f3000000000000'
self.assertEqual(expected, result)
def test_process_lun_id_single_val_make_hex_border_case(self):
lun_id = 256
result = self.linuxscsi.process_lun_id(lun_id)
expected = '0x0100000000000000'
self.assertEqual(expected, result)
def test_process_lun_id_single_var_return(self):
lun_id = 13
result = self.linuxscsi.process_lun_id(lun_id)
expected = 13
self.assertEqual(expected, result)

View File

View File

@ -0,0 +1,63 @@
# 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.
class FakeBrickLVM(object):
"""Logs and records calls, for unit tests."""
def __init__(self, vg_name, create, pv_list, vtype, execute=None):
super(FakeBrickLVM, self).__init__()
self.vg_size = '5.00'
self.vg_free_space = '5.00'
self.vg_name = vg_name
def supports_thin_provisioning():
return False
def get_volumes(self):
return ['fake-volume']
def get_volume(self, name):
return ['name']
def get_all_physical_volumes(vg_name=None):
return []
def get_physical_volumes(self):
return []
def update_volume_group_info(self):
pass
def create_thin_pool(self, name=None, size_str=0):
pass
def create_volume(self, name, size_str, lv_type='default', mirror_count=0):
pass
def create_lv_snapshot(self, name, source_lv_name, lv_type='default'):
pass
def delete(self, name):
pass
def revert(self, snapshot_name):
pass
def lv_has_snapshot(self, name):
return False
def activate_lv(self, lv, is_snapshot=False, permanent=False):
pass
def rename_volume(self, lv_name, new_name):
pass

View File

@ -0,0 +1,365 @@
# Copyright 2012 OpenStack Foundation
# All Rights Reserved.
#
# 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 mock
from oslo_concurrency import processutils
from os_brick import exception
from os_brick.local_dev import lvm as brick
from os_brick.tests import base
class BrickLvmTestCase(base.TestCase):
def setUp(self):
super(BrickLvmTestCase, self).setUp()
self.volume_group_name = 'fake-vg'
# Stub processutils.execute for static methods
self.mock_object(processutils, 'execute',
self.fake_execute)
self.vg = brick.LVM(self.volume_group_name,
'sudo',
create_vg=False,
physical_volumes=None,
lvm_type='default',
executor=self.fake_execute)
def failed_fake_execute(obj, *cmd, **kwargs):
return ("\n", "fake-error")
def fake_pretend_lvm_version(obj, *cmd, **kwargs):
return (" LVM version: 2.03.00 (2012-03-06)\n", "")
def fake_old_lvm_version(obj, *cmd, **kwargs):
# Does not support thin prov or snap activation
return (" LVM version: 2.02.65(2) (2012-03-06)\n", "")
def fake_customised_lvm_version(obj, *cmd, **kwargs):
return (" LVM version: 2.02.100(2)-RHEL6 (2013-09-12)\n", "")
def fake_execute(obj, *cmd, **kwargs):
cmd_string = ', '.join(cmd)
data = "\n"
if ('env, LC_ALL=C, vgs, --noheadings, --unit=g, -o, name' ==
cmd_string):
data = " fake-vg\n"
data += " some-other-vg\n"
elif ('env, LC_ALL=C, vgs, --noheadings, -o, name, fake-vg' ==
cmd_string):
data = " fake-vg\n"
elif 'env, LC_ALL=C, vgs, --version' in cmd_string:
data = " LVM version: 2.02.95(2) (2012-03-06)\n"
elif ('env, LC_ALL=C, vgs, --noheadings, -o, uuid, fake-vg' in
cmd_string):
data = " kVxztV-dKpG-Rz7E-xtKY-jeju-QsYU-SLG6Z1\n"
elif 'env, LC_ALL=C, vgs, --noheadings, --unit=g, ' \
'-o, name,size,free,lv_count,uuid, ' \
'--separator, :, --nosuffix' in cmd_string:
data = (" test-prov-cap-vg-unit:10.00:10.00:0:"
"mXzbuX-dKpG-Rz7E-xtKY-jeju-QsYU-SLG8Z4\n")
if 'test-prov-cap-vg-unit' in cmd_string:
return (data, "")
data = (" test-prov-cap-vg-no-unit:10.00:10.00:0:"
"mXzbuX-dKpG-Rz7E-xtKY-jeju-QsYU-SLG8Z4\n")
if 'test-prov-cap-vg-no-unit' in cmd_string:
return (data, "")
data = " fake-vg:10.00:10.00:0:"\
"kVxztV-dKpG-Rz7E-xtKY-jeju-QsYU-SLG6Z1\n"
if 'fake-vg' in cmd_string:
return (data, "")
data += " fake-vg-2:10.00:10.00:0:"\
"lWyauW-dKpG-Rz7E-xtKY-jeju-QsYU-SLG7Z2\n"
data += " fake-vg-3:10.00:10.00:0:"\
"mXzbuX-dKpG-Rz7E-xtKY-jeju-QsYU-SLG8Z3\n"
elif ('env, LC_ALL=C, lvs, --noheadings, '
'--unit=g, -o, vg_name,name,size, --nosuffix, '
'fake-vg/lv-nothere' in cmd_string):
raise processutils.ProcessExecutionError(
stderr="One or more specified logical volume(s) not found.")
elif ('env, LC_ALL=C, lvs, --noheadings, '
'--unit=g, -o, vg_name,name,size, --nosuffix, '
'fake-vg/lv-newerror' in cmd_string):
raise processutils.ProcessExecutionError(
stderr="Failed to find logical volume \"fake-vg/lv-newerror\"")
elif ('env, LC_ALL=C, lvs, --noheadings, '
'--unit=g, -o, vg_name,name,size' in cmd_string):
if 'fake-unknown' in cmd_string:
raise processutils.ProcessExecutionError(
stderr="One or more volume(s) not found."
)
if 'test-prov-cap-vg-unit' in cmd_string:
data = " fake-vg test-prov-cap-pool-unit 9.50g\n"
data += " fake-vg fake-volume-1 1.00g\n"
data += " fake-vg fake-volume-2 2.00g\n"
elif 'test-prov-cap-vg-no-unit' in cmd_string:
data = " fake-vg test-prov-cap-pool-no-unit 9.50\n"
data += " fake-vg fake-volume-1 1.00\n"
data += " fake-vg fake-volume-2 2.00\n"
elif 'test-found-lv-name' in cmd_string:
data = " fake-vg test-found-lv-name 9.50\n"
else:
data = " fake-vg fake-1 1.00g\n"
data += " fake-vg fake-2 1.00g\n"
elif ('env, LC_ALL=C, lvdisplay, --noheading, -C, -o, Attr' in
cmd_string):
if 'test-volumes' in cmd_string:
data = ' wi-a-'
else:
data = ' owi-a-'
elif 'env, LC_ALL=C, pvs, --noheadings' in cmd_string:
data = " fake-vg|/dev/sda|10.00|1.00\n"
data += " fake-vg|/dev/sdb|10.00|1.00\n"
data += " fake-vg|/dev/sdc|10.00|8.99\n"
data += " fake-vg-2|/dev/sdd|10.00|9.99\n"
elif 'env, LC_ALL=C, lvs, --noheadings, --unit=g' \
', -o, size,data_percent, --separator, :' in cmd_string:
if 'test-prov-cap-pool' in cmd_string:
data = " 9.5:20\n"
else:
data = " 9:12\n"
elif 'lvcreate, -T, -L, ' in cmd_string:
pass
elif 'lvcreate, -T, -V, ' in cmd_string:
pass
elif 'lvcreate, -n, ' in cmd_string:
pass
elif 'lvcreate, --name, ' in cmd_string:
pass
elif 'lvextend, -L, ' in cmd_string:
pass
else:
raise AssertionError('unexpected command called: %s' % cmd_string)
return (data, "")
def test_create_lv_snapshot(self):
self.assertIsNone(self.vg.create_lv_snapshot('snapshot-1', 'fake-1'))
with mock.patch.object(self.vg, 'get_volume', return_value=None):
try:
self.vg.create_lv_snapshot('snapshot-1', 'fake-non-existent')
except exception.VolumeDeviceNotFound as e:
self.assertEqual('fake-non-existent', e.kwargs['device'])
else:
self.fail("Exception not raised")
def test_vg_exists(self):
self.assertTrue(self.vg._vg_exists())
def test_get_vg_uuid(self):
self.assertEqual('kVxztV-dKpG-Rz7E-xtKY-jeju-QsYU-SLG6Z1',
self.vg._get_vg_uuid()[0])
def test_get_all_volumes(self):
out = self.vg.get_volumes()
self.assertEqual('fake-1', out[0]['name'])
self.assertEqual('1.00g', out[0]['size'])
self.assertEqual('fake-vg', out[0]['vg'])
def test_get_volume(self):
self.assertEqual('fake-1', self.vg.get_volume('fake-1')['name'])
def test_get_volume_none(self):
self.assertIsNone(self.vg.get_volume('fake-unknown'))
def test_get_lv_info_notfound(self):
# lv-nothere will raise lvm < 2.102.112 exception
self.assertEqual(
[],
self.vg.get_lv_info(
'sudo', vg_name='fake-vg', lv_name='lv-nothere')
)
# lv-newerror will raise lvm > 2.102.112 exception
self.assertEqual(
[],
self.vg.get_lv_info(
'sudo', vg_name='fake-vg', lv_name='lv-newerror')
)
def test_get_lv_info_found(self):
lv_info = [{'size': '9.50', 'name': 'test-found-lv-name',
'vg': 'fake-vg'}]
self.assertEqual(
lv_info,
self.vg.get_lv_info(
'sudo', vg_name='fake-vg',
lv_name='test-found-lv-name')
)
def test_get_lv_info_no_lv_name(self):
lv_info = [{'name': 'fake-1', 'size': '1.00g', 'vg': 'fake-vg'},
{'name': 'fake-2', 'size': '1.00g', 'vg': 'fake-vg'}]
self.assertEqual(
lv_info,
self.vg.get_lv_info(
'sudo', vg_name='fake-vg')
)
def test_get_all_physical_volumes(self):
# Filtered VG version
pvs = self.vg.get_all_physical_volumes('sudo', 'fake-vg')
self.assertEqual(3, len(pvs))
# Non-Filtered, all VG's
pvs = self.vg.get_all_physical_volumes('sudo')
self.assertEqual(4, len(pvs))
def test_get_physical_volumes(self):
pvs = self.vg.get_physical_volumes()
self.assertEqual(3, len(pvs))
def test_get_volume_groups(self):
self.assertEqual(3, len(self.vg.get_all_volume_groups('sudo')))
self.assertEqual(1,
len(self.vg.get_all_volume_groups('sudo', 'fake-vg')))
def test_thin_support(self):
# lvm.supports_thin() is a static method and doesn't
# use the self._executor fake we pass in on init
# so we need to stub processutils.execute appropriately
with mock.patch.object(processutils, 'execute',
side_effect=self.fake_execute):
self.assertTrue(self.vg.supports_thin_provisioning('sudo'))
with mock.patch.object(processutils, 'execute',
side_effect=self.fake_pretend_lvm_version):
self.assertTrue(self.vg.supports_thin_provisioning('sudo'))
with mock.patch.object(processutils, 'execute',
side_effect=self.fake_old_lvm_version):
self.assertFalse(self.vg.supports_thin_provisioning('sudo'))
with mock.patch.object(processutils, 'execute',
side_effect=self.fake_customised_lvm_version):
self.assertTrue(self.vg.supports_thin_provisioning('sudo'))
def test_snapshot_lv_activate_support(self):
self.vg._supports_snapshot_lv_activation = None
with mock.patch.object(processutils, 'execute',
side_effect=self.fake_execute):
self.assertTrue(self.vg.supports_snapshot_lv_activation)
self.vg._supports_snapshot_lv_activation = None
with mock.patch.object(processutils, 'execute',
side_effect=self.fake_old_lvm_version):
self.assertFalse(self.vg.supports_snapshot_lv_activation)
self.vg._supports_snapshot_lv_activation = None
def test_lvchange_ignskipact_support_yes(self):
"""Tests if lvchange -K is available via a lvm2 version check."""
self.vg._supports_lvchange_ignoreskipactivation = None
with mock.patch.object(processutils, 'execute',
side_effect=self.fake_pretend_lvm_version):
self.assertTrue(self.vg.supports_lvchange_ignoreskipactivation)
self.vg._supports_lvchange_ignoreskipactivation = None
with mock.patch.object(processutils, 'execute',
side_effect=self.fake_old_lvm_version):
self.assertFalse(self.vg.supports_lvchange_ignoreskipactivation)
self.vg._supports_lvchange_ignoreskipactivation = None
def test_thin_pool_creation(self):
# The size of fake-vg volume group is 10g, so the calculated thin
# pool size should be 9.5g (95% of 10g).
self.assertEqual("9.5g", self.vg.create_thin_pool())
# Passing a size parameter should result in a thin pool of that exact
# size.
for size in ("1g", "1.2g", "1.75g"):
self.assertEqual(size, self.vg.create_thin_pool(size_str=size))
def test_thin_pool_provisioned_capacity(self):
self.vg.vg_thin_pool = "test-prov-cap-pool-unit"
self.vg.vg_name = 'test-prov-cap-vg-unit'
self.assertEqual(
"9.5g",
self.vg.create_thin_pool(name=self.vg.vg_thin_pool))
self.assertEqual("9.50", self.vg.vg_thin_pool_size)
self.assertEqual(7.6, self.vg.vg_thin_pool_free_space)
self.assertEqual(3.0, self.vg.vg_provisioned_capacity)
self.vg.vg_thin_pool = "test-prov-cap-pool-no-unit"
self.vg.vg_name = 'test-prov-cap-vg-no-unit'
self.assertEqual(
"9.5g",
self.vg.create_thin_pool(name=self.vg.vg_thin_pool))
self.assertEqual("9.50", self.vg.vg_thin_pool_size)
self.assertEqual(7.6, self.vg.vg_thin_pool_free_space)
self.assertEqual(3.0, self.vg.vg_provisioned_capacity)
def test_thin_pool_free_space(self):
# The size of fake-vg-pool is 9g and the allocated data sums up to
# 12% so the calculated free space should be 7.92
self.assertEqual(float("7.92"),
self.vg._get_thin_pool_free_space("fake-vg",
"fake-vg-pool"))
def test_volume_create_after_thin_creation(self):
"""Test self.vg.vg_thin_pool is set to pool_name
See bug #1220286 for more info.
"""
vg_name = "vg-name"
pool_name = vg_name + "-pool"
pool_path = "%s/%s" % (vg_name, pool_name)
def executor(obj, *cmd, **kwargs):
self.assertEqual(pool_path, cmd[-1])
self.vg._executor = executor
self.vg.create_thin_pool(pool_name, "1G")
self.vg.create_volume("test", "1G", lv_type='thin')
self.assertEqual(pool_name, self.vg.vg_thin_pool)
def test_lv_has_snapshot(self):
self.assertTrue(self.vg.lv_has_snapshot('fake-vg'))
self.assertFalse(self.vg.lv_has_snapshot('test-volumes'))
def test_activate_lv(self):
self.vg._supports_lvchange_ignoreskipactivation = True
with mock.patch.object(self.vg, '_execute') as mock_exec:
self.vg.activate_lv('my-lv')
expected = [mock.call('lvchange', '-a', 'y', '--yes', '-K',
'fake-vg/my-lv', root_helper='sudo',
run_as_root=True)]
self.assertEqual(expected, mock_exec.call_args_list)
def test_get_mirrored_available_capacity(self):
self.assertEqual(2.0, self.vg.vg_mirror_free_space(1))
def test_lv_extend(self):
self.vg.deactivate_lv = mock.MagicMock()
# Extend lv with snapshot and make sure deactivate called
self.vg.create_volume("test", "1G")
self.vg.extend_volume("test", "2G")
self.vg.deactivate_lv.assert_called_once_with('test')
self.vg.deactivate_lv.reset_mock()
# Extend lv without snapshot so deactivate should not be called
self.vg.create_volume("test", "1G")
self.vg.vg_name = "test-volumes"
self.vg.extend_volume("test", "2G")
self.assertFalse(self.vg.deactivate_lv.called)

20
os_brick/version.py Normal file
View File

@ -0,0 +1,20 @@
# All Rights Reserved
#
# 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 pbr.version
version_info = pbr.version.VersionInfo('os-brick')
__version__ = version_info.version_string()

View File

@ -0,0 +1,3 @@
---
features:
- Added vStorage protocol support for RemoteFS connections.

View File

@ -0,0 +1,3 @@
---
fixes:
- Improved multipath device handling.

View File

@ -0,0 +1,3 @@
---
other:
- Start using reno to manage release notes.

View File

276
releasenotes/source/conf.py Normal file
View File

@ -0,0 +1,276 @@
# -*- coding: utf-8 -*-
# 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.
# Cinder Release Notes documentation build configuration file, created by
# sphinx-quickstart on Tue Nov 4 17:02:44 2015.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
# sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'oslosphinx',
'reno.sphinxext',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
# source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'OS-Brick Release Notes'
copyright = u'2015, Cinder Developers'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
from os_brick.version import version_info
# The full version, including alpha/beta/rc tags.
release = version_info.version_string_with_vcs()
# The short X.Y version.
version = version_info.canonical_version_string()
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
# language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
# today = ''
# Else, today_fmt is used as the format for a strftime call.
# today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = []
# The reST default role (used for this markup: `text`) to use for all
# documents.
# default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
# add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
# add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
# keep_warnings = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'default'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
# html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
# html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
# html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
# html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
# html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
# html_extra_path = []
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
# html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
# html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
# html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
# html_additional_pages = {}
# If false, no module index is generated.
# html_domain_indices = True
# If false, no index is generated.
# html_use_index = True
# If true, the index is split into individual pages for each letter.
# html_split_index = False
# If true, links to the reST sources are added to the pages.
# html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
# html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
# html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
# html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
# html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'OSBrickReleaseNotesdoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
# 'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
('index', 'OSBrickReleaseNotes.tex',
u'OS-Brick Release Notes Documentation',
u'Cinder Developers', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
# latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
# latex_use_parts = False
# If true, show page references after internal links.
# latex_show_pagerefs = False
# If true, show URL addresses after external links.
# latex_show_urls = False
# Documents to append as an appendix to all manuals.
# latex_appendices = []
# If false, no module index is generated.
# latex_domain_indices = True
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'osbrickreleasenotes',
u'OS-Brick Release Notes Documentation',
[u'Cinder Developers'], 1)
]
# If true, show URL addresses after external links.
# man_show_urls = False
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'OSBrickReleaseNotes',
u'OS-Brick Release Notes Documentation',
u'Cinder Developers', 'OSBrickReleaseNotes',
'Volume discovery and local storage management lib.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
# texinfo_appendices = []
# If false, no module index is generated.
# texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
# texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
# texinfo_no_detailmenu = False

View File

@ -0,0 +1,5 @@
=============
Release Notes
=============
.. release-notes::

View File

@ -2,15 +2,15 @@
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
pbr>=1.6
Babel>=1.3
eventlet>=0.17.4
pbr>=1.6 # Apache-2.0
Babel>=1.3 # BSD
eventlet!=0.18.3,>=0.18.2 # MIT
oslo.concurrency>=2.3.0 # Apache-2.0
oslo.log>=1.12.0 # Apache-2.0
oslo.log>=1.14.0 # Apache-2.0
oslo.serialization>=1.10.0 # Apache-2.0
oslo.i18n>=1.5.0 # Apache-2.0
oslo.i18n>=2.1.0 # Apache-2.0
oslo.service>=1.0.0 # Apache-2.0
oslo.utils>=2.8.0 # Apache-2.0
requests>=2.8.1
oslo.utils>=3.5.0 # Apache-2.0
requests!=2.9.0,>=2.8.1 # Apache-2.0
retrying!=1.3.0,>=1.2.3 # Apache-2.0
six>=1.9.0
six>=1.9.0 # MIT

View File

@ -15,7 +15,6 @@ classifier =
Programming Language :: Python
Programming Language :: Python :: 2
Programming Language :: Python :: 2.7
Programming Language :: Python :: 2.6
Programming Language :: Python :: 3
Programming Language :: Python :: 3.3
Programming Language :: Python :: 3.4

View File

@ -3,12 +3,13 @@
# process, which may cause wedges in the gate later.
hacking<0.11,>=0.10.0
coverage>=3.6
python-subunit>=0.0.18
sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2
coverage>=3.6 # Apache-2.0
python-subunit>=0.0.18 # Apache-2.0/BSD
reno>=0.1.1 # Apache2
sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 # BSD
oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0
oslotest>=1.10.0 # Apache-2.0
testrepository>=0.0.18
testscenarios>=0.4
testtools>=1.4.0
os-testr>=0.4.1
testrepository>=0.0.18 # Apache-2.0/BSD
testscenarios>=0.4 # Apache-2.0/BSD
testtools>=1.4.0 # MIT
os-testr>=0.4.1 # Apache-2.0

View File

@ -33,6 +33,9 @@ commands = python setup.py testr --coverage --testr-args='{posargs}'
[testenv:docs]
commands = python setup.py build_sphinx
[testenv:releasenotes]
commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html
[flake8]
# H803 skipped on purpose per list discussion.
# E123, E125 skipped as they are invalid PEP-8.