Browse Source

Fix iSCSI multipath rescan

iSCSI multipath rescan uses iscsiadm --rescan option for nodes and
sessions, which can end up recreating devices that had just been removed
if there's a race condition between the removal of a SCSI device and the
connection of a volume.

The race condition happens if a rescan done when attaching happens right
between us removing the path and removing the exported lun, because the
rescan will add not only the new path we are attaching, but the old path
we are removing, since the lun still hasn't been removed.

This would leave orphaned devices that unnecessarily pollute our
environment,

This patch narrows the rescan to only rescan for the specific target id,
channel, and lun number if we can find this information.

When we cannot find this information we do the scan as we were doing it
before.

Closes-Bug: #1664032
Change-Id: I1b3bd34db260165a6ea9ca061f946d6dfcf8553f
tags/1.12.0
Gorka Eguileor 2 years ago
parent
commit
a6e789f27e

+ 8
- 0
os_brick/exception.py View File

@@ -159,3 +159,11 @@ class VolumeEncryptionNotSupported(Invalid):
159 159
 # NOTE(mriedem): This extends ValueError to maintain backward compatibility.
160 160
 class InvalidConnectorProtocol(ValueError):
161 161
     pass
162
+
163
+
164
+class HostChannelsTargetsNotFound(BrickException):
165
+    message = _('Unable to find host, channel, and target for %(iqns)s.')
166
+
167
+    def __init__(self, message=None, iqns=None, found=None):
168
+        super(HostChannelsTargetsNotFound, self).__init__(message, iqns=iqns)
169
+        self.found = found

+ 99
- 21
os_brick/initiator/connectors/iscsi.py View File

@@ -13,6 +13,7 @@
13 13
 #    under the License.
14 14
 
15 15
 
16
+import collections
16 17
 import copy
17 18
 import glob
18 19
 import os
@@ -165,7 +166,8 @@ class ISCSIConnector(base.BaseLinuxConnector, base_iscsi.BaseISCSIConnector):
165 166
             LOG.info(_LI("Multipath discovery for iSCSI enabled"))
166 167
             # Multipath installed, discovering other targets if available
167 168
             try:
168
-                ips_iqns = self._discover_iscsi_portals(connection_properties)
169
+                ips_iqns_luns = self._discover_iscsi_portals(
170
+                    connection_properties)
169 171
             except Exception:
170 172
                 if 'target_portals' in connection_properties:
171 173
                     raise exception.TargetPortalsNotFound(
@@ -186,13 +188,14 @@ class ISCSIConnector(base.BaseLinuxConnector, base_iscsi.BaseISCSIConnector):
186 188
                 # latter, so try the ip,iqn combinations to find the targets
187 189
                 # which constitutes the multipath device.
188 190
                 main_iqn = connection_properties['target_iqn']
189
-                all_portals = set([ip for ip, iqn in ips_iqns])
190
-                match_portals = set([ip for ip, iqn in ips_iqns
191
-                                     if iqn == main_iqn])
191
+                all_portals = {(ip, lun) for ip, iqn, lun in ips_iqns_luns}
192
+                match_portals = {(ip, lun) for ip, iqn, lun in ips_iqns_luns
193
+                                 if iqn == main_iqn}
192 194
                 if len(all_portals) == len(match_portals):
193
-                    ips_iqns = zip(all_portals, [main_iqn] * len(all_portals))
195
+                    ips_iqns_luns = [(p[0], main_iqn, p[1])
196
+                                     for p in all_portals]
194 197
 
195
-            for ip, iqn in ips_iqns:
198
+            for ip, iqn, lun in ips_iqns_luns:
196 199
                 props = copy.deepcopy(connection_properties)
197 200
                 props['target_portal'] = ip
198 201
                 props['target_iqn'] = iqn
@@ -201,7 +204,8 @@ class ISCSIConnector(base.BaseLinuxConnector, base_iscsi.BaseISCSIConnector):
201 204
                         connected_to_portal = True
202 205
 
203 206
             if use_rescan:
204
-                self._rescan_iscsi()
207
+                self._rescan_iscsi(ips_iqns_luns)
208
+
205 209
             host_devices = self._get_device_path(connection_properties)
206 210
         else:
207 211
             LOG.info(_LI("Multipath discovery for iSCSI not enabled."))
@@ -283,12 +287,19 @@ class ISCSIConnector(base.BaseLinuxConnector, base_iscsi.BaseISCSIConnector):
283 287
     def _get_transport(self):
284 288
         return self.transport
285 289
 
290
+    @staticmethod
291
+    def _get_luns(con_props, iqns=None):
292
+        luns = con_props.get('target_luns')
293
+        num_luns = len(con_props['target_iqns']) if iqns is None else len(iqns)
294
+        return luns or [con_props.get('target_lun')] * num_luns
295
+
286 296
     def _discover_iscsi_portals(self, connection_properties):
287 297
         if all([key in connection_properties for key in ('target_portals',
288 298
                                                          'target_iqns')]):
289 299
             # Use targets specified by connection_properties
290
-            return zip(connection_properties['target_portals'],
291
-                       connection_properties['target_iqns'])
300
+            return list(zip(connection_properties['target_portals'],
301
+                        connection_properties['target_iqns'],
302
+                        self._get_luns(connection_properties)))
292 303
 
293 304
         out = None
294 305
         iscsi_transport = ('iser' if self._get_transport() == 'iser'
@@ -332,7 +343,9 @@ class ISCSIConnector(base.BaseLinuxConnector, base_iscsi.BaseISCSIConnector):
332 343
                  '-p', connection_properties['target_portal']],
333 344
                 check_exit_code=[0, 255])[0] or ""
