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