Merge pull request #275 from CanonicalLtd/maas-option

Re-add installpath controller, with added MAAS views.
This commit is contained in:
Michael Hudson-Doyle 2018-02-21 12:24:08 +13:00 committed by GitHub
commit 37d93a1954
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 213 additions and 59 deletions

View File

@ -2,6 +2,8 @@ Welcome:
lang: en_US
Keyboard:
layout: us
Installpath:
path: ubuntu
Network:
accept-default: yes
Filesystem:

View File

@ -18,27 +18,18 @@ import logging
import lsb_release
from subiquitycore.controller import BaseController
from subiquitycore.ui.dummy import DummyView
from subiquity.models.installpath import InstallpathModel
from subiquity.ui.views import InstallpathView
from subiquity.ui.views import InstallpathView, MAASView
log = logging.getLogger('subiquity.controller.installpath')
class InstallpathController(BaseController):
signals = [
('menu:installpath:main', 'installpath'),
('installpath:install-ubuntu', 'install_ubuntu'),
# ('installpath:maas-region-server', 'install_maas_region_server'),
# ('installpath:maas-cluster-server', 'install_maas_cluster_server'),
# ('installpath:test-media', 'test_media'),
# ('installpath:test-memory', 'test_memory')
]
def __init__(self, common):
super().__init__(common)
self.model = InstallpathModel()
self.model = self.base_model.installpath
self.answers = self.all_answers.get("Installpath", {})
def installpath(self):
title = "Ubuntu %s"%(lsb_release.get_distro_information()['RELEASE'],)
@ -51,25 +42,62 @@ class InstallpathController(BaseController):
self.ui.set_header(title, excerpt)
self.ui.set_footer(footer)
self.ui.set_body(InstallpathView(self.model, self.signal))
self.ui.set_body(InstallpathView(self.model, self))
if 'path' in self.answers:
path = self.answers['path']
self.model.path = path
if path == 'ubuntu':
self.install_ubuntu()
else:
self.model.update(self.answers)
self.signal.emit_signal('next-screen')
default = installpath
def cancel(self):
self.signal.emit_signal('prev-screen')
def choose_path(self, path):
self.model.path = path
getattr(self, 'install_' + path)()
def install_ubuntu(self):
log.debug("Installing Ubuntu path chosen.")
self.signal.emit_signal('next-screen')
def install_maas_region_server(self):
self.ui.set_body(DummyView(self.signal))
def install_maas_region(self):
# show region questions, seed model
title = "Metal as a Service (MAAS) Regional Controller Setup"
excerpt = _(
"MAAS runs a software-defined data centre - it turns a "
"collection of physical servers and switches into a bare "
"metal cloud with full open source IP address management "
"(IPAM) and instant provisioning on demand. By choosing "
"to install MAAS, a MAAS Region Controller API server and "
"PostgreSQL database will be installed."
)
self.ui.set_header(title, excerpt)
self.ui.set_footer("")
self.ui.set_body(MAASView(self.model, self))
def install_maas_cluster_server(self):
self.ui.set_body(DummyView(self.signal))
def install_maas_rack(self):
# show cack questions, seed model
title = "Metal as a Service (MAAS) Rack Controller Setup"
excerpt = _(
"The MAAS rack controller (maas-rackd) provides highly available, fast "
"and local broadcast services to the machines provisioned by MAAS. You "
"need a MAAS rack controller attached to each fabric (which is a set of "
"trunked switches). You can attach multiple rack controllers to these "
"physical networks for high availability, with secondary rack controllers "
"automatically stepping to provide these services if the primary rack "
"controller fails. By choosing to install a MAAS Rack controller, you will "
"have to connect it to a Region controller to service your machines."
)
def test_media(self):
self.ui.set_body(DummyView(self.signal))
self.ui.set_header(title, excerpt)
self.ui.set_footer("")
self.ui.set_body(MAASView(self.model, self))
def test_memory(self):
self.ui.set_body(DummyView(self.signal))
def setup_maas(self, result):
self.model.update(result)
self.signal.emit_signal('next-screen')

View File