334 345
 
335
-        return self._get_target_portals_from_iscsiadm_output(out)
346
+        ips, iqns = self._get_target_portals_from_iscsiadm_output(out)
347
+        luns = self._get_luns(connection_properties, iqns)
348
+        return list(zip(ips, iqns, luns))
336 349
 
337 350
     def _run_iscsiadm_update_discoverydb(self, connection_properties,
338 351
                                          iscsi_transport='default'):
@@ -607,16 +620,18 @@ class ISCSIConnector(base.BaseLinuxConnector, base_iscsi.BaseISCSIConnector):
607 620
                                   **kwargs)
608 621
 
609 622
     def _get_target_portals_from_iscsiadm_output(self, output):
610
-        # return both portals and iqns
623
+        # return both portals and iqns as 2 lists
611 624
         #
612 625
         # as we are parsing a command line utility, allow for the
613 626
         # possibility that additional debug data is spewed in the
614 627
         # stream, and only grab actual ip / iqn lines.
615
-        targets = []
628
+        ips = []
629
+        iqns = []
616 630
         for data in [line.split() for line in output.splitlines()]:
617 631
             if len(data) == 2 and data[1].startswith('iqn.'):
618
-                targets.append(data)
619
-        return targets
632
+                ips.append(data[0])
633
+                iqns.append(data[1])
634
+        return ips, iqns
620 635
 
621 636
     def _disconnect_volume_multipath_iscsi(self, connection_properties,
622 637
                                            multipath_name):
@@ -637,14 +652,14 @@ class ISCSIConnector(base.BaseLinuxConnector, base_iscsi.BaseISCSIConnector):
637 652
         # Do a discovery to find all targets.
638 653
         # Targets for multiple paths for the same multipath device
639 654
         # may not be the same.
640
-        all_ips_iqns = self._discover_iscsi_portals(connection_properties)
655
+        all_ips_iqns_luns = self._discover_iscsi_portals(connection_properties)
641 656
 
642 657
         # As discovery result may contain other targets' iqns, extract targets
643 658
         # to be disconnected whose block devices are already deleted here.
644 659
         ips_iqns = []
645 660
         entries = [device.lstrip('ip-').split('-lun-')[0]
646 661
                    for device in self._get_iscsi_devices()]
647
-        for ip, iqn in all_ips_iqns:
662
+        for ip, iqn, lun in all_ips_iqns_luns:
648 663
             ip_iqn = "%s-iscsi-%s" % (ip.split(",")[0], iqn)
649 664
             if ip_iqn not in entries:
650 665
                 ips_iqns.append([ip, iqn])
@@ -837,8 +852,71 @@ class ISCSIConnector(base.BaseLinuxConnector, base_iscsi.BaseISCSIConnector):
837 852
                    'out': out, 'err': err})
