Change subuiquity to run as firstboot for snappy.

Signed-off-by: Ryan Harper <ryan.harper@canonical.com>
This commit is contained in:
Ryan Harper 2016-06-22 14:19:54 -05:00
parent 2007d6401b
commit 346d4fa37d
23 changed files with 596 additions and 67 deletions

63
firstboot.md Normal file
View File

@ -0,0 +1,63 @@
Firstboot
---------
Firstboot is a tui that runs on the device's getty interfaces when a
system has not yet been configured. It displays the current network
configuration and allows user to modify that. It also collects
user information used to create a local user and import ssh public keys
Getting Started
---------------
Install pre-reqs:
% sudo apt-get update && sudo apt-get install qemu-system-x86 cloud-image-utils
Download the firstboot image and startup script
% wget http://people.canonical.com/~rharper/firstboot/firstboot.sh
% chmod +x ./firstboot.sh
% wget http://people.canonical.com/~rharper/firstboot/firstboot.raw.xz
% unxz firstboot.raw.xz
% ./firstboot.sh
This will launch the firstboot image under KVM using userspace networking
The main console will open in a new window, the serial console is available via
telnet session (telnet localhost 2447).
When firstboot displays the ssh URL, in the demo, since we're using qemu user
networking, we can't ssh directly to the VM, instead we redirect the guest's ssh
port 22 to host port 2222; this is a limitation of the demo. When ssh'ing to
the guest, use:
ssh -p 2222 <user>@localhost
How it works
------------
The firstboot program is launched after the getty service is available, and
disables getty on any tty and instead spawns the firstboot program. It will
remain available until one of the firstboot instances successfully completes.
After completion, firstboot will disable itself and re-enable getty services.
firstboot is based on subiquity, just pulling out a few of the panels and
reusing certain parts. The networking information is probed from the host
and allows user configuration. After completion of configuration, firstboot
uses the ``ip`` command to apply the new network config to the network devices
present. Long term, we'll supply network-config yaml to snappy or whatever
network configuration tool will be present and be responsible for bringing
networking up to the desired state.
For identity, we collect realname, username, password (and crypt it), and a
"ssh_import_id" URL. The ``ssh-import-id`` binary already supports both
launchpad (lp:) and github (gh:). In the demo, I added mock SSO support (sso:)
and this would trigger a call out to snappy login or what ever the right tool
to initiate a connection to the SSO for authentication and retrieval of the
user's ssh keys.
After collecting the input, we run ``ip``, ``useradd`` and ``ssh-import-id``
and display the current config, including ssh url. After selecting "Finish"
We restore the normal getty prompt from which the newly created user can login.

23
firstboot.sh Executable file
View File

@ -0,0 +1,23 @@
#!/bin/bash
BOOT=firstboot.raw
SEED=seed.img
[ ! -e ${SEED} ] && {
cat > user-data <<EOF
#cloud-config
password: passw0rd
chpasswd: { expire: False }
ssh_pwauth: True
EOF
echo "instance-id: $(uuidgen || echo i-abcdefg)" > meta-data
cloud-localds ${SEED} user-data meta-data
}
qemu-system-x86_64 -m 1024 --enable-kvm \
-snapshot \
-drive file=${BOOT},format=raw,if=virtio \
-net user -net nic,model=virtio \
-redir tcp:2222::22 \
-cdrom $SEED \
-monitor stdio \
-serial telnet:localhost:2447,nowait,server

39
firstboot@.service Normal file
View File

@ -0,0 +1,39 @@
[Unit]
Description=Ubuntu Snappy Firstboot Configuration Getty %I
After=systemd-user-sessions.service plymouth-quit-wait.service
ExecPreStart=systemctl stop getty@%I
After=rc-local.service
ExecStop=systemctl start getty@%I
Before=getty.target
IgnoreOnIsolate=yes
ConditionPathExists=/dev/tty0
ConditionPathExists=!/var/lib/firstboot/firstboot-complete
[Service]
Environment=PYTHONPATH=/home/ubuntu/firstboot
ExecStartPre=/bin/systemctl stop getty@%I
ExecStart=-/sbin/agetty -n --noclear -l /home/ubuntu/firstboot/bin/subiquity-tui %I $TERM
ExecStop=/bin/systemctl start getty@%I
#ExecStopPost=/bin/echo "Post stop, starting getty@%I"
#ExecStopPost=/bin/systemctl start getty@%I
Type=idle
Restart=always
RestartSec=0
UtmpIdentifier=%I
TTYPath=/dev/%I
TTYReset=yes
TTYVHangup=yes
TTYVTDisallocate=yes
KillMode=process
IgnoreSIGPIPE=no
SendsSIGHUP=yes
#KillMode=process
#Restart=always
#StandardInput=tty-force
#StandardOutput=tty
#StandardError=tty
[Install]
WantedBy=getty.target
DefaultInstance=tty1

