2 # -*- coding: utf-8 -*-
6 # Copyright © 2013-2018 Antergos
8 # This file is part of Cnchi.
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.
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.
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,
25 """ Installation process module. """
33 from mako.template import Template
35 from download import download
37 from installation import special_dirs
38 from installation import post_install
39 from installation import mount
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
47 import hardware.hardware as hardware
51 # When testing, no _() is available
54 except NameError as err:
59 """ Installation process thread class """
61 TMP_PACMAN_CONF = "/tmp/pacman.conf"
63 def __init__(self, settings, callback_queue, packages, metalinks,
64 mount_devices, fs_devices, ssd=None, blvm=False):
65 """ Initialize installation class """
67 self.settings = settings
68 self.events = Events(callback_queue)
69 self.packages = packages
70 self.metalinks = metalinks
72 self.method = self.settings.get('partition_mode')
74 self.desktop = self.settings.get('desktop').lower()
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
85 self.mount_devices = mount_devices
87 self.fs_devices = fs_devices
93 self.packages = packages
96 self.pacman_cache_dir = ''
98 # Cnchi will store here info (packages needed, post install actions, ...)
99 # for the detected hardware
100 self.hardware_install = None
102 def queue_fatal_event(self, txt):
103 """ Queues the fatal event and exits process """
106 self.events.add('error', txt)
107 # self.callback_queue.join()
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) """
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
120 mount.unmount_all_in_directory(DEST_DIR)
122 # NOTE: Advanced method formats root by default in advanced.py
123 if "/" in self.mount_devices:
124 root_partition = self.mount_devices["/"]
129 if "/boot" in self.mount_devices:
130 boot_partition = self.mount_devices["/boot"]
135 if "/boot/efi" in self.mount_devices:
136 efi_partition = self.mount_devices["/boot/efi"]
141 if "swap" in self.mount_devices:
142 swap_partition = self.mount_devices["swap"]
146 # Mount root partition
147 if self.method == "zfs":
149 logging.debug("ZFS: Mounting root")
150 cmd = ["zfs", "mount", "-a"]
153 txt = "Mounting root partition {0} into {1} directory".format(
154 root_partition, DEST_DIR)
156 cmd = ['mount', root_partition, DEST_DIR]
157 call(cmd, fatal=True)
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)
163 txt = _("Mounting boot partition {0} into {1} directory").format(
164 boot_partition, boot_path)
166 cmd = ['mount', boot_partition, boot_path]
167 call(cmd, fatal=True)
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)
178 cmd = ['mount', efi_partition, efi_path]
179 call(cmd, fatal=True)
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:
185 # Ignore devices without a mount path
188 mount_part = self.mount_devices[path]
190 if mount_part not in [root_partition, boot_partition, swap_partition]:
193 mount_dir = os.path.join(DEST_DIR, path)
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)
199 cmd = ['mount', mount_part, mount_dir]
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]
209 @misc.raise_privileges
211 """ Run installation """
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.
219 if not os.path.exists(DEST_DIR):
220 os.makedirs(DEST_DIR, mode=0o755, exist_ok=True)
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'
226 msg = _("Installing using the '{0}' method").format(self.method)
227 self.events.add('info', msg)
229 # Mount needed partitions (in automatic it's already done)
230 if self.method in ['alongside', 'advanced', 'zfs']:
231 self.mount_partitions()
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):
240 logging.debug("%s deleted", db_lock)
242 # Create some needed 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')]
248 for folder in folders:
249 os.makedirs(folder, mode=0o755, exist_ok=True)
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.
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")
262 for img in kernel_imgs:
263 if os.path.exists(img):
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')
277 logging.debug("Preparing pacman...")
278 self.prepare_pacman()
279 logging.debug("Pacman ready")
281 # Run driver's pre-install scripts
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)
295 logging.debug("Downloading packages...")
296 self.download_packages()
298 # This mounts (binds) /dev and others to /DEST_DIR/dev and others
299 special_dirs.mount(DEST_DIR)
301 logging.debug("Installing packages...")
302 self.install_packages()
304 logging.debug("Configuring system...")
305 post = post_install.PostInstallation(
312 post.configure_system(self.hardware_install)
314 # This unmounts (unbinds) /dev and others to /DEST_DIR/dev and others
315 special_dirs.umount(DEST_DIR)
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()
321 # Copy installer log to the new installation
322 logging.debug("Copying install log to /var/log/cnchi")
325 self.events.add('pulse', 'stop')
326 self.events.add('progress_bar', 'hide')
328 # Finally, try to unmount DEST_DIR
329 mount.unmount_all_in_directory(DEST_DIR)
333 # Installation finished successfully
334 self.events.add('finished', _("Installation finished"))
338 def download_packages(self):
339 """ Downloads necessary packages """
341 self.pacman_cache_dir = os.path.join(DEST_DIR, 'var/cache/pacman/pkg')
344 pacman_conf['file'] = Installation.TMP_PACMAN_CONF
345 pacman_conf['cache'] = self.pacman_cache_dir
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)
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)
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(
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(
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)
379 def prepare_pacman(self):
380 """ Configures pacman and syncs db on destination system """
382 self.create_pacman_conf_file()
384 msg = _("Updating package manager security. Please wait...")
385 self.events.add('info', msg)
386 self.prepare_pacman_keyring()
390 self.pacman = pac.Pac(
391 Installation.TMP_PACMAN_CONF, self.events.queue)
392 except Exception as ex:
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)
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."))
406 def prepare_pacman_keyring():
407 """ Add gnupg pacman files to installed system """
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)
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"]
421 # Delete old gnupg files
422 dest_path = os.path.join(DEST_DIR, "etc/pacman.d/gnupg")
423 cmd = ["rm", "-rf", dest_path]
427 # Tell pacman-key to regenerate gnupg files
428 # Initialize the pacman keyring
429 cmd = ["pacman-key", "--init", "--gpgdir", dest_path]
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"]
438 # path = os.path.join(DEST_DIR, "root/.gnupg/dirmngr_ldapservers.conf")
440 # https://bbs.archlinux.org/viewtopic.php?id=190380
441 with open(os.devnull, 'r') as dev_null:
443 call(cmd, stdin=dev_null)
445 # Refresh and update the signature keys
446 # cmd = ["pacman-key", "--refresh-keys", "--gpgdir", dest_path]
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:
458 except OSError as err:
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)
472 def install_packages(self):
473 """ Start pacman installation of packages """
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)
480 logging.debug("Installing packages...")
483 result = self.pacman.install(pkgs=self.packages)
484 except pac.pyalpm.error:
487 stale_pkgs = self.settings.get('cache_pkgs_md5_check_failed')
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.
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()
496 result = self.pacman.install(pkgs=self.packages)
497 except pac.pyalpm.error:
501 # Failure might be due to antergos mirror issues. Try using build server repo.
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()
507 result = self.pacman.install(pkgs=self.packages)
508 except pac.pyalpm.error:
512 txt = _("Can't install necessary packages. Cnchi can't continue.")
513 raise InstallError(txt)
515 # All downloading and installing has been done, so we hide progress bar
516 self.events.add('progress_bar', 'hide')
518 def is_running(self):
519 """ Checks if thread is running """
523 """ Checks if an error has been issued """
524 return not self.error