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:
commit
02cab5a223
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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()
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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))
|
|
@ -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),
|
||||
focus_map='button_secondary focus'))
|
||||
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)
|
|
@ -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,
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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))
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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/>.
|
|
@ -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)
|
|
@ -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)
|
|
@ -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.")
|
Loading…
Reference in New Issue