#!/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 argparse import ArgumentParser 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\S+)" # Name r"\s*:\s*" # Separator r"start=\s*(?P\d+),\s*" # Partition start record r"size=\s*(?P\d+),\s*" # Partition size record r"type=(?P\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 addArgument(self, *args, **kargs): self.parser.add_argument(*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 = '%(prog)s [options]' posUsage = '' for posArg in posArgs: (argName, argDesc) = posArg usage += ' %s' % argName posUsage += '\n %s: %s' % posArg usage += posUsage self.parser = ArgumentParser(usage=usage, description=description) self.addArgument('-d', '--debug', dest='debug', action='store_true', help='Verbose output.') self.addArgument('pos', nargs='*') def parseArgs(self, argv): self.options = self.parser.parse_args(argv[2:]) self.args = self.options.pos 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.addArgument('-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.addArgument('-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] ') 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()