ia64/xen-unstable

view tools/pygrub/src/pygrub @ 19294:455720d735c7

pygrub: Enable domU boot without xen specific arg

This patch makes domUs bring up without xen specific args to guest
kernels. A domU should be bootable without args parameter because
tools/examples/xmexample1 doesn't have one.

Signed-off-by: INAKOSHI Hiroya <inakoshi.hiroya@jp.fujitsu.com>
author Keir Fraser <keir.fraser@citrix.com>
date Mon Mar 09 09:19:10 2009 +0000 (2009-03-09)
parents 596f21d901f3
children 3118041f2259
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 origimg.reset(img.lines)
321 def edit_line(self, line):
322 self.screen.erase()
323 self.screen.addstr(1, 2, "[ Minimal BASH-like line editing is supported. ")
324 self.screen.addstr(2, 2, " ESC at any time cancels. ENTER at any time accepts your changes. ]")
325 self.screen.noutrefresh()
327 t = GrubLineEditor(self.screen, 5, 2, line)
328 enable_cursor(True)
329 ret = t.edit()
330 if ret:
331 return ret
332 return None
334 def command_line_mode(self):
335 self.screen.erase()
336 self.screen.addstr(1, 2, "[ Minimal BASH-like line editing is supported. ESC at any time ")
337 self.screen.addstr(2, 2, " exits. Typing 'boot' will boot with your entered commands. ] ")
338 self.screen.noutrefresh()
340 y = 5
341 lines = []
342 while 1:
343 t = GrubLineEditor(self.screen, y, 2)
344 enable_cursor(True)
345 ret = t.edit()
346 if ret:
347 if ret in ("quit", "return"):
348 break
349 elif ret != "boot":
350 y += 1
351 lines.append(ret)
352 continue
354 # if we got boot, then we want to boot the entered image
355 img = grub.GrubConf.GrubImage(lines)
356 self.cf.add_image(img)
357 self.selected_image = len(self.cf.images) - 1
358 self.isdone = True
359 break
361 # else, we cancelled and should just go back
362 break
364 def read_config(self, fn, fs = None):
365 """Read the given file to parse the config. If fs = None, then
366 we're being given a raw config file rather than a disk image."""
368 if not os.access(fn, os.R_OK):
369 raise RuntimeError, "Unable to access %s" %(fn,)
371 if platform.machine() == 'ia64':
372 self.cf = grub.LiloConf.LiloConfigFile()
373 # common distributions
374 file_list = ("/efi/debian/elilo.conf", "/efi/gentoo/elilo.conf",
375 "/efi/redflag/elilo.conf", "/efi/redhat/elilo.conf",
376 "/efi/SuSE/elilo.conf",)
377 # fallbacks
378 file_list += ("/efi/boot/elilo.conf", "/elilo.conf",)
379 else:
380 self.cf = grub.GrubConf.GrubConfigFile()
381 file_list = ("/boot/grub/menu.lst", "/boot/grub/grub.conf",
382 "/grub/menu.lst", "/grub/grub.conf")
384 if not fs:
385 # set the config file and parse it
386 self.cf.filename = fn
387 self.cf.parse()
388 return
390 for f in file_list:
391 if fs.file_exists(f):
392 self.cf.filename = f
393 break
394 if self.cf.filename is None:
395 raise RuntimeError, "couldn't find bootloader config file in the image provided."
396 f = fs.open_file(self.cf.filename)
397 buf = f.read()
398 del f
399 self.cf.parse(buf)
401 def run(self):
402 timeout = int(self.cf.timeout)
404 self.selected_image = self.cf.default
405 self.isdone = False
406 while not self.isdone:
407 self.run_main(timeout)
408 timeout = -1
410 return self.selected_image
412 def run_main(self, timeout = -1):
413 def draw():
414 # set up the screen
415 self.draw_main_windows()
416 self.text_win.addstr(0, 0, "Use the U and D keys to select which entry is highlighted.")
417 self.text_win.addstr(1, 0, "Press enter to boot the selected OS. 'e' to edit the")
418 self.text_win.addstr(2, 0, "commands before booting, 'a' to modify the kernel arguments ")
419 self.text_win.addstr(3, 0, "before booting, or 'c' for a command line.")
420 self.text_win.addch(0, 8, curses.ACS_UARROW)
421 self.text_win.addch(0, 14, curses.ACS_DARROW)
422 (y, x) = self.text_win.getmaxyx()
423 self.text_win.move(y - 1, x - 1)
424 self.text_win.noutrefresh()
426 # now loop until we hit the timeout or get a go from the user
427 mytime = 0
428 self.start_image = 0
429 while (timeout == -1 or mytime < int(timeout)):
430 draw()
431 if timeout != -1 and mytime != -1:
432 self.screen.addstr(20, 5, "Will boot selected entry in %2d seconds"
433 %(int(timeout) - mytime))
434 else:
435 self.screen.addstr(20, 5, " " * 80)
436 self.fill_entry_list()
437 curses.doupdate()
439 c = self.screen.getch()
440 if c == -1:
441 # Timed out waiting for a keypress
442 if mytime != -1:
443 mytime += 1
444 if mytime >= int(timeout):
445 self.isdone = True
446 break
447 else:
448 # received a keypress: stop the timer
449 mytime = -1
450 self.screen.timeout(-1)
452 # handle keypresses
453 if c == ord('c'):
454 self.command_line_mode()
455 break
456 elif c == ord('a'):
457 # find the kernel line, edit it and then boot
458 img = self.cf.images[self.selected_image]
459 for line in img.lines:
460 if line.startswith("kernel"):
461 l = self.edit_line(line)
462 if l is not None:
463 img.set_from_line(l, replace = True)
464 self.isdone = True
465 break
466 break
467 elif c == ord('e'):
468 img = self.cf.images[self.selected_image]
469 self.edit_entry(img)
470 break
471 elif c in (curses.KEY_ENTER, ord('\n'), ord('\r')):
472 self.isdone = True
473 break
474 elif c == curses.KEY_UP:
475 self.selected_image -= 1
476 elif c == curses.KEY_DOWN:
477 self.selected_image += 1
478 # elif c in (ord('q'), 27): # 27 == esc
479 # self.selected_image = -1
480 # self.isdone = True
481 # break
483 # bound at the top and bottom
484 if self.selected_image < 0:
485 self.selected_image = 0
486 elif self.selected_image >= len(self.cf.images):
487 self.selected_image = len(self.cf.images) - 1
489 def get_entry_idx(cf, entry):
490 # first, see if the given entry is numeric
491 try:
492 idx = string.atoi(entry)
493 return idx
494 except ValueError:
495 pass
497 # it's not, now check the labels for a match
498 for i in range(len(cf.images)):
499 if entry == cf.images[i].title:
500 return i
502 return None
504 def run_grub(file, entry, fs, arg):
505 global g
506 global sel
508 def run_main(scr, *args):
509 global sel
510 global g
511 sel = g.run()
513 g = Grub(file, fs)
514 if interactive:
515 curses.wrapper(run_main)
516 else:
517 sel = g.cf.default
519 # set the entry to boot as requested
520 if entry is not None:
521 idx = get_entry_idx(g.cf, entry)
522 if idx is not None and idx > 0 and idx < len(g.cf.images):
523 sel = idx
525 if sel == -1:
526 print "No kernel image selected!"
527 sys.exit(1)
529 img = g.cf.images[sel]
531 grubcfg = { "kernel": None, "ramdisk": None, "args": None }
533 grubcfg["kernel"] = img.kernel[1]
534 if img.initrd:
535 grubcfg["ramdisk"] = img.initrd[1]
536 if img.args:
537 grubcfg["args"] = img.args + " " + arg
539 return grubcfg
541 # If nothing has been specified, look for a Solaris domU. If found, perform the
542 # necessary tweaks.
543 def sniff_solaris(fs, cfg):
544 if not fs.file_exists("/platform/i86xpv/kernel/unix"):
545 return cfg
547 # darned python
548 longmode = (sys.maxint != 2147483647L)
549 if not longmode:
550 longmode = os.uname()[4] == "x86_64"
551 if not longmode:
552 if (os.access("/usr/bin/isainfo", os.R_OK) and
553 os.popen("/usr/bin/isainfo -b").read() == "64\n"):
554 longmode = True
556 if not cfg["kernel"]:
557 cfg["kernel"] = "/platform/i86xpv/kernel/unix"
558 cfg["ramdisk"] = "/platform/i86pc/boot_archive"
559 if longmode:
560 cfg["kernel"] = "/platform/i86xpv/kernel/amd64/unix"
561 cfg["ramdisk"] = "/platform/i86pc/amd64/boot_archive"
563 # Unpleasant. Typically we'll have 'root=foo -k' or 'root=foo /kernel -k',
564 # and we need to maintain Xen properties (root= and ip=) and the kernel
565 # before any user args.
567 xenargs = ""
568 userargs = ""
570 if not cfg["args"]:
571 cfg["args"] = cfg["kernel"]
572 else:
573 for arg in cfg["args"].split():
574 if re.match("^root=", arg) or re.match("^ip=", arg):
575 xenargs += arg + " "
576 elif arg != cfg["kernel"]:
577 userargs += arg + " "
578 cfg["args"] = xenargs + " " + cfg["kernel"] + " " + userargs
580 return cfg
582 if __name__ == "__main__":
583 sel = None
585 def usage():
586 print >> sys.stderr, "Usage: %s [-q|--quiet] [-i|--interactive] [--output=] [--kernel=] [--ramdisk=] [--args=] [--entry=] <image>" %(sys.argv[0],)
588 try:
589 opts, args = getopt.gnu_getopt(sys.argv[1:], 'qih::',
590 ["quiet", "interactive", "help", "output=",
591 "entry=", "kernel=", "ramdisk=", "args=",
592 "isconfig"])
593 except getopt.GetoptError:
594 usage()
595 sys.exit(1)
597 if len(args) < 1:
598 usage()
599 sys.exit(1)
600 file = args[0]
602 output = None
603 entry = None
604 interactive = True
605 isconfig = False
607 # what was passed in
608 incfg = { "kernel": None, "ramdisk": None, "args": "" }
609 # what grub or sniffing chose
610 chosencfg = { "kernel": None, "ramdisk": None, "args": None }
611 # what to boot
612 bootcfg = { "kernel": None, "ramdisk": None, "args": None }
614 for o, a in opts:
615 if o in ("-q", "--quiet"):
616 interactive = False
617 elif o in ("-i", "--interactive"):
618 interactive = True
619 elif o in ("-h", "--help"):
620 usage()
621 sys.exit()
622 elif o in ("--output",):
623 output = a
624 elif o in ("--kernel",):
625 incfg["kernel"] = a
626 elif o in ("--ramdisk",):
627 incfg["ramdisk"] = a
628 elif o in ("--args",):
629 incfg["args"] = a
630 elif o in ("--entry",):
631 entry = a
632 # specifying the entry to boot implies non-interactive
633 interactive = False
634 elif o in ("--isconfig",):
635 isconfig = True
637 if output is None or output == "-":
638 fd = sys.stdout.fileno()
639 else:
640 fd = os.open(output, os.O_WRONLY)
642 # debug
643 if isconfig:
644 chosencfg = run_grub(file, entry, fs, incfg["args"])
645 print " kernel: %s" % chosencfg["kernel"]
646 if img.initrd:
647 print " initrd: %s" % chosencfg["ramdisk"]
648 print " args: %s" % chosencfg["args"]
649 sys.exit(0)
651 # if boot filesystem is set then pass to fsimage.open
652 bootfsargs = '"%s"' % incfg["args"]
653 bootfsgroup = re.findall('zfs-bootfs=(.*?)[\s\,\"]', bootfsargs)
654 if bootfsgroup:
655 fs = fsimage.open(file, get_fs_offset(file), bootfsgroup[0])
656 else:
657 fs = fsimage.open(file, get_fs_offset(file))
659 chosencfg = sniff_solaris(fs, incfg)
661 if not chosencfg["kernel"]:
662 chosencfg = run_grub(file, entry, fs, incfg["args"])
664 data = fs.open_file(chosencfg["kernel"]).read()
665 (tfd, bootcfg["kernel"]) = tempfile.mkstemp(prefix="boot_kernel.",
666 dir="/var/run/xend/boot")
667 os.write(tfd, data)
668 os.close(tfd)
670 if chosencfg["ramdisk"]:
671 data = fs.open_file(chosencfg["ramdisk"],).read()
672 (tfd, bootcfg["ramdisk"]) = tempfile.mkstemp(prefix="boot_ramdisk.",
673 dir="/var/run/xend/boot")
674 os.write(tfd, data)
675 os.close(tfd)
676 else:
677 initrd = None
679 sxp = "linux (kernel %s)" % bootcfg["kernel"]
680 if bootcfg["ramdisk"]:
681 sxp += "(ramdisk %s)" % bootcfg["ramdisk"]
682 if chosencfg["args"]:
683 zfsinfo = fsimage.getbootstring(fs)
684 if zfsinfo is None:
685 sxp += "(args \"%s\")" % chosencfg["args"]
686 else:
687 e = re.compile("zfs-bootfs=[\w\-\.\:@/]+" )
688 (chosencfg["args"],count) = e.subn(zfsinfo, chosencfg["args"])
689 if count == 0:
690 chosencfg["args"] += " -B %s" % zfsinfo
691 sxp += "(args \"%s\")" % (chosencfg["args"])
693 sys.stdout.flush()
694 os.write(fd, sxp)