ia64/xen-unstable

view tools/pygrub/src/pygrub @ 19472:4b602fff137b

ia64, pygrub: Allow command-line editing in Lilo boot loader

On IA64 platform reset() function has one more parameter for Lilo
bootloader used by IA64 than for GRUB bootloader used for other
platforms.

Signed-off-by: Michal Novotny <minovotn@redhat.com>
author Keir Fraser <keir.fraser@citrix.com>
date Tue Mar 31 13:21:36 2009 +0100 (2009-03-31)
parents 0f53202cb132
children 0e24e9674ded
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', '/usr/lib64/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.noutrefresh()
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.erase()
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 curses.doupdate()
192 r = curses.textpad.Textbox.edit(self)
193 if self.cancelled:
194 return None
195 return string.join(self.line, "")
198 class Grub:
199 def __init__(self, file, fs = None):
200 self.screen = None
201 self.entry_win = None
202 self.text_win = None
203 if file:
204 self.read_config(file, fs)
206 def draw_main_windows(self):
207 if self.screen is None: #only init stuff once
208 self.screen = curses.initscr()
209 self.screen.timeout(1000)
210 if hasattr(curses, 'use_default_colors'):
211 try:
212 curses.use_default_colors()
213 except:
214 pass # Not important if we can't use colour
215 enable_cursor(False)
216 self.entry_win = curses.newwin(10, 74, 2, 1)
217 self.text_win = curses.newwin(10, 70, 12, 5)
218 curses.def_prog_mode()
220 curses.reset_prog_mode()
221 self.screen.erase()
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.noutrefresh()
228 def fill_entry_list(self):
229 self.entry_win.erase()
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 self.entry_win.attron(curses.A_REVERSE)
244 self.entry_win.addstr(y + 1 - self.start_image, 2, i.title.ljust(70))
245 if y == self.selected_image:
246 self.entry_win.attroff(curses.A_REVERSE)
247 self.entry_win.noutrefresh()
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.noutrefresh()
264 curline = 1
265 img = copy.deepcopy(origimg)
266 while 1:
267 draw()
268 self.entry_win.erase()
269 self.entry_win.box()
270 for idx in range(1, len(img.lines)):
271 # current line should be highlighted
272 if idx == curline:
273 self.entry_win.attron(curses.A_REVERSE)
275 # trim the line
276 l = img.lines[idx].ljust(70)
277 if len(l) > 70:
278 l = l[:69] + ">"
280 self.entry_win.addstr(idx, 2, l)
281 if idx == curline:
282 self.entry_win.attroff(curses.A_REVERSE)
283 self.entry_win.noutrefresh()
284 curses.doupdate()
286 c = self.screen.getch()
287 if c in (ord('q'), 27): # 27 == esc
288 break
289 elif c == curses.KEY_UP:
290 curline -= 1
291 elif c == curses.KEY_DOWN:
292 curline += 1
293 elif c == ord('b'):
294 self.isdone = True
295 break
296 elif c == ord('e'):
297 l = self.edit_line(img.lines[curline])
298 if l is not None:
299 img.set_from_line(l, replace = curline)
300 elif c == ord('d'):
301 img.lines.pop(curline)
302 elif c == ord('o'):
303 img.lines.insert(curline+1, "")
304 curline += 1
305 elif c == ord('O'):
306 img.lines.insert(curline, "")
307 elif c == ord('c'):
308 self.command_line_mode()
309 if self.isdone:
310 return
312 # bound at the top and bottom
313 if curline < 1:
314 curline = 1
315 elif curline >= len(img.lines):
316 curline = len(img.lines) - 1
318 if self.isdone:
319 # Fix to allow pygrub command-line editing in Lilo bootloader (used by IA64)
320 if platform.machine() == 'ia64':
321 origimg.reset(img.lines, img.path)
322 else:
323 origimg.reset(img.lines)
325 def edit_line(self, line):
326 self.screen.erase()
327 self.screen.addstr(1, 2, "[ Minimal BASH-like line editing is supported. ")
328 self.screen.addstr(2, 2, " ESC at any time cancels. ENTER at any time accepts your changes. ]")
329 self.screen.noutrefresh()
331 t = GrubLineEditor(self.screen, 5, 2, line)
332 enable_cursor(True)
333 ret = t.edit()
334 if ret:
335 return ret
336 return None
338 def command_line_mode(self):
339 self.screen.erase()
340 self.screen.addstr(1, 2, "[ Minimal BASH-like line editing is supported. ESC at any time ")
341 self.screen.addstr(2, 2, " exits. Typing 'boot' will boot with your entered commands. ] ")
342 self.screen.noutrefresh()
344 y = 5
345 lines = []
346 while 1:
347 t = GrubLineEditor(self.screen, y, 2)
348 enable_cursor(True)
349 ret = t.edit()
350 if ret:
351 if ret in ("quit", "return"):
352 break
353 elif ret != "boot":
354 y += 1
355 lines.append(ret)
356 continue
358 # if we got boot, then we want to boot the entered image
359 img = grub.GrubConf.GrubImage(lines)
360 self.cf.add_image(img)
361 self.selected_image = len(self.cf.images) - 1
362 self.isdone = True
363 break
365 # else, we cancelled and should just go back
366 break
368 def read_config(self, fn, fs = None):
369 """Read the given file to parse the config. If fs = None, then
370 we're being given a raw config file rather than a disk image."""
372 if not os.access(fn, os.R_OK):
373 raise RuntimeError, "Unable to access %s" %(fn,)
375 if platform.machine() == 'ia64':
376 self.cf = grub.LiloConf.LiloConfigFile()
377 # common distributions
378 file_list = ("/efi/debian/elilo.conf", "/efi/gentoo/elilo.conf",
379 "/efi/redflag/elilo.conf", "/efi/redhat/elilo.conf",
380 "/efi/SuSE/elilo.conf",)
381 # fallbacks
382 file_list += ("/efi/boot/elilo.conf", "/elilo.conf",)
383 else:
384 self.cf = grub.GrubConf.GrubConfigFile()
385 file_list = ("/boot/grub/menu.lst", "/boot/grub/grub.conf",
386 "/grub/menu.lst", "/grub/grub.conf")
388 if not fs:
389 # set the config file and parse it
390 self.cf.filename = fn
391 self.cf.parse()
392 return
394 for f in file_list:
395 if fs.file_exists(f):
396 self.cf.filename = f
397 break
398 if self.cf.filename is None:
399 raise RuntimeError, "couldn't find bootloader config file in the image provided."
400 f = fs.open_file(self.cf.filename)
401 buf = f.read()
402 del f
403 self.cf.parse(buf)
405 def run(self):
406 timeout = int(self.cf.timeout)
408 self.selected_image = self.cf.default
409 self.isdone = False
410 while not self.isdone:
411 self.run_main(timeout)
412 timeout = -1
414 return self.selected_image
416 def run_main(self, timeout = -1):
417 def draw():
418 # set up the screen
419 self.draw_main_windows()
420 self.text_win.addstr(0, 0, "Use the U and D keys to select which entry is highlighted.")
421 self.text_win.addstr(1, 0, "Press enter to boot the selected OS. 'e' to edit the")
422 self.text_win.addstr(2, 0, "commands before booting, 'a' to modify the kernel arguments ")
423 self.text_win.addstr(3, 0, "before booting, or 'c' for a command line.")
424 self.text_win.addch(0, 8, curses.ACS_UARROW)
425 self.text_win.addch(0, 14, curses.ACS_DARROW)
426 (y, x) = self.text_win.getmaxyx()
427 self.text_win.move(y - 1, x - 1)
428 self.text_win.noutrefresh()
430 # now loop until we hit the timeout or get a go from the user
431 mytime = 0
432 self.start_image = 0
433 while (timeout == -1 or mytime < int(timeout)):
434 draw()
435 if timeout != -1 and mytime != -1:
436 self.screen.addstr(20, 5, "Will boot selected entry in %2d seconds"
437 %(int(timeout) - mytime))
438 else:
439 self.screen.addstr(20, 5, " " * 80)
440 self.fill_entry_list()
441 curses.doupdate()
443 c = self.screen.getch()
444 if c == -1:
445 # Timed out waiting for a keypress
446 if mytime != -1:
447 mytime += 1
448 # curses.timeout() does not work properly on Solaris
449 # So we may come here even after a key has been pressed.
450 # Check both timeout and mytime to avoid exiting
451 # when we shouldn't.
452 if timeout != -1 and mytime >= int(timeout):
453 self.isdone = True
454 break
455 else:
456 # received a keypress: stop the timer
457 mytime = -1
458 self.screen.timeout(-1)
460 # handle keypresses
461 if c == ord('c'):
462 self.command_line_mode()
463 break
464 elif c == ord('a'):
465 # find the kernel line, edit it and then boot
466 img = self.cf.images[self.selected_image]
467 for line in img.lines:
468 if line.startswith("kernel"):
469 l = self.edit_line(line)
470 if l is not None:
471 img.set_from_line(l, replace = True)
472 self.isdone = True
473 break
474 break
475 elif c == ord('e'):
476 img = self.cf.images[self.selected_image]
477 self.edit_entry(img)
478 break
479 elif c in (curses.KEY_ENTER, ord('\n'), ord('\r')):
480 self.isdone = True
481 break
482 elif c == curses.KEY_UP:
483 self.selected_image -= 1
484 elif c == curses.KEY_DOWN:
485 self.selected_image += 1
486 # elif c in (ord('q'), 27): # 27 == esc
487 # self.selected_image = -1
488 # self.isdone = True
489 # break
491 # bound at the top and bottom
492 if self.selected_image < 0:
493 self.selected_image = 0
494 elif self.selected_image >= len(self.cf.images):
495 self.selected_image = len(self.cf.images) - 1
497 def get_entry_idx(cf, entry):
498 # first, see if the given entry is numeric
499 try:
500 idx = string.atoi(entry)
501 return idx
502 except ValueError:
503 pass
505 # it's not, now check the labels for a match
506 for i in range(len(cf.images)):
507 if entry == cf.images[i].title:
508 return i
510 return None
512 def run_grub(file, entry, fs, arg):
513 global g
514 global sel
516 def run_main(scr, *args):
517 global sel
518 global g
519 sel = g.run()
521 g = Grub(file, fs)
522 if interactive:
523 curses.wrapper(run_main)
524 else:
525 sel = g.cf.default
527 # set the entry to boot as requested
528 if entry is not None:
529 idx = get_entry_idx(g.cf, entry)
530 if idx is not None and idx > 0 and idx < len(g.cf.images):
531 sel = idx
533 if sel == -1:
534 print "No kernel image selected!"
535 sys.exit(1)
537 try:
538 img = g.cf.images[sel]
539 except IndexError:
540 log.debug("PyGrub: Default selection is not valid, using first boot configuration...")
541 img = g.cf.images[0]
543 grubcfg = { "kernel": None, "ramdisk": None, "args": None }
545 grubcfg["kernel"] = img.kernel[1]
546 if img.initrd:
547 grubcfg["ramdisk"] = img.initrd[1]
548 if img.args:
549 grubcfg["args"] = img.args + " " + arg
551 return grubcfg
553 # If nothing has been specified, look for a Solaris domU. If found, perform the
554 # necessary tweaks.
555 def sniff_solaris(fs, cfg):
556 if not fs.file_exists("/platform/i86xpv/kernel/unix"):
557 return cfg
559 # darned python
560 longmode = (sys.maxint != 2147483647L)
561 if not longmode:
562 longmode = os.uname()[4] == "x86_64"
563 if not longmode:
564 if (os.access("/usr/bin/isainfo", os.R_OK) and
565 os.popen("/usr/bin/isainfo -b").read() == "64\n"):
566 longmode = True
568 if not cfg["kernel"]:
569 cfg["kernel"] = "/platform/i86xpv/kernel/unix"
570 cfg["ramdisk"] = "/platform/i86pc/boot_archive"
571 if longmode:
572 cfg["kernel"] = "/platform/i86xpv/kernel/amd64/unix"
573 cfg["ramdisk"] = "/platform/i86pc/amd64/boot_archive"
575 # Unpleasant. Typically we'll have 'root=foo -k' or 'root=foo /kernel -k',
576 # and we need to maintain Xen properties (root= and ip=) and the kernel
577 # before any user args.
579 xenargs = ""
580 userargs = ""
582 if not cfg["args"]:
583 cfg["args"] = cfg["kernel"]
584 else:
585 for arg in cfg["args"].split():
586 if re.match("^root=", arg) or re.match("^ip=", arg):
587 xenargs += arg + " "
588 elif arg != cfg["kernel"]:
589 userargs += arg + " "
590 cfg["args"] = xenargs + " " + cfg["kernel"] + " " + userargs
592 return cfg
594 def sniff_netware(fs, cfg):
595 if not fs.file_exists("/nwserver/xnloader.sys"):
596 return cfg
598 if not cfg["kernel"]:
599 cfg["kernel"] = "/nwserver/xnloader.sys"
601 return cfg
603 if __name__ == "__main__":
604 sel = None
606 def usage():
607 print >> sys.stderr, "Usage: %s [-q|--quiet] [-i|--interactive] [--output=] [--kernel=] [--ramdisk=] [--args=] [--entry=] <image>" %(sys.argv[0],)
609 try:
610 opts, args = getopt.gnu_getopt(sys.argv[1:], 'qih::',
611 ["quiet", "interactive", "help", "output=",
612 "entry=", "kernel=", "ramdisk=", "args=",
613 "isconfig"])
614 except getopt.GetoptError:
615 usage()
616 sys.exit(1)
618 if len(args) < 1:
619 usage()
620 sys.exit(1)
621 file = args[0]
623 output = None
624 entry = None
625 interactive = True
626 isconfig = False
628 # what was passed in
629 incfg = { "kernel": None, "ramdisk": None, "args": "" }
630 # what grub or sniffing chose
631 chosencfg = { "kernel": None, "ramdisk": None, "args": None }
632 # what to boot
633 bootcfg = { "kernel": None, "ramdisk": None, "args": None }
635 for o, a in opts:
636 if o in ("-q", "--quiet"):
637 interactive = False
638 elif o in ("-i", "--interactive"):
639 interactive = True
640 elif o in ("-h", "--help"):
641 usage()
642 sys.exit()
643 elif o in ("--output",):
644 output = a
645 elif o in ("--kernel",):
646 incfg["kernel"] = a
647 elif o in ("--ramdisk",):
648 incfg["ramdisk"] = a
649 elif o in ("--args",):
650 incfg["args"] = a
651 elif o in ("--entry",):
652 entry = a
653 # specifying the entry to boot implies non-interactive
654 interactive = False
655 elif o in ("--isconfig",):
656 isconfig = True
658 if output is None or output == "-":
659 fd = sys.stdout.fileno()
660 else:
661 fd = os.open(output, os.O_WRONLY)
663 # debug
664 if isconfig:
665 chosencfg = run_grub(file, entry, fs, incfg["args"])
666 print " kernel: %s" % chosencfg["kernel"]
667 if img.initrd:
668 print " initrd: %s" % chosencfg["ramdisk"]
669 print " args: %s" % chosencfg["args"]
670 sys.exit(0)
672 # if boot filesystem is set then pass to fsimage.open
673 bootfsargs = '"%s"' % incfg["args"]
674 bootfsgroup = re.findall('zfs-bootfs=(.*?)[\s\,\"]', bootfsargs)
675 if bootfsgroup:
676 fs = fsimage.open(file, get_fs_offset(file), bootfsgroup[0])
677 else:
678 fs = fsimage.open(file, get_fs_offset(file))
680 chosencfg = sniff_solaris(fs, incfg)
682 if not chosencfg["kernel"]:
683 chosencfg = sniff_netware(fs, incfg)
685 if not chosencfg["kernel"]:
686 chosencfg = run_grub(file, entry, fs, incfg["args"])
688 data = fs.open_file(chosencfg["kernel"]).read()
689 (tfd, bootcfg["kernel"]) = tempfile.mkstemp(prefix="boot_kernel.",
690 dir="/var/run/xend/boot")
691 os.write(tfd, data)
692 os.close(tfd)
694 if chosencfg["ramdisk"]:
695 data = fs.open_file(chosencfg["ramdisk"],).read()
696 (tfd, bootcfg["ramdisk"]) = tempfile.mkstemp(prefix="boot_ramdisk.",
697 dir="/var/run/xend/boot")
698 os.write(tfd, data)
699 os.close(tfd)
700 else:
701 initrd = None
703 sxp = "linux (kernel %s)" % bootcfg["kernel"]
704 if bootcfg["ramdisk"]:
705 sxp += "(ramdisk %s)" % bootcfg["ramdisk"]
706 if chosencfg["args"]:
707 zfsinfo = fsimage.getbootstring(fs)
708 if zfsinfo is None:
709 sxp += "(args \"%s\")" % chosencfg["args"]
710 else:
711 e = re.compile("zfs-bootfs=[\w\-\.\:@/]+" )
712 (chosencfg["args"],count) = e.subn(zfsinfo, chosencfg["args"])
713 if count == 0:
714 chosencfg["args"] += " -B %s" % zfsinfo
715 sxp += "(args \"%s\")" % (chosencfg["args"])
717 sys.stdout.flush()
718 os.write(fd, sxp)