32
serial-firstboot@.service Normal file
View File

@ -0,0 +1,32 @@
[Unit]
Description=Ubuntu Snappy Firstboot Configuration Getty %I
BindsTo=dev-%i.device
#After=getty@tty.service
After=dev-%i.device systemd-user-sessions.service plymouth-quit-wait.service
After=rc-local.service
ConditionPathExists=!/var/lib/firstboot/firstboot-complete
[Service]
Environment=PYTHONPATH=/home/ubuntu/firstboot
ExecStartPre=/sbin/systemctl stop serial-getty@%I
ExecStart=-/sbin/agetty -n --noclear -l /home/ubuntu/firstboot/bin/subiquity-tui %I $TERM
ExecStop=/sbin/systemctl start serial-getty@%I
Type=idle
Restart=always
UtmpIdentifier=%I
TTYPath=/dev/%I
TTYReset=yes
TTYVHangup=yes
KillMode=process
IgnoreSIGPIPE=no
SendsSIGHUP=yes
#TTYVTDisallocate=yes
#KillMode=process
#Restart=always
#StandardInput=tty-force
#StandardOutput=tty
#StandardError=tty
[Install]
WantedBy=getty.target

View File

@ -34,6 +34,7 @@ class ControllerPolicy:
self.opts = common['opts']
self.loop = common['loop']
self.prober = common['prober']
self.controllers = common['controllers']
def register_signals(self):
""" Defines signals associated with controller from model """

View File

@ -19,3 +19,4 @@ from .network import NetworkController # NOQA
from .filesystem import FilesystemController # NOQA
from .installprogress import InstallProgressController # NOQA
from .identity import IdentityController # NOQA
from .login import LoginController # NOQA

View File

@ -13,15 +13,17 @@
# 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 logging
from subiquity.controller import ControllerPolicy
from subiquity.models import IdentityModel
from subiquity.ui.views import IdentityView
from subiquity.ui.views import IdentityView, LoginView
log = logging.getLogger('subiquity.controllers.identity')
class IdentityController(ControllerPolicy):
def __init__(self, common):
super().__init__(common)
self.model = IdentityModel()
self.model = IdentityModel(self.opts)
def identity(self):
title = "Profile setup"
@ -29,4 +31,23 @@ class IdentityController(ControllerPolicy):
footer = ""
self.ui.set_header(title, excerpt)
self.ui.set_footer(footer, 40)
self.ui.set_body(IdentityView(self.model, self.signal))
self.ui.set_body(IdentityView(self.model, self.signal, self.opts))
def login(self):
log.debug("Identity login view")
title = ("Snappy Ubuntu Core Pre-ownership Configuration Complete")
footer = ("View configured user and device access methods")
self.ui.set_header(title)
self.ui.set_footer(footer)
net_model = self.controllers['Network'].model
configured_ifaces = net_model.get_configured_interfaces()
login_view = LoginView(self.model,
self.signal,
self.model.user,
configured_ifaces)
self.ui.set_body(login_view)

View File

@ -0,0 +1,32 @@
# Copyright 2015 Canonical, Ltd.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/>.
from subiquity.ui.views import LoginView
from subiquity.models import LoginModel
from subiquity.controller import ControllerPolicy
class LoginController(ControllerPolicy):
def __init__(self, common):
super().__init__(common)
self.model = LoginModel()
def login(self):
title = "Snappy Ubuntu Core Pre-ownership Configuration Complete"
excerpt = "Your device is now configured. Login details below."
self.ui.set_header(title, excerpt)
view = LoginView(self.model, self.signal, self.model.user)
self.ui.set_body(view)

