diff --git a/os_brick/initiator/__init__.py b/os_brick/initiator/__init__.py index 95e0b1002..b84259689 100644 --- a/os_brick/initiator/__init__.py +++ b/os_brick/initiator/__init__.py @@ -58,3 +58,4 @@ VZSTORAGE = "VZSTORAGE" SHEEPDOG = "SHEEPDOG" VMDK = "VMDK" GPFS = "GPFS" +VERITAS_HYPERSCALE = "VERITAS_HYPERSCALE" diff --git a/os_brick/initiator/connector.py b/os_brick/initiator/connector.py index 6efb7007c..977e5ae47 100644 --- a/os_brick/initiator/connector.py +++ b/os_brick/initiator/connector.py @@ -98,6 +98,7 @@ connector_list = [ 'os_brick.initiator.windows.iscsi.WindowsISCSIConnector', 'os_brick.initiator.windows.fibre_channel.WindowsFCConnector', 'os_brick.initiator.windows.smbfs.WindowsSMBFSConnector', + 'os_brick.initiator.connectors.vrtshyperscale.HyperScaleConnector', ] # Mappings used to determine who to contruct in the factory @@ -143,7 +144,8 @@ _connector_mapping_linux = { 'os_brick.initiator.connectors.vmware.VmdkConnector', initiator.GPFS: 'os_brick.initiator.connectors.gpfs.GPFSConnector', - + initiator.VERITAS_HYPERSCALE: + 'os_brick.initiator.connectors.vrtshyperscale.HyperScaleConnector', } # Mapping for the S390X platform diff --git a/os_brick/initiator/connectors/vrtshyperscale.py b/os_brick/initiator/connectors/vrtshyperscale.py new file mode 100644 index 000000000..fea604c88 --- /dev/null +++ b/os_brick/initiator/connectors/vrtshyperscale.py @@ -0,0 +1,163 @@ +# Copyright (c) 2017 Veritas Technologies LLC. +# 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 json + +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.initiator.connectors import base +from os_brick import utils + +LOG = logging.getLogger(__name__) +synchronized = lockutils.synchronized_with_prefix('os-brick-vrts-hyperscale-') + +GUID_STR_LEN = 38 + + +class HyperScaleConnector(base.BaseLinuxConnector): + """Class implements the os-brick connector for HyperScale volumes.""" + + def __init__(self, root_helper, driver=None, + execute=putils.execute, + *args, **kwargs): + + super(HyperScaleConnector, self).__init__( + root_helper, driver=driver, + execute=execute, + *args, **kwargs) + + def get_volume_paths(self, connection_properties): + return [] + + def get_search_path(self): + return None + + def extend_volume(self, connection_properties): + raise NotImplementedError + + @staticmethod + def get_connector_properties(root_helper, *args, **kwargs): + """The HyperScale connector properties.""" + return {} + + @utils.trace + @synchronized('connect_volume') + def connect_volume(self, connection_properties): + """Connect a volume to an instance.""" + + out = None + err = None + device_info = {} + volume_name = None + + if 'name' in connection_properties.keys(): + volume_name = connection_properties['name'] + + if volume_name is None or len(volume_name) != GUID_STR_LEN: + msg = _("Failed to connect volume: invalid volume name.") + raise exception.BrickException(message=msg) + + cmd_arg = {'operation': 'connect_volume'} + cmd_arg['volume_guid'] = volume_name + cmdarg_json = json.dumps(cmd_arg) + + LOG.debug("HyperScale command hscli: %(cmd_arg)s", + {'cmd_arg': cmdarg_json}) + try: + (out, err) = self._execute('hscli', cmdarg_json, + run_as_root=True, + root_helper=self._root_helper) + + except putils.ProcessExecutionError as e: + msg = (_("Error executing hscli: %(err)s") % {'err': e.stderr}) + raise exception.BrickException(message=msg) + + LOG.debug("Result of hscli: stdout=%(out)s " + "stderr=%(err)s", + {'out': out, 'err': err}) + + if err != 0: + msg = (_("Failed to connect volume with stdout=%(out)s " + "stderr=%(err)s") % {'out': out, 'err': err}) + raise exception.BrickException(message=msg) + + output = json.loads(out) + payload = output.get('payload') + if payload is None: + msg = _("Failed to connect volume: " + "hscli returned invalid payload") + raise exception.BrickException(message=msg) + + if ('vsa_ip' not in payload.keys() or + 'refl_factor' not in payload.keys()): + msg = _("Failed to connect volume: " + "hscli returned invalid results") + raise exception.BrickException(message=msg) + + device_info['vsa_ip'] = payload.get('vsa_ip') + device_info['device_path'] = ( + "oflame://" + device_info['vsa_ip'] + ":9999/%7B" + + volume_name[1:-1] + "%7D") + + refl_factor = int(payload.get('refl_factor')) + device_info['refl_factor'] = str(refl_factor) + + if refl_factor > 0: + if 'refl_targets' not in payload.keys(): + msg = _("Failed to connect volume: " + "hscli returned inconsistent results") + raise exception.BrickException(message=msg) + + device_info['refl_targets'] = ( + payload.get('refl_targets')) + + return device_info + + @utils.trace + @synchronized('connect_volume') + def disconnect_volume(self, connection_properties, device_info): + """Disconnect a volume from an instance.""" + volume_name = None + + if 'name' in connection_properties.keys(): + volume_name = connection_properties['name'] + + if volume_name is None or len(volume_name) != GUID_STR_LEN: + msg = _("Failed to disconnect volume: invalid volume name") + raise exception.BrickException(message=msg) + + cmd_arg = {'operation': 'disconnect_volume'} + cmd_arg['volume_guid'] = volume_name + cmdarg_json = json.dumps(cmd_arg) + + LOG.debug("HyperScale command hscli: %(cmd_arg)s", + {'cmd_arg': cmdarg_json}) + try: + (out, err) = self._execute('hscli', cmdarg_json, + run_as_root=True, + root_helper=self._root_helper) + + except putils.ProcessExecutionError as e: + msg = (_("Error executing hscli: %(err)s") % {'err': e.stderr}) + raise exception.BrickException(message=msg) + + if err: + msg = (_("Failed to connect volume: stdout=%(out)s " + "stderr=%(err)s") % {'out': out, 'err': err}) + raise exception.BrickException(message=msg) diff --git a/os_brick/tests/initiator/connectors/test_vrtshyperscale.py b/os_brick/tests/initiator/connectors/test_vrtshyperscale.py new file mode 100644 index 000000000..8d8e1f3ff --- /dev/null +++ b/os_brick/tests/initiator/connectors/test_vrtshyperscale.py @@ -0,0 +1,168 @@ +# Copyright (c) 2017 Veritas Technologies LLC +# 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 json + +from oslo_concurrency import processutils + +from os_brick import exception +from os_brick.initiator.connectors import vrtshyperscale +from os_brick.tests.initiator import test_connector + + +class HyperScaleConnectorTestCase(test_connector.ConnectorTestCase): + """Test cases for Veritas HyperScale os-brick connector.""" + + def _fake_execute_success(self, *cmd, **kwargs): + """Mock successful execution of hscli""" + result_json = "" + err = 0 + args = json.loads(cmd[1]) + if args['operation'] == 'connect_volume': + result = {} + payload = {} + payload['vsa_ip'] = '192.0.2.2' + payload['refl_factor'] = '2' + payload['refl_targets'] = '192.0.2.3,192.0.2.4' + result['payload'] = payload + result_json = json.dumps(result) + return (result_json, err) + + def _fake_execute_hscli_missing(self, *cmd, **kwargs): + """Mock attempt to execute missing hscli""" + raise processutils.ProcessExecutionError() + return ("", 0) + + def _fake_execute_hscli_err(self, *cmd, **kwargs): + """Mock hscli returning error""" + result_json = "" + err = 'fake_hscli_error_msg' + return (result_json, err) + + def _fake_execute_hscli_res_inval(self, *cmd, **kwargs): + """Mock hscli returning unexpected values""" + result_json = "" + err = 0 + result = {} + payload = {} + payload['unexpected'] = 'junk' + result['payload'] = payload + result_json = json.dumps(result) + return (result_json, err) + + def test_connect_volume_normal(self): + """Test results of successful connect_volume()""" + connector = vrtshyperscale.HyperScaleConnector( + 'sudo', execute=self._fake_execute_success) + fake_connection_properties = { + 'name': '{8ee71c33-dcd0-4267-8f2b-e0742ecabe9f}' + } + device_info = connector.connect_volume(fake_connection_properties) + + self.assertEqual('192.0.2.2', device_info['vsa_ip']) + self.assertEqual('2', device_info['refl_factor']) + self.assertEqual('192.0.2.3,192.0.2.4', device_info['refl_targets']) + self.assertEqual( + 'oflame://192.0.2.2:9999/' + '%7B8ee71c33-dcd0-4267-8f2b-e0742ecabe9f%7D', + device_info['device_path']) + + def test_connect_volume_arg_missing(self): + """Test connect_volume with missing missing arguments""" + connector = vrtshyperscale.HyperScaleConnector( + 'sudo', execute=self._fake_execute_success) + fake_connection_properties = {} + self.assertRaises(exception.BrickException, + connector.connect_volume, + fake_connection_properties) + + def test_connect_volume_arg_inval(self): + """Test connect_volume with bad volume name argument""" + connector = vrtshyperscale.HyperScaleConnector( + 'sudo', execute=self._fake_execute_success) + fake_connection_properties = { + 'name': 'x' + } + self.assertRaises(exception.BrickException, + connector.connect_volume, + fake_connection_properties) + + def test_connect_volume_hscli_missing(self): + """Test connect_volume that can't call hscli""" + connector = vrtshyperscale.HyperScaleConnector( + 'sudo', execute=self._fake_execute_hscli_missing) + fake_connection_properties = { + 'name': '{8ee71c33-dcd0-4267-8f2b-e0742ecabe9f}' + } + self.assertRaises(exception.BrickException, + connector.connect_volume, + fake_connection_properties) + + def test_connect_volume_hscli_err(self): + """Test connect_volume when hscli returns an error""" + connector = vrtshyperscale.HyperScaleConnector( + 'sudo', execute=self._fake_execute_hscli_err) + fake_connection_properties = { + 'name': '{8ee71c33-dcd0-4267-8f2b-e0742ecabe9f}' + } + self.assertRaises(exception.BrickException, + connector.connect_volume, + fake_connection_properties) + + def test_connect_volume_hscli_res_inval(self): + """Test connect_volume if hscli returns an invalid result""" + connector = vrtshyperscale.HyperScaleConnector( + 'sudo', execute=self._fake_execute_hscli_res_inval) + fake_connection_properties = { + 'name': '{8ee71c33-dcd0-4267-8f2b-e0742ecabe9f}' + } + self.assertRaises(exception.BrickException, + connector.connect_volume, + fake_connection_properties) + + def test_disconnect_volume_normal(self): + """Test successful disconnect_volume call""" + connector = vrtshyperscale.HyperScaleConnector( + 'sudo', execute=self._fake_execute_success) + fake_connection_properties = { + 'name': '{8ee71c33-dcd0-4267-8f2b-e0742ecabe9f}' + } + fake_device_info = {} + connector.disconnect_volume(fake_connection_properties, + fake_device_info) + + def test_disconnect_volume_arg_missing(self): + """Test disconnect_volume with missing arguments""" + connector = vrtshyperscale.HyperScaleConnector( + 'sudo', execute=self._fake_execute_success) + fake_connection_properties = {} + fake_device_info = {} + self.assertRaises(exception.BrickException, + connector.disconnect_volume, + fake_connection_properties, + fake_device_info) + + def test_disconnect_volume_arg_inval(self): + """Test disconnect_volume with invalid volume name argument""" + connector = vrtshyperscale.HyperScaleConnector( + 'sudo', execute=self._fake_execute_success) + fake_connection_properties = { + 'name': 'x' + } + fake_device_info = {} + self.assertRaises(exception.BrickException, + connector.disconnect_volume, + fake_connection_properties, + fake_device_info) diff --git a/releasenotes/notes/veritas-hyperscale-connector-fe56cec68b1947cd.yaml b/releasenotes/notes/veritas-hyperscale-connector-fe56cec68b1947cd.yaml new file mode 100644 index 000000000..bab677a20 --- /dev/null +++ b/releasenotes/notes/veritas-hyperscale-connector-fe56cec68b1947cd.yaml @@ -0,0 +1,3 @@ +--- +features: + - Add Veritas HyperScale connector support