merge master
This commit is contained in:
commit
293793fa49
19
Makefile
19
Makefile
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -15,4 +15,7 @@
|
|||
|
||||
""" Console-Conf """
|
||||
|
||||
import subiquitycore.i18n
|
||||
from subiquitycore import i18n
|
||||
__all__ = [
|
||||
'i18n',
|
||||
]
|
||||
|
|
|
@ -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',
|
||||
]
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
from subiquitycore.models.network import NetworkModel
|
||||
from subiquitycore.models.identity import IdentityModel
|
||||
|
||||
|
||||
class ConsoleConfModel:
|
||||
"""The overall model for console-conf."""
|
||||
|
||||
|
|
|
@ -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',
|
||||
]
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -49,7 +49,6 @@ class LoginView(BaseView):
|
|||
])),
|
||||
]))
|
||||
|
||||
|
||||
def _build_buttons(self):
|
||||
return [
|
||||
done_btn("Done", on_press=self.done),
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -15,6 +15,9 @@
|
|||
|
||||
""" Subiquity """
|
||||
|
||||
__version__ = "0.0.5"
|
||||
from subiquitycore import i18n
|
||||
__all__ = [
|
||||
'i18n',
|
||||
]
|
||||
|
||||
import subiquitycore.i18n
|
||||
__version__ = "0.0.5"
|
||||
|
|
|
@ -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',
|
||||
]
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ from subiquity.ui.views import KeyboardView
|
|||
|
||||
log = logging.getLogger('subiquity.controllers.keyboard')
|
||||
|
||||
|
||||
class KeyboardController(BaseController):
|
||||
|
||||
signals = [
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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, '')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
"""
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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',
|
||||
]
|
||||
|
|
|
@ -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',
|
||||
]
|
||||
|
|
|
@ -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)])
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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("")),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()))
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -17,6 +17,7 @@ valid_data = {
|
|||
'confirm_password': 'password'
|
||||
}
|
||||
|
||||
|
||||
class IdentityViewTests(unittest.TestCase):
|
||||
|
||||
def make_view(self):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -15,6 +15,9 @@
|
|||
|
||||
""" SubiquityCore """
|
||||
|
||||
__version__ = "0.0.5"
|
||||
from subiquitycore import i18n
|
||||
__all__ = [
|
||||
'i18n',
|
||||
]
|
||||
|
||||
import subiquitycore.i18n
|
||||
__version__ = "0.0.5"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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("")]))
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
|
@ -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}
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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',
|
||||
]
|
||||
|
|
|
@ -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 = []
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue