merge master

This commit is contained in:
Michael Hudson-Doyle 2018-05-25 11:06:23 +12:00
commit 293793fa49
80 changed files with 1200 additions and 1454 deletions

View File

@ -6,6 +6,8 @@ PYTHONSRC=$(NAME)
PYTHONPATH=$(shell pwd):$(shell pwd)/probert
PROBERTDIR=./probert
PROBERT_REPO=https://github.com/CanonicalLtd/probert
export PYTHONPATH
CWD := $(shell pwd)
ifneq (,$(MACHINE))
MACHARGS=--machine=$(MACHINE)
@ -28,15 +30,18 @@ dryrun: probert i18n
$(MAKE) ui-view DRYRUN="--dry-run --uefi"
ui-view:
(PYTHONPATH=$(PYTHONPATH) bin/$(PYTHONSRC)-tui $(DRYRUN) $(MACHARGS))
(bin/$(PYTHONSRC)-tui $(DRYRUN) $(MACHARGS))
ui-view-serial:
(TERM=att4424 PYTHONPATH=$(PYTHONPATH) bin/$(PYTHONSRC)-tui $(DRYRUN) --serial)
(TERM=att4424 bin/$(PYTHONSRC)-tui $(DRYRUN) --serial)
lint:
echo "Running flake8 lint tests..."
python3 /usr/bin/flake8 bin/$(PYTHONSRC)-tui --ignore=F403
python3 /usr/bin/flake8 --exclude $(PYTHONSRC)/tests/ $(PYTHONSRC) --ignore=F403
lint: pep8 pyflakes3
pep8:
@$(CWD)/scripts/run-pep8
pyflakes3:
@$(CWD)/scripts/run-pyflakes3
unit:
echo "Running unit tests..."
@ -52,3 +57,5 @@ probert:
clean:
./debian/rules clean
.PHONY: lint pyflakes3 pep8

View File

