Refactoring of several parts of subiquity

Models/
  Inherit a model policy that outlines the minimum requirements.
  Things like exposed signals, menuing structure, and previous
  controller are expected to be there.

Controllers/
  Moved controller logic in a core controller which eases
  the signal emitter to view mappings.

Signals/
  Navigation and views are handled by a global Signal
  class which manages all emitted signals and their
  connection callbacks.

  This also removes the need for having to define
  callbacks in all of the view classes.

UI/
  Made a dummy view availble for those that have
  yet to be implemented.

Signed-off-by: Adam Stokes <adam.stokes@ubuntu.com>
This commit is contained in:
Adam Stokes 2015-07-21 21:50:36 -04:00
commit 02cab5a223
28 changed files with 987 additions and 1107 deletions

View File

@ -19,7 +19,7 @@ import sys
import logging
from subiquity.log import setup_logger
from subiquity import __version__ as VERSION
from subiquity.controllers import BaseController as Subiquity
from subiquity.core import Controller as Subiquity
from subiquity.ui.frame import SubiquityUI

View File

@ -1,113 +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/>.
import logging
import urwid
import urwid.curses_display
from subiquity.routes import Routes
from subiquity.palette import STYLES, STYLES_MONO
log = logging.getLogger('subiquity.controller')
class BaseControllerError(Exception):
""" Basecontroller exception """
pass
class BaseController:
def __init__(self, ui, opts):
self.ui = ui
self.opts = opts
def next_controller(self, *args, **kwds):
controller = Routes.next()
controller(self).show(*args, **kwds)
def prev_controller(self, *args, **kwds):
controller = Routes.prev()
controller(self).show(*args, **kwds)
def current_controller(self, *args, **kwds):
controller = Routes.current()
return controller(self)
def redraw_screen(self):
if hasattr(self, 'loop'):
try:
self.loop.draw_screen()
except AssertionError as e:
log.critical("Redraw screen error: {}".format(e))
def set_alarm_in(self, interval, cb):
self.loop.set_alarm_in(interval, cb)
return
def update(self, *args, **kwds):
""" Update loop """
pass
def exit(self):
raise urwid.ExitMainLoop()
def header_hotkeys(self, key):
if key in ['esc'] and Routes.current_idx() != 0:
self.prev_controller()
if key in ['q', 'Q', 'ctrl c']:
self.exit()
def set_body(self, w):
self.ui.set_body(w)
self.redraw_screen()
def set_header(self, title=None, excerpt=None):
self.ui.set_header(title, excerpt)
self.redraw_screen()
def set_footer(self, message):
self.ui.set_footer(message)
self.redraw_screen()
def run(self):
if not hasattr(self, 'loop'):
palette = STYLES
additional_opts = {
'screen': urwid.raw_display.Screen(),
'unhandled_input': self.header_hotkeys,
'handle_mouse': False
}
if self.opts.run_on_serial:
palette = STYLES_MONO
additional_opts['screen'] = urwid.curses_display.Screen()
else:
additional_opts['screen'].set_terminal_properties(colors=256)
additional_opts['screen'].reset_default_terminal_palette()
self.loop = urwid.MainLoop(
self.ui, palette, **additional_opts)
try:
self.set_alarm_in(0.05, self.begin)
self.loop.run()
except:
log.exception("Exception in controller.run():")
raise
def begin(self, *args, **kwargs):
""" Initializes the first controller for installation """
Routes.reset()
initial_controller = Routes.first()
initial_controller(self).show()

View File

@ -1,120 +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 subiquity.views.filesystem import (FilesystemView,
DiskPartitionView,
AddPartitionView)
from subiquity.models.filesystem import FilesystemModel
from subiquity.curtin import curtin_write_storage_actions
from urwid import connect_signal
import logging
import subprocess
log = logging.getLogger('subiquity.filesystemController')
class FilesystemController:
""" Filesystem Controller """
fs_model = FilesystemModel()
def __init__(self, ui):
self.ui = ui
# Filesystem actions
def show(self, *args, **kwds):
title = "Filesystem setup"
footer = ("Select available disks to format and mount")
fs_view = FilesystemView(self.fs_model)
connect_signal(fs_view, 'fs:done', self.finish)
connect_signal(fs_view, 'fs:dp:view', self.show_disk_partition_view)
self.ui.set_header(title)
self.ui.set_footer(footer)
self.ui.set_body(fs_view)
return
def finish(self, reset=False, actions=None):
"""
:param bool reset: Reset model options
:param actions: storage actions
Signal:
key: 'fs:done'
usage: emit_signal(self, 'fs:done', (reset, actions))
"""
if actions is None and reset is False:
return self.ui.prev_controller()
log.info("Rendering curtin config from user choices")
curtin_write_storage_actions(actions=actions)
if self.ui.opts.dry_run:
log.debug("filesystem: this is a dry-run")
print("\033c")
print("**** DRY_RUN ****")
print('NOT calling: '
'subprocess.check_call("/usr/local/bin/curtin_wrap.sh")')
print("**** DRY_RUN ****")
else:
log.debug("filesystem: this is the *real* thing")
print("\033c")
print("**** Calling curtin installer ****")
subprocess.check_call("/usr/local/bin/curtin_wrap.sh")
return self.ui.exit()
# DISK Partitioning actions
def show_disk_partition_view(self, disk):
log.debug("In disk partition view, using {} as the disk.".format(disk))
title = ("Paritition, format, and mount {}".format(disk))
footer = ("Paritition the disk, or format the entire device "
"without partitions.")
self.ui.set_header(title)
self.ui.set_footer(footer)
dp_view = DiskPartitionView(self.fs_model,
disk)
connect_signal(dp_view, 'fs:dp:done', self.finish_disk_paritition_view)
connect_signal(dp_view, 'fs:show-add-partition',
self.show_add_disk_partition_view)
self.ui.set_body(dp_view)
return
def finish_disk_paritition_view(self, result):
log.debug("Finish disk-p-v: {}".format(result))
return self.ui.exit()
# ADD Partitioning actions
def show_add_disk_partition_view(self, disk):
adp_view = AddPartitionView(self.fs_model,
disk)
connect_signal(adp_view,
'fs:add-partition:done',
self.finish_add_disk_paritition_view)
self.ui.set_body(adp_view)
return
def finish_add_disk_partition_view(self, partition_spec):
if not partition_spec:
log.debug("New partition: {}".format(partition_spec))
else:
log.debug("Empty partition spec, should go back one.")
return self.ui.exit()
__controller_class__ = FilesystemController

View File

@ -1,49 +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 subiquity.controllers.policy import ControllerPolicy
from subiquity.views.installpath import InstallpathView
from subiquity.models.installpath import InstallpathModel
import logging
log = logging.getLogger('subiquity.installpath')
class InstallpathController(ControllerPolicy):
"""InstallpathController"""
title = "15.10"
excerpt = ("Welcome to Ubuntu! The world's favourite platform "
"for clouds, clusters and amazing internet things. "
"This is the installer for Ubuntu on servers and "
"internet devices.")
footer = ("Use UP, DOWN arrow keys, and ENTER, to "
"navigate options")
def show(self, *args, **kwds):
log.debug("Loading install path controller")
self.ui.set_header(self.title, self.excerpt)
self.ui.set_footer(self.footer)
model = InstallpathModel()
self.ui.set_body(InstallpathView(model, self.finish))
return
def finish(self, install_selection=None):
log.debug("installpath cb selection: {}".format(install_selection))
if install_selection is None:
return self.ui.prev_controller()
return self.ui.next_controller()
__controller_class__ = InstallpathController

View File

@ -1,47 +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 subiquity.controllers.policy import ControllerPolicy
from subiquity.views.network import NetworkView
from subiquity.models.network import NetworkModel
import logging
log = logging.getLogger('subiquity.network')
class NetworkController(ControllerPolicy):
"""InstallpathController"""
title = "Network connections"
excerpt = ("Configure at least the main interface this server will "
"use to talk to other machines, and preferably provide "
"sufficient access for updates.")
footer = ("Additional networking info here")
def show(self, *args, **kwds):
self.model = NetworkModel()
self.ui.set_header(self.title, self.excerpt)
self.ui.set_footer(self.footer)
self.ui.set_body(NetworkView(self.model, self.finish))
return
def finish(self, interface=None):
if interface is None:
return self.ui.prev_controller()
log.info("Network Interface choosen: {}".format(interface))
return self.ui.next_controller()
__controller_class__ = NetworkController

View File

@ -1,45 +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/>.
""" Controller policy """
from abc import ABCMeta, abstractmethod
class ControllerPolicy(metaclass=ABCMeta):
""" Policy class for controller specifics
"""
signals = ('done', 'cancel', 'reset')
def __init__(self, ui):
self.ui = ui
@abstractmethod
def show(self, *args, **kwds):
""" Implements show action for the controller
This is the entrypoint for all initial controller
views.
"""
pass
@abstractmethod
def finish(self):
""" Implements finish action for controller.
This handles any callback data/procedures required
to move to the next controller or end the install.
"""
pass

View File

@ -1,46 +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 subiquity.controllers.policy import ControllerPolicy
from subiquity.views.welcome import WelcomeView
from subiquity.models.welcome import WelcomeModel
import logging
log = logging.getLogger('subiquity.controllers.welcome')
class WelcomeController(ControllerPolicy):
"""WelcomeController"""
title = "Wilkommen! Bienvenue! Welcome! Zdrastvutie! Welkom!"
excerpt = "Please choose your preferred language"
footer = ("Use UP, DOWN arrow keys, and ENTER, to "
"select your language.")
def show(self, *args, **kwds):
self.ui.set_header(self.title, self.excerpt)
self.ui.set_footer(self.footer)
self.ui.set_body(WelcomeView(WelcomeModel, self.finish))
return
def finish(self, language=None):
if language is None:
raise SystemExit("No language selected, exiting as there are no "
"more previous controllers to render.")
WelcomeModel.selected_language = language
log.debug("Welcome Model: {}".format(WelcomeModel()))
return self.ui.next_controller()
__controller_class__ = WelcomeController

288
subiquity/core.py Normal file
View File

