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 lang: en_US
Keyboard: Keyboard:
layout: us layout: us
Installpath:
path: ubuntu
Network: Network:
accept-default: yes accept-default: yes
Filesystem: Filesystem:

View File

@ -18,27 +18,18 @@ import logging
import lsb_release import lsb_release
from subiquitycore.controller import BaseController from subiquitycore.controller import BaseController
from subiquitycore.ui.dummy import DummyView
from subiquity.models.installpath import InstallpathModel from subiquity.ui.views import InstallpathView, MAASView
from subiquity.ui.views import InstallpathView
log = logging.getLogger('subiquity.controller.installpath') log = logging.getLogger('subiquity.controller.installpath')
class InstallpathController(BaseController): 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): def __init__(self, common):
super().__init__(common) super().__init__(common)
self.model = InstallpathModel() self.model = self.base_model.installpath
self.answers = self.all_answers.get("Installpath", {})
def installpath(self): def installpath(self):
title = "Ubuntu %s"%(lsb_release.get_distro_information()['RELEASE'],) 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_header(title, excerpt)
self.ui.set_footer(footer) 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 default = installpath
def cancel(self): def cancel(self):
self.signal.emit_signal('prev-screen') self.signal.emit_signal('prev-screen')
def choose_path(self, path):
self.model.path = path
getattr(self, 'install_' + path)()
def install_ubuntu(self): def install_ubuntu(self):
log.debug("Installing Ubuntu path chosen.") log.debug("Installing Ubuntu path chosen.")
self.signal.emit_signal('next-screen') self.signal.emit_signal('next-screen')
def install_maas_region_server(self): def install_maas_region(self):
self.ui.set_body(DummyView(self.signal)) # 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): def install_maas_rack(self):
self.ui.set_body(DummyView(self.signal)) # 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_header(title, excerpt)
self.ui.set_body(DummyView(self.signal)) self.ui.set_footer("")
self.ui.set_body(MAASView(self.model, self))
def test_memory(self): def setup_maas(self, result):
self.ui.set_body(DummyView(self.signal)) self.model.update(result)
self.signal.emit_signal('next-screen')

View File

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

View File

@ -26,16 +26,39 @@ class InstallpathModel(object):
('UI Text seen by user', <signal name>, <callback function string>) ('UI Text seen by user', <signal name>, <callback function string>)
""" """
def _refresh_install_paths(self): path = None
# TODO: Re-enable once available packages = {}
self.install_paths = [ debconf = {}
(_('Install Ubuntu'), 'installpath:install-ubuntu'),
# ('Install MAAS Region Server', 'installpath:maas-region-server'), @property
# ('Install MAAS Cluster Server', 'installpath:maas-cluster-server'), def paths(self):
# ('Test installation media', 'installpath:test-media'), return [
# ('Test machine memory', 'installpath:test-memory') (_('Install Ubuntu'), 'ubuntu'),
(_('Install MAAS Region Controller'), 'maas_region'),
(_('Install MAAS Rack Controller'), 'maas_rack'),
] ]
def get_menu(self): def update(self, results):
self._refresh_install_paths() if self.path == 'ubuntu':
return self.install_paths 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 subiquitycore.models.network import NetworkModel
from .filesystem import FilesystemModel from .filesystem import FilesystemModel
from .installpath import InstallpathModel
from .keyboard import KeyboardModel from .keyboard import KeyboardModel
from .locale import LocaleModel from .locale import LocaleModel
@ -43,6 +44,7 @@ class SubiquityModel:
root = os.path.abspath(".subiquity") root = os.path.abspath(".subiquity")
self.locale = LocaleModel(common['signal']) self.locale = LocaleModel(common['signal'])
self.keyboard = KeyboardModel(root) self.keyboard = KeyboardModel(root)
self.installpath = InstallpathModel()
self.network = NetworkModel() self.network = NetworkModel()
self.filesystem = FilesystemModel(common['prober']) self.filesystem = FilesystemModel(common['prober'])
self.identity = IdentityModel() self.identity = IdentityModel()
@ -66,10 +68,12 @@ class SubiquityModel:
if user.ssh_import_id is not None: if user.ssh_import_id is not None:
user_info['ssh_import_id'] = [user.ssh_import_id] user_info['ssh_import_id'] = [user.ssh_import_id]
# XXX this should set up the locale too. # XXX this should set up the locale too.
return { config = {
'users': [user_info], 'users': [user_info],
'hostname': self.identity.hostname, 'hostname': self.identity.hostname,
} }
config.update(self.installpath.render_cloudinit())
return config
def _cloud_init_files(self): def _cloud_init_files(self):
# TODO, this should be moved to the in-target cloud-config seed so on first # 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.network.render())
config.update(self.installpath.render())
return config return config

View File

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

View File

@ -19,48 +19,143 @@ Provides high level options for Ubuntu install
""" """
import logging import logging
from urwid import BoxAdapter import re
from urwid import connect_signal, BoxAdapter, Text
from subiquitycore.ui.lists import SimpleList from subiquitycore.ui.lists import SimpleList
from subiquitycore.ui.buttons import back_btn, menu_btn 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.utils import Padding, button_pile
from subiquitycore.ui.container import ListBox, Pile from subiquitycore.ui.container import ListBox, Pile
from subiquitycore.view import BaseView 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') log = logging.getLogger('subiquity.installpath')
class InstallpathView(BaseView): class InstallpathView(BaseView):
def __init__(self, model, signal): def __init__(self, model, controller):
self.model = model self.model = model
self.signal = signal self.controller = controller
self.items = [] self.items = []
back = back_btn(_("Back"), on_press=self.cancel)
self.body = [ self.body = [
Padding.center_79(self._build_model_inputs()), ('pack', Text("")),
Padding.line_break(""), Padding.center_79(self._build_choices()),
self._build_buttons(), ('pack', Text("")),
('pack', button_pile([back])),
('pack', Text("")),
] ]
super().__init__(ListBox(self.body)) super().__init__(Pile(self.body))
def _build_buttons(self): def _build_choices(self):
self.buttons = [ choices = []
back_btn(on_press=self.cancel), for label, path in self.model.paths:
] log.debug("Building inputs: {}".format(path))
return button_pile(self.buttons) choices.append(
def _build_model_inputs(self):
sl = []
for ipath, sig in self.model.get_menu():
log.debug("Building inputs: {}".format(ipath))
sl.append(
menu_btn( 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), def confirm(self, sender, path):
height=len(sl)) self.controller.choose_path(path)
def confirm(self, result, sig):
self.signal.emit_signal(sig)
def cancel(self, button=None): 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()