947a5ba9a5
This change replaces the mkblankimage.sh script, used for creating new disk images, with a new gem5img.py script. The new version is written in python instead of bash, takes its parameters from command line arguments instead of prompting for them, and finds a free loopback device dynamically instead of hardcoding /dev/loop1. The file system used is now optionally configurable, and the blank image is filled by a "hole" left by lseek and write instead of literally filling it with zeroes. The functionality of the new script is broken into subcommands "init", "mount", "umount", "new", "partition", and "format". "init" creates a new file of the appropriate size, partitions it, and then formats the first (and only) new parition. "mount" attaches a new loopback device to the first parition of the image file and mounts it to the specified mount point. "umount" unmounts the specified mount point and identifies and cleans up the underlying loopback device. "new", "partition", and "format" are the individual stages of "init" but broken out so they can be run individually. That's so an image can be reinitialized in place if needed. Two features of the original script are being dropped. The first is the ability to specify a source directory to copy into the new file system. The second is the ability to specify a list of commands to run which are expected to (but not required to) update the permissions of the files in the new fs. Both of these seem easy enough to do manually, especially given the "mount" and "umount" commands, that removing them would meaningfully simplify the script without making it less useful.
372 lines
11 KiB
Python
Executable file
372 lines
11 KiB
Python
Executable file
#!/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] <command arguments>'
|
|
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()
|