def uefi_remove_old_loaders(grubcfg, target): """Removes the old UEFI loaders from efibootmgr.""" efi_output = util.get_efibootmgr(target) current_uefi_boot = efi_output.get('current', None) old_efi_entries = { entry: info for entry, info in efi_output['entries'].items() if re.match(r'^.*File\(\\EFI.*$', info['path']) } old_efi_entries.pop(current_uefi_boot, None) remove_old_loaders = grubcfg.get('remove_old_uefi_loaders', True) if old_efi_entries: if remove_old_loaders: with util.ChrootableTarget(target) as in_chroot: for entry, info in old_efi_entries.items(): LOG.debug("removing old UEFI entry: %s" % info['name']) in_chroot.subp(['efibootmgr', '-B', '-b', entry], capture=True) else: LOG.debug("Skipped removing %d old UEFI entrie%s.", len(old_efi_entries), '' if len(old_efi_entries) == 1 else 's') for info in old_efi_entries.values(): LOG.debug( "UEFI entry '%s' might no longer exist and " "should be removed.", info['name'])
def apt_command(args): """ Main entry point for curtin apt-config standalone command This does not read the global config as handled by curthooks, but instead one can specify a different "target" and a new cfg via --config """ cfg = config.load_command_config(args, {}) if args.target is not None: target = args.target else: state = util.load_command_environment() target = state['target'] if target is None: sys.stderr.write("Unable to find target. " "Use --target or set TARGET_MOUNT_POINT\n") sys.exit(2) apt_cfg = cfg.get("apt") # if no apt config section is available, do nothing if apt_cfg is not None: LOG.debug("Handling apt to target %s with config %s", target, apt_cfg) try: with util.ChrootableTarget(target, sys_resolvconf=True): handle_apt(apt_cfg, target) except (RuntimeError, TypeError, ValueError, IOError): LOG.exception("Failed to configure apt features '%s'", apt_cfg) sys.exit(1) else: LOG.info("No apt config provided, skipping") sys.exit(0)
def uefi_reorder_loaders(grubcfg, target): """Reorders the UEFI BootOrder to place BootCurrent first. The specifically doesn't try to do to much. The order in which grub places a new EFI loader is up to grub. This only moves the BootCurrent to the front of the BootOrder. """ if grubcfg.get('reorder_uefi', True): efi_output = util.get_efibootmgr(target) currently_booted = efi_output.get('current', None) boot_order = efi_output.get('order', []) if currently_booted: if currently_booted in boot_order: boot_order.remove(currently_booted) boot_order = [currently_booted] + boot_order new_boot_order = ','.join(boot_order) LOG.debug( "Setting currently booted %s as the first " "UEFI loader.", currently_booted) LOG.debug("New UEFI boot order: %s", new_boot_order) with util.ChrootableTarget(target) as in_chroot: in_chroot.subp(['efibootmgr', '-o', new_boot_order]) else: LOG.debug("Skipped reordering of UEFI boot methods.") LOG.debug("Currently booted UEFI loader might no longer boot.")
def test_chrootable_target_default_mounts_uefi(self, m_uefi): m_uefi.return_value = True in_chroot = util.ChrootableTarget("mytarget") default_mounts = [ '/dev', '/proc', '/run', '/sys', '/sys/firmware/efi/efivars' ] self.assertEqual(sorted(default_mounts), sorted(in_chroot.mounts))
def in_target_main(args): if args.target is not None: target = args.target else: state = util.load_command_environment() target = state['target'] if args.target is None: sys.stderr.write("Unable to find target. " "Use --target or set TARGET_MOUNT_POINT\n") sys.exit(2) daemons = args.allow_daemons if paths.target_path(args.target) == "/": sys.stderr.write("WARN: Target is /, daemons are allowed.\n") daemons = True cmd = args.command_args with util.ChrootableTarget(target, allow_daemons=daemons) as chroot: exit = 0 if not args.interactive: try: chroot.subp(cmd, capture=args.capture) except util.ProcessExecutionError as e: exit = e.exit_code else: if chroot.target != "/": cmd = ["chroot", chroot.target] + args.command_args # in python 3.4 pty.spawn started returning a value. # There, it is the status from os.waitpid. From testing (py3.6) # that seemse to be exit_code * 256. ret = pty.spawn(cmd) # pylint: disable=E1111 if ret is not None: exit = int(ret / 256) sys.exit(exit)
def rpm_get_dist_id(target): """Use rpm command to extract the '%rhel' distro macro which returns the major os version id (6, 7, 8). This works for centos or rhel """ with util.ChrootableTarget(target) as in_chroot: dist, _ = in_chroot.subp(['rpm', '-E', '%rhel'], capture=True) return dist.rstrip()
def test_skip_rename_resolvconf_gone(self, m_rename): self.m_shutil.copy.side_effect = self.mycopy self.m_shutil.rmtree.side_effect = self.mydel with util.ChrootableTarget(self.target): tp = paths.target_path(self.target, path='/etc/resolv.conf') target_conf = util.load_file(tp) self.assertEqual(self.host_content, target_conf) self.assertEqual(0, m_rename.call_count)
def test_chrootable_target_renames_and_copies_resolvconf_if_symlink(self): target_rconf = os.path.join(self.target, 'etc/resolv.conf') os.symlink('../run/foobar/wark.conf', target_rconf) self.m_shutil.copy.side_effect = self.mycopy self.m_shutil.rmtree.side_effect = self.mydel with util.ChrootableTarget(self.target): target_conf = util.load_file( paths.target_path(self.target, path='/etc/resolv.conf')) self.assertEqual(self.host_content, target_conf)
def test_chrootable_target_renames_and_copies_resolvconf(self): content = "target_resolvconf" util.write_file(os.path.join(self.target, 'etc/resolv.conf'), content) self.m_shutil.copy.side_effect = self.mycopy self.m_shutil.rmtree.side_effect = self.mydel with util.ChrootableTarget(self.target): target_conf = util.load_file( paths.target_path(self.target, path='/etc/resolv.conf')) self.assertEqual(self.host_content, target_conf)
def test_chrootable_target_restores_resolvconf_on_copy_fail(self): content = "target_resolvconf" tconf = os.path.join(self.target, 'etc/resolv.conf') util.write_file(tconf, content) self.m_shutil.copy.side_effect = OSError('Failed to copy') self.m_shutil.rmtree.side_effect = self.mydel try: with util.ChrootableTarget(self.target): pass except OSError: pass target_conf = util.load_file(tconf) self.assertEqual(content, target_conf)
def install_grub(devices, target, uefi=None, grubcfg=None): """Install grub to devices inside target chroot. :param: devices: List of block device paths to install grub upon. :param: target: A string specifying the path to the chroot mountpoint. :param: uefi: A boolean set to True if system is UEFI bootable otherwise False. :param: grubcfg: An config dict with grub config options. """ if not devices: raise ValueError("Invalid parameter 'devices': %s" % devices) if not target: raise ValueError("Invalid parameter 'target': %s" % target) LOG.debug("installing grub to target=%s devices=%s [replace_defaults=%s]", target, devices, grubcfg.get('replace_default')) update_nvram = config.value_as_boolean(grubcfg.get('update_nvram', True)) distroinfo = distro.get_distroinfo(target=target) target_arch = distro.get_architecture(target=target) rhel_ver = (distro.rpm_get_dist_id(target) if distroinfo.family == distro.DISTROS.redhat else None) check_target_arch_machine(target, arch=target_arch, uefi=uefi) grub_name, grub_target = get_grub_package_name(target_arch, uefi, rhel_ver) grub_conf = get_grub_config_file(target, distroinfo.family) new_params = get_carryover_params(distroinfo) prepare_grub_dir(target, grub_conf) write_grub_config(target, grubcfg, grub_conf, new_params) grub_cmd = get_grub_install_command(uefi, distroinfo, target) if uefi: install_cmds, post_cmds = gen_uefi_install_commands( grub_name, grub_target, grub_cmd, update_nvram, distroinfo, devices, target) else: install_cmds, post_cmds = gen_install_commands(grub_name, grub_cmd, distroinfo, devices, rhel_ver) env = os.environ.copy() env['DEBIAN_FRONTEND'] = 'noninteractive' LOG.debug('Grub install cmds:\n%s', str(install_cmds + post_cmds)) with util.ChrootableTarget(target) as in_chroot: for cmd in install_cmds + post_cmds: in_chroot.subp(cmd, env=env, capture=True)
def netconfig_passthrough_available(target, feature='NETWORK_CONFIG_V2'): """ Determine if curtin can pass v2 network config to in target cloud-init """ LOG.debug('Checking in-target cloud-init for feature: %s', feature) with util.ChrootableTarget(target) as in_chroot: cloudinit = util.which('cloud-init', target=target) if not cloudinit: LOG.warning('Target does not have cloud-init installed') return False available = False try: out, _ = in_chroot.subp([cloudinit, 'features'], capture=True) available = feature in out.splitlines() except util.ProcessExecutionError: # we explicitly don't dump the exception as this triggers # vmtest failures when parsing the installation log file LOG.warning("Failed to probe cloudinit features") return False LOG.debug('cloud-init feature %s available? %s', feature, available) return available
def curthooks(args): state = util.load_command_environment() if args.target is not None: target = args.target else: target = state['target'] if target is None: sys.stderr.write("Unable to find target. " "Use --target or set TARGET_MOUNT_POINT\n") sys.exit(2) cfg = config.load_command_config(args, state) stack_prefix = state.get('report_stack_prefix', '') # if curtin-hooks hook exists in target we can defer to the in-target hooks if util.run_hook_if_exists(target, 'curtin-hooks'): # For vmtests to force execute centos_apply_network_config, uncomment # the value in examples/tests/centos_defaults.yaml if cfg.get('_ammend_centos_curthooks'): if cfg.get('cloudconfig'): handle_cloudconfig(cfg['cloudconfig'], base_dir=util.target_path( target, 'etc/cloud/cloud.cfg.d')) if target_is_centos(target) or target_is_rhel(target): LOG.info('Detected RHEL/CentOS image, running extra hooks') with events.ReportEventStack( name=stack_prefix, reporting_enabled=True, level="INFO", description="Configuring CentOS for first boot"): centos_apply_network_config(cfg.get('network', {}), target) sys.exit(0) if target_is_ubuntu_core(target): LOG.info('Detected Ubuntu-Core image, running hooks') with events.ReportEventStack( name=stack_prefix, reporting_enabled=True, level="INFO", description="Configuring Ubuntu-Core for first boot"): ubuntu_core_curthooks(cfg, target) sys.exit(0) with events.ReportEventStack( name=stack_prefix + '/writing-config', reporting_enabled=True, level="INFO", description="configuring apt configuring apt"): do_apt_config(cfg, target) disable_overlayroot(cfg, target) # LP: #1742560 prevent zfs-dkms from being installed (Xenial) if util.lsb_release(target=target)['codename'] == 'xenial': util.apt_update(target=target) with util.ChrootableTarget(target) as in_chroot: in_chroot.subp(['apt-mark', 'hold', 'zfs-dkms']) # packages may be needed prior to installing kernel with events.ReportEventStack(name=stack_prefix + '/installing-missing-packages', reporting_enabled=True, level="INFO", description="installing missing packages"): install_missing_packages(cfg, target) # If a /etc/iscsi/nodes/... file was created by block_meta then it # needs to be copied onto the target system nodes_location = os.path.join(os.path.split(state['fstab'])[0], "nodes") if os.path.exists(nodes_location): copy_iscsi_conf(nodes_location, target) # do we need to reconfigure open-iscsi? # If a mdadm.conf file was created by block_meta than it needs to be copied # onto the target system mdadm_location = os.path.join( os.path.split(state['fstab'])[0], "mdadm.conf") if os.path.exists(mdadm_location): copy_mdadm_conf(mdadm_location, target) # as per https://bugs.launchpad.net/ubuntu/+source/mdadm/+bug/964052 # reconfigure mdadm util.subp(['dpkg-reconfigure', '--frontend=noninteractive', 'mdadm'], data=None, target=target) with events.ReportEventStack(name=stack_prefix + '/installing-kernel', reporting_enabled=True, level="INFO", description="installing kernel"): setup_zipl(cfg, target) install_kernel(cfg, target) run_zipl(cfg, target) restore_dist_interfaces(cfg, target) with events.ReportEventStack(name=stack_prefix + '/setting-up-swap', reporting_enabled=True, level="INFO", description="setting up swap"): add_swap(cfg, target, state.get('fstab')) with events.ReportEventStack(name=stack_prefix + '/apply-networking-config', reporting_enabled=True, level="INFO", description="apply networking config"): apply_networking(target, state) with events.ReportEventStack(name=stack_prefix + '/writing-etc-fstab', reporting_enabled=True, level="INFO", description="writing etc/fstab"): copy_fstab(state.get('fstab'), target) with events.ReportEventStack(name=stack_prefix + '/configuring-multipath', reporting_enabled=True, level="INFO", description="configuring multipath"): detect_and_handle_multipath(cfg, target) with events.ReportEventStack( name=stack_prefix + '/system-upgrade', reporting_enabled=True, level="INFO", description="updating packages on target system"): system_upgrade(cfg, target) with events.ReportEventStack( name=stack_prefix + '/pollinate-user-agent', reporting_enabled=True, level="INFO", description="configuring pollinate user-agent on target system"): handle_pollinate_user_agent(cfg, target) # If a crypttab file was created by block_meta than it needs to be copied # onto the target system, and update_initramfs() needs to be run, so that # the cryptsetup hooks are properly configured on the installed system and # it will be able to open encrypted volumes at boot. crypttab_location = os.path.join( os.path.split(state['fstab'])[0], "crypttab") if os.path.exists(crypttab_location): copy_crypttab(crypttab_location, target) update_initramfs(target) # If udev dname rules were created, copy them to target udev_rules_d = os.path.join(state['scratch'], "rules.d") if os.path.isdir(udev_rules_d): copy_dname_rules(udev_rules_d, target) # As a rule, ARMv7 systems don't use grub. This may change some # day, but for now, assume no. They do require the initramfs # to be updated, and this also triggers boot loader setup via # flash-kernel. machine = platform.machine() if (machine.startswith('armv7') or machine.startswith('s390x') or machine.startswith('aarch64') and not util.is_uefi_bootable()): update_initramfs(target) else: setup_grub(cfg, target) sys.exit(0)
def centos_apply_network_config(netcfg, target=None): """ CentOS images execute built-in curthooks which only supports simple networking configuration. This hook enables advanced network configuration via config passthrough to the target. """ def cloud_init_repo(version): if not version: raise ValueError('Missing required version parameter') return CLOUD_INIT_YUM_REPO_TEMPLATE % version if netcfg: LOG.info('Removing embedded network configuration (if present)') ifcfgs = glob.glob( util.target_path(target, 'etc/sysconfig/network-scripts') + '/ifcfg-*') # remove ifcfg-* (except ifcfg-lo) for ifcfg in ifcfgs: if os.path.basename(ifcfg) != "ifcfg-lo": util.del_file(ifcfg) LOG.info( 'Checking cloud-init in target [%s] for network ' 'configuration passthrough support.', target) passthrough = net.netconfig_passthrough_available(target) LOG.debug('passthrough available via in-target: %s', passthrough) # if in-target cloud-init is not updated, upgrade via cloud-init repo if not passthrough: cloud_init_yum_repo = (util.target_path( target, 'etc/yum.repos.d/curtin-cloud-init.repo')) # Inject cloud-init daily yum repo util.write_file(cloud_init_yum_repo, content=cloud_init_repo(rpm_get_dist_id(target))) # we separate the installation of repository packages (epel, # cloud-init-el-release) as we need a new invocation of yum # to read the newly installed repo files. YUM_CMD = ['yum', '-y', '--noplugins', 'install'] retries = [1] * 30 with util.ChrootableTarget(target) as in_chroot: # ensure up-to-date ca-certificates to handle https mirror # connections in_chroot.subp(YUM_CMD + ['ca-certificates'], capture=True, log_captured=True, retries=retries) in_chroot.subp(YUM_CMD + ['epel-release'], capture=True, log_captured=True, retries=retries) in_chroot.subp(YUM_CMD + ['cloud-init-el-release'], log_captured=True, capture=True, retries=retries) in_chroot.subp(YUM_CMD + ['cloud-init'], capture=True, log_captured=True, retries=retries) # remove cloud-init el-stable bootstrap repo config as the # cloud-init-el-release package points to the correct repo util.del_file(cloud_init_yum_repo) # install bridge-utils if needed with util.ChrootableTarget(target) as in_chroot: try: in_chroot.subp(['rpm', '-q', 'bridge-utils'], capture=False, rcs=[0]) except util.ProcessExecutionError: LOG.debug('Image missing bridge-utils package, installing') in_chroot.subp(YUM_CMD + ['bridge-utils'], capture=True, log_captured=True, retries=retries) LOG.info('Passing network configuration through to target') net.render_netconfig_passthrough(target, netconfig={'network': netcfg})
def update_initramfs(target=None, all_kernels=False): cmd = ['update-initramfs', '-u'] if all_kernels: cmd.extend(['-k', 'all']) with util.ChrootableTarget(target) as in_chroot: in_chroot.subp(cmd)
def test_chrootable_target_custom_mounts(self): my_mounts = ['/foo', '/bar', '/wark'] in_chroot = util.ChrootableTarget("mytarget", mounts=my_mounts) self.assertEqual(sorted(my_mounts), sorted(in_chroot.mounts))
def run_zipl(cfg, target): if platform.machine() != 's390x': return with util.ChrootableTarget(target) as in_chroot: in_chroot.subp(['zipl'])
def add_apt_sources(srcdict, target=None, template_params=None, aa_repo_match=None): """ add entries in /etc/apt/sources.list.d for each abbreviated sources.list entry in 'srcdict'. When rendering template, also include the values in dictionary searchList """ if template_params is None: template_params = {} if aa_repo_match is None: raise ValueError('did not get a valid repo matcher') if not isinstance(srcdict, dict): raise TypeError('unknown apt format: %s' % (srcdict)) for filename in srcdict: ent = srcdict[filename] if 'filename' not in ent: ent['filename'] = filename add_apt_key(ent['filename'], ent, target) if 'source' not in ent: continue source = ent['source'] if source == 'proposed': source = APT_SOURCES_PROPOSED source = util.render_string(source, template_params) if not ent['filename'].startswith("/"): ent['filename'] = os.path.join("/etc/apt/sources.list.d/", ent['filename']) if not ent['filename'].endswith(".list"): ent['filename'] += ".list" if aa_repo_match(source): with util.ChrootableTarget(target, sys_resolvconf=True) as in_chroot: try: in_chroot.subp(["add-apt-repository", source], retries=(1, 2, 5, 10)) except util.ProcessExecutionError: LOG.exception("add-apt-repository failed.") raise continue sourcefn = paths.target_path(target, ent['filename']) try: contents = "%s\n" % (source) util.write_file(sourcefn, contents, omode="a") except IOError as detail: LOG.exception("failed write to file %s: %s", sourcefn, detail) raise distro.apt_update(target=target, force=True, comment="apt-source changed config") return
def setup_grub(cfg, target): # target is the path to the mounted filesystem # FIXME: these methods need moving to curtin.block # and using them from there rather than commands.block_meta from curtin.commands.block_meta import (extract_storage_ordered_dict, get_path_to_storage_volume) grubcfg = cfg.get('grub', {}) # copy legacy top level name if 'grub_install_devices' in cfg and 'install_devices' not in grubcfg: grubcfg['install_devices'] = cfg['grub_install_devices'] LOG.debug("setup grub on target %s", target) # if there is storage config, look for devices tagged with 'grub_device' storage_cfg_odict = None try: storage_cfg_odict = extract_storage_ordered_dict(cfg) except ValueError: pass if storage_cfg_odict: storage_grub_devices = [] for item_id, item in storage_cfg_odict.items(): if not item.get('grub_device'): continue LOG.debug("checking: %s", item) storage_grub_devices.append( get_path_to_storage_volume(item_id, storage_cfg_odict)) if len(storage_grub_devices) > 0: grubcfg['install_devices'] = storage_grub_devices LOG.debug("install_devices: %s", grubcfg.get('install_devices')) if 'install_devices' in grubcfg: instdevs = grubcfg.get('install_devices') if isinstance(instdevs, str): instdevs = [instdevs] if instdevs is None: LOG.debug("grub installation disabled by config") else: # If there were no install_devices found then we try to do the right # thing. That right thing is basically installing on all block # devices that are mounted. On powerpc, though it means finding PrEP # partitions. devs = block.get_devices_for_mp(target) blockdevs = set() for maybepart in devs: try: (blockdev, part) = block.get_blockdev_for_partition(maybepart) blockdevs.add(blockdev) except ValueError: # if there is no syspath for this device such as a lvm # or raid device, then a ValueError is raised here. LOG.debug("failed to find block device for %s", maybepart) if platform.machine().startswith("ppc64"): # assume we want partitions that are 4100 (PReP). The snippet here # just prints the partition number partitions of that type. shnip = textwrap.dedent(""" export LANG=C; for d in "$@"; do sgdisk "$d" --print | awk '$6 == prep { print d $1 }' "d=$d" prep=4100 done """) try: out, err = util.subp(['sh', '-c', shnip, '--'] + list(blockdevs), capture=True) instdevs = str(out).splitlines() if not instdevs: LOG.warn("No power grub target partitions found!") instdevs = None except util.ProcessExecutionError as e: LOG.warn("Failed to find power grub partitions: %s", e) instdevs = None else: instdevs = list(blockdevs) # UEFI requires grub-efi-{arch}. If a signed version of that package # exists then it will be installed. if util.is_uefi_bootable(): arch = util.get_architecture() pkgs = ['grub-efi-%s' % arch] # Architecture might support a signed UEFI loader uefi_pkg_signed = 'grub-efi-%s-signed' % arch if util.has_pkg_available(uefi_pkg_signed): pkgs.append(uefi_pkg_signed) # AMD64 has shim-signed for SecureBoot support if arch == "amd64": pkgs.append("shim-signed") # Install the UEFI packages needed for the architecture util.install_packages(pkgs, target=target) env = os.environ.copy() replace_default = grubcfg.get('replace_linux_default', True) if str(replace_default).lower() in ("0", "false"): env['REPLACE_GRUB_LINUX_DEFAULT'] = "0" else: env['REPLACE_GRUB_LINUX_DEFAULT'] = "1" if instdevs: instdevs = [block.get_dev_name_entry(i)[1] for i in instdevs] else: instdevs = ["none"] if util.is_uefi_bootable() and grubcfg.get('update_nvram', True): uefi_remove_old_loaders(grubcfg, target) LOG.debug("installing grub to %s [replace_default=%s]", instdevs, replace_default) with util.ChrootableTarget(target): args = ['install-grub'] if util.is_uefi_bootable(): args.append("--uefi") if grubcfg.get('update_nvram', True): LOG.debug("GRUB UEFI enabling NVRAM updates") args.append("--update-nvram") else: LOG.debug("NOT enabling UEFI nvram updates") LOG.debug("Target system may not boot") args.append(target) # capture stdout and stderr joined. join_stdout_err = ['sh', '-c', 'exec "$0" "$@" 2>&1'] out, _err = util.subp(join_stdout_err + args + instdevs, env=env, capture=True) LOG.debug("%s\n%s\n", args, out) if util.is_uefi_bootable() and grubcfg.get('update_nvram', True): uefi_reorder_loaders(grubcfg, target)
def test_chrootable_target_default_mounts(self): in_chroot = util.ChrootableTarget("mytarget") default_mounts = ['/dev', '/proc', '/run', '/sys'] self.assertEqual(sorted(default_mounts), sorted(in_chroot.mounts))
def test_chrootable_target_no_mounts(self): my_mounts = [] in_chroot = util.ChrootableTarget("mytarget", mounts=my_mounts) self.assertEqual(sorted(my_mounts), sorted(in_chroot.mounts))