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:
commit
8a0c84a3ed
3
.gitignore
vendored
3
.gitignore
vendored
@ -43,6 +43,9 @@ output/*/index.html
|
||||
# Sphinx
|
||||
doc/build
|
||||
|
||||
# Release notes
|
||||
releasenotes/build/
|
||||
|
||||
# pbr generates these
|
||||
AUTHORS
|
||||
ChangeLog
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
0
os_brick/local_dev/__init__.py
Normal file
0
os_brick/local_dev/__init__.py
Normal file
782
os_brick/local_dev/lvm.py
Normal file
782
os_brick/local_dev/lvm.py
Normal 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
|
@ -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)
|
||||
|
@ -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)
|
||||
|
0
os_brick/tests/local_dev/__init__.py
Normal file
0
os_brick/tests/local_dev/__init__.py
Normal file
63
os_brick/tests/local_dev/fake_lvm.py
Normal file
63
os_brick/tests/local_dev/fake_lvm.py
Normal 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
|
365
os_brick/tests/local_dev/test_brick_lvm.py
Normal file
365
os_brick/tests/local_dev/test_brick_lvm.py
Normal 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
20
os_brick/version.py
Normal 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()
|
@ -0,0 +1,3 @@
|
||||
---
|
||||
features:
|
||||
- Added vStorage protocol support for RemoteFS connections.
|
@ -0,0 +1,3 @@
|
||||
---
|
||||
fixes:
|
||||
- Improved multipath device handling.
|
@ -0,0 +1,3 @@
|
||||
---
|
||||
other:
|
||||
- Start using reno to manage release notes.
|
0
releasenotes/source/_static/.placeholder
Normal file
0
releasenotes/source/_static/.placeholder
Normal file
0
releasenotes/source/_templates/.placeholder
Normal file
0
releasenotes/source/_templates/.placeholder
Normal file
276
releasenotes/source/conf.py
Normal file
276
releasenotes/source/conf.py
Normal 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
|
5
releasenotes/source/index.rst
Normal file
5
releasenotes/source/index.rst
Normal file
@ -0,0 +1,5 @@
|
||||
=============
|
||||
Release Notes
|
||||
=============
|
||||
|
||||
.. release-notes::
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
3
tox.ini
3
tox.ini
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user