summaryrefslogtreecommitdiffstats
path: root/pyanaconda/storage/devicelibs/edd.py
blob: 30e8004d3c05e5f13ed5ca56100a74c80b964470 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
#
# edd.py
# BIOS EDD data parsing functions
#
# Copyright (C) 2010  Red Hat, Inc.  All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# Author(s): Hans de Goede <hdegoede@redhat.com>
#            Ales Kozumplik <akozumpl@redhat.com>
#

import glob
import logging
import os
import re
import struct

log = logging.getLogger("storage")

re_host_bus = re.compile(r'^PCI\s*(\S*)\s*channel: (\S*)\s*$')
re_interface_scsi = re.compile(r'^SCSI\s*id: (\S*)\s*lun: (\S*)\s*$')
re_interface_ata = re.compile(r'^ATA\s*device: (\S*)\s*$')

class EddEntry(object):
    """ This object merely collects what the /sys/firmware/edd/* entries can
        provide.
    """
    def __init__(self, sysfspath):
        self.type = None

        self.ata_device = None
        self.channel = None
        self.mbr_signature = None
        self.pci_dev = None
        self.scsi_id = None
        self.scsi_lun = None
        self.sectors = None

        self.load(sysfspath)

    def __str__(self):
        return \
            "\ttype: %(type)s, ata_device: %(ata_device)s\n" \
            "\tchannel: %(channel)s, mbr_signature: %(mbr_signature)s\n" \
            "\tpci_dev: %(pci_dev)s, scsi_id: %(scsi_id)s\n" \
            "\tscsi_lun: %(scsi_lun)s, sectors: %(sectors)s" % self.__dict__

    def _read_file(self, filename):
        contents = None
        if os.path.exists(filename):
            with open(filename) as f:
                contents = f.read().rstrip()
        return contents

    def load(self, sysfspath):
        interface = self._read_file(os.path.join(sysfspath, "interface"))
        if interface:
            self.type = interface.split()[0]
            if self.type == "SCSI":
                match = re_interface_scsi.match(interface)
                self.scsi_id = int(match.group(1))
                self.scsi_lun = int(match.group(2))
            elif self.type == "ATA":
                match = re_interface_ata.match(interface)
                self.ata_device = int(match.group(1))

        self.mbr_signature = self._read_file(
            os.path.join(sysfspath, "mbr_signature"))
        sectors = self._read_file(os.path.join(sysfspath, "sectors"))
        if sectors:
            self.sectors = int(sectors)
        hbus = self._read_file(os.path.join(sysfspath, "host_bus"))
        if hbus:
            match = re_host_bus.match(hbus)
            if match:
                self.pci_dev = match.group(1)
                self.channel = int(match.group(2))
            else:
                log.warning("edd: can not match host_bus: %s" % hbus)

class EddMatcher(object):
    """ This object tries to match given entry to a disk device name.

        Assuming, heuristic analysis and guessing hapens here.
    """
    def __init__(self, edd_entry):
        self.edd = edd_entry

    def devname_from_pci_dev(self):
        name = None
        if self.edd.type == "ATA" and \
                self.edd.channel is not None and \
                self.edd.ata_device is not None:
            path = "/sys/devices/pci0000:00/0000:%(pci_dev)s/host%(chan)d/"\
                "target%(chan)d:0:%(dev)d/%(chan)d:0:%(dev)d:0/block" % {
                'pci_dev' : self.edd.pci_dev,
                'chan' : self.edd.channel,
                'dev' : self.edd.ata_device
                }
            if os.path.isdir(path):
                block_entries = os.listdir(path)
                if len(block_entries) == 1:
                    name = block_entries[0]
            else:
                log.warning("edd: directory does not exist: %s" % path)
        elif self.edd.type == "SCSI":
            pattern = "/sys/devices/pci0000:00/0000:%(pci_dev)s/virtio*/block" % \
                {'pci_dev' : self.edd.pci_dev}
            matching_paths = glob.glob(pattern)
            if len(matching_paths) != 1 or not os.path.exists(matching_paths[0]):
                return None
            block_entries = os.listdir(matching_paths[0])
            if len(block_entries) == 1:
                name = block_entries[0]
        return name

    def match_via_mbrsigs(self, mbr_dict):
        """ Try to match the edd entry based on its mbr signature.

            This will obviously fail for a fresh drive/image, but in extreme
            cases can also show false positives for randomly matching data.
        """
        for (name, mbr_signature) in mbr_dict.items():
            if mbr_signature == self.edd.mbr_signature:
                return name
        return None