@ -0,0 +1,288 @@
# Copyright 2015 Canonical, Ltd.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
import urwid
import urwid.curses_display
import subprocess
from subiquity.signals import Signal
from subiquity.palette import STYLES, STYLES_MONO
from subiquity.curtin import curtin_write_storage_actions
# Modes import ----------------------------------------------------------------
from subiquity.welcome import WelcomeView, WelcomeModel
from subiquity.network import NetworkView, NetworkModel
from subiquity.installpath import InstallpathView, InstallpathModel
from subiquity.filesystem import (FilesystemView,
DiskPartitionView,
AddPartitionView,
FilesystemModel)
from subiquity.ui.dummy import DummyView
log = logging.getLogger('subiquity.core')
class CoreControllerError(Exception):
""" Basecontroller exception """
pass
class Controller:
def __init__(self, ui, opts):
self.ui = ui
self.opts = opts
self.models = {
"welcome": WelcomeModel(),
"network": NetworkModel(),
"installpath": InstallpathModel(),
"filesystem": FilesystemModel()
}
self.signal = Signal()
# self.signal.register_signals()
self._connect_signals()
def _connect_signals(self):
""" Connect signals used in the core controller
"""
signals = []
# Pull signals emitted from welcome path selections
for name, sig, cb in self.models["welcome"].get_signals():
signals.append((sig, getattr(self, cb)))
# Pull signals emitted from install path selections
for name, sig, cb in self.models["installpath"].get_signals():
signals.append((sig, getattr(self, cb)))
# Pull signals emitted from network selections
for name, sig, cb in self.models["network"].get_signals():
signals.append((sig, getattr(self, cb)))
# Pull signals emitted from filesystem selections
for name, sig, cb in self.models["filesystem"].get_signals():
signals.append((sig, getattr(self, cb)))
self.signal.connect_signals(signals)
log.debug(self.signal)
# EventLoop -------------------------------------------------------------------
def redraw_screen(self):
if hasattr(self, 'loop'):
try:
self.loop.draw_screen()
except AssertionError as e:
log.critical("Redraw screen error: {}".format(e))
def set_alarm_in(self, interval, cb):
self.loop.set_alarm_in(interval, cb)
return
def update(self, *args, **kwds):
""" Update loop """
pass
def exit(self):
raise urwid.ExitMainLoop()
def header_hotkeys(self, key):
if key in ['q', 'Q', 'ctrl c']:
self.exit()
def run(self):
if not hasattr(self, 'loop'):
palette = STYLES
additional_opts = {
'screen': urwid.raw_display.Screen(),
'unhandled_input': self.header_hotkeys,
'handle_mouse': False
}
if self.opts.run_on_serial:
palette = STYLES_MONO
additional_opts['screen'] = urwid.curses_display.Screen()
else:
additional_opts['screen'].set_terminal_properties(colors=256)
additional_opts['screen'].reset_default_terminal_palette()
self.loop = urwid.MainLoop(
self.ui, palette, **additional_opts)
try:
self.set_alarm_in(0.05, self.welcome)
self.loop.run()
except:
log.exception("Exception in controller.run():")
raise
# Base UI Actions -------------------------------------------------------------
def set_body(self, w):
self.ui.set_body(w)
self.redraw_screen()
def set_header(self, title=None, excerpt=None):
self.ui.set_header(title, excerpt)
self.redraw_screen()
def set_footer(self, message):
self.ui.set_footer(message)
self.redraw_screen()
# Modes ----------------------------------------------------------------------
# Welcome -----------------------------------------------------------------
def welcome(self, *args, **kwargs):
title = "Wilkommen! Bienvenue! Welcome! Zdrastvutie! Welkom!"
excerpt = "Please choose your preferred language"
footer = ("Use UP, DOWN arrow keys, and ENTER, to "
"select your language.")
self.ui.set_header(title, excerpt)
self.ui.set_footer(footer)
view = WelcomeView(self.models['welcome'], self.signal)
self.ui.set_body(view)
# InstallPath -------------------------------------------------------------
def installpath(self):
title = "15.10"
excerpt = ("Welcome to Ubuntu! The world's favourite platform "
"for clouds, clusters and amazing internet things. "
"This is the installer for Ubuntu on servers and "
"internet devices.")
footer = ("Use UP, DOWN arrow keys, and ENTER, to "
"navigate options")
self.ui.set_header(title, excerpt)
self.ui.set_footer(footer)
self.ui.set_body(InstallpathView(self.models["installpath"],
self.signal))
def install_ubuntu(self):
log.debug("Installing Ubuntu path chosen.")
self.signal.emit_signal('network:show')
def install_maas_region_server(self):
self.ui.set_body(DummyView(self.signal))
def install_maas_cluster_server(self):
self.ui.set_body(DummyView(self.signal))
def test_media(self):
self.ui.set_body(DummyView(self.signal))
def test_memory(self):
self.ui.set_body(DummyView(self.signal))
# Network -----------------------------------------------------------------
def network(self):
title = "Network connections"
excerpt = ("Configure at least the main interface this server will "
"use to talk to other machines, and preferably provide "
"sufficient access for updates.")
footer = ("Additional networking info here")
self.ui.set_header(title, excerpt)
self.ui.set_footer(footer)
self.ui.set_body(NetworkView(self.models["network"], self.signal))
def set_default_route(self):
self.ui.set_body(DummyView(self.signal))
def bond_interfaces(self):
self.ui.set_body(DummyView(self.signal))
def install_network_driver(self):
self.ui.set_body(DummyView(self.signal))
# Filesystem --------------------------------------------------------------
def filesystem(self):
title = "Filesystem setup"
footer = ("Select available disks to format and mount")
self.ui.set_header(title)
self.ui.set_footer(footer)
self.ui.set_body(FilesystemView(self.models["filesystem"],
self.signal))
def filesystem_handler(self, reset=False, actions=None):
if actions is None and reset is False:
urwid.emit_signal(self.signal, 'network:show', [])
log.info("Rendering curtin config from user choices")
curtin_write_storage_actions(actions=actions)
if self.opts.dry_run:
log.debug("filesystem: this is a dry-run")
print("\033c")
print("**** DRY_RUN ****")
print('NOT calling: '
'subprocess.check_call("/usr/local/bin/curtin_wrap.sh")')
print("**** DRY_RUN ****")
else:
log.debug("filesystem: this is the *real* thing")
print("\033c")
print("**** Calling curtin installer ****")
subprocess.check_call("/usr/local/bin/curtin_wrap.sh")
return self.ui.exit()
# Filesystem/Disk partition -----------------------------------------------
def disk_partition(self, disk):
log.debug("In disk partition view, using {} as the disk.".format(disk))
title = ("Paritition, format, and mount {}".format(disk))
footer = ("Paritition the disk, or format the entire device "
"without partitions.")
self.ui.set_header(title)
self.ui.set_footer(footer)
dp_view = DiskPartitionView(self.models["filesystem"],
self.signal,
disk)
self.ui.set_body(dp_view)
def disk_partition_handler(self, spec=None):
log.debug("Disk partition: {}".format(spec))
if spec is None:
urwid.emit_signal(self.signal, 'filesystem:show', [])
urwid.emit_signal(self.signal, 'filesystem:show-disk-partition', [])
def add_disk_partition(self, disk):
adp_view = AddPartitionView(self.models["filesystem"],
self.signal,
disk)
self.ui.set_body(adp_view)
def add_disk_partition_handler(self, partition_spec):
self.exit()
if not partition_spec:
log.debug("New partition: {}".format(partition_spec))
else:
log.debug("Empty partition spec, should go back one.")
def connect_iscsi_disk(self, *args, **kwargs):
self.ui.set_body(DummyView(self.signal))
def connect_ceph_disk(self, *args, **kwargs):
self.ui.set_body(DummyView(self.signal))
def create_volume_group(self, *args, **kwargs):
self.ui.set_body(DummyView(self.signal))
def create_raid(self, *args, **kwargs):
self.ui.set_body(DummyView(self.signal))
def setup_bcache(self, *args, **kwargs):
self.ui.set_body(DummyView(self.signal))
def add_first_gpt_partition(self, *args, **kwargs):
self.ui.set_body(DummyView(self.signal))
def create_swap_entire_device(self, *args, **kwargs):
self.ui.set_body(DummyView(self.signal))

View File

