Merge pull request #268 from CanonicalLtd/mwhudson/reorg-config-rendering

move curtin config rendering to a method on SubiquityModel
This commit is contained in:
Michael Hudson-Doyle 2017-11-24 12:12:37 +13:00 committed by GitHub
commit fadd568323
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 137 additions and 229 deletions

View File

@ -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')

View File

@ -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')

View File

@ -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)

View File

@ -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')

View File

@ -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

View File

@ -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