View File

@ -24,6 +24,7 @@ from subiquity.ui.views import (NetworkView,
from subiquity.ui.dummy import DummyView
from subiquity.curtin import curtin_write_network_actions
from subiquity.curtin import curtin_apply_networking
log = logging.getLogger("subiquity.controller.network")
@ -52,7 +53,11 @@ class NetworkController(ControllerPolicy):
'curtin_write_network_actions')
return None
self.signal.emit_signal('menu:filesystem:main')
curtin_apply_networking(actions, dryrun=self.opts.dry_run)
#self.signal.emit_signal('menu:filesystem:main')
# switch to identity view
self.signal.emit_signal('menu:identity:main')
def set_default_route(self):
self.ui.set_header("Default route")

View File

@ -25,10 +25,10 @@ class WelcomeController(ControllerPolicy):
self.model = WelcomeModel()
def welcome(self):
title = "Wilkommen! Bienvenue! Welcome! Zdrastvutie! Welkom!"
excerpt = "Please choose your preferred language"
title = "Ubuntu Core - Firstboot Configuration"
excerpt = "Welcome to snappy Ubuntu Core, a transactionally updated Ubuntu. It's a brave new world here in snappy Ubuntu Core! This machine has not been configured. Please continue to configure this device for use."
footer = ("Use UP, DOWN arrow keys, and ENTER, to "
"select your language.")
"configure your device.")
self.ui.set_header(title, excerpt)
self.ui.set_footer(footer)
view = WelcomeView(self.model, self.signal)

View File