@ -17,7 +17,6 @@
import argparse
import sys
import logging
import os
import signal
from subiquitycore.log import setup_logger
from subiquitycore import __version__ as VERSION
@ -39,9 +38,11 @@ checks:
- /sys
'''
def parse_options(argv):
parser = argparse.ArgumentParser(
description='console-conf - Pre-Ownership Configuration for Ubuntu Core',
description=(
'console-conf - Pre-Ownership Configuration for Ubuntu Core'),
prog='console-conf')
parser.add_argument('--dry-run', action='store_true',
dest='dry_run',
@ -52,13 +53,15 @@ def parse_options(argv):
parser.add_argument('--machine-config', metavar='CONFIG',
dest='machine_config',
help="Don't Probe. Use probe data file")
parser.add_argument('--screens', action='append', dest='screens', default=[])
parser.add_argument('--screens', action='append', dest='screens',
default=[])
parser.add_argument('--answers')
return parser.parse_args(argv)
LOGDIR = "/var/log/console-conf/"
def main():
opts = parse_options(sys.argv[1:])
global LOGDIR
@ -89,5 +92,6 @@ def main():
interface.run()
if __name__ == '__main__':
sys.exit(main())

View File

@ -47,10 +47,12 @@ checks:
- /usr/bin/curtin
'''
class ClickAction(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
namespace.scripts.append("c(" + repr(values) + ")")
def parse_options(argv):
parser = argparse.ArgumentParser(
description='SUbiquity - Ubiquity for Servers',
@ -67,9 +69,14 @@ def parse_options(argv):
parser.add_argument('--uefi', action='store_true',
dest='uefi',
help='run in uefi support mode')
parser.add_argument('--screens', action='append', dest='screens', default=[])
parser.add_argument('--script', metavar="SCRIPT", action='append', dest='scripts', default=[], help='Execute SCRIPT in a namespace containing view helpers and "ui"')
parser.add_argument('--click', metavar="PAT", action=ClickAction, help='Synthesize a click on a button matching PAT')
parser.add_argument('--screens', action='append', dest='screens',
default=[])
parser.add_argument('--script', metavar="SCRIPT", action='append',
dest='scripts', default=[],
help=('Execute SCRIPT in a namespace containing view '
'helpers and "ui"'))
parser.add_argument('--click', metavar="PAT", action=ClickAction,
help='Synthesize a click on a button matching PAT')
parser.add_argument('--answers')
parser.add_argument(
'--snaps-from-examples', action='store_true',
@ -85,6 +92,7 @@ LOGDIR = "/var/log/installer/"
AUTO_ANSWERS_FILE = "/subiquity_config/answers.yaml"
def main():
opts = parse_options(sys.argv[1:])
global LOGDIR
@ -119,5 +127,6 @@ def main():
subiquity_interface.run()
if __name__ == '__main__':
sys.exit(main())

View File

@ -15,4 +15,7 @@
""" Console-Conf """
import subiquitycore.i18n
from subiquitycore import i18n
__all__ = [
'i18n',
]

View File

@ -15,7 +15,13 @@
""" console-conf controllers """
from .welcome import WelcomeController # NOQA
from subiquitycore.controllers.network import NetworkController # NOQA
from subiquitycore.controllers.login import LoginController # NOQA
from .identity import IdentityController # NOQA
from .identity import IdentityController
from subiquitycore.controllers.login import LoginController
from subiquitycore.controllers.network import NetworkController
from .welcome import WelcomeController
__all__ = [
'IdentityController',
'LoginController',
'NetworkController',
'WelcomeController',
]

View File

@ -25,6 +25,7 @@ from console_conf.ui.views import IdentityView, LoginView
log = logging.getLogger('console_conf.controllers.identity')
def get_device_owner():
""" Check if device is owned """
@ -45,6 +46,7 @@ def get_device_owner():
return result
return None
def host_key_fingerprints():
"""Query sshd to find the host keys and then fingerprint them.
@ -92,13 +94,17 @@ def host_key_info():
return []
if len(fingerprints) == 1:
[(keytype, fingerprint)] = fingerprints
return single_host_key_tmpl.format(keytype=keytype, fingerprint=fingerprint)
return single_host_key_tmpl.format(keytype=keytype,
fingerprint=fingerprint)
lines = [host_keys_intro]
longest_type = max([len(keytype) for keytype, _ in fingerprints])
for keytype, fingerprint in fingerprints:
lines.append(host_key_tmpl.format(keytype=keytype, fingerprint=fingerprint, width=longest_type))
lines.append(host_key_tmpl.format(keytype=keytype,
fingerprint=fingerprint,
width=longest_type))
return "".join(lines)
login_details_tmpl = """\
Ubuntu Core 16 on {first_ip} ({tty_name})
{host_key_info}
@ -107,6 +113,7 @@ To login:
Personalize your account at https://login.ubuntu.com.
"""
login_details_tmpl_no_ip = """\
Ubuntu Core 16 on <no ip address> ({tty_name})
@ -116,18 +123,22 @@ supposed to be a DHCP server running on your network?)
Personalize your account at https://login.ubuntu.com.
"""
def write_login_details(fp, username, ips):
sshcommands = "\n"
for ip in ips:
sshcommands += " ssh %s@%s\n"%(username, ip)
tty_name = os.ttyname(0)[5:] # strip off the /dev/
sshcommands += " ssh %s@%s\n" % (username, ip)
tty_name = os.ttyname(0)[5:] # strip off the /dev/
if len(ips) == 0:
fp.write(login_details_tmpl_no_ip.format(
sshcommands=sshcommands, tty_name=tty_name))
else:
first_ip = ips[0]
fp.write(login_details_tmpl.format(
sshcommands=sshcommands, host_key_info=host_key_info(), tty_name=tty_name, first_ip=first_ip))
fp.write(login_details_tmpl.format(sshcommands=sshcommands,
host_key_info=host_key_info(),
tty_name=tty_name,
first_ip=first_ip))
def write_login_details_standalone():
owner = get_device_owner()
@ -135,7 +146,8 @@ def write_login_details_standalone():
print("No device owner details found.")
return 0
from probert import network
from subiquitycore.models.network import NETDEV_IGNORED_IFACE_NAMES, NETDEV_IGNORED_IFACE_TYPES
from subiquitycore.models.network import (NETDEV_IGNORED_IFACE_NAMES,
NETDEV_IGNORED_IFACE_TYPES)
import operator
observer = network.UdevObserver()
observer.start()
@ -153,7 +165,8 @@ def write_login_details_standalone():
print("Ubuntu Core 16 on <no ip address> ({})".format(tty_name))
print()
print("You cannot log in until the system has an IP address.")
print("(Is there supposed to be a DHCP server running on your network?)")
print("(Is there supposed to be a DHCP server running on "
"your network?)")
return 2
write_login_details(sys.stdout, owner['username'], ips)
return 0
@ -175,8 +188,11 @@ class IdentityController(BaseController):
device_owner = get_device_owner()
if device_owner is not None:
self.model.add_user(device_owner)
key_file = os.path.join(device_owner['homedir'], ".ssh/authorized_keys")
self.model.user.fingerprints = run_command(['ssh-keygen', '-lf', key_file]).stdout.replace('\r', '').splitlines()
key_file = os.path.join(device_owner['homedir'],
".ssh/authorized_keys")
self.model.user.fingerprints = (
run_command(['ssh-keygen', '-lf',
key_file]).stdout.replace('\r', '').splitlines())
self.login()
def identity_done(self, email):
@ -190,10 +206,12 @@ class IdentityController(BaseController):
else:
self.ui.frame.body.progress.set_text("Contacting store...")
self.loop.draw_screen()
cp = run_command(["snap", "create-user", "--sudoer", "--json", email])
cp = run_command(
["snap", "create-user", "--sudoer", "--json", email])
self.ui.frame.body.progress.set_text("")
if cp.returncode != 0:
self.ui.frame.body.error.set_text("Creating user failed:\n" + cp.stderr)
self.ui.frame.body.error.set_text(
"Creating user failed:\n" + cp.stderr)
return
else:
data = json.loads(cp.stdout)
@ -231,7 +249,8 @@ class IdentityController(BaseController):
def login_done(self):
if not self.opts.dry_run:
# stop the console-conf services (this will kill the current process).
# stop the console-conf services (this will kill the
# current process).
disable_console_conf()
self.signal.emit_signal('quit')

View File

@ -2,6 +2,7 @@
from subiquitycore.models.network import NetworkModel
from subiquitycore.models.identity import IdentityModel
class ConsoleConfModel:
"""The overall model for console-conf."""

View File

@ -15,8 +15,11 @@
""" ConsoleConf UI Views """
from .identity import IdentityView # NOQA
from .login import LoginView # NOQA
from .welcome import WelcomeView # NOQA
#from .network import NetworkView # NOQA
#from .network_configure_interface import NetworkConfigureInterfaceView # NOQA
from .identity import IdentityView
from .login import LoginView
from .welcome import WelcomeView
__all__ = [
'IdentityView',
'LoginView',
'WelcomeView',
]

View File

@ -63,7 +63,10 @@ class IdentityView(BaseView):
ListBox([
self._build_model_inputs(),
Padding.line_break(""),
Padding.center_79(Color.info_minor(Text("If you do not have an account, visit https://login.ubuntu.com to create one."))),
Padding.center_79(
Color.info_minor(
Text("If you do not have an account, visit "
"https://login.ubuntu.com to create one."))),
Padding.line_break(""),
Padding.center_90(Color.info_error(self.error)),
Padding.center_90(self.progress),

View File

@ -49,7 +49,6 @@ class LoginView(BaseView):
])),
]))
def _build_buttons(self):
return [
done_btn("Done", on_press=self.done),

View File

@ -34,7 +34,9 @@ class WelcomeView(BaseView):
def __init__(self, controller):
self.controller = controller
super().__init__(Pile([
ListBox([Text('')]), # need to have a listbox or something else "stretchy" here or urwid complains.
# need to have a listbox or something else "stretchy" here or
# urwid complains.
ListBox([Text('')]),
('pack', button_pile([ok_btn("OK", on_press=self.confirm)])),
('pack', Text("")),
], focus_item=1))

32
scripts/run-pep8 Executable file
View File

@ -0,0 +1,32 @@
#!/bin/bash
# This file is part of subiquity. See LICENSE file for copyright and license info.
pycheck_dirs=(
"console_conf/"
"subiquity/"
"subiquitycore/"
"tests/"
)
bin_files=(
"bin/console-conf-tui"
"bin/subiquity-tui"
)
CR="
"
[ "$1" = "-v" ] && { verbose="$1"; shift; } || verbose=""
set -f
if [ $# -eq 0 ]; then unset IFS
IFS="$CR"
files=( "${bin_files[@]}" "${pycheck_dirs[@]}" )
unset IFS
else
files=( "$@" )
fi
myname=${0##*/}
cmd=( "${myname#run-}" $verbose "${files[@]}" )
echo "Running: " "${cmd[@]}" 1>&2
exec "${cmd[@]}"
# vi: ts=4 expandtab syntax=sh

34
scripts/run-pyflakes3 Executable file
View File

@ -0,0 +1,34 @@
#!/bin/bash
# This file is part of curtin. See LICENSE file for copyright and license info.
pycheck_dirs=(
"console_conf/"
"subiquity/"
"subiquitycore/"
"tests/"
)
bin_files=(
"bin/console-conf-tui"
"bin/subiquity-tui"
)
CR="
"
[ "$1" = "-v" ] && { verbose="$1"; shift; } || verbose=""
set -f
if [ $# -eq 0 ]; then unset IFS
IFS="$CR"
files=( "${bin_files[@]}" "${pycheck_dirs[@]}" )
unset IFS
else
files=( "$@" )
fi
cmd=( "python3" -m "pyflakes" "${files[@]}" )
echo "Running: " "${cmd[@]}" 1>&2
export PYFLAKES_BUILTINS="_"
exec "${cmd[@]}"
# vi: ts=4 expandtab syntax=sh

View File

@ -15,6 +15,9 @@
""" Subiquity """
__version__ = "0.0.5"
from subiquitycore import i18n
__all__ = [
'i18n',
]
import subiquitycore.i18n
__version__ = "0.0.5"

View File

@ -13,14 +13,25 @@
# 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 subiquitycore.controllers.login import LoginController # NOQA
from subiquitycore.controllers.network import NetworkController # NOQA
from .identity import IdentityController # NOQA
from .installpath import InstallpathController # NOQA
from .installprogress import InstallProgressController # NOQA
from .filesystem import FilesystemController # NOQA
from .keyboard import KeyboardController # NOQA
from .proxy import ProxyController # NOQA
from .filesystem import FilesystemController
from .identity import IdentityController
from .installpath import InstallpathController
from .installprogress import InstallProgressController
from .keyboard import KeyboardController
from .proxy import ProxyController
from subiquitycore.controllers.login import LoginController
from subiquitycore.controllers.network import NetworkController
from .snaplist import SnapListController
from .welcome import WelcomeController # NOQA
from .welcome import WelcomeController
__all__ = [
'FilesystemController',
'IdentityController',
'InstallpathController',
'InstallProgressController',
'KeyboardController',
'ProxyController',
'LoginController',
'NetworkController',
'SnapListController',
'WelcomeController',
]

View File

@ -18,7 +18,6 @@ import os
from subiquitycore.controller import BaseController
from subiquitycore.ui.dummy import DummyView
from subiquitycore.ui.error import ErrorView
from subiquity.models.filesystem import align_up, humanize_size
from subiquity.ui.views import (
@ -50,8 +49,6 @@ class FilesystemController(BaseController):
self.answers.setdefault('guided', False)
self.answers.setdefault('guided-index', 0)
self.answers.setdefault('manual', False)
# self.iscsi_model = IscsiDiskModel()
# self.ceph_model = CephDiskModel()
self.model.probe() # probe before we complete
def default(self):
@ -90,7 +87,8 @@ class FilesystemController(BaseController):
# Filesystem/Disk partition -----------------------------------------------
def partition_disk(self, disk):
log.debug("In disk partition view, using {} as the disk.".format(disk.label))
log.debug("In disk partition view, using "
"{} as the disk.".format(disk.label))
dp_view = DiskPartitionView(self.model, self, disk)
self.ui.set_body(dp_view)
@ -135,9 +133,10 @@ class FilesystemController(BaseController):
old_fs._mount = None
self.model._mounts.remove(mount)
if spec['fstype'].label is not None:
fs = self.model.add_filesystem(partition, spec['fstype'].label)
fs = self.model.add_filesystem(partition,
spec['fstype'].label)
if spec['mount']:
self.model.add_mount(fs, spec['mount'])
self.model.add_mount(fs, spec['mount'])
self.partition_disk(disk)
return
@ -149,19 +148,22 @@ class FilesystemController(BaseController):
if UEFI_GRUB_SIZE_BYTES*2 >= disk.size:
part_size = disk.size // 2
log.debug('Adding EFI partition first')
part = self.model.add_partition(disk=disk, size=part_size, flag='boot')
part = self.model.add_partition(disk=disk, size=part_size,
flag='boot')
fs = self.model.add_filesystem(part, 'fat32')
self.model.add_mount(fs, '/boot/efi')
else:
log.debug('Adding grub_bios gpt partition first')
part = self.model.add_partition(disk=disk, size=BIOS_GRUB_SIZE_BYTES, flag='bios_grub')
part = self.model.add_partition(disk=disk,
size=BIOS_GRUB_SIZE_BYTES,
flag='bios_grub')
disk.grub_device = True
# adjust downward the partition size (if necessary) to accommodate
# bios/grub partition
if spec['size'] > disk.free:
log.debug("Adjusting request down:" +
"{} - {} = {}".format(spec['size'], part.size,
"{} - {} = {}".format(spec['size'], part.size,
disk.free))
spec['size'] = disk.free
@ -196,32 +198,21 @@ class FilesystemController(BaseController):
full = p.device.free == 0
p.device._partitions.remove(p)
if full:
largest_part = max((part.size, part) for part in p.device._partitions)[1]
largest_part = max((part.size, part)
for part in p.device._partitions)[1]
largest_part.size += p.size
if disk.free < p.size:
largest_part = max((part.size, part) for part in disk._partitions)[1]
largest_part = max((part.size, part)
for part in disk._partitions)[1]
largest_part.size -= (p.size - disk.free)
disk._partitions.insert(0, p)
p.device = disk
self.partition_disk(disk)
def connect_iscsi_disk(self, *args, **kwargs):
# title = ("Disk and filesystem setup")
# excerpt = ("Connect to iSCSI cluster")
# self.ui.set_header(title, excerpt)
# self.ui.set_footer("")
# self.ui.set_body(IscsiDiskView(self.iscsi_model,
# self.signal))
self.ui.set_body(DummyView(self.signal))
def connect_ceph_disk(self, *args, **kwargs):
# title = ("Disk and filesystem setup")
# footer = ("Select available disks to format and mount")
# excerpt = ("Connect to Ceph storage cluster")
# self.ui.set_header(title, excerpt)
# self.ui.set_footer(footer)
# self.ui.set_body(CephDiskView(self.ceph_model,
# self.signal))
self.ui.set_body(DummyView(self.signal))
def create_volume_group(self, *args, **kwargs):
@ -266,7 +257,8 @@ class FilesystemController(BaseController):
def format_entire(self, disk):
log.debug("format_entire {}".format(disk.label))
afv_view = FormatEntireView(self.model, self, disk, lambda : self.partition_disk(disk))
afv_view = FormatEntireView(self.model, self, disk,
lambda: self.partition_disk(disk))
self.ui.set_body(afv_view)
def format_mount_partition(self, partition):

View File

@ -28,6 +28,7 @@ class FetchSSHKeysFailure(Exception):
self.message = message
self.output = output
class IdentityController(BaseController):
def __init__(self, common):
@ -37,10 +38,8 @@ class IdentityController(BaseController):
def default(self):
self.ui.set_body(IdentityView(self.model, self, self.opts))
if 'realname' in self.answers and \
'username' in self.answers and \
'password' in self.answers and \
'hostname' in self.answers:
if all(elem in self.answers for elem in
['realname', 'username', 'password', 'hostname']):
d = {
'realname': self.answers['realname'],
'username': self.answers['username'],
@ -62,7 +61,7 @@ class IdentityController(BaseController):
try:
self._fetching_proc.terminate()
except ProcessLookupError:
pass # It's OK if the process has already terminated.
pass # It's OK if the process has already terminated.
self._fetching_proc = None
def _bg_fetch_ssh_keys(self, user_spec, proc, ssh_import_id):
@ -79,8 +78,12 @@ class IdentityController(BaseController):
cp = utils.run_command(['ssh-keygen', '-lf-'], input=key_material)
if cp.returncode != 0:
return FetchSSHKeysFailure(_("ssh-keygen failed to show fingerprint of downloaded keys:"), p.stderr)
fingerprints = cp.stdout.replace("# ssh-import-id {} ".format(ssh_import_id), "").strip().splitlines()
return FetchSSHKeysFailure(_("ssh-keygen failed to show "
"fingerprint of downloaded keys:"),
cp.stderr)
fingerprints = (
cp.stdout.replace("# ssh-import-id {} ".format(ssh_import_id),
"").strip().splitlines())
return user_spec, key_material, fingerprints
@ -98,15 +101,20 @@ class IdentityController(BaseController):
user_spec, key_material, fingerprints = result
if 'ssh_import_id' in self.answers:
user_spec['ssh_keys'] = key_material.splitlines()
self.loop.set_alarm_in(0.0, lambda loop, ud: self.done(user_spec))
self.loop.set_alarm_in(0.0,
lambda loop, ud: self.done(user_spec))
else:
self.ui.frame.body.confirm_ssh_keys(user_spec, key_material, fingerprints)
self.ui.frame.body.confirm_ssh_keys(user_spec, key_material,
fingerprints)
def fetch_ssh_keys(self, user_spec, ssh_import_id):
log.debug("User input: %s, fetching ssh keys for %s", user_spec, ssh_import_id)
self._fetching_proc = utils.start_command(['ssh-import-id', '-o-', ssh_import_id])
log.debug("User input: %s, fetching ssh keys for %s",
user_spec, ssh_import_id)
self._fetching_proc = utils.start_command(['ssh-import-id', '-o-',
ssh_import_id])
self.run_in_bg(
lambda: self._bg_fetch_ssh_keys(user_spec, self._fetching_proc, ssh_import_id),
lambda: self._bg_fetch_ssh_keys(user_spec, self._fetching_proc,
ssh_import_id),
self._fetched_ssh_keys)
def done(self, user_spec):

View File

@ -15,8 +15,6 @@
import logging
import lsb_release
from subiquitycore.controller import BaseController
from subiquity.ui.views import InstallpathView, MAASView
@ -30,6 +28,7 @@ class InstallpathController(BaseController):
super().__init__(common)
self.model = self.base_model.installpath
self.answers = self.all_answers.get("Installpath", {})
self.release = None
def installpath(self):
self.ui.set_body(InstallpathView(self.model, self))
@ -63,7 +62,8 @@ class InstallpathController(BaseController):
"treat physical servers like virtual machines (instances) "
"in the cloud. Rather than having to manage each server "
"individually, MAAS turns your bare metal into an elastic "
"cloud-like resource. \n\nFor further details, see https://maas.io/."
"cloud-like resource. \n\n"
"For further details, see https://maas.io/."
)
self.ui.set_body(MAASView(self.model, self, title, excerpt))

View File

@ -42,6 +42,7 @@ log = logging.getLogger("subiquitycore.controller.installprogress")
TARGET = '/target'
class InstallState:
NOT_STARTED = 0
RUNNING = 1
@ -256,12 +257,14 @@ class InstallProgressController(BaseController):
self.install_state = InstallState.ERROR
if log_text is not None:
self.progress_view.add_log_line(log_text)
self.progress_view.set_status(('info_error', _("An error has occurred")))
self.progress_view.set_status(
('info_error', _("An error has occurred")))
self.progress_view.show_complete(True)
self.default()
def _bg_run_command_logged(self, cmd, env=None):
cmd = ['systemd-cat', '--level-prefix=false', '--identifier=' + self._log_syslog_identifier] + cmd
cmd = ['systemd-cat', '--level-prefix=false',
'--identifier=' + self._log_syslog_identifier] + cmd
return utils.run_command(cmd, env=env)
def _journal_event(self, event):
@ -292,7 +295,23 @@ class InstallProgressController(BaseController):
if event_type not in ['start', 'finish']:
return
if event_type == 'start':
<<<<<<< HEAD
self._install_event_start(event.get("CURTIN_MESSAGE", "??"))
||||||| merged common ancestors
message = event.get("CURTIN_MESSAGE", "??")
if not self.progress_view_showing is None:
self.footer_description.set_text(message)
self.progress_view.add_event(self._event_indent + message)
self._event_indent += " "
self.footer_spinner.start()
=======
message = event.get("CURTIN_MESSAGE", "??")
if self.progress_view_showing is not None:
self.footer_description.set_text(message)
self.progress_view.add_event(self._event_indent + message)
self._event_indent += " "
self.footer_spinner.start()
>>>>>>> master
if event_type == 'finish':
self._install_event_finish()
@ -305,7 +324,7 @@ class InstallProgressController(BaseController):
for identifier in identifiers:
args.append("SYSLOG_IDENTIFIER={}".format(identifier))
reader.add_match(*args)
#reader.seek_tail()
def watch():
if reader.process() != journal.APPEND:
return
@ -326,17 +345,20 @@ class InstallProgressController(BaseController):
if self.opts.dry_run:
log.debug("Installprogress: this is a dry-run")
config_location = os.path.join('.subiquity/', config_file_name)
curtin_cmd = [
"python3", "scripts/replay-curtin-log.py", "examples/curtin-events.json", self._event_syslog_identifier,
]
curtin_cmd = ["python3", "scripts/replay-curtin-log.py",
"examples/curtin-events.json",
self._event_syslog_identifier]
else:
log.debug("Installprogress: this is the *REAL* thing")
config_location = os.path.join('/var/log/installer', config_file_name)
curtin_cmd = ['curtin', '--showtrace', '-c', config_location, 'install']
config_location = os.path.join('/var/log/installer',
config_file_name)
curtin_cmd = ['curtin', '--showtrace', '-c',
config_location, 'install']
self._write_config(
config_location,
self.base_model.render(target=TARGET, syslog_identifier=self._event_syslog_identifier))
ident = self._event_syslog_identifier
self._write_config(config_location,
self.base_model.render(target=TARGET,
syslog_identifier=ident))
return curtin_cmd
@ -347,9 +369,14 @@ class InstallProgressController(BaseController):
self.progress_view = ProgressView(self)
self.footer_spinner = self.progress_view.spinner
self.ui.set_footer(urwid.Columns([('pack', urwid.Text(_("Install in progress:"))), (self.footer_description), ('pack', self.footer_spinner)], dividechars=1))
self.ui.set_footer(urwid.Columns(
[('pack', urwid.Text(_("Install in progress:"))),
(self.footer_description),
('pack', self.footer_spinner)], dividechars=1))
self.journal_listener_handle = self.start_journald_listener([self._event_syslog_identifier, self._log_syslog_identifier], self._journal_event)
self.journal_listener_handle = self.start_journald_listener(
[self._event_syslog_identifier, self._log_syslog_identifier],
self._journal_event)
curtin_cmd = self._get_curtin_command()
@ -459,9 +486,11 @@ class InstallProgressController(BaseController):
def copy_logs_to_target(self):
if self.opts.dry_run:
return
utils.run_command(['cp', '-aT', '/var/log/installer', '/target/var/log/installer'])
utils.run_command(['cp', '-aT', '/var/log/installer',
'/target/var/log/installer'])
try:
with open('/target/var/log/installer/installer-journal.txt', 'w') as output:
with open('/target/var/log/installer/installer-journal.txt',
'w') as output:
utils.run_command(
['journalctl'],
stdout=output, stderr=subprocess.STDOUT)
@ -490,7 +519,8 @@ class InstallProgressController(BaseController):
self.progress_view.title = _("Install complete!")
self.progress_view.footer = _("Thank you for using Ubuntu!")
elif self.install_state == InstallState.ERROR:
self.progress_view.title = _('An error occurred during installation')
self.progress_view.footer = _('Please report this error in Launchpad')
self.progress_view.title = (
_('An error occurred during installation'))
self.progress_view.footer = (
_('Please report this error in Launchpad'))
self.ui.set_body(self.progress_view)

View File

@ -22,6 +22,7 @@ from subiquity.ui.views import KeyboardView
log = logging.getLogger('subiquity.controllers.keyboard')
class KeyboardController(BaseController):
signals = [

View File

@ -14,13 +14,12 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
from subiquitycore.controller import BaseController
from subiquity.ui.views import WelcomeView
log = logging.getLogger('subiquity.controllers.welcome')
class WelcomeController(BaseController):
def __init__(self, common):

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 attr
import collections
import glob
import logging
@ -20,9 +21,6 @@ import math
import os
import sys
import attr
HUMAN_UNITS = ['B', 'K', 'M', 'G', 'T', 'P']
log = logging.getLogger('subiquity.models.filesystem')
@ -38,9 +36,9 @@ def humanize_size(size):
return "0B"
p = int(math.floor(math.log(size, 2) / 10))
# We want to truncate the non-integral part, not round to nearest.
s = "{:.17f}".format(size / 2**(10*p))
s = "{:.17f}".format(size / 2 ** (10 * p))
i = s.index('.')
s = s[:i+4]
s = s[:i + 4]
return s + HUMAN_UNITS[int(p)]
@ -61,7 +59,7 @@ def dehumanize_size(size):
if len(parts) > 2:
raise ValueError(_("{!r} is not valid input").format(size_in))
elif len(parts) == 2:
div = 10**len(parts[1])
div = 10 ** len(parts[1])
size = parts[0] + parts[1]
else:
div = 1
@ -73,8 +71,10 @@ def dehumanize_size(size):
if suffix is not None:
if suffix not in HUMAN_UNITS:
raise ValueError("unrecognized suffix {!r} in {!r}".format(size_in[-1], size_in))
mult = 2**(10*HUMAN_UNITS.index(suffix))
raise ValueError(
"unrecognized suffix {!r} in {!r}".format(size_in[-1],
size_in))
mult = 2 ** (10 * HUMAN_UNITS.index(suffix))
else:
mult = 1
@ -86,9 +86,10 @@ def dehumanize_size(size):
def id_factory(name):
i = 0
def factory():
nonlocal i
r = "%s-%s"%(name, i)
r = "%s-%s" % (name, i)
i += 1
return r
return attr.Factory(factory)
@ -128,10 +129,14 @@ class Disk:
name = attr.ib(default="")
grub_device = attr.ib(default=False)
_partitions = attr.ib(default=attr.Factory(list), repr=False) # [Partition]
_fs = attr.ib(default=None, repr=False) # Filesystem
# [Partition]
_partitions = attr.ib(default=attr.Factory(list), repr=False)
# Filesystem
_fs = attr.ib(default=None, repr=False)
def partitions(self):
return self._partitions
def fs(self):
return self._fs
@ -158,7 +163,8 @@ class Disk:
@property
def size(self):
return max(0, align_down(self._info.size) - (2<<20)) # The first and last megabyte of the disk are not usable.
# The first and last megabyte of the disk are not usable.
return max(0, align_down(self._info.size) - (2 << 20))
def desc(self):
return _("local disk")
@ -188,13 +194,15 @@ class Partition:
id = attr.ib(default=id_factory("part"))
type = attr.ib(default="partition")
device = attr.ib(default=None) # Disk
device = attr.ib(default=None) # Disk
size = attr.ib(default=None)
wipe = attr.ib(default=None)
flag = attr.ib(default=None)
preserve = attr.ib(default=False)
_fs = attr.ib(default=None, repr=False) # Filesystem
# Filesystem
_fs = attr.ib(default=None, repr=False)
def fs(self):
return self._fs
@ -212,14 +220,13 @@ class Partition:
return fs_obj.is_mounted
return False
@property
def _number(self):
return self.device._partitions.index(self) + 1
@property
def path(self):
return "%s%s"%(self.device.path, self._number)
return "%s%s" % (self.device.path, self._number)
@attr.s
@ -228,12 +235,13 @@ class Filesystem:
id = attr.ib(default=id_factory("fs"))
type = attr.ib(default="format")
fstype = attr.ib(default=None)
volume = attr.ib(default=None) # Partition or Disk
volume = attr.ib(default=None) # Partition or Disk
label = attr.ib(default=None)
uuid = attr.ib(default=None)
preserve = attr.ib(default=False)
_mount = attr.ib(default=None, repr=False) # Mount
_mount = attr.ib(default=None, repr=False) # Mount
def mount(self):
return self._mount
@ -242,20 +250,21 @@ class Filesystem:
class Mount:
id = attr.ib(default=id_factory("mount"))
type = attr.ib(default="mount")
device = attr.ib(default=None) # Filesystem
device = attr.ib(default=None) # Filesystem
path = attr.ib(default=None)
def align_up(size, block_size=1 << 20):
return (size + block_size - 1) & ~(block_size - 1)
def align_down(size, block_size=1 << 20):
return size & ~(block_size - 1)
class FilesystemModel(object):
lower_size_limit = 128*(1<<20)
lower_size_limit = 128 * (1 << 20)
supported_filesystems = [
('ext4', True, FS('ext4', True)),
@ -263,8 +272,6 @@ class FilesystemModel(object):
('btrfs', True, FS('btrfs', True)),
('---', False),
('swap', True, FS('swap', False)),
#('bcache cache', True, FS('bcache cache', False)),
#('bcache store', True, FS('bcache store', False)),
('---', False),
('leave unformatted', True, FS(None, False)),
]
@ -282,11 +289,12 @@ class FilesystemModel(object):
def __init__(self, prober):
self.prober = prober
self._available_disks = {} # keyed by path, eg /dev/sda
self._available_disks = {} # keyed by path, eg /dev/sda
self.reset()
def reset(self):
self._disks = collections.OrderedDict() # only gets populated when something uses the disk
# only gets populated when something uses the disk
self._disks = collections.OrderedDict()
self._filesystems = []
self._partitions = []
self._mounts = []
@ -307,7 +315,7 @@ class FilesystemModel(object):
r.append(asdict(p))
for f in self._filesystems:
r.append(asdict(f))
for m in sorted(self._mounts, key=lambda m:len(m.path)):
for m in sorted(self._mounts, key=lambda m: len(m.path)):
r.append(asdict(m))
return r
@ -336,21 +344,20 @@ class FilesystemModel(object):
log.debug("fs probe %s", path)
if path in currently_mounted:
continue
if data['DEVTYPE'] == 'disk' \
and not data["DEVPATH"].startswith('/devices/virtual') \
and data["MAJOR"] != "2" \
and data['attrs'].get('ro') != "1":
#log.debug('disk={}\n{}'.format(
# path, json.dumps(data, indent=4, sort_keys=True)))
info = self.prober.get_storage_info(path)
self._available_disks[path] = Disk.from_info(info)
if data['DEVTYPE'] == 'disk':
if not data["DEVPATH"].startswith('/devices/virtual'):
if data["MAJOR"] != "2" and data['attrs'].get('ro') != "1":
# log.debug('disk={}\n{}'.format(
# path, json.dumps(data, indent=4, sort_keys=True)))
info = self.prober.get_storage_info(path)
self._available_disks[path] = Disk.from_info(info)
def _use_disk(self, disk):
if disk.path not in self._disks:
self._disks[disk.path] = disk
def all_disks(self):
return sorted(self._available_disks.values(), key=lambda x:x.label)
return sorted(self._available_disks.values(), key=lambda x: x.label)
def get_disk(self, path):
return self._available_disks.get(path)
@ -371,8 +378,9 @@ class FilesystemModel(object):
def add_filesystem(self, volume, fstype):
log.debug("adding %s to %s", fstype, volume)
if not volume.available:
if not (isinstance(volume, Partition) and volume.flag == 'bios_grub' and fstype == 'fat32'):
raise Exception("{} is not available".format(volume))
if not isinstance(volume, Partition):
if (volume.flag == 'bios_grub' and fstype == 'fat32'):
raise Exception("{} is not available".format(volume))
if isinstance(volume, Disk):
self._use_disk(volume)
if volume._fs is not None:
@ -399,7 +407,8 @@ class FilesystemModel(object):
def can_install(self):
# Do we need to check that there is a disk with the boot flag?
return '/' in self.get_mountpoint_to_devpath_mapping() and self.bootable()
return ('/' in self.get_mountpoint_to_devpath_mapping() and
self.bootable())
def bootable(self):
''' true if one disk has a boot partition '''
@ -417,313 +426,3 @@ class FilesystemModel(object):
if fs.fstype == "swap":
return False
return True
## class AttrDict(dict):
## __getattr__ = dict.__getitem__
## __setattr__ = dict.__setitem__
## class OldFilesystemModel(object):
## """ Model representing storage options
## """
## # TODO: what is "linear" level?
## raid_levels = [
## "0",
## "1",
## "5",
## "6",
## "10",
## ]
## def calculate_raid_size(self, raid_level, raid_devices, spare_devices):
## '''
## 0: array size is the size of the smallest component partition times
## the number of component partitions
## 1: array size is the size of the smallest component partition
## 5: array size is the size of the smallest component partition times
## the number of component partitions munus 1
## 6: array size is the size of the smallest component partition times
## the number of component partitions munus 2
## '''
## # https://raid.wiki.kernel.org/ \
## # index.php/RAID_superblock_formats#Total_Size_of_superblock
## # Version-1 superblock format on-disk layout:
## # Total size of superblock: 256 Bytes plus 2 bytes per device in the
## # array
## log.debug('calc_raid_size: level={} rd={} sd={}'.format(raid_level,
## raid_devices,
## spare_devices))
## overhead_bytes = 256 + (2 * (len(raid_devices) + len(spare_devices)))
## log.debug('calc_raid_size: overhead_bytes={}'.format(overhead_bytes))
## # find the smallest device
## min_dev_size = min([d.size for d in raid_devices])
## log.debug('calc_raid_size: min_dev_size={}'.format(min_dev_size))
## if raid_level == 0:
## array_size = min_dev_size * len(raid_devices)
## elif raid_level == 1:
## array_size = min_dev_size
## elif raid_level == 5:
## array_size = min_dev_size * (len(raid_devices) - 1)
## elif raid_level == 10:
## array_size = min_dev_size * int((len(raid_devices) /
## len(spare_devices)))
## total_size = array_size - overhead_bytes
## log.debug('calc_raid_size: array_size:{} - overhead:{} = {}'.format(
## array_size, overhead_bytes, total_size))
## return total_size
## def add_raid_device(self, raidspec):
## # assume raidspec has already been valided in view/controller
## log.debug('Attempting to create a raid device')
## '''
## raidspec = {
## 'devices': ['/dev/sdb 1.819T, HDS5C3020ALA632',
## '/dev/sdc 1.819T, 001-9YN164',
## '/dev/sdf 1.819T, 001-9YN164',
## '/dev/sdg 1.819T, 001-9YN164',
## '/dev/sdh 1.819T, HDS5C3020ALA632',
## '/dev/sdi 1.819T, 001-9YN164'],
## '/dev/sdj 1.819T, Unknown Model'],
## 'raid_level': '0',
## 'hot_spares': '0',
## 'chunk_size': '4K',
## }
## could be /dev/sda1, /dev/md0, /dev/bcache1, /dev/vg_foo/foobar2?
## '''
## raid_devices = []
## spare_devices = []
## all_devices = [r.split() for r in raidspec.get('devices', [])]
## nr_spares = int(raidspec.get('hot_spares'))
## # XXX: curtin requires a partition table on the base devices
## # and then one partition of type raid
## for (devpath, *_) in all_devices:
## disk = self.get_disk(devpath)
## # add or update a partition to be raid type
## if disk.path != devpath: # we must have got a partition
## raiddev = disk.get_partition(devpath)
## raiddev.flags = 'raid'
## else:
## disk.add_partition(1, disk.freespace, None, None, flag='raid')
## raiddev = disk
## if len(raid_devices) + nr_spares < len(all_devices):
## raid_devices.append(raiddev)
## else:
## spare_devices.append(raiddev)
## # auto increment md number based in registered devices
## raid_shortname = 'md{}'.format(len(self.raid_devices))
## raid_dev_name = '/dev/' + raid_shortname
## raid_serial = '{}_serial'.format(raid_dev_name)
## raid_model = '{}_model'.format(raid_dev_name)
## raid_parttype = 'gpt'
## raid_level = int(raidspec.get('raid_level'))
## raid_size = self.calculate_raid_size(raid_level, raid_devices,
## spare_devices)
## # create a Raiddev (pass in only the names)
## raid_parts = []
## for dev in raid_devices:
## self.set_holder(dev.devpath, raid_dev_name)
## self.set_tag(dev.devpath, 'member of MD ' + raid_shortname)
## for num, action in dev.partitions.items():
## raid_parts.append(action.action_id)
## spare_parts = []
## for dev in spare_devices:
## self.set_holder(dev.devpath, raid_dev_name)
## self.set_tag(dev.devpath, 'member of MD ' + raid_shortname)
## for num, action in dev.partitions.items():
## spare_parts.append(action.action_id)
## raid_dev = Raiddev(raid_dev_name, raid_serial, raid_model,
## raid_parttype, raid_size,
## raid_parts,
## raid_level,
## spare_parts)
## # add it to the model's info dict
## raid_dev_info = {
## 'type': 'disk',
## 'name': raid_dev_name,
## 'size': raid_size,
## 'serial': raid_serial,
## 'vendor': 'Linux Software RAID',
## 'model': raid_model,
## 'is_virtual': True,
## 'raw': {
## 'MAJOR': '9',
## },
## }
## self.info[raid_dev_name] = AttrDict(raid_dev_info)
## # add it to the model's raid devices
## self.raid_devices[raid_dev_name] = raid_dev
## # add it to the model's devices
## self.add_device(raid_dev_name, raid_dev)
## log.debug('Successfully added raid_dev: {}'.format(raid_dev))
## def add_lvm_volgroup(self, lvmspec):
## log.debug('Attempting to create an LVM volgroup device')
## '''
## lvm_volgroup_spec = {
## 'volgroup': 'my_volgroup_name',
## 'devices': ['/dev/sdb 1.819T, HDS5C3020ALA632']
## }
## '''
## lvm_shortname = lvmspec.get('volgroup')
## lvm_dev_name = '/dev/' + lvm_shortname
## lvm_serial = '{}_serial'.format(lvm_dev_name)
## lvm_model = '{}_model'.format(lvm_dev_name)
## lvm_parttype = 'gpt'
## lvm_devices = []
## # extract just the device name for disks in this volgroup
## all_devices = [r.split() for r in lvmspec.get('devices', [])]
## # XXX: curtin requires a partition table on the base devices
## # and then one partition of type lvm
## for (devpath, *_) in all_devices:
## disk = self.get_disk(devpath)
## self.set_holder(devpath, lvm_dev_name)
## self.set_tag(devpath, 'member of LVM ' + lvm_shortname)
## # add or update a partition to be raid type
## if disk.path != devpath: # we must have got a partition
## pv_dev = disk.get_partition(devpath)
## pv_dev.flags = 'lvm'
## else:
## disk.add_partition(1, disk.freespace, None, None, flag='lvm')
## pv_dev = disk
## lvm_devices.append(pv_dev)
## lvm_size = sum([pv.size for pv in lvm_devices])
## lvm_device_names = [pv.id for pv in lvm_devices]
## log.debug('lvm_devices: {}'.format(lvm_device_names))
## lvm_dev = LVMDev(lvm_dev_name, lvm_serial, lvm_model,
## lvm_parttype, lvm_size,
## lvm_shortname, lvm_device_names)
## log.debug('{} volgroup: {} devices: {}'.format(lvm_dev.id,
## lvm_dev.volgroup,
## lvm_dev.devices))
## # add it to the model's info dict
## lvm_dev_info = {
## 'type': 'disk',
## 'name': lvm_dev_name,
## 'size': lvm_size,
## 'serial': lvm_serial,
## 'vendor': 'Linux Volume Group (LVM2)',
## 'model': lvm_model,
## 'is_virtual': True,
## 'raw': {
## 'MAJOR': '9',
## },
## }
## self.info[lvm_dev_name] = AttrDict(lvm_dev_info)
## # add it to the model's lvm devices
## self.lvm_devices[lvm_dev_name] = lvm_dev
## # add it to the model's devices
## self.add_device(lvm_dev_name, lvm_dev)
## log.debug('Successfully added lvm_dev: {}'.format(lvm_dev))
## def add_bcache_device(self, bcachespec):
## # assume bcachespec has already been valided in view/controller
## log.debug('Attempting to create a bcache device')
## '''
## bcachespec = {
## 'backing_device': '/dev/sdc 1.819T, 001-9YN164',
## 'cache_device': '/dev/sdb 1.819T, HDS5C3020ALA632',
## }
## could be /dev/sda1, /dev/md0, /dev/vg_foo/foobar2?
## '''
## backing_device = self.get_disk(bcachespec['backing_device'].split()[0])
## cache_device = self.get_disk(bcachespec['cache_device'].split()[0])
## # auto increment md number based in registered devices
## bcache_shortname = 'bcache{}'.format(len(self.bcache_devices))
## bcache_dev_name = '/dev/' + bcache_shortname
## bcache_serial = '{}_serial'.format(bcache_dev_name)
## bcache_model = '{}_model'.format(bcache_dev_name)
## bcache_parttype = 'gpt'
## bcache_size = backing_device.size
## # create a Bcachedev (pass in only the names)
## bcache_dev = Bcachedev(bcache_dev_name, bcache_serial, bcache_model,
## bcache_parttype, bcache_size,
## backing_device, cache_device)
## # mark bcache holders
## self.set_holder(backing_device.devpath, bcache_dev_name)
## self.set_holder(cache_device.devpath, bcache_dev_name)
## # tag device use
## self.set_tag(backing_device.devpath,
## 'backing store for ' + bcache_shortname)
## cache_tag = self.get_tag(cache_device.devpath)
## if len(cache_tag) > 0:
## cache_tag += ", " + bcache_shortname
## else:
## cache_tag = "cache for " + bcache_shortname
## self.set_tag(cache_device.devpath, cache_tag)
## # add it to the model's info dict
## bcache_dev_info = {
## 'type': 'disk',
## 'name': bcache_dev_name,
## 'size': bcache_size,
## 'serial': bcache_serial,
## 'vendor': 'Linux bcache',
## 'model': bcache_model,
## 'is_virtual': True,
## 'raw': {
## 'MAJOR': '9',
## },
## }
## self.info[bcache_dev_name] = AttrDict(bcache_dev_info)
## # add it to the model's bcache devices
## self.bcache_devices[bcache_dev_name] = bcache_dev
## # add it to the model's devices
## self.add_device(bcache_dev_name, bcache_dev)
## log.debug('Successfully added bcache_dev: {}'.format(bcache_dev))
## def get_bcache_cachedevs(self):
## ''' return uniq list of bcache cache devices '''
## cachedevs = list(set([bcache_dev.cache_device for bcache_dev in
## self.bcache_devices.values()]))
## log.debug('bcache cache devs: {}'.format(cachedevs))
## return cachedevs
## def set_holder(self, held_device, holder_devpath):
## ''' insert a hold on `held_device' by adding `holder_devpath' to
## a list at self.holders[`held_device']
## '''
## if held_device not in self.holders:
## self.holders[held_device] = [holder_devpath]
## else:
## self.holders[held_device].append(holder_devpath)
## def clear_holder(self, held_device, holder_devpath):
## if held_device in self.holders:
## self.holders[held_device].remove(holder_devpath)
## def get_holders(self, held_device):
## return self.holders.get(held_device, [])
## def set_tag(self, device, tag):
## self.tags[device] = tag
## def get_tag(self, device):
## return self.tags.get(device, '')

View File

@ -46,34 +46,62 @@ class InstallpathModel(object):
elif self.path == 'maas_region':
self.source = '/media/region'
self.curtin['debconf_selections'] = {
'maas-username': 'maas-region-controller maas/username string %s' % results['username'],
'maas-password': 'maas-region-controller maas/password password %s' % results['password'],
'maas-username': ('maas-region-controller maas/username '
'string %s' % results['username']),
'maas-password': ('maas-region-controller maas/password '
'password %s' % results['password']),
}
self.curtin['late_commands'] = {
# Maintainer scripts cache results, from config files, if they exist
# These shouldn't exist, since this was fixed in livecd-rootfs
# but remove these, just to be sure
# Maintainer scripts cache results, from config files, if they
# exist. These shouldn't exist, since this was fixed in
# livecd-rootfs but remove these, just to be sure.
'900-maas': ['rm', '-f', '/target/etc/maas/rackd.conf'],
'901-maas': ['rm', '-f', '/target/etc/maas/region.conf'],
# All the crazy things are workarounds for maas maintainer scripts deficiencies
# see https://bugs.launchpad.net/ubuntu/+source/maas/+bugs?field.tag=subiquity
# All the crazy things are workarounds for maas maintainer
# scripts deficiencies see:
# LP: #1766209
# LP: #1766211
# LP: #1766214
# LP: #1766218
# LP: #1766241
#
# uuid is not initialized by reconfigure, maybe it should, if it is at all used
# make it so, to make it match the udeb/deb installs
'902-maas': ['curtin', 'in-target', '--', 'maas-rack', 'config', '--init'],
# this should do setups of maas-url for the rack controller, and secret if needed.
'903-maas': ['curtin', 'in-target', '--', 'dpkg-reconfigure', '-u', '-fnoninteractive', 'maas-rack-controller'],
# Below are workaround to make postgresql database running, and invoke-rc.d --force to not fail
# And a running postgresql is needed, to change the role password and to create an admin user
# uuid is not initialized by reconfigure, maybe it should,
# if it is at all used make it so, to make it match the
# udeb/deb installs
'902-maas': ['curtin', 'in-target', '--',
'maas-rack', 'config', '--init'],
# this should do setups of maas-url for the rack controller,
# and secret if needed.
'903-maas': ['curtin', 'in-target', '--', 'dpkg-reconfigure',
'-u', '-fnoninteractive', 'maas-rack-controller'],
# Below are workaround to make postgresql database running,
# and invoke-rc.d --force to not faill and a running postgresql
# is needed, to change the role password and to create an admin
# user.
'904-maas': ['mount', '-o', 'bind', '/proc', '/target/proc'],
'905-maas': ['mount', '-o', 'bind', '/sys', '/target/sys'],
'906-maas': ['mount', '-o', 'bind', '/dev', '/target/dev'],
'907-maas': ['mount', '-o', 'bind', '/target/bin/true', '/target/usr/sbin/invoke-rc.d'],
'908-maas': ['chroot', '/target', 'sh', '-c', 'pg_ctlcluster --skip-systemctl-redirect $(/bin/ls /var/lib/postgresql/) main start'],
# These are called like this, because reconfigure doesn't create nor change an admin user account, nor regens the semi-autogenerated maas-url
'909-maas': ['chroot', '/target', 'sh', '-c', 'debconf -fnoninteractive -omaas-region-controller /var/lib/dpkg/info/maas-region-controller.config configure'],
'910-maas': ['chroot', '/target', 'sh', '-c', 'debconf -fnoninteractive -omaas-region-controller /var/lib/dpkg/info/maas-region-controller.postinst configure'],
'911-maas': ['chroot', '/target', 'sh', '-c', 'pg_ctlcluster --skip-systemctl-redirect $(/bin/ls /var/lib/postgresql/) main stop'],
'907-maas': ['mount', '-o', 'bind', '/target/bin/true',
'/target/usr/sbin/invoke-rc.d'],
'908-maas': ['chroot', '/target', 'sh', '-c',
'pg_ctlcluster --skip-systemctl-redirect '
'$(/bin/ls /var/lib/postgresql/) main start'],
# These are called like this, because reconfigure doesn't
# create nor change an admin user account, nor regens the
# semi-autogenerated maas-url
'909-maas':
['chroot', '/target', 'sh', '-c', (
'debconf -fnoninteractive -omaas-region-controller '
'/var/lib/dpkg/info/maas-region-controller.config '
'configure')],
'910-maas':
['chroot', '/target', 'sh', '-c', (
'debconf -fnoninteractive -omaas-region-controller '
'/var/lib/dpkg/info/maas-region-controller.postinst '
'configure')],
'911-maas': ['chroot', '/target', 'sh', '-c', (
'pg_ctlcluster --skip-systemctl-redirect '
'$(/bin/ls /var/lib/postgresql/) main stop')],
'912-maas': ['umount', '/target/usr/sbin/invoke-rc.d'],
'913-maas': ['umount', '/target/dev'],
'914-maas': ['umount', '/target/sys'],
@ -82,16 +110,25 @@ class InstallpathModel(object):
elif self.path == 'maas_rack':
self.source = '/media/rack'
self.curtin['debconf_selections'] = {
'maas-url': 'maas-rack-controller maas-rack-controller/maas-url string %s' % results['url'],
'maas-secret': 'maas-rack-controller maas-rack-controller/shared-secret password %s' % results['secret'],
'maas-url': ('maas-rack-controller '
'maas-rack-controller/maas-url '
'string %s' % results['url']),
'maas-secret': ('maas-rack-controller '
'maas-rack-controller/shared-secret '
'password %s' % results['secret']),
}
self.curtin['late_commands'] = {
'90-maas': ['rm', '-f', '/target/etc/maas/rackd.conf'],
'91-maas': ['curtin', 'in-target', '--', 'maas-rack', 'config', '--init'],
# maas-rack-controller is broken, and does db_input & go on the password question in the postinst...
# when it should have been done in .config
# and it doesn't gracefully handle the case of db_go returning 30 skipped
'93-maas': ['curtin', 'in-target', '--', 'sh', '-c', 'debconf -fnoninteractive -omaas-rack-controller /var/lib/dpkg/info/maas-rack-controller.postinst configure || :'],
'91-maas': ['curtin', 'in-target', '--', 'maas-rack',
'config', '--init'],
# maas-rack-controller is broken, and does db_input & go on
# the password question in the postinst... when it should have
# been done in .config and it doesn't gracefully handle the
# case of db_go returning 30 skipped
'93-maas': ['curtin', 'in-target', '--', 'sh', '-c',
('debconf -fnoninteractive -omaas-rack-controller '
'/var/lib/dpkg/info/maas-rack-controller.postinst'
' configure || :')],
}
else:
raise ValueError("invalid Installpath %s" % self.path)

View File

@ -24,9 +24,10 @@ class IscsiDiskModel(object):
"""
menu = [
('Discover volumes now', 'iscsi:discover-volumes'),
('Use custom discovery credentials (advanced)', 'iscsi:custom-discovery-credentials'),
('Enter volume details manually', 'iscsi:manual-volume-details')
('Discover volumes now', 'iscsi:discover-volumes'),
('Use custom discovery credentials (advanced)',
'iscsi:custom-discovery-credentials'),
('Enter volume details manually', 'iscsi:manual-volume-details')
]
server_authentication = {

View File

@ -23,6 +23,7 @@ XKBOPTIONS="{options}"
BACKSPACE="guess"
"""
@attr.s
class KeyboardSetting:
layout = attr.ib()
@ -52,32 +53,40 @@ class KeyboardSetting:
new_variant = 'latinalternatequotes'
else:
new_variant = 'latin'
return KeyboardSetting(layout='rs,rs', variant=new_variant + ',' + self.variant)
return KeyboardSetting(layout='rs,rs',
variant=(new_variant +
',' + self.variant))
elif self.layout == 'jp':
if self.variant in ('106', 'common', 'OADG109A', 'nicola_f_bs', ''):
if self.variant in ('106', 'common', 'OADG109A',
'nicola_f_bs', ''):
return self
else:
return KeyboardSetting(layout='jp,jp', variant=',' + self.variant)
return KeyboardSetting(layout='jp,jp',
variant=',' + self.variant)
elif self.layout == 'lt':
if self.variant == 'us':
return KeyboardSetting(layout='lt,lt', variant='us,')
else:
return KeyboardSetting(layout='lt,lt', variant=self.variant + ',us')
return KeyboardSetting(layout='lt,lt',
variant=self.variant + ',us')
elif self.layout == 'me':
if self.variant == 'basic' or self.variant.startswith('latin'):
return self
else:
return KeyboardSetting(layout='me,me', variant=self.variant + ',us')
return KeyboardSetting(layout='me,me',
variant=self.variant + ',us')
elif self.layout in standard_non_latin_layouts:
return KeyboardSetting(layout='us,' + self.layout, variant=',' + self.variant)
return KeyboardSetting(layout='us,' + self.layout,
variant=',' + self.variant)
else:
return self
@classmethod
def from_config_file(cls, config_file):
content = open(config_file).read()
def optval(opt, default):
match = re.search('(?m)^\s*%s=(.*)$'%(opt,), content)
match = re.search('(?m)^\s*%s=(.*)$' % (opt,), content)
if match:
r = match.group(1).strip('"')
if r != '':
@ -94,8 +103,8 @@ class KeyboardSetting:
def for_ui(self):
"""
Attempt to guess a setting the user chose which resulted in the current config.
Basically the inverse of latinizable().
Attempt to guess a setting the user chose which resulted in the
current config. Basically the inverse of latinizable().
"""
if ',' in self.layout:
layout1, layout2 = self.layout.split(',', 1)
@ -126,17 +135,18 @@ class KeyboardSetting:
# Non-latin keyboard layouts that are handled in a uniform way
standard_non_latin_layouts = set(
('af', 'am', 'ara', 'ben', 'bd', 'bg', 'bt', 'by', 'et', 'ge',
'gh', 'gr', 'guj', 'guru', 'il', ''in'', 'iq', 'ir', 'iku', 'kan',
'kh', 'kz', 'la', 'lao', 'lk', 'kg', 'ma', 'mk', 'mm', 'mn', 'mv',
'mal', 'np', 'ori', 'pk', 'ru', 'scc', 'sy', 'syr', 'tel', 'th',
'tj', 'tam', 'tib', 'ua', 'ug', 'uz')
)
'gh', 'gr', 'guj', 'guru', 'il', ''in'', 'iq', 'ir', 'iku', 'kan',
'kh', 'kz', 'la', 'lao', 'lk', 'kg', 'ma', 'mk', 'mm', 'mn', 'mv',
'mal', 'np', 'ori', 'pk', 'ru', 'scc', 'sy', 'syr', 'tel', 'th',
'tj', 'tam', 'tib', 'ua', 'ug', 'uz')
)
class KeyboardModel:
def __init__(self, root):
self.root = root
self._kbnames_file = os.path.join(os.environ.get("SNAP", '.'), 'kbdnames.txt')
self._kbnames_file = os.path.join(os.environ.get("SNAP", '.'),
'kbdnames.txt')
self._clear()
if os.path.exists(self.config_path):
self.setting = KeyboardSetting.from_config_file(self.config_path)
@ -182,7 +192,9 @@ class KeyboardModel:
def lookup(self, code):
if ':' in code:
layout_code, variant_code = code.split(":", 1)
return self.layouts.get(layout_code, '?'), self.variants.get(layout_code, {}).get(variant_code, '?')
layout = self.layouts.get(layout_code, '?')
variant = self.variants.get(layout_code, {}).get(variant_code, '?')
return (layout, variant)
else:
return self.layouts.get(code, '?'), None

View File

@ -20,6 +20,7 @@ from subiquitycore import i18n
log = logging.getLogger('subiquity.models.locale')
class LocaleModel(object):
""" Model representing locale selection
@ -36,10 +37,10 @@ class LocaleModel(object):
('fr_FR', 'French'),
('de_DE', 'German'),
('el_GR', 'Greek, Modern (1453-)'),
# ('he_IL', 'Hebrew'), # disabled as it does not render correctly on a vt with default font
# ('he_IL', 'Hebrew'), # noqa: disabled as it does not render correctly on a vt with default font
('hu_HU', 'Hungarian'),
('lv_LV', 'Latvian'),
('nb_NO', 'Norsk bokmål'), # iso_639_3 for nb does not translate Norwgian
('nb_NO', 'Norsk bokmål'), # noqa: iso_639_3 for nb does not translate Norwgian
('pl_PL', 'Polish'),
('ru_RU', 'Russian'),
('es_ES', 'Spanish'),
@ -61,7 +62,8 @@ class LocaleModel(object):
for code, name in self.supported_languages:
native = name
if gettext.find('iso_639_3', languages=[code]):
native_lang = gettext.translation('iso_639_3', languages=[code])
native_lang = gettext.translation('iso_639_3',
languages=[code])
native = native_lang.gettext(name).capitalize()
languages.append((code, native))
return languages

View File

@ -31,9 +31,12 @@ from .snaplist import SnapListModel
def setup_yaml():
""" http://stackoverflow.com/a/8661021 """
represent_dict_order = lambda self, data: self.represent_mapping('tag:yaml.org,2002:map', data.items())
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()
@ -55,7 +58,9 @@ class SubiquityModel:
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")
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:
@ -83,9 +88,9 @@ class SubiquityModel:
return config
def _cloud_init_files(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.
# 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())})
@ -112,8 +117,10 @@ class SubiquityModel:
'install': {
'target': target,
'unmount': 'disabled',
'save_install_config': '/var/log/installer/curtin-install-cfg.yaml',
'save_install_log': '/var/log/installer/curtin-install.log',
'save_install_config':
'/var/log/installer/curtin-install-cfg.yaml',
'save_install_log':
'/var/log/installer/curtin-install.log',
},
'sources': {
@ -128,7 +135,10 @@ class SubiquityModel:
'pollinate': {
'user_agent': {
'subiquity': "%s_%s"%(os.environ.get("SNAP_VERSION", 'dry-run'), os.environ.get("SNAP_REVISION", 'dry-run')),
'subiquity': "%s_%s" % (os.environ.get("SNAP_VERSION",
'dry-run'),
os.environ.get("SNAP_REVISION",
'dry-run')),
},
},
@ -159,9 +169,9 @@ class SubiquityModel:
if self.proxy.proxy:
config['write_files']['snapd_dropin'] = {
'path': 'etc/systemd/system/snapd.service.d/snap_proxy.conf',
'content': self.proxy.proxy_systemd_dropin(),
}
'path': 'etc/systemd/system/snapd.service.d/snap_proxy.conf',
'content': self.proxy.proxy_systemd_dropin(),
}
if not self.filesystem.add_swapfile():
config['swap'] = {'size': 0}

View File

@ -1,10 +1,8 @@
import unittest
from subiquity.models.filesystem import dehumanize_size, humanize_size
class TestHumanizeSize(unittest.TestCase):
class TestHumanizeSize(unittest.TestCase):
basics = [
('1.000M', 2**20),
@ -19,13 +17,14 @@ class TestHumanizeSize(unittest.TestCase):
with self.subTest(input=string):
self.assertEqual(string, humanize_size(integer))
class TestDehumanizeSize(unittest.TestCase):
basics = [
('1', 1),
('134', 134),
('0.5B', 0), # Does it make sense to allow this?
('0.5B', 0), # Does it make sense to allow this?
('1B', 1),
('1K', 2**10),
@ -76,5 +75,6 @@ class TestDehumanizeSize(unittest.TestCase):
except ValueError as e:
actual_error = str(e)
else:
self.fail("dehumanize_size({!r}) did not error".format(input))
self.fail(
"dehumanize_size({!r}) did not error".format(input))
self.assertEqual(expected_error, actual_error)

View File

@ -1,463 +0,0 @@
"""
import argparse
import logging
import random
import testtools
import yaml
from mock import patch
from subiquitycore.models.blockdev import (Blockdev,
blockdev_align_up,
FIRST_PARTITION_OFFSET,
GPT_END_RESERVE,
sort_actions)
from subiquitycore.models.filesystem import FilesystemModel
from subiquitycore.prober import Prober
from subiquitycore.tests import fakes
GB = 1 << 40
class TestFilesystemModel(testtools.TestCase):
def setUp(self):
super(TestFilesystemModel, self).setUp()
# don't show logging messages while testing
logging.disable(logging.CRITICAL)
self.make_fsm()
# mocking the reading of the fake data saves on IO
@patch.object(Prober, '_load_machine_config')
@patch.object(Prober, 'get_storage')
def make_fsm(self, _get_storage, _load_machine_config):
_get_storage.return_value = fakes.FAKE_MACHINE_STORAGE_DATA
_load_machine_config.return_value = fakes.FAKE_MACHINE_JSON_DATA
self.opts = argparse.Namespace()
self.opts.machine_config = fakes.FAKE_MACHINE_JSON
self.opts.dry_run = True
self.prober = Prober(self.opts)
self.storage = fakes.FAKE_MACHINE_STORAGE_DATA
self.fsm = FilesystemModel(self.prober, self.opts)
def test_init(self):
self.assertNotEqual(self.fsm, None)
self.assertEqual(self.fsm.info, {})
self.assertEqual(self.fsm.devices, {})
self.assertEqual(self.fsm.raid_devices, {})
self.assertEqual(self.fsm.storage, {})
def test_get_signals(self):
self.assertEqual(sorted(self.fsm.get_signals()),
sorted(self.fsm.signals + self.fsm.fs_menu))
def test_get_signal_by_name(self):
for (name, signal, method) in self.fsm.get_signals():
self.assertEqual(self.fsm.get_signal_by_name(name), signal)
def test_get_menu(self):
self.assertEqual(sorted(self.fsm.get_menu()),
sorted(self.fsm.fs_menu))
def test_probe_storage(self):
'''sd[b..i]'''
disks = [d for d in self.storage.keys()
if self.storage[d]['DEVTYPE'] == 'disk' and
self.storage[d]['MAJOR'] in ['8', '253']]
self.fsm.probe_storage()
self.assertNotEqual(self.fsm.storage, {})
self.assertEqual(sorted(self.fsm.info.keys()),
sorted(disks))
def test_get_disk(self):
self.fsm.probe_storage()
diskname = random.choice(list(self.fsm.info.keys()))
disk = Blockdev(diskname,
self.fsm.info[diskname].serial,
self.fsm.info[diskname].model,
size=self.fsm.info[diskname].size)
test_disk = self.fsm.get_disk(diskname)
print(disk)
print(test_disk)
self.assertEqual(test_disk, disk)
def test_get_disk_from_partition(self):
self.fsm.probe_storage()
diskname = random.choice(list(self.fsm.info.keys()))
disk = self.fsm.get_disk(diskname)
disk.add_partition(1, disk.freespace, None, None, flag='raid')
partpath = '{}{}'.format(disk.path, 1)
print(partpath)
self.assertTrue(partpath[-1], 1)
test_disk = self.fsm.get_disk(partpath)
print(disk)
print(test_disk)
self.assertEqual(test_disk, disk)
def test_get_all_disks(self):
self.fsm.probe_storage()
all_disks = self.fsm.get_all_disks()
for disk in all_disks:
self.assertTrue(disk in self.fsm.devices.values())
def test_get_available_disks(self):
''' occupy one of the probed disks and ensure
that it's not included in the available disks
result since it's not actually avaialable
'''
self.fsm.probe_storage()
diskname = random.choice(list(self.fsm.info.keys()))
disk = self.fsm.get_disk(diskname)
disk.add_partition(1, disk.freespace, None, None, flag='raid')
avail_disks = self.fsm.get_available_disks()
self.assertLess(len(avail_disks), len(self.fsm.devices.values()))
self.assertTrue(disk not in avail_disks)
def test_add_device(self):
self.fsm.probe_storage()
diskname = random.choice(list(self.fsm.info.keys()))
disk = Blockdev(diskname,
self.fsm.info[diskname].serial,
self.fsm.info[diskname].model,
size=self.fsm.info[diskname].size)
devname = '/dev/md0'
self.fsm.add_device(devname, disk)
self.assertTrue(devname in self.fsm.devices)
def test_get_partitions(self):
self.fsm.probe_storage()
# no partitions
partitions = self.fsm.get_partitions()
self.assertEqual(len(partitions), 0)
# add one to a random disk
diskname = random.choice(list(self.fsm.info.keys()))
disk = self.fsm.get_disk(diskname)
disk.add_partition(1, disk.freespace, None, None, flag='raid')
# we added one, we should get one
partitions = self.fsm.get_partitions()
self.assertEqual(len(partitions), 1)
# it should have the same base device name
print(partitions, diskname)
self.assertTrue(partitions[0].startswith(diskname))
def test_installable(self):
self.fsm.probe_storage()
self.assertEqual(self.fsm.installable(), False)
# create a partition that installs to root(/)
diskname = random.choice(list(self.fsm.info.keys()))
disk = self.fsm.get_disk(diskname)
disk.add_partition(1, disk.freespace, 'ext4', '/', flag='bios_grub')
# now we should be installable
self.assertEqual(self.fsm.installable(), True)
def test_not_installable(self):
self.fsm.probe_storage()
# create a partition that installs to not root(/)
diskname = random.choice(list(self.fsm.info.keys()))
disk = self.fsm.get_disk(diskname)
disk.add_partition(1, disk.freespace, 'ext4', '/opt', flag='bios_grub')
# we should not be installable
self.assertEqual(self.fsm.installable(), False)
def test_bootable(self):
self.fsm.probe_storage()
self.assertEqual(self.fsm.bootable(), False)
# create a partition that installs to root(/)
diskname = random.choice(list(self.fsm.info.keys()))
disk = self.fsm.get_disk(diskname)
disk.add_partition(1, disk.freespace, 'ext4', '/', flag='bios_grub')
# now we should be installable
self.assertEqual(self.fsm.bootable(), True)
def test_get_empty_disks(self):
self.fsm.probe_storage()
empty = self.fsm.get_empty_disks()
avail_disks = self.fsm.get_available_disks()
self.assertEqual(len(empty), len(avail_disks))
# create a partition but not FS or Mount
diskname = random.choice(self.fsm.get_available_disk_names())
disk = self.fsm.get_disk(diskname)
disk.add_partition(1, int(disk.freespace / 2), None, None, flag='raid')
self.assertEqual(len(disk.partitions), 1)
print('disk: {}'.format(disk))
print('disk avail: {} is_mounted={} percent_free={}'.format(
disk.devpath, disk.is_mounted(), disk.percent_free,
len(disk.partitions)))
# we should have one less empty disk than available
empty = self.fsm.get_empty_disks()
avail_disks = self.fsm.get_available_disks()
print('empty')
for d in empty:
print('empty: {} is_mounted={} percent_free={}'.format(
d.devpath, d.is_mounted(), d.percent_free,
len(d.partitions)))
print('avail')
for d in avail_disks:
print('avail: {} is_mounted={} percent_free={}'.format(
d.devpath, d.is_mounted(), d.percent_free,
len(d.partitions)))
self.assertLess(len(empty), len(avail_disks))
def test_get_empty_disks_names(self):
self.fsm.probe_storage()
empty_names = self.fsm.get_empty_disk_names()
for name in empty_names:
print(name)
self.assertTrue(name in self.fsm.devices)
def test_get_empty_partition_names(self):
self.fsm.probe_storage()
empty = self.fsm.get_empty_partition_names()
self.assertEqual(empty, [])
# create a partition (not full sized) but not FS or Mount
diskname = random.choice(self.fsm.get_available_disk_names())
disk = self.fsm.get_disk(diskname)
disk.add_partition(1, int(disk.freespace / 2), None, None, flag=None)
# one empty partition
[empty] = self.fsm.get_empty_partition_names()
print('empty={}'.format(empty))
print('diskane={}'.format(diskname))
self.assertTrue(diskname in empty)
def test_get_empty_partition_names(self):
self.fsm.probe_storage()
diskname = random.choice(self.fsm.get_available_disk_names())
disk = self.fsm.get_disk(diskname)
# create a partition (not full sized) but not FS or Mount
disk.add_partition(1, int(disk.freespace / 2), None, None, flag='raid')
avail_disk_names = self.fsm.get_available_disk_names()
print(disk.devpath)
print(avail_disk_names)
self.assertTrue(disk.devpath in avail_disk_names)
class TestBlockdev(testtools.TestCase):
def setUp(self):
super(TestBlockdev, self).setUp()
self.devpath = '/dev/foobar'
self.serial = 'serial'
self.model = 'model'
self.parttype = 'gpt'
self.size = 128 * GB
self.bd = Blockdev(self.devpath, self.serial, self.model,
self.parttype, self.size)
def test_blockdev_init(self):
# verify
self.assertNotEqual(self.bd, None)
self.assertEqual(self.bd.available, True)
self.assertEqual(self.bd.blocktype, 'disk')
self.assertEqual(self.bd.devpath, self.devpath)
self.assertEqual(self.bd.freespace, self.size)
self.assertEqual(self.bd.model, self.model)
self.assertEqual(self.bd.path, self.devpath)
self.assertEqual(self.bd.percent_free, 100)
self.assertEqual(self.bd.size, self.size)
self.assertEqual(self.bd.usedspace, 0)
self.assertEqual(list(self.bd.available_partitions), [])
self.assertEqual(list(self.bd.filesystems), [])
self.assertEqual(list(self.bd.mounts), [])
self.assertEqual(list(self.bd.partitions), [])
self.assertEqual(list(self.bd.partnames), [])
# requires mock
#self.assertEqual(self.bd.mounted, [])
def add_partition(self, partnum=1, partsize=1 * GB, fstype='ext4',
mountpoint='/', flag='bios_grub'):
return self.bd.add_partition(partnum, partsize, fstype,
mountpoint, flag)
def test_blockdev_add_first_partition(self):
# add a default partition
partnum=1
partsize=1*GB
partpath='{}{}'.format(self.devpath, 1)
partsize_aligned = self.add_partition()
# verify
self.assertEqual(len(list(self.bd.partitions)), 1)
new_part = self.bd.partitions[1]
# first partition has an offset and alignment (1M)
size_plus_offset_aligned = blockdev_align_up(partsize + FIRST_PARTITION_OFFSET)
self.assertEqual(new_part.size, size_plus_offset_aligned -
FIRST_PARTITION_OFFSET)
# partition check
partpath = "{}{}".format(self.devpath, '1')
self.assertTrue(partpath in self.bd.partnames)
# format check
self.assertTrue(partpath in self.bd.filesystems)
# mount check
self.assertTrue(partpath in self.bd._mounts)
def test_blockdev_add_additional_partition(self):
self.add_partition()
partsize = 2 * GB
new_size = self.add_partition(partnum=2, partsize=partsize, fstype='ext4',
mountpoint='/foo', flag='boot')
self.assertEqual(len(list(self.bd.partitions)), 2)
print([action.get() for (num, action) in self.bd.partitions.items()])
# additional partitions don't have an offset, just alignment
new_part = self.bd.partitions[2]
offset_aligned = blockdev_align_up(partsize)
self.assertEqual(offset_aligned, new_part.size)
self.assertEqual(new_size, new_part.size)
self.assertEqual(offset_aligned, new_size)
def test_blockdev_add_partition_no_format_no_mount(self):
self.add_partition()
partnum=2
new_size = self.add_partition(partnum=partnum, partsize=1 * GB, fstype=None,
mountpoint=None, flag='raid')
partpath='{}{}'.format(self.devpath, partnum)
self.assertEqual(len(list(self.bd.partitions)), 2)
print([action.get() for (num, action) in self.bd.partitions.items()])
# format check
self.assertTrue(partpath not in self.bd.filesystems)
# mount check
self.assertTrue(partpath not in self.bd._mounts)
def test_blockdev_lastpartnumber(self):
self.add_partition()
self.assertEqual(self.bd.lastpartnumber, 1)
def test_blockdev_get_partition(self):
partpath='{}{}'.format(self.devpath, '1')
self.add_partition()
new_part = self.bd.partitions[1]
part2 = self.bd.get_partition(partpath)
self.assertEqual(new_part, part2)
def test_blockdev_get_partition_with_string(self):
''' attempt to add a partition with number as a string type '''
partnum = '1'
self.add_partition(partnum=partnum)
# format the partpath with devpath and partnum
partpath='{}{}'.format(self.devpath, partnum)
# we shouldn't be able to get it via a string index
self.assertRaises(KeyError, lambda x: self.bd.partitions[x], partnum)
# check that we did create the partition and store it
# with an integer as the key in the partitions dictionary
new_part = self.bd.partitions[int(partnum)]
part2 = self.bd.get_partition(partpath)
self.assertEqual(new_part, part2)
def test_blockdev_get_actions(self):
self.add_partition()
actions = self.bd.get_actions()
# actions: disk, partition, format, mount
self.assertEqual(len(actions), 4)
action_types = [a.get('type') for a in actions]
for a in ['disk', 'partition', 'format', 'mount']:
self.assertTrue(a in action_types)
def test_blockdev_sort_actions(self):
self.add_partition()
actions = sort_actions(self.bd.get_actions())
# self.bd has a partition, add_partition method adds a
# disk action, partition action, a format, and a mount point action.
# We should have a sorted order of actions which define disk,
# partition it, format and then mount confirm this by walking up
# the order and comparing action type
for (idx, a) in enumerate(actions):
print(idx, a)
order = ['disk', 'partition', 'format', 'mount']
for (idx, type) in enumerate(order):
print(idx, type)
self.assertEqual(order[idx], actions[idx].get('type'))
def test_blockdev_get_fs_table(self):
self.add_partition()
partnum = 1
partsize = self.bd.partitions[partnum].size
partpath = '{}{}'.format(self.devpath, partnum)
mount = self.bd._mounts[partpath]
fstype = self.bd.filesystems[partpath].fstype
# test
fs_table = self.bd.get_fs_table()
# verify
self.assertEqual(len(fs_table), len(self.bd.partitions))
self.assertEqual(mount, fs_table[0][0])
self.assertEqual(partsize, fs_table[0][1])
self.assertEqual(fstype, fs_table[0][2])
self.assertEqual(partpath, fs_table[0][3])
def test_blockdev_get_fs_table_swap(self):
self.add_partition()
partnum=2
self.add_partition(partnum=partnum, partsize=1 * GB, fstype='swap',
mountpoint=None, flag=None)
partsize = self.bd.partitions[partnum].size
partpath = '{}{}'.format(self.devpath, partnum)
fstype = 'swap'
mount = fstype
# test
fs_table = self.bd.get_fs_table()
# verify
self.assertEqual(len(fs_table), len(self.bd.partitions))
self.assertEqual(mount, fs_table[1][0])
self.assertEqual(partsize, fs_table[1][1])
self.assertEqual(fstype, fs_table[1][2])
self.assertEqual(partpath, fs_table[1][3])
def test_blockdev_available_partitions(self):
# add a non-empty partition
self.add_partition()
# we shouldn't have any empty partitions
empty = self.bd.available_partitions
self.assertEqual(empty, [])
partnum=2
self.add_partition(partnum=partnum, partsize=1 * GB,
fstype='leave unformatted',
mountpoint=None, flag=None)
# we should have one empty partition
empty = self.bd.available_partitions
print(empty)
self.assertEqual(len(empty), 1)
"""

View File

@ -1,5 +1,4 @@
import os
import re
from urwid import connect_signal, Padding, Text, WidgetWrap
@ -36,6 +35,7 @@ class _MountEditor(StringEditor):
OTHER = object()
LEAVE_UNMOUNTED = object()
class MountSelector(WidgetWrap):
def __init__(self, mountpoint_to_devpath_mapping):
opts = []
@ -48,7 +48,7 @@ class MountSelector(WidgetWrap):
first_opt = i
opts.append((mnt, True, mnt))
else:
opts.append(("%-*s (%s)"%(max_len, mnt, devpath), False))
opts.append(("%-*s (%s)" % (max_len, mnt, devpath), False))
if first_opt is None:
first_opt = len(opts)
opts.append((_('other'), True, OTHER))
@ -65,14 +65,16 @@ class MountSelector(WidgetWrap):
def _showhide_other(self, show):
if show and not self._other_showing:
self._w.contents.append((Padding(Columns([(1, Text("/")), self._other]), left=4), self._w.options('pack')))
self._w.contents.append(
(Padding(Columns([(1, Text("/")), self._other]), left=4),
self._w.options('pack')))
self._other_showing = True
elif not show and self._other_showing:
del self._w.contents[-1]
self._other_showing = False
def _select_mount(self, sender, value):
self._showhide_other(value==OTHER)
self._showhide_other(value == OTHER)
if value == OTHER:
self._w.focus_position = 1

View File

@ -19,7 +19,9 @@ from urwid import (
styles = {
'dots': {
'texts': [t.replace('*', '\N{bullet}') for t in ['|*----|', '|-*---|', '|--*--|', '|---*-|', '|----*|', '|---*-|', '|--*--|', '|-*---|']],
'texts': [t.replace('*', '\N{bullet}')
for t in ['|*----|', '|-*---|', '|--*--|', '|---*-|',
'|----*|', '|---*-|', '|--*--|', '|-*---|']],
'rate': 0.2,
},
'spin': {
@ -28,6 +30,7 @@ styles = {
},
}
class Spinner(Text):
def __init__(self, loop, style='spin', align='center'):
self.loop = loop
@ -38,7 +41,7 @@ class Spinner(Text):
self.handle = None
def _advance(self, sender=None, user_data=None):
self.spin_index = (self.spin_index + 1)%len(self.spin_text)
self.spin_index = (self.spin_index + 1) % len(self.spin_text)
self.set_text(self.spin_text[self.spin_index])
self.handle = self.loop.set_alarm_in(self.rate, self._advance)
@ -51,4 +54,3 @@ class Spinner(Text):
if self.handle is not None:
self.loop.remove_alarm(self.handle)
self.handle = None

View File

@ -13,20 +13,40 @@
# 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 .filesystem import (FilesystemView, # NOQA
from .filesystem import (FilesystemView,
PartitionView,
FormatEntireView,
DiskPartitionView,
DiskInfoView,
GuidedDiskSelectionView,
GuidedFilesystemView)
from .bcache import BcacheView # NOQA
from .raid import RaidView # NOQA
from .ceph import CephDiskView # NOQA
from .iscsi import IscsiDiskView # NOQA
from .lvm import LVMVolumeGroupView # NOQA
from .identity import IdentityView # NOQA
from .installpath import InstallpathView, MAASView # NOQA
from .installprogress import ProgressView # NOQA
from .bcache import BcacheView
from .raid import RaidView
from .ceph import CephDiskView
from .iscsi import IscsiDiskView
from .lvm import LVMVolumeGroupView
from .identity import IdentityView
from .installpath import InstallpathView, MAASView
from .installprogress import ProgressView
from .keyboard import KeyboardView
from .welcome import WelcomeView
__all__ = [
'BcacheView',
'CephDiskView',
'DiskInfoView',
'DiskPartitionView',
'FilesystemView',
'FormatEntireView',
'GuidedDiskSelectionView',
'GuidedFilesystemView',
'IdentityView',
'InstallpathView',
'IscsiDiskView',
'KeyboardView',
'LVMVolumeGroupView',
'MAASView',
'PartitionView',
'ProgressView',
'RaidView',
'WelcomeView',
]

View File

@ -25,3 +25,12 @@ from .disk_partition import DiskPartitionView
from .filesystem import FilesystemView
from .guided import GuidedDiskSelectionView, GuidedFilesystemView
from .partition import FormatEntireView, PartitionView
__all__ = [
'DiskInfoView',
'DiskPartitionView',
'FilesystemView',
'GuidedDiskSelectionView',
'GuidedFilesystemView',
'FormatEntireView',
'PartitionView',
]

View File

@ -16,8 +16,8 @@
import logging
from urwid import Text
from subiquitycore.ui.lists import SimpleList
from subiquitycore.ui.buttons import done_btn
from subiquitycore.ui.container import ListBox
from subiquitycore.ui.utils import button_pile, Padding
from subiquitycore.view import BaseView
@ -40,7 +40,7 @@ class DiskInfoView(BaseView):
for h in hdinfo:
body.append(Text(h))
body.append(self._build_buttons())
super().__init__(Padding.center_79(SimpleList(body)))
super().__init__(Padding.center_79(ListBox(body)))
def _build_buttons(self):
return button_pile([done_btn(_("Done"), on_press=self.done)])

View File

@ -41,8 +41,8 @@ class DiskPartitionView(BaseView):
('pack', Text("")),
Padding.center_79(ListBox(
self._build_model_inputs() + [
Text(""),
self.show_disk_info_w(),
Text(""),
self.show_disk_info_w(),
])),
('pack', Text("")),
('pack', self._build_buttons()),
@ -70,8 +70,8 @@ class DiskPartitionView(BaseView):
def format_volume(label, part):
size = humanize_size(part.size)
if part.fs() is None:
fstype = '-'
mountpoint = '-'
fstype = '-'
mountpoint = '-'
elif part.fs().mount() is None:
fstype = part.fs().fstype
mountpoint = '-'
@ -81,7 +81,8 @@ class DiskPartitionView(BaseView):
if part.type == 'disk':
part_btn = menu_btn(label, on_press=self._click_disk)
else:
part_btn = menu_btn(label, on_press=self._click_part, user_arg=part)
part_btn = menu_btn(label, on_press=self._click_part,
user_arg=part)
return Columns([
(label_width, part_btn),
(9, Text(size, align="right")),
@ -89,10 +90,13 @@ class DiskPartitionView(BaseView):
Text(mountpoint),
], 2)
if self.disk.fs() is not None:
partitioned_disks.append(format_volume(_("entire disk"), self.disk))
partitioned_disks.append(
format_volume(_("entire disk"), self.disk))
else:
for part in self.disk.partitions():
partitioned_disks.append(format_volume(_("Partition {}").format(part._number), part))
partitioned_disks.append(
format_volume(_("Partition {}").format(part._number),
part))
if self.disk.free > 0:
free_space = humanize_size(self.disk.free)
add_btn = menu_btn(final_label, on_press=self.add_partition)
@ -108,10 +112,11 @@ class DiskPartitionView(BaseView):
else:
partitioned_disks.append(Text(""))
partitioned_disks.append(
button_pile([other_btn(label=_("Select as boot disk"), on_press=self.make_boot_disk)]))
if len(self.disk.partitions()) == 0 and \
self.disk.available:
text = _("Format or create swap on entire device (unusual, advanced)")
button_pile([other_btn(label=_("Select as boot disk"),
on_press=self.make_boot_disk)]))
if len(self.disk.partitions()) == 0 and self.disk.available:
text = _("Format or create swap on entire device "
"(unusual, advanced)")
partitioned_disks.append(Text(""))
partitioned_disks.append(
menu_btn(label=text, on_press=self.format_entire))

View File

@ -50,6 +50,7 @@ the installation has started.
Are you sure you want to continue?""")
class FilesystemConfirmation(Stretchy):
def __init__(self, parent, controller):
self.parent = parent
@ -92,15 +93,6 @@ class FilesystemView(BaseView):
Text(""),
] + [Padding.push_4(p) for p in self._build_available_inputs()]
#+ [
#self._build_menu(),
#Text(""),
#Text("USED DISKS"),
#Text(""),
#self._build_used_disks(),
#Text(""),
#]
self.lb = Padding.center_95(ListBox(body))
bottom = Pile([
Text(""),
@ -118,24 +110,27 @@ class FilesystemView(BaseView):
def _build_used_disks(self):
log.debug('FileSystemView: building used disks')
return Color.info_minor(Text("No disks have been used to create a constructed disk."))
return Color.info_minor(
Text("No disks have been used to create a constructed disk."))
def _build_filesystem_list(self):
log.debug('FileSystemView: building part list')
cols = []
mount_point_text = _("MOUNT POINT")
longest_path = len(mount_point_text)
for m in sorted(self.model._mounts, key=lambda m:m.path):
for m in sorted(self.model._mounts, key=lambda m: m.path):
path = m.path
longest_path = max(longest_path, len(path))
for p, *dummy in reversed(cols):
if path.startswith(p):
path = [('info_minor', p), path[len(p):]]
break
cols.append((m.path, path, humanize_size(m.device.volume.size), m.device.fstype, m.device.volume.desc()))
cols.append((m.path, path, humanize_size(m.device.volume.size),
m.device.fstype, m.device.volume.desc()))
for fs in self.model._filesystems:
if fs.fstype == 'swap':
cols.append((None, _('SWAP'), humanize_size(fs.volume.size), fs.fstype, fs.volume.desc()))
cols.append((None, _('SWAP'), humanize_size(fs.volume.size),
fs.fstype, fs.volume.desc()))
if len(cols) == 0:
return Pile([Color.info_minor(
@ -144,14 +139,16 @@ class FilesystemView(BaseView):
type_text = _("TYPE")
size_width = max(len(size_text), 9)
type_width = max(len(type_text), self.model.longest_fs_name)
cols.insert(0, (None, mount_point_text, size_text, type_text, _("DEVICE TYPE")))
cols.insert(0, (None, mount_point_text, size_text, type_text,
_("DEVICE TYPE")))
pl = []
for dummy, a, b, c, d in cols:
if b == "SIZE":
b = Text(b, align='center')
else:
b = Text(b, align='right')
pl.append(Columns([(longest_path, Text(a)), (size_width, b), (type_width, Text(c)), Text(d)], 4))
pl.append(Columns([(longest_path, Text(a)), (size_width, b),
(type_width, Text(c)), Text(d)], 4))
return Pile(pl)
def _build_buttons(self):
@ -159,7 +156,8 @@ class FilesystemView(BaseView):
buttons = []
# don't enable done botton if we can't install
# XXX should enable/disable button rather than having it appear/disappear I think
# XXX should enable/disable button rather than having it
# appear/disappear I think
if self.model.can_install():
buttons.append(
done_btn(_("Done"), on_press=self.done))
@ -174,13 +172,16 @@ class FilesystemView(BaseView):
def col3(col1, col2, col3):
inputs.append(Columns([(42, col1), (10, col2), col3], 2))
def col2(col1, col2):
inputs.append(Columns([(42, col1), col2], 2))
def col1(col1):
inputs.append(Columns([(42, col1)], 1))
inputs = []
col3(Text(_("DEVICE")), Text(_("SIZE"), align="center"), Text(_("TYPE")))
col3(Text(_("DEVICE")), Text(_("SIZE"), align="center"),
Text(_("TYPE")))
r.append(Pile(inputs))
for disk in self.model.all_disks():
@ -197,11 +198,13 @@ class FilesystemView(BaseView):
label = _("entire device, ")
fs_obj = self.model.fs_by_name[fs.fstype]
if fs.mount():
label += "%-*s"%(self.model.longest_fs_name+2, fs.fstype+',') + fs.mount().path
label += "%-*s" % (self.model.longest_fs_name+2,
fs.fstype+',') + fs.mount().path
else:
label += fs.fstype
if fs_obj.label and fs_obj.is_mounted and not fs.mount():
disk_btn = menu_btn(label=label, on_press=self.click_disk, user_arg=disk)
disk_btn = menu_btn(label=label, on_press=self.click_disk,
user_arg=disk)
disk_btn = disk_btn
else:
disk_btn = Color.info_minor(Text(" " + label))
@ -211,16 +214,21 @@ class FilesystemView(BaseView):
fs = partition.fs()
if fs is not None:
if fs.mount():
label += "%-*s"%(self.model.longest_fs_name+2, fs.fstype+',') + fs.mount().path
label += "%-*s" % (self.model.longest_fs_name+2,
fs.fstype+',') + fs.mount().path
else:
label += fs.fstype
elif partition.flag == "bios_grub":
label += "bios_grub"
else:
label += _("unformatted")
size = Text("{:>9} ({}%)".format(humanize_size(partition.size), int(100*partition.size/disk.size)))
size = Text("{:>9} ({}%)".format(
humanize_size(partition.size),
int(100 * partition.size/disk.size)))
if partition.available:
part_btn = menu_btn(label=label, on_press=self.click_partition, user_arg=partition)
part_btn = menu_btn(label=label,
on_press=self.click_partition,
user_arg=partition)
col2(part_btn, size)
else:
part_btn = Color.info_minor(Text(" " + label))
@ -230,7 +238,7 @@ class FilesystemView(BaseView):
free = disk.free
percent = str(int(100*free/size))
if percent == "0":
percent = "%.2f"%(100*free/size,)
percent = "%.2f" % (100 * free / size,)
if disk.available and disk.used > 0:
label = _("Add/Edit Partitions")
size = "{:>9} ({}%) free".format(humanize_size(free), percent)
@ -240,9 +248,8 @@ class FilesystemView(BaseView):
else:
label = _("Edit Partitions")
size = ""
col2(
menu_btn(label=label, on_press=self.click_disk, user_arg=disk),
Text(size))
col2(menu_btn(label=label, on_press=self.click_disk,
user_arg=disk), Text(size))
r.append(Pile(inputs))
if len(r) == 1:
@ -256,26 +263,6 @@ class FilesystemView(BaseView):
def click_partition(self, sender, partition):
self.controller.format_mount_partition(partition)
def _build_menu(self):
log.debug('FileSystemView: building menu')
opts = []
#avail_disks = self.model.get_available_disk_names()
fs_menu = [
# ('Connect iSCSI network disk', 'filesystem:connect-iscsi-disk'),
# ('Connect Ceph network disk', 'filesystem:connect-ceph-disk'),
# ('Create volume group (LVM2)', 'menu:filesystem:main:create-volume-group'),
# ('Create software RAID (MD)', 'menu:filesystem:main:create-raid'),
# ('Setup hierarchichal storage (bcache)', 'menu:filesystem:main:setup-bcache'),
]
for opt, sig in fs_menu:
if len(avail_disks) > 1:
opts.append(menu_btn(label=opt,
on_press=self.on_fs_menu_press,
user_data=sig))
return Pile(opts)
def cancel(self, button=None):
self.controller.default()
@ -283,4 +270,5 @@ class FilesystemView(BaseView):
self.controller.reset()
def done(self, button):
self.show_stretchy_overlay(FilesystemConfirmation(self, self.controller))
self.show_stretchy_overlay(FilesystemConfirmation(self,
self.controller))

View File

@ -80,15 +80,18 @@ class GuidedDiskSelectionView(BaseView):
cancel = cancel_btn(_("Cancel"), on_press=self.cancel)
disks = []
for disk in self.model.all_disks():
label = "%-42s %s"%(disk.label, humanize_size(disk.size).rjust(9))
label = "%-42s %s" % (disk.label,
humanize_size(disk.size).rjust(9))
if disk.size >= model.lower_size_limit:
disk_btn = forward_btn(label, on_press=self.choose_disk, user_arg=disk)
disk_btn = forward_btn(label, on_press=self.choose_disk,
user_arg=disk)
else:
disk_btn = Color.info_minor(Text(" "+label))
disks.append(disk_btn)
body = Pile([
('pack', Text("")),
('pack', Padding.center_70(Text(_("Choose the disk to install to:")))),
('pack', Padding.center_70(
Text(_("Choose the disk to install to:")))),
('pack', Text("")),
Padding.center_70(ListBox(disks)),
('pack', Text("")),

View File

@ -51,6 +51,7 @@ class FSTypeField(FormField):
def _make_widget(self, form):
return Selector(opts=FilesystemModel.supported_filesystems)
class SizeWidget(StringEditor):
def __init__(self, form):
self.form = form
@ -70,17 +71,23 @@ class SizeWidget(StringEditor):
except ValueError:
return
if sz > self.form.max_size:
self.form.size.show_extra(('info_minor', _("Capped partition size at %s")%(self.form.size_str,)))
self.form.size.show_extra(
('info_minor',
_("Capped partition size at %s") % (self.form.size_str,)))
self.value = self.form.size_str
elif align_up(sz) != sz and humanize_size(align_up(sz)) != self.form.size.value:
sz_str = humanize_size(align_up(sz))
self.form.size.show_extra(('info_minor', _("Rounded size up to %s")%(sz_str,)))
self.value = sz_str
elif align_up(sz) != sz:
if humanize_size(align_up(sz)) != self.form.size.value:
sz_str = humanize_size(align_up(sz))
self.form.size.show_extra(
('info_minor', _("Rounded size up to %s") % (sz_str,)))
self.value = sz_str
class SizeField(FormField):
def _make_widget(self, form):
return SizeWidget(form)
class PartitionForm(Form):
def __init__(self, mountpoint_to_devpath_mapping, max_size, initial={}):
@ -128,7 +135,7 @@ class PartitionForm(Form):
return _('Path exceeds PATH_MAX')
dev = self.mountpoint_to_devpath_mapping.get(mount)
if dev is not None:
return _("%s is already mounted at %s")%(dev, mount)
return _("%s is already mounted at %s") % (dev, mount)
class PartitionFormatView(BaseView):
@ -137,7 +144,8 @@ class PartitionFormatView(BaseView):
def __init__(self, size, existing, initial, back, focus_buttons=False):
mountpoint_to_devpath_mapping = self.model.get_mountpoint_to_devpath_mapping()
mountpoint_to_devpath_mapping = (
self.model.get_mountpoint_to_devpath_mapping())
if existing is not None:
fs = existing.fs()
if fs is not None:
@ -150,13 +158,15 @@ class PartitionFormatView(BaseView):
del mountpoint_to_devpath_mapping[mount.path]
else:
initial['fstype'] = self.model.fs_by_name[None]
self.form = self.form_cls(mountpoint_to_devpath_mapping, size, initial)
self.form = self.form_cls(mountpoint_to_devpath_mapping, size,
initial)
self.back = back
connect_signal(self.form, 'submit', self.done)
connect_signal(self.form, 'cancel', self.cancel)
super().__init__(screen(self.make_body(), self.form.buttons, focus_buttons=focus_buttons))
super().__init__(screen(self.make_body(), self.form.buttons,
focus_buttons=focus_buttons))
def make_body(self):
return self.form.as_rows()
@ -170,12 +180,17 @@ Required bootloader partition
GRUB will be installed onto the target disk's MBR.
However, on a disk with a GPT partition table, there is not enough space after the MBR for GRUB to store its second-stage core.img, so a small unformatted partition is needed at the start of the disk. It will not contain a filesystem and will not be mounted, and cannot be edited here.""")
However, on a disk with a GPT partition table, there is not enough space
after the MBR for GRUB to store its second-stage core.img, so a small
unformatted partition is needed at the start of the disk. It will not contain
a filesystem and will not be mounted, and cannot be edited here.""")
boot_partition_description = _("""\
Required bootloader partition
This is the ESP / "EFI system partition" required by UEFI. Grub will be installed onto this partition, which must be formatted as fat32. The only aspect of this partition that can be edited is the size.""")
This is the ESP / "EFI system partition" required by UEFI. Grub will be
installed onto this partition, which must be formatted as fat32. The only
aspect of this partition that can be edited is the size.""")
class PartitionView(PartitionFormatView):
@ -202,7 +217,9 @@ class PartitionView(PartitionFormatView):
else:
self.footer = _("Edit partition details, format and mount.")
label = _("Save")
super().__init__(max_size, partition, initial, lambda : self.controller.partition_disk(disk), focus_buttons=label is None)
super().__init__(max_size, partition, initial,
lambda: self.controller.partition_disk(disk),
focus_buttons=label is None)
if label is not None:
self.form.buttons.base_widget[0].set_label(label)
else:
@ -210,7 +227,8 @@ class PartitionView(PartitionFormatView):
self.form.buttons.base_widget[0].set_label(_("OK"))
if partition is not None:
if partition.flag == "boot":
opts = [Option(("fat32", True, self.model.fs_by_name["fat32"]))]
opts = [Option(("fat32", True,
self.model.fs_by_name["fat32"]))]
self.form.fstype.widget._options = opts
self.form.fstype.widget.index = 0
self.form.mount.enabled = False
@ -234,7 +252,7 @@ class PartitionView(PartitionFormatView):
Text(""),
]
btn = delete_btn(_("Delete"), on_press=self.delete)
if self.partition.flag == "boot" or self.partition.flag == "bios_grub":
if self.partition.flag in ["boot", "bios_grub"]:
btn = WidgetDisable(Color.info_minor(btn.original_widget))
body.extend([
Text(""),
@ -247,7 +265,8 @@ class PartitionView(PartitionFormatView):
def done(self, form):
log.debug("Add Partition Result: {}".format(form.as_data()))
self.controller.partition_disk_handler(self.disk, self.partition, form.as_data())
self.controller.partition_disk_handler(self.disk, self.partition,
form.as_data())
class FormatEntireView(PartitionFormatView):
@ -257,14 +276,16 @@ class FormatEntireView(PartitionFormatView):
self.controller = controller
self.volume = volume
if isinstance(volume, Disk):
self.title = _("Format and/or mount {}").format(disk.label)
self.title = _("Format and/or mount {}").format(volume.label)
self.footer = _("Format or mount whole disk.")
else:
self.title = _("Partition, format, and mount {}").format(volume.device.label)
self.title = _("Partition, format, "
"and mount {}").format(volume.device.label)
self.footer = _("Edit partition details, format and mount.")
super().__init__(None, volume, {}, back)
def done(self, form):
log.debug("Add Partition Result: {}".format(form.as_data()))
self.controller.add_format_handler(self.volume, form.as_data(), self.back)
self.controller.add_format_handler(self.volume, form.as_data(),
self.back)

View File

@ -9,7 +9,6 @@ from subiquity.controllers.filesystem import FilesystemController
from subiquity.ui.views.filesystem.guided import GuidedFilesystemView
class GuidedFilesystemViewTests(unittest.TestCase):
def make_view(self):
@ -20,14 +19,16 @@ class GuidedFilesystemViewTests(unittest.TestCase):
view = self.make_view()
focus_path = view_helpers.get_focus_path(view)
for w in reversed(focus_path):
if isinstance(w, urwid.Button) and w.label == "Use An Entire Disk":
return
if isinstance(w, urwid.Button):
if w.label == "Use An Entire Disk":
return
else:
self.fail("Guided button not focus")
def test_click_guided(self):
view = self.make_view()
button = view_helpers.find_button_matching(view, "^Use An Entire Disk$")
button = (
view_helpers.find_button_matching(view, "^Use An Entire Disk$"))
view_helpers.click(button)
view.controller.guided.assert_called_once_with()

View File

@ -17,7 +17,6 @@ from subiquity.models.filesystem import (
from subiquity.ui.views.filesystem.partition import PartitionView
class PartitionViewTests(unittest.TestCase):
def make_view(self, partition=None):
@ -46,7 +45,8 @@ class PartitionViewTests(unittest.TestCase):
def test_delete_not_disabled_for_ordinary_partition(self):
view = self.make_view(Partition(size=50*(2**20)))
but, path = view_helpers.find_button_matching(view, "Delete", return_path=True)
but, path = view_helpers.find_button_matching(view, "Delete",
return_path=True)
self.assertIsNotNone(but)
for w in path:
if isinstance(w, urwid.WidgetDisable):
@ -54,7 +54,8 @@ class PartitionViewTests(unittest.TestCase):
def test_delete_disabled_for_boot_partition(self):
view = self.make_view(Partition(size=50*(2**20), flag="boot"))
but, path = view_helpers.find_button_matching(view, "Delete", return_path=True)
but, path = view_helpers.find_button_matching(view, "Delete",
return_path=True)
self.assertIsNotNone(but)
for w in path:
if isinstance(w, urwid.WidgetDisable):
@ -71,8 +72,8 @@ class PartitionViewTests(unittest.TestCase):
def test_create_partition(self):
valid_data = {
'size':"1M",
'fstype':FilesystemModel.fs_by_name["ext4"],
'size': "1M",
'fstype': FilesystemModel.fs_by_name["ext4"],
}
view = self.make_view()
view_helpers.enter_data(view.form, valid_data)

View File

@ -55,19 +55,24 @@ REALNAME_MAXLEN = 160
SSH_IMPORT_MAXLEN = 256 + 3 # account for lp: or gh:
USERNAME_MAXLEN = 32
class RealnameEditor(StringEditor, WantsToKnowFormField):
def valid_char(self, ch):
if len(ch) == 1 and ch in ':,=':
self.bff.in_error = True
self.bff.show_extra(("info_error", _("The characters : , and = are not permitted in this field")))
self.bff.show_extra(("info_error",
_("The characters : , and = are not permitted"
" in this field")))
return False
else:
return super().valid_char(ch)
class UsernameEditor(StringEditor, WantsToKnowFormField):
def __init__(self):
self.valid_char_pat = r'[-a-z0-9_]'
self.error_invalid_char = _("The only characters permitted in this field are a-z, 0-9, _ and -")
self.error_invalid_char = _("The only characters permitted in this "
"field are a-z, 0-9, _ and -")
super().__init__()
def valid_char(self, ch):
@ -78,6 +83,7 @@ class UsernameEditor(StringEditor, WantsToKnowFormField):
else:
return super().valid_char(ch)
RealnameField = simple_field(RealnameEditor)
UsernameField = simple_field(UsernameEditor)
PasswordField = simple_field(PasswordEditor)
@ -94,16 +100,20 @@ _ssh_import_data = {
'caption': _("Github Username:"),
'help': "Enter your Github username.",
'valid_char': r'[a-zA-Z0-9\-]',
'error_invalid_char': 'A Github username may only contain alphanumeric characters or hyphens.',
'error_invalid_char': ('A Github username may only contain '
'alphanumeric characters or hyphens.'),
},
'lp': {
'caption': _("Launchpad Username:"),
'help': "Enter your Launchpad username.",
'valid_char': r'[a-z0-9\+\.\-]',
'error_invalid_char': 'A Launchpad username may only contain lower-case alphanumeric characters, hyphens, plus, or periods.',
'error_invalid_char': ('A Launchpad username may only contain '
'lower-case alphanumeric characters, hyphens, '
'plus, or periods.'),
},
}
class IdentityForm(Form):
realname = RealnameField(_("Your name:"))
@ -119,7 +129,6 @@ class IdentityForm(Form):
(_("No"), True, None),
(_("from Github"), True, "gh"),
(_("from Launchpad"), True, "lp"),
#(_("from Ubuntu One account"), True, "sso"),
],
help=_("You can import your SSH keys from Github or Launchpad."))
import_username = UsernameField(_ssh_import_data[None]['caption'])
@ -135,7 +144,8 @@ class IdentityForm(Form):
return _("Server name must not be empty")
if len(self.hostname.value) > HOSTNAME_MAXLEN:
return _("Server name too long, must be < ") + str(HOSTNAME_MAXLEN)
return (_("Server name too long, must be < ") +
str(HOSTNAME_MAXLEN))
if not re.match(r'[a-z_][a-z0-9_-]*', self.hostname.value):
return _("Hostname must match NAME_REGEX, i.e. [a-z_][a-z0-9_-]*")
@ -166,6 +176,7 @@ class IdentityForm(Form):
# the signal handler stuffs the value here before doing
# validation (yes, this is a hack).
ssh_import_id_value = None
def validate_import_username(self):
if self.ssh_import_id_value is None:
return
@ -177,12 +188,19 @@ class IdentityForm(Form):
if self.ssh_import_id_value == 'lp':
lp_regex = r"^[a-z0-9][a-z0-9\+\.\-]+$"
if not re.match(lp_regex, self.import_username.value):
return _("""\
A Launchpad username must be at least two characters long and start with a letter or number. \
All letters must be lower-case. The characters +, - and . are also allowed after the first character.""")
return _("A Launchpad username must be at least two "
"characters long and start with a letter or "
"number. All letters must be lower-case. The "
"characters +, - and . are also allowed after "
"the first character.""")
elif self.ssh_import_id_value == 'gh':
if username.startswith('-') or username.endswith('-') or '--' in username or not re.match('^[a-zA-Z0-9\-]+$', username):
return _("A Github username may only contain alphanumeric characters or single hyphens, and cannot begin or end with a hyphen.")
if username.startswith('-') or (username.endswith('-') or
'--' in username or
not re.match('^[a-zA-Z0-9\-]+$',
username)):
return _("A Github username may only contain alphanumeric "
"characters or single hyphens, and cannot begin or "
"end with a hyphen.")
class FetchingSSHKeys(WidgetWrap):
@ -202,6 +220,7 @@ class FetchingSSHKeys(WidgetWrap):
('pack', spinner),
('pack', button_pile([button])),
])))
def cancel(self, sender):
self.parent.controller._fetch_cancel()
self.parent.remove_overlay()
@ -218,12 +237,15 @@ class ConfirmSSHKeys(Stretchy):
if len(fingerprints) > 1:
title = _("Confirm SSH keys")
header = _('Keys with the following fingerprints were fetched. Do you want to use them?')
header = _("Keys with the following fingerprints were fetched. "
"Do you want to use them?")
else:
title = _("Confirm SSH key")
header = _('A key with the following fingerprint was fetched. Do you want to use it?')
header = _("A key with the following fingerprint was fetched. "
"Do you want to use it?")
fingerprints = Pile([Text(fingerprint) for fingerprint in fingerprints])
fingerprints = Pile([Text(fingerprint)
for fingerprint in fingerprints])
super().__init__(
title,
@ -237,10 +259,12 @@ class ConfirmSSHKeys(Stretchy):
def cancel(self, sender):
self.parent.remove_overlay()
def ok(self, sender):
self.result['ssh_keys'] = self.key_material.splitlines()
self.parent.controller.done(self.result)
class FetchingSSHKeysFailed(Stretchy):
def __init__(self, parent, msg, stderr):
self.parent = parent
@ -256,12 +280,15 @@ class FetchingSSHKeysFailed(Stretchy):
"",
widgets,
2, 4)
def close(self, sender):
self.parent.remove_overlay()
class IdentityView(BaseView):
title = _("Profile setup")
excerpt = _("Enter the username and password (or ssh identity) you will use to log in to the system.")
excerpt = _("Enter the username and password (or ssh identity) you "
"will use to log in to the system.")
def __init__(self, model, controller, opts):
self.model = model
@ -272,11 +299,12 @@ class IdentityView(BaseView):
self.form = IdentityForm()
connect_signal(self.form, 'submit', self.done)
connect_signal(self.form.confirm_password.widget, 'change', self._check_password)
connect_signal(self.form.ssh_import_id.widget, 'select', self._select_ssh_import_id)
connect_signal(self.form.confirm_password.widget, 'change',
self._check_password)
connect_signal(self.form.ssh_import_id.widget, 'select',
self._select_ssh_import_id)
self.form.import_username.enabled = False
self.form_rows = ListBox(self.form.as_rows())
super().__init__(
screen(
@ -288,7 +316,8 @@ class IdentityView(BaseView):
def _check_password(self, sender, new_text):
password = self.form.password.value
if not password.startswith(new_text):
self.form.confirm_password.show_extra(("info_error", _("Passwords do not match")))
self.form.confirm_password.show_extra(
("info_error", _("Passwords do not match")))
else:
self.form.confirm_password.show_extra('')
@ -318,7 +347,8 @@ class IdentityView(BaseView):
if self.form.ssh_import_id.value:
fsk = FetchingSSHKeys(self)
self.show_overlay(fsk, width=fsk.width, min_width=None)
ssh_import_id = self.form.ssh_import_id.value + ":" + self.form.import_username.value
ssh_import_id = (self.form.ssh_import_id.value +
":" + self.form.import_username.value)
self.controller.fetch_ssh_keys(result, ssh_import_id)
else:
log.debug("User input: {}".format(result))
@ -326,7 +356,8 @@ class IdentityView(BaseView):
def confirm_ssh_keys(self, result, ssh_key, fingerprints):
self.remove_overlay()
self.show_stretchy_overlay(ConfirmSSHKeys(self, result, ssh_key, fingerprints))
self.show_stretchy_overlay(ConfirmSSHKeys(self, result, ssh_key,
fingerprints))
def fetching_ssh_keys_failed(self, msg, stderr):
self.remove_overlay()

View File

@ -32,7 +32,11 @@ from subiquitycore.view import BaseView
from subiquitycore.ui.interactive import (
PasswordEditor,
)
from subiquity.ui.views.identity import UsernameField, PasswordField, USERNAME_MAXLEN
from subiquity.ui.views.identity import (
UsernameField,
PasswordField,
USERNAME_MAXLEN,
)
from subiquitycore.ui.form import (
Form,
simple_field,
@ -55,7 +59,8 @@ class InstallpathView(BaseView):
"navigate options")
def __init__(self, model, controller):
self.title = self.title.format(lsb_release.get_distro_information()['RELEASE'])
self.title = self.title.format(
lsb_release.get_distro_information()['RELEASE'])
self.model = model
self.controller = controller
self.items = []
@ -79,6 +84,7 @@ class InstallpathView(BaseView):
def cancel(self, button=None):
self.controller.cancel()
class RegionForm(Form):
username = UsernameField(
@ -114,7 +120,8 @@ def to_bin(u):
class RackSecretEditor(PasswordEditor, WantsToKnowFormField):
def __init__(self):
self.valid_char_pat = r'[a-fA-F0-9]'
self.error_invalid_char = _("The secret can only contain hexadecimal characters, i.e. 0-9, a-f, A-F.")
self.error_invalid_char = _("The secret can only contain hexadecimal "
"characters, i.e. 0-9, a-f, A-F.")
super().__init__()
def valid_char(self, ch):
@ -125,21 +132,21 @@ class RackSecretEditor(PasswordEditor, WantsToKnowFormField):
else:
return super().valid_char(ch)
RackSecretField = simple_field(RackSecretEditor)
class RackForm(Form):
url = URLField(
_("Ubuntu MAAS Region API address:"),
help=_(
"e.g. \"http://192.168.1.1:5240/MAAS\". "
"localhost or 127.0.0.1 are not useful values here." ))
help=_("e.g. \"http://192.168.1.1:5240/MAAS\". "
"localhost or 127.0.0.1 are not useful values here."))
secret = RackSecretField(
_("MAAS shared secret:"),
help=_(
"The secret can be found in /var/lib/maas/secret "
"on the region controller. " ))
help=_("The secret can be found in /var/lib/maas/secret "
"on the region controller. "))
def validate_url(self):
if len(self.url.value) < 1:
@ -151,7 +158,7 @@ class RackForm(Form):
try:
to_bin(self.secret.value)
except binascii.Error as error:
return _("Secret could not be decoded: %s")%(error,)
return _("Secret could not be decoded: %s") % (error,)
class MAASView(BaseView):
@ -173,7 +180,8 @@ class MAASView(BaseView):
connect_signal(self.form, 'submit', self.done)
connect_signal(self.form, 'cancel', self.cancel)
super().__init__(self.form.as_screen(focus_buttons=False, excerpt=excerpt))
super().__init__(self.form.as_screen(focus_buttons=False,
excerpt=excerpt))
def done(self, result):
log.debug("User input: {}".format(result.as_data()))

View File

@ -29,6 +29,7 @@ from subiquity.ui.spinner import Spinner
log = logging.getLogger("subiquity.views.installprogress")
class MyLineBox(LineBox):
def format_title(self, title):
if title:
@ -74,7 +75,8 @@ class ProgressView(BaseView):
self.event_listbox = ListBox()
self.event_linebox = MyLineBox(self.event_listbox)
self.event_buttons = button_pile([other_btn(_("View full log"), on_press=self.view_log)])
self.event_buttons = button_pile([other_btn(_("View full log"),
on_press=self.view_log)])
event_body = [
('pack', Text("")),
('weight', 1, Padding.center_79(self.event_linebox)),
@ -88,7 +90,8 @@ class ProgressView(BaseView):
log_linebox = MyLineBox(self.log_listbox, _("Full installer output"))
log_body = [
('weight', 1, log_linebox),
('pack', button_pile([other_btn(_("Close"), on_press=self.close_log)])),
('pack', button_pile([other_btn(_("Close"),
on_press=self.close_log)])),
]
self.log_pile = Pile(log_body)
@ -106,10 +109,12 @@ class ProgressView(BaseView):
def add_event(self, text):
walker = self.event_listbox.base_widget.body
if len(walker) > 0:
# Remove the spinner from the line it is currently on, if there is one.
# Remove the spinner from the line it is currently on, if
# there is one.
walker[-1] = walker[-1][0]
# Add spinner to the line we are inserting.
new_line = Columns([('pack', Text(text)), ('pack', self.spinner)], dividechars=1)
new_line = Columns([('pack', Text(text)), ('pack', self.spinner)],
dividechars=1)
self._add_line(self.event_listbox, new_line)
def add_log_line(self, text):
@ -121,10 +126,12 @@ class ProgressView(BaseView):
def show_complete(self, include_exit=False):
p = self.event_buttons.original_widget
p.contents.append(
(ok_btn(_("Reboot Now"), on_press=self.reboot), p.options('pack')))
(ok_btn(_("Reboot Now"), on_press=self.reboot),
p.options('pack')))
if include_exit:
p.contents.append(
(cancel_btn(_("Exit To Shell"), on_press=self.quit), p.options('pack')))
(cancel_btn(_("Exit To Shell"), on_press=self.quit),
p.options('pack')))
w = 0
for b, o in p.contents:

