def apply_preserve_sources_list(target): # protect the just generated sources.list from cloud-init cloudfile = "/etc/cloud/cloud.cfg.d/curtin-preserve-sources.cfg" target_ver = distro.get_package_version('cloud-init', target=target) if not target_ver: LOG.info( "Attempt to read cloud-init version from target returned " "'%s', not writing preserve_sources_list config.", target_ver) return cfg = {'apt': {'preserve_sources_list': True}} if target_ver['major'] < 1: # anything cloud-init 0.X.X will get the old config key. cfg = {'apt_preserve_sources_list': True} try: util.write_file(paths.target_path(target, cloudfile), config.dump_config(cfg), mode=0o644) LOG.debug("Set preserve_sources_list to True in %s with: %s", cloudfile, cfg) except IOError: LOG.exception( "Failed to protect /etc/apt/sources.list from cloud-init in '%s'", cloudfile) raise
def ubuntu_core_curthooks(cfg, target=None): """ Ubuntu-Core 16 images cannot execute standard curthooks Instead we copy in any cloud-init configuration to the 'LABEL=writable' partition mounted at target. """ ubuntu_core_target = os.path.join(target, "system-data") cc_target = os.path.join(ubuntu_core_target, 'etc/cloud/cloud.cfg.d') cloudconfig = cfg.get('cloudconfig', None) if cloudconfig: # remove cloud-init.disabled, if found cloudinit_disable = os.path.join(ubuntu_core_target, 'etc/cloud/cloud-init.disabled') if os.path.exists(cloudinit_disable): util.del_file(cloudinit_disable) handle_cloudconfig(cloudconfig, base_dir=cc_target) netconfig = cfg.get('network', None) if netconfig: LOG.info('Writing network configuration') ubuntu_core_netconfig = os.path.join(cc_target, "50-curtin-networking.cfg") util.write_file(ubuntu_core_netconfig, content=config.dump_config({'network': netconfig}))
def dump(self): state = { 'version': self.version, 'config': self.config, 'network_state': self.network_state, } return curtin_config.dump_config(state)
def test_render_netconfig_passthrough(self, mock_writefile): netcfg = yaml.safe_load(self.config) pt_config = 'etc/cloud/cloud.cfg.d/50-curtin-networking.cfg' target_config = os.path.sep.join((self.target, pt_config), ) net.render_netconfig_passthrough(self.target, netconfig=netcfg) content = config.dump_config(netcfg) mock_writefile.assert_called_with(target_config, content=content)
def test_curtin_error_copies_config_and_error_tarfile_defaults(self): """On curtin error, install error_tarfile is created with all logs. Curtin config, install log and error_tarfile are copied into target. """ working_dir = self.tmp_path('working', _dir=self.new_root) ensure_dir(working_dir) target_dir = self.tmp_path('target', _dir=working_dir) write_file(self.logfile, 'old log') # Providing two dd images raises an error myargs = FakeArgs( config={'install': {'log_file': self.logfile}}, source=['dd-raw:https://localhost/raw_images/centos-6-3.img', 'dd-raw:https://localhost/cant/provide/two/images.img'], reportstack=FakeReportStack()) self.add_patch( 'curtin.commands.collect_logs.create_log_tarfile', 'm_tar') self.add_patch( 'curtin.commands.install.copy_install_log', 'm_copy_log') self.add_patch( 'curtin.commands.install.tempfile.mkdtemp', 'm_mkdtemp') self.m_mkdtemp.return_value = working_dir with self.assertRaises(ValueError) as context_manager: install.cmd_install(myargs) self.assertEqual( 'You may not use more than one disk image', str(context_manager.exception)) expected_cfg = copy.deepcopy(install.CONFIG_BUILTIN) expected_cfg['install']['log_file'] = self.logfile expected_cfg['proxy'] = {} expected_cfg['sources'] = { '00_cmdline': { 'type': 'dd-raw', 'uri': 'https://localhost/raw_images/centos-6-3.img'}, '01_cmdline': { 'type': 'dd-raw', 'uri': 'https://localhost/cant/provide/two/images.img'}} expected_cfg['write_files'] = { 'curtin_install_cfg': { 'owner': 'root:root', 'permissions': '0400', 'path': '/root/curtin-install-cfg.yaml', 'content': config.dump_config(expected_cfg)}} # Call create_log_tarfile to collect error logs. self.assertEqual( [mock.call('/var/log/curtin/curtin-error-logs.tar', expected_cfg)], self.m_tar.call_args_list) self.assertEqual( [mock.call(self.logfile, target_dir, '/root/curtin-install.log')], self.m_copy_log.call_args_list)
def render_netconfig_passthrough(target, netconfig=None): """ Extract original network config and pass it through to cloud-init in target """ cc = 'etc/cloud/cloud.cfg.d/50-curtin-networking.cfg' if not isinstance(netconfig, dict): raise ValueError('Network config must be a dictionary') if 'network' not in netconfig: raise ValueError("Network config must contain the key 'network'") content = config.dump_config(netconfig) cc_passthrough = os.path.sep.join((target, cc,)) LOG.info('Writing network config to %s: %s', cc, cc_passthrough) util.write_file(cc_passthrough, content=content)
def test_curthooks_net_config(self, mock_handle_cc, mock_del_file, mock_write_file): self.target = self.tmp_dir() cfg = { 'network': { 'version': '1', 'config': [{'type': 'physical', 'name': 'eth0', 'subnets': [{'type': 'dhcp4'}]}] } } curthooks.ubuntu_core_curthooks(cfg, target=self.target) self.assertEqual(len(mock_del_file.call_args_list), 0) self.assertEqual(len(mock_handle_cc.call_args_list), 0) netcfg_path = os.path.join(self.target, 'system-data', 'etc/cloud/cloud.cfg.d', '50-curtin-networking.cfg') netcfg = config.dump_config({'network': cfg.get('network')}) mock_write_file.assert_called_with(netcfg_path, content=netcfg) self.assertEqual(len(mock_del_file.call_args_list), 0)
def net_meta(args): # curtin net-meta --devices connected dhcp # curtin net-meta --devices configured dhcp # curtin net-meta --devices netboot dhcp # curtin net-meta --devices connected custom # if network-config hook exists in target, # we do not run the builtin if util.run_hook_if_exists(args.target, 'network-config'): sys.exit(0) state = util.load_command_environment() cfg = config.load_command_config(args, state) if cfg.get("network") is not None: args.mode = "custom" eni = "etc/network/interfaces" if args.mode == "auto": if not args.devices: args.devices = ["connected"] t_eni = None if args.target: t_eni = os.path.sep.join(( args.target, eni, )) if not os.path.isfile(t_eni): t_eni = None if t_eni: args.mode = "copy" else: args.mode = "dhcp" devices = [] if args.devices: for dev in args.devices: if dev in DEVNAME_ALIASES: devices += resolve_alias(dev) else: devices.append(dev) LOG.debug("net-meta mode is '%s'. devices=%s", args.mode, devices) output_network_config = os.environ.get("OUTPUT_NETWORK_CONFIG", "") if args.mode == "copy": if not args.target: raise argparse.ArgumentTypeError("mode 'copy' requires --target") t_eni = os.path.sep.join(( args.target, "etc/network/interfaces", )) with open(t_eni, "r") as fp: content = fp.read() LOG.warn( "net-meta mode is 'copy', static network interfaces files" "can be brittle. Copied interfaces: %s", content) target = args.output elif args.mode == "dhcp": target = output_network_config content = config.dump_config(interfaces_basic_dhcp(devices)) elif args.mode == 'custom': target = output_network_config content = config.dump_config(interfaces_custom(args)) else: raise Exception("Unexpected network config mode '%s'." % args.mode) if not target: raise Exception( "No target given for mode = '%s'. No where to write content: %s" % (args.mode, content)) LOG.debug("writing to file %s with network config: %s", target, content) if target == "-": sys.stdout.write(content) else: with open(target, "w") as fp: fp.write(content) sys.exit(0)
def cmd_install(args): from .collect_logs import create_log_tarfile cfg = deepcopy(CONFIG_BUILTIN) config.merge_config(cfg, args.config) for source in args.source: src = util.sanitize_source(source) cfg['sources']["%02d_cmdline" % len(cfg['sources'])] = src LOG.info(INSTALL_START_MSG) LOG.debug('LANG=%s', os.environ.get('LANG')) LOG.debug("merged config: %s" % cfg) if not len(cfg.get('sources', [])): raise util.BadUsage("no sources provided to install") for i in cfg['sources']: # we default to tgz for old style sources config cfg['sources'][i] = util.sanitize_source(cfg['sources'][i]) migrate_proxy_settings(cfg) for k in ('http_proxy', 'https_proxy', 'no_proxy'): if k in cfg['proxy']: os.environ[k] = cfg['proxy'][k] instcfg = cfg.get('install', {}) logfile = instcfg.get('log_file') error_tarfile = instcfg.get('error_tarfile') post_files = instcfg.get('post_files', [logfile]) # Generate curtin configuration dump and add to write_files unless # installation config disables dump yaml_dump_file = instcfg.get('save_install_config', SAVE_INSTALL_CONFIG) if yaml_dump_file: write_files = cfg.get('write_files', {}) write_files['curtin_install_cfg'] = { 'path': yaml_dump_file, 'permissions': '0400', 'owner': 'root:root', 'content': config.dump_config(cfg) } cfg['write_files'] = write_files # Load reporter clear_install_log(logfile) legacy_reporter = load_reporter(cfg) legacy_reporter.files = post_files writeline_and_stdout(logfile, INSTALL_START_MSG) args.reportstack.post_files = post_files workingd = None try: workingd = WorkingDir(cfg) dd_images = util.get_dd_images(cfg.get('sources', {})) if len(dd_images) > 1: raise ValueError("You may not use more than one disk image") LOG.debug(workingd.env()) env = os.environ.copy() env.update(workingd.env()) for name in cfg.get('stages'): desc = STAGE_DESCRIPTIONS.get(name, "stage %s" % name) reportstack = events.ReportEventStack( "stage-%s" % name, description=desc, parent=args.reportstack) env['CURTIN_REPORTSTACK'] = reportstack.fullname with reportstack: commands_name = '%s_commands' % name with util.LogTimer(LOG.debug, 'stage_%s' % name): stage = Stage(name, cfg.get(commands_name, {}), env, reportstack=reportstack, logfile=logfile) stage.run() if apply_kexec(cfg.get('kexec'), workingd.target): cfg['power_state'] = {'mode': 'reboot', 'delay': 'now', 'message': "'rebooting with kexec'"} writeline_and_stdout(logfile, INSTALL_PASS_MSG) legacy_reporter.report_success() except Exception as e: exp_msg = INSTALL_FAIL_MSG.format(exception=e) writeline(logfile, exp_msg) LOG.error(exp_msg) legacy_reporter.report_failure(exp_msg) if error_tarfile: create_log_tarfile(error_tarfile, cfg) raise e finally: log_target_path = instcfg.get('save_install_log', SAVE_INSTALL_LOG) if log_target_path and workingd: copy_install_log(logfile, workingd.target, log_target_path) if instcfg.get('unmount', "") == "disabled": LOG.info('Skipping unmount: config disabled target unmounting') elif workingd: # unmount everything (including iscsi disks) util.do_umount(workingd.target, recursive=True) # The open-iscsi service in the ephemeral environment handles # disconnecting active sessions. On Artful release the systemd # unit file has conditionals that are not met at boot time and # results in open-iscsi service not being started; This breaks # shutdown on Artful releases. # Additionally, in release < Artful, if the storage configuration # is layered, like RAID over iscsi volumes, then disconnecting # iscsi sessions before stopping the raid device hangs. # As it turns out, letting the open-iscsi service take down the # session last is the cleanest way to handle all releases # regardless of what may be layered on top of the iscsi disks. # # Check if storage configuration has iscsi volumes and if so ensure # iscsi service is active before exiting install if iscsi.get_iscsi_disks_from_config(cfg): iscsi.restart_iscsi_service() shutil.rmtree(workingd.top) apply_power_state(cfg.get('power_state')) sys.exit(0)
def dump_network_state(self): return curtin_config.dump_config(self.network_state)