@ -13,17 +13,165 @@
# 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/>.
""" Filesystem
Provides storage device selection and additional storage
configuration.
"""
import logging
import json
import argparse
from subiquity.filesystem.blockdev import Blockdev
from probert import prober
from probert.storage import StorageInfo
import math
from urwid import (WidgetWrap, ListBox, Pile, BoxAdapter,
Text, Columns, LineBox, Edit, RadioButton)
from subiquity.ui.lists import SimpleList
from subiquity.ui.buttons import done_btn, reset_btn, cancel_btn
from subiquity.ui.utils import Padding, Color
from subiquity.signals import emit_signal
log = logging.getLogger('subiquity.filesystem')
log = logging.getLogger('subiquity.filesystemView')
class FilesystemModel:
""" Model representing storage options
"""
prev_signal = (
'Back to network path',
'network:show',
'network'
)
signals = [
('Filesystem view',
'filesystem:show',
'filesystem'),
('Filesystem finish',
'filesystem:finish',
'filesystem_handler'),
('Show disk partition view',
'filesystem:show-disk-partition',
'disk_partition'),
('Finish disk partition',
'filesystem:finish-disk-partition',
'disk_partition_handler'),
('Add disk partition',
'filesystem:add-disk-partition',
'add_disk_partition'),
('Finish add disk partition',
'filesystem:finish-add-disk-partition',
'add_disk_partition_handler')
]
fs_menu = [
('Connect iSCSI network disk',
'filesystem:connect-iscsi-disk',
'connect_iscsi_disk'),
('Connect Ceph network disk',
'filesystem:connect-ceph-disk',
'connect_ceph_disk'),
('Create volume group (LVM2)',
'filesystem:create-volume-group',
'create_volume_group'),
('Create software RAID (MD)',
'filesystem:create-raid',
'create_raid'),
('Setup hierarchichal storage (bcache)',
'filesystem:setup-bcache',
'setup_bcache')
]
partition_menu = [
('Add first GPT partition',
'filesystem:add-first-gpt-partition',
'add_first_gpt_partition'),
('Format or create swap on entire device (unusual, advanced)',
'filesystem:create-swap-entire-device',
'create_swap_entire_device')
]
supported_filesystems = [
'ext4',
'xfs',
'btrfs',
'swap',
'bcache cache',
'bcache store',
'leave unformatted'
]
def __init__(self):
self.storage = {}
self.info = {}
self.devices = {}
self.options = argparse.Namespace(probe_storage=True,
probe_network=False)
self.prober = prober.Prober(self.options)
self.probe_storage()
def get_signal_by_name(self, selection):
for x, y, z in self.get_signals():
if x == selection:
return y
def get_signals(self):
return self.signals + self.fs_menu + self.partition_menu
def get_menu(self):
return self.fs_menu
def probe_storage(self):
self.prober.probe()
self.storage = self.prober.get_results().get('storage')
log.debug('storage probe data:\n{}'.format(
json.dumps(self.storage, indent=4, sort_keys=True)))
# TODO: replace this with Storage.get_device_by_match()
# which takes a lambda fn for matching
VALID_MAJORS = ['8', '253']
for disk in self.storage.keys():
if self.storage[disk]['DEVTYPE'] == 'disk' and \
self.storage[disk]['MAJOR'] in VALID_MAJORS:
log.debug('disk={}\n{}'.format(disk,
json.dumps(self.storage[disk], indent=4,
sort_keys=True)))
self.info[disk] = StorageInfo({disk: self.storage[disk]})
def get_disk(self, disk):
if disk not in self.devices:
self.devices[disk] = Blockdev(disk, self.info[disk].serial)
return self.devices[disk]
def get_partitions(self):
partitions = []
for dev in self.devices.values():
partnames = [part.path for part in dev.disk.partitions]
partitions += partnames
sorted(partitions)
return partitions
def get_available_disks(self):
return sorted(self.info.keys())
def get_used_disks(self):
return [dev.disk.path for dev in self.devices.values()
if dev.available is False]
def get_disk_info(self, disk):
return self.info[disk]
def get_disk_action(self, disk):
return self.devices[disk].get_actions()
def get_actions(self):
actions = []
for dev in self.devices.values():
actions += dev.get_actions()
return actions
def _humanize_size(size):
@ -36,13 +184,10 @@ def _humanize_size(size):
class AddPartitionView(WidgetWrap):
signals = [
'fs:add-partition:done',
'fs:add-partition:cancel'
]
def __init__(self, model, selected_disk):
def __init__(self, model, signal, selected_disk):
self.partition_spec = {}
self.signal = signal
self.model = model
body = ListBox([
Padding.center_79(
@ -91,7 +236,7 @@ class AddPartitionView(WidgetWrap):
return SimpleList(total_items)
def cancel(self, button):
emit_signal(self, 'fs:add-partition:done', False)
self.signal.emit_signal('filesystem:finish-add-disk-partition')
def done(self):
""" partition spec
@ -105,17 +250,14 @@ class AddPartitionView(WidgetWrap):
if not self.partition_spec:
# TODO: Maybe popup warning?
return
emit_signal(self, 'fs:add-partition:done', self.partition_spec)
self.signal.emit_signal(
'filesystem:finish-add-disk-partition', self.partition_spec)
class DiskPartitionView(WidgetWrap):
signals = [
'fs:dp:show-add-partition',
'fs:dp:done',
]
def __init__(self, model, selected_disk):
def __init__(self, model, signal, selected_disk):
self.model = model
self.signal = signal
self.selected_disk = selected_disk
self.body = [
Padding.center_79(self._build_model_inputs()),
@ -141,7 +283,7 @@ class DiskPartitionView(WidgetWrap):
col_2 = []
disk = self.model.get_disk_info(self.selected_disk)
btn = done_btn(label="FREE SPACE", on_press=self.add_paritition)
btn = done_btn(label="FREE SPACE", on_press=self.add_partition)
col_1.append(Color.button_primary(btn,
focus_map='button_primary focus'))
disk_sz = str(_humanize_size(disk.size))
@ -155,7 +297,7 @@ class DiskPartitionView(WidgetWrap):
def _build_menu(self):
opts = []
for opt in self.model.partition_menu:
for opt, sig, _ in self.model.partition_menu:
opts.append(
Color.button_secondary(done_btn(label=opt,
on_press=self.done),
@ -163,24 +305,19 @@ class DiskPartitionView(WidgetWrap):
return Pile(opts)
def add_partition(self, partition):
emit_signal(self, 'fs:dp:show-add-partition', True)
self.signal.emit_signal('filesystem:add-disk-partition', partition.label)
def done(self, button):
emit_signal(self, 'fs:dp:done', True)
self.signal.emit_signal('filesystem:finish-disk-partition')
def cancel(self, button):
emit_signal(self, 'fs:dp:done', False)
self.signal.emit_signal('filesystem:finish-disk-partition', False)
class FilesystemView(WidgetWrap):
signals = [
"fs:done",
"fs:reset",
"fs:dp:view"
]
def __init__(self, model):
def __init__(self, model, signal):
self.model = model
self.signal = signal
self.items = []
self.body = [
Padding.center_79(Text("FILE SYSTEM")),
@ -220,8 +357,6 @@ class FilesystemView(WidgetWrap):
buttons = [
Color.button_secondary(reset_btn(on_press=self.reset),
focus_map='button_secondary focus'),
Color.button_secondary(done_btn(on_press=self.done),
focus_map='button_secondary focus'),
]
return Pile(buttons)
@ -247,23 +382,26 @@ class FilesystemView(WidgetWrap):
def _build_menu(self):
opts = []
for opt in self.model.fs_menu:
for opt, sig, _ in self.model.get_menu():
opts.append(
Color.button_secondary(done_btn(label=opt,
on_press=self.done),
Color.button_secondary(
done_btn(label=opt,
on_press=self.on_fs_menu_press),
focus_map='button_secondary focus'))
return Pile(opts)
def done(self, button):
def on_fs_menu_press(self, result):
log.info("Filesystem View done() getting disk info")
actions = self.model.get_actions()
emit_signal(self, 'fs:done', False, actions)
self.signal.emit_signal(
self.model.get_signal_by_name(result.label), False, actions)
def cancel(self, button):
emit_signal(self, 'fs:done')
self.signal.emit_signal(self.model.get_previous_signal)
def reset(self, button):
emit_signal(self, 'fs:done', True)
self.signal.emit_signal('filesystem:done', True)
def show_disk_partition_view(self, partition):
emit_signal(self, 'fs:dp:view', partition.label)
self.signal.emit_signal('filesystem:show-disk-partition',
partition.label)

View File

@ -17,7 +17,7 @@ import parted
import yaml
from itertools import count
from subiquity.models.actions import (
from subiquity.filesystem.actions import (
Action,
PartitionAction,
FormatAction,

View File

@ -0,0 +1,110 @@
# 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/>.
""" Install Path
Provides high level options for Ubuntu install
"""
import logging
from urwid import (WidgetWrap, ListBox, Pile, BoxAdapter)
from subiquity.ui.lists import SimpleList
from subiquity.ui.buttons import confirm_btn, cancel_btn
from subiquity.ui.utils import Padding, Color
from subiquity.model import ModelPolicy
log = logging.getLogger('subiquity.installpath')
class InstallpathModel(ModelPolicy):
""" Model representing install options
List of install paths in the form of:
('UI Text seen by user', <signal name>, <callback function string>)
"""
prev_signal = ('Back to welcome screen',
'welcome:show',
'welcome')
signals = [
('Install Path View',
'installpath:show',
'installpath')
]
install_paths = [('Install Ubuntu',
'installpath:ubuntu',
'install_ubuntu'),
('Install MAAS Region Server',
'installpath:maas-region-server',
'install_maas_region_server'),
('Install MAAS Cluster Server',
'installpath:maas-cluster-server',
'install_maas_cluster_server'),
('Test installation media',
'installpath:test-media',
'test_media'),
('Test machine memory',
'installpath:test-memory',
'test_memory')]
def get_signal_by_name(self, selection):
for x, y, z in self.get_signals():
if x == selection:
return y
def get_signals(self):
return self.signals + self.install_paths
def get_menu(self):
return self.install_paths
class InstallpathView(WidgetWrap):
def __init__(self, model, signal):
self.model = model
self.signal = signal
self.items = []
self.body = [
Padding.center_79(self._build_model_inputs()),
Padding.line_break(""),
Padding.center_20(self._build_buttons()),
]
super().__init__(ListBox(self.body))
def _build_buttons(self):
self.buttons = [
Color.button_secondary(cancel_btn(on_press=self.cancel),
focus_map='button_secondary focus'),
]
return Pile(self.buttons)
def _build_model_inputs(self):
sl = []
for ipath, sig, _ in self.model.get_menu():
log.debug("Building inputs: {}".format(ipath))
sl.append(Color.button_primary(confirm_btn(label=ipath,
on_press=self.confirm),
focus_map='button_primary focus'))
return BoxAdapter(SimpleList(sl),
height=len(sl))
def confirm(self, result):
self.signal.emit_signal(
self.model.get_signal_by_name(result.label))
def cancel(self, button):
self.signal.emit_signal(self.model.get_previous_signal)

65
subiquity/model.py Normal file
View File

@ -0,0 +1,65 @@
# 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/>.
""" Model Policy
"""
from abc import ABC, abstractmethod
class ModelPolicyException(Exception):
"Problem in model policy"
class ModelPolicy(ABC):
""" Expected contract for defining models
"""
# Exposed emitter signals
signals = []
# Back navigation
prev_signal = None
@abstractmethod
def get_signal_by_name(self, *args, **kwargs):
""" Implements a getter for retrieving
signals exposed by the model
"""
pass
@abstractmethod
def get_signals(self):
""" Lists available signals for model
Should return a list with a tuple format of
[('Name of item', 'signal-name', 'callback function string')]
"""
pass
@abstractmethod
def get_menu(self):
""" Returns a list of menu items
Should return a list with a tuple format the same
as get_signals()
"""
pass
@property
def get_previous_signal(self):
""" Returns the previous defined signal
"""
name, signal, cb = self.prev_signal
return signal

View File

@ -1,68 +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/>.
""" Model Classes
Model's represent the stateful data bound from
input from the user.
"""
import json
import yaml
class Model:
"""Base model"""
fields = []
def to_json(self):
"""Marshals the model to json"""
return json.dumps(self.__dict__)
def to_yaml(self):
"""Marshals the model to yaml"""
return yaml.dump(self.__dict__)
class Field:
"""Base field class
New field types inherit this class, provides access to
validation checks and type definitions.
"""
default_error_messages = {
'invalid_choice': ('Value %(value)r is not a valid choice.'),
'blank': ('This field cannot be blank.')
}
def __init__(self, name=None, blank=False):
self.name = name
self.blank = blank
class ChoiceField(Field):
""" Choices Field
Provide a list of known options
:param list options: list of options to choose from
"""
def __init__(self, options):
self.options = options
def list_options(cls):
return cls.options

View File

@ -1,117 +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/>.
""" Filesystem Model
Provides storage device selection and additional storage
configuration.
"""
import logging
import json
import argparse
from subiquity import models
from subiquity.models.blockdev import Blockdev
from probert import prober
from probert.storage import StorageInfo
log = logging.getLogger('subiquity.filesystemModel')
class FilesystemModel(models.Model):
""" Model representing storage options
"""
fs_menu = [
'Connect iSCSI network disk',
'Connect Ceph network disk',
'Create volume group (LVM2)',
'Create software RAID (MD)',
'Setup hierarchichal storage (bcache)'
]
partition_menu = [
'Add first GPT partition',
'Format or create swap on entire device (unusual, advanced)'
]
supported_filesystems = [
'ext4',
'xfs',
'btrfs',
'swap',
'bcache cache',
'bcache store',
'leave unformatted'
]
def __init__(self):
self.storage = {}
self.info = {}
self.devices = {}
self.options = argparse.Namespace(probe_storage=True,
probe_network=False)
self.prober = prober.Prober(self.options)
self.probe_storage()
def probe_storage(self):
self.prober.probe()
self.storage = self.prober.get_results().get('storage')
log.debug('storage probe data:\n{}'.format(
json.dumps(self.storage, indent=4, sort_keys=True)))
# TODO: replace this with Storage.get_device_by_match()
# which takes a lambda fn for matching
VALID_MAJORS = ['8', '253']
for disk in self.storage.keys():
if self.storage[disk]['DEVTYPE'] == 'disk' and \
self.storage[disk]['MAJOR'] in VALID_MAJORS:
log.debug('disk={}\n{}'.format(disk,
json.dumps(self.storage[disk], indent=4,
sort_keys=True)))
self.info[disk] = StorageInfo({disk: self.storage[disk]})
def get_disk(self, disk):
if disk not in self.devices:
self.devices[disk] = Blockdev(disk, self.info[disk].serial)
return self.devices[disk]
def get_partitions(self):
partitions = []
for dev in self.devices.values():
partnames = [part.path for part in dev.disk.partitions]
partitions += partnames
sorted(partitions)
return partitions
def get_available_disks(self):
return sorted(self.info.keys())
def get_used_disks(self):
return [dev.disk.path for dev in self.devices.values()
if dev.available is False]
def get_disk_info(self, disk):
return self.info[disk]
def get_disk_action(self, disk):
return self.devices[disk].get_actions()
def get_actions(self):
actions = []
for dev in self.devices.values():
actions += dev.get_actions()
return actions

View File

@ -1,31 +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/>.
""" Identity Model
Represents information related to identification, for example,
User's first and last name, timezone, country, language preferences.
"""
from subiquity import models
class UserModel(models.Model):
""" User class to support personal information
"""
username = None
language = None
keyboard = None
timezone = None

View File

@ -1,34 +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/>.
""" Install Path Model
Provides high level options for Ubuntu install
"""
from subiquity import models
class InstallpathModel(models.Model):
""" Model representing install options
"""
install_paths = ['Install Ubuntu',
'Install MAAS Region Server',
'Install MAAS Cluster Server',
'Test installation media',
'Test machine memory']
selected_path = None

View File

@ -1,103 +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/>.
""" Network Model
Provides network device listings and extended network information
"""
import logging
from subiquity import models
import argparse
from probert import prober
log = logging.getLogger('subiquity.networkModel')
class SimpleInterface:
""" A simple interface class to encapsulate network information for
particular interface
"""
def __init__(self, attrs):
self.attrs = attrs
for i in self.attrs.keys():
if self.attrs[i] is None:
setattr(self, i, "Unknown")
else:
setattr(self, i, self.attrs[i])
class NetworkModel(models.Model):
""" Model representing network interfaces
"""
additional_options = ['Set default route',
'Bond interfaces',
'Install network driver']
def __init__(self):
self.network = {}
self.options = argparse.Namespace(probe_storage=False,
probe_network=True)
self.prober = prober.Prober(self.options)
def probe_network(self):
self.prober.probe()
self.network = self.prober.get_results().get('network')
def get_interfaces(self):
return [iface for iface in self.network.keys()
if self.network[iface]['type'] == 'eth' and
not self.network[iface]['hardware']['DEVPATH'].startswith(
'/devices/virtual/net')]
def get_vendor(self, iface):
hwinfo = self.network[iface]['hardware']
vendor_keys = [
'ID_VENDOR_FROM_DATABASE',
'ID_VENDOR',
'ID_VENDOR_ID'
]
for key in vendor_keys:
try:
return hwinfo[key]
except KeyError:
log.warn('Failed to get key '
'{} from interface {}'.format(key, iface))
pass
return 'Unknown Vendor'
def get_model(self, iface):
hwinfo = self.network[iface]['hardware']
model_keys = [
'ID_MODEL_FROM_DATABASE',
'ID_MODEL',
'ID_MODEL_ID'
]
for key in model_keys:
try:
return hwinfo[key]
except KeyError:
log.warn('Failed to get key '
'{} from interface {}'.format(key, iface))
pass
return 'Unknown Model'
def get_iface_info(self, iface):
ipinfo = SimpleInterface(self.network[iface]['ip'])
return (ipinfo, self.get_vendor(iface), self.get_model(iface))

View File

@ -1,33 +0,0 @@
# Copyright 2015 Canonical, Ltd.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
""" Welcome Model
Welcome model provides user with language selection
"""
from subiquity import models
class WelcomeModel(models.Model):
""" Model representing language selection
"""
supported_languages = ['English', 'Belgian', 'German', 'Italian']
selected_language = None
def __repr__(self):
return "<Selected: {}>".format(self.selected_language)

View File

@ -0,0 +1,208 @@
# 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/>.
""" Network Model
Provides network device listings and extended network information
"""
import logging
import argparse
from probert import prober
from urwid import (WidgetWrap, ListBox, Pile, BoxAdapter,
Text, Columns)
from subiquity.ui.lists import SimpleList
from subiquity.ui.buttons import confirm_btn, cancel_btn
from subiquity.ui.utils import Padding, Color
from subiquity.model import ModelPolicy
log = logging.getLogger('subiquity.network')
class SimpleInterface:
""" A simple interface class to encapsulate network information for
particular interface
"""
def __init__(self, attrs):
self.attrs = attrs
for i in self.attrs.keys():
if self.attrs[i] is None:
setattr(self, i, "Unknown")
else:
setattr(self, i, self.attrs[i])
class NetworkModel(ModelPolicy):
""" Model representing network interfaces
"""
prev_signal = ('Back to install path',
'installpath:show',
'installpath')
signals = [
('Network main view',
'network:show',
'network')
]
additional_options = [
('Set default route',
'network:set-default-route',
'set_default_route'),
('Bond interfaces',
'network:bond-interfaces',
'bond_interfaces'),
('Install network driver',
'network:install-network-driver',
'install_network_driver')
]
def __init__(self):
self.network = {}
self.options = argparse.Namespace(probe_storage=False,
probe_network=True)
self.prober = prober.Prober(self.options)
def get_signal_by_name(self, selection):
for x, y, z in self.get_signals():
if x == selection:
return y
def get_signals(self):
return self.signals + self.additional_options
def get_menu(self):
return self.additional_options
def probe_network(self):
self.prober.probe()
self.network = self.prober.get_results().get('network')
def get_interfaces(self):
return [iface for iface in self.network.keys()
if self.network[iface]['type'] == 'eth' and
not self.network[iface]['hardware']['DEVPATH'].startswith(
'/devices/virtual/net')]
def get_vendor(self, iface):
hwinfo = self.network[iface]['hardware']
vendor_keys = [
'ID_VENDOR_FROM_DATABASE',
'ID_VENDOR',
'ID_VENDOR_ID'
]
for key in vendor_keys:
try:
return hwinfo[key]
except KeyError:
log.warn('Failed to get key '
'{} from interface {}'.format(key, iface))
pass
return 'Unknown Vendor'
def get_model(self, iface):
hwinfo = self.network[iface]['hardware']
model_keys = [
'ID_MODEL_FROM_DATABASE',
'ID_MODEL',
'ID_MODEL_ID'
]
for key in model_keys:
try:
return hwinfo[key]
except KeyError:
log.warn('Failed to get key '
'{} from interface {}'.format(key, iface))
pass
return 'Unknown Model'
def get_iface_info(self, iface):
ipinfo = SimpleInterface(self.network[iface]['ip'])
return (ipinfo, self.get_vendor(iface), self.get_model(iface))
class NetworkView(WidgetWrap):
def __init__(self, model, signal):
self.model = model
self.signal = signal
self.items = []
self.body = [
Padding.center_79(self._build_model_inputs()),
Padding.line_break(""),
Padding.center_79(self._build_additional_options()),
Padding.line_break(""),
Padding.center_20(self._build_buttons()),
]
super().__init__(ListBox(self.body))
def _build_buttons(self):
buttons = [
Color.button_secondary(cancel_btn(on_press=self.cancel),
focus_map='button_secondary focus'),
]
return Pile(buttons)
def _build_model_inputs(self):
log.info("probing for network devices")
self.model.probe_network()
ifaces = self.model.get_interfaces()
col_1 = []
for iface in ifaces:
col_1.append(
Color.button_primary(
confirm_btn(label=iface,
on_press=self.on_net_dev_press),
focus_map='button_primary focus'))
col_1 = BoxAdapter(SimpleList(col_1),
height=len(col_1))
col_2 = []
for iface in ifaces:
ifinfo, iface_vendor, iface_model = self.model.get_iface_info(
iface)
col_2.append(Text("Address: {}".format(ifinfo.addr)))
col_2.append(
Text("{} - {}".format(iface_vendor,
iface_model)))
col_2 = BoxAdapter(SimpleList(col_2, is_selectable=False),
height=len(col_2))
return Columns([(10, col_1), col_2], 2)
def _build_additional_options(self):
opts = []
for opt, sig, _ in self.model.get_menu():
opts.append(
Color.button_secondary(
confirm_btn(label=opt,
on_press=self.additional_menu_select),
focus_map='button_secondary focus'))
return Pile(opts)
def additional_menu_select(self, result):
self.signal.emit_signal(self.model.get_signal_by_name(result.label))
def on_net_dev_press(self, result):
log.debug("Selected network dev: {}".format(result.label))
self.signal.emit_signal('filesystem:show')
def cancel(self, button):
self.signal.emit_signal(self.model.get_previous_signal)

View File

@ -1,81 +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 subiquity.controllers.welcome import WelcomeController
from subiquity.controllers.installpath import InstallpathController
from subiquity.controllers.network import NetworkController
from subiquity.controllers.filesystem import FilesystemController
class RoutesError(Exception):
""" Error in routes """
pass
class Routes:
""" Defines application routes and maps to their controller
Routes are inserted top down from start to finish. Maintaining
this order is required for routing to work.
"""
routes = [WelcomeController,
InstallpathController,
NetworkController,
FilesystemController]
current_route_idx = 0
@classmethod
def route(cls, idx):
""" Include route listing in controllers """
try:
_route = cls.routes[idx]
except IndexError:
raise RoutesError("Failed to load Route at index: {}".format(idx))
return _route
@classmethod
def current_idx(cls):
""" Returns current route index """
return cls.current_route_idx
@classmethod
def reset(cls):
""" Resets current route """
cls.current_route_idx = 0
@classmethod
def first(cls):
""" first controller/start of install """
return cls.route(0)
@classmethod
def last(cls):
""" end of install, last controller """
return cls.route(-1)
@classmethod
def current(cls):
""" return current route's controller """
return cls.route(cls.current_route_idx)
@classmethod
def next(cls):
cls.current_route_idx = cls.current_route_idx + 1
return cls.route(cls.current_route_idx)
@classmethod
def prev(cls):
cls.current_route_idx = cls.current_route_idx - 1
return cls.route(cls.current_route_idx)

View File

@ -13,19 +13,51 @@
# 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/>.
""" Registers all known signal emitters
"""
import urwid
import logging
SIGNALS = {}
log = logging.getLogger('subiquity.signals')
def register_signal(obj, name):
if obj.__class__ not in SIGNALS:
SIGNALS[obj.__class__] = []
if name not in SIGNALS[obj.__class__]:
SIGNALS[obj.__class__].append(name)
urwid.register_signal(obj.__class__, SIGNALS[obj.__class__])
class SignalException(Exception):
"Problem with a signal"
def emit_signal(obj, name, args):
register_signal(obj, name)
urwid.emit_signal(obj, name, args)
class Signal:
known_signals = []
def register_signals(self, signals):
if type(signals) is list:
self.known_signals.extend(signals)
urwid.register_signal(Signal, signals)
def emit_signal(self, name, *args, **kwargs):
log.debug("Emitter: {}, {}, {}".format(name, args, kwargs))
urwid.emit_signal(self, name, *args, **kwargs)
def connect_signal(self, name, cb, **kwargs):
log.debug(
"Emitter Connection: {}, {}, {}".format(name,
cb,
kwargs))
urwid.connect_signal(self, name, cb, **kwargs)
def connect_signals(self, signal_callback):
""" Connects a batch of signals
:param list signal_callback: List of tuples
eg. ('welcome:show', self.cb)
"""
if not type(signal_callback) is list:
raise SignalException(
"Passed something other than a required list.")
for sig, cb in signal_callback:
if sig not in self.known_signals:
self.register_signals(sig)
self.connect_signal(sig, cb)
def __repr__(self):
return "Known Signals: {}".format(self.known_signals)

45
subiquity/ui/dummy.py Normal file
View File

@ -0,0 +1,45 @@
# 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/>.
""" Dummy placeholder widget
"""
from urwid import (WidgetWrap, Text, Pile, ListBox)
from subiquity.ui.buttons import cancel_btn
from subiquity.ui.utils import Padding, Color
class DummyView(WidgetWrap):
def __init__(self, signal):
self.signal = signal
self.body = [
Padding.center_79(Text("This view is not yet implemented.")),
Padding.line_break(""),
Padding.center_79(Color.info_minor(Text("A place holder widget"))),
Padding.line_break(""),
Padding.center_79(self._build_buttons())
]
super().__init__(ListBox(self.body))
def _build_buttons(self):
buttons = [
Color.button_secondary(cancel_btn(label="Back to Start",
on_press=self.cancel),
focus_map='button_secondary focus'),
]
return Pile(buttons)
def cancel(self, result):
self.signal.emit_signal('welcome:show')

View File

@ -15,9 +15,8 @@
""" Base Frame Widget """
from urwid import Frame, WidgetWrap
from urwid import Frame, WidgetWrap, register_signal
from subiquity.ui.anchors import Header, Footer, Body
from subiquity.signals import register_signal
import logging

View File

@ -1,14 +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/>.

View File

@ -1,60 +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/>.
import logging
from urwid import (WidgetWrap, ListBox, Pile, BoxAdapter)
from subiquity.ui.lists import SimpleList
from subiquity.ui.buttons import confirm_btn, cancel_btn
from subiquity.ui.utils import Padding, Color
log = logging.getLogger('subiquity.installpathView')
class InstallpathView(WidgetWrap):
def __init__(self, model, cb):
log.debug("In install path view")
self.model = model
self.cb = cb
self.items = []
self.body = [
Padding.center_79(self._build_model_inputs()),
Padding.line_break(""),
Padding.center_20(self._build_buttons()),
]
super().__init__(ListBox(self.body))
def _build_buttons(self):
self.buttons = [
Color.button_secondary(cancel_btn(on_press=self.cancel),
focus_map='button_secondary focus'),
]
return Pile(self.buttons)
def _build_model_inputs(self):
sl = []
for ipath in self.model.install_paths:
sl.append(Color.button_primary(confirm_btn(label=ipath,
on_press=self.confirm),
focus_map='button_primary focus'))
return BoxAdapter(SimpleList(sl),
height=len(sl))
def confirm(self, button):
return self.cb(button.label)
def cancel(self, button):
return self.cb(None)

View File

@ -1,87 +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/>.
import logging
from urwid import (WidgetWrap, ListBox, Pile, BoxAdapter, Text, Columns)
from subiquity.ui.lists import SimpleList
from subiquity.ui.buttons import confirm_btn, cancel_btn
from subiquity.ui.utils import Padding, Color
log = logging.getLogger('subiquity.networkView')
class NetworkView(WidgetWrap):
def __init__(self, model, cb):
self.model = model
self.cb = cb
self.items = []
self.body = [
Padding.center_79(self._build_model_inputs()),
Padding.line_break(""),
Padding.center_79(self._build_additional_options()),
Padding.line_break(""),
Padding.center_20(self._build_buttons()),
]
super().__init__(ListBox(self.body))
def _build_buttons(self):
buttons = [
Color.button_secondary(cancel_btn(on_press=self.cancel),
focus_map='button_secondary focus'),
]
return Pile(buttons)
def _build_model_inputs(self):
log.info("probing for network devices")
self.model.probe_network()
ifaces = self.model.get_interfaces()
col_1 = []
for iface in ifaces:
col_1.append(
Color.button_primary(confirm_btn(label=iface,
on_press=self.confirm),
focus_map='button_primary focus'))
col_1 = BoxAdapter(SimpleList(col_1),
height=len(col_1))
col_2 = []
for iface in ifaces:
ifinfo, iface_vendor, iface_model = self.model.get_iface_info(
iface)
col_2.append(Text("Address: {}".format(ifinfo.addr)))
col_2.append(
Text("{} - {}".format(iface_vendor,
iface_model)))
col_2 = BoxAdapter(SimpleList(col_2, is_selectable=False),
height=len(col_2))
return Columns([(10, col_1), col_2], 2)
def _build_additional_options(self):
opts = []
for opt in self.model.additional_options:
opts.append(
Color.button_secondary(confirm_btn(label=opt,
on_press=self.confirm),
focus_map='button_secondary focus'))
return Pile(opts)
def confirm(self, button):
return self.cb(button.label)
def cancel(self, button):
return self.cb(None)

View File

@ -13,16 +13,57 @@
# 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 (WidgetWrap, ListBox, Pile, BoxAdapter)
""" Welcome
Welcome provides user with language selection
"""
import logging
from urwid import (WidgetWrap, ListBox, Pile, BoxAdapter, emit_signal)
from subiquity.ui.lists import SimpleList
from subiquity.ui.buttons import confirm_btn, cancel_btn
from subiquity.ui.utils import Padding, Color
from subiquity.model import ModelPolicy
log = logging.getLogger('subiquity.welcome')
class WelcomeModel(ModelPolicy):
""" Model representing language selection
"""
prev_signal = None
signals = [
("Welcome view",
'welcome:show',
'welcome')
]
supported_languages = ['English',
'Belgian',
'German',
'Italian']
selected_language = None
def get_signals(self):
return self.signals
def get_menu(self):
return self.supported_languages
def get_signal_by_name(self, selection):
for x, y, z in self.get_menu():
if x == selection:
return y
def __repr__(self):
return "<Selected: {}>".format(self.selected_language)
class WelcomeView(WidgetWrap):
def __init__(self, model, cb):
def __init__(self, model, signal):
self.model = model
self.cb = cb
self.signal = signal
self.items = []
self.body = [
Padding.center_79(self._build_model_inputs()),
@ -40,7 +81,7 @@ class WelcomeView(WidgetWrap):
def _build_model_inputs(self):
sl = []
for lang in self.model.supported_languages:
for lang in self.model.get_menu():
sl.append(Color.button_primary(
confirm_btn(label=lang, on_press=self.confirm),
focus_map="button_primary focus"))
@ -48,8 +89,10 @@ class WelcomeView(WidgetWrap):
return BoxAdapter(SimpleList(sl),
height=len(sl))
def confirm(self, button):
return self.cb(button.label)
def confirm(self, result):
self.model.selected_language = result.label
emit_signal(self.signal, 'installpath:show')
def cancel(self, button):
return self.cb(None)
raise SystemExit("No language selected, exiting as there are no "
"more previous controllers to render.")