838 853
         return (out, err)
839 854
 
840
-    def _rescan_iscsi(self):
841
-        self._run_iscsiadm_bare(('-m', 'node', '--rescan'),
842
-                                check_exit_code=[0, 1, 21, 255])
843
-        self._run_iscsiadm_bare(('-m', 'session', '--rescan'),
844
-                                check_exit_code=[0, 1, 21, 255])
855
+    @utils.retry(exception.HostChannelsTargetsNotFound, backoff_rate=1.5)
856
+    def _get_hosts_channels_targets_luns(self, ips_iqns_luns):
857
+        iqns = {iqn: lun for ip, iqn, lun in ips_iqns_luns}
858
+        LOG.debug('Getting hosts, channels, and targets for iqns: %s',
859
+                  iqns.keys())
860
+
861
+        # Get all targets indexed by scsi host path
862
+        targets_paths = glob.glob('/sys/class/scsi_host/host*/device/session*/'
863
+                                  'target*')
864
+        targets = collections.defaultdict(list)
865
+        for path in targets_paths:
866
+            target = path.split('/target')[1]
867
+            host = path.split('/device/')[0]
868
+            targets[host].append(target.split(':'))
869
+
870
+        # Get all scsi targets
871
+        sessions = glob.glob('/sys/class/scsi_host/host*/device/session*/'
872
+                             'iscsi_session/session*/targetname')
873
+
874
+        result = []
875
+        for session in sessions:
876
+            # Read iSCSI target name
877
+            try:
878
+                with open(session, 'r') as f:
879
+                    targetname = f.read().strip('\n')
880
+            except Exception:
881
+                continue
882
+
883
+            # If we are interested in it we store its target information
884
+            if targetname in iqns:
885
+                host = session.split('/device/')[0]
886
+                for __, channel, target_id in targets[host]:
887
+                    result.append((host, channel, target_id, iqns[targetname]))
888
+                # Stop as soon as we have the info of all our iqns, even if
889
+                # there are more sessions to check
890
+                del iqns[targetname]
891
+                if not iqns:
892
+                    break
893
+
894
+        # In some cases the login and udev triggers may not have been fast
895
+        # enough to create all sysfs entries, so we want to retry.
896
+        else:
897
+            raise exception.HostChannelsTargetsNotFound(iqns=iqns.keys(),
898
+                                                        found=result)
899
+        return result
900
+
901
+    def _rescan_iscsi(self, ips_iqns_luns):
902
+        try:
903
+            hctls = self._get_hosts_channels_targets_luns(ips_iqns_luns)
904
+        except exception.HostChannelsTargetsNotFound as e:
905
+            if not e.found:
906
+                LOG.error(_LE('iSCSI scan failed: %s'), e)
907
+                return
908
+
909
+            hctls = e.found
910
+            LOG.warning(_LW('iSCSI scan: %(error)s\nScanning %(hosts)s'),
911
+                        {'error': e, 'hosts': [h for h, c, t, l in hctls]})
912
+
913
+        for host_path, channel, target_id, target_lun in hctls:
914
+            LOG.debug('Scanning host %(host)s c: %(channel)s, '
915
+                      't: %(target)s, l: %(lun)s)',
916
+                      {'host': host_path, 'channel': channel,
917
+                       'target': target_id, 'lun': target_lun})
918
+            self._linuxscsi.echo_scsi_command(
919
+                "%s/scan" % host_path,
920
+                "%(c)s %(t)s %(l)s" % {'c': channel,
921
+                                       't': target_id,
922
+                                       'l': target_lun})

+ 153
- 28
os_brick/tests/initiator/connectors/test_iscsi.py View File

@@ -426,7 +426,7 @@ class ISCSIConnectorTestCase(test_connector.ConnectorTestCase):
426 426
         self.connector_with_multipath = \
427 427
             iscsi.ISCSIConnector(None, use_multipath=True)
428 428
         iscsiadm_mock.return_value = "%s %s" % (location, iqn)
429
-        portals_mock.return_value = [[location, iqn]]
429
+        portals_mock.return_value = ([location], [iqn])
430 430
 
431 431
         result = self.connector_with_multipath.connect_volume(
432 432
             connection_properties['data'])