@ -52,7 +52,9 @@ class Controller:
"Filesystem": None,
"Identity": None,
"InstallProgress": None,
"Login": None,
}
self.common['controllers'] = self.controllers
def _connect_base_signals(self):
""" Connect signals used in the core controller

View File

@ -17,7 +17,9 @@ import datetime
import logging
import os
import subprocess
import time
import yaml
import subiquity.utils as utils
log = logging.getLogger("subiquity.curtin")
@ -88,6 +90,46 @@ POST_INSTALL_LIST = [
POST_INSTALL = '\n' + "\n".join(POST_INSTALL_LIST) + '\n'
def curtin_configure_user(userinfo, dryrun=False):
usercmds = []
usercmds += ["useradd -m -p {confirm_password} {username}".format(**userinfo)]
if 'ssh_import_id' in userinfo:
target = "/home/{username}/.ssh/authorized_keys".format(**userinfo)
userinfo.update({'target': target})
ssh_id = userinfo.get('ssh_import_id')
if ssh_id.startswith('sso'):
log.info('call out to snappyd login')
else:
ssh_import_id = "ssh-import-id -o "
ssh_import_id += "{target} {ssh_import_id}".format(**userinfo)
usercmds += [ssh_import_id]
if not dryrun:
for cmd in usercmds:
utils.run_command(cmd.split(), shell=False)
# always run chown last
homedir = '/home/{username}'.format(**userinfo)
retries = 10
while not os.path.exists(homedir) and retries > 0:
log.debug('waiting on homedir')
retries -= 1
time.sleep(0.2)
if retries <= 0:
raise ValueError('Failed to create homedir')
chown = "chown {username}.{username} -R /home/{username}".format(**userinfo)
utils.run_command(chown.split(), shell=False)
# add sudo rule
with open('/etc/sudoers.d/firstboot-user', 'w') as fh:
fh.write('# firstboot config added user\n\n')
fh.write('{username} ALL=(ALL) NOPASSWD:ALL\n'.format(**userinfo))
else:
log.info('dry-run, skiping user configuration')
def curtin_userinfo_to_config(userinfo):
user_template = ' - default\\n' + \
' - name: {username}\\n' + \
@ -108,9 +150,10 @@ def curtin_hostinfo_to_config(hostinfo):
def curtin_write_postinst_config(userinfo):
# firstboot doesn't get hostinfo; but it's still present in the template
config = {
'users': curtin_userinfo_to_config(userinfo),
'hostinfo': curtin_hostinfo_to_config(userinfo),
'hostinfo': '',
}
with open(POST_INSTALL_CONFIG_FILE, 'w') as conf:
@ -151,6 +194,25 @@ def curtin_write_network_actions(actions):
conf.close()
def curtin_apply_networking(actions, dryrun=True):
log.info('Applying network actions:\n%s', actions)
network_commands = []
for entry in actions:
if entry['type'] == 'physical':
for subnet in entry.get('subnets', []):
if subnet['type'] == 'static':
cmd = "ifconfig %s %s" % (entry['name'], subnet['address'])
if 'netmask' in subnet:
cmd += " netmask %s" % subnet['netmask']
cmd += " up"
network_commands += [cmd]
for cmd in network_commands:
log.info('Running command: [%s]', cmd)
if not dryrun:
utils.run_command(cmd.split(), shell=False)
def curtin_write_preserved_actions(actions):
''' caller must use models.actions.preserve_action on
all elements of the actions'''

View File

@ -20,5 +20,6 @@ from .welcome import WelcomeModel # NOQA
from .identity import IdentityModel # NOQA
from .installprogress import InstallProgressModel # NOQA
from .iscsi_disk import IscsiDiskModel # NOQA
from .login import LoginModel # NOQA
from .raid import RaidModel # NOQA
from .ceph_disk import CephDiskModel # NOQA

View File

@ -20,6 +20,39 @@ from subiquity.utils import crypt_password
log = logging.getLogger('subiquity.models.identity')
class LocalUser(object):
def __init__(self, result):
self._realname = result.get('realname')
self._username = result.get('username')
self._password = result.get('password')
self._cpassword = result.get('confirm_password')
self._ssh_import_id = None
if 'ssh_import_id' in result:
self._ssh_import_id = result.get('ssh_import_id')
@property
def realname(self):
return self._realname
@property
def username(self):
return self._username
@property
def password(self):
return self._password
@property
def cpassword(self):
return self._cpassword
@property
def ssh_import_id(self):
return self._ssh_import_id
def __repr__(self):
return "%s <%s>" % (self._realname, self._username)
class IdentityModel(ModelPolicy):
""" Model representing user identity
@ -27,7 +60,10 @@ class IdentityModel(ModelPolicy):
signals = [
("Identity view",
'menu:identity:main',
'identity')
'identity'),
("Login view",
'menu:identity:login:main',
'login')
]
identity_menu = [
@ -42,6 +78,9 @@ class IdentityModel(ModelPolicy):
"validate_confirm_password")
]
def __init__(self, opts):
self.opts = opts
def get_signals(self):
return self.signals
@ -53,8 +92,18 @@ class IdentityModel(ModelPolicy):
if x == selection:
return y
def add_user(self, result):
if result:
self._user = LocalUser(result)
else:
self._user = None
@property
def user(self):
return self._user
def encrypt_password(self, passinput):
return crypt_password(passinput)
def __repr__(self):
return "<Username: {}>".format(self.username)
return "<LocalUser: {}>".format(self.user)

51
subiquity/models/login.py Normal file
View File

@ -0,0 +1,51 @@
# Copyright 2015 Canonical, Ltd.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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 logging
from subiquity.model import ModelPolicy
log = logging.getLogger('subiquity.login')
class LoginModel(ModelPolicy):
""" Model representing Final login screen
"""
prev_signal = 'menu:identity:main'
signals = [
("Login view",
'menu:login:main',
'login')
]
configured_logins = [
'local',
'ssh'
]
def get_signals(self):
return self.signals
def get_menu(self):
return self.configured_logins
def get_signal_by_name(self, selection):
for x, y, z in self.get_menu():
if x == selection:
return y
def __repr__(self):
return "<Configured: {}>".format(self.configured_logins)

View File

