489 lines
18 KiB
Python
489 lines
18 KiB
Python
# -*- coding: utf8 -*-
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import logging
|
|
import six
|
|
import uuid
|
|
|
|
from django.conf import settings
|
|
import django.forms
|
|
from django.utils.translation import ugettext_lazy as _
|
|
import horizon.exceptions
|
|
import horizon.forms
|
|
import horizon.messages
|
|
from os_cloud_config import keystone as keystone_config
|
|
from os_cloud_config.utils import clients
|
|
|
|
from tuskar_ui import api
|
|
import tuskar_ui.api.heat
|
|
import tuskar_ui.api.tuskar
|
|
import tuskar_ui.forms
|
|
import tuskar_ui.infrastructure.flavors.utils as flavors_utils
|
|
import tuskar_ui.utils.utils as tuskar_utils
|
|
|
|
MATCHING_DEPLOYMENT_MODE = flavors_utils.matching_deployment_mode()
|
|
LOG = logging.getLogger(__name__)
|
|
MESSAGE_ICONS = {
|
|
'ok': 'fa-check-square-o text-success',
|
|
'pending': 'fa-square-o text-info',
|
|
'error': 'fa-exclamation-circle text-danger',
|
|
'warning': 'fa-exclamation-triangle text-warning',
|
|
None: 'fa-exclamation-triangle text-warning',
|
|
}
|
|
WEBROOT = getattr(settings, 'WEBROOT', '/')
|
|
|
|
|
|
def validate_roles(request, plan):
|
|
"""Validates the roles in plan and returns dict describing the issues"""
|
|
for role in plan.role_list:
|
|
if (
|
|
plan.get_role_node_count(role) and
|
|
not role.is_valid_for_deployment(plan)
|
|
):
|
|
message = {
|
|
'text': _(u"Configure Roles."),
|
|
'is_critical': True,
|
|
'status': 'pending',
|
|
}
|
|
break
|
|
else:
|
|
message = {
|
|
'text': _(u"Configure Roles."),
|
|
'status': 'ok',
|
|
}
|
|
return message
|
|
|
|
|
|
def validate_global_parameters(request, plan):
|
|
pending_required_global_params = list(
|
|
api.tuskar.Parameter.pending_parameters(
|
|
api.tuskar.Parameter.required_parameters(
|
|
api.tuskar.Parameter.global_parameters(
|
|
plan.parameter_list()))))
|
|
if pending_required_global_params:
|
|
message = {
|
|
'text': _(u"Global Service Configuration."),
|
|
'is_critical': True,
|
|
'status': 'pending',
|
|
}
|
|
else:
|
|
message = {
|
|
'text': _(u"Global Service Configuration."),
|
|
'status': 'ok',
|
|
}
|
|
return message
|
|
|
|
|
|
def validate_plan(request, plan):
|
|
"""Validates the plan and returns a list of dicts describing the issues."""
|
|
messages = []
|
|
requested_nodes = 0
|
|
for role in plan.role_list:
|
|
node_count = plan.get_role_node_count(role)
|
|
requested_nodes += node_count
|
|
available_flavors = len(api.flavor.Flavor.list(request))
|
|
if available_flavors == 0:
|
|
messages.append({
|
|
'text': _(u"Define Flavors."),
|
|
'is_critical': True,
|
|
'status': 'pending',
|
|
})
|
|
else:
|
|
messages.append({
|
|
'text': _(u"Define Flavors."),
|
|
'status': 'ok',
|
|
})
|
|
available_nodes = len(api.node.Node.list(request, associated=False,
|
|
maintenance=False))
|
|
if available_nodes == 0:
|
|
messages.append({
|
|
'text': _(u"Register Nodes."),
|
|
'is_critical': True,
|
|
'status': 'pending',
|
|
})
|
|
elif requested_nodes > available_nodes:
|
|
messages.append({
|
|
'text': _(u"Not enough registered nodes for this plan. "
|
|
u"You need {0} more.").format(
|
|
requested_nodes - available_nodes),
|
|
'is_critical': True,
|
|
'status': 'error',
|
|
})
|
|
else:
|
|
messages.append({
|
|
'text': _(u"Register Nodes."),
|
|
'status': 'ok',
|
|
})
|
|
messages.append(validate_roles(request, plan))
|
|
messages.append(validate_global_parameters(request, plan))
|
|
if not MATCHING_DEPLOYMENT_MODE:
|
|
# All roles have to have the same flavor.
|
|
default_flavor_name = api.flavor.Flavor.list(request)[0].name
|
|
for role in plan.role_list:
|
|
if role.flavor(plan).name != default_flavor_name:
|
|
messages.append({
|
|
'text': _(u"Role {0} doesn't use default flavor.").format(
|
|
role.name,
|
|
),
|
|
'is_critical': False,
|
|
'statis': 'error',
|
|
})
|
|
roles_assigned = True
|
|
messages.append({
|
|
'text': _(u"Assign roles."),
|
|
'status': lambda: 'ok' if roles_assigned else 'pending',
|
|
})
|
|
try:
|
|
controller_role = plan.get_role_by_name("Controller")
|
|
except KeyError:
|
|
messages.append({
|
|
'text': _(u"Controller Role Needed."),
|
|
'is_critical': True,
|
|
'status': 'error',
|
|
'indent': 1,
|
|
})
|
|
roles_assigned = False
|
|
else:
|
|
if plan.get_role_node_count(controller_role) not in (1, 3):
|
|
messages.append({
|
|
'text': _(u"1 or 3 Controllers Needed."),
|
|
'is_critical': True,
|
|
'status': 'pending',
|
|
'indent': 1,
|
|
})
|
|
roles_assigned = False
|
|
else:
|
|
messages.append({
|
|
'text': _(u"1 or 3 Controllers Needed."),
|
|
'status': 'ok',
|
|
'indent': 1,
|
|
})
|
|
|
|
try:
|
|
compute_role = plan.get_role_by_name("Compute")
|
|
except KeyError:
|
|
messages.append({
|
|
'text': _(u"Compute Role Needed."),
|
|
'is_critical': True,
|
|
'status': 'error',
|
|
'indent': 1,
|
|
})
|
|
roles_assigned = False
|
|
else:
|
|
if plan.get_role_node_count(compute_role) < 1:
|
|
messages.append({
|
|
'text': _(u"1 Compute Needed."),
|
|
'is_critical': True,
|
|
'status': 'pending',
|
|
'indent': 1,
|
|
})
|
|
roles_assigned = False
|
|
else:
|
|
messages.append({
|
|
'text': _(u"1 Compute Needed."),
|
|
'status': 'ok',
|
|
'indent': 1,
|
|
})
|
|
for message in messages:
|
|
status = message.get('status')
|
|
if callable(status):
|
|
message['status'] = status = status()
|
|
message['classes'] = MESSAGE_ICONS.get(status, MESSAGE_ICONS[None])
|
|
return messages
|
|
|
|
|
|
class EditPlan(horizon.forms.SelfHandlingForm):
|
|
def __init__(self, *args, **kwargs):
|
|
super(EditPlan, self).__init__(*args, **kwargs)
|
|
self.plan = api.tuskar.Plan.get_the_plan(self.request)
|
|
self.fields.update(self._role_count_fields(self.plan))
|
|
|
|
def _role_count_fields(self, plan):
|
|
fields = {}
|
|
for role in plan.role_list:
|
|
field = django.forms.IntegerField(
|
|
label=role.name,
|
|
widget=tuskar_ui.forms.NumberPickerInput(attrs={
|
|
'min': 1 if role.name in ('Controller', 'Compute') else 0,
|
|
'step': 2 if role.name == 'Controller' else 1,
|
|
}),
|
|
initial=plan.get_role_node_count(role),
|
|
required=False
|
|
)
|
|
field.role = role
|
|
fields['%s-count' % role.id] = field
|
|
return fields
|
|
|
|
def handle(self, request, data):
|
|
parameters = dict(
|
|
(field.role.node_count_parameter_name, data[name])
|
|
for (name, field) in self.fields.items() if name.endswith('-count')
|
|
)
|
|
# NOTE(gfidente): this is a bad hack meant to magically add the
|
|
# parameter which enables Neutron L3 HA when the number of
|
|
# Controllers is > 1
|
|
try:
|
|
controller_role = self.plan.get_role_by_name('Controller')
|
|
compute_role = self.plan.get_role_by_name('Compute')
|
|
except Exception as e:
|
|
LOG.warning('Unable to find a required role: %s', e.message)
|
|
else:
|
|
number_controllers = parameters[
|
|
controller_role.node_count_parameter_name]
|
|
if number_controllers > 1:
|
|
for role in [controller_role, compute_role]:
|
|
l3ha_param = role.parameter_prefix + 'NeutronL3HA'
|
|
parameters[l3ha_param] = 'True'
|
|
l3agent_param = (role.parameter_prefix +
|
|
'NeutronAllowL3AgentFailover')
|
|
parameters[l3agent_param] = 'True'
|
|
dhcp_agents_per_net = (number_controllers if number_controllers and
|
|
number_controllers > 3 else 3)
|
|
dhcp_agents_param = (controller_role.parameter_prefix +
|
|
'NeutronDhcpAgentsPerNetwork')
|
|
parameters[dhcp_agents_param] = dhcp_agents_per_net
|
|
|
|
try:
|
|
ceph_storage_role = self.plan.get_role_by_name('Ceph-Storage')
|
|
except Exception as e:
|
|
LOG.warning('Unable to find role: %s', 'Ceph-Storage')
|
|
else:
|
|
if parameters[ceph_storage_role.node_count_parameter_name] > 0:
|
|
parameters.update({
|
|
'CephClusterFSID': six.text_type(uuid.uuid4()),
|
|
'CephMonKey': tuskar_utils.create_cephx_key(),
|
|
'CephAdminKey': tuskar_utils.create_cephx_key()
|
|
})
|
|
|
|
cinder_enable_rbd_param = (controller_role.parameter_prefix
|
|
+ 'CinderEnableRbdBackend')
|
|
glance_backend_param = (controller_role.parameter_prefix +
|
|
'GlanceBackend')
|
|
nova_enable_rbd_param = (compute_role.parameter_prefix +
|
|
'NovaEnableRbdBackend')
|
|
cinder_enable_iscsi_param = (
|
|
controller_role.parameter_prefix +
|
|
'CinderEnableIscsiBackend')
|
|
|
|
parameters.update({
|
|
cinder_enable_rbd_param: True,
|
|
glance_backend_param: 'rbd',
|
|
nova_enable_rbd_param: True,
|
|
cinder_enable_iscsi_param: False
|
|
})
|
|
|
|
try:
|
|
self.plan = self.plan.patch(request, self.plan.uuid, parameters)
|
|
except Exception as e:
|
|
horizon.exceptions.handle(request, _("Unable to update the plan."))
|
|
LOG.exception(e)
|
|
return False
|
|
return True
|
|
|
|
|
|
class ScaleOut(EditPlan):
|
|
def __init__(self, *args, **kwargs):
|
|
super(ScaleOut, self).__init__(*args, **kwargs)
|
|
for name, field in self.fields.items():
|
|
if name.endswith('-count'):
|
|
field.widget.attrs['min'] = field.initial
|
|
|
|
def handle(self, request, data):
|
|
if not super(ScaleOut, self).handle(request, data):
|
|
return False
|
|
plan = self.plan
|
|
try:
|
|
stack = api.heat.Stack.get_by_plan(self.request, plan)
|
|
stack.update(request, plan.name, plan.templates)
|
|
except Exception as e:
|
|
LOG.exception(e)
|
|
if hasattr(e, 'error'):
|
|
horizon.exceptions.handle(
|
|
request,
|
|
_(
|
|
"Unable to deploy overcloud. Reason: {0}"
|
|
).format(e.error['error']['message']),
|
|
)
|
|
return False
|
|
else:
|
|
raise
|
|
else:
|
|
msg = _('Deployment in progress.')
|
|
horizon.messages.success(request, msg)
|
|
return True
|
|
|
|
|
|
class DeployOvercloud(horizon.forms.SelfHandlingForm):
|
|
network_isolation = horizon.forms.BooleanField(
|
|
label=_("Enable Network Isolation"),
|
|
required=False)
|
|
|
|
def handle(self, request, data):
|
|
try:
|
|
plan = api.tuskar.Plan.get_the_plan(request)
|
|
except Exception as e:
|
|
LOG.exception(e)
|
|
horizon.exceptions.handle(request,
|
|
_("Unable to deploy overcloud."))
|
|
return False
|
|
|
|
# If network isolation selected, read environment file data
|
|
# and add to plan
|
|
env_temp = '/usr/share/openstack-tripleo-heat-templates/environments'
|
|
try:
|
|
if self.cleaned_data['network_isolation']:
|
|
with open(env_temp, 'r') as env_file:
|
|
env_contents = ''.join(
|
|
[line for line in
|
|
env_file.readlines() if '#' not in line]
|
|
)
|
|
plan.environment += env_contents
|
|
except Exception as e:
|
|
LOG.exception(e)
|
|
pass
|
|
|
|
# Auto-generate missing passwords and certificates
|
|
if plan.list_generated_parameters():
|
|
generated_params = plan.make_generated_parameters()
|
|
plan = plan.patch(request, plan.uuid, generated_params)
|
|
|
|
# Validate plan and create stack
|
|
for message in validate_plan(request, plan):
|
|
if message.get('is_critical'):
|
|
horizon.messages.success(request, message.text)
|
|
return False
|
|
try:
|
|
stack = api.heat.Stack.get_by_plan(self.request, plan)
|
|
if not stack:
|
|
api.heat.Stack.create(request, plan.name, plan.templates)
|
|
except Exception as e:
|
|
LOG.exception(e)
|
|
horizon.exceptions.handle(
|
|
request, _("Unable to deploy overcloud. Reason: {0}").format(
|
|
e.error['error']['message']))
|
|
return False
|
|
else:
|
|
msg = _('Deployment in progress.')
|
|
horizon.messages.success(request, msg)
|
|
return True
|
|
|
|
|
|
class UndeployOvercloud(horizon.forms.SelfHandlingForm):
|
|
def handle(self, request, data):
|
|
try:
|
|
plan = api.tuskar.Plan.get_the_plan(request)
|
|
stack = api.heat.Stack.get_by_plan(self.request, plan)
|
|
if stack:
|
|
api.heat.Stack.delete(request, stack.id)
|
|
except Exception as e:
|
|
LOG.exception(e)
|
|
horizon.exceptions.handle(request,
|
|
_("Unable to undeploy overcloud."))
|
|
return False
|
|
else:
|
|
msg = _('Undeployment in progress.')
|
|
horizon.messages.success(request, msg)
|
|
return True
|
|
|
|
|
|
class PostDeployInit(horizon.forms.SelfHandlingForm):
|
|
admin_email = horizon.forms.CharField(label=_("Admin Email"))
|
|
public_host = horizon.forms.CharField(
|
|
label=_("Public Host"), initial="", required=False)
|
|
region = horizon.forms.CharField(
|
|
label=_("Region"), initial="regionOne")
|
|
|
|
def build_endpoints(self, plan, controller_role):
|
|
return {
|
|
"ceilometer": {
|
|
"password": plan.parameter_value(
|
|
controller_role.parameter_prefix + 'CeilometerPassword')},
|
|
"cinder": {
|
|
"password": plan.parameter_value(
|
|
controller_role.parameter_prefix + 'CinderPassword')},
|
|
"cinderv2": {
|
|
"password": plan.parameter_value(
|
|
controller_role.parameter_prefix + 'CinderPassword')},
|
|
"ec2": {
|
|
"password": plan.parameter_value(
|
|
controller_role.parameter_prefix + 'GlancePassword')},
|
|
"glance": {
|
|
"password": plan.parameter_value(
|
|
controller_role.parameter_prefix + 'GlancePassword')},
|
|
"heat": {
|
|
"password": plan.parameter_value(
|
|
controller_role.parameter_prefix + 'HeatPassword')},
|
|
"neutron": {
|
|
"password": plan.parameter_value(
|
|
controller_role.parameter_prefix + 'NeutronPassword')},
|
|
"nova": {
|
|
"password": plan.parameter_value(
|
|
controller_role.parameter_prefix + 'NovaPassword')},
|
|
"novav3": {
|
|
"password": plan.parameter_value(
|
|
controller_role.parameter_prefix + 'NovaPassword')},
|
|
"swift": {
|
|
"password": plan.parameter_value(
|
|
controller_role.parameter_prefix + 'SwiftPassword'),
|
|
'path': '/v1/AUTH_%(tenant_id)s',
|
|
'admin_path': '/v1'},
|
|
"horizon": {
|
|
'port': '80',
|
|
'path': WEBROOT,
|
|
'admin_path': '%sadmin' % WEBROOT}}
|
|
|
|
def handle(self, request, data):
|
|
try:
|
|
plan = api.tuskar.Plan.get_the_plan(request)
|
|
controller_role = plan.get_role_by_name("Controller")
|
|
stack = api.heat.Stack.get_by_plan(self.request, plan)
|
|
|
|
admin_token = plan.parameter_value(
|
|
controller_role.parameter_prefix + 'AdminToken')
|
|
admin_password = plan.parameter_value(
|
|
controller_role.parameter_prefix + 'AdminPassword')
|
|
admin_email = data['admin_email']
|
|
auth_ip = stack.keystone_ip
|
|
auth_url = stack.keystone_auth_url
|
|
auth_tenant = 'admin'
|
|
auth_user = 'admin'
|
|
|
|
# do the keystone init
|
|
keystone_config.initialize(
|
|
auth_ip, admin_token, admin_email, admin_password,
|
|
region='regionOne', ssl=None, public=None, user='heat-admin',
|
|
pki_setup=False)
|
|
|
|
# retrieve needed Overcloud clients
|
|
keystone_client = clients.get_keystone_client(
|
|
auth_user, admin_password, auth_tenant, auth_url)
|
|
|
|
# do the setup endpoints
|
|
keystone_config.setup_endpoints(
|
|
self.build_endpoints(plan, controller_role),
|
|
public_host=data['public_host'],
|
|
region=data['region'],
|
|
os_auth_url=auth_url,
|
|
client=keystone_client)
|
|
|
|
except Exception as e:
|
|
LOG.exception(e)
|
|
horizon.exceptions.handle(request,
|
|
_("Unable to initialize Overcloud."))
|
|
return False
|
|
else:
|
|
msg = _('Overcloud has been initialized.')
|
|
horizon.messages.success(request, msg)
|
|
return True
|