1# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
2#
3#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4#    not use this file except in compliance with the License. You may obtain
5#    a copy of the License at
6#
7#         http://www.apache.org/licenses/LICENSE-2.0
8#
9#    Unless required by applicable law or agreed to in writing, software
10#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12#    License for the specific language governing permissions and limitations
13#    under the License.
14
15"""Generic linux Fibre Channel utilities."""
16
17import errno
18import os
19
20from oslo_concurrency import processutils as putils
21from oslo_log import log as logging
22
23from os_brick.initiator import linuxscsi
24
25LOG = logging.getLogger(__name__)
26
27
28class LinuxFibreChannel(linuxscsi.LinuxSCSI):
29
30    def has_fc_support(self):
31        FC_HOST_SYSFS_PATH = '/sys/class/fc_host'
32        if os.path.isdir(FC_HOST_SYSFS_PATH):
33            return True
34        else:
35            return False
36
37    def _get_hba_channel_scsi_target_lun(self, hba, conn_props):
38        """Get HBA channels, SCSI targets, LUNs to FC targets for given HBA.
39
40        Given an HBA and the connection properties we look for the HBA channels
41        and SCSI targets for each of the FC targets that this HBA has been
42        granted permission to connect.
43
44        For drivers that don't return an initiator to target map we try to find
45        the info for all the target ports.
46
47        For drivers that return an initiator_target_map we use the
48        initiator_target_lun_map entry that was generated by the FC connector
49        based on the contents of the connection information data to know which
50        target ports to look for.
51
52        :returns: 2-Tuple with the first entry being a list of [c, t, l]
53        entries where the target port was found, and the second entry of the
54        tuple being a set of luns for ports that were not found.
55        """
56        # We want the targets' WWPNs, so we use the initiator_target_map if
57        # present for this hba or default to targets if not present.
58        targets = conn_props['targets']
59        if conn_props.get('initiator_target_map') is not None:
60            # This map we try to use was generated by the FC connector
61            targets = conn_props['initiator_target_lun_map'].get(
62                hba['port_name'], targets)
63
64        # Leave only the number from the host_device field (ie: host6)
65        host_device = hba['host_device']
66        if host_device and len(host_device) > 4:
67            host_device = host_device[4:]
68
69        path = '/sys/class/fc_transport/target%s:' % host_device
70        ctls = []
71        luns_not_found = set()
72        for wwpn, lun in targets:
73            cmd = 'grep -Gil "%(wwpns)s" %(path)s*/port_name' % {'wwpns': wwpn,
74                                                                 'path': path}
75            try:
76                # We need to run command in shell to expand the * glob
77                out, _err = self._execute(cmd, shell=True)
78                ctls += [line.split('/')[4].split(':')[1:] + [lun]
79                         for line in out.split('\n') if line.startswith(path)]
80            except Exception as exc:
81                LOG.debug('Could not get HBA channel and SCSI target ID, path:'
82                          ' %(path)s*, reason: %(reason)s', {'path': path,
83                                                             'reason': exc})
84                # If we didn't find any paths add it to the not found list
85                luns_not_found.add(lun)
86        return ctls, luns_not_found
87
88    def rescan_hosts(self, hbas, connection_properties):
89        LOG.debug('Rescaning HBAs %(hbas)s with connection properties '
90                  '%(conn_props)s', {'hbas': hbas,
91                                     'conn_props': connection_properties})
92        # Use initiator_target_lun_map (generated from initiator_target_map by
93        # the FC connector) as HBA exclusion map
94        ports = connection_properties.get('initiator_target_lun_map')
95        if ports:
96            hbas = [hba for hba in hbas if hba['port_name'] in ports]
97            LOG.debug('Using initiator target map to exclude HBAs: %s',
98                      hbas)
99
100        # Most storage arrays get their target ports automatically detected
101        # by the Linux FC initiator and sysfs gets populated with that
102        # information, but there are some that don't.  We'll do a narrow scan
103        # using the channel, target, and LUN for the former and a wider scan
104        # for the latter.  If all paths to a former type of array were down on
105        # the system boot the array could look like it's of the latter type
106        # and make us bring us unwanted volumes into the system by doing a
107        # broad scan.  To prevent this from happening Cinder drivers can use
108        # the "enable_wildcard_scan" key in the connection_info to let us know
109        # they don't want us to do broad scans even in those cases.
110        broad_scan = connection_properties.get('enable_wildcard_scan', True)
111        if not broad_scan:
112            LOG.debug('Connection info disallows broad SCSI scanning')
113
114        process = []
115        skipped = []
116        get_ctls = self._get_hba_channel_scsi_target_lun
117        for hba in hbas:
118            ctls, luns_wildcards = get_ctls(hba, connection_properties)
119            # If we found the target ports, ignore HBAs that din't find them
120            if ctls:
121                process.append((hba, ctls))
122
123            # If target ports not found and should have, then the HBA is not
124            # connected to our storage
125            elif not broad_scan:
126                LOG.debug('Skipping HBA %s, nothing to scan, target port '
127                          'not connected to initiator', hba['node_name'])
128
129            # If we haven't found any target ports we may need to do broad
130            # SCSI scans
131            elif not process:
132                skipped.append((hba,
133                                [('-', '-', lun) for lun in luns_wildcards]))
134
135        # If we didn't find any target ports use wildcards if they are enabled
136        process = process or skipped
137
138        for hba, ctls in process:
139            for hba_channel, target_id, target_lun in ctls:
140                LOG.debug('Scanning %(host)s (wwnn: %(wwnn)s, c: '
141                          '%(channel)s, t: %(target)s, l: %(lun)s)',
142                          {'host': hba['host_device'],
143                           'wwnn': hba['node_name'], 'channel': hba_channel,
144                           'target': target_id, 'lun': target_lun})
145                self.echo_scsi_command(
146                    "/sys/class/scsi_host/%s/scan" % hba['host_device'],
147                    "%(c)s %(t)s %(l)s" % {'c': hba_channel,
148                                           't': target_id,
149                                           'l': target_lun})
150
151    def get_fc_hbas(self):
152        """Get the Fibre Channel HBA information."""
153
154        if not self.has_fc_support():
155            # there is no FC support in the kernel loaded
156            # so there is no need to even try to run systool
157            LOG.debug("No Fibre Channel support detected on system.")
158            return []
159
160        out = None
161        try:
162            out, _err = self._execute('systool', '-c', 'fc_host', '-v',
163                                      run_as_root=True,
164                                      root_helper=self._root_helper)
165        except putils.ProcessExecutionError as exc:
166            # This handles the case where rootwrap is used
167            # and systool is not installed
168            # 96 = nova.cmd.rootwrap.RC_NOEXECFOUND:
169            if exc.exit_code == 96:
170                LOG.warning("systool is not installed")
171            return []
172        except OSError as exc:
173            # This handles the case where rootwrap is NOT used
174            # and systool is not installed
175            if exc.errno == errno.ENOENT:
176                LOG.warning("systool is not installed")
177            return []
178
179        # No FC HBAs were found
180        if out is None:
181            return []
182
183        lines = out.split('\n')
184        # ignore the first 2 lines
185        lines = lines[2:]
186        hbas = []
187        hba = {}
188        lastline = None
189        for line in lines:
190            line = line.strip()
191            # 2 newlines denotes a new hba port
192            if line == '' and lastline == '':
193                if len(hba) > 0:
194                    hbas.append(hba)
195                    hba = {}
196            else:
197                val = line.split('=')
198                if len(val) == 2:
199                    key = val[0].strip().replace(" ", "")
200                    value = val[1].strip()
201                    hba[key] = value.replace('"', '')
202            lastline = line
203
204        return hbas
205
206    def get_fc_hbas_info(self):
207        """Get Fibre Channel WWNs and device paths from the system, if any."""
208
209        # Note(walter-boring) modern Linux kernels contain the FC HBA's in /sys
210        # and are obtainable via the systool app
211        hbas = self.get_fc_hbas()
212
213        hbas_info = []
214        for hba in hbas:
215            wwpn = hba['port_name'].replace('0x', '')
216            wwnn = hba['node_name'].replace('0x', '')
217            device_path = hba['ClassDevicepath']
218            device = hba['ClassDevice']
219            hbas_info.append({'port_name': wwpn,
220                              'node_name': wwnn,
221                              'host_device': device,
222                              'device_path': device_path})
223        return hbas_info
224
225    def get_fc_wwpns(self):
226        """Get Fibre Channel WWPNs from the system, if any."""
227
228        # Note(walter-boring) modern Linux kernels contain the FC HBA's in /sys
229        # and are obtainable via the systool app
230        hbas = self.get_fc_hbas()
231
232        wwpns = []
233        for hba in hbas:
234            if hba['port_state'] == 'Online':
235                wwpn = hba['port_name'].replace('0x', '')
236                wwpns.append(wwpn)
237
238        return wwpns
239
240    def get_fc_wwnns(self):
241        """Get Fibre Channel WWNNs from the system, if any."""
242
243        # Note(walter-boring) modern Linux kernels contain the FC HBA's in /sys
244        # and are obtainable via the systool app
245        hbas = self.get_fc_hbas()
246
247        wwnns = []
248        for hba in hbas:
249            if hba['port_state'] == 'Online':
250                wwnn = hba['node_name'].replace('0x', '')
251                wwnns.append(wwnn)
252
253        return wwnns
254
255
256class LinuxFibreChannelS390X(LinuxFibreChannel):
257    def get_fc_hbas_info(self):
258        """Get Fibre Channel WWNs and device paths from the system, if any."""
259
260        hbas = self.get_fc_hbas()
261
262        hbas_info = []
263        for hba in hbas:
264            if hba['port_state'] == 'Online':
265                wwpn = hba['port_name'].replace('0x', '')
266                wwnn = hba['node_name'].replace('0x', '')
267                device_path = hba['ClassDevicepath']
268                device = hba['ClassDevice']
269                hbas_info.append({'port_name': wwpn,
270                                  'node_name': wwnn,
271                                  'host_device': device,
272                                  'device_path': device_path})
273        return hbas_info
274
275    def configure_scsi_device(self, device_number, target_wwn, lun):
276        """Write the LUN to the port's unit_add attribute.
277
278        If auto-discovery of Fibre-Channel target ports is
279        disabled on s390 platforms, ports need to be added to
280        the configuration.
281        If auto-discovery of LUNs is disabled on s390 platforms
282        luns need to be added to the configuration through the
283        unit_add interface
284        """
285        LOG.debug("Configure lun for s390: device_number=%(device_num)s "
286                  "target_wwn=%(target_wwn)s target_lun=%(target_lun)s",
287                  {'device_num': device_number,
288                   'target_wwn': target_wwn,
289                   'target_lun': lun})
290        filepath = ("/sys/bus/ccw/drivers/zfcp/%s/%s" %
291                    (device_number, target_wwn))
292        if not (os.path.exists(filepath)):
293            zfcp_device_command = ("/sys/bus/ccw/drivers/zfcp/%s/port_rescan" %
294                                   (device_number))
295            LOG.debug("port_rescan call for s390: %s", zfcp_device_command)
296            try:
297                self.echo_scsi_command(zfcp_device_command, "1")
298            except putils.ProcessExecutionError as exc:
299                LOG.warning("port_rescan call for s390 failed exit"
300                            " %(code)s, stderr %(stderr)s",
301                            {'code': exc.exit_code, 'stderr': exc.stderr})
302
303        zfcp_device_command = ("/sys/bus/ccw/drivers/zfcp/%s/%s/unit_add" %
304                               (device_number, target_wwn))
305        LOG.debug("unit_add call for s390 execute: %s", zfcp_device_command)
306        try:
307            self.echo_scsi_command(zfcp_device_command, lun)
308        except putils.ProcessExecutionError as exc:
309            LOG.warning("unit_add call for s390 failed exit %(code)s, "
310                        "stderr %(stderr)s",
311                        {'code': exc.exit_code, 'stderr': exc.stderr})
312
313    def deconfigure_scsi_device(self, device_number, target_wwn, lun):
314        """Write the LUN to the port's unit_remove attribute.
315
316        If auto-discovery of LUNs is disabled on s390 platforms
317        luns need to be removed from the configuration through the
318        unit_remove interface
319        """
320        LOG.debug("Deconfigure lun for s390: "
321                  "device_number=%(device_num)s "
322                  "target_wwn=%(target_wwn)s target_lun=%(target_lun)s",
323                  {'device_num': device_number,
324                   'target_wwn': target_wwn,
325                   'target_lun': lun})
326        zfcp_device_command = ("/sys/bus/ccw/drivers/zfcp/%s/%s/unit_remove" %
327                               (device_number, target_wwn))
328        LOG.debug("unit_remove call for s390 execute: %s", zfcp_device_command)
329        try:
330            self.echo_scsi_command(zfcp_device_command, lun)
331        except putils.ProcessExecutionError as exc:
332            LOG.warning("unit_remove call for s390 failed exit %(code)s, "
333                        "stderr %(stderr)s",
334                        {'code': exc.exit_code, 'stderr': exc.stderr})
335