Merge pull request #268 from CanonicalLtd/mwhudson/reorg-config-rendering
move curtin config rendering to a method on SubiquityModel
This commit is contained in:
commit
fadd568323
|
@ -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')
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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)
|
||||
|
|
|
@ -13,173 +13,29 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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')
|
||||
|
|
|
@ -13,6 +13,10 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue