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