View File

@ -57,8 +57,10 @@ class IscsiDiskView(BaseView):
),
Columns(
[
("weight", 0.2, Text("Connect anonymously", align="right")),
("weight", 0.3, Color.string_input(Pile(self.connect_anon.group)))
("weight", 0.2,
Text("Connect anonymously", align="right")),
("weight", 0.3,
Color.string_input(Pile(self.connect_anon.group)))
],
dividechars=4
),
@ -78,8 +80,10 @@ class IscsiDiskView(BaseView):
),
Columns(
[
("weight", 0.2, Text("Require server auth", align="right")),
("weight", 0.3, Color.string_input(Pile(self.server_auth.group)))
("weight", 0.2,
Text("Require server auth", align="right")),
("weight", 0.3,
Color.string_input(Pile(self.server_auth.group)))
],
dividechars=4
),
@ -120,5 +124,5 @@ class IscsiDiskView(BaseView):
# TODO: List found volumes
return Pile(items)
def confirm(self, result):
def confirm(self, result, sig):
self.signal.emit_signal(sig)

View File

@ -56,10 +56,13 @@ class AutoDetectBase(WidgetWrap):
self.step = step
lb = LineBox(self.make_body(), _("Keyboard auto-detection"))
super().__init__(lb)
def start(self):
pass
def stop(self):
pass
def keypress(self, size, key):
if key == 'esc':
self.keyboard_detector.backup()
@ -77,7 +80,9 @@ class AutoDetectIntro(AutoDetectBase):
def make_body(self):
return Pile([
Text(_("Keyboard detection starting. You will be asked a series of questions about your keyboard. Press escape at any time to go back to the previous screen.")),
Text(_("Keyboard detection starting. You will be asked a "
"series of questions about your keyboard. Press escape "
"at any time to go back to the previous screen.")),
Text(""),
button_pile([
ok_btn(label=_("OK"), on_press=self.ok),
@ -98,6 +103,7 @@ class AutoDetectFailed(AutoDetectBase):
button_pile([ok_btn(label="OK", on_press=self.ok)]),
])
class AutoDetectResult(AutoDetectBase):
preamble = _("""\
@ -123,10 +129,10 @@ another layout or run the automated detection again.
var_text = _("Variant")
width = max(len(layout_text), len(var_text), 12)
if variant is not None:
var_desc = [Text("%*s: %s"%(width, var_text, variant))]
var_desc = [Text("%*s: %s" % (width, var_text, variant))]
return Pile([
Text(_(self.preamble)),
Text("%*s: %s"%(width, layout_text, layout)),
Text("%*s: %s" % (width, layout_text, layout)),
] + var_desc + [
Text(_(self.postamble)),
button_pile([ok_btn(label=_("OK"), on_press=self.ok)]),
@ -147,7 +153,8 @@ class AutoDetectPressKey(AutoDetectBase):
return Pile([
Text(_("Please press one of the following keys:")),
Text(""),
Columns([Text(s, align="center") for s in self.step.symbols], dividechars=1),
Columns([Text(s, align="center")
for s in self.step.symbols], dividechars=1),
Text(""),
Color.info_error(self.error_text),
])
@ -174,7 +181,8 @@ class AutoDetectPressKey(AutoDetectBase):
elif key.startswith('press '):
code = int(key[len('press '):])
if code not in self.step.keycodes:
self.error_text.set_text(_("Input was not recognized, try again"))
self.error_text.set_text(_("Input was not recognized, "
"try again"))
return
v = self.step.keycodes[code]
else:
@ -260,6 +268,7 @@ class Detector:
self.overlay.start()
self.keyboard_view.show_overlay(self.overlay)
class ApplyingConfig(WidgetWrap):
def __init__(self, loop):
spinner = Spinner(loop, style='dots')
@ -277,9 +286,13 @@ class ApplyingConfig(WidgetWrap):
toggle_text = _("""\
You will need a way to toggle the keyboard between the national layout and the standard Latin layout.
You will need a way to toggle the keyboard between the national layout and
the standard Latin layout.
Right Alt or Caps Lock keys are often chosen for ergonomic reasons (in the latter case, use the combination Shift+Caps Lock for normal Caps toggle). Alt+Shift is also a popular combination; it will however lose its usual behavior in Emacs and other programs that use it for specific needs.
Right Alt or Caps Lock keys are often chosen for ergonomic reasons (in the
latter case, use the combination Shift+Caps Lock for normal Caps toggle).
Alt+Shift is also a popular combination; it will however lose its usual
behavior in Emacs and other programs that use it for specific needs.
Not all listed keys are present on all keyboards. """)
@ -314,7 +327,7 @@ class ToggleQuestion(Stretchy):
self.selector.value = 'alt_shift_toggle'
if self.parent.model.setting.toggle:
try:
self.selector.value = self.parent.model.setting.toggle
self.selector.value = self.parent.model.setting.toggle
except AttributeError:
pass
@ -368,7 +381,7 @@ class KeyboardView(BaseView):
opts = []
for layout, desc in model.layouts.items():
opts.append(Option((desc, True, layout)))
opts.sort(key=lambda o:o.label)
opts.sort(key=lambda o: o.label)
connect_signal(self.form, 'submit', self.done)
connect_signal(self.form, 'cancel', self.cancel)
connect_signal(self.form.layout.widget, "select", self.select_layout)
@ -382,18 +395,23 @@ class KeyboardView(BaseView):
pass
if self.opts.run_on_serial:
excerpt = _('Please select the layout of the keyboard directly attached to the system, if any.')
excerpt = _('Please select the layout of the keyboard directly '
'attached to the system, if any.')
else:
excerpt = _('Please select your keyboard layout below, or select "Identify keyboard" to detect your layout automatically.')
excerpt = _('Please select your keyboard layout below, or select '
'"Identify keyboard" to detect your layout '
'automatically.')
lb_contents = self.form.as_rows()
if not self.opts.run_on_serial:
lb_contents.extend([
Text(""),
button_pile([
other_btn(label=_("Identify keyboard"), on_press=self.detect)]),
other_btn(label=_("Identify keyboard"),
on_press=self.detect)]),
])
super().__init__(screen(lb_contents, self.form.buttons, excerpt=excerpt))
super().__init__(screen(lb_contents, self.form.buttons,
excerpt=excerpt))
def detect(self, sender):
detector = Detector(self)
@ -434,11 +452,12 @@ class KeyboardView(BaseView):
log.debug("%s", layout)
opts = []
default_i = -1
for i, (variant, variant_desc) in enumerate(self.model.variants[layout].items()):
layout_items = enumerate(self.model.variants[layout].items())
for i, (variant, variant_desc) in layout_items:
if variant == "":
default_i = i
opts.append(Option((variant_desc, True, variant)))
opts.sort(key=lambda o:o.label)
opts.sort(key=lambda o: o.label)
if default_i < 0:
opts.insert(0, Option(("default", True, "")))
self.form.variant.widget._options = opts

View File

@ -17,12 +17,14 @@
# /usr/share/console-setup/pc105.tree. This code parses that data into
# subclasses of Step.
class Step:
def __repr__(self):
kvs = []
for k, v in self.__dict__.items():
kvs.append("%s=%r" % (k, v))
return "%s(%s)" % (self.__class__.__name__, ", ".join(sorted(kvs)))
def check(self):
pass
@ -32,20 +34,24 @@ class StepPressKey(Step):
def __init__(self):
self.symbols = []
self.keycodes = {}
def check(self):
if len(self.symbols) == 0 or len(self.keycodes) == 0:
raise Exception
class StepKeyPresent(Step):
# "Is this symbol present on your keyboard"
def __init__(self, symbol):
self.symbol = symbol
self.yes = None
self.no = None
def check(self):
if self.yes is None or self.no is None:
raise Exception
class StepResult(Step):
# "This is the autodetected layout"
def __init__(self, result):

View File

@ -30,12 +30,10 @@ from subiquitycore.ui.form import (
log = logging.getLogger('subiquity.installpath')
proxy_help = _("""\
If you need to use a HTTP proxy to access the outside world, enter the \
proxy information here. Otherwise, leave this blank.
The proxy information should be given in the standard form of \
"http://[[user][:pass]@]host[:port]/".""")
proxy_help = _("If you need to use a HTTP proxy to access the outside world, "
"enter the proxy information here. Otherwise, leave this blank."
"\n\nThe proxy information should be given in the standard "
"form of \"http://[[user][:pass]@]host[:port]/\".")
class ProxyForm(Form):
@ -46,7 +44,8 @@ class ProxyForm(Form):
class ProxyView(BaseView):
title = _("Configure proxy")
excerpt = _("If this system requires a proxy to connect to the internet, enter its details here.")
excerpt = _("If this system requires a proxy to connect to the internet, "
"enter its details here.")
def __init__(self, model, controller):
self.model = model

View File

@ -17,6 +17,7 @@ valid_data = {
'confirm_password': 'password'
}
class IdentityViewTests(unittest.TestCase):
def make_view(self):

View File

@ -20,9 +20,8 @@ Welcome provides user with language selection
"""
import logging
from urwid import Text
from subiquitycore.ui.lists import SimpleList
from subiquitycore.ui.buttons import forward_btn
from subiquitycore.ui.container import Pile
from subiquitycore.ui.container import Pile, ListBox
from subiquitycore.ui.utils import Padding
from subiquitycore.view import BaseView
@ -38,23 +37,25 @@ class WelcomeView(BaseView):
self.controller = controller
super().__init__(Pile([
('pack', Text("")),
('pack', Padding.center_79(Text(_("Please choose your preferred language")))),
('pack', Padding.center_79(
Text(_("Please choose your preferred language")))),
('pack', Text("")),
Padding.center_50(self._build_model_inputs()),
('pack', Text("")),
]))
def _build_model_inputs(self):
sl = []
btns = []
current_index = None
for i, (code, native) in enumerate(self.model.get_languages()):
if code == self.model.selected_language:
current_index = i
sl.append(forward_btn(label=native, on_press=self.confirm, user_arg=code))
btns.append(forward_btn(label=native, on_press=self.confirm,
user_arg=code))
lb = SimpleList(sl)
lb = ListBox(btns)
if current_index is not None:
lb._w.focus_position = current_index
lb.base_widget.focus_position = current_index
return lb
def confirm(self, sender, code):

View File

@ -15,6 +15,9 @@
""" SubiquityCore """
__version__ = "0.0.5"
from subiquitycore import i18n
__all__ = [
'i18n',
]
import subiquitycore.i18n
__version__ = "0.0.5"

View File

@ -53,9 +53,12 @@ class BaseController(ABC):
exception will crash the process so be careful!
"""
fut = self.pool.submit(func)
def in_main_thread(ignored):
callback(fut)
pipe = self.loop.watch_pipe(in_main_thread)
def in_random_thread(ignored):
os.write(pipe, b'x')
fut.add_done_callback(in_random_thread)

View File

@ -55,7 +55,8 @@ class DownNetworkDevices(BackgroundTask):
self.devs_to_down = devs_to_down
def __repr__(self):
return 'DownNetworkDevices(%s)'%([dev.name for dev in self.devs_to_down],)
return 'DownNetworkDevices(%s)' % ([dev.name for dev in
self.devs_to_down],)
def start(self):
for dev in self.devs_to_down:
@ -83,7 +84,7 @@ class WaitForDefaultRouteTask(CancelableTask):
self.event_receiver = event_receiver
def __repr__(self):
return 'WaitForDefaultRouteTask(%r)'%(self.timeout,)
return 'WaitForDefaultRouteTask(%r)' % (self.timeout,)
def got_route(self):
os.write(self.success_w, b'x')
@ -95,7 +96,8 @@ class WaitForDefaultRouteTask(CancelableTask):
def _bg_run(self):
try:
r, _, _ = select.select([self.fail_r, self.success_r], [], [], self.timeout)
r, _, _ = select.select([self.fail_r, self.success_r], [], [],
self.timeout)
return self.success_r in r
finally:
os.close(self.fail_r)
@ -177,6 +179,7 @@ network:
password: password
'''
class NetworkController(BaseController, TaskWatcher):
signals = [
('menu:network:main:set-default-v4-route', 'set_default_v4_route'),
@ -203,7 +206,8 @@ class NetworkController(BaseController, TaskWatcher):
self.model.parse_netplan_configs(self.root)
self.network_event_receiver = SubiquityNetworkEventReceiver(self.model)
self.observer, fds = self.prober.probe_network(self.network_event_receiver)
self.observer, fds = (
self.prober.probe_network(self.network_event_receiver))
for fd in fds:
self.loop.watch_file(fd, partial(self._data_ready, fd))
@ -211,7 +215,7 @@ class NetworkController(BaseController, TaskWatcher):
cp = run_command(['udevadm', 'settle', '-t', '0'])
if cp.returncode != 0:
log.debug("waiting 0.1 to let udev event queue settle")
self.loop.set_alarm_in(0.1, lambda loop, ud:self._data_ready(fd))
self.loop.set_alarm_in(0.1, lambda loop, ud: self._data_ready(fd))
return
self.observer.data_ready(fd)
v = self.ui.frame.body
@ -238,20 +242,23 @@ class NetworkController(BaseController, TaskWatcher):
return os.path.join(self.root, 'etc/netplan', netplan_config_file_name)
def network_finish(self, config):
log.debug("network config: \n%s", yaml.dump(sanitize_config(config), default_flow_style=False))
log.debug("network config: \n%s",
yaml.dump(sanitize_config(config), default_flow_style=False))
netplan_path = self.netplan_path
while True:
try:
tmppath = '%s.%s' % (netplan_path, random.randrange(0, 1000))
fd = os.open(tmppath, os.O_WRONLY | os.O_EXCL | os.O_CREAT, 0o0600)
fd = os.open(tmppath,
os.O_WRONLY | os.O_EXCL | os.O_CREAT, 0o0600)
except FileExistsError:
continue
else:
break
w = os.fdopen(fd, 'w')
with w:
w.write("# This is the network config written by '{}'\n".format(self.opts.project))
w.write("# This is the network config written by "
"'%s'\n" % (self.opts.project))
w.write(yaml.dump(config))
os.rename(tmppath, netplan_path)
self.model.parse_netplan_configs(self.root)
@ -264,25 +271,36 @@ class NetworkController(BaseController, TaskWatcher):
if os.path.exists('/lib/netplan/generate'):
# If netplan appears to be installed, run generate to at
# least test that what we wrote is acceptable to netplan.
tasks.append(('generate', BackgroundProcess(['netplan', 'generate', '--root', self.root])))
tasks.append(('generate',
BackgroundProcess(['netplan', 'generate',
'--root', self.root])))
if not self.tried_once:
tasks.append(('timeout', WaitForDefaultRouteTask(3, self.network_event_receiver)))
tasks.append(
('timeout',
WaitForDefaultRouteTask(3, self.network_event_receiver))
)
tasks.append(('fail', BackgroundProcess(['false'])))
self.tried_once = True
else:
devs_to_down = []
for dev in self.model.get_all_netdevs():
if dev._configuration != self.model.config.config_for_device(dev._net_info):
devcfg = self.model.config.config_for_device(dev._net_info)
if dev._configuration != devcfg:
devs_to_down.append(dev)
tasks = []
if devs_to_down:
tasks.extend([
('stop-networkd', BackgroundProcess(['systemctl', 'stop', 'systemd-networkd.service'])),
('down', DownNetworkDevices(self.observer.rtlistener, devs_to_down)),
('stop-networkd',
BackgroundProcess(['systemctl',
'stop', 'systemd-networkd.service'])),
('down',
DownNetworkDevices(self.observer.rtlistener,
devs_to_down)),
])
tasks.extend([
('apply', BackgroundProcess(['netplan', 'apply'])),
('timeout', WaitForDefaultRouteTask(30, self.network_event_receiver)),
('timeout',
WaitForDefaultRouteTask(30, self.network_event_receiver)),
])
def cancel():
@ -308,29 +326,43 @@ class NetworkController(BaseController, TaskWatcher):
self.signal.emit_signal('next-screen')
def set_default_v4_route(self):
#self.ui.set_header("Default route")
self.ui.set_body(NetworkSetDefaultRouteView(self.model, socket.AF_INET, self))
self.ui.set_header("Default route")
self.ui.set_body(
NetworkSetDefaultRouteView(self.model, socket.AF_INET, self))
def set_default_v6_route(self):
#self.ui.set_header("Default route")
self.ui.set_body(NetworkSetDefaultRouteView(self.model, socket.AF_INET6, self))
self.ui.set_header("Default route")
self.ui.set_body(
NetworkSetDefaultRouteView(self.model, socket.AF_INET6, self))
def bond_interfaces(self):
#self.ui.set_header("Bond interfaces")
self.ui.set_body(NetworkBondInterfacesView(self.model, self))
def network_configure_interface(self, iface):
self.ui.set_body(NetworkConfigureInterfaceView(self.model, self, iface))
self.ui.set_header(_("Network interface {}").format(iface))
self.ui.set_footer("")
self.ui.set_body(
NetworkConfigureInterfaceView(self.model, self, iface))
def network_configure_ipv4_interface(self, iface):
self.ui.set_body(NetworkConfigureIPv4InterfaceView(self.model, self, iface))
self.ui.set_header(_(
"Network interface {} manual IPv4 configuration").format(iface))
self.ui.set_footer("")
self.ui.set_body(
NetworkConfigureIPv4InterfaceView(self.model, self, iface))
def network_configure_wlan_interface(self, iface):
self.ui.set_header(_(
"Network interface {} WIFI configuration").format(iface))
self.ui.set_footer("")
self.ui.set_body(NetworkConfigureWLANView(self.model, self, iface))
def network_configure_ipv6_interface(self, iface):
self.ui.set_body(NetworkConfigureIPv6InterfaceView(self.model, self, iface))
self.ui.set_header(_(
"Network interface {} manual IPv6 configuration").format(iface))
self.ui.set_footer("")
self.ui.set_body(
NetworkConfigureIPv6InterfaceView(self.model, self, iface))
def install_network_driver(self):
self.ui.set_body(DummyView(self))

View File

@ -35,22 +35,23 @@ class ApplicationError(Exception):
""" Basecontroller exception """
pass
# From uapi/linux/kd.h:
KDGKBTYPE = 0x4B33 # get keyboard type
GIO_CMAP = 0x4B70 # gets colour palette on VGA+
PIO_CMAP = 0x4B71 # sets colour palette on VGA+
GIO_CMAP = 0x4B70 # gets colour palette on VGA+
PIO_CMAP = 0x4B71 # sets colour palette on VGA+
UO_R, UO_G, UO_B = 0xe9, 0x54, 0x20
# /usr/include/linux/kd.h
K_RAW = 0x00
K_XLATE = 0x01
K_RAW = 0x00
K_XLATE = 0x01
K_MEDIUMRAW = 0x02
K_UNICODE = 0x03
K_OFF = 0x04
K_UNICODE = 0x03
K_OFF = 0x04
KDGKBMODE = 0x4B44 # gets current keyboard mode
KDSKBMODE = 0x4B45 # sets current keyboard mode
KDGKBMODE = 0x4B44 # gets current keyboard mode
KDSKBMODE = 0x4B45 # sets current keyboard mode
class ISO_8613_3_Screen(urwid.raw_display.Screen):
@ -65,7 +66,8 @@ class ISO_8613_3_Screen(urwid.raw_display.Screen):
def _attrspec_to_escape(self, a):
f_r, f_g, f_b = self._fg_to_rgb[a.foreground]
b_r, b_g, b_b = self._bg_to_rgb[a.background]
return "\x1b[38;2;{};{};{};48;2;{};{};{}m".format(f_r, f_g, f_b, b_r, b_g, b_b)
return "\x1b[38;2;{};{};{};48;2;{};{};{}m".format(f_r, f_g, f_b,
b_r, b_g, b_b)
def is_linux_tty():
@ -78,7 +80,6 @@ def is_linux_tty():
return r == b'\x02'
def setup_screen(colors, styles):
"""Return a palette and screen to be passed to MainLoop.
@ -96,7 +97,8 @@ def setup_screen(colors, styles):
# class that displays maps the standard color name to the value
# specified in colors using 24-bit control codes.
if len(colors) != 8:
raise Exception("setup_screen must be passed a list of exactly 8 colors")
raise Exception(
"setup_screen must be passed a list of exactly 8 colors")
urwid_8_names = (
'black',
'dark red',
@ -128,7 +130,6 @@ def setup_screen(colors, styles):
return ISO_8613_3_Screen(_urwid_name_to_rgb), urwid_palette
class KeyCodesFilter:
"""input_filter that can pass (medium) raw keycodes to the application
@ -149,7 +150,8 @@ class KeyCodesFilter:
def enter_keycodes_mode(self):
log.debug("enter_keycodes_mode")
self.filtering = True
# Read the old keyboard mode (it will proably always be K_UNICODE but well).
# Read the old keyboard mode (it will proably always be
# K_UNICODE but well).
o = bytearray(4)
fcntl.ioctl(self._fd, KDGKBMODE, o)
self._old_mode = struct.unpack('i', o)[0]
@ -159,13 +161,16 @@ class KeyCodesFilter:
self._old_settings = termios.tcgetattr(self._fd)
new_settings = termios.tcgetattr(self._fd)
new_settings[tty.IFLAG] = 0
new_settings[tty.LFLAG] = new_settings[tty.LFLAG] & ~(termios.ECHO | termios.ICANON | termios.ISIG)
new_settings[tty.LFLAG] = new_settings[tty.LFLAG] & ~(termios.ECHO |
termios.ICANON |
termios.ISIG)
new_settings[tty.CC][termios.VMIN] = 0
new_settings[tty.CC][termios.VTIME] = 0
termios.tcsetattr(self._fd, termios.TCSAFLUSH, new_settings)
# Finally, set the keyboard mode to K_MEDIUMRAW, which causes
# the keyboard driver in the kernel to pass us keycodes.
log.debug("old mode was %s, setting mode to %s", self._old_mode, K_MEDIUMRAW)
log.debug("old mode was %s, setting mode to %s",
self._old_mode, K_MEDIUMRAW)
fcntl.ioctl(self._fd, KDSKBMODE, K_MEDIUMRAW)
def exit_keycodes_mode(self):
@ -188,9 +193,12 @@ class KeyCodesFilter:
p = 'release '
else:
p = 'press '
if i + 2 < n and (codes[i] & 0x7f) == 0 and (codes[i + 1] & 0x80) != 0 and (codes[i + 2] & 0x80) != 0:
kc = ((codes[i + 1] & 0x7f) << 7) | (codes[i + 2] & 0x7f)
i += 3
if i + 2 < n and (codes[i] & 0x7f) == 0:
if (codes[i + 1] & 0x80) != 0:
if (codes[i + 2] & 0x80) != 0:
kc = (((codes[i + 1] & 0x7f) << 7) |
(codes[i + 2] & 0x7f))
i += 3
else:
kc = codes[i] & 0x7f
i += 1
@ -266,7 +274,8 @@ class Application:
"input_filter": input_filter,
}
if opts.screens:
self.controllers = [c for c in self.controllers if c in opts.screens]
self.controllers = [c for c in self.controllers
if c in opts.screens]
ui.progress_completion = len(self.controllers)
self.common['controllers'] = dict.fromkeys(self.controllers)
self.controller_index = -1
@ -356,18 +365,21 @@ class Application:
loop.set_alarm_in(0.01, _run_script)
def c(pat):
but = view_helpers.find_button_matching(self.common['ui'], '.*' + pat + '.*')
but = view_helpers.find_button_matching(self.common['ui'],
'.*' + pat + '.*')
if not but:
ss.wait_count += 1
if ss.wait_count > 10:
raise Exception("no button found matching %r after waiting for 10 secs"%(pat,))
wait(1, func=lambda : c(pat))
raise Exception("no button found matching %r after"
"waiting for 10 secs" % pat)
wait(1, func=lambda: c(pat))
return
ss.wait_count = 0
view_helpers.click(but)
def wait(delay, func=None):
ss.waiting = True
def next(loop, user_data):
ss.waiting = False
if func is not None:
@ -394,7 +406,8 @@ class Application:
self.common['loop'] = urwid.MainLoop(
self.common['ui'], palette=palette, screen=screen,
handle_mouse=False, pop_ups=True, input_filter=self.common['input_filter'].filter)
handle_mouse=False, pop_ups=True,
input_filter=self.common['input_filter'].filter)
log.debug("Running event loop: {}".format(
self.common['loop'].event_loop))
@ -403,7 +416,8 @@ class Application:
self.common['loop'].set_alarm_in(0.05, self.next_screen)
if self.common['opts'].scripts:
self.run_scripts(self.common['opts'].scripts)
controllers_mod = __import__('%s.controllers' % self.project, None, None, [''])
controllers_mod = __import__('%s.controllers' % self.project,
None, None, [''])
for k in self.controllers:
log.debug("Importing controller: {}".format(k))
klass = getattr(controllers_mod, k+"Controller")
@ -411,7 +425,7 @@ class Application:
log.debug("*** %s", self.common['controllers'])
self._connect_base_signals()
self.common['loop'].run()
except:
except Exception:
log.exception("Exception in controller.run():")
raise
finally:

View File

@ -27,12 +27,15 @@ if os.path.isdir(build_mo):
localedir = build_mo
syslog.syslog('Final localedir is ' + localedir)
def switch_language(code='en_US'):
if code != 'en_US' and 'FAKE_TRANSLATE' in os.environ:
def my_gettext(message):
return "_(%s)" % message
elif code:
translation = gettext.translation('subiquity', localedir=localedir, languages=[code])
translation = gettext.translation('subiquity', localedir=localedir,
languages=[code])
def my_gettext(message):
if not message:
return message
@ -40,6 +43,7 @@ def switch_language(code='en_US'):
import builtins
builtins.__dict__['_'] = my_gettext
switch_language()
__all__ = ['switch_language']

View File

@ -35,8 +35,6 @@ def setup_logger(dir):
log.setLevel('DEBUG')
log.setFormatter(
logging.Formatter("%(asctime)s %(name)s:%(lineno)d %(message)s"))
# log_filter = logging.Filter(name='subiquity')
# log.addFilter(log_filter)
logger = logging.getLogger('')
logger.setLevel('DEBUG')

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/>.
from .network import NetworkModel # NOQA
from .identity import IdentityModel # NOQA
from .login import LoginModel # NOQA
from .identity import IdentityModel
from .login import LoginModel
from .network import NetworkModel
__all__ = ['IdentityModel', 'LoginModel', 'NetworkModel']

View File

@ -21,7 +21,8 @@ import logging
import os
from socket import AF_INET, AF_INET6
import yaml, yaml.reader
import yaml
from yaml.reader import ReaderError
NETDEV_IGNORED_IFACE_NAMES = ['lo']
@ -72,7 +73,7 @@ class NetplanConfig:
def parse_netplan_config(self, config):
try:
config = yaml.safe_load(config)
except yaml.reader.ReaderError as e:
except ReaderError as e:
log.info("could not parse config: %s", e)
return
network = config.get('network')
@ -188,7 +189,7 @@ class Networkdev:
return "{}G".format(int(speed / 1000))
def dhcp_for_version(self, version):
dhcp_key = 'dhcp%s'%(version,)
dhcp_key = 'dhcp%s' % version
return self._configuration.get(dhcp_key, False)
@property
@ -218,11 +219,13 @@ class Networkdev:
fam = AF_INET
elif version == 6:
fam = AF_INET6
return [addr.ip for _, addr in sorted(self._net_info.addresses.items()) if addr.family == fam]
return [addr.ip for _, addr in sorted(self._net_info.addresses.items())
if addr.family == fam]
@property
def actual_ip_addresses(self):
return self.actual_ip_addresses_for_version(4) + self.actual_ip_addresses_for_version(6)
return (self.actual_ip_addresses_for_version(4) +
self.actual_ip_addresses_for_version(6))
def configured_ip_addresses_for_version(self, version):
r = []
@ -233,17 +236,18 @@ class Networkdev:
@property
def actual_global_ip_addresses(self):
return [addr.ip for _, addr in sorted(self._net_info.addresses.items()) if addr.scope == "global"]
return [addr.ip for _, addr in sorted(self._net_info.addresses.items())
if addr.scope == "global"]
@property
def configured_ip_addresses(self):
return self._configuration.setdefault('addresses', [])
def configured_gateway_for_version(self, version):
return self._configuration.get('gateway%s'%(version,), None)
return self._configuration.get('gateway%s' % version, None)
def set_configured_gateway_for_version(self, version, gateway):
key = 'gateway%s'%(version,)
key = 'gateway%s' % version
if gateway is None:
self._configuration.pop(key, None)
else:
@ -304,7 +308,7 @@ class Networkdev:
self.remove_ip_networks_for_version(6)
def remove_ip_networks_for_version(self, version):
dhcp_key = 'dhcp%s'%(version,)
dhcp_key = 'dhcp%s' % version
setattr(self, dhcp_key, False)
addrs = []
for ip in self._configuration.get('addresses', []):
@ -346,28 +350,26 @@ def _sanitize_inteface_config(iface_config):
if 'password' in ap_config:
ap_config['password'] = '<REDACTED>'
def sanitize_interface_config(iface_config):
iface_config = copy.deepcopy(iface_config)
_sanitize_inteface_config(iface_config)
return iface_config
def sanitize_config(config):
"""Return a copy of config with passwords redacted."""
config = copy.deepcopy(config)
for iface, iface_config in config.get('network', {}).get('wifis', {}).items():
interfaces = config.get('network', {}).get('wifis', {}).items()
for iface, iface_config in interfaces:
_sanitize_inteface_config(iface_config)
return config
class NetworkModel(object):
""" Model representing network interfaces
"""
additional_options = [
#('Set a custom IPv4 default route', 'menu:network:main:set-default-v4-route'),
#('Set a custom IPv6 default route', 'menu:network:main:set-default-v6-route'),
#('Bond interfaces', 'menu:network:main:bond-interfaces'),
#('Install network driver', 'network:install-network-driver'),
]
additional_options = []
# TODO: what is "linear" level?
bonding_modes = {
@ -382,8 +384,8 @@ class NetworkModel(object):
def __init__(self, support_wlan=True):
self.support_wlan = support_wlan
self.devices = {} # Maps ifindex to Networkdev
self.devices_by_name = {} # Maps interface names to Networkdev
self.devices = {} # Maps ifindex to Networkdev
self.devices_by_name = {} # Maps interface names to Networkdev
self.default_v4_gateway = None
self.default_v6_gateway = None
self.v4_gateway_dev = None
@ -393,9 +395,10 @@ class NetworkModel(object):
def parse_netplan_configs(self, netplan_root):
self.config = NetplanConfig()
configs_by_basename = {}
paths = glob.glob(os.path.join(netplan_root, 'lib/netplan', "*.yaml")) + \
glob.glob(os.path.join(netplan_root, 'etc/netplan', "*.yaml")) + \
glob.glob(os.path.join(netplan_root, 'run/netplan', "*.yaml"))
paths = (
glob.glob(os.path.join(netplan_root, 'lib/netplan', "*.yaml")) +
glob.glob(os.path.join(netplan_root, 'etc/netplan', "*.yaml")) +
glob.glob(os.path.join(netplan_root, 'run/netplan', "*.yaml")))
for path in paths:
configs_by_basename[os.path.basename(path)] = path
for _, path in sorted(configs_by_basename.items()):
@ -419,7 +422,8 @@ class NetworkModel(object):
if link.is_virtual:
return
config = self.config.config_for_device(link)
log.debug("new_link %s %s with config %s", ifindex, link.name, sanitize_interface_config(config))
log.debug("new_link %s %s with config %s",
ifindex, link.name, sanitize_interface_config(config))
self.devices[ifindex] = Networkdev(link, config)
self.devices_by_name[link.name] = Networkdev(link, config)
@ -489,7 +493,7 @@ class NetworkModel(object):
},
"type": "bond"
}
bondinfo = make_network_info(ifname, info)
bondinfo = info
bonddev = Networkdev(ifname, 'bond')
bonddev.configure(probe_info=bondinfo)
@ -558,9 +562,10 @@ class NetworkModel(object):
nw_routes = []
if self.default_v4_gateway:
nw_routes.append({ 'to': '0.0.0.0/0', 'via': self.default_v4_gateway })
nw_routes.append(
{'to': '0.0.0.0/0', 'via': self.default_v4_gateway})
if self.default_v6_gateway is not None:
nw_routes.append({ 'to': '::/0', 'via': self.default_v6_gateway })
nw_routes.append({'to': '::/0', 'via': self.default_v6_gateway})
if len(nw_routes) > 0:
config['network']['routes'] = nw_routes

View File

@ -15,8 +15,8 @@
import logging
import yaml
import os
from probert.network import (StoredDataObserver, UdevObserver)
from probert.network import (StoredDataObserver,
UdevObserver)
from probert.storage import (Storage,
StorageInfo)
@ -38,8 +38,8 @@ class Prober():
if self.opts.machine_config:
log.debug('User specified machine_config: {}'.format(
self.opts.machine_config))
self.saved_config = \
self._load_machine_config(self.opts.machine_config)
self.saved_config = (
self._load_machine_config(self.opts.machine_config))
self.probe_data = self.saved_config
log.debug('Prober() init finished, data:{}'.format(self.saved_config))

View File

@ -65,8 +65,6 @@ class Signal:
urwid.emit_signal(self, 'menu:welcome:main')
def emit_signal(self, name, *args, **kwargs):
# Disabled because it can reveal credentials in the arguments.
#log.debug("Emitter: {}, {}, {}".format(name, args, kwargs))
if name.startswith("menu:"):
log.debug(" emit: before: "
"size={} stack={}".format(len(self.signal_stack),

View File

@ -110,7 +110,7 @@ class PythonSleep(CancelableTask):
self.cancel_r, self.cancel_w = os.pipe()
def __repr__(self):
return 'PythonSleep(%r)'%(self.duration,)
return 'PythonSleep(%r)' % (self.duration,)
def start(self):
pass
@ -125,7 +125,8 @@ class PythonSleep(CancelableTask):
# and there's no other way to fail so just return.
def end(self, observer, fut):
# Call fut.result() to cater for the case that _bg_run somehow managed to raise an exception.
# Call fut.result() to cater for the case that _bg_run somehow managed
# to raise an exception.
fut.result()
# Call task_succeeded() because if we got here, we weren't canceled.
observer.task_succeeded()
@ -141,7 +142,7 @@ class BackgroundProcess(CancelableTask):
self.proc = None
def __repr__(self):
return 'BackgroundProcess(%r)'%(self.cmd,)
return 'BackgroundProcess(%r)' % (self.cmd,)
def start(self):
self.proc = start_command(self.cmd)
@ -167,7 +168,7 @@ class BackgroundProcess(CancelableTask):
try:
self.proc.terminate()
except ProcessLookupError:
pass # It's OK if the process has already terminated.
pass # It's OK if the process has already terminated.
class TaskWatcher(ABC):
@ -201,7 +202,8 @@ class TaskSequence:
self._run1()
def cancel(self):
if self.curtask is not None and isinstance(self.curtask, CancelableTask):
if self.curtask is not None and isinstance(self.curtask,
CancelableTask):
log.debug("canceling %s", self.curtask)
self.curtask.cancel()
self.canceled = True
@ -220,11 +222,12 @@ class TaskSequence:
self.task_complete_or_failed_called = False
try:
self.curtask.end(self, fut)
except:
except Exception:
log.exception("%s failed", self.stage)
self.task_failed(sys.exc_info())
if not self.task_complete_or_failed_called:
raise RuntimeError("{} {}.end did not call task_complete or task_failed".format(self.stage, self.curtask))
raise RuntimeError("{} {}.end did not call task_complete or "
"task_failed".format(self.stage, self.curtask))
def task_succeeded(self):
self.task_complete_or_failed_called = True

View File

@ -1,11 +1,12 @@
import re
import urwid
def find_with_pred(w, pred, return_path=False):
def _walk(w, path):
if not isinstance(w, urwid.Widget):
raise RuntimeError("_walk walked to non-widget %r via %r" % (w, path))
raise RuntimeError(
"_walk walked to non-widget %r via %r" % (w, path))
if pred(w):
return w, path
if hasattr(w, '_wrapped_widget'):
@ -35,22 +36,27 @@ def find_with_pred(w, pred, return_path=False):
else:
return r
def find_button_matching(w, pat, return_path=False):
def pred(w):
return isinstance(w, urwid.Button) and re.match(pat, w.label)
return find_with_pred(w, pred, return_path)
def click(but):
but._emit('click')
def keypress(w, key, size=(30, 1)):
w.keypress(size, key)
def get_focus_path(w):
path = []
while True:
path.append(w)
if isinstance(w, urwid.ListBox) and w.set_focus_pending == "first selectable":
if isinstance(w, urwid.ListBox) and (w.set_focus_pending ==
"first selectable"):
for w2 in w.body:
if w2.selectable():
w = w2
@ -67,6 +73,7 @@ def get_focus_path(w):
break
return path
def enter_data(form, data):
for k, v in data.items():
getattr(form, k).value = v

View File

@ -15,7 +15,6 @@
from urwid import WidgetWrap, Pile, Text, ProgressBar
from subiquitycore.ui.utils import Padding, Color
from subiquitycore.ui.lists import SimpleList
class Header(WidgetWrap):
@ -42,6 +41,7 @@ class StepsProgressBar(ProgressBar):
def get_text(self):
return "{} / {}".format(self.current, self.done)
class Footer(WidgetWrap):
""" Footer widget
@ -64,11 +64,3 @@ class Footer(WidgetWrap):
message_widget,
]
super().__init__(Color.frame_footer(Pile(status)))
class Body(WidgetWrap):
""" Body widget
"""
def __init__(self):
super().__init__(SimpleList([Text("")]))

View File

@ -15,6 +15,7 @@
from urwid import AttrMap, Button, Text
def _stylized_button(left, right, style):
class Btn(Button):
button_left = Text(left)
@ -26,9 +27,11 @@ def _stylized_button(left, right, style):
super().__init__(btn, style + '_button', style + '_button focus')
return StyleAttrMap
def action_button(style):
return _stylized_button('[', ']', style)
menu_btn = _stylized_button("", ">", "menu")
forward_btn = _stylized_button("", ">", "done")
done_btn = action_button("done")
@ -42,5 +45,3 @@ delete_btn = danger_btn
back_btn = other_btn
cancel_btn = other_btn
reset_btn = other_btn

View File

@ -14,26 +14,28 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# This is adapted from
# https://github.com/pimutils/khal/commit/bd7c5f928a7670de9afae5657e66c6dc846688ac, which has this license:
# https://github.com/pimutils/khal/commit/bd7c5f928a7670de9afae5657e66c6dc846688ac # noqa
#
#
# Copyright (c) 2013-2015 Christian Geier et al.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
# the Software, and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR OPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""Extended versions of urwid containers.
@ -107,7 +109,6 @@ def _has_other_selectable(widgets, cur_focus):
return False
for key, command in list(urwid.command_map._command.items()):
if command in ('next selectable', 'prev selectable', urwid.ACTIVATE):
urwid.command_map[key + ' no wrap'] = command
@ -149,7 +150,7 @@ class TabCyclingPile(urwid.Pile):
# reason we can't do this via simple subclassing is the need to
# call _select_{first,last}_selectable on the newly focused
# element when the focus changes.
def keypress(self, size, key ):
def keypress(self, size, key):
"""Pass the keypress to the widget in focus.
Unhandled 'up' and 'down' keys may cause a focus change."""
if not self.contents:
@ -157,7 +158,9 @@ class TabCyclingPile(urwid.Pile):
# start subiquity change: new code
downkey = key
if not key.endswith(' no wrap') and self._command_map[key] in ('next selectable', 'prev selectable'):
if not key.endswith(' no wrap') and (self._command_map[key] in
('next selectable',
'prev selectable')):
if _has_other_selectable(self._widgets(), self.focus_position):
downkey += ' no wrap'
# end subiquity change
@ -169,9 +172,12 @@ class TabCyclingPile(urwid.Pile):
i = self.focus_position
if self.selectable():
tsize = self.get_item_size(size, i, True, item_rows)
# start subiquity change: pass downkey to focus, not key, do not return if command_map[upkey] is next/prev selectable
# start subiquity change: pass downkey to focus, not key,
# do not return if command_map[upkey] is next/prev selectable
upkey = self.focus.keypress(tsize, downkey)
if self._command_map[upkey] not in ('cursor up', 'cursor down', 'next selectable', 'prev selectable'):
if self._command_map[upkey] not in ('cursor up', 'cursor down',
'next selectable',
'prev selectable'):
return upkey
# end subiquity change
@ -187,7 +193,8 @@ class TabCyclingPile(urwid.Pile):
self._select_first_selectable()
return key
elif self._command_map[key] == 'prev selectable':
for i, (w, o) in reversed(list(enumerate(self._contents[:self.focus_position]))):
positions = self._contents[:self.focus_position]
for i, (w, o) in reversed(list(enumerate(positions))):
if w.selectable():
self.set_focus(i)
_maybe_call(w, "_select_last_selectable")
@ -197,9 +204,9 @@ class TabCyclingPile(urwid.Pile):
return key
# continued subiquity change: set 'meth' appropriately
elif self._command_map[key] == 'cursor up':
candidates = list(range(i-1, -1, -1)) # count backwards to 0
candidates = list(range(i-1, -1, -1)) # count backwards to 0
meth = '_select_last_selectable'
else: # self._command_map[key] == 'cursor down'
else: # self._command_map[key] == 'cursor down'
candidates = list(range(i+1, len(self.contents)))
meth = '_select_first_selectable'
# end subiquity change
@ -222,7 +229,7 @@ class TabCyclingPile(urwid.Pile):
rows = item_rows[j]
if self._command_map[key] == 'cursor up':
rowlist = list(range(rows-1, -1, -1))
else: # self._command_map[key] == 'cursor down'
else: # self._command_map[key] == 'cursor down'
rowlist = list(range(rows))
for row in rowlist:
tsize = self.get_item_size(size, j, True, item_rows)
@ -237,15 +244,16 @@ class TabCyclingPile(urwid.Pile):
class OneSelectableColumns(urwid.Columns):
def __init__(self, widget_list, dividechars=0, focus_column=None,
min_width=1, box_columns=None):
super().__init__(widget_list, dividechars, focus_column, min_width, box_columns)
min_width=1, box_columns=None):
super().__init__(widget_list, dividechars, focus_column,
min_width, box_columns)
selectables = 0
for w, o in self._contents:
if w.selectable():
selectables +=1
selectables += 1
if selectables > 1:
raise Exception("subiquity only supports one selectable in a Columns")
raise Exception(
"subiquity only supports one selectable in a Columns")
class TabCyclingListBox(urwid.ListBox):
@ -288,7 +296,9 @@ class TabCyclingListBox(urwid.ListBox):
def keypress(self, size, key):
downkey = key
if not key.endswith(' no wrap') and self._command_map[key] in ('next selectable', 'prev selectable'):
if not key.endswith(' no wrap') and (self._command_map[key] in
('next selectable',
'prev selectable')):
if _has_other_selectable(self.body, self.focus_position):
downkey += ' no wrap'
upkey = super().keypress(size, downkey)
@ -309,7 +319,8 @@ class TabCyclingListBox(urwid.ListBox):
self._select_first_selectable()
return key
elif self._command_map[key] == 'prev selectable':
for i, w in reversed(list(enumerate(self.body[:self.focus_position]))):
positions = self.body[:self.focus_position]
for i, w in reversed(list(enumerate(positions))):
if w.selectable():
self.set_focus(i)
_maybe_call(w, "_select_last_selectable")
@ -352,6 +363,7 @@ class FocusTrackingMixin:
class FocusTrackingPile(FocusTrackingMixin, TabCyclingPile):
pass
class FocusTrackingColumns(FocusTrackingMixin, OneSelectableColumns):
pass
@ -386,6 +398,7 @@ class FocusTrackingListBox(TabCyclingListBox):
Columns = FocusTrackingColumns
Pile = FocusTrackingPile
class ScrollBarListBox(urwid.WidgetDecoration):
def __init__(self, lb):
@ -396,9 +409,11 @@ class ScrollBarListBox(urwid.WidgetDecoration):
f("\N{FULL BLOCK}", 'scrollbar_fg'),
]
self.bar = Pile([
('weight', 1, f("\N{BOX DRAWINGS LIGHT VERTICAL}", 'scrollbar_bg')),
('weight', 1, f("\N{BOX DRAWINGS LIGHT VERTICAL}",
'scrollbar_bg')),
('weight', 1, self.boxes[0]),
('weight', 1, f("\N{BOX DRAWINGS LIGHT VERTICAL}", 'scrollbar_bg')),
('weight', 1, f("\N{BOX DRAWINGS LIGHT VERTICAL}",
'scrollbar_bg')),
])
super().__init__(lb)
@ -418,7 +433,9 @@ class ScrollBarListBox(urwid.WidgetDecoration):
# case for all the listboxes we have in subiquity today.
maxcol, maxrow = size
offset, inset = self.original_widget.get_focus_offset_inset((maxcol - 1, maxrow))
offset, inset = (
self.original_widget.get_focus_offset_inset((maxcol - 1,
maxrow)))
seen_focus = False
height = height_before_focus = 0
@ -464,7 +481,8 @@ class ScrollBarListBox(urwid.WidgetDecoration):
(self.bar.contents[2][0], self.bar.options('weight', bottom)),
]
canvases = [
(self.original_widget.render((maxcol - 1, maxrow), focus), self.original_widget.focus_position, True, maxcol - 1),
(self.original_widget.render((maxcol - 1, maxrow), focus),
self.original_widget.focus_position, True, maxcol - 1),
(self.bar.render((1, maxrow)), None, False, 1)
]
return urwid.CanvasJoin(canvases)

