ia64/xen-unstable

view tools/pygrub/src/pygrub @ 14808:ed78f08aad61

pygrub: Support GPT (GUID Partition Table) as used by EFI.
Signed-off-by: Shinya Kuwamura <kuwa@jp.fujitsu.com>
Signed-off-by: Tomohiro Takahashi <takatom@jp.fujitsu.com>
author kfraser@localhost.localdomain
date Wed Apr 11 15:27:14 2007 +0100 (2007-04-11)
parents ff6a1607c17b
children 6cd828db1a5d
line source
1 #!/usr/bin/python
2 #
3 # pygrub - simple python-based bootloader for Xen
4 #
5 # Copyright 2005-2006 Red Hat, Inc.
6 # Jeremy Katz <katzj@redhat.com>
7 #
8 # This software may be freely redistributed under the terms of the GNU
9 # general public license.
10 #
11 # You should have received a copy of the GNU General Public License
12 # along with this program; if not, write to the Free Software
13 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
14 #
16 import os, sys, string, struct, tempfile, re
17 import copy
18 import logging
20 import curses, _curses, curses.wrapper, curses.textpad, curses.ascii
21 import getopt
23 sys.path = [ '/usr/lib/python' ] + sys.path
25 import fsimage
26 import grub.GrubConf
28 PYGRUB_VER = 0.5
30 def enable_cursor(ison):
31 if ison:
32 val = 2
33 else:
34 val = 0
36 try:
37 curses.curs_set(val)
38 except _curses.error:
39 pass
41 def is_disk_image(file):
42 fd = os.open(file, os.O_RDONLY)
43 buf = os.read(fd, 512)
44 os.close(fd)
46 if len(buf) >= 512 and \
47 struct.unpack("H", buf[0x1fe: 0x200]) == (0xaa55,):
48 return True
49 return False
51 def get_active_partition(file):
52 """Find the offset for the start of the first active partition "
53 "in the disk image file."""
55 fd = os.open(file, os.O_RDONLY)
56 buf = os.read(fd, 512)
57 for poff in (446, 462, 478, 494): # partition offsets
58 # active partition has 0x80 as the first byte
59 if struct.unpack("<c", buf[poff:poff+1]) == ('\x80',):
60 return buf[poff:poff+16]
62 # type=0xee: GUID partition table
63 # XXX assume the first partition is active
64 if struct.unpack("<c", buf[poff+4:poff+5]) == ('\xee',):
65 os.lseek(fd, 0x400, 0)
66 buf = os.read(fd, 512)
67 return buf[24:40] # XXX buf[32:40]
69 # if there's not a partition marked as active, fall back to
70 # the first partition
71 return buf[446:446+16]
73 SECTOR_SIZE=512
74 DK_LABEL_LOC=1
75 DKL_MAGIC=0xdabe
76 V_ROOT=0x2
78 def get_solaris_slice(file, offset):
79 """Find the root slice in a Solaris VTOC."""
81 fd = os.open(file, os.O_RDONLY)
82 os.lseek(fd, offset + (DK_LABEL_LOC * SECTOR_SIZE), 0)
83 buf = os.read(fd, 512)
84 if struct.unpack("<H", buf[508:510])[0] != DKL_MAGIC:
85 raise RuntimeError, "Invalid disklabel magic"
87 nslices = struct.unpack("<H", buf[30:32])[0]
89 for i in range(nslices):
90 sliceoff = 72 + 12 * i
91 slicetag = struct.unpack("<H", buf[sliceoff:sliceoff+2])[0]
92 slicesect = struct.unpack("<L", buf[sliceoff+4:sliceoff+8])[0]
93 if slicetag == V_ROOT:
94 return slicesect * SECTOR_SIZE
96 raise RuntimeError, "No root slice found"
98 FDISK_PART_SOLARIS=0xbf
99 FDISK_PART_SOLARIS_OLD=0x82
101 def get_fs_offset(file):
102 if not is_disk_image(file):
103 return 0
105 partbuf = get_active_partition(file)
106 if len(partbuf) == 0:
107 raise RuntimeError, "Unable to find active partition on disk"
109 offset = struct.unpack("<L", partbuf[8:12])[0] * SECTOR_SIZE
111 type = struct.unpack("<B", partbuf[4:5])[0]
113 if type == FDISK_PART_SOLARIS or type == FDISK_PART_SOLARIS_OLD:
114 offset += get_solaris_slice(file, offset)
116 return offset
118 class GrubLineEditor(curses.textpad.Textbox):
119 def __init__(self, screen, startx, starty, line = ""):
120 screen.addstr(startx, starty, "> ")
121 screen.refresh()
122 win = curses.newwin(1, 74, startx, starty + 2)
123 curses.textpad.Textbox.__init__(self, win)
125 self.line = list(line)
126 self.pos = len(line)
127 self.cancelled = False
128 self.show_text()
130 def show_text(self):
131 """Show the text. One of our advantages over standard textboxes
132 is that we can handle lines longer than the window."""
134 self.win.clear()
135 p = self.pos
136 off = 0
137 while p > 70:
138 p -= 55
139 off += 55
141 l = self.line[off:off+70]
142 self.win.addstr(0, 0, string.join(l, ("")))
143 if self.pos > 70:
144 self.win.addch(0, 0, curses.ACS_LARROW)
146 self.win.move(0, p)
148 def do_command(self, ch):
149 # we handle escape as well as moving the line around, so have
150 # to override some of the default handling
152 self.lastcmd = ch
153 if ch == 27: # esc
154 self.cancelled = True
155 return 0
156 elif curses.ascii.isprint(ch):
157 self.line.insert(self.pos, chr(ch))
158 self.pos += 1
159 elif ch == curses.ascii.SOH: # ^a
160 self.pos = 0
161 elif ch in (curses.ascii.STX,curses.KEY_LEFT):
162 if self.pos > 0:
163 self.pos -= 1
164 elif ch in (curses.ascii.BS,curses.KEY_BACKSPACE):
165 if self.pos > 0:
166 self.pos -= 1
167 if self.pos < len(self.line):
168 self.line.pop(self.pos)
169 elif ch == curses.ascii.EOT: # ^d
170 if self.pos < len(self.line):
171 self.line.pop(self.pos)
172 elif ch == curses.ascii.ENQ: # ^e
173 self.pos = len(self.line)
174 elif ch in (curses.ascii.ACK, curses.KEY_RIGHT):
175 if self.pos < len(self.line):
176 self.pos +=1
177 elif ch == curses.ascii.VT: # ^k
178 self.line = self.line[:self.pos]
179 else:
180 return curses.textpad.Textbox.do_command(self, ch)
181 self.show_text()
182 return 1
184 def edit(self):
185 r = curses.textpad.Textbox.edit(self)
186 if self.cancelled:
187 return None
188 return string.join(self.line, "")
191 class Grub:
192 def __init__(self, file, fs = None):
193 self.screen = None
194 self.entry_win = None
195 self.text_win = None
196 if file:
197 self.read_config(file, fs)
199 def draw_main_windows(self):
200 if self.screen is None: #only init stuff once
201 self.screen = curses.initscr()
202 self.screen.timeout(1000)
203 if hasattr(curses, 'use_default_colors'):
204 try:
205 curses.use_default_colors()
206 except:
207 pass # Not important if we can't use colour
208 enable_cursor(False)
209 self.entry_win = curses.newwin(10, 74, 2, 1)
210 self.text_win = curses.newwin(10, 70, 12, 5)
211 curses.def_prog_mode()
213 curses.reset_prog_mode()
214 self.screen.clear()
215 self.screen.refresh()
217 # create basic grub screen with a box of entries and a textbox
218 self.screen.addstr(1, 4, "pyGRUB version %s" %(PYGRUB_VER,))
219 self.entry_win.box()
220 self.screen.refresh()
222 def fill_entry_list(self):
223 self.entry_win.clear()
224 self.entry_win.box()
225 for y in range(0, len(self.cf.images)):
226 i = self.cf.images[y]
227 if (0, y) > self.entry_win.getmaxyx():
228 break
229 if y == self.selected_image:
230 attr = curses.A_REVERSE
231 else:
232 attr = 0
233 self.entry_win.addstr(y + 1, 2, i.title.ljust(70), attr)
234 self.entry_win.refresh()
236 def edit_entry(self, origimg):
237 def draw():
238 self.draw_main_windows()
240 self.text_win.addstr(0, 0, "Use the U and D keys to select which entry is highlighted.")
241 self.text_win.addstr(1, 0, "Press 'b' to boot, 'e' to edit the selected command in the")
242 self.text_win.addstr(2, 0, "boot sequence, 'c' for a command-line, 'o' to open a new line")
243 self.text_win.addstr(3, 0, "after ('O' for before) the selected line, 'd' to remove the")
244 self.text_win.addstr(4, 0, "selected line, or escape to go back to the main menu.")
245 self.text_win.addch(0, 8, curses.ACS_UARROW)
246 self.text_win.addch(0, 14, curses.ACS_DARROW)
247 (y, x) = self.text_win.getmaxyx()
248 self.text_win.move(y - 1, x - 1)
249 self.text_win.refresh()
251 curline = 1
252 img = copy.deepcopy(origimg)
253 while 1:
254 draw()
255 self.entry_win.clear()
256 self.entry_win.box()
257 for idx in range(1, len(img.lines)):
258 # current line should be highlighted
259 attr = 0
260 if idx == curline:
261 attr = curses.A_REVERSE
263 # trim the line
264 l = img.lines[idx].ljust(70)
265 if len(l) > 70:
266 l = l[:69] + ">"
268 self.entry_win.addstr(idx, 2, l, attr)
269 self.entry_win.refresh()
271 c = self.screen.getch()
272 if c in (ord('q'), 27): # 27 == esc
273 break
274 elif c == curses.KEY_UP:
275 curline -= 1
276 elif c == curses.KEY_DOWN:
277 curline += 1
278 elif c == ord('b'):
279 self.isdone = True
280 break
281 elif c == ord('e'):
282 l = self.edit_line(img.lines[curline])
283 if l is not None:
284 img.set_from_line(l, replace = curline)
285 elif c == ord('d'):
286 img.lines.pop(curline)
287 elif c == ord('o'):
288 img.lines.insert(curline+1, "")
289 curline += 1
290 elif c == ord('O'):
291 img.lines.insert(curline, "")
292 elif c == ord('c'):
293 self.command_line_mode()
294 if self.isdone:
295 return
297 # bound at the top and bottom
298 if curline < 1:
299 curline = 1
300 elif curline >= len(img.lines):
301 curline = len(img.lines) - 1
303 if self.isdone:
304 origimg.reset(img.lines)
306 def edit_line(self, line):
307 self.screen.clear()
308 self.screen.addstr(1, 2, "[ Minimal BASH-like line editing is supported. ")
309 self.screen.addstr(2, 2, " ESC at any time cancels. ENTER at any time accepts your changes. ]")
310 self.screen.refresh()
312 t = GrubLineEditor(self.screen, 5, 2, line)
313 enable_cursor(True)
314 ret = t.edit()
315 if ret:
316 return ret
317 return None
319 def command_line_mode(self):
320 self.screen.clear()
321 self.screen.addstr(1, 2, "[ Minimal BASH-like line editing is supported. ESC at any time ")
322 self.screen.addstr(2, 2, " exits. Typing 'boot' will boot with your entered commands. ] ")
323 self.screen.refresh()
325 y = 5
326 lines = []
327 while 1:
328 t = GrubLineEditor(self.screen, y, 2)
329 enable_cursor(True)
330 ret = t.edit()
331 if ret:
332 if ret in ("quit", "return"):
333 break
334 elif ret != "boot":
335 y += 1
336 lines.append(ret)
337 continue
339 # if we got boot, then we want to boot the entered image
340 img = grub.GrubConf.GrubImage(lines)
341 self.cf.add_image(img)
342 self.selected_image = len(self.cf.images) - 1
343 self.isdone = True
344 break
346 # else, we cancelled and should just go back
347 break
349 def read_config(self, fn, fs = None):
350 """Read the given file to parse the config. If fs = None, then
351 we're being given a raw config file rather than a disk image."""
353 if not os.access(fn, os.R_OK):
354 raise RuntimeError, "Unable to access %s" %(fn,)
356 self.cf = grub.GrubConf.GrubConfigFile()
358 if not fs:
359 # set the config file and parse it
360 self.cf.filename = fn
361 self.cf.parse()
362 return
364 grubfile = None
365 for f in ("/boot/grub/menu.lst", "/boot/grub/grub.conf",
366 "/grub/menu.lst", "/grub/grub.conf"):
367 if fs.file_exists(f):
368 grubfile = f
369 break
370 if grubfile is None:
371 raise RuntimeError, "we couldn't find grub config file in the image provided."
372 f = fs.open_file(grubfile)
373 buf = f.read()
374 del f
375 # then parse the grub config
376 self.cf.parse(buf)
378 def run(self):
379 timeout = int(self.cf.timeout)
381 self.selected_image = self.cf.default
382 self.isdone = False
383 while not self.isdone:
384 self.run_main(timeout)
385 timeout = -1
387 return self.selected_image
389 def run_main(self, timeout = -1):
390 def draw():
391 # set up the screen
392 self.draw_main_windows()
393 self.text_win.addstr(0, 0, "Use the U and D keys to select which entry is highlighted.")
394 self.text_win.addstr(1, 0, "Press enter to boot the selected OS. 'e' to edit the")
395 self.text_win.addstr(2, 0, "commands before booting, 'a' to modify the kernel arguments ")
396 self.text_win.addstr(3, 0, "before booting, or 'c' for a command line.")
397 self.text_win.addch(0, 8, curses.ACS_UARROW)
398 self.text_win.addch(0, 14, curses.ACS_DARROW)
399 (y, x) = self.text_win.getmaxyx()
400 self.text_win.move(y - 1, x - 1)
401 self.text_win.refresh()
403 # now loop until we hit the timeout or get a go from the user
404 mytime = 0
405 while (timeout == -1 or mytime < int(timeout)):
406 draw()
407 if timeout != -1 and mytime != -1:
408 self.screen.addstr(20, 5, "Will boot selected entry in %2d seconds"
409 %(int(timeout) - mytime))
410 else:
411 self.screen.addstr(20, 5, " " * 80)
412 self.fill_entry_list()
414 c = self.screen.getch()
415 if c == -1:
416 # Timed out waiting for a keypress
417 if mytime != -1:
418 mytime += 1
419 if mytime >= int(timeout):
420 self.isdone = True
421 break
422 else:
423 # received a keypress: stop the timer
424 mytime = -1
425 self.screen.timeout(-1)
427 # handle keypresses
428 if c == ord('c'):
429 self.command_line_mode()
430 break
431 elif c == ord('a'):
432 # find the kernel line, edit it and then boot
433 img = self.cf.images[self.selected_image]
434 for line in img.lines:
435 if line.startswith("kernel"):
436 l = self.edit_line(line)
437 if l is not None:
438 img.set_from_line(l, replace = True)
439 self.isdone = True
440 break
441 break
442 elif c == ord('e'):
443 img = self.cf.images[self.selected_image]
444 self.edit_entry(img)
445 break
446 elif c in (curses.KEY_ENTER, ord('\n'), ord('\r')):
447 self.isdone = True
448 break
449 elif c == curses.KEY_UP:
450 self.selected_image -= 1
451 elif c == curses.KEY_DOWN:
452 self.selected_image += 1
453 # elif c in (ord('q'), 27): # 27 == esc
454 # self.selected_image = -1
455 # self.isdone = True
456 # break
458 # bound at the top and bottom
459 if self.selected_image < 0:
460 self.selected_image = 0
461 elif self.selected_image >= len(self.cf.images):
462 self.selected_image = len(self.cf.images) - 1
464 def get_entry_idx(cf, entry):
465 # first, see if the given entry is numeric
466 try:
467 idx = string.atoi(entry)
468 return idx
469 except ValueError:
470 pass
472 # it's not, now check the labels for a match
473 for i in range(len(cf.images)):
474 if entry == cf.images[i].title:
475 return i
477 return None
479 def run_grub(file, entry, fs):
480 global g
481 global sel
483 def run_main(scr, *args):
484 global sel
485 global g
486 sel = g.run()
488 g = Grub(file, fs)
489 if interactive:
490 curses.wrapper(run_main)
491 else:
492 sel = g.cf.default
494 # set the entry to boot as requested
495 if entry is not None:
496 idx = get_entry_idx(g.cf, entry)
497 if idx is not None and idx > 0 and idx < len(g.cf.images):
498 sel = idx
500 if sel == -1:
501 print "No kernel image selected!"
502 sys.exit(1)
504 img = g.cf.images[sel]
506 grubcfg = { "kernel": None, "ramdisk": None, "args": None }
508 grubcfg["kernel"] = img.kernel[1]
509 if img.initrd:
510 grubcfg["ramdisk"] = img.initrd[1]
511 if img.args:
512 grubcfg["args"] = img.args
514 return grubcfg
516 # If nothing has been specified, look for a Solaris domU. If found, perform the
517 # necessary tweaks.
518 def sniff_solaris(fs, cfg):
519 if not fs.file_exists("/platform/i86xpv/kernel/unix"):
520 return cfg
522 # darned python
523 longmode = (sys.maxint != 2147483647L)
524 if not longmode:
525 longmode = os.uname()[4] == "x86_64"
526 if not longmode:
527 if (os.access("/usr/bin/isainfo", os.R_OK) and
528 os.popen("/usr/bin/isainfo -b").read() == "64\n"):
529 longmode = True
531 if not cfg["kernel"]:
532 cfg["kernel"] = "/platform/i86xpv/kernel/unix"
533 cfg["ramdisk"] = "/platform/i86pc/boot_archive"
534 if longmode:
535 cfg["kernel"] = "/platform/i86xpv/kernel/amd64/unix"
536 cfg["ramdisk"] = "/platform/i86pc/amd64/boot_archive"
538 # Unpleasant. Typically we'll have 'root=foo -k' or 'root=foo /kernel -k',
539 # and we need to maintain Xen properties (root= and ip=) and the kernel
540 # before any user args.
542 xenargs = ""
543 userargs = ""
545 if not cfg["args"]:
546 cfg["args"] = cfg["kernel"]
547 else:
548 for arg in cfg["args"].split():
549 if re.match("^root=", arg) or re.match("^ip=", arg):
550 xenargs += arg + " "
551 elif arg != cfg["kernel"]:
552 userargs += arg + " "
553 cfg["args"] = xenargs + " " + cfg["kernel"] + " " + userargs
555 return cfg
557 if __name__ == "__main__":
558 sel = None
560 def usage():
561 print >> sys.stderr, "Usage: %s [-q|--quiet] [-i|--interactive] [--output=] [--kernel=] [--ramdisk=] [--args=] [--entry=] <image>" %(sys.argv[0],)
563 try:
564 opts, args = getopt.gnu_getopt(sys.argv[1:], 'qih::',
565 ["quiet", "interactive", "help", "output=",
566 "entry=", "kernel=", "ramdisk=", "args=",
567 "isconfig"])
568 except getopt.GetoptError:
569 usage()
570 sys.exit(1)
572 if len(args) < 1:
573 usage()
574 sys.exit(1)
575 file = args[0]
577 output = None
578 entry = None
579 interactive = True
580 isconfig = False
582 # what was passed in
583 incfg = { "kernel": None, "ramdisk": None, "args": None }
584 # what grub or sniffing chose
585 chosencfg = { "kernel": None, "ramdisk": None, "args": None }
586 # what to boot
587 bootcfg = { "kernel": None, "ramdisk": None, "args": None }
589 for o, a in opts:
590 if o in ("-q", "--quiet"):
591 interactive = False
592 elif o in ("-i", "--interactive"):
593 interactive = True
594 elif o in ("-h", "--help"):
595 usage()
596 sys.exit()
597 elif o in ("--output",):
598 output = a
599 elif o in ("--kernel",):
600 incfg["kernel"] = a
601 elif o in ("--ramdisk",):
602 incfg["ramdisk"] = a
603 elif o in ("--args",):
604 incfg["args"] = a
605 elif o in ("--entry",):
606 entry = a
607 # specifying the entry to boot implies non-interactive
608 interactive = False
609 elif o in ("--isconfig",):
610 isconfig = True
612 if output is None or output == "-":
613 fd = sys.stdout.fileno()
614 else:
615 fd = os.open(output, os.O_WRONLY)
617 # debug
618 if isconfig:
619 chosencfg = run_grub(file, entry)
620 print " kernel: %s" % chosencfg["kernel"]
621 if img.initrd:
622 print " initrd: %s" % chosencfg["ramdisk"]
623 print " args: %s" % chosencfg["args"]
624 sys.exit(0)
626 fs = fsimage.open(file, get_fs_offset(file))
628 chosencfg = sniff_solaris(fs, incfg)
630 if not chosencfg["kernel"]:
631 chosencfg = run_grub(file, entry, fs)
633 data = fs.open_file(chosencfg["kernel"]).read()
634 (tfd, bootcfg["kernel"]) = tempfile.mkstemp(prefix="boot_kernel.",
635 dir="/var/run/xend/boot")
636 os.write(tfd, data)
637 os.close(tfd)
639 if chosencfg["ramdisk"]:
640 data = fs.open_file(chosencfg["ramdisk"],).read()
641 (tfd, bootcfg["ramdisk"]) = tempfile.mkstemp(prefix="boot_ramdisk.",
642 dir="/var/run/xend/boot")
643 os.write(tfd, data)
644 os.close(tfd)
645 else:
646 initrd = None
648 sxp = "linux (kernel %s)" % bootcfg["kernel"]
649 if bootcfg["ramdisk"]:
650 sxp += "(ramdisk %s)" % bootcfg["ramdisk"]
651 if chosencfg["args"]:
652 sxp += "(args \"%s\")" % chosencfg["args"]
654 sys.stdout.flush()
655 os.write(fd, sxp)