Add new v3 attach/detach workflow

This patch adds the ability to use the new v 3.27 microversioned
attach/detach workflow that was added to Cinder.  Since this is an extension
to the cinderclient, the code checks to make sure the client version and the
cinder API version are capable of executing the new v3.27 attach/detach
workflow, before using it.

Change-Id: I4c3fdc841108020ee310ddc0424a44480010fda1
This commit is contained in:
Walter A. Boring IV 2017-05-31 16:51:15 +00:00
parent a10a81bc7c
commit 3a8b113926
5 changed files with 160 additions and 2 deletions

View File

@ -16,14 +16,15 @@ Command-line interface to the os-brick.
"""
from __future__ import print_function
import json
import socket
from cinderclient import utils
import pbr.version
from brick_cinderclient_ext import brick_utils
from brick_cinderclient_ext import client as brick_client
from cinderclient import utils
__version__ = pbr.version.VersionInfo(

View File

@ -12,9 +12,13 @@
from __future__ import print_function
import cinderclient
from cinderclient import api_versions
from cinderclient import exceptions
from os_brick.initiator import connector
from oslo_concurrency import processutils
from oslo_utils import uuidutils
from pbr import version as pbr_version
from brick_cinderclient_ext import brick_utils
from brick_cinderclient_ext import volume_actions as actions
@ -28,14 +32,29 @@ class Client(object):
1.0.0 - Initial version
1.1.0 - Query volume paths implementation
1.2.0 - Add --nic attribute to get-connector
1.3.0 - Added new v3 attach/detach workflow support
"""
version = '1.2.0'
version = '1.3.0'
# Use the legacy attach/detach workflow?
_use_legacy_attach = True
def __init__(self, volumes_client=None):
self.volumes_client = volumes_client
# Test to see if we have a version of the cinderclient
# that can do the new volume attach/detach API
version_want = pbr_version.SemanticVersion(major=2)
current_version = cinderclient.version_info.semantic_version()
if (self.volumes_client and current_version >= version_want):
# We have a recent enough client to test the microversion we need.
required_version = api_versions.APIVersion("3.27")
if self.volumes_client.api_version.matches(required_version):
# we can use the new attach/detach API
self._use_legacy_attach = False
def _brick_get_connector(self, protocol, driver=None,
execute=processutils.execute,
use_multipath=False,
@ -67,6 +86,28 @@ class Client(object):
def attach(self, volume_id, hostname, mountpoint=None, mode='rw',
multipath=False, enforce_multipath=False, nic=None):
"""Main entry point for trying to attach a volume.
If the cinderclient has a recent version that can do the new attach
workflow, lets try that. Otherwise we revert to the older attach
workflow.
"""
if self._use_legacy_attach:
return self._legacy_attach(volume_id, hostname,
mountpoint=mountpoint,
mode=mode, multipath=multipath,
enforce_multipath=enforce_multipath,
nic=nic)
else:
return self._attach(volume_id, hostname,
mountpoint=mountpoint,
mode=mode, multipath=multipath,
enforce_multipath=enforce_multipath,
nic=nic)
def _legacy_attach(self, volume_id, hostname, mountpoint=None, mode='rw',
multipath=False, enforce_multipath=False, nic=None):
"""The original/legacy attach workflow."""
# Reserve volume before attachment
with actions.Reserve(self.volumes_client, volume_id) as cmd:
cmd.reserve()
@ -87,9 +128,51 @@ class Client(object):
mountpoint, mode, hostname)
return device_info
def _attach(self, volume_id, hostname, mountpoint=None, mode='rw',
multipath=False, enforce_multipath=False, nic=None):
"""Attempt to use the v3 API for attach workflow.
If the cinder API microversion is good enough, we will use the new
attach workflow, otherwise we resort back to the old workflow.
"""
# We can use the new attach/detach workflow
connector_properties = self.get_connector(
multipath=multipath,
enforce_multipath=enforce_multipath, nic=nic)
instance_id = uuidutils.generate_uuid()
info = self.volumes_client.attachments.create(
volume_id, connector_properties, instance_id)
connection = info['connection_info']
with actions.VerifyProtocol(self.volumes_client, volume_id) as cmd:
cmd.verify(connection['driver_volume_type'])
brick_connector = self._brick_get_connector(
connection['driver_volume_type'], do_local_attach=True)
device_info = brick_connector.connect_volume(connection)
return device_info
def detach(self, volume_id, attachment_uuid=None, multipath=False,
enforce_multipath=False, device_info=None, nic=None):
if self._use_legacy_attach:
self._legacy_detach(volume_id,
attachment_uuid=attachment_uuid,
multipath=multipath,
enforce_multipath=enforce_multipath,
device_info=device_info, nic=nic)
else:
self._detach(volume_id,
attachment_uuid=attachment_uuid,
multipath=multipath,
enforce_multipath=enforce_multipath,
device_info=device_info, nic=nic)
def _legacy_detach(self, volume_id, attachment_uuid=None, multipath=False,
enforce_multipath=False, device_info=None, nic=None):
"""The original/legacy detach workflow."""
with actions.BeginDetach(self.volumes_client, volume_id) as cmd:
cmd.reserve()
@ -107,6 +190,27 @@ class Client(object):
with actions.DetachVolume(self.volumes_client, volume_id) as cmd:
cmd.detach(self, attachment_uuid, multipath, enforce_multipath)
def _detach(self, volume_id, attachment_uuid=None, multipath=False,
enforce_multipath=False, device_info=None, nic=None):
if not attachment_uuid:
# We need the specific attachment uuid to know which one to detach.
# if None was passed in we can only work if there is one and only
# one attachment for the volume.
# Get the list of attachments for the volume.
search_opts = {'volume_id': volume_id}
attachments = self.volumes_client.attachments.list(
search_opts=search_opts)
if len(attachments) == 0:
raise exceptions.NoAttachmentsFound(volume_id=volume_id)
if len(attachments) == 1:
attachment_uuid = attachments[0].id
else:
# We have more than 1 attachment and we don't know which to use
raise exceptions.NeedAttachmentUUID(volume_id=volume_id)
self.volumes_client.attachments.delete(attachment_uuid)
def get_volume_paths(self, volume_id, use_multipath=False):
"""Gets volume paths on the system for a specific volume."""
conn_props = self.get_connector(multipath=use_multipath)