@@ -525,6 +525,8 @@ class ISCSIConnectorTestCase(test_connector.ConnectorTestCase):
525 525
                           self.connector_with_multipath.connect_volume,
526 526
                           connection_properties['data'])
527 527
 
528
+    @mock.patch.object(iscsi.ISCSIConnector,
529
+                       '_get_hosts_channels_targets_luns', return_value=[])
528 530
     @mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn')
529 531
     @mock.patch.object(os.path, 'exists', return_value=True)
530 532
     @mock.patch.object(host_driver.HostDriver, 'get_all_block_devices')
@@ -538,7 +540,8 @@ class ISCSIConnectorTestCase(test_connector.ConnectorTestCase):
538 540
     def test_connect_volume_with_multiple_portals(
539 541
             self, mock_process_lun_id, mock_discover_mpath_device,
540 542
             mock_get_iqn, mock_run_multipath, mock_iscsi_devices,
541
-            mock_get_device_map, mock_devices, mock_exists, mock_scsi_wwn):
543
+            mock_get_device_map, mock_devices, mock_exists, mock_scsi_wwn,
544
+            mock_get_htcls):
542 545
         mock_scsi_wwn.return_value = test_connector.FAKE_SCSI_WWN
543 546
         location1 = '10.0.2.15:3260'
544 547
         location2 = '[2001:db8::1]:3260'
@@ -547,10 +550,12 @@ class ISCSIConnectorTestCase(test_connector.ConnectorTestCase):
547 550
         name2 = 'volume-00000001-2'
548 551
         iqn1 = 'iqn.2010-10.org.openstack:%s' % name1
549 552
         iqn2 = 'iqn.2010-10.org.openstack:%s' % name2
553
+        lun1 = 1
554
+        lun2 = 2
550 555
         fake_multipath_dev = '/dev/mapper/fake-multipath-dev'
551 556
         vol = {'id': 1, 'name': name1}
552 557
         connection_properties = self.iscsi_connection_multipath(
553
-            vol, [location1, location2], [iqn1, iqn2], [1, 2])
558
+            vol, [location1, location2], [iqn1, iqn2], [lun1, lun2])
554 559
         devs = ['/dev/disk/by-path/ip-%s-iscsi-%s-lun-1' % (location1, iqn1),
555 560
                 '/dev/disk/by-path/ip-%s-iscsi-%s-lun-2' % (dev_loc2, iqn2)]
556 561
         mock_devices.return_value = devs
@@ -558,7 +563,7 @@ class ISCSIConnectorTestCase(test_connector.ConnectorTestCase):
558 563
         mock_get_iqn.return_value = [iqn1, iqn2]
559 564
         mock_discover_mpath_device.return_value = (
560 565
             fake_multipath_dev, test_connector.FAKE_SCSI_WWN)
561
-        mock_process_lun_id.return_value = [1, 2]
566
+        mock_process_lun_id.return_value = [lun1, lun2]
562 567
 
563 568
         result = self.connector_with_multipath.connect_volume(
564 569
             connection_properties['data'])
@@ -580,6 +585,11 @@ class ISCSIConnectorTestCase(test_connector.ConnectorTestCase):
580 585
         for command in expected_commands:
581 586
             self.assertIn(command, self.cmds)
582 587
 
588
+        mock_get_htcls.assert_called_once_with([(location1, iqn1, lun1),
589
+                                                (location2, iqn2, lun2)])
590
+
591
+    @mock.patch.object(iscsi.ISCSIConnector,
592
+                       '_get_hosts_channels_targets_luns', return_value=[])
583 593
     @mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn')
584 594
     @mock.patch.object(os.path, 'exists')
585 595
     @mock.patch.object(host_driver.HostDriver, 'get_all_block_devices')
@@ -595,8 +605,7 @@ class ISCSIConnectorTestCase(test_connector.ConnectorTestCase):
595 605
             self, mock_process_lun_id, mock_discover_mpath_device,
596 606
             mock_iscsiadm, mock_get_iqn, mock_run_multipath,
597 607
             mock_iscsi_devices, mock_get_multipath_device_map,
