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
|
import logging
|
||||||
from subiquity.log import setup_logger
|
from subiquity.log import setup_logger
|
||||||
from subiquity import __version__ as VERSION
|
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
|
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
|
# 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/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
""" Filesystem
|
||||||
|
|
||||||
|
Provides storage device selection and additional storage
|
||||||
|
configuration.
|
||||||
|
|
||||||
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
import json
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from subiquity.filesystem.blockdev import Blockdev
|
||||||
|
from probert import prober
|
||||||
|
from probert.storage import StorageInfo
|
||||||
import math
|
import math
|
||||||
from urwid import (WidgetWrap, ListBox, Pile, BoxAdapter,
|
from urwid import (WidgetWrap, ListBox, Pile, BoxAdapter,
|
||||||
Text, Columns, LineBox, Edit, RadioButton)
|
Text, Columns, LineBox, Edit, RadioButton)
|
||||||
from subiquity.ui.lists import SimpleList
|
from subiquity.ui.lists import SimpleList
|
||||||
from subiquity.ui.buttons import done_btn, reset_btn, cancel_btn
|
from subiquity.ui.buttons import done_btn, reset_btn, cancel_btn
|
||||||
from subiquity.ui.utils import Padding, Color
|
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):
|
def _humanize_size(size):
|
||||||
|
@ -36,13 +184,10 @@ def _humanize_size(size):
|
||||||
|
|
||||||
|
|
||||||
class AddPartitionView(WidgetWrap):
|
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.partition_spec = {}
|
||||||
|
self.signal = signal
|
||||||
self.model = model
|
self.model = model
|
||||||
body = ListBox([
|
body = ListBox([
|
||||||
Padding.center_79(
|
Padding.center_79(
|
||||||
|
@ -91,7 +236,7 @@ class AddPartitionView(WidgetWrap):
|
||||||
return SimpleList(total_items)
|
return SimpleList(total_items)
|
||||||
|
|
||||||
def cancel(self, button):
|
def cancel(self, button):
|
||||||
emit_signal(self, 'fs:add-partition:done', False)
|
self.signal.emit_signal('filesystem:finish-add-disk-partition')
|
||||||
|
|
||||||
def done(self):
|
def done(self):
|
||||||
""" partition spec
|
""" partition spec
|
||||||
|
@ -105,17 +250,14 @@ class AddPartitionView(WidgetWrap):
|
||||||
if not self.partition_spec:
|
if not self.partition_spec:
|
||||||
# TODO: Maybe popup warning?
|
# TODO: Maybe popup warning?
|
||||||
return
|
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):
|
class DiskPartitionView(WidgetWrap):
|
||||||
signals = [
|
def __init__(self, model, signal, selected_disk):
|
||||||
'fs:dp:show-add-partition',
|
|
||||||
'fs:dp:done',
|
|
||||||
]
|
|
||||||
|
|
||||||
def __init__(self, model, selected_disk):
|
|
||||||
self.model = model
|
self.model = model
|
||||||
|
self.signal = signal
|
||||||
self.selected_disk = selected_disk
|
self.selected_disk = selected_disk
|
||||||
self.body = [
|
self.body = [
|
||||||
Padding.center_79(self._build_model_inputs()),
|
Padding.center_79(self._build_model_inputs()),
|
||||||
|
@ -141,7 +283,7 @@ class DiskPartitionView(WidgetWrap):
|
||||||
col_2 = []
|
col_2 = []
|
||||||
|
|
||||||
disk = self.model.get_disk_info(self.selected_disk)
|
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,
|
col_1.append(Color.button_primary(btn,
|
||||||
focus_map='button_primary focus'))
|
focus_map='button_primary focus'))
|
||||||
disk_sz = str(_humanize_size(disk.size))
|
disk_sz = str(_humanize_size(disk.size))
|
||||||
|
@ -155,7 +297,7 @@ class DiskPartitionView(WidgetWrap):
|
||||||
|
|
||||||
def _build_menu(self):
|
def _build_menu(self):
|
||||||
opts = []
|
opts = []
|
||||||
for opt in self.model.partition_menu:
|
for opt, sig, _ in self.model.partition_menu:
|
||||||
opts.append(
|
opts.append(
|
||||||
Color.button_secondary(done_btn(label=opt,
|
Color.button_secondary(done_btn(label=opt,
|
||||||
on_press=self.done),
|
on_press=self.done),
|
||||||
|
@ -163,24 +305,19 @@ class DiskPartitionView(WidgetWrap):
|
||||||
return Pile(opts)
|
return Pile(opts)
|
||||||
|
|
||||||
def add_partition(self, partition):
|
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):
|
def done(self, button):
|
||||||
emit_signal(self, 'fs:dp:done', True)
|
self.signal.emit_signal('filesystem:finish-disk-partition')
|
||||||
|
|
||||||
def cancel(self, button):
|
def cancel(self, button):
|
||||||
emit_signal(self, 'fs:dp:done', False)
|
self.signal.emit_signal('filesystem:finish-disk-partition', False)
|
||||||
|
|
||||||
|
|
||||||
class FilesystemView(WidgetWrap):
|
class FilesystemView(WidgetWrap):
|
||||||
signals = [
|
def __init__(self, model, signal):
|
||||||
"fs:done",
|
|
||||||
"fs:reset",
|
|
||||||
"fs:dp:view"
|
|
||||||
]
|
|
||||||
|
|
||||||
def __init__(self, model):
|
|
||||||
self.model = model
|
self.model = model
|
||||||
|
self.signal = signal
|
||||||
self.items = []
|
self.items = []
|
||||||
self.body = [
|
self.body = [
|
||||||
Padding.center_79(Text("FILE SYSTEM")),
|
Padding.center_79(Text("FILE SYSTEM")),
|
||||||
|
@ -220,8 +357,6 @@ class FilesystemView(WidgetWrap):
|
||||||
buttons = [
|
buttons = [
|
||||||
Color.button_secondary(reset_btn(on_press=self.reset),
|
Color.button_secondary(reset_btn(on_press=self.reset),
|
||||||
focus_map='button_secondary focus'),
|
focus_map='button_secondary focus'),
|
||||||
Color.button_secondary(done_btn(on_press=self.done),
|
|
||||||
focus_map='button_secondary focus'),
|
|
||||||
]
|
]
|
||||||
return Pile(buttons)
|
return Pile(buttons)
|
||||||
|
|
||||||
|
@ -247,23 +382,26 @@ class FilesystemView(WidgetWrap):
|
||||||
|
|
||||||
def _build_menu(self):
|
def _build_menu(self):
|
||||||
opts = []
|
opts = []
|
||||||
for opt in self.model.fs_menu:
|
for opt, sig, _ in self.model.get_menu():
|
||||||
opts.append(
|
opts.append(
|
||||||
Color.button_secondary(done_btn(label=opt,
|
Color.button_secondary(
|
||||||
on_press=self.done),
|
done_btn(label=opt,
|
||||||
focus_map='button_secondary focus'))
|
on_press=self.on_fs_menu_press),
|
||||||
|
focus_map='button_secondary focus'))
|
||||||
return Pile(opts)
|
return Pile(opts)
|
||||||
|
|
||||||
def done(self, button):
|
def on_fs_menu_press(self, result):
|
||||||
log.info("Filesystem View done() getting disk info")
|
log.info("Filesystem View done() getting disk info")
|
||||||
actions = self.model.get_actions()
|
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):
|
def cancel(self, button):
|
||||||
emit_signal(self, 'fs:done')
|
self.signal.emit_signal(self.model.get_previous_signal)
|
||||||
|
|
||||||
def reset(self, button):
|
def reset(self, button):
|
||||||
emit_signal(self, 'fs:done', True)
|
self.signal.emit_signal('filesystem:done', True)
|
||||||
|
|
||||||
def show_disk_partition_view(self, partition):
|
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
|
import yaml
|
||||||
from itertools import count
|
from itertools import count
|
||||||
|
|
||||||
from subiquity.models.actions import (
|
from subiquity.filesystem.actions import (
|
||||||
Action,
|
Action,
|
||||||
PartitionAction,
|
PartitionAction,
|
||||||
FormatAction,
|
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
|
# 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/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
""" Registers all known signal emitters
|
||||||
|
"""
|
||||||
import urwid
|
import urwid
|
||||||
|
import logging
|
||||||
|
|
||||||
SIGNALS = {}
|
log = logging.getLogger('subiquity.signals')
|
||||||
|
|
||||||
|
|
||||||
def register_signal(obj, name):
|
class SignalException(Exception):
|
||||||
if obj.__class__ not in SIGNALS:
|
"Problem with a signal"
|
||||||
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):
|
class Signal:
|
||||||
register_signal(obj, name)
|
known_signals = []
|
||||||
urwid.emit_signal(obj, name, args)
|
|
||||||
|
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 """
|
""" 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.ui.anchors import Header, Footer, Body
|
||||||
from subiquity.signals import register_signal
|
|
||||||
import logging
|
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
|
# 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/>.
|
# 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.lists import SimpleList
|
||||||
from subiquity.ui.buttons import confirm_btn, cancel_btn
|
from subiquity.ui.buttons import confirm_btn, cancel_btn
|
||||||
from subiquity.ui.utils import Padding, Color
|
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):
|
class WelcomeView(WidgetWrap):
|
||||||
def __init__(self, model, cb):
|
def __init__(self, model, signal):
|
||||||
self.model = model
|
self.model = model
|
||||||
self.cb = cb
|
self.signal = signal
|
||||||
self.items = []
|
self.items = []
|
||||||
self.body = [
|
self.body = [
|
||||||
Padding.center_79(self._build_model_inputs()),
|
Padding.center_79(self._build_model_inputs()),
|
||||||
|
@ -40,7 +81,7 @@ class WelcomeView(WidgetWrap):
|
||||||
|
|
||||||
def _build_model_inputs(self):
|
def _build_model_inputs(self):
|
||||||
sl = []
|
sl = []
|
||||||
for lang in self.model.supported_languages:
|
for lang in self.model.get_menu():
|
||||||
sl.append(Color.button_primary(
|
sl.append(Color.button_primary(
|
||||||
confirm_btn(label=lang, on_press=self.confirm),
|
confirm_btn(label=lang, on_press=self.confirm),
|
||||||
focus_map="button_primary focus"))
|
focus_map="button_primary focus"))
|
||||||
|
@ -48,8 +89,10 @@ class WelcomeView(WidgetWrap):
|
||||||
return BoxAdapter(SimpleList(sl),
|
return BoxAdapter(SimpleList(sl),
|
||||||
height=len(sl))
|
height=len(sl))
|
||||||
|
|
||||||
def confirm(self, button):
|
def confirm(self, result):
|
||||||
return self.cb(button.label)
|
self.model.selected_language = result.label
|
||||||
|
emit_signal(self.signal, 'installpath:show')
|
||||||
|
|
||||||
def cancel(self, button):
|
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