@ -68,6 +68,9 @@ class Networkdev():
log.debug('Post config action: {}'.format(self.action.get()))
def __repr__(self):
return "%s: %s" % (self.ifname, self.ip)
@property
def is_configured(self):
return (self.action is not None and

View File

@ -30,5 +30,8 @@ class MenuSelectButton(Button):
confirm_btn = partial(PlainButton, label="Confirm", on_press=None)
cancel_btn = partial(PlainButton, label="Cancel", on_press=None)
done_btn = partial(PlainButton, label="Done", on_press=None)
finish_btn = partial(PlainButton, label="Finish", on_press=None)
ok_btn = partial(PlainButton, label="OK", on_press=None)
continue_btn = partial(PlainButton, label="Continue", on_press=None)
reset_btn = partial(PlainButton, label="Reset", on_press=None)
menu_btn = partial(MenuSelectButton, on_press=None)

View File

@ -32,3 +32,4 @@ from .installpath import InstallpathView # NOQA
from .installprogress import ProgressView # NOQA
from .welcome import WelcomeView # NOQA
from .identity import IdentityView # NOQA
from .login import LoginView # NOQA

View File

@ -27,7 +27,8 @@ from subiquity.ui.interactive import (PasswordEditor,
UsernameEditor)
from subiquity.ui.utils import Padding, Color
from subiquity.view import ViewPolicy
from subiquity.curtin import curtin_write_postinst_config
from subiquity.curtin import (curtin_write_postinst_config,
curtin_configure_user)
log = logging.getLogger("subiquity.views.identity")
@ -39,9 +40,10 @@ USERNAME_MAXLEN = 32
class IdentityView(ViewPolicy):
def __init__(self, model, signal):
def __init__(self, model, signal, opts):
self.model = model
self.signal = signal
self.opts = opts
self.items = []
self.realname = RealnameEditor(caption="")
self.hostname = UsernameEditor(caption="")
@ -82,25 +84,6 @@ class IdentityView(ViewPolicy):
],
dividechars=4
),
Columns(
[
("weight", 0.2, Text("Your server's name:",
align="right")),
("weight", 0.3,
Color.string_input(self.hostname,
focus_map="string_input focus"))
],
dividechars=4
),
Columns(
[
("weight", 0.2, Text("", align="right")),
("weight", 0.3, Color.info_minor(
Text("The name it uses when it talks to "
"other computers", align="left"))),
],
dividechars=4
),
Columns(
[
("weight", 0.2, Text("Pick a username:", align="right")),
@ -145,6 +128,7 @@ class IdentityView(ViewPolicy):
("weight", 0.2, Text("", align="right")),
("weight", 0.3, Color.info_minor(
Text("Input your SSH user id from "
"Ubuntu SSO (sso:email), "
"Launchpad (lp:username) or "
"Github (gh:username).",
align="left"))),
@ -168,17 +152,6 @@ class IdentityView(ViewPolicy):
self.realname.value = ""
return
if len(self.hostname.value) < 1:
self.error.set_text("Server name missing.")
self.hostname.value = ""
return
if len(self.hostname.value) > HOSTNAME_MAXLEN:
self.error.set_text("Server name too long, must be < " +
str(HOSTNAME_MAXLEN))
self.hostname.value = ""
return
if len(self.username.value) < 1:
self.error.set_text("Username missing.")
self.username.value = ""
@ -213,7 +186,6 @@ class IdentityView(ViewPolicy):
log.debug("*crypted* User input: {} {} {}".format(
self.username.value, cpassword, cpassword))
result = {
"hostname": self.hostname.value,
"realname": self.realname.value,
"username": self.username.value,
"password": cpassword,
@ -230,17 +202,20 @@ class IdentityView(ViewPolicy):
return
log.debug("User input: {}".format(result))
self.model.add_user(result)
try:
curtin_write_postinst_config(result)
curtin_configure_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')
'curtin_write_postinst_config', result)
return None
self.signal.emit_signal('installprogress:wrote-postinstall')
# show progress view
self.signal.emit_signal('menu:installprogress:main')
# show login view
self.signal.emit_signal('menu:identity:login:main')
def cancel(self, button):
self.signal.prev_signal()

129
subiquity/ui/views/login.py Normal file
View File

@ -0,0 +1,129 @@
# Copyright 2015 Canonical, Ltd.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/>.
""" Login
Login provides user with language selection
"""
import copy
import logging
import os
from urwid import (ListBox, Pile, BoxAdapter, Text)
from subiquity.ui.lists import SimpleList
from subiquity.ui.buttons import finish_btn
from subiquity.ui.utils import Padding, Color
from subiquity.view import ViewPolicy
from subiquity import utils
log = logging.getLogger("subiquity.views.login")
class LoginView(ViewPolicy):
def __init__(self, model, signal, user, ifaces):
self.model = model
self.signal = signal
self.user = user
self.ifaces = ifaces
self.items = []
self.body = [
Padding.line_break(""),
Padding.line_break(""),
Padding.line_break(""),
Padding.center_79(self._build_model_inputs()),
Padding.line_break(""),
Padding.fixed_10(self._build_buttons())
]
super().__init__(ListBox(self.body))
def _build_buttons(self):
self.buttons = [
Color.button(finish_btn(on_press=self.done),
focus_map='button focus'),
]
return Pile(self.buttons)
def auth_name(self, idstr):
# lp:<id>
# gh:<id>
# sso:<id>
auth_type = idstr.split(":")[0]
auth_to_name = {
'lp': 'Launchpad',
'gh': 'Github',
'sso': 'Ubuntu SSO'
}
return auth_to_name.get(auth_type, 'Unknown Authenication')
def _build_model_inputs(self):
"""
This device is registered to Ryan Harper. Ryan Harper added
a user, raharper, to the device for local access on the console.
Remote access was enabled via authentication with Launchpad as
lp:raharper and public ssh keys were added to the system for
remote access.
Ryan Harper can remotely connect to this system via SSH:
ssh rharper@192.168.11.58
ssh rharper@192.168.11.44
"""
local_tpl = (
"This device is registered to {realname}. {realname} added a"
" user, <{username}> to the device for access.")
remote_tpl = (
"\n\nRemote access was enabled via authentication with {auth} user"
" <{ssh_import_id}>.\nPublic SSH keys were added to the device "
"for remote access.\n\n{realname} can connect remotely to this "
"device via SSH:")
sl = []
ssh = []
user = self.model.user
login_info = {
'realname': user.realname,
'username': user.username,
}
login_info.update({'auth': self.auth_name(user.ssh_import_id),
'ssh_import_id': user.ssh_import_id.split(":")[-1]})
print(login_info)
login_text = local_tpl.format(**login_info)
if user.ssh_import_id:
login_text += remote_tpl.format(**login_info)
for iface in self.ifaces:
ip = str(iface.ip).split("/")[0]
ssh_iface = " ssh %s@%s" % (user.username, ip)
ssh += [Padding.center_50(Text(ssh_iface))]
sl += [Text(login_text),
Padding.line_break("")] + ssh
return Pile(sl)
def confirm(self, result):
self.done()
def done(self, button):
# mark ourselves complete
utils.mark_firstboot_complete()
# disable the UI service restoring getty service
utils.disable_first_boot_service()
self.signal.emit_signal('quit')

View File

@ -46,23 +46,26 @@ class NetworkView(ViewPolicy):
Padding.fixed_10(self._build_buttons()),
]
# FIXME determine which UX widget should have focus
super().__init__(ListBox(self.body))
self.lb = ListBox(self.body)
self.lb.set_focus(4) # _build_buttons
super().__init__(self.lb)
def _build_buttons(self):
cancel = cancel_btn(on_press=self.cancel)
done = done_btn(on_press=self.done)
cancel = Color.button(cancel_btn(on_press=self.cancel),
focus_map='button focus')
done = Color.button(done_btn(on_press=self.done),
focus_map='button focus')
self.default_focus = done
buttons = [
Color.button(done, focus_map='button focus'),
Color.button(cancel, focus_map='button focus')
]
return Pile(buttons)
buttons = [done, cancel]
return Pile(buttons, focus_item=done)
def _build_model_inputs(self):
log.info("probing for network devices")
self.model.probe_network()
ifaces = self.model.get_all_interface_names()
ifname_width = 4 # default padding
ifname_width = 8 # default padding
col_1 = []
for iface in ifaces:
@ -70,7 +73,7 @@ class NetworkView(ViewPolicy):
Color.menu_button(
menu_btn(label=iface,
on_press=self.on_net_dev_press),
focus_map='menu_button focus'))
focus_map='button focus'))
col_1.append(Text("")) # vertical holder for ipv6 status
col_1.append(Text("")) # vertical holder for ipv4 status
col_1 = BoxAdapter(SimpleList(col_1),
@ -114,8 +117,8 @@ class NetworkView(ViewPolicy):
col_2 = BoxAdapter(SimpleList(col_2, is_selectable=False),
height=len(col_2))
ifname_width += len(max(ifaces, key=len))
if ifname_width > 14:
ifname_width = 14
if ifname_width > 20:
ifname_width = 20
else:
col_2 = Pile([Text("No network interfaces detected.")])
@ -152,7 +155,7 @@ class NetworkView(ViewPolicy):
Color.menu_button(
menu_btn(label=opt,
on_press=self.additional_menu_select),
focus_map='menu_button focus'))
focus_map='button focus'))
return Pile(opts)
def additional_menu_select(self, result):