598
-            mock_devices, mock_exists,
599
-            mock_scsi_wwn):
608
+            mock_devices, mock_exists, mock_scsi_wwn, mock_get_htcls):
600 609
         mock_scsi_wwn.return_value = test_connector.FAKE_SCSI_WWN
601 610
         location1 = '10.0.2.15:3260'
602 611
         location2 = '[2001:db8::1]:3260'
@@ -659,6 +668,12 @@ class ISCSIConnectorTestCase(test_connector.ConnectorTestCase):
659 668
         mock_iscsiadm.assert_any_call(props, ('--logout',),
660 669
                                       check_exit_code=[0, 21, 255])
661 670
 
671
+        lun1, lun2 = connection_properties['data']['target_luns']
672
+        mock_get_htcls.assert_called_once_with([(location1, iqn1, lun1),
673
+                                               (location2, iqn2, lun2)])
674
+
675
+    @mock.patch.object(iscsi.ISCSIConnector,
676
+                       '_get_hosts_channels_targets_luns', return_value=[])
662 677
     @mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn')
663 678
     @mock.patch.object(os.path, 'exists', return_value=True)
664 679
     @mock.patch.object(iscsi.ISCSIConnector,
@@ -671,7 +686,8 @@ class ISCSIConnectorTestCase(test_connector.ConnectorTestCase):
671 686
     def test_connect_volume_with_multipath_connecting(
672 687
             self, mock_discover_mpath_device, mock_run_multipath,
673 688
             mock_iscsi_devices, mock_devices,
674
-            mock_connect, mock_portals, mock_exists, mock_scsi_wwn):
689
+            mock_connect, mock_portals, mock_exists, mock_scsi_wwn,
690
+            mock_get_htcls):
675 691
         mock_scsi_wwn.return_value = test_connector.FAKE_SCSI_WWN
676 692
         location1 = '10.0.2.15:3260'
677 693
         location2 = '[2001:db8::1]:3260'
@@ -687,8 +703,8 @@ class ISCSIConnectorTestCase(test_connector.ConnectorTestCase):
687 703
                 '/dev/disk/by-path/ip-%s-iscsi-%s-lun-2' % (dev_loc2, iqn2)]
688 704
         mock_devices.return_value = devs
689 705
         mock_iscsi_devices.return_value = devs
690
-        mock_portals.return_value = [[location1, iqn1], [location2, iqn1],
691
-                                     [location2, iqn2]]
706
+        mock_portals.return_value = ([location1, location2, location2],
707
+                                     [iqn1, iqn1, iqn2])
692 708
         mock_discover_mpath_device.return_value = (
693 709
             fake_multipath_dev, test_connector.FAKE_SCSI_WWN)
694 710
 
@@ -704,8 +720,16 @@ class ISCSIConnectorTestCase(test_connector.ConnectorTestCase):
704 720
         props2['target_portal'] = locations[1]
705 721
         expected_calls = [mock.call(props1), mock.call(props2)]
706 722
         self.assertEqual(expected_result, result)
723
+        mock_connect.assert_has_calls(expected_calls, any_order=True)
707 724
         self.assertEqual(expected_calls, mock_connect.call_args_list)
708
-
725
+        lun = connection_properties['data']['target_lun']
726
+        self.assertEqual(1, mock_get_htcls.call_count)
727
+        # Order of elements in the list is randomized because it comes from
728
+        # a set.
729
+        self.assertSetEqual({(location1, iqn1, lun), (location2, iqn1, lun)},
730
+                            set(mock_get_htcls.call_args[0][0]))
731
+
732
+    @mock.patch('retrying.time.sleep', mock.Mock())
709 733
     @mock.patch.object(os.path, 'exists', return_value=True)
710 734
     @mock.patch.object(iscsi.ISCSIConnector,
711 735
                        '_get_target_portals_from_iscsiadm_output')
@@ -729,8 +753,8 @@ class ISCSIConnectorTestCase(test_connector.ConnectorTestCase):
729 753
                 '/dev/disk/by-path/ip-%s-iscsi-%s-lun-2' % (location2, iqn2)]
730 754
         mock_devices.return_value = devs
731 755
         mock_iscsi_devices.return_value = devs
732
-        mock_portals.return_value = [[location1, iqn1], [location2, iqn1],
733
-                                     [location2, iqn2]]
756
+        mock_portals.return_value = ([location1, location2, location2],
757
+                                     [iqn1, iqn1, iqn2])
734 758
 
