Remove VxFlexOS connector external dependencies

The OpenStack os-brick library uses hardcoded paths to binary files to
interact with the VxFlexOS SDC. This leads to problems when using
containerized OpenStack (Kolla & Red Hat). Due to the fact that VxFlexOS
SDC binary files has to be used inside containers (nova, cinder, etc.)
the overcloud deployment must be performed in 3 stages:
  1) deploy overcloud without additional volume mounts
  2) install the VxFlexOS client on the controller and compute nodes
  3) update overcloud with additional volume mounts

Using these changes overcloud can be deployed without update step after
initial deployment since os-brick does not have external dependencies
and uses python built-in libraries. The scini device through which the
VxFlexOS client interacts is presented in the containers by default
because /dev directory from the host is mounted in all containers.

Change-Id: Ifc4dee0a51bafd6aa9865ec66c46c10087daa667
Closes-Bug: #1846483
This commit is contained in:
Ivan Pchelintsev 2019-10-10 13:16:39 +03:00
parent cc17adb70f
commit 2d694361fe
3 changed files with 132 additions and 72 deletions

View File

@ -19,13 +19,13 @@ import six
from six.moves import urllib
from oslo_concurrency import lockutils
from oslo_concurrency import processutils as putils
from oslo_log import log as logging
from os_brick import exception
from os_brick.i18n import _
from os_brick import initiator
from os_brick.initiator.connectors import base
from os_brick.privileged import scaleio as priv_scaleio
from os_brick import utils
LOG = logging.getLogger(__name__)
@ -33,14 +33,26 @@ DEVICE_SCAN_ATTEMPTS_DEFAULT = 3
synchronized = lockutils.synchronized_with_prefix('os-brick-')
def io(_type, nr):
"""Implementation of _IO macro from <sys/ioctl.h>."""
return ioc(0x0, _type, nr, 0)
def ioc(direction, _type, nr, size):
"""Implementation of _IOC macro from <sys/ioctl.h>."""
return direction | (size & 0x1fff) << 16 | ord(_type) << 8 | nr
class ScaleIOConnector(base.BaseLinuxConnector):
"""Class implements the connector driver for ScaleIO."""
OK_STATUS_CODE = 200
VOLUME_NOT_MAPPED_ERROR = 84
VOLUME_ALREADY_MAPPED_ERROR = 81
GET_GUID_CMD = ['/opt/emc/scaleio/sdc/bin/drv_cfg', '--query_guid']
RESCAN_VOLS_CMD = ['/opt/emc/scaleio/sdc/bin/drv_cfg', '--rescan']
GET_GUID_OP_CODE = io('a', 14)
RESCAN_VOLS_OP_CODE = io('a', 10)
def __init__(self, root_helper, driver=None,
device_scan_attempts=initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT,
@ -64,6 +76,26 @@ class ScaleIOConnector(base.BaseLinuxConnector):
self.iops_limit = None
self.bandwidth_limit = None
def _get_guid(self):
try:
guid = priv_scaleio.get_guid(self.GET_GUID_OP_CODE)
LOG.info("Current sdc guid: %s", guid)
return guid
except (IOError, OSError, ValueError) as e:
msg = _("Error querying sdc guid: %s") % e
LOG.error(msg)
raise exception.BrickException(message=msg)
def _rescan_vols(self):
LOG.info("ScaleIO rescan volumes")
try:
priv_scaleio.rescan_vols(self.RESCAN_VOLS_OP_CODE)
except (IOError, OSError) as e:
msg = _("Error querying volumes: %s") % e
LOG.error(msg)
raise exception.BrickException(message=msg)
@staticmethod
def get_connector_properties(root_helper, *args, **kwargs):
"""The ScaleIO connector properties."""
@ -298,7 +330,7 @@ class ScaleIOConnector(base.BaseLinuxConnector):
"scaleIO Volume name: %(volume_name)s, SDC IP: %(sdc_ip)s, "
"REST Server IP: %(server_ip)s, "
"REST Server username: %(username)s, "
"iops limit:%(iops_limit)s, "
"iops limit: %(iops_limit)s, "
"bandwidth limit: %(bandwidth_limit)s."
), {
'volume_name': self.volume_name,
@ -311,24 +343,7 @@ class ScaleIOConnector(base.BaseLinuxConnector):
}
)
LOG.info("ScaleIO sdc query guid command: %(cmd)s",
{'cmd': self.GET_GUID_CMD})
try:
(out, err) = self._execute(*self.GET_GUID_CMD, run_as_root=True,
root_helper=self._root_helper)
LOG.info("Map volume %(cmd)s: stdout=%(out)s "
"stderr=%(err)s",
{'cmd': self.GET_GUID_CMD, 'out': out, 'err': err})
except putils.ProcessExecutionError as e:
msg = (_("Error querying sdc guid: %(err)s") % {'err': e.stderr})
LOG.error(msg)
raise exception.BrickException(message=msg)
guid = out
LOG.info("Current sdc guid: %(guid)s", {'guid': guid})
guid = self._get_guid()
params = {'guid': guid, 'allowMultipleMappings': 'TRUE'}
self.volume_id = self.volume_id or self._get_volume_id()
@ -424,6 +439,10 @@ class ScaleIOConnector(base.BaseLinuxConnector):
:type connection_properties: dict
:param device_info: historical difference, but same as connection_props
:type device_info: dict
:type force: bool
:param ignore_errors: When force is True, this will decide whether to
ignore errors or raise an exception once finished
the operation. Default is False.
"""
self.get_config(connection_properties)
self.volume_id = self.volume_id or self._get_volume_id()
@ -438,25 +457,7 @@ class ScaleIOConnector(base.BaseLinuxConnector):
'server_ip': self.server_ip}
)
LOG.info("ScaleIO sdc query guid command: %(cmd)s",
{'cmd': self.GET_GUID_CMD})
try:
(out, err) = self._execute(*self.GET_GUID_CMD, run_as_root=True,
root_helper=self._root_helper)
LOG.info(
"Unmap volume %(cmd)s: stdout=%(out)s stderr=%(err)s",
{'cmd': self.GET_GUID_CMD, 'out': out, 'err': err}
)
except putils.ProcessExecutionError as e:
msg = _("Error querying sdc guid: %(err)s") % {'err': e.stderr}
LOG.error(msg)
raise exception.BrickException(message=msg)
guid = out
LOG.info("Current sdc guid: %(guid)s", {'guid': guid})
guid = self._get_guid()
params = {'guid': guid}
headers = {'content-type': 'application/json'}
request = (
@ -499,21 +500,7 @@ class ScaleIOConnector(base.BaseLinuxConnector):
for a ScaleIO volume.
"""
LOG.info("ScaleIO rescan volumes: %(cmd)s",
{'cmd': self.RESCAN_VOLS_CMD})
try:
(out, err) = self._execute(*self.RESCAN_VOLS_CMD, run_as_root=True,
root_helper=self._root_helper)
LOG.debug("Rescan volumes %(cmd)s: stdout=%(out)s",
{'cmd': self.RESCAN_VOLS_CMD, 'out': out})
except putils.ProcessExecutionError as e:
msg = (_("Error querying volumes: %(err)s") % {'err': e.stderr})
LOG.error(msg)
raise exception.BrickException(message=msg)
self._rescan_vols()
volume_paths = self.get_volume_paths(connection_properties)
if volume_paths:
return self.get_device_size(volume_paths[0])

View File

@ -0,0 +1,72 @@
# 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.
from binascii import hexlify
from contextlib import contextmanager
from fcntl import ioctl
import os
import struct
import uuid
from os_brick import privileged
SCINI_DEVICE_PATH = '/dev/scini'
@contextmanager
def open_scini_device():
"""Open scini device for low-level I/O using contextmanager.
File descriptor will be closed after all operations performed if it was
opened successfully.
:return: scini device file descriptor
:rtype: int
"""
fd = None
try:
fd = os.open(SCINI_DEVICE_PATH, os.O_RDWR)
yield fd
finally:
if fd:
os.close(fd)
@privileged.default.entrypoint
def get_guid(op_code):
"""Query ScaleIO sdc GUID via ioctl request.
:param op_code: operational code
:type op_code: int
:return: ScaleIO sdc GUID
:rtype: str
"""
with open_scini_device() as fd:
out = ioctl(fd, op_code, struct.pack('QQQ', 0, 0, 0))
# The first 8 bytes contain a return code that is not used
# so they can be discarded.
out_to_hex = hexlify(out[8:]).decode()
return str(uuid.UUID(out_to_hex))
@privileged.default.entrypoint
def rescan_vols(op_code):
"""Rescan ScaleIO volumes via ioctl request.
:param op_code: operational code
:type op_code: int
"""
with open_scini_device() as fd:
ioctl(fd, op_code, struct.pack('Q', 0))

View File

@ -17,8 +17,6 @@ import os
import requests
import six
from oslo_concurrency import processutils as putils
from os_brick import exception
from os_brick.initiator.connectors import scaleio
from os_brick.tests.initiator import test_connector
@ -35,7 +33,7 @@ class ScaleIOConnectorTestCase(test_connector.ConnectorTestCase):
}
# Fake SDC GUID
fake_guid = 'FAKE_GUID'
fake_guid = '013a5304-d053-4b30-a34f-ee3ad983236d'
def setUp(self):
super(ScaleIOConnectorTestCase, self).setUp()
@ -84,6 +82,12 @@ class ScaleIOConnectorTestCase(test_connector.ConnectorTestCase):
self.mock_object(os, 'listdir',
return_value=["emc-vol-{}".format(self.vol['id'])])
# Patch scaleio privileged calls
self.get_guid_mock = self.mock_object(scaleio.priv_scaleio, 'get_guid',
return_value=self.fake_guid)
self.rescan_vols_mock = self.mock_object(scaleio.priv_scaleio,
'rescan_vols')
# The actual ScaleIO connector
self.connector = scaleio.ScaleIOConnector(
'sudo', execute=self.fake_execute)
@ -117,14 +121,6 @@ class ScaleIOConnectorTestCase(test_connector.ConnectorTestCase):
return super(ScaleIOConnectorTestCase.MockHTTPSResponse,
self).text
def fake_execute(self, *cmd, **kwargs):
"""Fakes the rootwrap call"""
return self.fake_guid, None
def fake_missing_execute(self, *cmd, **kwargs):
"""Error when trying to call rootwrap drv_cfg"""
raise putils.ProcessExecutionError("Test missing drv_cfg.")
def handle_scaleio_request(self, url, *args, **kwargs):
"""Fake REST server"""
api_call = url.split(':', 2)[2].split('/', 1)[1].replace('api/', '')
@ -170,6 +166,8 @@ class ScaleIOConnectorTestCase(test_connector.ConnectorTestCase):
def test_connect_volume(self):
"""Successful connect to volume"""
self.connector.connect_volume(self.fake_connection_properties)
self.get_guid_mock.assert_called_once_with(
self.connector.GET_GUID_OP_CODE)
def test_connect_volume_without_volume_id(self):
"""Successful connect to volume without a Volume Id"""
@ -177,6 +175,8 @@ class ScaleIOConnectorTestCase(test_connector.ConnectorTestCase):
connection_properties.pop('scaleIO_volume_id')
self.connector.connect_volume(connection_properties)
self.get_guid_mock.assert_called_once_with(
self.connector.GET_GUID_OP_CODE)
def test_connect_with_bandwidth_limit(self):
"""Successful connect to volume with bandwidth limit"""
@ -197,6 +197,8 @@ class ScaleIOConnectorTestCase(test_connector.ConnectorTestCase):
def test_disconnect_volume(self):
"""Successful disconnect from volume"""
self.connector.disconnect_volume(self.fake_connection_properties, None)
self.get_guid_mock.assert_called_once_with(
self.connector.GET_GUID_OP_CODE)
def test_disconnect_volume_without_volume_id(self):
"""Successful disconnect from volume without a Volume Id"""
@ -204,6 +206,8 @@ class ScaleIOConnectorTestCase(test_connector.ConnectorTestCase):
connection_properties.pop('scaleIO_volume_id')
self.connector.disconnect_volume(connection_properties, None)
self.get_guid_mock.assert_called_once_with(
self.connector.GET_GUID_OP_CODE)
def test_error_id(self):
"""Fail to connect with bad volume name"""
@ -232,11 +236,6 @@ class ScaleIOConnectorTestCase(test_connector.ConnectorTestCase):
dict(errorCode=401, message='bad login'), 401)
self.assertRaises(exception.BrickException, self.test_connect_volume)
def test_error_bad_drv_cfg(self):
"""Fail to connect with missing rootwrap executable"""
self.connector.set_execute(self.fake_missing_execute)
self.assertRaises(exception.BrickException, self.test_connect_volume)
def test_error_map_volume(self):
"""Fail to connect with REST API failure"""
self.mock_calls[self.action_format.format(
@ -294,3 +293,5 @@ class ScaleIOConnectorTestCase(test_connector.ConnectorTestCase):
self.fake_connection_properties)
self.assertEqual(extended_size,
mock_device_size.return_value)
self.rescan_vols_mock.assert_called_once_with(
self.connector.RESCAN_VOLS_OP_CODE)