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