3 # Copyright 2020 Google, Inc.
5 # Copyright (c) 2020 ARM Limited
8 # The license below extends only to copyright in the software and shall
9 # not be construed as granting a license to any other intellectual
10 # property including but not limited to intellectual property relating
11 # to a hardware implementation of the functionality of the software
12 # licensed hereunder. You may use the software subject to the license
13 # terms below provided that you ensure that this notice is replicated
14 # unmodified and in its entirety in all distributions of the software,
15 # modified or unmodified, in source code or in binary form.
17 # Redistribution and use in source and binary forms, with or without
18 # modification, are permitted provided that the following conditions are
19 # met: redistributions of source code must retain the above copyright
20 # notice, this list of conditions and the following disclaimer;
21 # redistributions in binary form must reproduce the above copyright
22 # notice, this list of conditions and the following disclaimer in the
23 # documentation and/or other materials provided with the distribution;
24 # neither the name of the copyright holders nor the names of its
25 # contributors may be used to endorse or promote products derived from
26 # this software without specific prior written permission.
28 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
29 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
30 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
31 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
32 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
33 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
34 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
35 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
36 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
37 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
38 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42 # Script for managing a gem5 disk image.
45 from optparse
import OptionParser
47 from os
import environ
as env
49 from subprocess
import CalledProcessError
, Popen
, PIPE
, STDOUT
50 from sys
import exit
, argv
54 MaxLBACylinders
= 16383
57 MaxLBABlocks
= MaxLBACylinders
* MaxLBAHeads
* MaxLBASectors
62 # Setup PATH to look in the sbins.
63 env
['PATH'] += ':/sbin:/usr/sbin'
65 # Whether to print debug output.
68 # Figure out cylinders, heads and sectors from a size in blocks.
69 def chsFromSize(sizeInBlocks
):
70 if sizeInBlocks
>= MaxLBABlocks
:
71 sizeInMBs
= (sizeInBlocks
* BlockSize
) / MB
72 print('%d MB is too big for LBA, truncating file.' % sizeInMBs
)
73 return (MaxLBACylinders
, MaxLBAHeads
, MaxLBASectors
)
75 sectors
= sizeInBlocks
79 headSize
= sizeInBlocks
/ sectors
84 cylinders
= sizeInBlocks
/ (sectors
* heads
)
86 return (cylinders
, heads
, sectors
)
89 # Figure out if we should use sudo.
91 if not hasattr(needSudo
, 'notRoot'):
92 needSudo
.notRoot
= (os
.geteuid() != 0)
94 print('You are not root. Using sudo.')
95 return needSudo
.notRoot
97 # Run an external command.
98 def runCommand(command
, inputVal
=''):
99 print("%>", ' '.join(command
))
100 proc
= Popen(command
, stdin
=PIPE
)
101 proc
.communicate(inputVal
.encode())
102 return proc
.returncode
104 # Run an external command and capture its output. This is intended to be
105 # used with non-interactive commands where the output is for internal use.
106 def getOutput(command
, inputVal
=''):
109 print("%>", ' '.join(command
))
110 proc
= Popen(command
, stderr
=STDOUT
,
111 stdin
=PIPE
, stdout
=PIPE
)
112 (out
, err
) = proc
.communicate(inputVal
)
113 return (out
.decode(), proc
.returncode
)
115 # Run a command as root, using sudo if necessary.
116 def runPriv(command
, inputVal
=''):
117 realCommand
= command
119 realCommand
= [findProg('sudo')] + command
120 return runCommand(realCommand
, inputVal
)
122 def privOutput(command
, inputVal
=''):
123 realCommand
= command
125 realCommand
= [findProg('sudo')] + command
126 return getOutput(realCommand
, inputVal
)
128 # Find the path to a program.
129 def findProg(program
, cleanupDev
=None):
130 (out
, returncode
) = getOutput(['which', program
])
134 exit("Unable to find program %s, check your PATH variable." % program
)
137 class LoopbackDevice(object):
138 def __init__(self
, devFile
=None):
139 self
.devFile
= devFile
141 return str(self
.devFile
)
143 def setup(self
, fileName
, offset
=False):
144 assert not self
.devFile
145 (out
, returncode
) = privOutput([findProg('losetup'), '-f'])
149 self
.devFile
= out
.strip()
150 command
= [findProg('losetup'), self
.devFile
, fileName
]
152 off
= findPartOffset(self
.devFile
, fileName
, 0)
153 command
= command
[:1] + \
154 ["-o", "%d" % off
] + \
156 return runPriv(command
)
160 returncode
= runPriv([findProg('losetup'), '-d', self
.devFile
])
164 def findPartOffset(devFile
, fileName
, partition
):
165 # Attach a loopback device to the file so we can use sfdisk on it.
166 dev
= LoopbackDevice()
168 # Dump the partition information.
169 command
= [findProg('sfdisk'), '-d', dev
.devFile
]
170 (out
, returncode
) = privOutput(command
)
175 # Parse each line of the sfdisk output looking for the first
176 # partition description.
177 SFDISK_PARTITION_INFO_RE
= re
.compile(
178 r
"^\s*" # Start of line
179 r
"(?P<name>\S+)" # Name
180 r
"\s*:\s*" # Separator
181 r
"start=\s*(?P<start>\d+),\s*" # Partition start record
182 r
"size=\s*(?P<size>\d+),\s*" # Partition size record
183 r
"type=(?P<type>\d+)" # Partition type record
184 r
"\s*$" # End of line
186 lines
= out
.splitlines()
188 match
= SFDISK_PARTITION_INFO_RE
.match(line
)
190 sectors
= int(match
.group("start"))
193 # No partition description was found
194 print("No partition description was found in sfdisk output:")
195 print("\n".join(" {}".format(line
.rstrip()) for line
in lines
))
196 print("Could not determine size of first partition.")
199 # Free the loopback device and return an answer.
201 return sectors
* BlockSize
203 def mountPointToDev(mountPoint
):
204 (mountTable
, returncode
) = getOutput([findProg('mount')])
208 mountTable
= mountTable
.splitlines()
209 for line
in mountTable
:
210 chunks
= line
.split()
212 if os
.path
.samefile(chunks
[2], mountPoint
):
213 return LoopbackDevice(chunks
[0])
219 # Commands for the gem5img.py script
223 class Command(object):
224 def addOption(self
, *args
, **kargs
):
225 self
.parser
.add_option(*args
, **kargs
)
227 def __init__(self
, name
, description
, posArgs
):
229 self
.description
= description
231 self
.posArgs
= posArgs
232 commands
[self
.name
] = self
233 commandOrder
.append(self
.name
)
234 usage
= 'usage: %prog [options]'
236 for posArg
in posArgs
:
237 (argName
, argDesc
) = posArg
238 usage
+= ' %s' % argName
239 posUsage
+= '\n %s: %s' % posArg
241 self
.parser
= OptionParser(usage
=usage
, description
=description
)
242 self
.addOption('-d', '--debug', dest
='debug', action
='store_true',
243 help='Verbose output.')
245 def parseArgs(self
, argv
):
246 (self
.options
, self
.args
) = self
.parser
.parse_args(argv
[2:])
247 if len(self
.args
) != len(self
.posArgs
):
248 self
.parser
.error('Incorrect number of arguments')
250 if self
.options
.debug
:
255 exit('Unimplemented command %s!' % self
.name
)
256 self
.func(self
.options
, self
.args
)
259 # A command which prepares an image with an partition table and an empty file
261 initCom
= Command('init', 'Create an image with an empty file system.',
262 [('file', 'Name of the image file.'),
263 ('mb', 'Size of the file in MB.')])
264 initCom
.addOption('-t', '--type', dest
='fstype', action
='store',
266 help='Type of file system to use. Appended to mkfs.')
268 # A command to mount the first partition in the image.
269 mountCom
= Command('mount', 'Mount the first partition in the disk image.',
270 [('file', 'Name of the image file.'),
271 ('mount point', 'Where to mount the image.')])
273 def mountComFunc(options
, args
):
274 (path
, mountPoint
) = args
275 if not os
.path
.isdir(mountPoint
):
276 print("Mount point %s is not a directory." % mountPoint
)
278 dev
= LoopbackDevice()
279 if dev
.setup(path
, offset
=True) != 0:
282 if runPriv([findProg('mount'), str(dev
), mountPoint
]) != 0:
286 mountCom
.func
= mountComFunc
288 # A command to unmount the first partition in the image.
289 umountCom
= Command('umount', 'Unmount the disk image mounted at mount_point.',
290 [('mount_point', 'What mount point to unmount.')])
292 def umountComFunc(options
, args
):
294 if not os
.path
.isdir(mountPoint
):
295 print("Mount point %s is not a directory." % mountPoint
)
298 dev
= mountPointToDev(mountPoint
)
300 print("Unable to find mount information for %s." % mountPoint
)
302 # Unmount the loopback device.
303 if runPriv([findProg('umount'), mountPoint
]) != 0:
306 # Destroy the loopback device.
309 umountCom
.func
= umountComFunc
312 # A command to create an empty file to hold the image.
313 newCom
= Command('new', 'File creation part of "init".',
314 [('file', 'Name of the image file.'),
315 ('mb', 'Size of the file in MB.')])
317 def newImage(file, mb
):
318 (cylinders
, heads
, sectors
) = chsFromSize((mb
* MB
) / BlockSize
)
319 size
= cylinders
* heads
* sectors
* BlockSize
321 # We lseek to the end of the file and only write one byte there. This
322 # leaves a "hole" which many file systems are smart enough not to actually
323 # store to disk and which is defined to read as zero.
324 fd
= os
.open(file, os
.O_WRONLY | os
.O_CREAT
)
325 os
.lseek(fd
, size
- 1, os
.SEEK_SET
)
328 def newComFunc(options
, args
):
334 newCom
.func
= newComFunc
336 # A command to partition the image file like a raw disk device.
337 partitionCom
= Command('partition', 'Partition part of "init".',
338 [('file', 'Name of the image file.')])
340 def partition(dev
, cylinders
, heads
, sectors
):
341 # Use sfdisk to partition the device
342 # The specified options are intended to work with both new and old
343 # versions of sfdisk (see https://askubuntu.com/a/819614)
345 return runPriv([findProg('sfdisk'), '--no-reread', '-u', 'S', '-L', \
346 str(dev
)], inputVal
=comStr
)
348 def partitionComFunc(options
, args
):
351 dev
= LoopbackDevice()
352 if dev
.setup(path
) != 0:
355 # Figure out the dimensions of the file.
356 size
= os
.path
.getsize(path
)
357 if partition(dev
, *chsFromSize(size
/ BlockSize
)) != 0:
363 partitionCom
.func
= partitionComFunc
365 # A command to format the first partition in the image.
366 formatCom
= Command('format', 'Formatting part of "init".',
367 [('file', 'Name of the image file.')])
368 formatCom
.addOption('-t', '--type', dest
='fstype', action
='store',
370 help='Type of file system to use. Appended to mkfs.')
372 def formatImage(dev
, fsType
):
373 return runPriv([findProg('mkfs.%s' % fsType
, dev
), str(dev
)])
375 def formatComFunc(options
, args
):
378 dev
= LoopbackDevice()
379 if dev
.setup(path
, offset
=True) != 0:
383 if formatImage(dev
, options
.fstype
) != 0:
389 formatCom
.func
= formatComFunc
391 def initComFunc(options
, args
):
395 dev
= LoopbackDevice()
396 if dev
.setup(path
) != 0:
398 size
= os
.path
.getsize(path
)
399 if partition(dev
, *chsFromSize((mb
* MB
) / BlockSize
)) != 0:
403 if dev
.setup(path
, offset
=True) != 0:
405 if formatImage(dev
, options
.fstype
) != 0:
410 initCom
.func
= initComFunc
413 # Figure out what command was requested and execute it.
414 if len(argv
) < 2 or argv
[1] not in commands
:
415 print('Usage: %s [command] <command arguments>')
416 print('where [command] is one of ')
417 for name
in commandOrder
:
418 command
= commands
[name
]
419 print(' %s: %s' % (command
.name
, command
.description
))
420 print('Watch for orphaned loopback devices and delete them with')
421 print('losetup -d. Mounted images will belong to root, so you may need')
422 print('to use sudo to modify their contents.')
425 command
= commands
[argv
[1]]
426 command
.parseArgs(argv
)