View File

@ -41,11 +41,13 @@ from subiquitycore.ui.utils import button_pile, Color, screen
log = logging.getLogger("subiquitycore.ui.form")
class Toggleable(delegate_to_widget_mixin('_original_widget'), WidgetDecoration):
class Toggleable(delegate_to_widget_mixin('_original_widget'),
WidgetDecoration):
def __init__(self, original):
if not isinstance(original, AttrMap):
raise RuntimeError("Toggleable must be passed an AttrMap, not %s", original)
raise RuntimeError(
"Toggleable must be passed an AttrMap, not %s", original)
self.original = original
self.enabled = False
self.enable()
@ -57,7 +59,8 @@ class Toggleable(delegate_to_widget_mixin('_original_widget'), WidgetDecoration)
def disable(self):
if self.enabled:
self.original_widget = WidgetDisable(Color.info_minor(self.original.original_widget))
self.original_widget = (
WidgetDisable(Color.info_minor(self.original.original_widget)))
self.enabled = False
@ -98,6 +101,7 @@ class WantsToKnowFormField(object):
def set_bound_form_field(self, bff):
self.bff = bff
class BoundFormField(object):
def __init__(self, field, form, widget):
@ -243,7 +247,8 @@ class BoundFormField(object):
if val != self._enabled:
self._enabled = val
if self.pile is not None:
self.pile.contents[0] = (self._cols(), self.pile.contents[0][1])
self.pile.contents[0] = (self._cols(),
self.pile.contents[0][1])
def simple_field(widget_maker):
@ -282,6 +287,7 @@ class URLEditor(StringEditor, WantsToKnowFormField):
raise ValueError(_("This field must be a %s URL.") % schemes)
return v
URLField = simple_field(URLEditor)
@ -306,7 +312,7 @@ class MetaForm(MetaSignals):
if v.caption is None:
v.caption = k + ":"
_unbound_fields.append(v)
_unbound_fields.sort(key=lambda f:f.index)
_unbound_fields.sort(key=lambda f: f.index)
self._unbound_fields = _unbound_fields
@ -318,8 +324,10 @@ class Form(object, metaclass=MetaForm):
cancel_label = _("Cancel")
def __init__(self, initial={}):
self.done_btn = Toggleable(done_btn(_(self.ok_label), on_press=self._click_done))
self.cancel_btn = Toggleable(cancel_btn(_(self.cancel_label), on_press=self._click_cancel))
self.done_btn = Toggleable(done_btn(_(self.ok_label),
on_press=self._click_done))
self.cancel_btn = Toggleable(cancel_btn(_(self.cancel_label),
on_press=self._click_cancel))
self.buttons = button_pile([self.done_btn, self.cancel_btn])
self._fields = []
for field in self._unbound_fields:

View File

@ -15,8 +15,13 @@
""" Base Frame Widget """
from urwid import Frame, WidgetWrap
from subiquitycore.ui.anchors import Header, Footer, Body
from urwid import (
Frame,
Text,
WidgetWrap,
)
from subiquitycore.ui.anchors import Header, Footer
from subiquitycore.ui.container import ListBox
from subiquitycore.ui.utils import Color
import logging
@ -28,9 +33,10 @@ class SubiquityUI(WidgetWrap):
def __init__(self):
self.header = Header("")
self.body = Body()
self.footer = Footer("", 0, 1)
self.frame = Frame(self.body, header=self.header, footer=self.footer)
self.frame = Frame(
ListBox([Text("")]),
header=self.header, footer=self.footer)
self.progress_current = 0
self.progress_completion = 0
super().__init__(Color.body(self.frame))
@ -42,7 +48,8 @@ class SubiquityUI(WidgetWrap):
self.frame.header = Header(title)
def set_footer(self, message):
self.frame.footer = Footer(message, self.progress_current, self.progress_completion)
self.frame.footer = Footer(message, self.progress_current,
self.progress_completion)
def set_body(self, widget):
self.set_header(_(widget.title))

View File

@ -1,33 +0,0 @@
# 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 urwid import SimpleFocusListWalker, WidgetWrap
from subiquitycore.ui.container import ListBox
class SimpleList(WidgetWrap):
def __init__(self, contents, is_selectable=True):
self.contents = contents
self.is_selectable = is_selectable
super().__init__(self._build_widget())
def _build_widget(self):
lw = SimpleFocusListWalker(list(self.contents))
return ListBox(lw)
def selectable(self):
return self.is_selectable