View File

@ -21,7 +21,7 @@ Welcome provides user with language selection
import logging
from urwid import (ListBox, Pile, BoxAdapter)
from subiquity.ui.lists import SimpleList
from subiquity.ui.buttons import menu_btn, cancel_btn
from subiquity.ui.buttons import menu_btn, ok_btn, cancel_btn
from subiquity.ui.utils import Padding, Color
from subiquity.view import ViewPolicy
@ -33,8 +33,8 @@ class WelcomeView(ViewPolicy):
self.model = model
self.signal = signal
self.items = []
#Padding.center_50(self._build_model_inputs()),
self.body = [
Padding.center_50(self._build_model_inputs()),
Padding.line_break(""),
Padding.fixed_10(self._build_buttons())
]
@ -42,7 +42,7 @@ class WelcomeView(ViewPolicy):
def _build_buttons(self):
self.buttons = [
Color.button(cancel_btn(on_press=self.cancel),
Color.button(ok_btn(on_press=self.confirm),
focus_map='button focus'),
]
return Pile(self.buttons)
@ -59,8 +59,8 @@ class WelcomeView(ViewPolicy):
def confirm(self, result):
self.model.selected_language = result.label
log.debug('calling installpath')
self.signal.emit_signal('menu:installpath:main')
log.debug('calling network')
self.signal.emit_signal('menu:network:main')
def cancel(self, button):
raise SystemExit("No language selected, exiting as there are no "

View File

@ -18,8 +18,9 @@ import errno
import logging
import os
import random
import sys
import yaml
from subprocess import Popen, PIPE
from subprocess import Popen, PIPE, call
from subiquity.async import Async
log = logging.getLogger("subiquity.utils")
@ -107,7 +108,7 @@ def run_command_async(cmd, timeout=None):
return Async.pool.submit(run_command, cmd, timeout)
def run_command(command, timeout=None):
def run_command(command, timeout=None, shell=False):
""" Execute command through system shell
:param command: command to run
:param timeout: (optional) use 'timeout' to limit time. default 300
@ -131,7 +132,7 @@ def run_command(command, timeout=None):
# http://stackoverflow.com/ +
# questions/27022810/urwid-watch-file-blocks-keypress
r, w = os.pipe()
p = Popen(command, shell=True,
p = Popen(command, shell=shell,
stdin=r, stdout=PIPE, stderr=PIPE,
bufsize=-1, env=cmd_env, close_fds=True)
os.close(w)
@ -213,3 +214,35 @@ def sudo_user():
"""
sudo_user = os.getenv('SUDO_USER', None)
return sudo_user
def mark_firstboot_complete():
""" Touch our firstboot-complete eyecatcher """
log.info('marking firstboot service complete')
firstboot = '/var/lib/firstboot/firstboot-complete'
if not os.path.exists(os.path.dirname(firstboot)):
os.makedirs(os.path.dirname(firstboot))
with open(firstboot, 'w') as fp:
os.utime(fp.name, None)
fp.close()
def disable_first_boot_service():
""" Stop firstboot service; which also restores getty service """
log.info('disabling first boot service')
tty = os.ttyname(sys.stdout.fileno()).split("/")[-1]
if not tty.startswith('tty'):
log.debug('tty is not tty: %s , skipping service shutdown', tty)
return
cmd = "systemctl stop firstboot@%s" % tty
log.info("disabling firstboot service with %s", cmd)
fid = os.fork()
if fid == 0:
try:
subprocess.call([cmd])
os._exit(0)
except:
log.warn("%s returned non-zero" % cmd)
os._exit(1)
return