refactor controller code and signal handling

Signed-off-by: Adam Stokes <adam.stokes@ubuntu.com>
This commit is contained in:
Adam Stokes 2015-07-21 11:55:02 -04:00
parent c156c741cb
commit f429372d3c
24 changed files with 500 additions and 931 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

253
subiquity/core.py Normal file
View File

@ -0,0 +1,253 @@
# 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)
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.signal = Signal()
self.signal.register_signals()
# 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)
model = WelcomeModel()
view = WelcomeView(model, self.signal)
urwid.connect_signal(self.signal, 'welcome:finish',
self.welcome_handler)
urwid.connect_signal(self.signal, 'installpath:show', self.installpath)
self.ui.set_body(view)
self.welcome_model = WelcomeModel()
def welcome_handler(self, language=None):
log.debug("Welcome handler")
if language is None:
raise SystemExit("No language selected, exiting as there are no "
"more previous controllers to render.")
self.welcome_model.selected_language = language
log.debug("Welcome Model: {}".format(self.welcome_model))
urwid.emit_signal(self.signal, 'installpath:show')
# 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.installpath_model = InstallpathModel()
urwid.connect_signal(self.signal, 'installpath:finish',
self.installpath_handler)
urwid.connect_signal(self.signal, 'welcome:show', self.welcome)
self.ui.set_body(InstallpathView(self.installpath_model, self.signal))
def installpath_handler(self, install_selection=None):
if install_selection is None:
urwid.emit_signal(self.signal, 'welcome:show', [])
urwid.emit_signal(self.signal, 'network:show', [])
# 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.network_model = NetworkModel()
self.ui.set_header(title, excerpt)
self.ui.set_footer(footer)
urwid.connect_signal(self.signal, 'network:finish',
self.network_handler)
urwid.connect_signal(self.signal, 'installpath:show', self.installpath)
urwid.connect_signal(self.signal, 'filesystem:show', self.filesystem)
self.ui.set_body(NetworkView(self.network_model))
def network_handler(self, interface=None):
log.info("Network Interface choosen: {}".format(interface))
if interface is None:
urwid.emit_signal(self.signal, 'installpath:show', [])
urwid.emit_signal(self.signal, 'filesystem:show', [])
# 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.fs_model = FilesystemModel()
urwid.connect_signal(self.signal, 'filesystem:finish',
self.filesystem_handler)
urwid.connect_signal(self.signal, 'filesystem:show-disk-partition',
self.disk_partition)
self.ui.set_body(FilesystemView(self.fs_model))
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.fs_model,
disk)
urwid.connect_signal(self.signal, 'filesystem:finish-disk-partition',
self.disk_paritition_handler)
urwid.connect_signal(self.signal, 'filesystem:add-disk-partition',
self.add_disk_partition)
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.fs_model,
disk)
urwid.connect_signal(self.signal,
'filesystem:finish-add-disk-partition',
self.add_disk_paritition_handler)
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.")

View File

@ -13,17 +13,114 @@
# 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
"""
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
def _humanize_size(size):

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

@ -13,21 +13,37 @@
# 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 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
log = logging.getLogger('subiquity.installpathView')
class InstallpathModel:
""" 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
class InstallpathView(WidgetWrap):
def __init__(self, model, cb):
def __init__(self, model, signal):
log.debug("In install path view")
self.model = model
self.cb = cb
self.signal = signal
self.items = []
self.body = [
Padding.center_79(self._build_model_inputs()),
@ -54,7 +70,7 @@ class InstallpathView(WidgetWrap):
height=len(sl))
def confirm(self, button):
return self.cb(button.label)
emit_signal(self.signal, 'installpath:finish', button.label)
def cancel(self, button):
return self.cb(None)
emit_signal(self.signal, 'welcome:show', None)

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

@ -13,14 +13,98 @@
# 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
log = logging.getLogger('subiquity.networkView')
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:
""" 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))
class NetworkView(WidgetWrap):

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

@ -15,17 +15,19 @@
import urwid
SIGNALS = {}
class Signal:
signals = [
'welcome:show',
'welcome:finish',
'installpath:show',
'installpath:finish',
'filesystem:show',
'filesystem:show-disk-partition',
'filesystem:finish-disk-partition',
'filesystem:finish',
'filesystem:add-disk-partition'
]
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__])
def emit_signal(obj, name, args):
register_signal(obj, name)
urwid.emit_signal(obj, name, args)
def register_signals(self):
urwid.register_signal(Signal, self.signals)

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

@ -13,16 +13,35 @@
# 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
log = logging.getLogger('subiquity.welcome')
class WelcomeModel:
""" Model representing language selection
"""
supported_languages = ['English', 'Belgian', 'German', 'Italian']
selected_language = None
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()),
@ -49,7 +68,7 @@ class WelcomeView(WidgetWrap):
height=len(sl))
def confirm(self, button):
return self.cb(button.label)
emit_signal(self.signal, 'welcome:finish', button.label)
def cancel(self, button):
return self.cb(None)
emit_signal(self.signal, 'welcome:finish', None)