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"""
31 from collections import namedtuple
32 from collections import OrderedDict
39 BBSIZE = 8192 # size of boot area with label
40 SBSIZE = 8192 # max size of fs superblock
42 DISKMAGIC = 0x82564557
46 """Represents a Master Boot Record."""
47 class Partition(object):
48 """Represents a partition entry in MBR"""
51 def __init__(self, raw_part):
52 """Create a Partition instance"""
59 ) = struct.unpack(self.fmt, raw_part)
62 """Pack the partition values into a binary string"""
63 return struct.pack(self.fmt,
73 """Returns the size of an MBR partition entry"""
74 return struct.calcsize(MBR.Partition.fmt)
77 start = self.unpack_chs(self.start)
78 end = self.unpack_chs(self.end)
79 return "%d %s %d %s %d %d" % (self.status, start, self.type, end,
80 self.first_sector, self.sector_count)
84 """Unpacks a CHS address string to a tuple."""
88 head = struct.unpack('<B', chs[0])[0]
89 sector = struct.unpack('<B', chs[1])[0] & 0x3f
90 cylinder = (struct.unpack('<B', chs[1])[0] & 0xC0) << 2 | \
91 struct.unpack('<B', chs[2])[0]
93 return (cylinder, head, sector)
96 def pack_chs(cylinder, head, sector):
97 """Packs a CHS tuple to an address string."""
99 assert 1 <= sector <= 63
100 assert 0 <= head <= 255
103 # If the cylinders overflow then put the value (1023, 254, 63) to
104 # the tuple. At least this is what OpenBSD does.
111 byte1 = (cylinder >> 2) & 0xC0 | sector
112 byte2 = cylinder & 0xff
114 return struct.pack('<BBB', byte0, byte1, byte2)
116 def __init__(self, block):
117 """Create an MBR instance"""
119 self.fmt = "<444s2x16s16s16s16s2s"
120 raw_part = {} # Offset Length Contents
121 (self.code_area, # 0 440(max. 446) code area
122 # 440 2(optional) disk signature
123 # 444 2 Usually nulls
124 raw_part[0], # 446 16 Partition 0
125 raw_part[1], # 462 16 Partition 1
126 raw_part[2], # 478 16 Partition 2
127 raw_part[3], # 494 16 Partition 3
128 self.signature # 510 2 MBR signature
129 ) = struct.unpack(self.fmt, block)
133 self.part[i] = self.Partition(raw_part[i])
136 """Return the size of a Master Boot Record."""
137 return struct.calcsize(self.fmt)
140 """Pack an MBR to a binary string."""
141 return struct.pack(self.fmt,
153 ret += "Partition %d: %s\n" % (i, self.part[i])
154 ret += "Signature: %s %s\n" % (hex(ord(self.signature[0])),
155 hex(ord(self.signature[1])))
156 title = "Master Boot Record"
157 return "%s\n%s\n%s\n" % (title, len(title) * "=", ret)
161 """Represents a BSD Disk"""
163 def __init__(self, device):
164 """Create a Disk instance"""
167 self.disklabel = None
169 with open(device, "rb") as d:
170 sector0 = d.read(BLOCKSIZE)
171 self.mbr = MBR(sector0)
174 ptype = self.mbr.part[i].type
175 if ptype in (0xa5, 0xa6, 0xa9):
176 d.seek(BLOCKSIZE * self.mbr.part[i].first_sector)
178 if ptype == 0xa5: # FreeBSD
179 self.disklabel = BSDDisklabel(d)
180 elif ptype == 0xa6: # OpenBSD
181 self.disklabel = OpenBSDDisklabel(d)
183 self.disklabel = BSDDisklabel(d)
186 assert self.disklabel is not None, "No *BSD partition found"
189 """Write the changes back to the media"""
190 with open(self.device, 'rw+b') as d:
191 d.write(self.mbr.pack())
193 d.seek(self.mbr.part[self.part_num].first_sector * BLOCKSIZE)
194 self.disklabel.write_to(d)
197 """Print the partitioning info of the Disk"""
198 return str(self.mbr) + str(self.disklabel)
200 def enlarge(self, new_size):
201 """Enlarge the disk and return the last usable sector"""
204 end = self.disklabel.enlarge(new_size)
207 start = self.mbr.part[self.part_num].first_sector
208 self.mbr.part[self.part_num].sector_count = end - start + 1
210 ntracks = self.disklabel.field['ntracks']
211 nsectors = self.disklabel.field['nsectors']
213 cylinder = end // (ntracks * nsectors)
214 header = (end // nsectors) % ntracks
215 sector = (end % 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 """Enlarge the last partition to cover up all the free space"""
221 self.disklabel.enlarge_last_partition()
223 def get_last_partition_id(self):
224 """Get the ID of the last partition"""
225 return self.disklabel.get_last_partition_id()
228 """Get the Disklabel Unique Identifier (works only for OpenBSD)"""
229 if 'uid' in self.disklabel.field:
230 return self.disklabel.field['uid']
235 class DisklabelBase(object):
236 """Disklabel base class"""
237 __metaclass__ = abc.ABCMeta
239 def __init__(self, device):
240 """Create a Disklabel instance"""
242 # Subclasses need to overwrite this
246 @abc.abstractproperty
248 """Fields format string for the disklabel fields"""
251 def pack(self, checksum=None):
252 """Return a binary copy of the Disklabel block"""
255 for k, v in self.field.iteritems():
258 if checksum is not None:
259 out['checksum'] = checksum
261 return struct.pack(self.fmt, * out.values() + [self.ptable.pack()])
263 def compute_checksum(self):
264 """Compute the checksum of the disklabel"""
266 raw = cStringIO.StringIO(self.pack(0))
271 checksum ^= struct.unpack('<H', uint16)[0]
279 def enlarge(self, new_size):
280 """Enlarge the disk and return the last usable sector"""
283 def write_to(self, device):
284 """Write the disklabel to a device"""
286 # The disklabel starts at sector 1
287 device.seek(BLOCKSIZE, os.SEEK_CUR)
288 device.write(self.pack())
291 def enlarge_last_partition(self):
292 """Enlarge the last partition to consume all the usable space"""
296 def get_last_partition_id(self):
297 """Get the ID of the last partition"""
302 """Print the Disklabel"""
306 class PartitionTableBase(object):
307 """Base Class for disklabel partition tables"""
308 __metaclass__ = abc.ABCMeta
310 @abc.abstractproperty
312 """Partition fields format string"""
315 @abc.abstractproperty
317 """The partition fields"""
320 def __init__(self, ptable, pnumber):
321 """Create a Partition Table instance"""
323 self.Partition = namedtuple('Partition', self.fields)
326 size = struct.calcsize(self.fmt)
327 raw = cStringIO.StringIO(ptable)
329 for _ in xrange(pnumber):
331 self.Partition(*struct.unpack(self.fmt, raw.read(size)))
337 """Print the Partition table"""
339 for i in xrange(len(self.part)):
340 val += "%c: %s\n" % (chr(ord('a') + i), str(self.part[i]))
344 """Packs the partition table into a binary string."""
346 for i in xrange(len(self.part)):
347 ret += struct.pack(self.fmt, *self.part[i])
348 return ret + ((364 - len(self.part) * 16) * '\x00')
351 class BSDDisklabel(DisklabelBase):
352 """Represents an BSD Disklabel"""
354 class PartitionTable(PartitionTableBase):
355 """Represents a BSD Partition Table"""
359 """Partition fields format string"""
364 """The partition fields"""
365 return [ # Offset Length Contents
366 'size', # 0 4 Number of sectors in partition
367 'offset', # 4 4 Starting sector of the partition
368 'fsize', # 8 4 File system basic fragment size
369 'fstype', # 12 1 File system type
370 'frag', # 13 1 File system fragments per block
371 'cpg' # 14 2 File system cylinders per group
376 return "<IHH16s16sIIIIIIHHIHHHHIII20s20sIHHII364s"
378 def __init__(self, device):
379 """Create a BSD DiskLabel instance"""
380 super(BSDDisklabel, self).__init__(device)
382 # Disklabel starts at offset one
383 device.seek(BLOCKSIZE, os.SEEK_CUR)
384 sector1 = device.read(BLOCKSIZE)
386 d_ = OrderedDict() # Off Len Content
387 (d_["magic"], # 0 4 Magic
388 d_["dtype"], # 4 2 Drive Type
389 d_["subtype"], # 6 2 Subtype
390 d_["typename"], # 8 16 Type Name
391 d_["packname"], # 24 16 Pack Identifier
392 d_["secsize"], # 32 4 Bytes per sector
393 d_["nsectors"], # 36 4 Data sectors per track
394 d_["ntracks"], # 40 4 Tracks per cylinder
395 d_["ncylinders"], # 44 4 Data cylinders per unit
396 d_["secpercyl"], # 48 4 Data sectors per cylinder
397 d_["secperunit"], # 52 4 Data sectors per unit
398 d_["sparespertrack"], # 56 2 Spare sectors per track
399 d_["sparespercyl"], # 58 2 Spare sectors per cylinder
400 d_["acylinders"], # 60 4 Alternative cylinders per unit
401 d_["rpm"], # 64 2 Rotation Speed
402 d_["interleave"], # 66 2 Hardware sector interleave
403 d_["trackskew"], # 68 2 Sector 0 skew, per track
404 d_["cylskew"], # 70 2 Sector 0 skew, per cylinder
405 d_["headswitch"], # 72 4 Head switch time
406 d_["trkseek"], # 76 4 Track-to-track seek
407 d_["flags"], # 80 4 Generic Flags
408 d_["drivedata"], # 84 5*4 Drive-type specific information
409 d_["spare"], # 104 5*4 Reserved for future use
410 d_["magic2"], # 124 4 Magic Number
411 d_["checksum"], # 128 2 Xor of data including partitions
412 d_["npartitions"], # 130 2 Number of partitions following
413 d_["bbsize"], # 132 4 size of boot area at sn0, bytes
414 d_["sbsize"], # 136 4 Max size of fs superblock, bytes
415 ptable_raw # 140 16*16 Partition Table
416 ) = struct.unpack(self.fmt, sector1)
418 assert d_['magic'] == d_['magic2'] == DISKMAGIC, "Disklabel not valid"
419 self.ptable = self.PartitionTable(ptable_raw, d_['npartitions'])
422 def enlarge(self, new_size):
423 raise NotImplementedError
425 def enlarge_last_partition(self):
426 raise NotImplementedError
428 def get_last_partition_id(self):
429 raise NotImplementedError
432 """Print the Disklabel"""
436 # Those fields may contain null bytes
437 typename = self.field['typename'].strip('\x00').strip()
438 packname = self.field['packname'].strip('\x00').strip()
441 "%s\n%s\n" % (title, len(title) * "=") + \
442 "Magic Number: 0x%(magic)x\n" \
443 "Drive type: %(dtype)d\n" \
444 "Subtype: %(subtype)d\n" % self.field + \
445 "Typename: %s\n" % typename + \
446 "Pack Identifier: %s\n" % packname + \
447 "# of bytes per sector: %(secsize)d\n" \
448 "# of data sectors per track: %(nsectors)d\n" \
449 "# of tracks per cylinder: %(ntracks)d\n" \
450 "# of data cylinders per unit: %(ncylinders)d\n" \
451 "# of data sectors per cylinder: %(secpercyl)d\n" \
452 "# of data sectors per unit: %(secperunit)d\n" \
453 "# of spare sectors per track: %(sparespertrack)d\n" \
454 "# of spare sectors per cylinder: %(sparespercyl)d\n" \
455 "Alt. cylinders per unit: %(acylinders)d\n" \
456 "Rotational speed: %(rpm)d\n" \
457 "Hardware sector interleave: %(interleave)d\n" \
458 "Sector 0 skew, per track: %(trackskew)d\n" \
459 "Sector 0 skew, per cylinder: %(cylskew)d\n" \
460 "Head switch time, usec: %(headswitch)d\n" \
461 "Track-to-track seek, usec: %(trkseek)d\n" \
462 "Generic Flags: %(flags)r\n" \
463 "Drive data: %(drivedata)r\n" \
464 "Reserved for future use: %(spare)r\n" \
465 "The magic number again: 0x%(magic2)x\n" \
466 "Checksum: %(checksum)d\n" \
467 "Number of partitions: %(npartitions)d\n" \
468 "Size of boot aread at sn0: %(bbsize)d\n" \
469 "Max size of fs superblock: %(sbsize)d\n" % self.field + \
473 class OpenBSDDisklabel(DisklabelBase):
474 """Represents an OpenBSD Disklabel"""
476 class PartitionTable(PartitionTableBase):
477 """Reprepsents an OpenBSD Partition Table"""
485 return [ # Offset Length Contents
486 'size', # 0 4 Number of sectors in the partition
487 'offset', # 4 4 Starting sector of the partition
488 'offseth', # 8 2 Starting sector (high part)
489 'sizeh', # 10 2 Number of sectors (high part)
490 'fstype', # 12 1 Filesystem type
491 'frag', # 13 1 Filesystem Fragments per block
492 'cpg' # 14 2 FS cylinders per group
495 def setpsize(self, i, size):
496 """Set size for partition i"""
498 self.part[i] = self.Partition(
499 size & 0xffffffff, tmp.offset, tmp.offseth, size >> 32,
500 tmp.fstype, tmp.frag, tmp.cpg)
502 def getpsize(self, i):
503 """Get size for partition i"""
504 return (self.part[i].sizeh << 32) + self.part[i].size
506 def setpoffset(self, i, offset):
507 """Set offset for partition i"""
509 self.part[i] = self.Partition(
510 tmp.size, offset & 0xffffffff, offset >> 32, tmp.sizeh,
513 def getpoffset(self, i):
514 """Get offset for partition i"""
515 return (self.part[i].offseth << 32) + self.part[i].offset
519 return "<IHH16s16sIIIIII8sIHHIII20sHH16sIHHII364s"
521 def __init__(self, device):
522 """Create a DiskLabel instance"""
524 super(OpenBSDDisklabel, self).__init__(device)
526 # Disklabel starts at offset one
527 device.seek(BLOCKSIZE, os.SEEK_CUR)
528 sector1 = device.read(BLOCKSIZE)
530 d_ = OrderedDict() # Off Len Content
531 (d_["magic"], # 0 4 Magic
532 d_["dtype"], # 4 2 Drive Type
533 d_["subtype"], # 6 2 Subtype
534 d_["typename"], # 8 16 Type Name
535 d_["packname"], # 24 16 Pack Identifier
536 d_["secsize"], # 32 4 Bytes per sector
537 d_["nsectors"], # 36 4 Data sectors per track
538 d_["ntracks"], # 40 4 Tracks per cylinder
539 d_["ncylinders"], # 44 4 Data cylinders per unit
540 d_["secpercyl"], # 48 4 Data sectors per cylinder
541 d_["secperunit"], # 52 4 Data sectors per unit
542 d_["uid"], # 56 8 Unique label identifier
543 d_["acylinders"], # 64 4 Alt cylinders per unit
544 d_["bstarth"], # 68 2 Start of useable region (high part)
545 d_["bendh"], # 70 2 Size of usable region (high part)
546 d_["bstart"], # 72 4 Start of useable region
547 d_["bend"], # 76 4 End of usable region
548 d_["flags"], # 80 4 Generic Flags
549 d_["drivedata"], # 84 5*4 Drive-type specific information
550 d_["secperunith"], # 104 2 Number of data sectors (high part)
551 d_["version"], # 106 2 Version
552 d_["spare"], # 108 4*4 Reserved for future use
553 d_["magic2"], # 124 4 Magic number
554 d_["checksum"], # 128 2 Xor of data including partitions
555 d_["npartitions"], # 130 2 Number of partitions in following
556 d_["bbsize"], # 132 4 size of boot area at sn0, bytes
557 d_["sbsize"], # 136 4 Max size of fs superblock, bytes
558 ptable_raw # 140 16*16 Partition Table
559 ) = struct.unpack(self.fmt, sector1)
561 assert d_['magic'] == d_['magic2'] == DISKMAGIC, "Disklabel not valid"
562 self.ptable = self.PartitionTable(ptable_raw, d_['npartitions'])
565 def setdsize(self, dsize):
567 self.field['secperunith'] = dsize >> 32
568 self.field['secperunit'] = dsize & 0xffffffff
572 return (self.field['secperunith'] << 32) + self.field['secperunit']
574 dsize = property(getdsize, setdsize, None, "disk size")
576 def setbstart(self, bstart):
577 """Set start of useable region"""
578 self.field['bstarth'] = bstart >> 32
579 self.field['bstart'] = bstart & 0xffffffff
582 """Get start of usable region"""
583 return (self.field['bstarth'] << 32) + self.field['bstart']
585 bstart = property(getbstart, setbstart, None, "start of usable region")
587 def setbend(self, bend):
588 """Set end of useable region"""
589 self.field['bendh'] = bend >> 32
590 self.field['bend'] = bend & 0xffffffff
593 """Get end of usable region"""
594 return (self.field['bendh'] << 32) + self.field['bend']
596 bend = property(getbend, setbend, None, "end of usable region")
598 def enlarge(self, new_size):
599 """Enlarge the disk and return the last usable sector"""
601 assert new_size >= self.dsize, \
602 "New size cannot be smaller that %s" % self.dsize
605 self.dsize = new_size
606 self.field['ncylinders'] = self.dsize // (self.field['nsectors'] *
607 self.field['ntracks'])
608 self.bend = (self.field['ncylinders'] * self.field['nsectors'] *
609 self.field['ntracks'])
611 # Partition 'c' descriptes the entire disk
612 self.ptable.setpsize(2, new_size)
614 # Update the checksum
615 self.field['checksum'] = self.compute_checksum()
617 # The last usable sector is the end of the usable region minus one
620 def get_last_partition_id(self):
621 """Returns the id of the last partition"""
624 # Don't check partition 'c' which is the whole disk
625 for i in filter(lambda x: x != 2, range(len(self.ptable.part))):
626 curr_end = self.ptable.getpsize(i) + self.ptable.getpoffset(i)
631 assert end > 0, "No partition found"
635 def enlarge_last_partition(self):
636 """Enlarge the last partition to cover up all the free space"""
638 part_num = self.get_last_partition_id()
640 end = self.ptable.getpsize(part_num) + self.ptable.getpoffset(part_num)
642 assert end > 0, "No partition found"
644 if self.ptable.part[part_num].fstype == 1: # Swap partition.
645 #TODO: Maybe create a warning?
648 if end > (self.bend - 1024):
651 self.ptable.setpsize(
652 part_num, self.bend - self.ptable.getpoffset(part_num) - 1024)
654 self.field['checksum'] = self.compute_checksum()
657 """Print the Disklabel"""
659 # Those values may contain null bytes
660 typename = self.field['typename'].strip('\x00').strip()
661 packname = self.field['packname'].strip('\x00').strip()
663 duid = "".join(x.encode('hex') for x in self.field['uid'])
667 "%s\n%s\n" % (title, len(title) * "=") + \
668 "Magic Number: 0x%(magic)x\n" \
669 "Drive type: %(dtype)d\n" \
670 "Subtype: %(subtype)d\n" % self.field + \
671 "Typename: %s\n" % typename + \
672 "Pack Identifier: %s\n" % packname + \
673 "# of bytes per sector: %(secsize)d\n" \
674 "# of data sectors per track: %(nsectors)d\n" \
675 "# of tracks per cylinder: %(ntracks)d\n" \
676 "# of data cylinders per unit: %(ncylinders)d\n" \
677 "# of data sectors per cylinder: %(secpercyl)d\n" \
678 "# of data sectors per unit: %(secperunit)d\n" % self.field + \
679 "DUID: %s\n" % duid + \
680 "Alt. cylinders per unit: %(acylinders)d\n" \
681 "Start of useable region (high part): %(bstarth)d\n" \
682 "Size of useable region (high part): %(bendh)d\n" \
683 "Start of useable region: %(bstart)d\n" \
684 "End of usable region: %(bend)d\n" \
685 "Generic Flags: %(flags)r\n" \
686 "Drive data: %(drivedata)r\n" \
687 "Number of data sectors (high part): %(secperunith)d\n" \
688 "Version: %(version)d\n" \
689 "Reserved for future use: %(spare)r\n" \
690 "The magic number again: 0x%(magic2)x\n" \
691 "Checksum: %(checksum)d\n" \
692 "Number of partitions: %(npartitions)d\n" \
693 "Size of boot aread at sn0: %(bbsize)d\n" \
694 "Max size of fs superblock: %(sbsize)d\n" % self.field + \
699 """Main entry point"""
700 usage = "Usage: %prog [options] <input_media>"
701 parser = optparse.OptionParser(usage=usage)
703 parser.add_option("-l", "--list", action="store_true", dest="list",
705 help="list the disklabel on the specified media")
706 parser.add_option("--get-last-partition", action="store_true",
707 dest="last_part", default=False,
708 help="print the label of the last partition")
710 "--get-duid", action="store_true", dest="duid", default=False,
711 help="print the Disklabel Unique Identifier (OpenBSD only)")
712 parser.add_option("-d", "--enlarge-disk", type="int", dest="disk_size",
713 default=None, metavar="SIZE",
714 help="Enlarge the disk to this SIZE (in sectors)")
716 "-p", "--enlarge-partition", action="store_true",
717 dest="enlarge_partition", default=False,
718 help="Enlarge the last partition to cover up the free space")
720 options, args = parser.parse_args(sys.argv[1:])
723 parser.error("Wrong number of arguments")
732 print "%s" % "".join(x.encode('hex') for x in disk.get_duid())
735 if options.last_part:
736 print "%c" % chr(ord('a') + disk.get_last_partition_id())
738 if options.disk_size is not None:
739 disk.enlarge(options.disk_size)
741 if options.enlarge_partition:
742 disk.enlarge_last_partition()
748 if __name__ == '__main__':
751 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :