When unmounting a disk image manually using the `gem5img umount mount_point` command, the operation can fail if the process is unable to stat any of the mounts in the mount table. On some systems this can occur even when running using sudo. Added an exception check so any mount points that fail to stat will not cause the whole script to terminate early. Change-Id: I69cd2494ad0e8c989e19ecd8af8a811905cd6c09 Reviewed-on: https://gem5-review.googlesource.com/c/public/gem5/+/39897 Reviewed-by: Bobby R. Bruce <bbruce@ucdavis.edu> Reviewed-by: Gabe Black <gabe.black@gmail.com> Maintainer: Bobby R. Bruce <bbruce@ucdavis.edu> Maintainer: Gabe Black <gabe.black@gmail.com> Tested-by: kokoro <noreply+kokoro@google.com>
428 lines
14 KiB
Python
Executable File
428 lines
14 KiB
Python
Executable File
#!/usr/bin/python3
|
|
#
|
|
# Copyright 2020 Google, Inc.
|
|
#
|
|
# Copyright (c) 2020 ARM Limited
|
|
# All rights reserved
|
|
#
|
|
# The license below extends only to copyright in the software and shall
|
|
# not be construed as granting a license to any other intellectual
|
|
# property including but not limited to intellectual property relating
|
|
# to a hardware implementation of the functionality of the software
|
|
# licensed hereunder. You may use the software subject to the license
|
|
# terms below provided that you ensure that this notice is replicated
|
|
# unmodified and in its entirety in all distributions of the software,
|
|
# modified or unmodified, in source code or in binary form.
|
|
#
|
|
# Redistribution and use in source and binary forms, with or without
|
|
# modification, are permitted provided that the following conditions are
|
|
# met: redistributions of source code must retain the above copyright
|
|
# notice, this list of conditions and the following disclaimer;
|
|
# redistributions in binary form must reproduce the above copyright
|
|
# notice, this list of conditions and the following disclaimer in the
|
|
# documentation and/or other materials provided with the distribution;
|
|
# neither the name of the copyright holders nor the names of its
|
|
# contributors may be used to endorse or promote products derived from
|
|
# this software without specific prior written permission.
|
|
#
|
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
#
|
|
# 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
|
|
import re
|
|
|
|
# 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.encode())
|
|
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.decode(), 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 out.strip()
|
|
|
|
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 = out.strip()
|
|
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)
|
|
|
|
# Parse each line of the sfdisk output looking for the first
|
|
# partition description.
|
|
SFDISK_PARTITION_INFO_RE = re.compile(
|
|
r"^\s*" # Start of line
|
|
r"(?P<name>\S+)" # Name
|
|
r"\s*:\s*" # Separator
|
|
r"start=\s*(?P<start>\d+),\s*" # Partition start record
|
|
r"size=\s*(?P<size>\d+),\s*" # Partition size record
|
|
r"type=(?P<type>\d+)" # Partition type record
|
|
r"\s*$" # End of line
|
|
)
|
|
lines = out.splitlines()
|
|
for line in lines :
|
|
match = SFDISK_PARTITION_INFO_RE.match(line)
|
|
if match:
|
|
sectors = int(match.group("start"))
|
|
break
|
|
else:
|
|
# No partition description was found
|
|
print("No partition description was found in sfdisk output:")
|
|
print("\n".join(" {}".format(line.rstrip()) for line in lines))
|
|
print("Could not determine size of first partition.")
|
|
exit(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()
|
|
try:
|
|
if os.path.samefile(chunks[2], mountPoint):
|
|
return LoopbackDevice(chunks[0])
|
|
except OSError:
|
|
continue
|
|
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 disk image mounted at mount_point.',
|
|
[('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, b'\0')
|
|
|
|
def newComFunc(options, args):
|
|
(file, mb) = args
|
|
mb = int(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 sfdisk to partition the device
|
|
# The specified options are intended to work with both new and old
|
|
# versions of sfdisk (see https://askubuntu.com/a/819614)
|
|
comStr = ';'
|
|
return runPriv([findProg('sfdisk'), '--no-reread', '-u', 'S', '-L', \
|
|
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 = int(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()
|