1# 2# GrubConf.py - Simple grub.conf parsing 3# 4# Copyright 2009 Citrix Systems Inc. 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, see <http://www.gnu.org/licenses/>. 13# 14 15import os, sys 16import logging 17import re 18 19def grub_split(s, maxsplit = -1): 20 eq = s.find('=') 21 if eq == -1: 22 return s.split(None, maxsplit) 23 24 # see which of a space or tab is first 25 sp = s.find(' ') 26 tab = s.find('\t') 27 if (tab != -1 and tab < sp) or (tab != -1 and sp == -1): 28 sp = tab 29 30 if eq != -1 and eq < sp or (eq != -1 and sp == -1): 31 return s.split('=', maxsplit) 32 else: 33 return s.split(None, maxsplit) 34 35def grub_exact_split(s, num): 36 ret = grub_split(s, num - 1) 37 if len(ret) < num: 38 return ret + [""] * (num - len(ret)) 39 return ret 40 41def get_path(s): 42 """Returns a tuple of (GrubDiskPart, path) corresponding to string.""" 43 if not s.startswith('('): 44 return (None, s) 45 idx = s.find(')') 46 if idx == -1: 47 raise ValueError, "Unable to find matching ')'" 48 d = s[:idx] 49 return (GrubDiskPart(d), s[idx + 1:]) 50 51class GrubDiskPart(object): 52 def __init__(self, str): 53 if str.find(',') != -1: 54 (self.disk, self.part) = str.split(",", 2) 55 else: 56 self.disk = str 57 self.part = None 58 59 def __repr__(self): 60 if self.part is not None: 61 return "d%dp%d" %(self.disk, self.part) 62 else: 63 return "d%d" %(self.disk,) 64 65 def get_disk(self): 66 return self._disk 67 def set_disk(self, val): 68 val = val.replace("(", "").replace(")", "") 69 if val.startswith("/dev/xvd"): 70 disk = val[len("/dev/xvd")] 71 self._disk = ord(disk)-ord('a') 72 else: 73 self._disk = int(val[2:]) 74 disk = property(get_disk, set_disk) 75 76 def get_part(self): 77 return self._part 78 def set_part(self, val): 79 if val is None: 80 self._part = val 81 return 82 val = val.replace("(", "").replace(")", "") 83 if val[:5] == "msdos": 84 val = val[5:] 85 if val[:3] == "gpt": 86 val = val[3:] 87 self._part = int(val) 88 part = property(get_part, set_part) 89 90class _GrubImage(object): 91 def __init__(self, title, lines): 92 self.reset(lines) 93 self.title = title.strip() 94 95 def __repr__(self): 96 return ("title: %s\n" 97 " root: %s\n" 98 " kernel: %s\n" 99 " args: %s\n" 100 " initrd: %s\n" %(self.title, self.root, self.kernel, 101 self.args, self.initrd)) 102 def _parse(self, lines): 103 map(self.set_from_line, lines) 104 105 def reset(self, lines): 106 self._root = self._initrd = self._kernel = self._args = None 107 self.lines = [] 108 self._parse(lines) 109 110 def set_root(self, val): 111 self._root = GrubDiskPart(val) 112 def get_root(self): 113 return self._root 114 root = property(get_root, set_root) 115 116 def set_kernel(self, val): 117 if val.find(" ") == -1: 118 self._kernel = get_path(val) 119 self._args = None 120 return 121 (kernel, args) = val.split(None, 1) 122 self._kernel = get_path(kernel) 123 self._args = args 124 def get_kernel(self): 125 return self._kernel 126 def get_args(self): 127 return self._args 128 kernel = property(get_kernel, set_kernel) 129 args = property(get_args) 130 131 def set_initrd(self, val): 132 self._initrd = get_path(val) 133 def get_initrd(self): 134 return self._initrd 135 initrd = property(get_initrd, set_initrd) 136 137class GrubImage(_GrubImage): 138 def __init__(self, title, lines): 139 _GrubImage.__init__(self, title, lines) 140 141 def set_from_line(self, line, replace = None): 142 (com, arg) = grub_exact_split(line, 2) 143 144 if self.commands.has_key(com): 145 if self.commands[com] is not None: 146 setattr(self, self.commands[com], arg.strip()) 147 else: 148 logging.info("Ignored image directive %s" %(com,)) 149 else: 150 logging.warning("Unknown image directive %s" %(com,)) 151 152 # now put the line in the list of lines 153 if replace is None: 154 self.lines.append(line) 155 else: 156 self.lines.pop(replace) 157 self.lines.insert(replace, line) 158 159 # set up command handlers 160 commands = { "root": "root", 161 "rootnoverify": "root", 162 "kernel": "kernel", 163 "initrd": "initrd", 164 "chainloader": None, 165 "module": None} 166 167class _GrubConfigFile(object): 168 def __init__(self, fn = None): 169 self.filename = fn 170 self.images = [] 171 self.timeout = -1 172 self._default = 0 173 self.passwordAccess = True 174 self.passExc = None 175 176 if fn is not None: 177 self.parse() 178 179 def parse(self, buf = None): 180 raise RuntimeError, "unimplemented parse function" 181 182 def hasPasswordAccess(self): 183 return self.passwordAccess 184 185 def setPasswordAccess(self, val): 186 self.passwordAccess = val 187 188 def hasPassword(self): 189 return hasattr(self, 'password') 190 191 def checkPassword(self, password): 192 # Always allow if no password defined in grub.conf 193 if not self.hasPassword(): 194 return True 195 196 pwd = getattr(self, 'password').split() 197 198 # We check whether password is in MD5 hash for comparison 199 if pwd[0] == '--md5': 200 try: 201 import crypt 202 if crypt.crypt(password, pwd[1]) == pwd[1]: 203 return True 204 except Exception, e: 205 self.passExc = "Can't verify password: %s" % str(e) 206 return False 207 208 # ... and if not, we compare it as a plain text 209 if pwd[0] == password: 210 return True 211 212 return False 213 214 def set(self, line): 215 (com, arg) = grub_exact_split(line, 2) 216 if self.commands.has_key(com): 217 if self.commands[com] is not None: 218 setattr(self, self.commands[com], arg.strip()) 219 else: 220 logging.info("Ignored directive %s" %(com,)) 221 else: 222 logging.warning("Unknown directive %s" %(com,)) 223 224 def add_image(self, image): 225 self.images.append(image) 226 227 def _get_default(self): 228 return self._default 229 def _set_default(self, val): 230 if val == "saved": 231 self._default = 0 232 else: 233 self._default = val 234 235 if self._default < 0: 236 raise ValueError, "default must be positive number" 237 default = property(_get_default, _set_default) 238 239 def set_splash(self, val): 240 self._splash = get_path(val) 241 def get_splash(self): 242 return self._splash 243 splash = property(get_splash, set_splash) 244 245 # set up command handlers 246 commands = { "default": "default", 247 "timeout": "timeout", 248 "fallback": "fallback", 249 "hiddenmenu": "hiddenmenu", 250 "splashimage": "splash", 251 "password": "password" } 252 for c in ("bootp", "color", "device", "dhcp", "hide", "ifconfig", 253 "pager", "partnew", "parttype", "rarp", "serial", 254 "setkey", "terminal", "terminfo", "tftpserver", "unhide"): 255 commands[c] = None 256 del c 257 258class GrubConfigFile(_GrubConfigFile): 259 def __init__(self, fn = None): 260 _GrubConfigFile.__init__(self,fn) 261 262 def new_image(self, title, lines): 263 return GrubImage(title, lines) 264 265 def parse(self, buf = None): 266 if buf is None: 267 if self.filename is None: 268 raise ValueError, "No config file defined to parse!" 269 270 f = open(self.filename, 'r') 271 lines = f.readlines() 272 f.close() 273 else: 274 lines = buf.split("\n") 275 276 img = None 277 title = "" 278 for l in lines: 279 l = l.strip() 280 # skip blank lines 281 if len(l) == 0: 282 continue 283 # skip comments 284 if l.startswith('#'): 285 continue 286 # new image 287 if l.startswith("title"): 288 if img is not None: 289 self.add_image(GrubImage(title, img)) 290 img = [] 291 title = l[6:] 292 continue 293 294 if img is not None: 295 img.append(l) 296 continue 297 298 (com, arg) = grub_exact_split(l, 2) 299 if self.commands.has_key(com): 300 if self.commands[com] is not None: 301 setattr(self, self.commands[com], arg.strip()) 302 else: 303 logging.info("Ignored directive %s" %(com,)) 304 else: 305 logging.warning("Unknown directive %s" %(com,)) 306 307 if img: 308 self.add_image(GrubImage(title, img)) 309 310 if self.hasPassword(): 311 self.setPasswordAccess(False) 312 313def grub2_handle_set(arg): 314 (com,arg) = grub_split(arg,2) 315 com="set:" + com 316 m = re.match("([\"\'])(.*)\\1", arg) 317 if m is not None: 318 arg=m.group(2) 319 return (com,arg) 320 321class Grub2Image(_GrubImage): 322 def __init__(self, title, lines): 323 _GrubImage.__init__(self, title, lines) 324 325 def set_from_line(self, line, replace = None): 326 (com, arg) = grub_exact_split(line, 2) 327 328 if com == "set": 329 (com,arg) = grub2_handle_set(arg) 330 331 if self.commands.has_key(com): 332 if self.commands[com] is not None: 333 setattr(self, self.commands[com], arg.strip()) 334 else: 335 logging.info("Ignored image directive %s" %(com,)) 336 elif com.startswith('set:'): 337 pass 338 else: 339 logging.warning("Unknown image directive %s" %(com,)) 340 341 # now put the line in the list of lines 342 if replace is None: 343 self.lines.append(line) 344 else: 345 self.lines.pop(replace) 346 self.lines.insert(replace, line) 347 348 commands = {'set:root': 'root', 349 'linux': 'kernel', 350 'linux16': 'kernel', 351 'initrd': 'initrd', 352 'initrd16': 'initrd', 353 'echo': None, 354 'insmod': None, 355 'search': None} 356 357class Grub2ConfigFile(_GrubConfigFile): 358 def __init__(self, fn = None): 359 _GrubConfigFile.__init__(self, fn) 360 361 def new_image(self, title, lines): 362 return Grub2Image(title, lines) 363 364 def parse(self, buf = None): 365 if buf is None: 366 if self.filename is None: 367 raise ValueError, "No config file defined to parse!" 368 369 f = open(self.filename, 'r') 370 lines = f.readlines() 371 f.close() 372 else: 373 lines = buf.split("\n") 374 375 in_function = False 376 img = None 377 title = "" 378 menu_level=0 379 for l in lines: 380 l = l.strip() 381 # skip blank lines 382 if len(l) == 0: 383 continue 384 # skip comments 385 if l.startswith('#'): 386 continue 387 388 # skip function declarations 389 if l.startswith('function'): 390 in_function = True 391 continue 392 if in_function: 393 if l.startswith('}'): 394 in_function = False 395 continue 396 397 # new image 398 title_match = re.match('^menuentry ["\'](.*?)["\'] (.*){', l) 399 if title_match: 400 if img is not None: 401 raise RuntimeError, "syntax error: cannot nest menuentry (%d %s)" % (len(img),img) 402 img = [] 403 title = title_match.group(1) 404 continue 405 406 if l.startswith("submenu"): 407 menu_level += 1 408 continue 409 410 if l.startswith("}"): 411 if img is None: 412 if menu_level > 0: 413 menu_level -= 1 414 continue 415 else: 416 raise RuntimeError, "syntax error: closing brace without menuentry" 417 418 self.add_image(Grub2Image(title, img)) 419 img = None 420 continue 421 422 if img is not None: 423 img.append(l) 424 continue 425 426 (com, arg) = grub_exact_split(l, 2) 427 428 if com == "set": 429 (com,arg) = grub2_handle_set(arg) 430 431 if self.commands.has_key(com): 432 if self.commands[com] is not None: 433 arg_strip = arg.strip() 434 if arg_strip == "${saved_entry}" or arg_strip == "${next_entry}": 435 logging.warning("grub2's saved_entry/next_entry not supported") 436 arg = "0" 437 setattr(self, self.commands[com], arg_strip) 438 else: 439 logging.info("Ignored directive %s" %(com,)) 440 elif com.startswith('set:'): 441 pass 442 else: 443 logging.warning("Unknown directive %s" %(com,)) 444 445 if img is not None: 446 raise RuntimeError, "syntax error: end of file with open menuentry(%d %s)" % (len(img),img) 447 448 if self.hasPassword(): 449 self.setPasswordAccess(False) 450 451 commands = {'set:default': 'default', 452 'set:root': 'root', 453 'set:timeout': 'timeout', 454 'terminal': None, 455 'insmod': None, 456 'load_env': None, 457 'save_env': None, 458 'search': None, 459 'if': None, 460 'fi': None, 461 } 462 463if __name__ == "__main__": 464 if len(sys.argv) < 3: 465 raise RuntimeError, "Need a grub version (\"grub\" or \"grub2\") and a grub.conf or grub.cfg to read" 466 if sys.argv[1] == "grub": 467 g = GrubConfigFile(sys.argv[2]) 468 elif sys.argv[1] == "grub2": 469 g = Grub2ConfigFile(sys.argv[2]) 470 else: 471 raise RuntimeError, "Unknown config type %s" % sys.argv[1] 472 for i in g.images: 473 print i #, i.title, i.root, i.kernel, i.args, i.initrd 474