OSDN Git Service

Initial commit
[rebornos/cnchi-gnome-mac-osdn.git] / Cnchi / install.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 #
4 # install.py
5 #
6 # Copyright © 2013-2018 Antergos
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 2 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 # You should have received a copy of the GNU General Public License
21 # along with Cnchi; if not, write to the Free Software
22 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
23 # MA 02110-1301, USA.
24
25 """ Installation process module. """
26
27 import glob
28 import logging
29 import os
30 import shutil
31 import sys
32
33 from mako.template import Template
34
35 from download import download
36
37 from installation import special_dirs
38 from installation import post_install
39 from installation import mount
40
41 import misc.extra as misc
42 from misc.extra import InstallError
43 from misc.run_cmd import call
44 from misc.events import Events
45 import pacman.pac as pac
46
47 import hardware.hardware as hardware
48
49 DEST_DIR = "/install"
50
51 # When testing, no _() is available
52 try:
53     _("")
54 except NameError as err:
55     def _(message):
56         return message
57
58 class Installation():
59     """ Installation process thread class """
60
61     TMP_PACMAN_CONF = "/tmp/pacman.conf"
62
63     def __init__(self, settings, callback_queue, packages, metalinks,
64                  mount_devices, fs_devices, ssd=None, blvm=False):
65         """ Initialize installation class """
66
67         self.settings = settings
68         self.events = Events(callback_queue)
69         self.packages = packages
70         self.metalinks = metalinks
71
72         self.method = self.settings.get('partition_mode')
73
74         self.desktop = self.settings.get('desktop').lower()
75
76         # This flag tells us if there is a lvm partition (from advanced install)
77         # If it's true we'll have to add the 'lvm2' hook to mkinitcpio
78         self.blvm = blvm
79
80         if ssd is not None:
81             self.ssd = ssd
82         else:
83             self.ssd = {}
84
85         self.mount_devices = mount_devices
86
87         self.fs_devices = fs_devices
88
89         self.running = True
90         self.error = False
91
92         self.auto_device = ""
93         self.packages = packages
94         self.pacman = None
95
96         self.pacman_cache_dir = ''
97
98         # Cnchi will store here info (packages needed, post install actions, ...)
99         # for the detected hardware
100         self.hardware_install = None
101
102     def queue_fatal_event(self, txt):
103         """ Queues the fatal event and exits process """
104         self.error = True
105         self.running = False
106         self.events.add('error', txt)
107         # self.callback_queue.join()
108         sys.exit(0)
109
110     def mount_partitions(self):
111         """ Do not call this in automatic mode as AutoPartition class mounts
112         the root and boot devices itself. (We call it if using ZFS, though) """
113
114         if os.path.exists(DEST_DIR) and not self.method == "zfs":
115             # If we're recovering from a failed/stoped install, there'll be
116             # some mounted directories. Try to unmount them first.
117             # We use unmount_all_in_directory from auto_partition to do this.
118             # ZFS already mounts everything automagically (except /boot that
119             # is not in zfs)
120             mount.unmount_all_in_directory(DEST_DIR)
121
122         # NOTE: Advanced method formats root by default in advanced.py
123         if "/" in self.mount_devices:
124             root_partition = self.mount_devices["/"]
125         else:
126             root_partition = ""
127
128         # Boot partition
129         if "/boot" in self.mount_devices:
130             boot_partition = self.mount_devices["/boot"]
131         else:
132             boot_partition = ""
133
134         # EFI partition
135         if "/boot/efi" in self.mount_devices:
136             efi_partition = self.mount_devices["/boot/efi"]
137         else:
138             efi_partition = ""
139
140         # Swap partition
141         if "swap" in self.mount_devices:
142             swap_partition = self.mount_devices["swap"]
143         else:
144             swap_partition = ""
145
146         # Mount root partition
147         if self.method == "zfs":
148             # Mount /
149             logging.debug("ZFS: Mounting root")
150             cmd = ["zfs", "mount", "-a"]
151             call(cmd)
152         elif root_partition:
153             txt = "Mounting root partition {0} into {1} directory".format(
154                 root_partition, DEST_DIR)
155             logging.debug(txt)
156             cmd = ['mount', root_partition, DEST_DIR]
157             call(cmd, fatal=True)
158
159         # We also mount the boot partition if it's needed
160         boot_path = os.path.join(DEST_DIR, "boot")
161         os.makedirs(boot_path, mode=0o755, exist_ok=True)
162         if boot_partition:
163             txt = _("Mounting boot partition {0} into {1} directory").format(
164                 boot_partition, boot_path)
165             logging.debug(txt)
166             cmd = ['mount', boot_partition, boot_path]
167             call(cmd, fatal=True)
168
169         if self.method == "zfs" and efi_partition:
170             # In automatic zfs mode, it could be that we have a specific EFI
171             # partition (different from /boot partition). This happens if using
172             # EFI and grub2 bootloader
173             efi_path = os.path.join(DEST_DIR, "boot", "efi")
174             os.makedirs(efi_path, mode=0o755, exist_ok=True)
175             txt = _("Mounting EFI partition {0} into {1} directory").format(
176                 efi_partition, efi_path)
177             logging.debug(txt)
178             cmd = ['mount', efi_partition, efi_path]
179             call(cmd, fatal=True)
180
181         # In advanced mode, mount all partitions (root and boot are already mounted)
182         if self.method == 'advanced':
183             for path in self.mount_devices:
184                 if path == "":
185                     # Ignore devices without a mount path
186                     continue
187
188                 mount_part = self.mount_devices[path]
189
190                 if mount_part not in [root_partition, boot_partition, swap_partition]:
191                     if path[0] == '/':
192                         path = path[1:]
193                     mount_dir = os.path.join(DEST_DIR, path)
194                     try:
195                         os.makedirs(mount_dir, mode=0o755, exist_ok=True)
196                         txt = _("Mounting partition {0} into {1} directory")
197                         txt = txt.format(mount_part, mount_dir)
198                         logging.debug(txt)
199                         cmd = ['mount', mount_part, mount_dir]
200                         call(cmd)
201                     except OSError:
202                         logging.warning(
203                             "Could not create %s directory", mount_dir)
204                 elif mount_part == swap_partition:
205                     logging.debug("Activating swap in %s", mount_part)
206                     cmd = ['swapon', swap_partition]
207                     call(cmd)
208
209     @misc.raise_privileges
210     def run(self):
211         """ Run installation """
212
213         # From this point, on a warning situation, Cnchi should try to continue,
214         # so we need to catch the exception here. If we don't catch the exception
215         # here, it will be catched in run() and managed as a fatal error.
216         # On the other hand, if we want to clarify the exception message we can
217         # catch it here and then raise an InstallError exception.
218
219         if not os.path.exists(DEST_DIR):
220             os.makedirs(DEST_DIR, mode=0o755, exist_ok=True)
221
222         # Make sure the antergos-repo-priority package's alpm hook doesn't run.
223         if not os.environ.get('CNCHI_RUNNING', False):
224             os.environ['CNCHI_RUNNING'] = 'True'
225
226         msg = _("Installing using the '{0}' method").format(self.method)
227         self.events.add('info', msg)
228
229         # Mount needed partitions (in automatic it's already done)
230         if self.method in ['alongside', 'advanced', 'zfs']:
231             self.mount_partitions()
232
233         # Nasty workaround:
234         # If pacman was stoped and /var is in another partition than root
235         # (so as to be able to resume install), database lock file will still
236         # be in place. We must delete it or this new installation will fail
237         db_lock = os.path.join(DEST_DIR, "var/lib/pacman/db.lck")
238         if os.path.exists(db_lock):
239             os.remove(db_lock)
240             logging.debug("%s deleted", db_lock)
241
242         # Create some needed folders
243         folders = [
244             os.path.join(DEST_DIR, 'var/lib/pacman'),
245             os.path.join(DEST_DIR, 'etc/pacman.d/gnupg'),
246             os.path.join(DEST_DIR, 'var/log')]
247
248         for folder in folders:
249             os.makedirs(folder, mode=0o755, exist_ok=True)
250
251         # If kernel images exists in /boot they are most likely from a failed
252         # install attempt and need to be removed otherwise pyalpm will raise a
253         # fatal exception later on.
254         kernel_imgs = (
255             "/install/boot/vmlinuz-linux",
256             "/install/boot/vmlinuz-linux-lts",
257             "/install/boot/initramfs-linux.img",
258             "/install/boot/initramfs-linux-fallback.img",
259             "/install/boot/initramfs-linux-lts.img",
260             "/install/boot/initramfs-linux-lts-fallback.img")
261
262         for img in kernel_imgs:
263             if os.path.exists(img):
264                 os.remove(img)
265
266         # If intel-ucode or grub2-theme-antergos files exist in /boot they are
267         # most likely either from another linux installation or from a failed
268         # install attempt and need to be removed otherwise pyalpm will refuse
269         # to install those packages (like above)
270         if os.path.exists('/install/boot/intel-ucode.img'):
271             logging.debug("Removing previous intel-ucode.img file found in /boot")
272             os.remove('/install/boot/intel-ucode.img')
273         if os.path.exists('/install/boot/grub/themes/Antergos-Default'):
274             logging.debug("Removing previous Antergos-Default grub2 theme found in /boot")
275             shutil.rmtree('/install/boot/grub/themes/Antergos-Default')
276
277         logging.debug("Preparing pacman...")
278         self.prepare_pacman()
279         logging.debug("Pacman ready")
280
281         # Run driver's pre-install scripts
282         try:
283             logging.debug("Running hardware drivers pre-install jobs...")
284             proprietary = self.settings.get('feature_graphic_drivers')
285             self.hardware_install = hardware.HardwareInstall(
286                 self.settings.get("cnchi"),
287                 use_proprietary_graphic_drivers=proprietary)
288             self.hardware_install.pre_install(DEST_DIR)
289         except Exception as ex:
290             template = "Error in hardware module. " \
291                        "An exception of type {0} occured. Arguments:\n{1!r}"
292             message = template.format(type(ex).__name__, ex.args)
293             logging.error(message)
294
295         logging.debug("Downloading packages...")
296         self.download_packages()
297
298         # This mounts (binds) /dev and others to /DEST_DIR/dev and others
299         special_dirs.mount(DEST_DIR)
300
301         logging.debug("Installing packages...")
302         self.install_packages()
303
304         logging.debug("Configuring system...")
305         post = post_install.PostInstallation(
306             self.settings,
307             self.events.queue,
308             self.mount_devices,
309             self.fs_devices,
310             self.ssd,
311             self.blvm)
312         post.configure_system(self.hardware_install)
313
314         # This unmounts (unbinds) /dev and others to /DEST_DIR/dev and others
315         special_dirs.umount(DEST_DIR)
316
317         # Run postinstall script (we need special dirs unmounted but dest_dir mounted!)
318         logging.debug("Running postinstall.sh script...")
319         post.set_desktop_settings()
320
321         # Copy installer log to the new installation
322         logging.debug("Copying install log to /var/log/cnchi")
323         post.copy_logs()
324
325         self.events.add('pulse', 'stop')
326         self.events.add('progress_bar', 'hide')
327
328         # Finally, try to unmount DEST_DIR
329         mount.unmount_all_in_directory(DEST_DIR)
330
331         self.running = False
332
333         # Installation finished successfully
334         self.events.add('finished', _("Installation finished"))
335         self.error = False
336         return True
337
338     def download_packages(self):
339         """ Downloads necessary packages """
340
341         self.pacman_cache_dir = os.path.join(DEST_DIR, 'var/cache/pacman/pkg')
342
343         pacman_conf = {}
344         pacman_conf['file'] = Installation.TMP_PACMAN_CONF
345         pacman_conf['cache'] = self.pacman_cache_dir
346
347         download_packages = download.DownloadPackages(
348             package_names=self.packages,
349             pacman_conf=pacman_conf,
350             settings=self.settings,
351             callback_queue=self.events.queue)
352
353         # Metalinks have already been calculated before,
354         # When downloadpackages class has been called in process.py to test
355         # that Cnchi was able to create it before partitioning/formatting
356         download_packages.start_download(self.metalinks)
357
358     def create_pacman_conf_file(self):
359         """ Creates a temporary pacman.conf """
360         myarch = os.uname()[-1]
361         msg = _("Creating a temporary pacman.conf for {0} architecture").format(
362             myarch)
363         logging.debug(msg)
364
365         # Template functionality. Needs Mako (see http://www.makotemplates.org/)
366         template_file_name = os.path.join(
367             self.settings.get('data'), 'pacman.tmpl')
368         file_template = Template(filename=template_file_name)
369         file_rendered = file_template.render(
370             destDir=DEST_DIR,
371             arch=myarch,
372             desktop=self.desktop)
373         filename = Installation.TMP_PACMAN_CONF
374         dirname = os.path.dirname(filename)
375         os.makedirs(dirname, mode=0o755, exist_ok=True)
376         with open(filename, "w") as my_file:
377             my_file.write(file_rendered)
378
379     def prepare_pacman(self):
380         """ Configures pacman and syncs db on destination system """
381
382         self.create_pacman_conf_file()
383
384         msg = _("Updating package manager security. Please wait...")
385         self.events.add('info', msg)
386         self.prepare_pacman_keyring()
387
388         # Init pyalpm
389         try:
390             self.pacman = pac.Pac(
391                 Installation.TMP_PACMAN_CONF, self.events.queue)
392         except Exception as ex:
393             self.pacman = None
394             template = ("Can't initialize pyalpm. "
395                         "An exception of type {0} occured. Arguments:\n{1!r}")
396             message = template.format(type(ex).__name__, ex.args)
397             logging.error(message)
398             raise InstallError(message)
399
400         # Refresh pacman databases
401         if not self.pacman.refresh():
402             logging.error("Can't refresh pacman databases.")
403             raise InstallError(_("Can't refresh pacman databases."))
404
405     @staticmethod
406     def prepare_pacman_keyring():
407         """ Add gnupg pacman files to installed system """
408
409         dirs = ["var/cache/pacman/pkg", "var/lib/pacman"]
410         for pacman_dir in dirs:
411             mydir = os.path.join(DEST_DIR, pacman_dir)
412             os.makedirs(mydir, mode=0o755, exist_ok=True)
413
414         # Be sure that haveged is running (liveCD)
415         # haveged is a daemon that generates system entropy; this speeds up
416         # critical operations in cryptographic programs such as gnupg
417         # (including the generation of new keyrings)
418         cmd = ["systemctl", "start", "haveged"]
419         call(cmd)
420
421         # Delete old gnupg files
422         dest_path = os.path.join(DEST_DIR, "etc/pacman.d/gnupg")
423         cmd = ["rm", "-rf", dest_path]
424         call(cmd)
425         os.mkdir(dest_path)
426
427         # Tell pacman-key to regenerate gnupg files
428         # Initialize the pacman keyring
429         cmd = ["pacman-key", "--init", "--gpgdir", dest_path]
430         call(cmd)
431
432         # Load the signature keys
433         # Delete antergos dest_pat, add rebornos dest-path (Rafael)
434         cmd = ["pacman-key", "--populate", "--gpgdir",
435                dest_path, "archlinux", "rebornos"]
436         call(cmd)
437
438         # path = os.path.join(DEST_DIR, "root/.gnupg/dirmngr_ldapservers.conf")
439         # Run dirmngr
440         # https://bbs.archlinux.org/viewtopic.php?id=190380
441         with open(os.devnull, 'r') as dev_null:
442             cmd = ["dirmngr"]
443             call(cmd, stdin=dev_null)
444
445         # Refresh and update the signature keys
446         # cmd = ["pacman-key", "--refresh-keys", "--gpgdir", dest_path]
447         # call(cmd)
448
449     def delete_stale_pkgs(self, stale_pkgs):
450         """ Failure might be due to stale cached packages. Delete them. """
451         for stale_pkg in stale_pkgs:
452             filepath = os.path.join(self.pacman_cache_dir, stale_pkg)
453             to_delete = glob.glob(filepath + '***') if filepath else []
454             if to_delete and len(to_delete) <= 20:
455                 for fpath in to_delete:
456                     try:
457                         os.remove(fpath)
458                     except OSError as err:
459                         logging.error(err)
460
461     @staticmethod
462     def use_build_server_repo():
463         """ Setup pacman.conf to use build server repository """
464         with open('/etc/pacman.conf', 'r') as pacman_conf:
465             contents = pacman_conf.readlines()
466         with open('/etc/pacman.conf', 'w') as new_pacman_conf:
467             for line in contents:
468                 if 'reborn-mirrorlist' in line:
469                     line = 'Server = https://repo.rebornos.org/RebornOS/'
470                 new_pacman_conf.write(line)
471
472     def install_packages(self):
473         """ Start pacman installation of packages """
474         result = False
475         # This shouldn't be necessary if download.py really downloaded all
476         # needed packages, but it does not do it (why?)
477         for cache_dir in self.settings.get('xz_cache'):
478             self.pacman.handle.add_cachedir(cache_dir)
479
480         logging.debug("Installing packages...")
481
482         try:
483             result = self.pacman.install(pkgs=self.packages)
484         except pac.pyalpm.error:
485             pass
486
487         stale_pkgs = self.settings.get('cache_pkgs_md5_check_failed')
488
489         if not result and stale_pkgs and os.path.exists(self.pacman_cache_dir):
490             # Failure might be due to stale cached packages. Delete them and try again.
491             logging.warning(
492                 "Can't install necessary packages. Let's try again deleting stale packages first.")
493             self.delete_stale_pkgs(stale_pkgs)
494             self.pacman.refresh()
495             try:
496                 result = self.pacman.install(pkgs=self.packages)
497             except pac.pyalpm.error:
498                 pass
499
500         if not result:
501             # Failure might be due to antergos mirror issues. Try using build server repo.
502             logging.warning(
503                 "Can't install necessary packages. Let's try again using a tier 1 mirror.")
504             self.use_build_server_repo()
505             self.pacman.refresh()
506             try:
507                 result = self.pacman.install(pkgs=self.packages)
508             except pac.pyalpm.error:
509                 pass
510
511         if not result:
512             txt = _("Can't install necessary packages. Cnchi can't continue.")
513             raise InstallError(txt)
514
515         # All downloading and installing has been done, so we hide progress bar
516         self.events.add('progress_bar', 'hide')
517
518     def is_running(self):
519         """ Checks if thread is running """
520         return self.running
521
522     def is_ok(self):
523         """ Checks if an error has been issued """
524         return not self.error