def biosdev_to_edd_dir(biosdev):
    return "/sys/firmware/edd/int13_dev%x" % biosdev

def collect_edd_data():
    edd_data_dict = {}
    # the hard drive numbering starts at 0x80 (128 decimal):
    for biosdev in range(0x80, 0x80+16):
        sysfspath = biosdev_to_edd_dir(biosdev)
        if not os.path.exists(sysfspath):
            break
        edd_data_dict[biosdev] = EddEntry(sysfspath)
    return edd_data_dict

def collect_mbrs(devices):
    """ Read MBR signatures from devices.

        Returns a dict mapping device names to their MBR signatures. It is not
        guaranteed this will succeed, with a new disk for instance.
    """
    mbr_dict = {}
    for dev in devices:
        try:
            fd = os.open(dev.path, os.O_RDONLY)
            # The signature is the unsigned integer at byte 440:
            os.lseek(fd, 440, 0)
            mbrsig = struct.unpack('I', os.read(fd, 4))
            os.close(fd)
        except OSError as e:
            log.warning("edd: error reading mbrsig from disk %s: %s" %
                        (dev.name, str(e)))
            continue

        mbrsig_str = "0x%08x" % mbrsig
        # sanity check
        if mbrsig_str == '0x00000000':
            log.info("edd: MBR signature on %s is zero. new disk image?" % dev.name)
            continue
        else:
            for (dev_name, mbrsig_str_old) in mbr_dict.items():
                if mbrsig_str_old == mbrsig_str:
                    log.error("edd: dupicite MBR signature %s for %s and %s" %
                              (mbrsig_str, dev_name, dev.name))
                    # this actually makes all the other data useless
                    return {}
        # update the dictionary
        mbr_dict[dev.name] = mbrsig_str
    log.info("edd: collected mbr signatures: %s" % mbr_dict)
    return mbr_dict

def get_edd_dict(devices):
    """ Generates the 'device name' -> 'edd number' mapping.

        The EDD kernel module that exposes /sys/firmware/edd is thoroughly
        broken, the information there is incomplete and sometimes downright
        wrong. So after we mine out all useful information that the files under
        /sys/firmware/edd/int13_*/ can provide, we resort to heuristics and
        guessing. Our first attempt is, by looking at the device type int
        'interface', attempting to map pci device number, channel number etc. to
        a sysfs path, check that the path really exists, then read the device
        name (e.g 'sda') from there. Should this fail we try to match contents
        of 'mbr_signature' to a real MBR signature found on the existing block
        devices.
    """
    mbr_dict = collect_mbrs(devices)
    edd_entries_dict = collect_edd_data()
    global edd_dict
    for (edd_number, edd_entry) in edd_entries_dict.items():
        log.debug("edd: data extracted from 0x%x:\n%s" % (edd_number, edd_entry))
        matcher = EddMatcher(edd_entry)
        # first try to match through the pci dev etc.
        name = matcher.devname_from_pci_dev()
        # next try to compare mbr signatures
        if name:
            log.debug("edd: matched 0x%x to %s using pci_dev" % (edd_number, name))
        else:
            name = matcher.match_via_mbrsigs(mbr_dict)
            if name:
                log.info("edd: matched 0x%x to %s using MBR sig" % (edd_number, name))

        if name:
            old_edd_number = edd_dict.get(name)
            if old_edd_number:
                log.info("edd: both edd entries 0x%x and 0x%x seem to map to %s" %
                          (old_edd_number, edd_number, name))
                # this means all the other data can be confused and useless
                return {}
            edd_dict[name] = edd_number
            continue
        log.error("edd: unable to match edd entry 0x%x" % edd_number)
    return edd_dict

edd_dict = {}