debuggers.hg

view tools/pygrub/src/pygrub @ 16706:50bd5d2c15cf

Fix pygrub handling of many kernels

If there are a large number of kernel images configured in grub.conf
there will be too many to fit in the limited size pygrub display. This
patch fixes this so that the list of kernels scrolls as needed.

Signed-off-by: Daniel P. Berrange <berrange@redhat.com>
author Keir Fraser <keir.fraser@citrix.com>
date Thu Dec 27 12:56:32 2007 +0000 (2007-12-27)
parents 24f47ff4fb25
children 145eff6c96fc
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.6
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()
232 maxy = self.entry_win.getmaxyx()[0]-3 # maxy - 2 for the frame + index
233 if self.selected_image > self.start_image + maxy:
234 self.start_image = self.selected_image
235 if self.selected_image < self.start_image:
236 self.start_image = self.selected_image
238 for y in range(self.start_image, len(self.cf.images)):
239 i = self.cf.images[y]
240 if y > self.start_image + maxy:
241 break
242 if y == self.selected_image:
243 attr = curses.A_REVERSE
244 else:
245 attr = 0
246 self.entry_win.addstr(y + 1 - self.start_image, 2, i.title.ljust(70), attr)
247 self.entry_win.refresh()
249 def edit_entry(self, origimg):
250 def draw():
251 self.draw_main_windows()
253 self.text_win.addstr(0, 0, "Use the U and D keys to select which entry is highlighted.")
254 self.text_win.addstr(1, 0, "Press 'b' to boot, 'e' to edit the selected command in the")
255 self.text_win.addstr(2, 0, "boot sequence, 'c' for a command-line, 'o' to open a new line")
256 self.text_win.addstr(3, 0, "after ('O' for before) the selected line, 'd' to remove the")
257 self.text_win.addstr(4, 0, "selected line, or escape to go back to the main menu.")
258 self.text_win.addch(0, 8, curses.ACS_UARROW)
259 self.text_win.addch(0, 14, curses.ACS_DARROW)
260 (y, x) = self.text_win.getmaxyx()
261 self.text_win.move(y - 1, x - 1)
262 self.text_win.refresh()
264 curline = 1
265 img = copy.deepcopy(origimg)
266 while 1:
267 draw()
268 self.entry_win.clear()
269 self.entry_win.box()
270 for idx in range(1, len(img.lines)):
271 # current line should be highlighted
272 attr = 0
273 if idx == curline:
274 attr = curses.A_REVERSE
276 # trim the line
277 l = img.lines[idx].ljust(70)
278 if len(l) > 70:
279 l = l[:69] + ">"
281 self.entry_win.addstr(idx, 2, l, attr)
282 self.entry_win.refresh()
284 c = self.screen.getch()
285 if c in (ord('q'), 27): # 27 == esc
286 break
287 elif c == curses.KEY_UP:
288 curline -= 1
289 elif c == curses.KEY_DOWN:
290 curline += 1
291 elif c == ord('b'):
292 self.isdone = True
293 break
294 elif c == ord('e'):
295 l = self.edit_line(img.lines[curline])
296 if l is not None:
297 img.set_from_line(l, replace = curline)
298 elif c == ord('d'):
299 img.lines.pop(curline)
300 elif c == ord('o'):
301 img.lines.insert(curline+1, "")
302 curline += 1
303 elif c == ord('O'):
304 img.lines.insert(curline, "")
305 elif c == ord('c'):
306 self.command_line_mode()
307 if self.isdone:
308 return
310 # bound at the top and bottom
311 if curline < 1:
312 curline = 1
313 elif curline >= len(img.lines):
314 curline = len(img.lines) - 1
316 if self.isdone:
317 origimg.reset(img.lines)
319 def edit_line(self, line):
320 self.screen.clear()
321 self.screen.addstr(1, 2, "[ Minimal BASH-like line editing is supported. ")
322 self.screen.addstr(2, 2, " ESC at any time cancels. ENTER at any time accepts your changes. ]")
323 self.screen.refresh()
325 t = GrubLineEditor(self.screen, 5, 2, line)
326 enable_cursor(True)
327 ret = t.edit()
328 if ret:
329 return ret
330 return None
332 def command_line_mode(self):
333 self.screen.clear()
334 self.screen.addstr(1, 2, "[ Minimal BASH-like line editing is supported. ESC at any time ")
335 self.screen.addstr(2, 2, " exits. Typing 'boot' will boot with your entered commands. ] ")
336 self.screen.refresh()
338 y = 5
339 lines = []
340 while 1:
341 t = GrubLineEditor(self.screen, y, 2)
342 enable_cursor(True)
343 ret = t.edit()
344 if ret:
345 if ret in ("quit", "return"):
346 break
347 elif ret != "boot":
348 y += 1
349 lines.append(ret)
350 continue
352 # if we got boot, then we want to boot the entered image
353 img = grub.GrubConf.GrubImage(lines)
354 self.cf.add_image(img)
355 self.selected_image = len(self.cf.images) - 1
356 self.isdone = True
357 break
359 # else, we cancelled and should just go back
360 break
362 def read_config(self, fn, fs = None):
363 """Read the given file to parse the config. If fs = None, then
364 we're being given a raw config file rather than a disk image."""
366 if not os.access(fn, os.R_OK):
367 raise RuntimeError, "Unable to access %s" %(fn,)
369 if platform.machine() == 'ia64':
370 self.cf = grub.LiloConf.LiloConfigFile()
371 # common distributions
372 file_list = ("/efi/debian/elilo.conf", "/efi/gentoo/elilo.conf",
373 "/efi/redflag/elilo.conf", "/efi/redhat/elilo.conf",
374 "/efi/SuSE/elilo.conf",)
375 # fallbacks
376 file_list += ("/efi/boot/elilo.conf", "/elilo.conf",)
377 else:
378 self.cf = grub.GrubConf.GrubConfigFile()
379 file_list = ("/boot/grub/menu.lst", "/boot/grub/grub.conf",
380 "/grub/menu.lst", "/grub/grub.conf")
382 if not fs:
383 # set the config file and parse it
384 self.cf.filename = fn
385 self.cf.parse()
386 return
388 for f in file_list:
389 if fs.file_exists(f):
390 self.cf.filename = f
391 break
392 if self.cf.filename is None:
393 raise RuntimeError, "couldn't find bootloader config file in the image provided."
394 f = fs.open_file(self.cf.filename)
395 buf = f.read()
396 del f
397 self.cf.parse(buf)
399 def run(self):
400 timeout = int(self.cf.timeout)
402 self.selected_image = self.cf.default
403 self.isdone = False
404 while not self.isdone:
405 self.run_main(timeout)
406 timeout = -1
408 return self.selected_image
410 def run_main(self, timeout = -1):
411 def draw():
412 # set up the screen
413 self.draw_main_windows()
414 self.text_win.addstr(0, 0, "Use the U and D keys to select which entry is highlighted.")
415 self.text_win.addstr(1, 0, "Press enter to boot the selected OS. 'e' to edit the")
416 self.text_win.addstr(2, 0, "commands before booting, 'a' to modify the kernel arguments ")
417 self.text_win.addstr(3, 0, "before booting, or 'c' for a command line.")
418 self.text_win.addch(0, 8, curses.ACS_UARROW)
419 self.text_win.addch(0, 14, curses.ACS_DARROW)
420 (y, x) = self.text_win.getmaxyx()
421 self.text_win.move(y - 1, x - 1)
422 self.text_win.refresh()
424 # now loop until we hit the timeout or get a go from the user
425 mytime = 0
426 self.start_image = 0
427 while (timeout == -1 or mytime < int(timeout)):
428 draw()
429 if timeout != -1 and mytime != -1:
430 self.screen.addstr(20, 5, "Will boot selected entry in %2d seconds"
431 %(int(timeout) - mytime))
432 else:
433 self.screen.addstr(20, 5, " " * 80)
434 self.fill_entry_list()
436 c = self.screen.getch()
437 if c == -1:
438 # Timed out waiting for a keypress
439 if mytime != -1:
440 mytime += 1
441 if mytime >= int(timeout):
442 self.isdone = True
443 break
444 else:
445 # received a keypress: stop the timer
446 mytime = -1
447 self.screen.timeout(-1)
449 # handle keypresses
450 if c == ord('c'):
451 self.command_line_mode()
452 break
453 elif c == ord('a'):
454 # find the kernel line, edit it and then boot
455 img = self.cf.images[self.selected_image]
456 for line in img.lines:
457 if line.startswith("kernel"):
458 l = self.edit_line(line)
459 if l is not None:
460 img.set_from_line(l, replace = True)
461 self.isdone = True
462 break
463 break
464 elif c == ord('e'):
465 img = self.cf.images[self.selected_image]
466 self.edit_entry(img)
467 break
468 elif c in (curses.KEY_ENTER, ord('\n'), ord('\r')):
469 self.isdone = True
470 break
471 elif c == curses.KEY_UP:
472 self.selected_image -= 1
473 elif c == curses.KEY_DOWN:
474 self.selected_image += 1
475 # elif c in (ord('q'), 27): # 27 == esc
476 # self.selected_image = -1
477 # self.isdone = True
478 # break
480 # bound at the top and bottom
481 if self.selected_image < 0:
482 self.selected_image = 0
483 elif self.selected_image >= len(self.cf.images):
484 self.selected_image = len(self.cf.images) - 1
486 def get_entry_idx(cf, entry):
487 # first, see if the given entry is numeric
488 try:
489 idx = string.atoi(entry)
490 return idx
491 except ValueError:
492 pass
494 # it's not, now check the labels for a match
495 for i in range(len(cf.images)):
496 if entry == cf.images[i].title:
497 return i
499 return None
501 def run_grub(file, entry, fs):
502 global g
503 global sel
505 def run_main(scr, *args):
506 global sel
507 global g
508 sel = g.run()
510 g = Grub(file, fs)
511 if interactive:
512 curses.wrapper(run_main)
513 else:
514 sel = g.cf.default
516 # set the entry to boot as requested
517 if entry is not None:
518 idx = get_entry_idx(g.cf, entry)
519 if idx is not None and idx > 0 and idx < len(g.cf.images):
520 sel = idx
522 if sel == -1:
523 print "No kernel image selected!"
524 sys.exit(1)
526 img = g.cf.images[sel]
528 grubcfg = { "kernel": None, "ramdisk": None, "args": None }
530 grubcfg["kernel"] = img.kernel[1]
531 if img.initrd:
532 grubcfg["ramdisk"] = img.initrd[1]
533 if img.args:
534 grubcfg["args"] = img.args
536 return grubcfg
538 # If nothing has been specified, look for a Solaris domU. If found, perform the
539 # necessary tweaks.
540 def sniff_solaris(fs, cfg):
541 if not fs.file_exists("/platform/i86xpv/kernel/unix"):
542 return cfg
544 # darned python
545 longmode = (sys.maxint != 2147483647L)
546 if not longmode:
547 longmode = os.uname()[4] == "x86_64"
548 if not longmode:
549 if (os.access("/usr/bin/isainfo", os.R_OK) and
550 os.popen("/usr/bin/isainfo -b").read() == "64\n"):
551 longmode = True
553 if not cfg["kernel"]:
554 cfg["kernel"] = "/platform/i86xpv/kernel/unix"
555 cfg["ramdisk"] = "/platform/i86pc/boot_archive"
556 if longmode:
557 cfg["kernel"] = "/platform/i86xpv/kernel/amd64/unix"
558 cfg["ramdisk"] = "/platform/i86pc/amd64/boot_archive"
560 # Unpleasant. Typically we'll have 'root=foo -k' or 'root=foo /kernel -k',
561 # and we need to maintain Xen properties (root= and ip=) and the kernel
562 # before any user args.
564 xenargs = ""
565 userargs = ""
567 if not cfg["args"]:
568 cfg["args"] = cfg["kernel"]
569 else:
570 for arg in cfg["args"].split():
571 if re.match("^root=", arg) or re.match("^ip=", arg):
572 xenargs += arg + " "
573 elif arg != cfg["kernel"]:
574 userargs += arg + " "
575 cfg["args"] = xenargs + " " + cfg["kernel"] + " " + userargs
577 return cfg
579 if __name__ == "__main__":
580 sel = None
582 def usage():
583 print >> sys.stderr, "Usage: %s [-q|--quiet] [-i|--interactive] [--output=] [--kernel=] [--ramdisk=] [--args=] [--entry=] <image>" %(sys.argv[0],)
585 try:
586 opts, args = getopt.gnu_getopt(sys.argv[1:], 'qih::',
587 ["quiet", "interactive", "help", "output=",
588 "entry=", "kernel=", "ramdisk=", "args=",
589 "isconfig"])
590 except getopt.GetoptError:
591 usage()
592 sys.exit(1)
594 if len(args) < 1:
595 usage()
596 sys.exit(1)
597 file = args[0]
599 output = None
600 entry = None
601 interactive = True
602 isconfig = False
604 # what was passed in
605 incfg = { "kernel": None, "ramdisk": None, "args": None }
606 # what grub or sniffing chose
607 chosencfg = { "kernel": None, "ramdisk": None, "args": None }
608 # what to boot
609 bootcfg = { "kernel": None, "ramdisk": None, "args": None }
611 for o, a in opts:
612 if o in ("-q", "--quiet"):
613 interactive = False
614 elif o in ("-i", "--interactive"):
615 interactive = True
616 elif o in ("-h", "--help"):
617 usage()
618 sys.exit()
619 elif o in ("--output",):
620 output = a
621 elif o in ("--kernel",):
622 incfg["kernel"] = a
623 elif o in ("--ramdisk",):
624 incfg["ramdisk"] = a
625 elif o in ("--args",):
626 incfg["args"] = a
627 elif o in ("--entry",):
628 entry = a
629 # specifying the entry to boot implies non-interactive
630 interactive = False
631 elif o in ("--isconfig",):
632 isconfig = True
634 if output is None or output == "-":
635 fd = sys.stdout.fileno()
636 else:
637 fd = os.open(output, os.O_WRONLY)
639 # debug
640 if isconfig:
641 chosencfg = run_grub(file, entry)
642 print " kernel: %s" % chosencfg["kernel"]
643 if img.initrd:
644 print " initrd: %s" % chosencfg["ramdisk"]
645 print " args: %s" % chosencfg["args"]
646 sys.exit(0)
648 fs = fsimage.open(file, get_fs_offset(file))
650 chosencfg = sniff_solaris(fs, incfg)
652 if not chosencfg["kernel"]:
653 chosencfg = run_grub(file, entry, fs)
655 data = fs.open_file(chosencfg["kernel"]).read()
656 (tfd, bootcfg["kernel"]) = tempfile.mkstemp(prefix="boot_kernel.",
657 dir="/var/run/xend/boot")
658 os.write(tfd, data)
659 os.close(tfd)
661 if chosencfg["ramdisk"]:
662 data = fs.open_file(chosencfg["ramdisk"],).read()
663 (tfd, bootcfg["ramdisk"]) = tempfile.mkstemp(prefix="boot_ramdisk.",
664 dir="/var/run/xend/boot")
665 os.write(tfd, data)
666 os.close(tfd)
667 else:
668 initrd = None
670 sxp = "linux (kernel %s)" % bootcfg["kernel"]
671 if bootcfg["ramdisk"]:
672 sxp += "(ramdisk %s)" % bootcfg["ramdisk"]
673 if chosencfg["args"]:
674 sxp += "(args \"%s\")" % chosencfg["args"]
676 sys.stdout.flush()
677 os.write(fd, sxp)