# Copyright 1999-2006 Gentoo Foundation # This source code is distributed under the terms of version 2 of the GNU # General Public License as published by the Free Software Foundation, a copy # of which can be found in the main directory of this project. import commands, string, os, parted, copy from glob import glob from GLIException import * import GLIUtility import time MEGABYTE = 1024 * 1024 supported_filesystems = { 'all': ( 'ext2', 'ext3', 'linux-swap', 'xfs', 'jfs', 'reiserfs' ), 'x86': ( 'ntfs', 'fat16', 'fat32' ), 'amd64': ( 'ntfs', 'fat16', 'fat32' ), 'ppc': ( 'reiserfs', 'apple_bootstrap', 'hfs' ) } labelinfo = { 'msdos': { 'ignoredparts': [], 'extended': True }, 'mac': { 'ignoredparts': [1], 'extended': False }, 'sun': { 'ignoredparts': [3], 'extended': False }, 'loop': { 'ignoredparts': [], 'extended': False }, 'gpt': { 'ignoredparts': [], 'extended': False } } archinfo = { 'sparc': 'sun', 'hppa': 'msdos', 'x86': 'msdos', 'amd64': 'msdos', 'ppc': 'mac' } parted_type_map = { 'linux-swap': 'swap' } ## # This class provides a partitioning abstraction for the frontends class Device: "Class representing a partitionable device." _device = None _partitions = None _geometry = None _parted_dev = None _parted_disk = None _arch = None _disklabel = None _install_profile = None ## # Initialization function for Device class # @param device Device node (e.g. /dev/hda) of device being represented # @param arch Architecture that we're partition for (defaults to 'x86' for now) def __init__(self, device, arch, install_profile): self._device = device self._arch = arch self._partitions = [] self._geometry = {} self._parted_dev = parted.PedDevice.get(self._device) try: self._parted_disk = parted.PedDisk.new(self._parted_dev) except: self._parted_disk = self._parted_dev.disk_new_fresh(parted.disk_type_get(archinfo[self._arch])) self._disklabel = self._parted_disk.type.name self._labelinfo = labelinfo[self._disklabel] self.set_disk_geometry_from_disk() self.set_partitions_from_disk() self._install_profile = install_profile def __getitem__(self, name): return self.get_partition(name) def __iter__(self): for part in self.get_partitions(): yield part ## # Returns list of supported filesystems based on arch def get_supported_filesystems(self): return supported_filesystems['all'] + supported_filesystems[self._arch] ## # Sets disk geometry info from disk. This function is used internally by __init__() def set_disk_geometry_from_disk(self): self._geometry = { 'sector_size': self._parted_dev.sector_size, 'total_bytes': self._parted_dev.length * self._parted_dev.sector_size, 'heads': self._parted_dev.heads, 'cylinders': self._parted_dev.cylinders, 'sectors': self._parted_dev.sectors, 'cylinder_bytes': self._parted_dev.heads * self._parted_dev.sectors * self._parted_dev.sector_size, 'total_sectors': self._parted_dev.length, 'sectors_in_cylinder': self._parted_dev.heads * self._parted_dev.sectors, 'total_mb': long((self._parted_dev.length * self._parted_dev.sector_size) / MEGABYTE) } ## # Sets partition info from disk. def set_partitions_from_disk(self): self._partitions = [] parted_part = self._parted_disk.next_partition() while parted_part: # Make sure it's a physical partition or unallocated space if parted_part.num >= 1 or parted_part.type_name == "free": self._partitions.append(Partition(self, parted_part)) parted_part = self._parted_disk.next_partition(parted_part) ## # Checks to see if partitions are mounted on device def has_mounted_partitions(self): if self._parted_dev.is_busy(): return True return False ## # Returns the partition object with the specified minor # @param idx Index of partition object def get_partition(self, idx): return self._partitions[idx] def get_partition_idx_from_part(self, part): for idx, tmppart in enumerate(self._partitions): if tmppart['start'] == part['start'] and tmppart['end'] == part['end']: return idx else: return -1 def get_partition_idx_from_minor(self, minor): for idx, tmppart in enumerate(self._partitions): if tmppart['minor'] == minor: return idx else: return -1 def get_partition_idx_from_start_end(self, start, end): for idx, tmppart in enumerate(self._partitions): if tmppart['start'] == start and tmppart['end'] == end: return idx else: return -1 def get_partiton_at_start(self, start): for i, part in enumerate(self._partitions): if part['start'] == start: return i return -1 ## # Returns name of device (e.g. /dev/hda) being represented def get_device(self): return self._device ## # Uses magic to apply the recommended partition layout def do_recommended(self): freeidx = -1 free_part = None remaining_free = 0 recommended_parts = [ { 'type': "ext2", 'size': 100, 'mountpoint': '/boot', 'mountopts': 'defaults' }, { 'type': "linux-swap", 'size': 512, 'mountpoint': 'swap', 'mountopts': 'defaults' }, { 'type': "ext3", 'size': "*", 'mountpoint': '/', 'mountopts': 'noatime' } ] localmounts = copy.deepcopy(self._install_profile.get_mounts()) physical_memory = int(GLIUtility.spawn(r"free -m | egrep '^Mem:' | sed -e 's/^Mem: \+//' -e 's/ \+.\+$//'", return_output=True)[1].strip()) self.clear_partitions() for newpart in recommended_parts: # Find idx of remaining unallocated space for idx, part in enumerate(self._partitions): # We want to ignore any "false" free space if part['type'] == "free" and part['mb'] >= 10: freeidx = idx free_part = part remaining_free = free_part['mb'] break # Small hack to calculate optimal swap partition size if newpart['type'] == "linux-swap" and physical_memory: newpart['size'] = physical_memory * 2 if newpart['size'] > 2048: newpart['size'] = 2048 if newpart['size'] == "*": newpart['size'] = remaining_free newidx = self.add_partition(freeidx, newpart['size'], newpart['type']) for i, mount in enumerate(localmounts): if mount['devnode'] == self._partitions[newidx]['devnode']: del localmounts[i] break localmounts.append({ 'devnode': self._partitions[newidx]['devnode'], 'type': parted_type_map.get(newpart['type'], newpart['type']), 'mountpoint': newpart['mountpoint'], 'mountopts': newpart['mountopts'] }) self._install_profile.set_mounts(localmounts) def _megabytes_to_sectors(self, mb, sector_bytes=512): return long(mb * MEGABYTE / sector_bytes) def _sectors_to_megabytes(self, sectors, sector_bytes=512): return float((float(sectors) * sector_bytes)/ float(MEGABYTE)) ## # Adds a new partition to the partition info # @param freeidx minor of unallocated space partition is being created in # @param mb size of partition in MB # @param type Partition type (ext2, ext3, fat32, linux-swap, free, extended, etc.) def add_partition(self, freeidx, mb, fs, mkfsopts="", pregap=0): types = { 'primary': parted.PARTITION_PRIMARY, 'extended': parted.PARTITION_EXTENDED, 'logical': parted.PARTITION_LOGICAL } fs_types = {} fstype = None try: free_part = self._partitions[freeidx] except: # raise an exception here pass if mb > free_part['mb']: # raise an exception here pass # Enumerate supported filesystem types fs_type = parted.file_system_type_get_next() while fs_type: fs_types[fs_type.name] = fs_type fs_type = parted.file_system_type_get_next(fs_type) # apple_bootstrap is a "magic" hfs if fs == "apple_bootstrap": fs = "hfs" # determine correct partition type parttype = "primary" if fs == "extended": fstype = None parttype = "extended" else: # grab relevant parted filesystemtype object if fs: fstype = fs_types[fs] if free_part.is_logical(): parttype = "logical" # figure out start/end sectors start = free_part['start'] + self._megabytes_to_sectors(pregap) end = start + self._megabytes_to_sectors(mb) parted_newpart = self._parted_disk.partition_new(types[parttype], fstype, start, end) constraint = self._parted_disk.dev.constraint_any() self._parted_disk.add_partition(parted_newpart, constraint) self._parted_disk.commit() self.set_partitions_from_disk() if parttype != "extended": newpart = self._partitions[self.get_partition_idx_from_minor(parted_newpart.num)] devnode = newpart['devnode'] format_cmds = { 'linux-swap': "mkswap", 'fat16': "mkfs.vfat -F 16", 'fat32': "mkfs.vfat -F 32", 'ntfs': "mkntfs", 'xfs': "mkfs.xfs -f", 'jfs': "mkfs.jfs -f", 'reiserfs': "mkfs.reiserfs -f", 'ext2': "mkfs.ext2", 'ext3': "mkfs.ext3", 'hfs': "hformat", 'apple_bootstrap': "hformat" } if fs in format_cmds: cmdname = format_cmds[fs] else: raise GLIException("PartitionFormatError", 'fatal', '_partition_format_step', "Unknown partition type " + fstype) # sleep a bit first time.sleep(1) wait_for_device_node(devnode) cmd = "%s %s %s" % (cmdname, mkfsopts, devnode) ret = GLIUtility.spawn(cmd) if not GLIUtility.exitsuccess(ret): raise GLIException("PartitionFormatError", 'fatal', '_partition_format_step', "Could not create %s filesystem on %s" % (fstype, devnode)) self.set_partitions_from_disk() newidx = self.get_partition_idx_from_start_end(parted_newpart.geom.start, parted_newpart.geom.end) return newidx ## # Removes partition from partition info # @param minor Minor of partition to remove def remove_partition(self, partidx): try: tmp_part = self._partitions[partidx] except: # raise exception here pass if tmp_part['minor'] < 1: # raise an exception here pass try: self._parted_disk.delete_partition(self._parted_disk.get_partition(tmp_part['minor'])) except: # raise an exception here pass self._parted_disk.commit() self.set_partitions_from_disk() # def resize_partition(self, partidx, mb): # part = self._partitions[partidx] # type = part['type'] # start = part['start'] # end = start + long(mb * MEGABYTE / 512) - 1 # # total_sectors = end - start + 1 # total_bytes = long(total_sectors) * 512 # # # Delete partition and recreate at same start point with new size if growing # if mb > part['mb']: # curminor = self._find_current_minor_for_part(device, start) # self._parted_disk.delete_partition(self._parted_disk.get_partition(part['minor'])) # if part['logical']: # tmptype = "logical" # else: # tmptype = "primary" # self._add_partition(parted_disk, start, end, tmptype, tmppart_new['type'], strict_start=True) # parted_disk.commit() # # curminor = self._find_current_minor_for_part(device, start) # devnode = device + str(curminor) # # wait_for_device_node(devnode) # # if type in ("ext2", "ext3"): # resizecmd = "resize2fs %s %sK" % (devnode, str(int((total_bytes - (2 * MEGABYTE)) / 1024))) # self._logger.log("_partition_resize_step(): running: " + resizecmd) # ret = GLIUtility.spawn(resizecmd, logfile=self._compile_logfile, append_log=True) # if not GLIUtility.exitsuccess(ret): # raise GLIException("PartitionResizeError", 'fatal', 'partition', "could not resize ext2/3 filesystem on " + devnode) # elif type == "ntfs": # ret = GLIUtility.spawn("yes | ntfsresize -v --size " + str(total_bytes) + " " + devnode, logfile=self._compile_logfile, append_log=True) # if not GLIUtility.exitsuccess(ret): # raise GLIException("PartitionResizeError", 'fatal', 'partition', "could not resize NTFS filesystem on " + devnode) # elif type in ("linux-swap", "fat32", "fat16"): # parted_fs = parted_disk.get_partition(curminor).geom.file_system_open() # resize_constraint = parted_fs.get_resize_constraint() # if total_sectors < resize_constraint.min_size or start != resize_constraint.start_range.start: # raise GLIException("PartitionError", 'fatal', 'partition', "New size specified for " + devnode + " is not within allowed boundaries (blame parted)") # new_geom = resize_constraint.start_range.duplicate() # new_geom.set_start(start) # new_geom.set_end(end) # try: # parted_fs.resize(new_geom) # except: # raise GLIException("PartitionResizeError", 'fatal', 'partition', "could not resize " + devnode) # self._logger.log(" Deleting old minor " + str(oldpart) + " to be recreated in next pass") # self._delete_partition(parted_disk, oldpart) # parted_disk.delete_all() # parted_disk.commit() ## # This function clears the partition table def clear_partitions(self, disklabel=None): if not disklabel: disklabel = archinfo[self._arch] self._parted_disk = self._parted_dev.disk_new_fresh(parted.disk_type_get(disklabel)) self._disklabel = disklabel self._parted_disk.commit() self.set_partitions_from_disk() ## # Returns an ordered list (disk order) of partitions def get_partitions(self): return self._partitions ## # Returns the minor of the extended partition, if any def get_extended_partition(self): for idx, part in enumerate(self._partitions): if part.is_extended(): return idx return -1 ## # Returns the drive model def get_model(self): return self._parted_dev.model ## # Returns the disklabel type def get_disklabel(self): return self._disklabel ## # Returns all the geometry information def get_geometry(self): return self._geometry ## # This class represents a partition within a GLIStorageDevice object class Partition: "Class representing a single partition within a Device object" ## # Initialization function for the Partition class # @param device Parent GLIStorageDevice object # @param parted_part parted.Partition object def __init__(self, device, parted_part): self._device = device self._start = parted_part.geom.start self._end = parted_part.geom.end self._type = parted_part.type_name self._minor = parted_part.num self._mb = int((self._end - self._start + 1) * device._geometry['sector_size'] / MEGABYTE) - 1 self._part_name = "" self._resizeable = False # determine the /dev node that refers to this partition tmpdevice = device.get_device() label_type = device._parted_disk.type.name if label_type == "loop": self._devnode = tmpdevice elif tmpdevice[-1] in "0123456789": self._devnode = tmpdevice + "p" + str(self._minor) else: self._devnode = tmpdevice + str(self._minor) if not self._type == "free": if parted_part.fs_type: self._type = parted_part.fs_type.name if self._type == "hfs" and parted_part.is_flag_available(1) and parted_part.get_flag(1): self._type = "apple_bootstrap" else: # Add additional partition identification code here self._type = "unknown" if parted_part.type == 2: self._type = "extended" if device._parted_disk.type.check_feature(parted.DISK_TYPE_PARTITION_NAME): self._part_name = parted_part.get_name() # The 10 is completely arbitrary. If flags seem to be missed, this number should be increased # for flag in range(0, 10): # if parted_part.is_flag_available(flag) and parted_part.get_flag(flag): # self._flags.append(flag) if type == "ext2" or type == "ext3": block_size = long(string.strip(commands.getoutput("dumpe2fs -h " + self._devnode + r" 2>&1 | grep -e '^Block size:' | sed -e 's/^Block size:\s\+//'"))) free_blocks = long(string.strip(commands.getoutput("dumpe2fs -h " + self._devnode + r" 2>&1 | grep -e '^Free blocks:' | sed -e 's/^Free blocks:\s\+//'"))) free_bytes = long(block_size * free_blocks) # can't hurt to pad (the +50) it a bit since this is really just a guess self._min_size = self._mb - long(free_bytes / MEGABYTE) + 50 self._resizeable = True elif type == "ntfs": min_bytes = long(commands.getoutput("ntfsresize -f --info " + self._devnode + " | grep -e '^You might resize' | sed -e 's/You might resize at //' -e 's/ bytes or .\+//'")) self._min_size = long(min_bytes / MEGABYTE) + 50 self._resizeable = True else: try: parted_fs = parted_part.geom.file_system_open() resize_constraint = parted_fs.get_resize_constraint() min_bytes = resize_constraint.min_size * self._device._geometry['sector_size'] self._min_size = long(min_bytes / MEGABYTE) + 1 self._resizeable = True except: self._min_size = self._mb self._resizeable = False def __getitem__(self, name): tmpdict = { 'start': self.get_start, 'end': self.get_end, 'type': self.get_type, 'minor': self.get_minor, 'mb': self.get_mb, # 'flags': self.get_flags, 'name': self.get_name, 'devnode': self.get_devnode, 'resizeable': self.get_resizeable, 'min_size': self.get_min_size, 'max_size': self.get_max_size, 'extended': self.is_extended, 'logical': self.is_logical, 'device': self.get_device } if name in tmpdict: return tmpdict[name]() else: raise ValueError(name + " is not a valid attribute!") def __setitem__(self, name, value): tmpdict = { # 'flags': self.set_flags, 'name': self.set_name } if name in tmpdict: tmpdict[name](value) else: raise ValueError(name + " is not a valid attribute!") ## # Returns the dev node that this partition will have def get_devnode(self): return self._devnode ## # Returns whether or not the partition is extended def is_extended(self): if self._type == "extended": return True else: return False ## # Returns whether or not the partition is logical def is_logical(self): if self._type == "free": ext_idx = self._device.get_extended_partition() if ext_idx == -1: return False ext_part = self._device[ext_idx] if self._start >= ext_part['start'] and self._start <= ext_part['end'] and self._end >= ext_part['start'] and self._end <= ext_part['end']: return True else: return False elif self._device._labelinfo['extended'] and self._minor > 4: return True else: return False ## # Returns a list of logical partitions if this is an extended partition def get_logicals(self): if not self.is_extended(): return None logicals = [] for part in self._device._partitions: if part.is_logical(): logicals.append(part) return logicals ## # Returns the start sector for the partition def get_start(self): return long(self._start) ## # Returns end sector for the partition def get_end(self): return long(self._end) ## # Returns size of partition in MB def get_mb(self): return self._mb ## # Returns type of partition def get_type(self): return self._type ## # Returns parent Device object def get_device(self): return self._device ## # Returns minor of partition def get_minor(self): return self._minor ## # Returns whether the partition is resizeable def get_resizeable(self): return self._resizeable ## # Sets partition flags def set_flags(self, flags): self._flags = flags ## # Returns partition flags def get_flags(self): return self._flags ## # Sets partition name def set_name(self, name): self._name = name ## # Returns partition name def get_name(self): return self._name ## # Returns minimum MB for resize def get_min_size(self): if self._resizeable: return self._min_size elif self._type == "free": return 0 else: return self._mb ## # Returns maximum MB for resize def get_max_size(self): if self._resizeable: partidx = self._device.get_partition_idx_from_minor(self._minor) next_part = self._device._partitions[partidx + 1] if next_part['type'] == "free": if (next_part.is_logical() and self.is_logical()) or (not next_part.is_logical() and not self.is_logical()): return self._mb + next_part['mb'] else: return self._mb else: return self._mb else: return self._mb """ ## # Resizes the partition # @param mb New size in MB def resize(self, mb): minor_pos = self._device.get_partition_idx_from_minor(self._minor) try: free_minor = self._device._partitions[minor_pos+1].get_minor() except: free_minor = 0 if mb < self._mb: # Shrinking if not free_minor or not self._device.get_partition(free_minor).get_type() == "free": if self._device._disklabel == "mac": free_minor = self._minor + 1 elif self.is_logical(): free_minor = self._minor + FREE_MINOR_FRAC_LOG else: free_minor = self._minor + FREE_MINOR_FRAC_PRI if self._device.get_partition(free_minor): for i, part in enumerate(self._device._partitions): if i <= minor_pos or free_minor > part.get_minor(): continue part.set_minor(part.get_minor() + 1) self._device._partitions.insert(minor_pos+1, Partition(self._device, free_minor, self._mb - mb, 0, 0, "free", format=False, existing=False)) else: self._device.get_partition(free_minor).set_mb(self._device.get_partition(free_minor).get_mb() + (self._mb - mb)) self._mb = mb else: if mb == self._mb + self._device.get_partition(free_minor).get_mb(): # Using all available unallocated space self._device._partitions.pop(self._device.get_partition_idx_from_minor(free_minor)) self._mb = mb else: # Growing self._device.get_partition(free_minor).set_mb(self._device.get_partition(free_minor).get_mb() - (mb - self._mb)) self._mb = mb self._resized = True self._device.tidy_partitions() """ ## # Returns a list of detected partitionable devices def detect_devices(): devices = [] # Make sure sysfs exists # TODO: rewrite for 2.4 support if not os.path.exists("/sys/bus"): raise GLIException("GLIStorageDeviceError", 'fatal', 'detect_devices', "no sysfs found (you MUST use a kernel >2.6)") # Make sure /proc/partitions exists if not os.path.exists("/proc/partitions"): raise GLIException("GLIStorageDeviceError", 'fatal', 'detect_devices', "/proc/partitions does not exist! Please make sure procfs is in your kernel and mounted!") # Load /proc/partitions into the variable 'partitions' partitions = [] for line in open("/proc/partitions"): tmpparts = line.split() if len(tmpparts) < 4 or not tmpparts[0].isdigit() or not tmpparts[1].isdigit(): continue # Get the major, minor and device name major = int(tmpparts[0]) minor = int(tmpparts[1]) device = "/dev/" + tmpparts[3] # If there is no /dev/'device_name', then scan # all the devices in /dev to try and find a # devices with the same major and minor if not os.path.exists(device): device = None for path, dirs, files in os.walk("/dev"): for d_file in files: full_file = os.path.join(path, d_file) if not os.path.exists(full_file): continue statres = os.stat(full_file) fmaj = os.major(statres.st_rdev) fmin = os.minor(statres.st_rdev) if fmaj == major and fmin == minor: device = full_file break if not device: continue partitions.append(( major, minor, device )) # Scan sysfs for the devices of type 'x' # 'x' being a member of the list below: # Compaq cards.../sys/block/{cciss,ida}!cXdX/dev for dev_glob in ("/sys/bus/ide/devices/*/block*/dev", "/sys/bus/scsi/devices/*/block*/dev", "/sys/block/cciss*/dev", "/sys/block/ida*/dev"): sysfs_devices = glob(dev_glob) if not sysfs_devices: continue for sysfs_device in sysfs_devices: # Get the major and minor info try: major, minor = open(sysfs_device).read().split(":") major = int(major) minor = int(minor) except: raise GLIException("GLIStorageDeviceError", 'fatal', 'detect_devices', "invalid major/minor in " + sysfs_device) # Find a device listed in /proc/partitions # that has the same minor and major as our # current block device. for record in partitions: if major == record[0] and minor == record[1]: devices.append(record[2]) # We have assembled the list of devices, so return it return devices def list_partitions(): # Load /proc/partitions into the variable 'partitions' # return [("/dev/" + x[3]) for x in [y.strip().split() for y in open("/proc/partitions", "r").readlines()] if len(x) >= 4 and x[3][-1].isdigit()] partitions = {} for line in open("/proc/partitions"): tmpparts = line.split() if len(tmpparts) < 4 or not tmpparts[0].isdigit() or not tmpparts[1].isdigit(): continue device = "/dev/" + tmpparts[3] if device == "/dev/loop0": #we don't want this. continue if not device[-1].isdigit(): #A Drive continue else: partitions[device] = tmpparts[2] return partitions def wait_for_device_node(devnode): if GLIUtility.is_file("/sbin/udevsettle"): GLIUtility.spawn("/sbin/udevsettle") if not GLIUtility.is_file(devnode): GLIUtility.spawn("/sbin/udevsettle") else: for i in range(0, 10): if GLIUtility.is_file(devnode): break time.sleep(1) time.sleep(1) for i in range(0, 10): if GLIUtility.is_file(devnode): break time.sleep(1)