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:
parent
cc17adb70f
commit
2d694361fe
@ -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])
|
||||
|
72
os_brick/privileged/scaleio.py
Normal file
72
os_brick/privileged/scaleio.py
Normal 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))
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user