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