ia64/xen-unstable

view tools/pygrub/src/pygrub @ 15006:24f47ff4fb25

[IA64] Presently pygrub only looks in /efi/redhat/elilo.conf. It
should check for other distributions, plus a couple fallback
locations.

Signed-off-by: Aron Griffis <aron@hp.com>
author kfraser@localhost.localdomain
date Thu May 03 09:33:12 2007 +0100 (2007-05-03)
parents 986b102f84c2
children 50bd5d2c15cf
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
19 import platform
21 import curses, _curses, curses.wrapper, curses.textpad, curses.ascii
22 import getopt
24 sys.path = [ '/usr/lib/python' ] + sys.path
26 import fsimage
27 import grub.GrubConf
28 import grub.LiloConf
30 PYGRUB_VER = 0.5
32 def enable_cursor(ison):
33 if ison:
34 val = 2
35 else:
36 val = 0
38 try:
39 curses.curs_set(val)
40 except _curses.error:
41 pass
43 def is_disk_image(file):
44 fd = os.open(file, os.O_RDONLY)
45 buf = os.read(fd, 512)
46 os.close(fd)
48 if len(buf) >= 512 and \
49 struct.unpack("H", buf[0x1fe: 0x200]) == (0xaa55,):
50 return True
51 return False
53 def get_active_partition(file):
54 """Find the offset for the start of the first active partition "
55 "in the disk image file."""
57 fd = os.open(file, os.O_RDONLY)
58 buf = os.read(fd, 512)
59 for poff in (446, 462, 478, 494): # partition offsets
60 # active partition has 0x80 as the first byte
61 if struct.unpack("<c", buf[poff:poff+1]) == ('\x80',):
62 return buf[poff:poff+16]
64 # if there's not a partition marked as active, fall back to
65 # the first partition
66 return buf[446:446+16]
68 SECTOR_SIZE=512
69 DK_LABEL_LOC=1
70 DKL_MAGIC=0xdabe
71 V_ROOT=0x2
73 def get_solaris_slice(file, offset):
74 """Find the root slice in a Solaris VTOC."""
76 fd = os.open(file, os.O_RDONLY)
77 os.lseek(fd, offset + (DK_LABEL_LOC * SECTOR_SIZE), 0)
78 buf = os.read(fd, 512)
79 if struct.unpack("<H", buf[508:510])[0] != DKL_MAGIC:
80 raise RuntimeError, "Invalid disklabel magic"
82 nslices = struct.unpack("<H", buf[30:32])[0]
84 for i in range(nslices):
85 sliceoff = 72 + 12 * i
86 slicetag = struct.unpack("<H", buf[sliceoff:sliceoff+2])[0]
87 slicesect = struct.unpack("<L", buf[sliceoff+4:sliceoff+8])[0]
88 if slicetag == V_ROOT:
89 return slicesect * SECTOR_SIZE
91 raise RuntimeError, "No root slice found"
93 def get_fs_offset_gpt(file):
94 fd = os.open(file, os.O_RDONLY)
95 # assume the first partition is an EFI system partition.
96 os.lseek(fd, SECTOR_SIZE * 2, 0)
97 buf = os.read(fd, 512)
98 return struct.unpack("<Q", buf[32:40])[0] * SECTOR_SIZE
100 FDISK_PART_SOLARIS=0xbf
101 FDISK_PART_SOLARIS_OLD=0x82
102 FDISK_PART_GPT=0xee
104 def get_fs_offset(file):
105 if not is_disk_image(file):
106 return 0
108 partbuf = get_active_partition(file)
109 if len(partbuf) == 0:
110 raise RuntimeError, "Unable to find active partition on disk"
112 offset = struct.unpack("<L", partbuf[8:12])[0] * SECTOR_SIZE
114 type = struct.unpack("<B", partbuf[4:5])[0]
116 if type == FDISK_PART_SOLARIS or type == FDISK_PART_SOLARIS_OLD:
117 offset += get_solaris_slice(file, offset)
119 if type == FDISK_PART_GPT:
120 offset = get_fs_offset_gpt(file)
122 return offset
124 class GrubLineEditor(curses.textpad.Textbox):
125 def __init__(self, screen, startx, starty, line = ""):
126 screen.addstr(startx, starty, "> ")
127 screen.refresh()
128 win = curses.newwin(1, 74, startx, starty + 2)
129 curses.textpad.Textbox.__init__(self, win)
131 self.line = list(line)
132 self.pos = len(line)
133 self.cancelled = False
134 self.show_text()
136 def show_text(self):
137 """Show the text. One of our advantages over standard textboxes
138 is that we can handle lines longer than the window."""
140 self.win.clear()
141 p = self.pos
142 off = 0
143 while p > 70:
144 p -= 55
145 off += 55
147 l = self.line[off:off+70]
148 self.win.addstr(0, 0, string.join(l, ("")))
149 if self.pos > 70:
150 self.win.addch(0, 0, curses.ACS_LARROW)
152 self.win.move(0, p)
154 def do_command(self, ch):
155 # we handle escape as well as moving the line around, so have
156 # to override some of the default handling
158 self.lastcmd = ch
159 if ch == 27: # esc
160 self.cancelled = True
161 return 0
162 elif curses.ascii.isprint(ch):
163 self.line.insert(self.pos, chr(ch))
164 self.pos += 1
165 elif ch == curses.ascii.SOH: # ^a
166 self.pos = 0
167 elif ch in (curses.ascii.STX,curses.KEY_LEFT):
168 if self.pos > 0:
169 self.pos -= 1
170 elif ch in (curses.ascii.BS,curses.KEY_BACKSPACE):
171 if self.pos > 0:
172 self.pos -= 1
173 if self.pos < len(self.line):
174 self.line.pop(self.pos)
175 elif ch == curses.ascii.EOT: # ^d
176 if self.pos < len(self.line):
177 self.line.pop(self.pos)
178 elif ch == curses.ascii.ENQ: # ^e
179 self.pos = len(self.line)
180 elif ch in (curses.ascii.ACK, curses.KEY_RIGHT):
181 if self.pos < len(self.line):
182 self.pos +=1
183 elif ch == curses.ascii.VT: # ^k
184 self.line = self.line[:self.pos]
185 else:
186 return curses.textpad.Textbox.do_command(self, ch)
187 self.show_text()
188 return 1
190 def edit(self):
191 r = curses.textpad.Textbox.edit(self)
192 if self.cancelled:
193 return None
194 return string.join(self.line, "")
197 class Grub:
198 def __init__(self, file, fs = None):
199 self.screen = None
200 self.entry_win = None
201 self.text_win = None
202 if file:
203 self.read_config(file, fs)
205 def draw_main_windows(self):
206 if self.screen is None: #only init stuff once
207 self.screen = curses.initscr()
208 self.screen.timeout(1000)
209 if hasattr(curses, 'use_default_colors'):
210 try:
211 curses.use_default_colors()
212 except:
213 pass # Not important if we can't use colour
214 enable_cursor(False)
215 self.entry_win = curses.newwin(10, 74, 2, 1)
216 self.text_win = curses.newwin(10, 70, 12, 5)
217 curses.def_prog_mode()
219 curses.reset_prog_mode()
220 self.screen.clear()
221 self.screen.refresh()
223 # create basic grub screen with a box of entries and a textbox
224 self.screen.addstr(1, 4, "pyGRUB version %s" %(PYGRUB_VER,))
225 self.entry_win.box()
226 self.screen.refresh()
228 def fill_entry_list(self):
229 self.entry_win.clear()
230 self.entry_win.box()
231 for y in range(0, len(self.cf.images)):
232 i = self.cf.images[y]
233 if (0, y) > self.entry_win.getmaxyx():
234 break
235 if y == self.selected_image:
236 attr = curses.A_REVERSE
237 else:
238 attr = 0
239 self.entry_win.addstr(y + 1, 2, i.title.ljust(70), attr)
240 self.entry_win.refresh()
242 def edit_entry(self, origimg):
243 def draw():
244 self.draw_main_windows()
246 self.text_win.addstr(0, 0, "Use the U and D keys to select which entry is highlighted.")
247 self.text_win.addstr(1, 0, "Press 'b' to boot, 'e' to edit the selected command in the")
248 self.text_win.addstr(2, 0, "boot sequence, 'c' for a command-line, 'o' to open a new line")
249 self.text_win.addstr(3, 0, "after ('O' for before) the selected line, 'd' to remove the")
250 self.text_win.addstr(4, 0, "selected line, or escape to go back to the main menu.")
251 self.text_win.addch(0, 8, curses.ACS_UARROW)
252 self.text_win.addch(0, 14, curses.ACS_DARROW)
253 (y, x) = self.text_win.getmaxyx()
254 self.text_win.move(y - 1, x - 1)
255 self.text_win.refresh()
257 curline = 1
258 img = copy.deepcopy(origimg)
259 while 1:
260 draw()
261 self.entry_win.clear()
262 self.entry_win.box()
263 for idx in range(1, len(img.lines)):
264 # current line should be highlighted
265 attr = 0
266 if idx == curline:
267 attr = curses.A_REVERSE
269 # trim the line
270 l = img.lines[idx].ljust(70)
271 if len(l) > 70:
272 l = l[:69] + ">"
274 self.entry_win.addstr(idx, 2, l, attr)
275 self.entry_win.refresh()
277 c = self.screen.getch()
278 if c in (ord('q'), 27): # 27 == esc
279 break
280 elif c == curses.KEY_UP:
281 curline -= 1
282 elif c == curses.KEY_DOWN:
283 curline += 1
284 elif c == ord('b'):
285 self.isdone = True
286 break
287 elif c == ord('e'):
288 l = self.edit_line(img.lines[curline])
289 if l is not None:
290 img.set_from_line(l, replace = curline)
291 elif c == ord('d'):
292 img.lines.pop(curline)
293 elif c == ord('o'):
294 img.lines.insert(curline+1, "")
295 curline += 1
296 elif c == ord('O'):
297 img.lines.insert(curline, "")
298 elif c == ord('c'):
299 self.command_line_mode()
300 if self.isdone:
301 return
303 # bound at the top and bottom
304 if curline < 1:
305 curline = 1
306 elif curline >= len(img.lines):
307 curline = len(img.lines) - 1
309 if self.isdone:
310 origimg.reset(img.lines)
312 def edit_line(self, line):
313 self.screen.clear()
314 self.screen.addstr(1, 2, "[ Minimal BASH-like line editing is supported. ")
315 self.screen.addstr(2, 2, " ESC at any time cancels. ENTER at any time accepts your changes. ]")
316 self.screen.refresh()
318 t = GrubLineEditor(self.screen, 5, 2, line)
319 enable_cursor(True)
320 ret = t.edit()
321 if ret:
322 return ret
323 return None
325 def command_line_mode(self):
326 self.screen.clear()
327 self.screen.addstr(1, 2, "[ Minimal BASH-like line editing is supported. ESC at any time ")
328 self.screen.addstr(2, 2, " exits. Typing 'boot' will boot with your entered commands. ] ")
329 self.screen.refresh()
331 y = 5
332 lines = []
333 while 1:
334 t = GrubLineEditor(self.screen, y, 2)
335 enable_cursor(True)
336 ret = t.edit()
337 if ret:
338 if ret in ("quit", "return"):
339 break
340 elif ret != "boot":
341 y += 1
342 lines.append(ret)
343 continue
345 # if we got boot, then we want to boot the entered image
346 img = grub.GrubConf.GrubImage(lines)
347 self.cf.add_image(img)
348 self.selected_image = len(self.cf.images) - 1
349 self.isdone = True
350 break
352 # else, we cancelled and should just go back
353 break
355 def read_config(self, fn, fs = None):
356 """Read the given file to parse the config. If fs = None, then
357 we're being given a raw config file rather than a disk image."""
359 if not os.access(fn, os.R_OK):
360 raise RuntimeError, "Unable to access %s" %(fn,)
362 if platform.machine() == 'ia64':
363 self.cf = grub.LiloConf.LiloConfigFile()
364 # common distributions
365 file_list = ("/efi/debian/elilo.conf", "/efi/gentoo/elilo.conf",
366 "/efi/redflag/elilo.conf", "/efi/redhat/elilo.conf",
367 "/efi/SuSE/elilo.conf",)
368 # fallbacks
369 file_list += ("/efi/boot/elilo.conf", "/elilo.conf",)
370 else:
371 self.cf = grub.GrubConf.GrubConfigFile()
372 file_list = ("/boot/grub/menu.lst", "/boot/grub/grub.conf",
373 "/grub/menu.lst", "/grub/grub.conf")
375 if not fs:
376 # set the config file and parse it
377 self.cf.filename = fn
378 self.cf.parse()
379 return
381 for f in file_list:
382 if fs.file_exists(f):
383 self.cf.filename = f
384 break
385 if self.cf.filename is None:
386 raise RuntimeError, "couldn't find bootloader config file in the image provided."
387 f = fs.open_file(self.cf.filename)
388 buf = f.read()
389 del f
390 self.cf.parse(buf)
392 def run(self):
393 timeout = int(self.cf.timeout)
395 self.selected_image = self.cf.default
396 self.isdone = False
397 while not self.isdone:
398 self.run_main(timeout)
399 timeout = -1
401 return self.selected_image
403 def run_main(self, timeout = -1):
404 def draw():
405 # set up the screen
406 self.draw_main_windows()
407 self.text_win.addstr(0, 0, "Use the U and D keys to select which entry is highlighted.")
408 self.text_win.addstr(1, 0, "Press enter to boot the selected OS. 'e' to edit the")
409 self.text_win.addstr(2, 0, "commands before booting, 'a' to modify the kernel arguments ")
410 self.text_win.addstr(3, 0, "before booting, or 'c' for a command line.")
411 self.text_win.addch(0, 8, curses.ACS_UARROW)
412 self.text_win.addch(0, 14, curses.ACS_DARROW)
413 (y, x) = self.text_win.getmaxyx()
414 self.text_win.move(y - 1, x - 1)
415 self.text_win.refresh()
417 # now loop until we hit the timeout or get a go from the user
418 mytime = 0
419 while (timeout == -1 or mytime < int(timeout)):
420 draw()
421 if timeout != -1 and mytime != -1:
422 self.screen.addstr(20, 5, "Will boot selected entry in %2d seconds"
423 %(int(timeout) - mytime))
424 else:
425 self.screen.addstr(20, 5, " " * 80)
426 self.fill_entry_list()
428 c = self.screen.getch()
429 if c == -1:
430 # Timed out waiting for a keypress
431 if mytime != -1:
432 mytime += 1
433 if mytime >= int(timeout):
434 self.isdone = True
435 break
436 else:
437 # received a keypress: stop the timer
438 mytime = -1
439 self.screen.timeout(-1)
441 # handle keypresses
442 if c == ord('c'):
443 self.command_line_mode()
444 break
445 elif c == ord('a'):
446 # find the kernel line, edit it and then boot
447 img = self.cf.images[self.selected_image]
448 for line in img.lines:
449 if line.startswith("kernel"):
450 l = self.edit_line(line)
451 if l is not None:
452 img.set_from_line(l, replace = True)
453 self.isdone = True
454 break
455 break
456 elif c == ord('e'):
457 img = self.cf.images[self.selected_image]
458 self.edit_entry(img)
459 break
460 elif c in (curses.KEY_ENTER, ord('\n'), ord('\r')):
461 self.isdone = True
462 break
463 elif c == curses.KEY_UP:
464 self.selected_image -= 1
465 elif c == curses.KEY_DOWN:
466 self.selected_image += 1
467 # elif c in (ord('q'), 27): # 27 == esc
468 # self.selected_image = -1
469 # self.isdone = True
470 # break
472 # bound at the top and bottom
473 if self.selected_image < 0:
474 self.selected_image = 0
475 elif self.selected_image >= len(self.cf.images):
476 self.selected_image = len(self.cf.images) - 1
478 def get_entry_idx(cf, entry):
479 # first, see if the given entry is numeric
480 try:
481 idx = string.atoi(entry)
482 return idx
483 except ValueError:
484 pass
486 # it's not, now check the labels for a match
487 for i in range(len(cf.images)):
488 if entry == cf.images[i].title:
489 return i
491 return None
493 def run_grub(file, entry, fs):
494 global g
495 global sel
497 def run_main(scr, *args):
498 global sel
499 global g
500 sel = g.run()
502 g = Grub(file, fs)
503 if interactive:
504 curses.wrapper(run_main)
505 else:
506 sel = g.cf.default
508 # set the entry to boot as requested
509 if entry is not None:
510 idx = get_entry_idx(g.cf, entry)
511 if idx is not None and idx > 0 and idx < len(g.cf.images):
512 sel = idx
514 if sel == -1:
515 print "No kernel image selected!"
516 sys.exit(1)
518 img = g.cf.images[sel]
520 grubcfg = { "kernel": None, "ramdisk": None, "args": None }
522 grubcfg["kernel"] = img.kernel[1]
523 if img.initrd:
524 grubcfg["ramdisk"] = img.initrd[1]
525 if img.args:
526 grubcfg["args"] = img.args
528 return grubcfg
530 # If nothing has been specified, look for a Solaris domU. If found, perform the
531 # necessary tweaks.
532 def sniff_solaris(fs, cfg):
533 if not fs.file_exists("/platform/i86xpv/kernel/unix"):
534 return cfg
536 # darned python
537 longmode = (sys.maxint != 2147483647L)
538 if not longmode:
539 longmode = os.uname()[4] == "x86_64"
540 if not longmode:
541 if (os.access("/usr/bin/isainfo", os.R_OK) and
542 os.popen("/usr/bin/isainfo -b").read() == "64\n"):
543 longmode = True
545 if not cfg["kernel"]:
546 cfg["kernel"] = "/platform/i86xpv/kernel/unix"
547 cfg["ramdisk"] = "/platform/i86pc/boot_archive"
548 if longmode:
549 cfg["kernel"] = "/platform/i86xpv/kernel/amd64/unix"
550 cfg["ramdisk"] = "/platform/i86pc/amd64/boot_archive"
552 # Unpleasant. Typically we'll have 'root=foo -k' or 'root=foo /kernel -k',
553 # and we need to maintain Xen properties (root= and ip=) and the kernel
554 # before any user args.
556 xenargs = ""
557 userargs = ""
559 if not cfg["args"]:
560 cfg["args"] = cfg["kernel"]
561 else:
562 for arg in cfg["args"].split():
563 if re.match("^root=", arg) or re.match("^ip=", arg):
564 xenargs += arg + " "
565 elif arg != cfg["kernel"]:
566 userargs += arg + " "
567 cfg["args"] = xenargs + " " + cfg["kernel"] + " " + userargs
569 return cfg
571 if __name__ == "__main__":
572 sel = None
574 def usage():
575 print >> sys.stderr, "Usage: %s [-q|--quiet] [-i|--interactive] [--output=] [--kernel=] [--ramdisk=] [--args=] [--entry=] <image>" %(sys.argv[0],)
577 try:
578 opts, args = getopt.gnu_getopt(sys.argv[1:], 'qih::',
579 ["quiet", "interactive", "help", "output=",
580 "entry=", "kernel=", "ramdisk=", "args=",
581 "isconfig"])
582 except getopt.GetoptError:
583 usage()
584 sys.exit(1)
586 if len(args) < 1:
587 usage()
588 sys.exit(1)
589 file = args[0]
591 output = None
592 entry = None
593 interactive = True
594 isconfig = False
596 # what was passed in
597 incfg = { "kernel": None, "ramdisk": None, "args": None }
598 # what grub or sniffing chose
599 chosencfg = { "kernel": None, "ramdisk": None, "args": None }
600 # what to boot
601 bootcfg = { "kernel": None, "ramdisk": None, "args": None }
603 for o, a in opts:
604 if o in ("-q", "--quiet"):
605 interactive = False
606 elif o in ("-i", "--interactive"):
607 interactive = True
608 elif o in ("-h", "--help"):
609 usage()
610 sys.exit()
611 elif o in ("--output",):
612 output = a
613 elif o in ("--kernel",):
614 incfg["kernel"] = a
615 elif o in ("--ramdisk",):
616 incfg["ramdisk"] = a
617 elif o in ("--args",):
618 incfg["args"] = a
619 elif o in ("--entry",):
620 entry = a
621 # specifying the entry to boot implies non-interactive
622 interactive = False
623 elif o in ("--isconfig",):
624 isconfig = True
626 if output is None or output == "-":
627 fd = sys.stdout.fileno()
628 else:
629 fd = os.open(output, os.O_WRONLY)
631 # debug
632 if isconfig:
633 chosencfg = run_grub(file, entry)
634 print " kernel: %s" % chosencfg["kernel"]
635 if img.initrd:
636 print " initrd: %s" % chosencfg["ramdisk"]
637 print " args: %s" % chosencfg["args"]
638 sys.exit(0)
640 fs = fsimage.open(file, get_fs_offset(file))
642 chosencfg = sniff_solaris(fs, incfg)
644 if not chosencfg["kernel"]:
645 chosencfg = run_grub(file, entry, fs)
647 data = fs.open_file(chosencfg["kernel"]).read()
648 (tfd, bootcfg["kernel"]) = tempfile.mkstemp(prefix="boot_kernel.",
649 dir="/var/run/xend/boot")
650 os.write(tfd, data)
651 os.close(tfd)
653 if chosencfg["ramdisk"]:
654 data = fs.open_file(chosencfg["ramdisk"],).read()
655 (tfd, bootcfg["ramdisk"]) = tempfile.mkstemp(prefix="boot_ramdisk.",
656 dir="/var/run/xend/boot")
657 os.write(tfd, data)
658 os.close(tfd)
659 else:
660 initrd = None
662 sxp = "linux (kernel %s)" % bootcfg["kernel"]
663 if bootcfg["ramdisk"]:
664 sxp += "(ramdisk %s)" % bootcfg["ramdisk"]
665 if chosencfg["args"]:
666 sxp += "(args \"%s\")" % chosencfg["args"]
668 sys.stdout.flush()
669 os.write(fd, sxp)