#!/usr/bin/python # # gem5img.py # Script for managing a gem5 disk image. # from optparse import OptionParser import os from os import environ as env import string from subprocess import CalledProcessError, Popen, PIPE, STDOUT from sys import exit, argv # Some constants. MaxLBACylinders = 16383 MaxLBAHeads = 16 MaxLBASectors = 63 MaxLBABlocks = MaxLBACylinders * MaxLBAHeads * MaxLBASectors BlockSize = 512 MB = 1024 * 1024 # Setup PATH to look in the sbins. env['PATH'] += ':/sbin:/usr/sbin' # Whether to print debug output. debug = False # Figure out cylinders, heads and sectors from a size in blocks. def chsFromSize(sizeInBlocks): if sizeInBlocks >= MaxLBABlocks: sizeInMBs = (sizeInBlocks * BlockSize) / MB print '%d MB is too big for LBA, truncating file.' % sizeInMBs return (MaxLBACylinders, MaxLBAHeads, MaxLBASectors) sectors = sizeInBlocks if sizeInBlocks > 63: sectors = 63 headSize = sizeInBlocks / sectors heads = 16 if headSize < 16: heads = sizeInBlocks cylinders = sizeInBlocks / (sectors * heads) return (cylinders, heads, sectors) # Figure out if we should use sudo. def needSudo(): if not hasattr(needSudo, 'notRoot'): needSudo.notRoot = (os.geteuid() != 0) if needSudo.notRoot: print 'You are not root. Using sudo.' return needSudo.notRoot # Run an external command. def runCommand(command, inputVal=''): print "%>", ' '.join(command) proc = Popen(command, stdin=PIPE) proc.communicate(inputVal) return proc.returncode # Run an external command and capture its output. This is intended to be # used with non-interactive commands where the output is for internal use. def getOutput(command, inputVal=''): global debug if debug: print "%>", ' '.join(command) proc = Popen(command, stderr=STDOUT, stdin=PIPE, stdout=PIPE) (out, err) = proc.communicate(inputVal) return (out, proc.returncode) # Run a command as root, using sudo if necessary. def runPriv(command, inputVal=''): realCommand = command if needSudo(): realCommand = [findProg('sudo')] + command return runCommand(realCommand, inputVal) def privOutput(command, inputVal=''): realCommand = command if needSudo(): realCommand = [findProg('sudo')] + command return getOutput(realCommand, inputVal) # Find the path to a program. def findProg(program, cleanupDev=None): (out, returncode) = getOutput(['which', program]) if returncode != 0: if cleanupDev: cleanupDev.destroy() exit("Unable to find program %s, check your PATH variable." % program) return string.strip(out) class LoopbackDevice(object): def __init__(self, devFile=None): self.devFile = devFile def __str__(self): return str(self.devFile) def setup(self, fileName, offset=False): assert not self.devFile (out, returncode) = privOutput([findProg('losetup'), '-f']) if returncode != 0: print out return returncode self.devFile = string.strip(out) command = [findProg('losetup'), self.devFile, fileName] if offset: off = findPartOffset(self.devFile, fileName, 0) command = command[:1] + \ ["-o", "%d" % off] + \ command[1:] return runPriv(command) def destroy(self): assert self.devFile returncode = runPriv([findProg('losetup'), '-d', self.devFile]) self.devFile = None return returncode def findPartOffset(devFile, fileName, partition): # Attach a loopback device to the file so we can use sfdisk on it. dev = LoopbackDevice() dev.setup(fileName) # Dump the partition information. command = [findProg('sfdisk'), '-d', dev.devFile] (out, returncode) = privOutput(command) if returncode != 0: print out exit(returncode) lines = out.splitlines() # Make sure the first few lines of the output look like what we expect. assert(lines[0][0] == '#') assert(lines[1] == 'unit: sectors') assert(lines[2] == '') # This line has information about the first partition. chunks = lines[3].split() # The fourth chunk is the offset of the partition in sectors followed by # a comma. We drop the comma and convert that to an integer. sectors = string.atoi(chunks[3][:-1]) # Free the loopback device and return an answer. dev.destroy() return sectors * BlockSize def mountPointToDev(mountPoint): (mountTable, returncode) = getOutput([findProg('mount')]) if returncode != 0: print mountTable exit(returncode) mountTable = mountTable.splitlines() for line in mountTable: chunks = line.split() if os.path.samefile(chunks[2], mountPoint): return LoopbackDevice(chunks[0]) return None # Commands for the gem5img.py script commands = {} commandOrder = [] class Command(object): def addOption(self, *args, **kargs): self.parser.add_option(*args, **kargs) def __init__(self, name, description, posArgs): self.name = name self.description = description self.func = None self.posArgs = posArgs commands[self.name] = self commandOrder.append(self.name) usage = 'usage: %prog [options]' posUsage = '' for posArg in posArgs: (argName, argDesc) = posArg usage += ' %s' % argName posUsage += '\n %s: %s' % posArg usage += posUsage self.parser = OptionParser(usage=usage, description=description) self.addOption('-d', '--debug', dest='debug', action='store_true', help='Verbose output.') def parseArgs(self, argv): (self.options, self.args) = self.parser.parse_args(argv[2:]) if len(self.args) != len(self.posArgs): self.parser.error('Incorrect number of arguments') global debug if self.options.debug: debug = True def runCom(self): if not self.func: exit('Unimplemented command %s!' % self.name) self.func(self.options, self.args) # A command which prepares an image with an partition table and an empty file # system. initCom = Command('init', 'Create an image with an empty file system.', [('file', 'Name of the image file.'), ('mb', 'Size of the file in MB.')]) initCom.addOption('-t', '--type', dest='fstype', action='store', default='ext2', help='Type of file system to use. Appended to mkfs.') # A command to mount the first partition in the image. mountCom = Command('mount', 'Mount the first partition in the disk image.', [('file', 'Name of the image file.'), ('mount point', 'Where to mount the image.')]) def mountComFunc(options, args): (path, mountPoint) = args if not os.path.isdir(mountPoint): print "Mount point %s is not a directory." % mountPoint dev = LoopbackDevice() if dev.setup(path, offset=True) != 0: exit(1) if runPriv([findProg('mount'), str(dev), mountPoint]) != 0: dev.destroy() exit(1) mountCom.func = mountComFunc # A command to unmount the first partition in the image. umountCom = Command('umount', 'Unmount the first partition in the disk image.', [('mount point', 'What mount point to unmount.')]) def umountComFunc(options, args): (mountPoint,) = args if not os.path.isdir(mountPoint): print "Mount point %s is not a directory." % mountPoint exit(1) dev = mountPointToDev(mountPoint) if not dev: print "Unable to find mount information for %s." % mountPoint # Unmount the loopback device. if runPriv([findProg('umount'), mountPoint]) != 0: exit(1) # Destroy the loopback device. dev.destroy() umountCom.func = umountComFunc # A command to create an empty file to hold the image. newCom = Command('new', 'File creation part of "init".', [('file', 'Name of the image file.'), ('mb', 'Size of the file in MB.')]) def newImage(file, mb): (cylinders, heads, sectors) = chsFromSize((mb * MB) / BlockSize) size = cylinders * heads * sectors * BlockSize # We lseek to the end of the file and only write one byte there. This # leaves a "hole" which many file systems are smart enough not to actually # store to disk and which is defined to read as zero. fd = os.open(file, os.O_WRONLY | os.O_CREAT) os.lseek(fd, size - 1, os.SEEK_SET) os.write(fd, '\0') def newComFunc(options, args): (file, mb) = args mb = string.atoi(mb) newImage(file, mb) newCom.func = newComFunc # A command to partition the image file like a raw disk device. partitionCom = Command('partition', 'Partition part of "init".', [('file', 'Name of the image file.')]) def partition(dev, cylinders, heads, sectors): # Use fdisk to partition the device comStr = '0,\n;\n;\n;\n' return runPriv([findProg('sfdisk'), '--no-reread', '-D', \ '-C', "%d" % cylinders, \ '-H', "%d" % heads, \ '-S', "%d" % sectors, \ str(dev)], inputVal=comStr) def partitionComFunc(options, args): (path,) = args dev = LoopbackDevice() if dev.setup(path) != 0: exit(1) # Figure out the dimensions of the file. size = os.path.getsize(path) if partition(dev, *chsFromSize(size / BlockSize)) != 0: dev.destroy() exit(1) dev.destroy() partitionCom.func = partitionComFunc # A command to format the first partition in the image. formatCom = Command('format', 'Formatting part of "init".', [('file', 'Name of the image file.')]) formatCom.addOption('-t', '--type', dest='fstype', action='store', default='ext2', help='Type of file system to use. Appended to mkfs.') def formatImage(dev, fsType): return runPriv([findProg('mkfs.%s' % fsType, dev), str(dev)]) def formatComFunc(options, args): (path,) = args dev = LoopbackDevice() if dev.setup(path, offset=True) != 0: exit(1) # Format the device. if formatImage(dev, options.fstype) != 0: dev.destroy() exit(1) dev.destroy() formatCom.func = formatComFunc def initComFunc(options, args): (path, mb) = args mb = string.atoi(mb) newImage(path, mb) dev = LoopbackDevice() if dev.setup(path) != 0: exit(1) size = os.path.getsize(path) if partition(dev, *chsFromSize((mb * MB) / BlockSize)) != 0: dev.destroy() exit(1) dev.destroy() if dev.setup(path, offset=True) != 0: exit(1) if formatImage(dev, options.fstype) != 0: dev.destroy() exit(1) dev.destroy() initCom.func = initComFunc # Figure out what command was requested and execute it. if len(argv) < 2 or argv[1] not in commands: print 'Usage: %s [command] ' print 'where [command] is one of ' for name in commandOrder: command = commands[name] print ' %s: %s' % (command.name, command.description) print 'Watch for orphaned loopback devices and delete them with' print 'losetup -d. Mounted images will belong to root, so you may need' print 'to use sudo to modify their contents.' exit(1) command = commands[argv[1]] command.parseArgs(argv) command.runCom()