ia64/xen-unstable

view tools/pygrub/src/pygrub @ 14615:ff6a1607c17b

Editing long lines in pygrub interactively could lead to tracebacks.
Attached patch fixes things.

Signed-off-by: Jeremy Katz <katzj@redhat.com>
author Tim Deegan <Tim.Deegan@xensource.com>
date Wed Mar 28 09:12:16 2007 +0000 (2007-03-28)
parents 1c7efb60176c
children ed78f08aad61
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 # if there's not a partition marked as active, fall back to
63 # the first partition
64 return buf[446:446+16]
66 SECTOR_SIZE=512
67 DK_LABEL_LOC=1
68 DKL_MAGIC=0xdabe
69 V_ROOT=0x2
71 def get_solaris_slice(file, offset):
72 """Find the root slice in a Solaris VTOC."""
74 fd = os.open(file, os.O_RDONLY)
75 os.lseek(fd, offset + (DK_LABEL_LOC * SECTOR_SIZE), 0)
76 buf = os.read(fd, 512)
77 if struct.unpack("<H", buf[508:510])[0] != DKL_MAGIC:
78 raise RuntimeError, "Invalid disklabel magic"
80 nslices = struct.unpack("<H", buf[30:32])[0]
82 for i in range(nslices):
83 sliceoff = 72 + 12 * i
84 slicetag = struct.unpack("<H", buf[sliceoff:sliceoff+2])[0]
85 slicesect = struct.unpack("<L", buf[sliceoff+4:sliceoff+8])[0]
86 if slicetag == V_ROOT:
87 return slicesect * SECTOR_SIZE
89 raise RuntimeError, "No root slice found"
91 FDISK_PART_SOLARIS=0xbf
92 FDISK_PART_SOLARIS_OLD=0x82
94 def get_fs_offset(file):
95 if not is_disk_image(file):
96 return 0
98 partbuf = get_active_partition(file)
99 if len(partbuf) == 0:
100 raise RuntimeError, "Unable to find active partition on disk"
102 offset = struct.unpack("<L", partbuf[8:12])[0] * SECTOR_SIZE
104 type = struct.unpack("<B", partbuf[4:5])[0]
106 if type == FDISK_PART_SOLARIS or type == FDISK_PART_SOLARIS_OLD:
107 offset += get_solaris_slice(file, offset)
109 return offset
111 class GrubLineEditor(curses.textpad.Textbox):
112 def __init__(self, screen, startx, starty, line = ""):
113 screen.addstr(startx, starty, "> ")
114 screen.refresh()
115 win = curses.newwin(1, 74, startx, starty + 2)
116 curses.textpad.Textbox.__init__(self, win)
118 self.line = list(line)
119 self.pos = len(line)
120 self.cancelled = False
121 self.show_text()
123 def show_text(self):
124 """Show the text. One of our advantages over standard textboxes
125 is that we can handle lines longer than the window."""
127 self.win.clear()
128 p = self.pos
129 off = 0
130 while p > 70:
131 p -= 55
132 off += 55
134 l = self.line[off:off+70]
135 self.win.addstr(0, 0, string.join(l, ("")))
136 if self.pos > 70:
137 self.win.addch(0, 0, curses.ACS_LARROW)
139 self.win.move(0, p)
141 def do_command(self, ch):
142 # we handle escape as well as moving the line around, so have
143 # to override some of the default handling
145 self.lastcmd = ch
146 if ch == 27: # esc
147 self.cancelled = True
148 return 0
149 elif curses.ascii.isprint(ch):
150 self.line.insert(self.pos, chr(ch))
151 self.pos += 1
152 elif ch == curses.ascii.SOH: # ^a
153 self.pos = 0
154 elif ch in (curses.ascii.STX,curses.KEY_LEFT):
155 if self.pos > 0:
156 self.pos -= 1
157 elif ch in (curses.ascii.BS,curses.KEY_BACKSPACE):
158 if self.pos > 0:
159 self.pos -= 1
160 if self.pos < len(self.line):
161 self.line.pop(self.pos)
162 elif ch == curses.ascii.EOT: # ^d
163 if self.pos < len(self.line):
164 self.line.pop(self.pos)
165 elif ch == curses.ascii.ENQ: # ^e
166 self.pos = len(self.line)
167 elif ch in (curses.ascii.ACK, curses.KEY_RIGHT):
168 if self.pos < len(self.line):
169 self.pos +=1
170 elif ch == curses.ascii.VT: # ^k
171 self.line = self.line[:self.pos]
172 else:
173 return curses.textpad.Textbox.do_command(self, ch)
174 self.show_text()
175 return 1
177 def edit(self):
178 r = curses.textpad.Textbox.edit(self)
179 if self.cancelled:
180 return None
181 return string.join(self.line, "")
184 class Grub:
185 def __init__(self, file, fs = None):
186 self.screen = None
187 self.entry_win = None
188 self.text_win = None
189 if file:
190 self.read_config(file, fs)
192 def draw_main_windows(self):
193 if self.screen is None: #only init stuff once
194 self.screen = curses.initscr()
195 self.screen.timeout(1000)
196 if hasattr(curses, 'use_default_colors'):
197 try:
198 curses.use_default_colors()
199 except:
200 pass # Not important if we can't use colour
201 enable_cursor(False)
202 self.entry_win = curses.newwin(10, 74, 2, 1)
203 self.text_win = curses.newwin(10, 70, 12, 5)
204 curses.def_prog_mode()
206 curses.reset_prog_mode()
207 self.screen.clear()
208 self.screen.refresh()
210 # create basic grub screen with a box of entries and a textbox
211 self.screen.addstr(1, 4, "pyGRUB version %s" %(PYGRUB_VER,))
212 self.entry_win.box()
213 self.screen.refresh()
215 def fill_entry_list(self):
216 self.entry_win.clear()
217 self.entry_win.box()
218 for y in range(0, len(self.cf.images)):
219 i = self.cf.images[y]
220 if (0, y) > self.entry_win.getmaxyx():
221 break
222 if y == self.selected_image:
223 attr = curses.A_REVERSE
224 else:
225 attr = 0
226 self.entry_win.addstr(y + 1, 2, i.title.ljust(70), attr)
227 self.entry_win.refresh()
229 def edit_entry(self, origimg):
230 def draw():
231 self.draw_main_windows()
233 self.text_win.addstr(0, 0, "Use the U and D keys to select which entry is highlighted.")
234 self.text_win.addstr(1, 0, "Press 'b' to boot, 'e' to edit the selected command in the")
235 self.text_win.addstr(2, 0, "boot sequence, 'c' for a command-line, 'o' to open a new line")
236 self.text_win.addstr(3, 0, "after ('O' for before) the selected line, 'd' to remove the")
237 self.text_win.addstr(4, 0, "selected line, or escape to go back to the main menu.")
238 self.text_win.addch(0, 8, curses.ACS_UARROW)
239 self.text_win.addch(0, 14, curses.ACS_DARROW)
240 (y, x) = self.text_win.getmaxyx()
241 self.text_win.move(y - 1, x - 1)
242 self.text_win.refresh()
244 curline = 1
245 img = copy.deepcopy(origimg)
246 while 1:
247 draw()
248 self.entry_win.clear()
249 self.entry_win.box()
250 for idx in range(1, len(img.lines)):
251 # current line should be highlighted
252 attr = 0
253 if idx == curline:
254 attr = curses.A_REVERSE
256 # trim the line
257 l = img.lines[idx].ljust(70)
258 if len(l) > 70:
259 l = l[:69] + ">"
261 self.entry_win.addstr(idx, 2, l, attr)
262 self.entry_win.refresh()
264 c = self.screen.getch()
265 if c in (ord('q'), 27): # 27 == esc
266 break
267 elif c == curses.KEY_UP:
268 curline -= 1
269 elif c == curses.KEY_DOWN:
270 curline += 1
271 elif c == ord('b'):
272 self.isdone = True
273 break
274 elif c == ord('e'):
275 l = self.edit_line(img.lines[curline])
276 if l is not None:
277 img.set_from_line(l, replace = curline)
278 elif c == ord('d'):
279 img.lines.pop(curline)
280 elif c == ord('o'):
281 img.lines.insert(curline+1, "")
282 curline += 1
283 elif c == ord('O'):
284 img.lines.insert(curline, "")
285 elif c == ord('c'):
286 self.command_line_mode()
287 if self.isdone:
288 return
290 # bound at the top and bottom
291 if curline < 1:
292 curline = 1
293 elif curline >= len(img.lines):
294 curline = len(img.lines) - 1
296 if self.isdone:
297 origimg.reset(img.lines)
299 def edit_line(self, line):
300 self.screen.clear()
301 self.screen.addstr(1, 2, "[ Minimal BASH-like line editing is supported. ")
302 self.screen.addstr(2, 2, " ESC at any time cancels. ENTER at any time accepts your changes. ]")
303 self.screen.refresh()
305 t = GrubLineEditor(self.screen, 5, 2, line)
306 enable_cursor(True)
307 ret = t.edit()
308 if ret:
309 return ret
310 return None
312 def command_line_mode(self):
313 self.screen.clear()
314 self.screen.addstr(1, 2, "[ Minimal BASH-like line editing is supported. ESC at any time ")
315 self.screen.addstr(2, 2, " exits. Typing 'boot' will boot with your entered commands. ] ")
316 self.screen.refresh()
318 y = 5
319 lines = []
320 while 1:
321 t = GrubLineEditor(self.screen, y, 2)
322 enable_cursor(True)
323 ret = t.edit()
324 if ret:
325 if ret in ("quit", "return"):
326 break
327 elif ret != "boot":
328 y += 1
329 lines.append(ret)
330 continue
332 # if we got boot, then we want to boot the entered image
333 img = grub.GrubConf.GrubImage(lines)
334 self.cf.add_image(img)
335 self.selected_image = len(self.cf.images) - 1
336 self.isdone = True
337 break
339 # else, we cancelled and should just go back
340 break
342 def read_config(self, fn, fs = None):
343 """Read the given file to parse the config. If fs = None, then
344 we're being given a raw config file rather than a disk image."""
346 if not os.access(fn, os.R_OK):
347 raise RuntimeError, "Unable to access %s" %(fn,)
349 self.cf = grub.GrubConf.GrubConfigFile()
351 if not fs:
352 # set the config file and parse it
353 self.cf.filename = fn
354 self.cf.parse()
355 return
357 grubfile = None
358 for f in ("/boot/grub/menu.lst", "/boot/grub/grub.conf",
359 "/grub/menu.lst", "/grub/grub.conf"):
360 if fs.file_exists(f):
361 grubfile = f
362 break
363 if grubfile is None:
364 raise RuntimeError, "we couldn't find grub config file in the image provided."
365 f = fs.open_file(grubfile)
366 buf = f.read()
367 del f
368 # then parse the grub config
369 self.cf.parse(buf)
371 def run(self):
372 timeout = int(self.cf.timeout)
374 self.selected_image = self.cf.default
375 self.isdone = False
376 while not self.isdone:
377 self.run_main(timeout)
378 timeout = -1
380 return self.selected_image
382 def run_main(self, timeout = -1):
383 def draw():
384 # set up the screen
385 self.draw_main_windows()
386 self.text_win.addstr(0, 0, "Use the U and D keys to select which entry is highlighted.")
387 self.text_win.addstr(1, 0, "Press enter to boot the selected OS. 'e' to edit the")
388 self.text_win.addstr(2, 0, "commands before booting, 'a' to modify the kernel arguments ")
389 self.text_win.addstr(3, 0, "before booting, or 'c' for a command line.")
390 self.text_win.addch(0, 8, curses.ACS_UARROW)
391 self.text_win.addch(0, 14, curses.ACS_DARROW)
392 (y, x) = self.text_win.getmaxyx()
393 self.text_win.move(y - 1, x - 1)
394 self.text_win.refresh()
396 # now loop until we hit the timeout or get a go from the user
397 mytime = 0
398 while (timeout == -1 or mytime < int(timeout)):
399 draw()
400 if timeout != -1 and mytime != -1:
401 self.screen.addstr(20, 5, "Will boot selected entry in %2d seconds"
402 %(int(timeout) - mytime))
403 else:
404 self.screen.addstr(20, 5, " " * 80)
405 self.fill_entry_list()
407 c = self.screen.getch()
408 if c == -1:
409 # Timed out waiting for a keypress
410 if mytime != -1:
411 mytime += 1
412 if mytime >= int(timeout):
413 self.isdone = True
414 break
415 else:
416 # received a keypress: stop the timer
417 mytime = -1
418 self.screen.timeout(-1)
420 # handle keypresses
421 if c == ord('c'):
422 self.command_line_mode()
423 break
424 elif c == ord('a'):
425 # find the kernel line, edit it and then boot
426 img = self.cf.images[self.selected_image]
427 for line in img.lines:
428 if line.startswith("kernel"):
429 l = self.edit_line(line)
430 if l is not None:
431 img.set_from_line(l, replace = True)
432 self.isdone = True
433 break
434 break
435 elif c == ord('e'):
436 img = self.cf.images[self.selected_image]
437 self.edit_entry(img)
438 break
439 elif c in (curses.KEY_ENTER, ord('\n'), ord('\r')):
440 self.isdone = True
441 break
442 elif c == curses.KEY_UP:
443 self.selected_image -= 1
444 elif c == curses.KEY_DOWN:
445 self.selected_image += 1
446 # elif c in (ord('q'), 27): # 27 == esc
447 # self.selected_image = -1
448 # self.isdone = True
449 # break
451 # bound at the top and bottom
452 if self.selected_image < 0:
453 self.selected_image = 0
454 elif self.selected_image >= len(self.cf.images):
455 self.selected_image = len(self.cf.images) - 1
457 def get_entry_idx(cf, entry):
458 # first, see if the given entry is numeric
459 try:
460 idx = string.atoi(entry)
461 return idx
462 except ValueError:
463 pass
465 # it's not, now check the labels for a match
466 for i in range(len(cf.images)):
467 if entry == cf.images[i].title:
468 return i
470 return None
472 def run_grub(file, entry, fs):
473 global g
474 global sel
476 def run_main(scr, *args):
477 global sel
478 global g
479 sel = g.run()
481 g = Grub(file, fs)
482 if interactive:
483 curses.wrapper(run_main)
484 else:
485 sel = g.cf.default
487 # set the entry to boot as requested
488 if entry is not None:
489 idx = get_entry_idx(g.cf, entry)
490 if idx is not None and idx > 0 and idx < len(g.cf.images):
491 sel = idx
493 if sel == -1:
494 print "No kernel image selected!"
495 sys.exit(1)
497 img = g.cf.images[sel]
499 grubcfg = { "kernel": None, "ramdisk": None, "args": None }
501 grubcfg["kernel"] = img.kernel[1]
502 if img.initrd:
503 grubcfg["ramdisk"] = img.initrd[1]
504 if img.args:
505 grubcfg["args"] = img.args
507 return grubcfg
509 # If nothing has been specified, look for a Solaris domU. If found, perform the
510 # necessary tweaks.
511 def sniff_solaris(fs, cfg):
512 if not fs.file_exists("/platform/i86xpv/kernel/unix"):
513 return cfg
515 # darned python
516 longmode = (sys.maxint != 2147483647L)
517 if not longmode:
518 longmode = os.uname()[4] == "x86_64"
519 if not longmode:
520 if (os.access("/usr/bin/isainfo", os.R_OK) and
521 os.popen("/usr/bin/isainfo -b").read() == "64\n"):
522 longmode = True
524 if not cfg["kernel"]:
525 cfg["kernel"] = "/platform/i86xpv/kernel/unix"
526 cfg["ramdisk"] = "/platform/i86pc/boot_archive"
527 if longmode:
528 cfg["kernel"] = "/platform/i86xpv/kernel/amd64/unix"
529 cfg["ramdisk"] = "/platform/i86pc/amd64/boot_archive"
531 # Unpleasant. Typically we'll have 'root=foo -k' or 'root=foo /kernel -k',
532 # and we need to maintain Xen properties (root= and ip=) and the kernel
533 # before any user args.
535 xenargs = ""
536 userargs = ""
538 if not cfg["args"]:
539 cfg["args"] = cfg["kernel"]
540 else:
541 for arg in cfg["args"].split():
542 if re.match("^root=", arg) or re.match("^ip=", arg):
543 xenargs += arg + " "
544 elif arg != cfg["kernel"]:
545 userargs += arg + " "
546 cfg["args"] = xenargs + " " + cfg["kernel"] + " " + userargs
548 return cfg
550 if __name__ == "__main__":
551 sel = None
553 def usage():
554 print >> sys.stderr, "Usage: %s [-q|--quiet] [-i|--interactive] [--output=] [--kernel=] [--ramdisk=] [--args=] [--entry=] <image>" %(sys.argv[0],)
556 try:
557 opts, args = getopt.gnu_getopt(sys.argv[1:], 'qih::',
558 ["quiet", "interactive", "help", "output=",
559 "entry=", "kernel=", "ramdisk=", "args=",
560 "isconfig"])
561 except getopt.GetoptError:
562 usage()
563 sys.exit(1)
565 if len(args) < 1:
566 usage()
567 sys.exit(1)
568 file = args[0]
570 output = None
571 entry = None
572 interactive = True
573 isconfig = False
575 # what was passed in
576 incfg = { "kernel": None, "ramdisk": None, "args": None }
577 # what grub or sniffing chose
578 chosencfg = { "kernel": None, "ramdisk": None, "args": None }
579 # what to boot
580 bootcfg = { "kernel": None, "ramdisk": None, "args": None }
582 for o, a in opts:
583 if o in ("-q", "--quiet"):
584 interactive = False
585 elif o in ("-i", "--interactive"):
586 interactive = True
587 elif o in ("-h", "--help"):
588 usage()
589 sys.exit()
590 elif o in ("--output",):
591 output = a
592 elif o in ("--kernel",):
593 incfg["kernel"] = a
594 elif o in ("--ramdisk",):
595 incfg["ramdisk"] = a
596 elif o in ("--args",):
597 incfg["args"] = a
598 elif o in ("--entry",):
599 entry = a
600 # specifying the entry to boot implies non-interactive
601 interactive = False
602 elif o in ("--isconfig",):
603 isconfig = True
605 if output is None or output == "-":
606 fd = sys.stdout.fileno()
607 else:
608 fd = os.open(output, os.O_WRONLY)
610 # debug
611 if isconfig:
612 chosencfg = run_grub(file, entry)
613 print " kernel: %s" % chosencfg["kernel"]
614 if img.initrd:
615 print " initrd: %s" % chosencfg["ramdisk"]
616 print " args: %s" % chosencfg["args"]
617 sys.exit(0)
619 fs = fsimage.open(file, get_fs_offset(file))
621 chosencfg = sniff_solaris(fs, incfg)
623 if not chosencfg["kernel"]:
624 chosencfg = run_grub(file, entry, fs)
626 data = fs.open_file(chosencfg["kernel"]).read()
627 (tfd, bootcfg["kernel"]) = tempfile.mkstemp(prefix="boot_kernel.",
628 dir="/var/run/xend/boot")
629 os.write(tfd, data)
630 os.close(tfd)
632 if chosencfg["ramdisk"]:
633 data = fs.open_file(chosencfg["ramdisk"],).read()
634 (tfd, bootcfg["ramdisk"]) = tempfile.mkstemp(prefix="boot_ramdisk.",
635 dir="/var/run/xend/boot")
636 os.write(tfd, data)
637 os.close(tfd)
638 else:
639 initrd = None
641 sxp = "linux (kernel %s)" % bootcfg["kernel"]
642 if bootcfg["ramdisk"]:
643 sxp += "(ramdisk %s)" % bootcfg["ramdisk"]
644 if chosencfg["args"]:
645 sxp += "(args \"%s\")" % chosencfg["args"]
647 sys.stdout.flush()
648 os.write(fd, sxp)