3 # Copyright 2020 Google, Inc.
5 # Redistribution and use in source and binary forms, with or without
6 # modification, are permitted provided that the following conditions are
7 # met: redistributions of source code must retain the above copyright
8 # notice, this list of conditions and the following disclaimer;
9 # redistributions in binary form must reproduce the above copyright
10 # notice, this list of conditions and the following disclaimer in the
11 # documentation and/or other materials provided with the distribution;
12 # neither the name of the copyright holders nor the names of its
13 # contributors may be used to endorse or promote products derived from
14 # this software without specific prior written permission.
16 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 # Script for managing a gem5 disk image.
33 from optparse
import OptionParser
35 from os
import environ
as env
37 from subprocess
import CalledProcessError
, Popen
, PIPE
, STDOUT
38 from sys
import exit
, argv
42 MaxLBACylinders
= 16383
45 MaxLBABlocks
= MaxLBACylinders
* MaxLBAHeads
* MaxLBASectors
50 # Setup PATH to look in the sbins.
51 env
['PATH'] += ':/sbin:/usr/sbin'
53 # Whether to print debug output.
56 # Figure out cylinders, heads and sectors from a size in blocks.
57 def chsFromSize(sizeInBlocks
):
58 if sizeInBlocks
>= MaxLBABlocks
:
59 sizeInMBs
= (sizeInBlocks
* BlockSize
) / MB
60 print('%d MB is too big for LBA, truncating file.' % sizeInMBs
)
61 return (MaxLBACylinders
, MaxLBAHeads
, MaxLBASectors
)
63 sectors
= sizeInBlocks
67 headSize
= sizeInBlocks
/ sectors
72 cylinders
= sizeInBlocks
/ (sectors
* heads
)
74 return (cylinders
, heads
, sectors
)
77 # Figure out if we should use sudo.
79 if not hasattr(needSudo
, 'notRoot'):
80 needSudo
.notRoot
= (os
.geteuid() != 0)
82 print('You are not root. Using sudo.')
83 return needSudo
.notRoot
85 # Run an external command.
86 def runCommand(command
, inputVal
=''):
87 print("%>", ' '.join(command
))
88 proc
= Popen(command
, stdin
=PIPE
)
89 proc
.communicate(inputVal
)
90 return proc
.returncode
92 # Run an external command and capture its output. This is intended to be
93 # used with non-interactive commands where the output is for internal use.
94 def getOutput(command
, inputVal
=''):
97 print("%>", ' '.join(command
))
98 proc
= Popen(command
, stderr
=STDOUT
,
99 stdin
=PIPE
, stdout
=PIPE
)
100 (out
, err
) = proc
.communicate(inputVal
)
101 return (out
, proc
.returncode
)
103 # Run a command as root, using sudo if necessary.
104 def runPriv(command
, inputVal
=''):
105 realCommand
= command
107 realCommand
= [findProg('sudo')] + command
108 return runCommand(realCommand
, inputVal
)
110 def privOutput(command
, inputVal
=''):
111 realCommand
= command
113 realCommand
= [findProg('sudo')] + command
114 return getOutput(realCommand
, inputVal
)
116 # Find the path to a program.
117 def findProg(program
, cleanupDev
=None):
118 (out
, returncode
) = getOutput(['which', program
])
122 exit("Unable to find program %s, check your PATH variable." % program
)
123 return string
.strip(out
)
125 class LoopbackDevice(object):
126 def __init__(self
, devFile
=None):
127 self
.devFile
= devFile
129 return str(self
.devFile
)
131 def setup(self
, fileName
, offset
=False):
132 assert not self
.devFile
133 (out
, returncode
) = privOutput([findProg('losetup'), '-f'])
137 self
.devFile
= string
.strip(out
)
138 command
= [findProg('losetup'), self
.devFile
, fileName
]
140 off
= findPartOffset(self
.devFile
, fileName
, 0)
141 command
= command
[:1] + \
142 ["-o", "%d" % off
] + \
144 return runPriv(command
)
148 returncode
= runPriv([findProg('losetup'), '-d', self
.devFile
])
152 def findPartOffset(devFile
, fileName
, partition
):
153 # Attach a loopback device to the file so we can use sfdisk on it.
154 dev
= LoopbackDevice()
156 # Dump the partition information.
157 command
= [findProg('sfdisk'), '-d', dev
.devFile
]
158 (out
, returncode
) = privOutput(command
)
162 lines
= out
.splitlines()
163 # Make sure the first few lines of the output look like what we expect.
164 assert(lines
[0][0] == '#' or lines
[0].startswith('label:'))
165 assert(lines
[1] == 'unit: sectors' or lines
[1].startswith('label-id:'))
166 assert(lines
[2] == '' or lines
[2].startswith('device:'))
167 if lines
[0][0] == '#' :
168 # Parsing an 'old style' dump oputput
169 # Line 4 has information about the first partition.
170 chunks
= lines
[3].split()
172 # Parsing a 'new style' dump oputput
173 # Line 6 has information about the first partition.
174 chunks
= lines
[5].split()
175 # The fourth chunk is the offset of the partition in sectors followed by
176 # a comma. We drop the comma and convert that to an integer.
177 sectors
= string
.atoi(chunks
[3][:-1])
178 # Free the loopback device and return an answer.
180 return sectors
* BlockSize
182 def mountPointToDev(mountPoint
):
183 (mountTable
, returncode
) = getOutput([findProg('mount')])
187 mountTable
= mountTable
.splitlines()
188 for line
in mountTable
:
189 chunks
= line
.split()
190 if os
.path
.samefile(chunks
[2], mountPoint
):
191 return LoopbackDevice(chunks
[0])
195 # Commands for the gem5img.py script
199 class Command(object):
200 def addOption(self
, *args
, **kargs
):
201 self
.parser
.add_option(*args
, **kargs
)
203 def __init__(self
, name
, description
, posArgs
):
205 self
.description
= description
207 self
.posArgs
= posArgs
208 commands
[self
.name
] = self
209 commandOrder
.append(self
.name
)
210 usage
= 'usage: %prog [options]'
212 for posArg
in posArgs
:
213 (argName
, argDesc
) = posArg
214 usage
+= ' %s' % argName
215 posUsage
+= '\n %s: %s' % posArg
217 self
.parser
= OptionParser(usage
=usage
, description
=description
)
218 self
.addOption('-d', '--debug', dest
='debug', action
='store_true',
219 help='Verbose output.')
221 def parseArgs(self
, argv
):
222 (self
.options
, self
.args
) = self
.parser
.parse_args(argv
[2:])
223 if len(self
.args
) != len(self
.posArgs
):
224 self
.parser
.error('Incorrect number of arguments')
226 if self
.options
.debug
:
231 exit('Unimplemented command %s!' % self
.name
)
232 self
.func(self
.options
, self
.args
)
235 # A command which prepares an image with an partition table and an empty file
237 initCom
= Command('init', 'Create an image with an empty file system.',
238 [('file', 'Name of the image file.'),
239 ('mb', 'Size of the file in MB.')])
240 initCom
.addOption('-t', '--type', dest
='fstype', action
='store',
242 help='Type of file system to use. Appended to mkfs.')
244 # A command to mount the first partition in the image.
245 mountCom
= Command('mount', 'Mount the first partition in the disk image.',
246 [('file', 'Name of the image file.'),
247 ('mount point', 'Where to mount the image.')])
249 def mountComFunc(options
, args
):
250 (path
, mountPoint
) = args
251 if not os
.path
.isdir(mountPoint
):
252 print("Mount point %s is not a directory." % mountPoint
)
254 dev
= LoopbackDevice()
255 if dev
.setup(path
, offset
=True) != 0:
258 if runPriv([findProg('mount'), str(dev
), mountPoint
]) != 0:
262 mountCom
.func
= mountComFunc
264 # A command to unmount the first partition in the image.
265 umountCom
= Command('umount', 'Unmount the first partition in the disk image.',
266 [('mount point', 'What mount point to unmount.')])
268 def umountComFunc(options
, args
):
270 if not os
.path
.isdir(mountPoint
):
271 print("Mount point %s is not a directory." % mountPoint
)
274 dev
= mountPointToDev(mountPoint
)
276 print("Unable to find mount information for %s." % mountPoint
)
278 # Unmount the loopback device.
279 if runPriv([findProg('umount'), mountPoint
]) != 0:
282 # Destroy the loopback device.
285 umountCom
.func
= umountComFunc
288 # A command to create an empty file to hold the image.
289 newCom
= Command('new', 'File creation part of "init".',
290 [('file', 'Name of the image file.'),
291 ('mb', 'Size of the file in MB.')])
293 def newImage(file, mb
):
294 (cylinders
, heads
, sectors
) = chsFromSize((mb
* MB
) / BlockSize
)
295 size
= cylinders
* heads
* sectors
* BlockSize
297 # We lseek to the end of the file and only write one byte there. This
298 # leaves a "hole" which many file systems are smart enough not to actually
299 # store to disk and which is defined to read as zero.
300 fd
= os
.open(file, os
.O_WRONLY | os
.O_CREAT
)
301 os
.lseek(fd
, size
- 1, os
.SEEK_SET
)
304 def newComFunc(options
, args
):
310 newCom
.func
= newComFunc
312 # A command to partition the image file like a raw disk device.
313 partitionCom
= Command('partition', 'Partition part of "init".',
314 [('file', 'Name of the image file.')])
316 def partition(dev
, cylinders
, heads
, sectors
):
317 # Use sfdisk to partition the device
318 # The specified options are intended to work with both new and old
319 # versions of sfdisk (see https://askubuntu.com/a/819614)
321 return runPriv([findProg('sfdisk'), '--no-reread', '-u', 'S', '-L', \
322 str(dev
)], inputVal
=comStr
)
324 def partitionComFunc(options
, args
):
327 dev
= LoopbackDevice()
328 if dev
.setup(path
) != 0:
331 # Figure out the dimensions of the file.
332 size
= os
.path
.getsize(path
)
333 if partition(dev
, *chsFromSize(size
/ BlockSize
)) != 0:
339 partitionCom
.func
= partitionComFunc
341 # A command to format the first partition in the image.
342 formatCom
= Command('format', 'Formatting part of "init".',
343 [('file', 'Name of the image file.')])
344 formatCom
.addOption('-t', '--type', dest
='fstype', action
='store',
346 help='Type of file system to use. Appended to mkfs.')
348 def formatImage(dev
, fsType
):
349 return runPriv([findProg('mkfs.%s' % fsType
, dev
), str(dev
)])
351 def formatComFunc(options
, args
):
354 dev
= LoopbackDevice()
355 if dev
.setup(path
, offset
=True) != 0:
359 if formatImage(dev
, options
.fstype
) != 0:
365 formatCom
.func
= formatComFunc
367 def initComFunc(options
, args
):
371 dev
= LoopbackDevice()
372 if dev
.setup(path
) != 0:
374 size
= os
.path
.getsize(path
)
375 if partition(dev
, *chsFromSize((mb
* MB
) / BlockSize
)) != 0:
379 if dev
.setup(path
, offset
=True) != 0:
381 if formatImage(dev
, options
.fstype
) != 0:
386 initCom
.func
= initComFunc
389 # Figure out what command was requested and execute it.
390 if len(argv
) < 2 or argv
[1] not in commands
:
391 print('Usage: %s [command] <command arguments>')
392 print('where [command] is one of ')
393 for name
in commandOrder
:
394 command
= commands
[name
]
395 print(' %s: %s' % (command
.name
, command
.description
))
396 print('Watch for orphaned loopback devices and delete them with')
397 print('losetup -d. Mounted images will belong to root, so you may need')
398 print('to use sudo to modify their contents.')
401 command
= commands
[argv
[1]]
402 command
.parseArgs(argv
)