735 759
         mock_connect.return_value = False
736 760
         self.assertRaises(exception.FailedISCSITargetPortalLogin,
@@ -768,9 +792,10 @@ class ISCSIConnectorTestCase(test_connector.ConnectorTestCase):
768 792
         test_output = '''10.15.84.19:3260 iqn.1992-08.com.netapp:sn.33615311
769 793
                          10.15.85.19:3260 iqn.1992-08.com.netapp:sn.33615311'''
770 794
         res = connector._get_target_portals_from_iscsiadm_output(test_output)
771
-        ip_iqn1 = ['10.15.84.19:3260', 'iqn.1992-08.com.netapp:sn.33615311']
772
-        ip_iqn2 = ['10.15.85.19:3260', 'iqn.1992-08.com.netapp:sn.33615311']
773
-        expected = [ip_iqn1, ip_iqn2]
795
+        ips = ['10.15.84.19:3260', '10.15.85.19:3260']
796
+        iqns = ['iqn.1992-08.com.netapp:sn.33615311',
797
+                'iqn.1992-08.com.netapp:sn.33615311']
798
+        expected = (ips, iqns)
774 799
         self.assertEqual(expected, res)
775 800
 
776 801
     @mock.patch.object(os, 'walk')
@@ -832,7 +857,7 @@ class ISCSIConnectorTestCase(test_connector.ConnectorTestCase):
832 857
         portal = '10.0.0.1:3260'
833 858
         dev = ('ip-%s-iscsi-%s-lun-0' % (portal, iqn1))
834 859
 
835
-        get_portals_mock.return_value = [[portal, iqn1]]
860
+        get_portals_mock.return_value = ([portal], [iqn1])
836 861
         multipath_iqn_mock.return_value = iqns
837 862
         get_all_devices_mock.return_value = [dev, '/dev/mapper/md-1']
838 863
         get_multipath_device_map_mock.return_value = {dev: '/dev/mapper/md-3'}
@@ -865,7 +890,7 @@ class ISCSIConnectorTestCase(test_connector.ConnectorTestCase):
865 890
 
866 891
         # Multiple targets are discovered, but only block devices for target-1
867 892
         # is deleted and target-2 is in use.
868
-        get_portals_mock.return_value = [[portal, iqn1], [portal, iqn2]]
893
+        get_portals_mock.return_value = ([portal, portal], [iqn1, iqn2])
869 894
         multipath_iqn_mock.return_value = [iqn2, iqn2]
870 895
         get_all_devices_mock.return_value = [dev2, '/dev/mapper/md-1']
871 896
         get_multipath_map_mock.return_value = {dev2: '/dev/mapper/md-3'}
@@ -897,7 +922,7 @@ class ISCSIConnectorTestCase(test_connector.ConnectorTestCase):
897 922
         name = 'volume-00000001'
898 923
         iqn = 'iqn.2010-10.org.openstack:%s' % name
899 924
 
900
-        get_portals_mock.return_value = [[portal, iqn]]
925
+        get_portals_mock.return_value = ([portal], [iqn])
901 926
         fake_property = {'target_portal': portal,
902 927
                          'target_iqn': iqn}
903 928
         self.connector._disconnect_volume_multipath_iscsi(fake_property,
@@ -925,7 +950,7 @@ class ISCSIConnectorTestCase(test_connector.ConnectorTestCase):
925 950
         iqn = 'iqn.2010-10.org.openstack:%s' % name
926 951
         dev = ('ip-%s-iscsi-%s-lun-0' % (portal, iqn))
927 952
 
928
-        get_portals_mock.return_value = [[portal, iqn]]
953
+        get_portals_mock.return_value = ([portal], [iqn])
929 954
         get_all_devices_mock.return_value = [dev, '/dev/mapper/md-1']
930 955
         get_iscsi_devices_mock.return_value = []
931 956
 
@@ -939,13 +964,11 @@ class ISCSIConnectorTestCase(test_connector.ConnectorTestCase):
939 964
     def test_iscsiadm_discover_parsing(self):
940 965
         # Ensure that parsing iscsiadm discover ignores cruft.
941 966
 
942
-        targets = [
943
-            ["192.168.204.82:3260,1",
944
-             ("iqn.2010-10.org.openstack:volume-"
945
-              "f9b12623-6ce3-4dac-a71f-09ad4249bdd3")],
946
-            ["192.168.204.82:3261,1",
947
-             ("iqn.2010-10.org.openstack:volume-"
948
-              "f9b12623-6ce3-4dac-a71f-09ad4249bdd4")]]
967
+        ips = ["192.168.204.82:3260,1", "192.168.204.82:3261,1"]
968
+        iqns = ["iqn.2010-10.org.openstack:volume-"
969
+                "f9b12623-6ce3-4dac-a71f-09ad4249bdd3",
970
+                "iqn.2010-10.org.openstack:volume-"
971
+                "f9b12623-6ce3-4dac-a71f-09ad4249bdd4"]
949 972
 
950 973
         # This slight wonkiness brought to you by pep8, as the actual
951 974
         # example output runs about 97 chars wide.
@@ -954,10 +977,10 @@ Starting iSCSI initiator service: done
954 977
 Setting up iSCSI targets: unused
955 978
 %s %s
956 979
 %s %s
957
-""" % (targets[0][0], targets[0][1], targets[1][0], targets[1][1])
980
+""" % (ips[0], iqns[0], ips[1], iqns[1])
958 981
         out = self.connector.\
959 982
             _get_target_portals_from_iscsiadm_output(sample_input)
960
-        self.assertEqual(out, targets)
983
+        self.assertEqual((ips, iqns), out)
961 984
 
962 985
     def test_sanitize_log_run_iscsiadm(self):
963 986
         # Tests that the parameters to the _run_iscsiadm function
@@ -1037,3 +1060,105 @@ Setting up iSCSI targets: unused
1037 1060
         self.assertRaises(exception.TargetPortalsNotFound,
1038 1061
                           self.connector._get_potential_volume_paths,
1039 1062
                           connection_properties)
1063
+
1064
+    @mock.patch.object(iscsi.ISCSIConnector,
1065
+                       '_get_hosts_channels_targets_luns')
1066
+    def test_rescan_iscsi_no_hctls(self, mock_get_htcls):
1067
+        mock_get_htcls.side_effect = exception.HostChannelsTargetsNotFound(
1068
+            iqns=['iqn1', 'iqn2'], found=[])
1069
+        with mock.patch.object(self.connector, '_linuxscsi') as mock_linuxscsi:
1070
+            self.connector._rescan_iscsi(mock.sentinel.input)
1071
+            mock_linuxscsi.echo_scsi_command.assert_not_called()
1072
+        mock_get_htcls.assert_called_once_with(mock.sentinel.input)
1073
+
1074
+    @mock.patch.object(iscsi.ISCSIConnector,
1075
+                       '_get_hosts_channels_targets_luns')
1076
+    def test_rescan_iscsi_partial_hctls(self, mock_get_htcls):
1077
+        mock_get_htcls.side_effect = exception.HostChannelsTargetsNotFound(
1078
+            iqns=['iqn1'], found=[('h', 'c', 't', 'l')])
1079
+        with mock.patch.object(self.connector, '_linuxscsi') as mock_linuxscsi:
1080
+            self.connector._rescan_iscsi(mock.sentinel.input)
1081
+            mock_linuxscsi.echo_scsi_command.assert_called_once_with(
1082
+                'h/scan', 'c t l')
1083
+        mock_get_htcls.assert_called_once_with(mock.sentinel.input)
1084
+
1085
+    @mock.patch.object(iscsi.ISCSIConnector,
1086
+                       '_get_hosts_channels_targets_luns')
1087
+    @mock.patch.object(iscsi.ISCSIConnector, '_run_iscsiadm_bare')
1088
+    def test_rescan_iscsi_hctls(self, mock_iscsiadm, mock_get_htcls):
1089
+        mock_get_htcls.return_value = [
1090
+            ('/sys/class/iscsi_host/host4', '0', '0', '1'),
1091
+            ('/sys/class/iscsi_host/host5', '0', '0', '2'),
1092
+        ]
1093
+
1094
+        with mock.patch.object(self.connector, '_linuxscsi') as mock_linuxscsi:
1095
+            self.connector._rescan_iscsi(mock.sentinel.input)
1096
+            mock_linuxscsi.echo_scsi_command.assert_has_calls((
1097
+                mock.call('/sys/class/iscsi_host/host4/scan', '0 0 1'),
1098
+                mock.call('/sys/class/iscsi_host/host5/scan', '0 0 2'),
1099
+            ))
1100
+        mock_get_htcls.assert_called_once_with(mock.sentinel.input)
1101
+        mock_iscsiadm.assert_not_called()
1102
+
1103
+    @mock.patch('six.moves.builtins.open', create=True)
1104
+    @mock.patch('glob.glob')
1105
+    def test_get_hctls(self, mock_glob, mock_open):
1106
+        host4 = '/sys/class/scsi_host/host4'
1107
+        host5 = '/sys/class/scsi_host/host5'
1108
+        host6 = '/sys/class/scsi_host/host6'
1109
+        host7 = '/sys/class/scsi_host/host7'
1110
+
1111
+        mock_glob.side_effect = (
1112
+            (host4 + '/device/session5/target0:1:2',
1113
+             host5 + '/device/session6/target3:4:5',
1114
+             host6 + '/device/session7/target6:7:8',
1115
+             host7 + '/device/session8/target9:10:11'),
1116
+            (host4 + '/device/session5/iscsi_session/session5/targetname',
1117
+             host5 + '/device/session6/iscsi_session/session6/targetname',
1118
+             host6 + '/device/session7/iscsi_session/session7/targetname',
1119
+             host7 + '/device/session8/iscsi_session/session8/targetname'),
1120
+        )
1121
+
1122
+        mock_open.side_effect = (
1123
+            mock.mock_open(read_data='iqn0\n').return_value,
1124
+            mock.mock_open(read_data='iqn1\n').return_value,
1125
+            mock.mock_open(read_data='iqn2\n').return_value,
1126
+            mock.mock_open(read_data='iqn3\n').return_value,
1127
+        )
1128
+
1129
+        ips_iqns_luns = [('ip1', 'iqn1', 'lun1'), ('ip2', 'iqn2', 'lun2')]
1130
+        result = self.connector._get_hosts_channels_targets_luns(ips_iqns_luns)
1131
+        self.assertEqual(
1132
+            [(host5, '4', '5', 'lun1'), (host6, '7', '8', 'lun2')],
1133
+            result)
1134
+        mock_glob.assert_has_calls((
1135
+            mock.call('/sys/class/scsi_host/host*/device/session*/target*'),
1136
+            mock.call('/sys/class/scsi_host/host*/device/session*/'
1137
+                      'iscsi_session/session*/targetname'),
1138
+        ))
1139
+        self.assertEqual(3, mock_open.call_count)
1140
+
1141
+    @mock.patch('retrying.time.sleep', mock.Mock())
1142
+    @mock.patch('six.moves.builtins.open', create=True)
1143
+    @mock.patch('glob.glob', return_value=[])
1144
+    def test_get_hctls_not_found(self, mock_glob, mock_open):
1145
+        host4 = '/sys/class/scsi_host/host4'
1146
+        mock_glob.side_effect = [
1147
+            [(host4 + '/device/session5/target0:1:2')],
1148
+            [(host4 + '/device/session5/iscsi_session/session5/targetname')],
1149
+        ] * 3
1150
+        # Test exception on open as well as having only half of the htcls
1151
+        mock_open.side_effect = [
1152
+            mock.Mock(side_effect=Exception()),
1153
+            mock.mock_open(read_data='iqn1\n').return_value,
1154
+            mock.mock_open(read_data='iqn1\n').return_value,
1155
+        ]
1156
+
1157
+        ips_iqns_luns = [('ip1', 'iqn1', 'lun1'), ('ip2', 'iqn2', 'lun2')]
1158
+
1159
+        exc = self.assertRaises(
1160
+            exception.HostChannelsTargetsNotFound,
1161
+            self.connector._get_hosts_channels_targets_luns, ips_iqns_luns)
1162
+
1163
+        # Verify exception contains found results
1164
+        self.assertEqual([(host4, '1', '2', 'lun1')], exc.found)

Loading…
Cancel
Save