diff --git a/os_brick/initiator/connector.py b/os_brick/initiator/connector.py index 2c41d7e50..949d7d44b 100644 --- a/os_brick/initiator/connector.py +++ b/os_brick/initiator/connector.py @@ -192,6 +192,12 @@ class InitiatorConnector(executor.Executor): execute=execute, device_scan_attempts=device_scan_attempts, *args, **kwargs) + elif protocol == "HGST": + return HGSTConnector(root_helper=root_helper, + driver=driver, + execute=execute, + device_scan_attempts=device_scan_attempts, + *args, **kwargs) else: msg = (_("Invalid InitiatorConnector protocol " "specified %(protocol)s") % @@ -1333,3 +1339,127 @@ class HuaweiStorHyperConnector(InitiatorConnector): return analyse_result else: return None + + +class HGSTConnector(InitiatorConnector): + """Connector class to attach/detach HGST volumes.""" + VGCCLUSTER = 'vgc-cluster' + + def __init__(self, root_helper, driver=None, + execute=putils.execute, + device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT, + *args, **kwargs): + super(HGSTConnector, self).__init__(root_helper, driver=driver, + execute=execute, + device_scan_attempts= + device_scan_attempts, + *args, **kwargs) + self._vgc_host = None + + def _log_cli_err(self, err): + """Dumps the full command output to a logfile in error cases.""" + LOG.error(_LE("CLI fail: '%(cmd)s' = %(code)s\nout: %(stdout)s\n" + "err: %(stderr)s"), + {'cmd': err.cmd, 'code': err.exit_code, + 'stdout': err.stdout, 'stderr': err.stderr}) + + def _find_vgc_host(self): + """Finds vgc-cluster hostname for this box.""" + params = [self.VGCCLUSTER, "domain-list", "-1"] + try: + out, unused = self._execute(*params, run_as_root=True, + root_helper=self._root_helper) + except putils.ProcessExecutionError as err: + self._log_cli_err(err) + msg = _("Unable to get list of domain members, check that " + "the cluster is running.") + raise exception.BrickException(message=msg) + domain = out.splitlines() + params = ["ip", "addr", "list"] + try: + out, unused = self._execute(*params, run_as_root=False) + except putils.ProcessExecutionError as err: + self._log_cli_err(err) + msg = _("Unable to get list of IP addresses on this host, " + "check permissions and networking.") + raise exception.BrickException(message=msg) + nets = out.splitlines() + for host in domain: + try: + ip = socket.gethostbyname(host) + for l in nets: + x = l.strip() + if x.startswith("inet %s/" % ip): + return host + except socket.error: + pass + msg = _("Current host isn't part of HGST domain.") + raise exception.BrickException(message=msg) + + def _hostname(self): + """Returns hostname to use for cluster operations on this box.""" + if self._vgc_host is None: + self._vgc_host = self._find_vgc_host() + return self._vgc_host + + def connect_volume(self, connection_properties): + """Attach a Space volume to running host. + + connection_properties for HGST must include: + name - Name of space to attach + """ + if connection_properties is None: + msg = _("Connection properties passed in as None.") + raise exception.BrickException(message=msg) + if 'name' not in connection_properties: + msg = _("Connection properties missing 'name' field.") + raise exception.BrickException(message=msg) + device_info = { + 'type': 'block', + 'device': connection_properties['name'], + 'path': '/dev/' + connection_properties['name'] + } + volname = device_info['device'] + params = [self.VGCCLUSTER, 'space-set-apphosts'] + params += ['-n', volname] + params += ['-A', self._hostname()] + params += ['--action', 'ADD'] + try: + self._execute(*params, run_as_root=True, + root_helper=self._root_helper) + except putils.ProcessExecutionError as err: + self._log_cli_err(err) + msg = (_("Unable to set apphost for space %s") % volname) + raise exception.BrickException(message=msg) + + return device_info + + def disconnect_volume(self, connection_properties, device_info): + """Detach and flush the volume. + + connection_properties for HGST must include: + name - Name of space to detach + noremovehost - Host which should never be removed + """ + if connection_properties is None: + msg = _("Connection properties passed in as None.") + raise exception.BrickException(message=msg) + if 'name' not in connection_properties: + msg = _("Connection properties missing 'name' field.") + raise exception.BrickException(message=msg) + if 'noremovehost' not in connection_properties: + msg = _("Connection properties missing 'noremovehost' field.") + raise exception.BrickException(message=msg) + if connection_properties['noremovehost'] != self._hostname(): + params = [self.VGCCLUSTER, 'space-set-apphosts'] + params += ['-n', connection_properties['name']] + params += ['-A', self._hostname()] + params += ['--action', 'DELETE'] + try: + self._execute(*params, run_as_root=True, + root_helper=self._root_helper) + except putils.ProcessExecutionError as err: + self._log_cli_err(err) + msg = (_("Unable to set apphost for space %s") % + connection_properties['name']) + raise exception.BrickException(message=msg) diff --git a/os_brick/tests/initiator/test_connector.py b/os_brick/tests/initiator/test_connector.py index 8d3c253cd..3106bf3d2 100644 --- a/os_brick/tests/initiator/test_connector.py +++ b/os_brick/tests/initiator/test_connector.py @@ -1319,3 +1319,173 @@ class HuaweiStorHyperConnectorTestCase(ConnectorTestCase): LOG.debug("self.cmds = %s." % self.cmds) LOG.debug("expected = %s." % expected_commands) + + +class HGSTConnectorTestCase(ConnectorTestCase): + """Test cases for HGST initiator class.""" + + IP_OUTPUT = """ +1: lo: mtu 65536 qdisc noqueue state UNKNOWN + link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 + inet 127.0.0.1/8 scope host lo + valid_lft forever preferred_lft forever + inet 169.254.169.254/32 scope link lo + valid_lft forever preferred_lft forever + inet6 ::1/128 scope host + valid_lft forever preferred_lft forever +2: em1: mtu 1500 qdisc mq master + link/ether 00:25:90:d9:18:08 brd ff:ff:ff:ff:ff:ff + inet6 fe80::225:90ff:fed9:1808/64 scope link + valid_lft forever preferred_lft forever +3: em2: mtu 1500 qdisc mq state + link/ether 00:25:90:d9:18:09 brd ff:ff:ff:ff:ff:ff + inet 192.168.0.23/24 brd 192.168.0.255 scope global em2 + valid_lft forever preferred_lft forever + inet6 fe80::225:90ff:fed9:1809/64 scope link + valid_lft forever preferred_lft forever + """ + + DOMAIN_OUTPUT = """localhost""" + + DOMAIN_FAILED = """this.better.not.resolve.to.a.name.or.else""" + + SET_APPHOST_OUTPUT = """ +VLVM_SET_APPHOSTS0000000395 +Request Succeeded + """ + + def setUp(self): + super(HGSTConnectorTestCase, self).setUp() + self.connector = connector.HGSTConnector( + None, execute=self._fake_exec) + self._fail_set_apphosts = False + self._fail_ip = False + self._fail_domain_list = False + + def _fake_exec_set_apphosts(self, *cmd): + if self._fail_set_apphosts: + raise putils.ProcessExecutionError(None, None, 1) + else: + return self.SET_APPHOST_OUTPUT, '' + + def _fake_exec_ip(self, *cmd): + if self._fail_ip: + # Remove localhost so there is no IP match + return self.IP_OUTPUT.replace("127.0.0.1", "x.x.x.x"), '' + else: + return self.IP_OUTPUT, '' + + def _fake_exec_domain_list(self, *cmd): + if self._fail_domain_list: + return self.DOMAIN_FAILED, '' + else: + return self.DOMAIN_OUTPUT, '' + + def _fake_exec(self, *cmd, **kwargs): + self.cmdline = " ".join(cmd) + if cmd[0] == "ip": + return self._fake_exec_ip(*cmd) + elif cmd[0] == "vgc-cluster": + if cmd[1] == "domain-list": + return self._fake_exec_domain_list(*cmd) + elif cmd[1] == "space-set-apphosts": + return self._fake_exec_set_apphosts(*cmd) + else: + return '', '' + + def test_factory(self): + """Can we instantiate a HGSTConnector of the right kind?""" + obj = connector.InitiatorConnector.factory('HGST', None) + self.assertEqual("HGSTConnector", obj.__class__.__name__) + + def test_connect_volume(self): + """Tests that a simple connection succeeds""" + self._fail_set_apphosts = False + self._fail_ip = False + self._fail_domain_list = False + cprops = {'name': 'space', 'noremovehost': 'stor1'} + dev_info = self.connector.connect_volume(cprops) + self.assertEqual('block', dev_info['type']) + self.assertEqual('space', dev_info['device']) + self.assertEqual('/dev/space', dev_info['path']) + + def test_connect_volume_nohost_fail(self): + """This host should not be found, connect should fail.""" + self._fail_set_apphosts = False + self._fail_ip = True + self._fail_domain_list = False + cprops = {'name': 'space', 'noremovehost': 'stor1'} + self.assertRaises(exception.BrickException, + self.connector.connect_volume, + cprops) + + def test_connect_volume_nospace_fail(self): + """The space command will fail, exception to be thrown""" + self._fail_set_apphosts = True + self._fail_ip = False + self._fail_domain_list = False + cprops = {'name': 'space', 'noremovehost': 'stor1'} + self.assertRaises(exception.BrickException, + self.connector.connect_volume, + cprops) + + def test_disconnect_volume(self): + """Simple disconnection should pass and disconnect me""" + self._fail_set_apphosts = False + self._fail_ip = False + self._fail_domain_list = False + self._cmdline = "" + cprops = {'name': 'space', 'noremovehost': 'stor1'} + self.connector.disconnect_volume(cprops, None) + exp_cli = ("vgc-cluster space-set-apphosts -n space " + "-A localhost --action DELETE") + self.assertEqual(exp_cli, self.cmdline) + + def test_disconnect_volume_nohost(self): + """Should not run a setapphosts because localhost will""" + """be the noremotehost""" + self._fail_set_apphosts = False + self._fail_ip = False + self._fail_domain_list = False + self._cmdline = "" + cprops = {'name': 'space', 'noremovehost': 'localhost'} + self.connector.disconnect_volume(cprops, None) + # The last command should be the IP listing, not set apphosts + exp_cli = ("ip addr list") + self.assertEqual(exp_cli, self.cmdline) + + def test_disconnect_volume_fails(self): + """The set-apphosts should fail, exception to be thrown""" + self._fail_set_apphosts = True + self._fail_ip = False + self._fail_domain_list = False + self._cmdline = "" + cprops = {'name': 'space', 'noremovehost': 'stor1'} + self.assertRaises(exception.BrickException, + self.connector.disconnect_volume, + cprops, None) + + def test_bad_connection_properties(self): + """Send in connection_properties missing required fields""" + # Invalid connection_properties + self.assertRaises(exception.BrickException, + self.connector.connect_volume, + None) + # Name required for connect_volume + cprops = {'noremovehost': 'stor1'} + self.assertRaises(exception.BrickException, + self.connector.connect_volume, + cprops) + # Invalid connection_properties + self.assertRaises(exception.BrickException, + self.connector.disconnect_volume, + None, None) + # Name and noremovehost needed for disconnect_volume + cprops = {'noremovehost': 'stor1'} + self.assertRaises(exception.BrickException, + self.connector.disconnect_volume, + cprops, None) + cprops = {'name': 'space'} + self.assertRaises(exception.BrickException, + self.connector.disconnect_volume, + cprops, None)