diff --git a/subiquity/controllers/filesystem.py b/subiquity/controllers/filesystem.py index 4ae471f2..cbbd073b 100644 --- a/subiquity/controllers/filesystem.py +++ b/subiquity/controllers/filesystem.py @@ -20,10 +20,6 @@ from subiquitycore.controller import BaseController from subiquitycore.ui.dummy import DummyView from subiquitycore.ui.error import ErrorView -from subiquity.curtin import ( - CURTIN_CONFIGS, - curtin_write_storage_actions, - ) from subiquity.models.filesystem import humanize_size from subiquity.ui.views import ( BcacheView, @@ -107,36 +103,8 @@ class FilesystemController(BaseController): self.ui.set_body(ErrorView(self.signal, error_msg)) def finish(self): - log.info("Rendering curtin config from user choices") - try: - curtin_write_storage_actions( - CURTIN_CONFIGS['storage'], - actions=self.model.render()) - except PermissionError: - log.exception('Failed to write storage actions') - self.filesystem_error('curtin_write_storage_actions') - return None - - log.info("Rendering preserved config for post install") - preserved_actions = [] - for a in self.model.render(): - a['preserve'] = True - preserved_actions.append(a) - try: - curtin_write_storage_actions( - CURTIN_CONFIGS['preserved'], - actions=preserved_actions) - except PermissionError: - log.exception('Failed to write preserved actions') - self.filesystem_error('curtin_write_preserved_actions') - return None - - # mark that we've writting out curtin config - self.signal.emit_signal('installprogress:wrote-install') - # start curtin install in background - self.signal.emit_signal('installprogress:curtin-install') - + self.signal.emit_signal('installprogress:filesystem-config-done') # switch to next screen self.signal.emit_signal('next-screen') diff --git a/subiquity/controllers/identity.py b/subiquity/controllers/identity.py index a307236b..bef801d1 100644 --- a/subiquity/controllers/identity.py +++ b/subiquity/controllers/identity.py @@ -19,7 +19,6 @@ import logging from subiquitycore.controller import BaseController from subiquitycore.user import create_user -from subiquity.curtin import curtin_write_postinst_config from subiquity.ui.views import IdentityView log = logging.getLogger('subiquity.controllers.identity') @@ -60,13 +59,12 @@ class IdentityController(BaseController): log.debug("User input: {}".format(result)) self.model.add_user(result) try: - curtin_write_postinst_config(result) create_user(result, dryrun=self.opts.dry_run) except PermissionError: log.exception('Failed to write curtin post-install config') self.signal.emit_signal('filesystem:error', 'curtin_write_postinst_config', result) return None - self.signal.emit_signal('installprogress:wrote-postinstall') + self.signal.emit_signal('installprogress:identity-config-done') # show next view self.signal.emit_signal('next-screen') diff --git a/subiquity/controllers/installprogress.py b/subiquity/controllers/installprogress.py index 880aa60b..fc645f71 100644 --- a/subiquity/controllers/installprogress.py +++ b/subiquity/controllers/installprogress.py @@ -13,6 +13,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import datetime import fcntl import logging import os @@ -20,17 +21,18 @@ import random import subprocess import sys +import yaml + from systemd import journal from subiquitycore import utils from subiquitycore.controller import BaseController -from subiquity.curtin import (CURTIN_CONFIGS, - CURTIN_INSTALL_LOG, - CURTIN_POSTINSTALL_LOG, - curtin_install_cmd, - curtin_write_network_config, - curtin_write_reporting_config) +from subiquity.curtin import ( + CURTIN_INSTALL_LOG, + CURTIN_POSTINSTALL_LOG, + curtin_install_cmd, + ) from subiquity.ui.views import ProgressView @@ -49,10 +51,8 @@ class InstallState: class InstallProgressController(BaseController): signals = [ - ('installprogress:curtin-install', 'curtin_start_install'), - ('installprogress:wrote-install', 'curtin_wrote_install'), - ('installprogress:wrote-postinstall', 'curtin_wrote_postinstall'), - ('network-config-written', 'curtin_wrote_network_config'), + ('installprogress:filesystem-config-done', 'filesystem_config_done'), + ('installprogress:identity-config-done', 'identity_config_done'), ] def __init__(self, common): @@ -66,13 +66,10 @@ class InstallProgressController(BaseController): self.journald_forwarder_proc = None self.curtin_event_stack = [] - def curtin_wrote_network_config(self, path): - curtin_write_network_config(open(path).read()) + def filesystem_config_done(self): + self.curtin_start_install() - def curtin_wrote_install(self): - pass - - def curtin_wrote_postinstall(self): + def identity_config_done(self): self.postinstall_written = True if self.install_state == InstallState.DONE_INSTALL: self.curtin_start_postinstall() @@ -126,33 +123,45 @@ class InstallProgressController(BaseController): callback(event) self.loop.watch_file(reader.fileno(), watch) - def curtin_start_install(self): - log.debug('Curtin Install: calling curtin with ' - 'storage/net/postinstall config') + def _write_config(self, path, config): + with open(path, 'w') as conf: + datestr = '# Autogenerated by SUbiquity: {} UTC\n'.format( + str(datetime.datetime.utcnow())) + conf.write(datestr) + conf.write(yaml.dump(config)) - self.install_state = InstallState.RUNNING_INSTALL - - self.start_journald_forwarder() - - self.start_journald_listener("curtin_event", self.curtin_event) - - curtin_write_reporting_config(self.reporting_url) + def _get_curtin_command(self, install_step): + config_file_name = 'subiquity-curtin-%s.conf' % (install_step,) if self.opts.dry_run: + config_location = os.path.join('.subiquity/', config_file_name) log.debug("Installprogress: this is a dry-run") curtin_cmd = [ "python3", "scripts/replay-curtin-log.py", - self.reporting_url, "examples/curtin-events-install.json", + self.reporting_url, "examples/curtin-events-%s.json" % (install_step,), ] else: + config_location = os.path.join('/tmp', config_file_name) log.debug("Installprogress: this is the *REAL* thing") - configs = [ - CURTIN_CONFIGS['storage'], - CURTIN_CONFIGS['network'], - CURTIN_CONFIGS['reporting'], - ] + configs = [config_location] curtin_cmd = curtin_install_cmd(configs) + self._write_config( + config_location, + self.base_model.render( + install_step=install_step, reporting_url=self.reporting_url)) + + return curtin_cmd + + def curtin_start_install(self): + log.debug('Curtin Install: calling curtin with ' + 'storage/net/postinstall config') + self.install_state = InstallState.RUNNING_INSTALL + self.start_journald_forwarder() + self.start_journald_listener("curtin_event", self.curtin_event) + + curtin_cmd = self._get_curtin_command("install") + log.debug('Curtin install cmd: {}'.format(curtin_cmd)) self.run_in_bg(lambda: self.run_command_logged(curtin_cmd, CURTIN_INSTALL_LOG), self.curtin_install_completed) @@ -185,20 +194,8 @@ class InstallProgressController(BaseController): self.progress_view.clear_log_tail() self.progress_view.set_status(_("Running postinstall step")) self.start_tail_proc() - if self.opts.dry_run: - log.debug("Installprogress: this is a dry-run") - curtin_cmd = [ - "python3", "scripts/replay-curtin-log.py", - self.reporting_url, "examples/curtin-events-postinstall.json", - ] - else: - log.debug("Installprogress: this is the *REAL* thing") - configs = [ - CURTIN_CONFIGS['postinstall'], - CURTIN_CONFIGS['preserved'], - CURTIN_CONFIGS['reporting'], - ] - curtin_cmd = curtin_install_cmd(configs) + + curtin_cmd = self._get_curtin_command("postinstall") log.debug('Curtin postinstall cmd: {}'.format(curtin_cmd)) self.run_in_bg(lambda: self.run_command_logged(curtin_cmd, CURTIN_POSTINSTALL_LOG), self.curtin_postinstall_completed) diff --git a/subiquity/curtin.py b/subiquity/curtin.py index 3ae9466b..9be99109 100644 --- a/subiquity/curtin.py +++ b/subiquity/curtin.py @@ -13,173 +13,29 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import copy -import datetime +from collections import OrderedDict import logging import os + import yaml log = logging.getLogger("subiquity.curtin") -TMPDIR = '/tmp' CURTIN_SEARCH_PATH = ['/usr/local/curtin/bin', '/usr/bin'] CURTIN_INSTALL_PATH = ['/media/root-ro', '/rofs', '/'] CURTIN_INSTALL_LOG = '/tmp/subiquity-curtin-install.log' CURTIN_POSTINSTALL_LOG = '/tmp/subiquity-curtin-postinstall.log' -CONF_PREFIX = os.path.join(TMPDIR, 'subiquity-config-') -CURTIN_NETWORK_CONFIG_FILE = CONF_PREFIX + 'network.yaml' -CURTIN_STORAGE_CONFIG_FILE = CONF_PREFIX + 'storage.yaml' -CURTIN_REPORTING_CONFIG_FILE = CONF_PREFIX + 'reporting.yaml' -CURTIN_PRESERVED_CONFIG_FILE = CONF_PREFIX + 'storage-preserved.yaml' -POST_INSTALL_CONFIG_FILE = CONF_PREFIX + 'postinst.yaml' -CURTIN_CONFIGS = { - 'network': CURTIN_NETWORK_CONFIG_FILE, - 'storage': CURTIN_STORAGE_CONFIG_FILE, - 'postinstall': POST_INSTALL_CONFIG_FILE, - 'preserved': CURTIN_PRESERVED_CONFIG_FILE, - 'reporting': CURTIN_REPORTING_CONFIG_FILE, -} - -CURTIN_CONFIG_BASE = { - 'reporting': { - 'subiquity': { - 'type': 'print', - }, - }, - - 'partitioning_commands': { - 'builtin': 'curtin block-meta custom', - }, - } -# TODO, this should be moved to the in-target cloud-config seed so on first -# boot of the target, it reconfigures datasource_list to none for subsequent -# boots. -POST_INSTALL_CONFIG = { - 'write_files': { - 'postinst_metadata': { - 'path': 'var/lib/cloud/seed/nocloud-net/meta-data', - 'content': 'instance-id: inst-3011\n', - }, - 'postinst_userdata': { - 'path': 'var/lib/cloud/seed/nocloud-net/user-data', - # 'content' gets filled in later - }, - 'postinst_enable_cloudinit': { - 'path': 'etc/cloud/ds-identify.cfg', - 'content': 'policy: enabled\n', - }, - } - } - - -def curtin_userinfo_to_config(userinfo): - users_and_groups_path = os.path.join(os.environ.get("SNAP", "/does-not-exist"), "users-and-groups") - if os.path.exists(users_and_groups_path): - groups = open(users_and_groups_path).read().split() - else: - groups = ['admin'] - groups.append('sudo') - user = { - 'name': userinfo['username'], - 'gecos': userinfo['realname'], - 'passwd': userinfo['password'], - 'shell': '/bin/bash', - 'groups': groups, - 'lock-passwd': False, - } - if 'ssh_import_id' in userinfo: - user['ssh_import_id'] = [userinfo['ssh_import_id']] - return [user] - -def curtin_hostinfo_to_config(hostinfo): - return { - 'hostname': hostinfo['hostname'], - } - - -def curtin_write_postinst_config(userinfo): - cloud_init_config = { - 'users': curtin_userinfo_to_config(userinfo), - 'hostname': userinfo['hostname'], - } - userdata = '#cloud-config\n' + yaml.dump(cloud_init_config) - config = copy.deepcopy(POST_INSTALL_CONFIG) - config['write_files']['postinst_userdata']['content'] = userdata - with open(POST_INSTALL_CONFIG_FILE, 'w') as conf: - datestr = '# Autogenerated by SUbiquity: {} UTC\n'.format( - str(datetime.datetime.utcnow())) - conf.write(datestr) - conf.write(yaml.dump(config)) - - -def curtin_write_storage_actions(path, actions): - config = copy.deepcopy(CURTIN_CONFIG_BASE) - config['storage'] = { - 'version': 1, - 'config': actions, - } - datestr = '# Autogenerated by SUbiquity: {} UTC\n'.format( - str(datetime.datetime.utcnow())) - with open(path, 'w') as conf: - conf.write(datestr) - conf.write(yaml.dump(config)) - - -from collections import OrderedDict def setup_yaml(): """ http://stackoverflow.com/a/8661021 """ represent_dict_order = lambda self, data: self.represent_mapping('tag:yaml.org,2002:map', data.items()) yaml.add_representer(OrderedDict, represent_dict_order) + setup_yaml() -def curtin_write_network_config(netplan_config): - # As soon as curtin and cloud-init support v2 network config - # (RSN!) we can just pass this sensibly to curtin. But for now, - # just use write_files to install the config and make sure curtin - # and cloud-init doesn't do any networking stuff of their own - # accord. - curtin_conf = { - 'write_files': { - 'netplan': { - 'path': 'etc/netplan/00-installer.yaml', - 'content': netplan_config, - 'permissions': '0600', - }, - 'nonet': { - 'path': 'etc/cloud/cloud.cfg.d/subiquity-disable-cloudinit-networking.cfg', - 'content': 'network: {config: disabled}\n', - } - }, - 'network_commands': {'builtin': []}, - } - curtin_config = yaml.dump(curtin_conf, default_flow_style=False) - datestr = '# Autogenerated by SUbiquity: {} UTC\n'.format( - str(datetime.datetime.utcnow())) - with open(CURTIN_NETWORK_CONFIG_FILE, 'w') as conf: - conf.write(datestr) - conf.write(curtin_config) - -def curtin_write_reporting_config(reporting_url): - curtin_conf = { - 'reporting': { - 'subiquity': { - 'type': 'webhook', - 'endpoint': reporting_url, - }, - }, - } - curtin_config = yaml.dump(curtin_conf, default_flow_style=False) - datestr = '# Autogenerated by SUbiquity: {} UTC\n'.format( - str(datetime.datetime.utcnow())) - with open(CURTIN_REPORTING_CONFIG_FILE, 'w') as conf: - conf.write(datestr) - conf.write(curtin_config) - - def curtin_find_curtin(): for p in CURTIN_SEARCH_PATH: curtin = os.path.join(p, 'curtin') diff --git a/subiquity/models/subiquity.py b/subiquity/models/subiquity.py index b3467856..a4e42e76 100644 --- a/subiquity/models/subiquity.py +++ b/subiquity/models/subiquity.py @@ -13,6 +13,10 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import os +import uuid +import yaml + from subiquitycore.models.identity import IdentityModel from subiquitycore.models.network import NetworkModel @@ -28,3 +32,82 @@ class SubiquityModel: self.network = NetworkModel() self.filesystem = FilesystemModel(common['prober']) self.identity = IdentityModel() + + def _cloud_init_config(self): + user = self.identity.user + users_and_groups_path = os.path.join(os.environ.get("SNAP", "/does-not-exist"), "users-and-groups") + if os.path.exists(users_and_groups_path): + groups = open(users_and_groups_path).read().split() + else: + groups = ['admin'] + groups.append('sudo') + user_info = { + 'name': user.username, + 'gecos': user.realname, + 'passwd': user.password, + 'shell': '/bin/bash', + 'groups': groups, + 'lock-passwd': False, + } + if user.ssh_import_id is not None: + user_info['ssh_import_id'] = [user.ssh_import_id] + # XXX this should set up the locale too. + return { + 'users': [user_info], + 'hostname': self.identity.hostname, + } + + def _write_files_config(self): + # TODO, this should be moved to the in-target cloud-config seed so on first + # boot of the target, it reconfigures datasource_list to none for subsequent + # boots. + # (mwhudson does not entirely know what the above means!) + userdata = '#cloud-config\n' + yaml.dump(self._cloud_init_config()) + metadata = yaml.dump({'instance-id': str(uuid.uuid4())}) + return { + 'postinst_metadata': { + 'path': 'var/lib/cloud/seed/nocloud-net/meta-data', + 'content': metadata, + }, + 'postinst_userdata': { + 'path': 'var/lib/cloud/seed/nocloud-net/user-data', + 'content': userdata, + }, + 'postinst_enable_cloudinit': { + 'path': 'etc/cloud/ds-identify.cfg', + 'content': 'policy: enabled\n', + }, + } + + def render(self, install_step, reporting_url=None): + disk_actions = self.filesystem.render() + if install_step == "postinstall": + for a in disk_actions: + a['preserve'] = True + config = { + 'partitioning_commands': { + 'builtin': 'curtin block-meta custom', + }, + + 'reporting': { + 'subiquity': { + 'type': 'print', + }, + }, + + 'storage': { + 'version': 1, + 'config': disk_actions, + }, + } + if install_step == "install": + config.update(self.network.render()) + else: + config['write_files'] = self._write_files_config() + + if reporting_url is not None: + config['reporting']['subiquity'] = { + 'type': 'webhook', + 'endpoint': reporting_url, + } + return config diff --git a/subiquitycore/models/identity.py b/subiquitycore/models/identity.py index 5e17c646..47491ee8 100644 --- a/subiquitycore/models/identity.py +++ b/subiquitycore/models/identity.py @@ -60,13 +60,19 @@ class IdentityModel(object): def __init__(self): self._user = None + self._hostname = None def add_user(self, result): if result: self._user = LocalUser(result) + self._hostname = result['hostname'] else: self._user = None + @property + def hostname(self): + return self._hostname + @property def user(self): return self._user