View File

@ -28,8 +28,9 @@ from subiquitycore.ui.container import ListBox
class _PopUpButton(SelectableIcon):
"""It looks a bit like a radio button, but it just emits 'click' on activation."""
"""It looks a bit like a radio button, but it just emits
'click' on activation.
"""
signals = ['click']
states = {
@ -55,7 +56,7 @@ class _PopUpSelectDialog(WidgetWrap):
group = []
for i, option in enumerate(self.parent._options):
if option.enabled:
btn = _PopUpButton(option.label, state=i==cur_index)
btn = _PopUpButton(option.label, state=(i == cur_index))
connect_signal(btn, 'click', self.click, i)
group.append(AttrWrap(btn, 'menu_button', 'menu_button focus'))
else:
@ -106,7 +107,8 @@ class Option:
class Selector(PopUpLauncher):
"""A widget that allows the user to chose between options by popping up a list of options.
"""A widget that allows the user to chose between options by popping
up a list of options.
(A bit like <select> in an HTML form).
"""
@ -172,6 +174,9 @@ class Selector(PopUpLauncher):
return _PopUpSelectDialog(self, self.index)
def get_pop_up_parameters(self):
width = max([len(o.label) for o in self._options]) \
+ len(self._prefix) + 3 # line on left, space, line on right
return {'left':-1, 'top':-self.index-1, 'overlay_width':width, 'overlay_height':len(self._options) + 2}
# line on left, space, line on right
width = (max([len(o.label) for o in self._options]) +
len(self._prefix) + 3)
return {'left': -1, 'top': -self.index - 1,
'overlay_width': width,
'overlay_height': len(self._options) + 2}

View File

@ -50,6 +50,7 @@ import urwid
from subiquitycore.ui.container import ListBox, Pile
class Stretchy:
def __init__(self, title, widgets, stretchy_index, focus_index):
"""
@ -71,17 +72,20 @@ class Stretchy:
class StretchyOverlay(urwid.Widget):
_selectable = True
_sizing = frozenset([urwid.BOX])
def __init__(self, bottom_w, stretchy):
self.bottom_w = bottom_w
self.stretchy = stretchy
self.listbox = ListBox([stretchy.stretchy_w])
def entry(i, w):
if i == stretchy.stretchy_index:
return ('weight', 1, self.listbox)
else:
return ('pack', w)
inner_pile = Pile([entry(i, w) for (i, w) in enumerate(stretchy.widgets)])
inner_pile = Pile(
[entry(i, w) for (i, w) in enumerate(stretchy.widgets)])
inner_pile.focus_position = stretchy.focus_index
# this Filler/Padding/LineBox/Filler/Padding construction
# seems ridiculous but it works.
@ -98,12 +102,14 @@ class StretchyOverlay(urwid.Widget):
top=1, bottom=1, height=('relative', 100))
def _top_size(self, size, focus):
# Returns the size of the top widget and whether the scollbar will be shown.
# Returns the size of the top widget and whether
# the scollbar will be shown.
maxcol, maxrow = size # we are a BOX widget
maxcol, maxrow = size # we are a BOX widget
outercol = min(maxcol, 80)
innercol = outercol - 10 # (3 outer padding, 1 line, 2 inner padding) x 2
fixed_rows = 6 # lines at top and bottom and padding
# (3 outer padding, 1 line, 2 inner padding) x 2
innercol = outercol - 10
fixed_rows = 6 # lines at top and bottom and padding
for i, widget in enumerate(self.stretchy.widgets):
if i == self.stretchy.stretchy_index:
@ -130,7 +136,8 @@ class StretchyOverlay(urwid.Widget):
def keypress(self, size, key):
top_size, scrollbar_visible = self._top_size(size, True)
self.listbox.base_widget._selectable = scrollbar_visible or self.stretchy.stretchy_w.selectable()
self.listbox._selectable = (
scrollbar_visible or self.stretchy.stretchy_w.selectable())
return self.top_w.keypress(top_size, key)
def render(self, size, focus):

View File

@ -130,6 +130,7 @@ class Padding:
"""
line_break = partialmethod(Text)
# This makes assumptions about the style names defined by both
# subiquity and console_conf. The fix is to stop using the Color class
# below, I think.
@ -159,6 +160,7 @@ STYLE_NAMES = set([
'progress_complete',
])
def apply_style_map(cls):
""" Applies AttrMap attributes to Color class
@ -198,10 +200,13 @@ def button_pile(buttons):
for button in buttons:
button = button.base_widget
if not isinstance(button, Button):
raise RuntimeError("button_pile takes a list of buttons, not %s", button)
raise RuntimeError("button_pile takes a list of buttons, not %s",
button)
max_label = max(len(button.label), max_label)
width = max_label + 4
return _Padding(Pile(buttons), min_width=width, width=width, align='center')
return _Padding(Pile(buttons), min_width=width,
width=width, align='center')
def screen(rows, buttons, focus_buttons=True, excerpt=None):
"""Helper to create a common screen layout.
@ -239,4 +244,3 @@ def screen(rows, buttons, focus_buttons=True, excerpt=None):
if focus_buttons:
pile.focus_position = len(excerpt_rows) + 3
return Padding.center_79(pile)

View File

@ -13,10 +13,21 @@
# 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 .network import NetworkView # NOQA
from .network_default_route import NetworkSetDefaultRouteView # NOQA
from .network_configure_interface import NetworkConfigureInterfaceView # NOQA
from .network_configure_manual_interface import NetworkConfigureIPv4InterfaceView, NetworkConfigureIPv6InterfaceView # NOQA
from .network_configure_wlan_interface import NetworkConfigureWLANView # NOQA
from .network_bond_interfaces import NetworkBondInterfacesView # NOQA
from .login import LoginView # NOQA
from .login import LoginView
from .network_bond_interfaces import NetworkBondInterfacesView
from .network_configure_interface import NetworkConfigureInterfaceView
from .network_configure_manual_interface import (
NetworkConfigureIPv4InterfaceView, NetworkConfigureIPv6InterfaceView)
from .network_configure_wlan_interface import NetworkConfigureWLANView
from .network_default_route import NetworkSetDefaultRouteView
from .network import NetworkView
__all__ = [
'LoginView',
'NetworkBondInterfacesView',
'NetworkConfigureInterfaceView',
'NetworkConfigureIPv4InterfaceView',
'NetworkConfigureIPv6InterfaceView',
'NetworkConfigureWLANView',
'NetworkSetDefaultRouteView',
'NetworkView',
]

View File

@ -22,7 +22,7 @@ import logging
from urwid import Text
from subiquitycore.ui.buttons import done_btn
from subiquitycore.ui.container import Pile, ListBox
from subiquitycore.ui.utils import Padding, Color
from subiquitycore.ui.utils import Padding
from subiquitycore.view import BaseView
log = logging.getLogger("subiquitycore.views.login")
@ -101,8 +101,10 @@ class LoginView(BaseView):
login_text = local_tpl.format(**login_info)
if user.ssh_import_id:
login_info.update({'auth': self.auth_name(user.ssh_import_id),
'ssh_import_id': user.ssh_import_id.split(":")[-1]})
login_info.update({
'auth': self.auth_name(user.ssh_import_id),
'ssh_import_id': user.ssh_import_id.split(":")[-1]
})
login_text += remote_tpl.format(**login_info)
ips = []

View File

@ -28,6 +28,7 @@ from urwid import (
Text,
WidgetWrap,
)
from urwid import Padding as uPadding
from subiquitycore.ui.buttons import back_btn, cancel_btn, done_btn, menu_btn
from subiquitycore.ui.container import Columns, ListBox, Pile
@ -44,8 +45,8 @@ class ApplyingConfigWidget(WidgetWrap):
self.cancel_func = cancel_func
button = cancel_btn(_("Cancel"), on_press=self.do_cancel)
self.bar = ProgressBar(normal='progress_incomplete',
complete='progress_complete',
current=0, done=step_count)
complete='progress_complete',
current=0, done=step_count)
box = LineBox(Pile([self.bar,
button_pile([button])]),
title=_("Applying network config"))
@ -57,16 +58,21 @@ class ApplyingConfigWidget(WidgetWrap):
def do_cancel(self, sender):
self.cancel_func()
def _build_wifi_info(dev):
r = []
if dev.actual_ssid is not None:
if dev.configured_ssid is not None:
if dev.actual_ssid != dev.configured_ssid:
r.append(Text(_("Associated to '%s', will associate to '%s'" % (dev.actual_ssid, dev.configured_ssid))))
r.append(
Text(_("Associated to '%s', will "
"associate to '%s'" % (dev.actual_ssid,
dev.configured_ssid))))
else:
r.append(Text(_("Associated to '%s'" % dev.actual_ssid)))
else:
r.append(Text(_("No access point configured, but associated to '%s'" % dev.actual_ssid)))
r.append(Text(_("No access point configured, but associated "
"to '%s'" % dev.actual_ssid)))
else:
if dev.configured_ssid is not None:
r.append(Text(_("Will associate to '%s'" % dev.configured_ssid)))
@ -74,16 +80,17 @@ def _build_wifi_info(dev):
r.append(Text(_("No access point configured")))
return r
def _format_address_list(label, addresses):
if len(addresses) == 0:
return []
elif len(addresses) == 1:
return [Text(label%('',)+' '+str(addresses[0]))]
return [Text(label % ('',) + ' ' + str(addresses[0]))]
else:
ips = []
for ip in addresses:
ips.append(str(ip))
return [Text(label%('es',) + ' ' + ', '.join(ips))]
return [Text(label % ('es',) + ' ' + ', '.join(ips))]
def _build_gateway_ip_info_for_version(dev, version):
@ -91,27 +98,37 @@ def _build_gateway_ip_info_for_version(dev, version):
configured_ip_addresses = dev.configured_ip_addresses_for_version(version)
if dev.dhcp_for_version(version):
if dev.actual_ip_addresses_for_version(version):
return _format_address_list(_("Will use DHCP for IPv%s, currently has address%%s:" % version), actual_ip_addresses)
return _format_address_list(_("Will use DHCP for IPv%s, currently "
"has address%%s:" % version),
actual_ip_addresses)
return [Text(_("Will use DHCP for IPv%s" % version))]
elif configured_ip_addresses:
if sorted(actual_ip_addresses) == sorted(configured_ip_addresses):
return _format_address_list(_("Using static address%%s for IPv%s:" % version), actual_ip_addresses)
p = _format_address_list(_("Will use static address%%s for IPv%s:" % version), configured_ip_addresses)
return _format_address_list(
_("Using static address%%s for IPv%s:" % version),
actual_ip_addresses)
p = _format_address_list(
_("Will use static address%%s for IPv%s:" % version),
configured_ip_addresses)
if actual_ip_addresses:
p.extend(_format_address_list(_("Currently has address%s:"), actual_ip_addresses))
p.extend(_format_address_list(_("Currently has address%s:"),
actual_ip_addresses))
return p
elif actual_ip_addresses:
return _format_address_list(_("Has no IPv%s configuration, currently has address%%s:" % version), actual_ip_addresses)
return _format_address_list(_("Has no IPv%s configuration, currently "
"has address%%s:" % version),
actual_ip_addresses)
else:
return [Text(_("IPv%s is not configured" % version))]
class NetworkView(BaseView):
title = _("Network connections")
excerpt = _("Configure at least one interface this server can use to talk to "
"other machines, and which preferably provides sufficient access for "
"updates.")
footer = _("Select an interface to configure it or select Done to continue")
excerpt = _("Configure at least one interface this server can use to talk "
"to other machines, and which preferably provides sufficient "
"access for updates.")
footer = _("Select an interface to configure it or select Done to "
"continue")
def __init__(self, model, controller):
self.model = model
@ -165,7 +182,8 @@ class NetworkView(BaseView):
if dev.type == 'wlan':
col_2.extend(_build_wifi_info(dev))
if len(dev.actual_ip_addresses) == 0 and dev.type == 'eth' and not dev.is_connected:
if len(dev.actual_ip_addresses) == 0 and (
dev.type == 'eth' and not dev.is_connected):
col_2.append(Color.info_primary(Text(_("Not connected"))))
col_2.extend(_build_gateway_ip_info_for_version(dev, 4))
col_2.extend(_build_gateway_ip_info_for_version(dev, 6))
@ -174,10 +192,10 @@ class NetworkView(BaseView):
template = ''
if dev.hwaddr:
template += '{} '.format(dev.hwaddr)
## TODO is this to translate?
# TODO is this to translate?
if dev.is_bond_slave:
template += '(Bonded) '
## TODO to check if this is affected by translations
# TODO to check if this is affected by translations
if not dev.vendor.lower().startswith('unknown'):
vendor = textwrap.wrap(dev.vendor, 15)[0]
template += '{} '.format(vendor)
@ -188,7 +206,8 @@ class NetworkView(BaseView):
template += '({})'.format(dev.speed)
col_2.append(Color.info_minor(Text(template)))
iface_menus.append(Columns([(ifname_width, Pile(col_1)), Pile(col_2)], 2))
iface_menus.append(
Columns([(ifname_width, Pile(col_1)), Pile(col_2)], 2))
return iface_menus
@ -197,8 +216,9 @@ class NetworkView(BaseView):
Padding.center_79(self.additional_options),
Padding.line_break(""),
]
self.listbox.base_widget.body[:] = widgets
self.additional_options.contents = [ (obj, ('pack', None)) for obj in self._build_additional_options() ]
self.listbox.body[:] = widgets
self.additional_options.contents = [
(obj, ('pack', None)) for obj in self._build_additional_options()]
def _build_additional_options(self):
labels = []
@ -243,8 +263,8 @@ class NetworkView(BaseView):
on_press=self.additional_menu_select,
user_data=sig))
from urwid import Padding
buttons = [ Padding(button, align='left', width=max_btn_len + 6) for button in buttons ]
buttons = [uPadding(button, align='left', width=max_btn_len + 6)
for button in buttons]
r = labels + buttons
if len(r) > 0:
r[0:0] = [Text("")]
@ -265,19 +285,20 @@ class NetworkView(BaseView):
]
if action == 'stop-networkd':
exc = info[0]
self.error.set_text("Stopping systemd-networkd-failed: %r" % (exc.stderr,))
self.error.set_text(
"Stopping systemd-networkd-failed: %r" % (exc.stderr,))
elif action == 'apply':
self.error.set_text("Network configuration could not be applied; " + \
self.error.set_text("Network configuration could not be applied; "
"please verify your settings.")
elif action == 'timeout':
self.error.set_text("Network configuration timed out; " + \
self.error.set_text("Network configuration timed out; "
"please verify your settings.")
elif action == 'down':
self.error.set_text("Downing network interfaces failed.")
elif action == 'canceled':
self.error.set_text("Network configuration canceled.")
else:
self.error.set_text("An unexpected error has occurred; " + \
self.error.set_text("An unexpected error has occurred; "
"please verify your settings.")
def done(self, result):

View File

@ -21,12 +21,14 @@ from subiquitycore.view import BaseView
from subiquitycore.ui.buttons import done_btn, menu_btn, _stylized_button
from subiquitycore.ui.container import ListBox, Pile
from subiquitycore.ui.utils import button_pile, Padding
from subiquitycore.ui.views.network import _build_gateway_ip_info_for_version, _build_wifi_info
from subiquitycore.ui.views.network import (
_build_gateway_ip_info_for_version, _build_wifi_info)
log = logging.getLogger('subiquitycore.network.network_configure_interface')
choice_btn = _stylized_button("", "", "menu")
class NetworkConfigureInterfaceView(BaseView):
def __init__(self, model, controller, name):
@ -107,16 +109,16 @@ class NetworkConfigureInterfaceView(BaseView):
buttons = [
menu_btn(label=" %s" % _("Use a static IPv4 configuration"),
on_press=self.show_ipv4_configuration),
on_press=self.show_ipv4_configuration),
choice_btn(label=" %s" % _("Use DHCPv4 on this interface"),
on_press=self.enable_dhcp4),
on_press=self.enable_dhcp4),
choice_btn(label=" %s" % _("Do not use"),
on_press=self.clear_ipv4),
on_press=self.clear_ipv4),
]
for btn in buttons:
btn.original_widget._label._cursor_position = 1
padding = getattr(Padding, 'left_{}'.format(button_padding))
buttons = [ padding(button) for button in buttons ]
buttons = [padding(button) for button in buttons]
return buttons
@ -125,24 +127,24 @@ class NetworkConfigureInterfaceView(BaseView):
buttons = [
menu_btn(label=" %s" % _("Use a static IPv6 configuration"),
on_press=self.show_ipv6_configuration),
on_press=self.show_ipv6_configuration),
choice_btn(label=" %s" % _("Use DHCPv6 on this interface"),
on_press=self.enable_dhcp6),
on_press=self.enable_dhcp6),
choice_btn(label=" %s" % _("Do not use"),
on_press=self.clear_ipv6),
on_press=self.clear_ipv6),
]
for btn in buttons:
btn.original_widget._label._cursor_position = 1
padding = getattr(Padding, 'left_{}'.format(button_padding))
buttons = [ padding(button) for button in buttons ]
buttons = [padding(button) for button in buttons]
return buttons
def _build_wifi_config(self):
btn = menu_btn(label=_("Configure WIFI settings"), on_press=self.show_wlan_configuration)
btn = menu_btn(label=_("Configure WIFI settings"),
on_press=self.show_wlan_configuration)
return [Padding.left_70(btn)]
def _build_buttons(self):
@ -159,9 +161,14 @@ class NetworkConfigureInterfaceView(BaseView):
self.controller.default()
return
if self.dev.type == 'wlan':
self.wifi_info.contents = [ (obj, ('pack', None)) for obj in _build_wifi_info(self.dev) ]
self.ipv4_info.contents = [ (obj, ('pack', None)) for obj in _build_gateway_ip_info_for_version(self.dev, 4) ]
self.ipv6_info.contents = [ (obj, ('pack', None)) for obj in _build_gateway_ip_info_for_version(self.dev, 6) ]
self.wifi_info.contents = [
(obj, ('pack', None)) for obj in _build_wifi_info(self.dev)]
self.ipv4_info.contents = [
(obj, ('pack', None))
for obj in _build_gateway_ip_info_for_version(self.dev, 4)]
self.ipv6_info.contents = [
(obj, ('pack', None))
for obj in _build_gateway_ip_info_for_version(self.dev, 6)]
def clear_ipv4(self, btn):
self.dev.remove_ip_networks_for_version(4)

View File

@ -24,7 +24,8 @@ from subiquitycore.ui.interactive import RestrictedEditor, StringEditor
from subiquitycore.ui.form import Form, FormField, StringField
log = logging.getLogger('subiquitycore.network.network_configure_ipv4_interface')
log = logging.getLogger(
'subiquitycore.network.network_configure_ipv4_interface')
ip_families = {
4: {
@ -42,6 +43,7 @@ class IPField(FormField):
def __init__(self, *args, **kw):
self.has_mask = kw.pop('has_mask', False)
super().__init__(*args, **kw)
def _make_widget(self, form):
if form.ip_version == 6:
return StringEditor()
@ -67,8 +69,10 @@ class NetworkConfigForm(Form):
subnet = IPField(_("Subnet:"), has_mask=True)
address = IPField(_("Address:"))
gateway = IPField(_("Gateway:"))
nameservers = StringField(_("Name servers:"), help=_("IP addresses, comma separated"))
searchdomains = StringField(_("Search domains:"), help=_("Domains, comma separated"))
nameservers = StringField(_("Name servers:"),
help=_("IP addresses, comma separated"))
searchdomains = StringField(_("Search domains:"),
help=_("Domains, comma separated"))
def clean_subnet(self, subnet):
log.debug("clean_subnet %r", subnet)
@ -83,7 +87,8 @@ class NetworkConfigForm(Form):
except ValueError:
return
if address not in subnet:
raise ValueError(_("'%s' is not contained in '%s'") % (address, subnet))
raise ValueError(
_("'%s' is not contained in '%s'") % (address, subnet))
return address
def clean_gateway(self, gateway):
@ -114,23 +119,28 @@ class BaseNetworkConfigureManualView(BaseView):
self.model = model
self.controller = controller
self.dev = self.model.get_netdev_by_name(name)
self.title = _("Network interface {} manual IPv{} configuration").format(name, self.ip_version)
self.title = _("Network interface {} manual IPv{} "
"configuration").format(name, self.ip_version)
self.is_gateway = False
self.form = NetworkConfigForm(self.ip_version)
connect_signal(self.form, 'submit', self.done)
connect_signal(self.form, 'cancel', self.cancel)
self.form.subnet.help = _("Example: %s"%(self.example_address,))
configured_addresses = self.dev.configured_ip_addresses_for_version(self.ip_version)
self.form.subnet.help = _("Example: %s" % (self.example_address,))
configured_addresses = (
self.dev.configured_ip_addresses_for_version(self.ip_version))
if configured_addresses:
addr = ipaddress.ip_interface(configured_addresses[0])
self.form.subnet.value = str(addr.network)
self.form.address.value = str(addr.ip)
configured_gateway = self.dev.configured_gateway_for_version(self.ip_version)
configured_gateway = (
self.dev.configured_gateway_for_version(self.ip_version))
if configured_gateway:
self.form.gateway.value = configured_gateway
self.form.nameservers.value = ', '.join(self.dev.configured_nameservers)
self.form.searchdomains.value = ', '.join(self.dev.configured_searchdomains)
self.form.nameservers.value = (
', '.join(self.dev.configured_nameservers))
self.form.searchdomains.value = (
', '.join(self.dev.configured_searchdomains))
self.error = Text("", align='center')
super().__init__(self.form.as_screen(focus_buttons=False))
@ -162,15 +172,16 @@ class BaseNetworkConfigureManualView(BaseView):
self.model.set_default_v4_gateway(self.dev.name,
self.gateway_input.value)
self.is_gateway = True
self.set_as_default_gw_button.contents = \
[ (obj, ('pack', None)) \
for obj in self._build_set_as_default_gw_button() ]
self.set_as_default_gw_button.contents = [
(obj, ('pack', None))
for obj in self._build_set_as_default_gw_button()]
except ValueError:
# FIXME: set error message UX ala identity
pass
def done(self, sender):
# XXX this converting from and to and from strings thing is a bit out of hand.
# XXX this converting from and to and from strings thing is a
# bit out of hand.
gateway = self.form.gateway.value
if gateway is not None:
gateway = str(gateway)
@ -192,6 +203,7 @@ class BaseNetworkConfigureManualView(BaseView):
self.model.default_gateway = None
self.controller.network_configure_interface(self.dev.name)
class NetworkConfigureIPv4InterfaceView(BaseNetworkConfigureManualView):
ip_version = 4
example_address = '192.168.9.0/24'

View File

@ -1,6 +1,5 @@
from urwid import (
BoxAdapter,
Button,
connect_signal,
LineBox,
Text,
@ -13,15 +12,19 @@ from subiquitycore.ui.form import Form, PasswordField, StringField
from subiquitycore.ui.utils import Color, Padding
import logging
log = logging.getLogger('subiquitycore.network.network_configure_wlan_interface')
log = logging.getLogger(
'subiquitycore.network.network_configure_wlan_interface')
class NetworkList(WidgetWrap):
def __init__(self, parent, ssids):
self.parent = parent
button = cancel_btn(_("Cancel"), on_press=self.do_cancel)
ssid_list = [menu_btn(label=ssid, on_press=self.do_network) for ssid in ssids]
p = Pile([BoxAdapter(ListBox(ssid_list), height=10), Padding.fixed_10(button)])
ssid_list = [menu_btn(label=ssid, on_press=self.do_network)
for ssid in ssids]
p = Pile([BoxAdapter(ListBox(ssid_list), height=10),
Padding.fixed_10(button)])
box = LineBox(p, title="Select a network")
super().__init__(box)
@ -49,6 +52,7 @@ class WLANForm(Form):
elif len(psk) > 63:
return "Password must be less than 63 characters long"
class NetworkConfigureWLANView(BaseView):
def __init__(self, model, controller, name):
self.model = model
@ -89,7 +93,7 @@ class NetworkConfigureWLANView(BaseView):
self.show_overlay(NetworkList(self, self.dev.actual_ssids))
def start_scan(self, sender):
self.keypress((0,0), 'up')
self.keypress((0, 0), 'up')
try:
self.controller.start_scan(self.dev)
except RuntimeError as r:
@ -98,7 +102,8 @@ class NetworkConfigureWLANView(BaseView):
def _build_iface_inputs(self):
if len(self.dev.actual_ssids) > 0:
networks_btn = menu_btn("Choose a visible network", on_press=self.show_ssid_list)
networks_btn = menu_btn("Choose a visible network",
on_press=self.show_ssid_list)
else:
networks_btn = Color.info_minor(Columns(
[
@ -117,8 +122,10 @@ class NetworkConfigureWLANView(BaseView):
('fixed', 1, Text(">"))
], dividechars=1))
warning = (
"Only open or WPA2/PSK networks are supported at this time.")
col = [
Padding.center_79(Color.info_minor(Text("Only open or WPA2/PSK networks are supported at this time."))),
Padding.center_79(Color.info_minor(Text(warning))),
Padding.line_break(""),
self.ssid_row,
Padding.fixed_30(networks_btn),
@ -134,11 +141,13 @@ class NetworkConfigureWLANView(BaseView):
# The interface is gone
self.controller.default()
return
self.inputs.contents = [ (obj, ('pack', None)) for obj in self._build_iface_inputs() ]
self.inputs.contents = [(obj, ('pack', None))
for obj in self._build_iface_inputs()]
def done(self, sender):
if self.dev.configured_ssid is None and self.form.ssid.value:
# Turn DHCP4 on by default when specifying an SSID for the first time...
# Turn DHCP4 on by default when specifying an SSID for
# the first time...
self.dev.dhcp4 = True
if self.form.ssid.value:
ssid = self.form.ssid.value

View File

@ -91,7 +91,7 @@ class NetworkSetDefaultRouteView(BaseView):
items.append(Padding.center_79(
menu_btn(label="Specify the default route manually",
on_press=self.show_edit_default_route)))
on_press=self.show_edit_default_route)))
return items
def _build_buttons(self):

View File

@ -5,7 +5,8 @@ from unittest import mock
from subiquitycore.controllers.network import NetworkController
from subiquitycore.models.network import Networkdev, NetworkModel
from subiquitycore.testing import view_helpers
from subiquitycore.ui.views.network_configure_manual_interface import NetworkConfigureIPv4InterfaceView
from subiquitycore.ui.views.network_configure_manual_interface import (
NetworkConfigureIPv4InterfaceView)
valid_data = {
@ -16,19 +17,22 @@ valid_data = {
'searchdomains': '.custom',
}
class TestNetworkConfigureIPv4InterfaceView(unittest.TestCase):
def make_view(self):
model = mock.create_autospec(spec=NetworkModel)
controller = mock.create_autospec(spec=NetworkController)
ifname = 'ifname'
def get_netdev_by_name(name):
if name == ifname:
dev = mock.create_autospec(spec=Networkdev)
dev.configured_ip_addresses_for_version = lambda v:[]
dev.configured_ip_addresses_for_version = lambda v: []
return dev
else:
raise AssertionError("get_netdev_by_name called with unexpected arg %s"%(name,))
raise AssertionError("get_netdev_by_name called with "
"unexpected arg %s" % (name,))
model.get_netdev_by_name.side_effect = get_netdev_by_name
return NetworkConfigureIPv4InterfaceView(model, controller, ifname)

View File

@ -14,7 +14,6 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import crypt
import errno
import logging
import os
import random
@ -69,7 +68,8 @@ def environment_check(check):
for i in items:
if not os.path.exists(i):
if 'SNAP' in os.environ:
log.warn("Adjusting path for snaps: {}".format(os.environ.get('SNAP')))
log.warn("Adjusting path for snaps: %s",
os.environ.get('SNAP'))
i = os.environ.get('SNAP') + i
if not os.path.exists(i):
env_ok = False
@ -103,7 +103,9 @@ def _clean_env(env):
return env
def run_command(cmd, *, input=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8', errors='replace', env=None, **kw):
def run_command(cmd, *, input=None, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, encoding='utf-8', errors='replace',
env=None, **kw):
"""A wrapper around subprocess.run with logging and different defaults.
We never ever want a subprocess to inherit our file descriptors!
@ -114,7 +116,8 @@ def run_command(cmd, *, input=None, stdout=subprocess.PIPE, stderr=subprocess.PI
input = input.encode(encoding)
log.debug("run_command called: %s", cmd)
try:
cp = subprocess.run(cmd, input=input, stdout=stdout, stderr=stderr, env=_clean_env(env), **kw)
cp = subprocess.run(cmd, input=input, stdout=stdout, stderr=stderr,
env=_clean_env(env), **kw)
if encoding:
if isinstance(cp.stdout, bytes):
cp.stdout = cp.stdout.decode(encoding)
@ -128,13 +131,16 @@ def run_command(cmd, *, input=None, stdout=subprocess.PIPE, stderr=subprocess.PI
return cp
def start_command(cmd, *, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8', errors='replace', env=None, **kw):
def start_command(cmd, *, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, encoding='utf-8', errors='replace',
env=None, **kw):
"""A wrapper around subprocess.Popen with logging and different defaults.
We never ever want a subprocess to inherit our file descriptors!
"""
log.debug('start_command called: %s', cmd)
return subprocess.Popen(cmd, stdin=stdin, stdout=stdout, stderr=stderr, env=_clean_env(env), **kw)
return subprocess.Popen(cmd, stdin=stdin, stdout=stdout, stderr=stderr,
env=_clean_env(env), **kw)
# FIXME: replace with passlib and update package deps
@ -156,14 +162,18 @@ def crypt_password(passwd, algo='SHA-512'):
def disable_console_conf():
""" Stop console-conf service; which also restores getty service """
log.info('disabling console-conf service')
run_command(["systemctl", "stop", "--no-block", "console-conf@*.service", "serial-console-conf@*.service"])
run_command(["systemctl", "stop", "--no-block", "console-conf@*.service",
"serial-console-conf@*.service"])
return
def disable_subiquity():
""" Stop subiquity service; which also restores getty service """
log.info('disabling subiquity service')
run_command(["mkdir", "-p", "/run/subiquity"])
run_command(["touch", "/run/subiquity/complete"])
run_command(["systemctl", "start", "--no-block", "getty@tty1.service"])
run_command(["systemctl", "stop", "--no-block", "snap.subiquity.subiquity-service.service", "serial-subiquity@*.service"])
run_command(["systemctl", "stop", "--no-block",
"snap.subiquity.subiquity-service.service",
"serial-subiquity@*.service"])
return

View File

@ -36,7 +36,8 @@ class BaseView(WidgetWrap):
height='pack'
)
PADDING = 3
# Don't expect callers to account for the padding if they pass a fixed width.
# Don't expect callers to account for the padding if
# they pass a fixed width.
if 'width' in kw:
if isinstance(kw['width'], int):
kw['width'] += 2*PADDING