OSDN Git Service

2020.05.10 update
[rebornos/cnchi-gnome-osdn.git] / Cnchi / grub2.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 #
4 # grub2.py
5 #
6 # Copyright © 2013-2019 RebornOS
7 #
8 # This file is part of Cnchi.
9 #
10 # Cnchi is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 3 of the License, or
13 # (at your option) any later version.
14 #
15 # Cnchi is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 # GNU General Public License for more details.
19 #
20 # The following additional terms are in effect as per Section 7 of the license:
21 #
22 # The preservation of all legal notices and author attributions in
23 # the material or in the Appropriate Legal Notices displayed
24 # by works containing it is required.
25 #
26 # You should have received a copy of the GNU General Public License
27 # along with Cnchi; If not, see <http://www.gnu.org/licenses/>.
28
29
30 """ GRUB2 bootloader installation """
31
32 import logging
33 import os
34 import shutil
35 import subprocess
36 import re
37 import threading
38 import time
39
40 try:
41     import parted3.fs_module as fs
42     from installation import special_dirs
43     from misc.run_cmd import call, chroot_call
44     from misc.extra import random_generator
45 except ImportError:
46     pass
47
48
49 # When testing, no _() is available
50 try:
51     _("")
52 except NameError as err:
53     def _(message):
54         return message
55
56
57 class Grub2(object):
58     """ Class to perform boot loader installation """
59
60     def __init__(self, dest_dir, settings, uuids):
61         self.dest_dir = dest_dir
62         self.settings = settings
63         self.uuids = uuids
64
65     def install(self):
66         """ Install Grub2 bootloader """
67         self.modify_grub_default()
68         self.prepare_grub_d()
69
70         if os.path.exists('/sys/firmware/efi'):
71             logging.debug("Cnchi will install the Grub2 (efi) loader")
72             self.install_efi()
73         else:
74             logging.debug("Cnchi will install the Grub2 (bios) loader")
75             self.install_bios()
76
77         self.check_root_uuid_in_grub()
78
79     def check_root_uuid_in_grub(self):
80         """ Checks grub.cfg for correct root UUID """
81         if self.settings.get("zfs"):
82             # No root uuid checking if using zfs
83             return
84
85         if "/" not in self.uuids:
86             logging.warning(
87                 "Root uuid variable is not set. I can't check root UUID"
88                 "in grub.cfg, let's hope it's ok")
89             return
90
91         ruuid_str = 'root=UUID={0}'.format(self.uuids["/"])
92
93         cmdline_linux = self.settings.get('GRUB_CMDLINE_LINUX')
94         if cmdline_linux is None:
95             cmdline_linux = ""
96
97         cmdline_linux_default = self.settings.get('GRUB_CMDLINE_LINUX_DEFAULT')
98         if cmdline_linux_default is None:
99             cmdline_linux_default = ""
100
101         boot_command = 'linux /vmlinuz-linux {0} {1} {2}\n'.format(
102             ruuid_str,
103             cmdline_linux,
104             cmdline_linux_default)
105
106         pattern = re.compile(
107             "menuentry 'RebornOS'[\s\S]*initramfs-linux.img\n}")
108
109         cfg = os.path.join(self.dest_dir, "boot/grub/grub.cfg")
110         with open(cfg) as grub_file:
111             parse = grub_file.read()
112
113         if not self.settings.get('use_luks') and ruuid_str not in parse:
114             entry = pattern.search(parse)
115             if entry:
116                 logging.debug(
117                     "Wrong uuid in grub.cfg, Cnchi will try to fix it.")
118                 new_entry = re.sub(
119                     "linux\t/vmlinuz.*quiet\n",
120                     boot_command,
121                     entry.group())
122                 parse = parse.replace(entry.group(), new_entry)
123
124                 with open(cfg, 'w') as grub_file:
125                     grub_file.write(parse)
126
127     def modify_grub_default(self):
128         """ If using LUKS as root, we need to modify GRUB_CMDLINE_LINUX
129             GRUB_CMDLINE_LINUX : Command-line arguments to add to menu entries
130             for the Linux kernel.
131             GRUB_CMDLINE_LINUX_DEFAULT : Unless ‘GRUB_DISABLE_RECOVERY’ is set
132             to ‘true’, two menu entries will be generated for each Linux kernel:
133             one default entry and one entry for recovery mode. This option lists
134             command-line arguments to add only to the default menu entry, after
135             those listed in ‘GRUB_CMDLINE_LINUX’. """
136
137         plymouth_bin = os.path.join(self.dest_dir, "usr/bin/plymouth")
138         cmd_linux_default = "quiet"
139         cmd_linux = ""
140
141         # https://www.kernel.org/doc/Documentation/kernel-parameters.txt
142         # cmd_linux_default : quiet splash resume=UUID=ABC zfs=ABC
143
144         if os.path.exists(plymouth_bin):
145             cmd_linux_default += " splash"
146
147         # resume does not work in zfs (or so it seems)
148         if "swap" in self.uuids and not self.settings.get("zfs"):
149             cmd_linux_default += " resume=UUID={0}".format(self.uuids["swap"])
150
151         if self.settings.get("zfs"):
152             zfs_pool_name = self.settings.get("zfs_pool_name")
153             cmd_linux += " zfs={0}".format(zfs_pool_name)
154
155         if self.settings.get('use_luks'):
156             # When using separate boot partition,
157             # add GRUB_ENABLE_CRYPTODISK to grub.cfg
158             if self.uuids["/"] != self.uuids["/boot"]:
159                 self.set_grub_option("GRUB_ENABLE_CRYPTODISK", "y")
160
161             # Let GRUB automatically add the kernel parameters for
162             # root encryption
163             luks_root_volume = self.settings.get('luks_root_volume')
164             logging.debug("Luks Root Volume: %s", luks_root_volume)
165
166             if (self.settings.get("partition_mode") == "advanced" and
167                     self.settings.get('use_luks_in_root')):
168                 # In advanced, if using luks in root device,
169                 # we store root device it in luks_root_device var
170                 root_device = self.settings.get('luks_root_device')
171                 self.uuids["/"] = fs.get_uuid(root_device)
172
173             cmd_linux += " cryptdevice=/dev/disk/by-uuid/{0}:{1}".format(
174                 self.uuids["/"],
175                 luks_root_volume)
176
177             if self.settings.get("luks_root_password") == "":
178                 # No luks password, so user wants to use a keyfile
179                 cmd_linux += " cryptkey=/dev/disk/by-uuid/{0}:ext2:/.keyfile-root".format(
180                     self.uuids["/boot"])
181
182         # Remove leading/ending spaces
183         cmd_linux_default = cmd_linux_default.strip()
184         cmd_linux = cmd_linux.strip()
185
186         # Modify /etc/default/grub
187         self.set_grub_option(
188             "GRUB_THEME", "/boot/grub/themes/Vimix/theme.txt")
189         self.set_grub_option("GRUB_DISTRIBUTOR", "RebornOS")
190         self.set_grub_option("GRUB_CMDLINE_LINUX_DEFAULT", cmd_linux_default)
191         self.set_grub_option("GRUB_CMDLINE_LINUX", cmd_linux)
192
193         # Also store grub line in settings, we'll use it later in check_root_uuid_in_grub()
194         try:
195             self.settings.set('GRUB_CMDLINE_LINUX', cmd_linux)
196         except AttributeError:
197             pass
198
199         logging.debug("Grub configuration completed successfully.")
200
201     def set_grub_option(self, option, cmd):
202         """ Changes a grub setup option in /etc/default/grub """
203         try:
204             default_grub_path = os.path.join(
205                 self.dest_dir, "etc/default", "grub")
206             default_grub_lines = []
207
208             with open(default_grub_path, 'r', newline='\n') as grub_file:
209                 default_grub_lines = [x for x in grub_file.readlines()]
210
211             with open(default_grub_path, 'w', newline='\n') as grub_file:
212                 param_in_file = False
213                 param_to_look_for = option + '='
214                 for line in default_grub_lines:
215                     if param_to_look_for in line:
216                         # Option was already in file, update it
217                         line = '{0}="{1}"\n'.format(option, cmd)
218                         param_in_file = True
219                     grub_file.write(line)
220
221                 if not param_in_file:
222                     # Option was not found. Thus, append new option
223                     grub_file.write('\n{0}="{1}"\n'.format(option, cmd))
224
225             logging.debug('Set %s="%s" in /etc/default/grub', option, cmd)
226         except FileNotFoundError as ex:
227             logging.error(ex)
228         except Exception as ex:
229             tpl1 = "Can't modify {0}".format(default_grub_path)
230             tpl2 = "An exception of type {0} occured. Arguments:\n{1!r}"
231             template = '{0} {1}'.format(tpl1, tpl2)
232             message = template.format(type(ex).__name__, ex.args)
233             logging.error(message)
234
235     def prepare_grub_d(self):
236         """ Copies 10_antergos script into /etc/grub.d/ """
237         grub_d_dir = os.path.join(self.dest_dir, "etc/grub.d")
238         script_dir = os.path.join(self.settings.get("cnchi"), "scripts")
239         script = "10_antergos"
240
241         os.makedirs(grub_d_dir, mode=0o755, exist_ok=True)
242
243         script_path = os.path.join(script_dir, script)
244         if os.path.exists(script_path):
245             try:
246                 shutil.copy2(script_path, grub_d_dir)
247                 os.chmod(os.path.join(grub_d_dir, script), 0o755)
248             except FileNotFoundError:
249                 logging.debug("Could not copy %s to grub.d", script)
250             except FileExistsError:
251                 pass
252         else:
253             logging.warning("Can't find script %s", script_path)
254
255     def grub_ripper(self):
256         while True:
257             time.sleep(10)
258             try:
259                 ret = subprocess.check_output(
260                     ['pidof', 'grub-mount']).decode().strip()
261                 if ret:
262                     subprocess.check_output(['kill', '-9', ret.split()[0]])
263                 else:
264                     break
265             except subprocess.CalledProcessError as err:
266                 logging.warning("Error running %s: %s", err.cmd, err.output)
267                 break
268
269     def run_mkconfig(self):
270         """ Create grub.cfg file using grub-mkconfig """
271         logging.debug("Generating grub.cfg...")
272
273         # Make sure that /dev and others are mounted (binded).
274         special_dirs.mount(self.dest_dir)
275
276         # Hack to kill grub-mount hanging
277         threading.Thread(target=self.grub_ripper).start()
278
279         # Add -l option to os-prober's umount call so that it does not hang
280         self.apply_osprober_patch()
281         logging.debug("Running grub-mkconfig...")
282         locale = self.settings.get("locale")
283         cmd = 'LANG={0} grub-mkconfig -o /boot/grub/grub.cfg'.format(locale)
284         cmd_sh = ['sh', '-c', cmd]
285         if not chroot_call(cmd_sh, self.dest_dir, timeout=300):
286             msg = ("grub-mkconfig does not respond. Killing grub-mount and"
287                    "os-prober so we can continue.")
288             logging.error(msg)
289             call(['killall', 'grub-mount'])
290             call(['killall', 'os-prober'])
291
292     def install_bios(self):
293         """ Install Grub2 bootloader in a BIOS system """
294         grub_location = self.settings.get('bootloader_device')
295         txt = _("Installing GRUB(2) BIOS boot loader in {0}").format(
296             grub_location)
297         logging.info(txt)
298
299         # /dev and others need to be mounted (binded).
300         # We call mount_special_dirs here just to be sure
301         special_dirs.mount(self.dest_dir)
302
303         grub_install = ['grub-install',
304                         '--directory=/usr/lib/grub/i386-pc',
305                         '--target=i386-pc',
306                         '--boot-directory=/boot',
307                         '--recheck']
308
309         # Use --force when installing in /dev/sdXY or in /dev/mmcblk
310         if len(grub_location) > len("/dev/sdX"):
311             grub_install.append("--force")
312
313         grub_install.append(grub_location)
314
315         chroot_call(grub_install, self.dest_dir)
316
317         self.install_locales()
318
319         self.run_mkconfig()
320
321         grub_cfg_path = os.path.join(self.dest_dir, "boot/grub/grub.cfg")
322         with open(grub_cfg_path) as grub_cfg:
323             if "Antergos" in grub_cfg.read():
324                 txt = _("GRUB(2) BIOS has been successfully installed.")
325                 logging.info(txt)
326                 self.settings.set('bootloader_installation_successful', True)
327             else:
328                 txt = _("ERROR installing GRUB(2) BIOS.")
329                 logging.warning(txt)
330                 self.settings.set('bootloader_installation_successful', False)
331
332     def install_efi(self):
333         """ Install Grub2 bootloader in a UEFI system """
334         uefi_arch = "x86_64"
335         spec_uefi_arch = "x64"
336         spec_uefi_arch_caps = "X64"
337         fpath = '/install/boot/efi/EFI/RebornOS'
338         bootloader_id = 'RebornOS' if not os.path.exists(fpath) else \
339             'RebornOS_{0}'.format(random_generator())
340
341         # grub2 in efi needs efibootmgr
342         if not os.path.exists("/usr/bin/efibootmgr"):
343             txt = _(
344                 "Please install efibootmgr package to install Grub2 for x86_64-efi platform.")
345             logging.warning(txt)
346             txt = _("GRUB(2) will NOT be installed")
347             logging.warning(txt)
348             self.settings.set('bootloader_installation_successful', False)
349             return
350
351         txt = _("Installing GRUB(2) UEFI {0} boot loader").format(uefi_arch)
352         logging.info(txt)
353
354         grub_install = [
355             'grub-install',
356             '--target={0}-efi'.format(uefi_arch),
357             '--efi-directory=/install/boot/efi',
358             '--bootloader-id={0}'.format(bootloader_id),
359             '--boot-directory=/install/boot',
360             '--recheck']
361         load_module = ['modprobe', '-a', 'efivarfs']
362
363         call(load_module, timeout=15)
364         call(grub_install, timeout=120)
365
366         self.install_locales()
367
368         # Copy grub into dirs known to be used as default by some OEMs
369         # if they do not exist yet.
370         grub_defaults = [
371             os.path.join(
372                 self.dest_dir,
373                 "boot/efi/EFI/BOOT",
374                 "BOOT{0}.efi".format(spec_uefi_arch_caps)),
375             os.path.join(
376                 self.dest_dir,
377                 "boot/efi/EFI/Microsoft/Boot",
378                 "bootmgfw.efi")]
379
380         grub_path = os.path.join(
381             self.dest_dir,
382             "boot/efi/EFI/antergos_grub",
383             "grub{0}.efi".format(spec_uefi_arch))
384
385         for grub_default in grub_defaults:
386             path = grub_default.split()[0]
387             if not os.path.exists(path):
388                 msg = _("No OEM loader found in %s. Copying Grub(2) into dir.")
389                 logging.info(msg, path)
390                 os.makedirs(path, mode=0o755)
391                 msg_failed = _("Copying Grub(2) into OEM dir failed: %s")
392                 try:
393                     shutil.copy(grub_path, grub_default)
394                 except FileNotFoundError:
395                     logging.warning(msg_failed, _("File not found."))
396                 except FileExistsError:
397                     logging.warning(msg_failed, _("File already exists."))
398                 except Exception as ex:
399                     template = "An exception of type {0} occured. Arguments:\n{1!r}"
400                     message = template.format(type(ex).__name__, ex.args)
401                     logging.error(message)
402
403         self.run_mkconfig()
404
405         paths = [
406             os.path.join(self.dest_dir, "boot/grub/x86_64-efi/core.efi"),
407             os.path.join(
408                 self.dest_dir,
409                 "boot/efi/EFI/{0}".format(bootloader_id),
410                 "grub{0}.efi".format(spec_uefi_arch))]
411
412         exists = True
413         for path in paths:
414             if not os.path.exists(path):
415                 exists = False
416                 logging.debug("Path '%s' doesn't exist, when it should", path)
417
418         if exists:
419             logging.info("GRUB(2) UEFI install completed successfully")
420             self.settings.set('bootloader_installation_successful', True)
421         else:
422             logging.warning(
423                 "GRUB(2) UEFI install may not have completed successfully.")
424             self.settings.set('bootloader_installation_successful', False)
425
426     def apply_osprober_patch(self):
427         """ Adds -l option to os-prober's umount call so that it does not hang """
428         osp_path = os.path.join(
429             self.dest_dir,
430             "usr/lib/os-probes/50mounted-tests")
431         if os.path.exists(osp_path):
432             with open(osp_path) as osp:
433                 text = osp.read().replace("umount", "umount -l")
434             with open(osp_path, 'w') as osp:
435                 osp.write(text)
436             logging.debug("50mounted-tests file patched successfully")
437         else:
438             logging.warning("Failed to patch 50mounted-tests, file not found.")
439
440     def install_locales(self):
441         """ Install Grub2 locales """
442         logging.debug("Installing Grub2 locales.")
443         dest_locale_dir = os.path.join(self.dest_dir, "boot/grub/locale")
444
445         os.makedirs(dest_locale_dir, mode=0o755, exist_ok=True)
446
447         grub_mo = os.path.join(
448             self.dest_dir,
449             "usr/share/locale/en@quot/LC_MESSAGES/grub.mo")
450
451         try:
452             shutil.copy2(grub_mo, os.path.join(dest_locale_dir, "en.mo"))
453         except FileNotFoundError:
454             logging.warning("Can't install GRUB(2) locale.")
455         except FileExistsError:
456             # Ignore if already exists
457             pass
458
459
460 if __name__ == '__main__':
461     os.makedirs("/install/etc/default", mode=0o755, exist_ok=True)
462     shutil.copy2("/etc/default/grub", "/install/etc/default/grub")
463     dest_dir = "/install"
464     settings = {}
465     settings["zfs"] = True
466     settings["zfs_pool_name"] = "Reborn_d3sq"
467     settings["use_luks"] = True
468     uuids = {}
469     uuids["/"] = "ABCD"
470     uuids["/boot"] = "ZXCV"
471     grub2 = Grub2(dest_dir, settings, uuids)
472     grub2.modify_grub_default()