View File

@ -39,3 +39,12 @@ class NicNotFound(BrickInterfaceException):
class IncorrectNic(BrickInterfaceException):
# TODO(mdovgal): change message after adding ipv6 support
message = _("Network interface %(iface)s has not ipv4 address assigned.")
class NoAttachmentsFound(BrickInterfaceException):
message = _("There were no attachments found for %(volume_id)s")
class NeedAttachmentUUID(BrickInterfaceException):
message = _("Volume %(volume_id)s has more than one attachment. "
"Please pass in the attachment_uuid you wish to detach.")

View File

@ -12,7 +12,9 @@
import mock
from cinderclient import api_versions
from oslotest import base
from pbr import version as pbr_version
from brick_cinderclient_ext import client
@ -27,6 +29,7 @@ class TestBrickClient(base.BaseTestCase):
def _init_fake_cinderclient(self, protocol):
# Init fake cinderclient
self.mock_vc = mock.Mock()
self.mock_vc.version_info = mock.Mock()
conn_data = {'key': 'value'}
connection = {'driver_volume_type': protocol, 'data': conn_data}
self.mock_vc.volumes.initialize_connection.return_value = connection
@ -81,6 +84,46 @@ class TestBrickClient(base.BaseTestCase):
multipath=True,
execute=mock_execute)
def test_client_use_new_attach_no_volumes_client(self):
brick_client = client.Client(None)
self.assertTrue(brick_client._use_legacy_attach)
@mock.patch('cinderclient.version_info.semantic_version')
def test_client_use_new_attach_v1_cinderclient(self,
mock_semantic_version):
self._init_fake_cinderclient('iscsi')
mock_semantic_version.return_value = pbr_version.SemanticVersion(
major=1, minor=0)
self.client.volumes_client.version_info.semantic_version
brick_client = client.Client(self.client.volumes_client)
self.assertTrue(brick_client._use_legacy_attach)
@mock.patch('cinderclient.version_info.semantic_version')
def test_client_use_new_attach_v2_cinderclient_3_0(self,
mock_semantic_version):
self._init_fake_cinderclient('iscsi')
mock_semantic_version.return_value = pbr_version.SemanticVersion(
major=2, minor=0)
self.client.volumes_client.version_info.semantic_version
current_api_version = api_versions.APIVersion("3.0")
self.client.volumes_client.api_version = current_api_version
brick_client = client.Client(self.client.volumes_client)
self.assertTrue(brick_client._use_legacy_attach)
@mock.patch('cinderclient.version_info.semantic_version')
def test_client_use_new_attach_v2_cinderclient_3_27(self,
mock_semantic_version):
self._init_fake_cinderclient('iscsi')
mock_semantic_version.return_value = pbr_version.SemanticVersion(
major=2, minor=0)
self.client.volumes_client.version_info.semantic_version
current_api_version = api_versions.APIVersion("3.27")
self.client.volumes_client.api_version = current_api_version
brick_client = client.Client(self.client.volumes_client)
self.assertFalse(brick_client._use_legacy_attach)
@mock.patch('os_brick.initiator.connector.get_connector_properties')
def test_attach_iscsi(self, mock_conn_prop):
connection = self._init_fake_cinderclient('iscsi')

View File

@ -4,6 +4,7 @@
hacking<0.11,>=0.10.0
python-cinderclient>=2.0.1 # Apache-2.0
coverage!=4.4,>=4.0 # Apache-2.0
ddt>=1.0.1 # MIT
python-subunit>=0.0.18 # Apache-2.0/BSD