diff --git a/os_brick/initiator/connectors/scaleio.py b/os_brick/initiator/connectors/scaleio.py index 31fdf0b07..0f65b960d 100644 --- a/os_brick/initiator/connectors/scaleio.py +++ b/os_brick/initiator/connectors/scaleio.py @@ -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 .""" + + return ioc(0x0, _type, nr, 0) + + +def ioc(direction, _type, nr, size): + """Implementation of _IOC macro from .""" + + 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]) diff --git a/os_brick/privileged/scaleio.py b/os_brick/privileged/scaleio.py new file mode 100644 index 000000000..322289e3f --- /dev/null +++ b/os_brick/privileged/scaleio.py @@ -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)) diff --git a/os_brick/tests/initiator/connectors/test_scaleio.py b/os_brick/tests/initiator/connectors/test_scaleio.py index 4384a7121..14e45b2df 100644 --- a/os_brick/tests/initiator/connectors/test_scaleio.py +++ b/os_brick/tests/initiator/connectors/test_scaleio.py @@ -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)