3 # -*- coding: utf-8 -*-
5 # Copyright (C) 2013 GRNET S.A.
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.
12 # This program is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 # General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program; if not, write to the Free Software
19 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
22 """This module provides the code for handling BSD disklabels"""
30 from collections import namedtuple
37 BBSIZE = 8192 # size of boot area with label
38 SBSIZE = 8192 # max size of fs superblock
42 """Represents a Master Boot Record."""
43 class Partition(object):
44 """Represents a partition entry in MBR"""
47 def __init__(self, raw_part):
48 """Create a Partition instance"""
56 ) = struct.unpack(self.format, raw_part)
59 """Pack the partition values into a binary string"""
60 return struct.pack(self.format,
70 """Returns the size of an MBR partition entry"""
71 return struct.calcsize(MBR.Partition.format)
74 start = self.unpack_chs(self.start)
75 end = self.unpack_chs(self.end)
76 return "%d %s %d %s %d %d" % (self.status, start, self.type, end,
77 self.first_sector, self.sector_count)
81 """Unpacks a CHS address string to a tuple."""
85 head = struct.unpack('<B', chs[0])[0]
86 sector = struct.unpack('<B', chs[1])[0] & 0x3f
87 cylinder = (struct.unpack('<B', chs[1])[0] & 0xC0) << 2 | \
88 struct.unpack('<B', chs[2])[0]
90 return (cylinder, head, sector)
93 def pack_chs(cylinder, head, sector):
94 """Packs a CHS tuple to an address string."""
96 assert 1 <= sector <= 63
97 assert 0 <= head <= 255
100 # If the cylinders overflow then put the value (1023, 254, 63) to
101 # the tuple. At least this is what OpenBSD does.
108 byte1 = (cylinder >> 2) & 0xC0 | sector
109 byte2 = cylinder & 0xff
111 return struct.pack('<BBB', byte0, byte1, byte2)
113 format = "<444s2x16s16s16s16s2s"
115 Offset Length Contents
116 0 440(max. 446) code area
117 440 2(optional) disk signature
125 def __init__(self, block):
126 """Create an MBR instance"""
133 self.signature) = struct.unpack(self.format, block)
137 self.part[i] = self.Partition(raw_part[i])
141 """Return the size of a Master Boot Record."""
142 return struct.calcsize(MBR.format)
145 """Pack an MBR to a binary string."""
146 return struct.pack(self.format,
157 ret += "Partition %d: %s\n" % (i, self.part[i])
158 ret += "Signature: %s %s\n" % (hex(ord(self.signature[0])),
159 hex(ord(self.signature[1])))
160 title = "Master Boot Record"
161 return "%s\n%s\n%s\n" % (title, len(title) * "=", ret)
165 """Represents an BSD Disklabel"""
167 def __init__(self, disk):
168 """Create a DiskLabel instance"""
171 self.disklabel = None
173 with open(disk, "rb") as d:
174 sector0 = d.read(BLOCKSIZE)
175 self.mbr = MBR(sector0)
178 ptype = self.mbr.part[i].type
179 if ptype in (0xa5, 0xa6, 0xa9):
180 d.seek(BLOCKSIZE * self.mbr.part[i].first_sector)
182 if ptype == 0xa5: # FreeBSD
183 self.disklabel = BSD_Disklabel(d)
184 elif ptype == 0xa6: # OpenBSD
185 self.disklabel = OpenBSD_Disklabel(d)
187 self.disklabel = BSD_Disklabel(d)
190 assert self.disklabel is not None, "No *BSD partition found"
193 """Write the disklabel back to the media"""
194 with open(self.disk, 'rw+b') as d:
195 d.write(self.mbr.pack())
197 d.seek(self.mbr.part[self.part_num].first_sector * BLOCKSIZE)
198 self.disklabel.write_to(d)
201 return str(self.mbr) + str(self.disklabel)
203 def enlarge_disk(self, new_size):
204 """Enlarge the size of the disk and return the last usable sector"""
207 end = self.disklabel.enlarge_disk(new_size)
210 start = self.mbr.part[self.part_num].first_sector
211 self.mbr.part[self.part_num].sector_count = end - start + 1
213 cylinder = end // (self.disklabel.ntracks * self.disklabel.nsectors)
214 header = (end // self.disklabel.nsectors) % self.disklabel.ntracks
215 sector = (end % self.disklabel.nsectors) + 1
216 chs = MBR.Partition.pack_chs(cylinder, header, sector)
217 self.mbr.part[self.part_num].end = chs
219 def enlarge_last_partition(self):
220 self.disklabel.enlarge_last_partition()
227 class OpenBSD_Disklabel:
228 """Represents an OpenBSD Disklabel"""
229 format = "<IHH16s16sIIIIII8sIHHIII20sHH16sIHHII364s"
231 Offset Length Contents
236 24 16 Pack Identifier
237 32 4 Bytes per sector
238 36 4 Data sectors per track
239 40 4 Tracks per cilinder
240 44 4 Data cylinders per unit
241 48 4 Data sectors per cylynder
242 52 4 Data sectors per unit
243 56 8 Unique label identifier
244 64 4 Alt cylinders per unit
245 68 2 Start of useable region (high part)
246 70 2 Size of usable region (high part)
247 72 4 Start of useable region
248 76 4 End of usable region
250 84 5*4 Drive-type specific information
251 104 2 Number of data sectors (high part)
253 108 4*4 Reserved for future use
255 128 2 Xor of data Inclu. partitions
256 130 2 Number of partitions in following
257 132 4 size of boot area at sn0, bytes
258 136 4 Max size of fs superblock, bytes
259 140 16*16 Partition Table
262 class PartitionTable:
263 """Reprepsents an OpenBSD Partition Table"""
267 Offset Length Contents
268 0 4 Number of sectors in the partition
270 8 2 Starting sector (high part)
271 10 2 Number of sectors (high part)
273 13 1 Filesystem Fragment per block
274 14 2 FS cylinders per group
277 Partition = namedtuple(
278 'Partition', 'size, offset, offseth, sizeh, fstype, frag, cpg')
280 def __init__(self, ptable, pnumber):
281 """Create a Partition Table instance"""
284 size = struct.calcsize(self.format)
286 raw = cStringIO.StringIO(ptable)
288 for i in range(pnumber):
290 *struct.unpack(self.format, raw.read(size)))
296 """Print the Partition table"""
298 for i in range(len(self.part)):
299 val += "%c: %s\n" % (chr(ord('a') + i), str(self.part[i]))
303 """Packs the partition table into a binary string."""
305 for i in range(len(self.part)):
306 ret += struct.pack(self.format,
309 self.part[i].offseth,
316 def setpsize(self, i, size):
317 """Set size for partition i"""
319 self.part[i] = self.Partition(size & 0xffffffff, tmp.offset,
320 tmp.offseth, size >> 32, tmp.fstype,
323 def getpsize(self, i):
324 """Get size for partition i"""
325 return (self.part[i].sizeh << 32) + self.part[i].size
327 def setpoffset(self, i, offset):
328 """Set offset for partition i"""
330 self.part[i] = self.Partition(tmp.size, offset & 0xffffffff,
331 offset >> 32, tmp.sizeh, tmp.frag,
334 def getpoffset(self, i):
335 """Get offset for partition i"""
336 return (self.part[i].offseth << 32) + self.part[i].offset
338 DISKMAGIC = 0x82564557
340 def __init__(self, device):
341 """Create a DiskLabel instance"""
343 device.seek(BLOCKSIZE, os.SEEK_CUR)
344 # The offset of the disklabel from the beginning of the partition is
346 sector1 = device.read(BLOCKSIZE)
375 ptable_raw) = struct.unpack(self.format, sector1)
377 assert self.magic == self.DISKMAGIC, "Disklabel is not valid"
379 self.ptable = self.PartitionTable(ptable_raw, self.npartitions)
381 def pack(self, checksum=None):
382 return struct.pack(self.format,
406 self.checksum if checksum is None else checksum,
411 ((364 - self.npartitions * 16) * '\x00'))
413 def compute_checksum(self):
414 """Compute the checksum of the disklabel"""
416 raw = cStringIO.StringIO(self.pack(0))
421 checksum ^= struct.unpack('<H', uint16)[0]
428 def setdsize(self, dsize):
430 self.secperunith = dsize >> 32
431 self.secperunit = dsize & 0xffffffff
435 return (self.secperunith << 32) + self.secperunit
437 def setbstart(self, bstart):
438 """Set start of useable region"""
439 self.bstarth = bstart >> 32
440 self.bstart = bstart & 0xffffffff
443 """Get start of usable region"""
444 return (self.bstarth << 32) + self.bstart
446 def setbend(self, bend):
447 """Set size of useable region"""
448 self.bendh = bend >> 32
449 self.bend = bend & 0xffffffff
452 """Get size of usable region"""
453 return (self.bendh << 32) + self.bend
455 def enlarge_disk(self, new_size):
456 """Enlarge the size of the disk and return the last usable sector"""
458 assert new_size >= self.getdsize(), \
459 "New size cannot be smaller that %s" % self.getdsize()
462 self.setdsize(new_size)
463 self.ncylinders = self.getdsize() // (self.nsectors * self.ntracks)
464 self.setbend(self.ncylinders * self.nsectors * self.ntracks)
466 # Partition 'c' descriptes the entire disk
467 self.ptable.setpsize(2, new_size)
469 # Update the checksum
470 self.checksum = self.compute_checksum()
472 # getbend() gives back the size of the usable region and not the end of
473 # the usable region. I named it like this because this is how it is
474 # named in OpenBSD. To get the last usable sector you need to reduce
476 return self.getbend() - 1
478 def write_to(self, device):
479 """Write the disklabel to a device"""
481 # The disklabel starts at sector 1
482 device.seek(BLOCKSIZE, os.SEEK_CUR)
483 device.write(self.pack())
485 def get_last_partition_id(self):
486 """Returns the id of the last partition"""
489 # Don't check partition 'c' which is the whole disk
490 for i in filter(lambda x: x != 2, range(self.npartitions)):
491 curr_end = self.ptable.getpsize(i) + self.ptable.getpoffset(i)
496 assert end > 0, "No partition found"
500 def enlarge_last_partition(self):
501 """Enlarge the last partition to cover up all the free space"""
503 part_num = self.get_last_partition_id()
505 end = self.ptable.getpsize(part_num) + self.ptable.getpoffset(part_num)
507 assert end > 0, "No partition found"
509 if self.ptable.part[part_num].fstype == 1: # Swap partition.
510 #TODO: Maybe create a warning?
513 if end > (self.getbend() - 1024):
516 self.ptable.setpsize(
517 part_num, self.getbend() - self.ptable.getpoffset(part_num) - 1024)
519 self.checksum = self.compute_checksum()
522 """Print the Disklabel"""
526 "%s\n%s\n" % (title, len(title) * "=") + \
527 "Magic Number: 0x%x\n" % self.magic + \
528 "Drive type: %d\n" % self.dtype + \
529 "Subtype: %d\n" % self.subtype + \
530 "Typename: %s\n" % self.typename.strip('\x00').strip() + \
531 "Pack Identifier: %s\n" % self.packname.strip('\x00').strip() + \
532 "Number of bytes per sector: %d\n" % self.secsize + \
533 "Number of data sectors per track: %d\n" % self.nsectors + \
534 "Number of tracks per cylinder: %d\n" % self.ntracks + \
535 "Number of data cylinders per unit: %d\n" % self.ncylinders + \
536 "Number of data sectors per cylinder: %d\n" % self.secpercyl + \
537 "Number of data sectors per unit: %d\n" % self.secperunit + \
538 "DUID: %s\n" % "".join(x.encode('hex') for x in self.uid) + \
539 "Alt. cylinders per unit: %d\n" % self.acylinders + \
540 "Start of useable region (high part): %d\n" % self.bstarth + \
541 "Size of useable region (high part): %d\n" % self.bendh + \
542 "Start of useable region: %d\n" % self.bstart + \
543 "End of usable region: %d\n" % self.bend + \
544 "Generic Flags: %r\n" % self.flags + \
545 "Drive data: %r\n" % self.drivedata + \
546 "Number of data sectors (high part): %d\n" % self.secperunith + \
547 "Version: %d\n" % self.version + \
548 "Reserved for future use: %r\n" % self.spare + \
549 "The magic number again: 0x%x\n" % self.magic2 + \
550 "Checksum: %d\n" % self.checksum + \
551 "Number of partitions: %d\n" % self.npartitions + \
552 "Size of boot aread at sn0: %d\n" % self.bbsize + \
553 "Max size of fs superblock: %d\n" % self.sbsize + \
557 if __name__ == '__main__':
559 usage = "Usage: %prog [options] <input_media>"
560 parser = optparse.OptionParser(usage=usage)
562 parser.add_option("-l", "--list", action="store_true", dest="list",
564 help="list the disklabel on the specified media")
565 parser.add_option("--get-last-partition", action="store_true",
566 dest="last_part", default=False,
567 help="print the label of the last partition")
568 parser.add_option("--get-duid", action="store_true", dest="duid",
570 help="print the disklabel unique identifier")
571 parser.add_option("-d", "--enlarge-disk", type="int", dest="disk_size",
572 default=None, metavar="SIZE",
573 help="Enlarge the disk to this SIZE (in sectors)")
575 "-p", "--enlarge-partition", action="store_true",
576 dest="enlarge_partition", default=False,
577 help="Enlarge the last partition to cover up the free space")
579 options, args = parser.parse_args(sys.argv[1:])
582 parser.error("Wrong number of arguments")
584 disklabel = Disklabel(args[0])
591 print "%s" % "".join(x.encode('hex') for x in disklabel.uid)
594 if options.last_part:
595 print "%c" % chr(ord('a') + disklabel.get_last_partition_id())
597 if options.disk_size is not None:
598 disklabel.enlarge_disk(options.disk_size)
600 if options.enlarge_partition:
601 disklabel.enlarge_last_partition()
607 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :