Merge pull request #275 from CanonicalLtd/maas-option
Re-add installpath controller, with added MAAS views.
This commit is contained in:
commit
37d93a1954
|
@ -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:
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -33,6 +33,7 @@ class Subiquity(Application):
|
||||||
controllers = [
|
controllers = [
|
||||||
"Welcome",
|
"Welcome",
|
||||||
"Keyboard",
|
"Keyboard",
|
||||||
|
"Installpath",
|
||||||
"Network",
|
"Network",
|
||||||
"Filesystem",
|
"Filesystem",
|
||||||
"Identity",
|
"Identity",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in New Issue