@ -33,6 +33,7 @@ class Subiquity(Application):
controllers = [
"Welcome",
"Keyboard",
"Installpath",
"Network",
"Filesystem",
"Identity",

View File

@ -26,16 +26,39 @@ class InstallpathModel(object):
('UI Text seen by user', <signal name>, <callback function string>)
"""
def _refresh_install_paths(self):
# TODO: Re-enable once available
self.install_paths = [
(_('Install Ubuntu'), 'installpath:install-ubuntu'),
# ('Install MAAS Region Server', 'installpath:maas-region-server'),
# ('Install MAAS Cluster Server', 'installpath:maas-cluster-server'),
# ('Test installation media', 'installpath:test-media'),
# ('Test machine memory', 'installpath:test-memory')
path = None
packages = {}
debconf = {}
@property
def paths(self):
return [
(_('Install Ubuntu'), 'ubuntu'),
(_('Install MAAS Region Controller'), 'maas_region'),
(_('Install MAAS Rack Controller'), 'maas_rack'),
]
def get_menu(self):
self._refresh_install_paths()
return self.install_paths
def update(self, results):
if self.path == 'ubuntu':
self.packages = {}
self.debconf = {}
elif self.path == 'maas_region':
self.packages = {'packages': ['maas']}
self.debconf['debconf_selections'] = {
'maas-username': 'maas-region-controller maas/username string %s' % results['username'],
'maas-password': 'maas-region-controller maas/password password %s' % results['password'],
}
elif self.path == 'maas_rack':
self.packages = {'packages': ['maas-rack-controller']}
self.debconf['debconf_selections'] = {
'maas-url': 'maas-rack-controller maas-rack-controller/maas-url string %s' % results['url'],
'maas-secret': 'maas-rack-controller maas-rack-controller/shared-secret password %s' % results['secret'],
}
else:
raise ValueError("invalid Installpath %s" % self.path)
def render(self):
return self.debconf
def render_cloudinit(self):
return self.packages

View File

@ -22,6 +22,7 @@ from subiquitycore.models.identity import IdentityModel
from subiquitycore.models.network import NetworkModel
from .filesystem import FilesystemModel
from .installpath import InstallpathModel
from .keyboard import KeyboardModel
from .locale import LocaleModel
@ -43,6 +44,7 @@ class SubiquityModel:
root = os.path.abspath(".subiquity")
self.locale = LocaleModel(common['signal'])
self.keyboard = KeyboardModel(root)
self.installpath = InstallpathModel()
self.network = NetworkModel()
self.filesystem = FilesystemModel(common['prober'])
self.identity = IdentityModel()
@ -66,10 +68,12 @@ class SubiquityModel:
if user.ssh_import_id is not None:
user_info['ssh_import_id'] = [user.ssh_import_id]
# XXX this should set up the locale too.
return {
config = {
'users': [user_info],
'hostname': self.identity.hostname,
}
config.update(self.installpath.render_cloudinit())
return config
def _cloud_init_files(self):
# TODO, this should be moved to the in-target cloud-config seed so on first
@ -130,5 +134,6 @@ class SubiquityModel:
}
config.update(self.network.render())
config.update(self.installpath.render())
return config

View File

@ -26,7 +26,7 @@ from .ceph import CephDiskView # NOQA
from .iscsi import IscsiDiskView # NOQA
from .lvm import LVMVolumeGroupView # NOQA
from .identity import IdentityView # NOQA
from .installpath import InstallpathView # NOQA
from .installpath import InstallpathView, MAASView # NOQA
from .installprogress import ProgressView # NOQA
from .keyboard import KeyboardView
from .welcome import WelcomeView

View File

@ -19,48 +19,143 @@ Provides high level options for Ubuntu install
"""
import logging
from urwid import BoxAdapter
import re
from urwid import connect_signal, BoxAdapter, Text
from subiquitycore.ui.lists import SimpleList
from subiquitycore.ui.buttons import back_btn, menu_btn
from subiquitycore.ui.interactive import StringEditor
from subiquitycore.ui.utils import Padding, button_pile
from subiquitycore.ui.container import ListBox, Pile
from subiquitycore.view import BaseView
from subiquity.ui.views.identity import UsernameField, PasswordField, USERNAME_MAXLEN
from subiquitycore.ui.form import (
simple_field,
Form,
WantsToKnowFormField,
)
log = logging.getLogger('subiquity.installpath')
class InstallpathView(BaseView):
def __init__(self, model, signal):
def __init__(self, model, controller):
self.model = model
self.signal = signal
self.controller = controller
self.items = []
back = back_btn(_("Back"), on_press=self.cancel)
self.body = [
Padding.center_79(self._build_model_inputs()),
Padding.line_break(""),
self._build_buttons(),
('pack', Text("")),
Padding.center_79(self._build_choices()),
('pack', Text("")),
('pack', button_pile([back])),
('pack', Text("")),
]
super().__init__(ListBox(self.body))
super().__init__(Pile(self.body))
def _build_buttons(self):
self.buttons = [
back_btn(on_press=self.cancel),
]
return button_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(
def _build_choices(self):
choices = []
for label, path in self.model.paths:
log.debug("Building inputs: {}".format(path))
choices.append(
menu_btn(
label=ipath, on_press=self.confirm, user_arg=sig))
label=label, on_press=self.confirm, user_arg=path))
return ListBox(choices)
return BoxAdapter(SimpleList(sl),
height=len(sl))
def confirm(self, result, sig):
self.signal.emit_signal(sig)
def confirm(self, sender, path):
self.controller.choose_path(path)
def cancel(self, button=None):
self.signal.emit_signal('prev-screen')
self.controller.cancel()
class URLEditor(StringEditor, WantsToKnowFormField):
pass
URLField = simple_field(URLEditor)
class RegionForm(Form):
username = UsernameField(
_("Pick a MAAS username:"),
help=_("MAAS requires an administrative account to be created before you can use MAAS."))
password = PasswordField(
_("Choose a password:"),
help=_("Please enter the password for the MAAS administrator's account."))
def validate_username(self):
if len(self.username.value) < 1:
return _("Username missing")
if len(self.username.value) > USERNAME_MAXLEN:
return _("Username too long, must be < ") + str(USERNAME_MAXLEN)
if not re.match(r'[a-z_][a-z0-9_-]*', self.username.value):
return _("Username must match NAME_REGEX, i.e. [a-z_][a-z0-9_-]*")
def validate_password(self):
if len(self.password.value) < 1:
return _("Password must be set")
class RackForm(Form):
url = URLField(
_("Ubuntu MAAS Region API address:"),
help=_(
"The MAAS rack controller and nodes need to contact "
"the MAAS region controller API. Set the URL at which "
"they can reach the MAAS API remotely, e.g. \"http://192.168.1.1:5240/MAAS\" "
"Since nodes must be able to access this URL, localhost or 127.0.0.1 are not "
"useful values here."))
secret = PasswordField(
_("MAAS Rack Controller shared secret:"),
help=_(
"The MAAS rack controller needs to contact the MAAS region "
"controller with the shared secret found in /var/lib/maas/secret "
"on the region controller."))
def validate_url(self):
if len(self.url.value) < 1:
return _("API address must be set")
def validate_secret(self):
if len(self.secret.value) < 1:
return _("Secret must be set")
class MAASView(BaseView):
def __init__(self, model, controller):
self.model = model
self.controller = controller
self.signal = controller.signal
self.items = []
if self.model.path == "maas_region":
self.form = RegionForm()
elif self.model.path == "maas_rack":
self.form = RackForm()
else:
raise ValueError("invalid MAAS form %s" % self.model.path)
connect_signal(self.form, 'submit', self.done)
connect_signal(self.form, 'cancel', self.cancel)
body = Pile([
('pack', Text("")),
Padding.center_90(ListBox([self.form.as_rows(self)])),
('pack', Pile([
('pack', Text("")),
self.form.buttons,
('pack', Text("")),
], focus_item=1)),
])
super().__init__(body)
def done(self, result):
log.debug("User input: {}".format(result.as_data()))
self.controller.setup_maas(result.as_data())
def cancel(self, result=None):
self.controller.default()