1 |
charliebrady |
1.1 |
# |
2 |
|
|
# edd.py |
3 |
|
|
# BIOS EDD data parsing functions |
4 |
|
|
# |
5 |
|
|
# Copyright (C) 2010 Red Hat, Inc. All rights reserved. |
6 |
|
|
# |
7 |
|
|
# This program is free software; you can redistribute it and/or modify |
8 |
|
|
# it under the terms of the GNU General Public License as published by |
9 |
|
|
# the Free Software Foundation; either version 2 of the License, or |
10 |
|
|
# (at your option) any later version. |
11 |
|
|
# |
12 |
|
|
# This program is distributed in the hope that it will be useful, |
13 |
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
14 |
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
15 |
|
|
# GNU General Public License for more details. |
16 |
|
|
# |
17 |
|
|
# You should have received a copy of the GNU General Public License |
18 |
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
19 |
|
|
# |
20 |
|
|
# Author(s): Hans de Goede <hdegoede@redhat.com> |
21 |
|
|
# Ales Kozumplik <akozumpl@redhat.com> |
22 |
|
|
# |
23 |
|
|
|
24 |
|
|
import glob |
25 |
|
|
import logging |
26 |
|
|
import os |
27 |
|
|
import re |
28 |
|
|
import struct |
29 |
|
|
|
30 |
|
|
log = logging.getLogger("storage") |
31 |
|
|
|
32 |
|
|
re_host_bus = re.compile(r'^PCI\s*(\S*)\s*channel: (\S*)\s*$') |
33 |
|
|
re_interface_scsi = re.compile(r'^SCSI\s*id: (\S*)\s*lun: (\S*)\s*$') |
34 |
|
|
re_interface_ata = re.compile(r'^ATA\s*device: (\S*)\s*$') |
35 |
|
|
|
36 |
|
|
class EddEntry(object): |
37 |
|
|
""" This object merely collects what the /sys/firmware/edd/* entries can |
38 |
|
|
provide. |
39 |
|
|
""" |
40 |
|
|
def __init__(self, sysfspath): |
41 |
|
|
self.type = None |
42 |
|
|
|
43 |
|
|
self.ata_device = None |
44 |
|
|
self.channel = None |
45 |
|
|
self.mbr_signature = None |
46 |
|
|
self.pci_dev = None |
47 |
|
|
self.scsi_id = None |
48 |
|
|
self.scsi_lun = None |
49 |
|
|
self.sectors = None |
50 |
|
|
|
51 |
|
|
self.load(sysfspath) |
52 |
|
|
|
53 |
|
|
def __str__(self): |
54 |
|
|
return \ |
55 |
|
|
"\ttype: %(type)s, ata_device: %(ata_device)s\n" \ |
56 |
|
|
"\tchannel: %(channel)s, mbr_signature: %(mbr_signature)s\n" \ |
57 |
|
|
"\tpci_dev: %(pci_dev)s, scsi_id: %(scsi_id)s\n" \ |
58 |
|
|
"\tscsi_lun: %(scsi_lun)s, sectors: %(sectors)s" % self.__dict__ |
59 |
|
|
|
60 |
|
|
def _read_file(self, filename): |
61 |
|
|
contents = None |
62 |
|
|
if os.path.exists(filename): |
63 |
|
|
with open(filename) as f: |
64 |
|
|
contents = f.read().rstrip() |
65 |
|
|
return contents |
66 |
|
|
|
67 |
|
|
def load(self, sysfspath): |
68 |
|
|
interface = self._read_file(os.path.join(sysfspath, "interface")) |
69 |
|
|
if interface: |
70 |
|
|
self.type = interface.split()[0] |
71 |
|
|
if self.type == "SCSI": |
72 |
|
|
match = re_interface_scsi.match(interface) |
73 |
|
|
self.scsi_id = int(match.group(1)) |
74 |
|
|
self.scsi_lun = int(match.group(2)) |
75 |
|
|
elif self.type == "ATA": |
76 |
|
|
match = re_interface_ata.match(interface) |
77 |
|
|
self.ata_device = int(match.group(1)) |
78 |
|
|
|
79 |
|
|
self.mbr_signature = self._read_file( |
80 |
|
|
os.path.join(sysfspath, "mbr_signature")) |
81 |
|
|
sectors = self._read_file(os.path.join(sysfspath, "sectors")) |
82 |
|
|
if sectors: |
83 |
|
|
self.sectors = int(sectors) |
84 |
|
|
hbus = self._read_file(os.path.join(sysfspath, "host_bus")) |
85 |
|
|
if hbus: |
86 |
|
|
match = re_host_bus.match(hbus) |
87 |
|
|
if match: |
88 |
|
|
self.pci_dev = match.group(1) |
89 |
|
|
self.channel = int(match.group(2)) |
90 |
|
|
else: |
91 |
|
|
log.warning("edd: can not match host_bus: %s" % hbus) |
92 |
|
|
|
93 |
|
|
class EddMatcher(object): |
94 |
|
|
""" This object tries to match given entry to a disk device name. |
95 |
|
|
|
96 |
|
|
Assuming, heuristic analysis and guessing hapens here. |
97 |
|
|
""" |
98 |
|
|
def __init__(self, edd_entry): |
99 |
|
|
self.edd = edd_entry |
100 |
|
|
|
101 |
|
|
def devname_from_pci_dev(self): |
102 |
|
|
name = None |
103 |
|
|
if self.edd.type == "ATA" and \ |
104 |
|
|
self.edd.channel is not None and \ |
105 |
|
|
self.edd.ata_device is not None: |
106 |
|
|
path = "/sys/devices/pci0000:00/0000:%(pci_dev)s/host%(chan)d/"\ |
107 |
|
|
"target%(chan)d:0:%(dev)d/%(chan)d:0:%(dev)d:0/block" % { |
108 |
|
|
'pci_dev' : self.edd.pci_dev, |
109 |
|
|
'chan' : self.edd.channel, |
110 |
|
|
'dev' : self.edd.ata_device |
111 |
|
|
} |
112 |
|
|
if os.path.isdir(path): |
113 |
|
|
block_entries = os.listdir(path) |
114 |
|
|
if len(block_entries) == 1: |
115 |
|
|
name = block_entries[0] |
116 |
|
|
else: |
117 |
|
|
log.warning("edd: directory does not exist: %s" % path) |
118 |
|
|
elif self.edd.type == "SCSI": |
119 |
|
|
pattern = "/sys/devices/pci0000:00/0000:%(pci_dev)s/virtio*/block" % \ |
120 |
|
|
{'pci_dev' : self.edd.pci_dev} |
121 |
|
|
matching_paths = glob.glob(pattern) |
122 |
|
|
if len(matching_paths) != 1 or not os.path.exists(matching_paths[0]): |
123 |
|
|
return None |
124 |
|
|
block_entries = os.listdir(matching_paths[0]) |
125 |
|
|
if len(block_entries) == 1: |
126 |
|
|
name = block_entries[0] |
127 |
|
|
return name |
128 |
|
|
|
129 |
|
|
def match_via_mbrsigs(self, mbr_dict): |
130 |
|
|
""" Try to match the edd entry based on its mbr signature. |
131 |
|
|
|
132 |
|
|
This will obviously fail for a fresh drive/image, but in extreme |
133 |
|
|
cases can also show false positives for randomly matching data. |
134 |
|
|
""" |
135 |
|
|
for (name, mbr_signature) in mbr_dict.items(): |
136 |
|
|
if mbr_signature == self.edd.mbr_signature: |
137 |
|
|
return name |
138 |
|
|
return None |
139 |
|
|
|
140 |
|
|
def biosdev_to_edd_dir(biosdev): |
141 |
|
|
return "/sys/firmware/edd/int13_dev%x" % biosdev |
142 |
|
|
|
143 |
|
|
def collect_edd_data(): |
144 |
|
|
edd_data_dict = {} |
145 |
|
|
# the hard drive numbering starts at 0x80 (128 decimal): |
146 |
|
|
for biosdev in range(0x80, 0x80+16): |
147 |
|
|
sysfspath = biosdev_to_edd_dir(biosdev) |
148 |
|
|
if not os.path.exists(sysfspath): |
149 |
|
|
break |
150 |
|
|
edd_data_dict[biosdev] = EddEntry(sysfspath) |
151 |
|
|
return edd_data_dict |
152 |
|
|
|
153 |
|
|
def collect_mbrs(devices): |
154 |
|
|
""" Read MBR signatures from devices. |
155 |
|
|
|
156 |
|
|
Returns a dict mapping device names to their MBR signatures. It is not |
157 |
|
|
guaranteed this will succeed, with a new disk for instance. |
158 |
|
|
""" |
159 |
|
|
mbr_dict = {} |
160 |
|
|
for dev in devices: |
161 |
|
|
try: |
162 |
|
|
fd = os.open(dev.path, os.O_RDONLY) |
163 |
|
|
# The signature is the unsigned integer at byte 440: |
164 |
|
|
os.lseek(fd, 440, 0) |
165 |
|
|
mbrsig = struct.unpack('I', os.read(fd, 4)) |
166 |
|
|
os.close(fd) |
167 |
|
|
except OSError as e: |
168 |
|
|
log.warning("edd: error reading mbrsig from disk %s: %s" % |
169 |
|
|
(dev.name, str(e))) |
170 |
|
|
continue |
171 |
|
|
|
172 |
|
|
mbrsig_str = "0x%08x" % mbrsig |
173 |
|
|
# sanity check |
174 |
|
|
if mbrsig_str == '0x00000000': |
175 |
|
|
log.info("edd: MBR signature on %s is zero. new disk image?" % dev.name) |
176 |
|
|
continue |
177 |
|
|
else: |
178 |
|
|
for (dev_name, mbrsig_str_old) in mbr_dict.items(): |
179 |
|
|
if mbrsig_str_old == mbrsig_str: |
180 |
|
|
log.error("edd: dupicite MBR signature %s for %s and %s" % |
181 |
|
|
(mbrsig_str, dev_name, dev.name)) |
182 |
|
|
# this actually makes all the other data useless |
183 |
|
|
return {} |
184 |
|
|
# update the dictionary |
185 |
|
|
mbr_dict[dev.name] = mbrsig_str |
186 |
|
|
log.info("edd: collected mbr signatures: %s" % mbr_dict) |
187 |
|
|
return mbr_dict |
188 |
|
|
|
189 |
|
|
def get_edd_dict(devices): |
190 |
|
|
""" Generates the 'device name' -> 'edd number' mapping. |
191 |
|
|
|
192 |
|
|
The EDD kernel module that exposes /sys/firmware/edd is thoroughly |
193 |
|
|
broken, the information there is incomplete and sometimes downright |
194 |
|
|
wrong. So after we mine out all useful information that the files under |
195 |
|
|
/sys/firmware/edd/int13_*/ can provide, we resort to heuristics and |
196 |
|
|
guessing. Our first attempt is, by looking at the device type int |
197 |
|
|
'interface', attempting to map pci device number, channel number etc. to |
198 |
|
|
a sysfs path, check that the path really exists, then read the device |
199 |
|
|
name (e.g 'sda') from there. Should this fail we try to match contents |
200 |
|
|
of 'mbr_signature' to a real MBR signature found on the existing block |
201 |
|
|
devices. |
202 |
|
|
""" |
203 |
|
|
mbr_dict = collect_mbrs(devices) |
204 |
|
|
edd_entries_dict = collect_edd_data() |
205 |
|
|
edd_dict = {} |
206 |
|
|
for (edd_number, edd_entry) in edd_entries_dict.items(): |
207 |
|
|
log.debug("edd: data extracted from 0x%x:\n%s" % (edd_number, edd_entry)) |
208 |
|
|
matcher = EddMatcher(edd_entry) |
209 |
|
|
# first try to match through the pci dev etc. |
210 |
|
|
name = matcher.devname_from_pci_dev() |
211 |
|
|
# next try to compare mbr signatures |
212 |
|
|
if name: |
213 |
|
|
log.debug("edd: matched 0x%x to %s using pci_dev" % (edd_number, name)) |
214 |
|
|
else: |
215 |
|
|
name = matcher.match_via_mbrsigs(mbr_dict) |
216 |
|
|
if name: |
217 |
|
|
log.info("edd: matched 0x%x to %s using MBR sig" % (edd_number, name)) |
218 |
|
|
|
219 |
|
|
if name: |
220 |
|
|
old_edd_number = edd_dict.get(name) |
221 |
|
|
if old_edd_number: |
222 |
|
|
log.info("edd: both edd entries 0x%x and 0x%x seem to map to %s" % |
223 |
|
|
(old_edd_number, edd_number, name)) |
224 |
|
|
# this means all the other data can be confused and useless |
225 |
|
|
return {} |
226 |
|
|
edd_dict[name] = edd_number |
227 |
|
|
continue |
228 |
|
|
log.error("edd: unable to match edd entry 0x%x" % edd_number) |
229 |
|
|
return edd_dict |