Making init keystone work

Calling os-cloud-config to init keystone.

Unmocking part of Heat calls, relation to plan will
not work unless we actually deploy a template created
by Tuskar-API. So mocking a relation and returning
always the first plan from plan-list.

Depends on https://review.openstack.org/#/c/113883/

Change-Id: I0a17eada05265d2bc4cc1ef6a611936522e9c6e9
This commit is contained in:
Ladislav Smola 2014-08-20 18:37:55 +02:00
parent f430370d8d
commit 4301fee899
11 changed files with 260 additions and 33 deletions

View File

@ -105,7 +105,7 @@ class Stack(base.APIResourceWrapper):
are none
:rtype: list of tuskar_ui.api.heat.Stack
"""
stacks = mock_heat.Stack.list()
stacks, has_more_data, has_prev_data = heat.stacks_list(request)
return [cls(stack, request=request) for stack in stacks]
@classmethod
@ -118,7 +118,7 @@ class Stack(base.APIResourceWrapper):
found
:rtype: tuskar_ui.api.heat.Stack or None
"""
return cls(mock_heat.Stack.get(stack_id))
return cls(heat.stack_get(request, stack_id), request=request)
@classmethod
@handle_errors(_("Unable to retrieve stack"))
@ -130,9 +130,18 @@ class Stack(base.APIResourceWrapper):
found
:rtype: tuskar_ui.api.heat.Stack or None
"""
for stack in Stack.list(request):
if stack.plan and (stack.plan.id == plan.id):
return stack
# TODO(lsmola) until we have working deployment through Tuskar-API,
# this will not work
#for stack in Stack.list(request):
# if stack.plan and (stack.plan.id == plan.id):
# return stack
try:
stack = Stack.list(request)[0]
except IndexError:
return None
# TODO(lsmola) stack list actually does not contain all the detail
# info, there should be call for that, investigate
return Stack.get(request, stack.id)
@classmethod
@handle_errors(_("Unable to delete Heat stack"), [])
@ -221,9 +230,27 @@ class Stack(base.APIResourceWrapper):
exists as well; None otherwise
:rtype: tuskar_ui.api.tuskar.OvercloudPlan
"""
if 'plan_id' in self.parameters:
return tuskar.OvercloudPlan.get(self._request,
self.parameters['plan_id'])
# TODO(lsmola) replace this by actual reference, I am pretty sure
# the relation won't be stored in parameters, that would mean putting
# that into template, which doesn't make sense
#if 'plan_id' in self.parameters:
# return tuskar.OvercloudPlan.get(self._request,
# self.parameters['plan_id'])
try:
plan = tuskar.OvercloudPlan.list(self._request)[0]
except IndexError:
return None
return plan
@cached_property
def is_initialized(self):
"""Check if this Stack is successfully initialized.
:return: True if this Stack is successfully initialized, False
otherwise
:rtype: bool
"""
return len(self.dashboard_urls) > 0
@cached_property
def is_deployed(self):
@ -290,22 +317,22 @@ class Stack(base.APIResourceWrapper):
return getattr(self, 'outputs', [])
@cached_property
def keystone_ip(self):
def keystone_auth_url(self):
for output in self.stack_outputs:
if output['output_key'] == 'KeystoneURL':
return urlparse.urlparse(output['output_value']).hostname
return output['output_value']
@cached_property
def keystone_ip(self):
if self.keystone_auth_url:
return urlparse.urlparse(self.keystone_auth_url).hostname
@cached_property
def overcloud_keystone(self):
for output in self.stack_outputs:
if output['output_key'] == 'KeystoneURL':
break
else:
return None
try:
return overcloud_keystoneclient(
self._request,
output['output_value'],
self.keystone_auth_url,
self.plan.parameter_value('AdminPassword'))
except Exception:
LOG.debug('Unable to connect to overcloud keystone.')

View File

@ -19,6 +19,7 @@ from django.core import urlresolvers
from mock import patch, call # noqa
from horizon import exceptions as horizon_exceptions
from openstack_dashboard.test import helpers
from openstack_dashboard.test.test_data import utils
from tuskar_ui import api
@ -44,6 +45,10 @@ class NodesTests(test.BaseAdminViewTests, helpers.APITestCase):
def _raise_tuskar_exception(self, request, *args, **kwargs):
raise self.exceptions.tuskar
@handle_errors("Error!", [])
def _raise_horizon_exception_not_found(self, request, *args, **kwargs):
raise horizon_exceptions.NotFound
def test_index_get(self):
with patch('tuskar_ui.api.node.Node', **{
@ -90,7 +95,12 @@ class NodesTests(test.BaseAdminViewTests, helpers.APITestCase):
'spec_set': ['image_get'],
'image_get.return_value': image,
}),
) as (_OvercloudRole, Node, _nova, _glance):
patch('tuskar_ui.api.heat.Resource', **{
'spec_set': ['get_by_node'], # Only allow these attributes
'get_by_node.side_effect': (
self._raise_horizon_exception_not_found),
}),
) as (_OvercloudRole, Node, _nova, _glance, _resource):
res = self.client.get(INDEX_URL + '?tab=nodes__registered')
# FIXME(lsmola) horrible count, optimize
self.assertEqual(Node.list.call_count, 6)
@ -240,14 +250,21 @@ class NodesTests(test.BaseAdminViewTests, helpers.APITestCase):
def test_node_detail(self):
node = api.node.Node(self.ironicclient_nodes.list()[0])
with patch('tuskar_ui.api.node.Node', **{
'spec_set': ['get'], # Only allow these attributes
'get.return_value': node,
}) as mock:
with contextlib.nested(
patch('tuskar_ui.api.node.Node', **{
'spec_set': ['get'], # Only allow these attributes
'get.return_value': node,
}),
patch('tuskar_ui.api.heat.Resource', **{
'spec_set': ['get_by_node'], # Only allow these attributes
'get_by_node.side_effect': (
self._raise_horizon_exception_not_found),
}),
) as (mock_node, mock_heat):
res = self.client.get(
urlresolvers.reverse(DETAIL_VIEW, args=(node.uuid,))
)
self.assertEqual(mock.get.call_count, 1)
self.assertEqual(mock_node.get.call_count, 1)
self.assertTemplateUsed(res, 'infrastructure/nodes/details.html')
self.assertEqual(res.context['node'], node)

View File

@ -19,6 +19,7 @@ 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_setup
from tuskar_ui import api
import tuskar_ui.api.heat
@ -92,3 +93,64 @@ class UndeployOvercloud(horizon.forms.SelfHandlingForm):
msg = _('Undeployment in progress.')
horizon.messages.success(request, msg)
return True
class PostDeployInit(horizon.forms.SelfHandlingForm):
def build_endpoints(self, plan):
return {
"ceilometer": {
"password": plan.parameter_value('CeilometerPassword')},
"cinder": {
"password": plan.parameter_value('CinderPassword')},
"ec2": {
"password": plan.parameter_value('GlancePassword')},
"glance": {
"password": plan.parameter_value('GlancePassword')},
"heat": {
"password": plan.parameter_value('HeatPassword')},
"neutron": {
"password": plan.parameter_value('NeutronPassword')},
"nova": {
"password": plan.parameter_value('NovaPassword')},
"novav3": {
"password": plan.parameter_value('NovaPassword')},
"swift": {
"password": plan.parameter_value('SwiftPassword')},
"horizon": {}}
def handle(self, request, data):
try:
plan = api.tuskar.OvercloudPlan.get_the_plan(request)
stack = api.heat.Stack.get_by_plan(self.request, plan)
admin_token = plan.parameter_value('AdminToken')
admin_password = plan.parameter_value('AdminPassword')
admin_email = 'example@example.org'
auth_ip = stack.keystone_ip
auth_url = stack.keystone_auth_url
auth_tenant = 'admin'
auth_user = 'admin'
# do the keystone init
keystone_setup.initialize(
auth_ip, admin_token, admin_email, admin_password,
region='regionOne', ssl=None, public=None, user='heat-admin')
# do the setup endpoints
keystone_setup.setup_endpoints(
self.build_endpoints(plan), public_host=None, region=None,
os_username=auth_user, os_password=admin_password,
os_tenant_name=auth_tenant, os_auth_url=auth_url)
# do the neutron init
# TODO(lsmola) neutron needs to be prepared in os-cloud-config
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

View File

@ -49,8 +49,27 @@
</div>
{% endif %}
{% if not dashboard_urls and not stack.is_deploying and not stack.is_failed %}
Your OpenStack cloud is deployed, but it needs to be initialized.
{% if stack.is_deployed and not stack.is_initialized %}
<div class="row">
<div class="col-xs-12">
<div class="alert alert-info">
<div class="row">
<div class="col-xs-2">
<span class="text-success" style="font-size: x-large; vertical-align:middle">
<i class="glyphicon glyphicon-warning-sign"></i>
</span>
</div>
<div class="col-xs-10">
<p>{% trans "Your OpenStack cloud is deployed but it needs to get initialized in order to get live." %}</p>
<a href="{% url 'horizon:infrastructure:overview:post_deploy_init' %}"
class="btn btn-primary ajax-modal">
{% trans "Initialize" %}
</a>
</div>
</div>
</div>
</div>
</div>
{% else %}
Deployment is live
<div class="widget">
@ -65,6 +84,8 @@
</div>
{% endif %}
<hr>
<a href="{% url 'horizon:infrastructure:overview:undeploy_confirmation' %}"
class="btn btn-danger ajax-modal">
<i class="glyphicon glyphicon-fire"></i>

View File

@ -0,0 +1,26 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% load url from future %}
{% block form_id %}post_deploy_init_form{% endblock %}
{% block form_action %}{% url 'horizon:infrastructure:overview:post_deploy_init' %}{% endblock %}
{% block modal_id %}provision_modal{% endblock %}
{% block modal-header %}{% trans "Initialize Overcloud" %}{% endblock %}
{% block modal-body %}
<div>
<p>{% trans "Your OpenStack cloud is deployed but it needs to get initialized in order to get live." %}
</p>
<p>{% trans "This operation can't be undone, are you sure?" %}
</p>
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
{% endblock %}
{% block modal-footer %}
<input class="btn btn-primary" type="submit" value="{% trans "Initialize" %}" />
<a href="{% url 'horizon:infrastructure:overview:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

@ -0,0 +1,11 @@
{% extends 'infrastructure/base.html' %}
{% load i18n %}
{% block title %}{% trans "Initialize" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Initialize Overcloud") %}
{% endblock page_header %}
{% block infrastructure_main %}
{% include "infrastructure/overview/_post_deploy_init.html" %}
{% endblock %}

View File

@ -30,6 +30,8 @@ DEPLOY_URL = urlresolvers.reverse(
'horizon:infrastructure:overview:deploy_confirmation')
DELETE_URL = urlresolvers.reverse(
'horizon:infrastructure:overview:undeploy_confirmation')
POST_DEPLOY_INIT_URL = urlresolvers.reverse(
'horizon:infrastructure:overview:post_deploy_init')
TEST_DATA = utils.TestDataContainer()
heat_data.data(TEST_DATA)
tuskar_data.data(TEST_DATA)
@ -162,3 +164,20 @@ class OverviewTests(test.BaseAdminViewTests):
):
res = self.client.post(DELETE_URL)
self.assertRedirectsNoFollow(res, INDEX_URL)
def test_post_deploy_init_get(self):
stack = api.heat.Stack(TEST_DATA.heatclient_stacks.first())
with contextlib.nested(
_mock_plan(),
patch('tuskar_ui.api.heat.Stack.get_by_plan',
return_value=stack),
):
res = self.client.get(POST_DEPLOY_INIT_URL)
self.assertTemplateUsed(
res, 'infrastructure/overview/post_deploy_init.html')
def test_post_deploy_init_post(self):
# TODO(lsmola) add this test once os-cloud-config changes are
# released, otherwise it will throw error in the Gate
pass

View File

@ -26,4 +26,7 @@ urlpatterns = urls.patterns(
urls.url(r'^undeploy-confirmation$',
views.UndeployConfirmationView.as_view(),
name='undeploy_confirmation'),
urls.url(r'^post-deploy-init$',
views.PostDeployInitView.as_view(),
name='post_deploy_init'),
)

View File

@ -173,3 +173,22 @@ class UndeployConfirmationView(horizon.forms.ModalFormView, StackMixin):
initial = super(UndeployConfirmationView, self).get_initial(**kwargs)
initial['stack_id'] = self.get_stack().id
return initial
class PostDeployInitView(horizon.forms.ModalFormView, StackMixin):
form_class = forms.PostDeployInit
template_name = 'infrastructure/overview/post_deploy_init.html'
def get_success_url(self):
return reverse(INDEX_URL)
def get_context_data(self, **kwargs):
context = super(PostDeployInitView,
self).get_context_data(**kwargs)
context['stack_id'] = self.get_stack().id
return context
def get_initial(self, **kwargs):
initial = super(PostDeployInitView, self).get_initial(**kwargs)
initial['stack_id'] = self.get_stack().id
return initial

View File

@ -27,16 +27,20 @@ class HeatAPITests(test.APITestCase):
def test_stack_list(self):
stacks = self.heatclient_stacks.list()
with patch('tuskar_ui.test.test_driver.heat_driver.Stack.list',
return_value=stacks):
ret_val = api.heat.Stack.list(self.request)
for stack in ret_val:
with patch('openstack_dashboard.api.heat.stacks_list',
return_value=(stacks, None, None)):
stacks = api.heat.Stack.list(self.request)
for stack in stacks:
self.assertIsInstance(stack, api.heat.Stack)
self.assertEqual(1, len(ret_val))
self.assertEqual(1, len(stacks))
def test_stack_get(self):
stack = self.heatclient_stacks.first()
ret_val = api.heat.Stack.get(self.request, stack.id)
with patch('openstack_dashboard.api.heat.stack_get',
return_value=stack):
ret_val = api.heat.Stack.get(self.request, stack.id)
self.assertIsInstance(ret_val, api.heat.Stack)
def test_stack_plan(self):
@ -44,8 +48,8 @@ class HeatAPITests(test.APITestCase):
self.request)
plan = self.tuskarclient_plans.first()
with patch('tuskarclient.v2.plans.PlanManager.get',
return_value=plan):
with patch('tuskarclient.v2.plans.PlanManager.list',
return_value=[plan]):
ret_val = stack.plan
self.assertIsInstance(ret_val, api.tuskar.OvercloudPlan)

View File

@ -141,7 +141,25 @@ def data(TEST):
'label': 'Admin Password',
'description': 'Admin password',
'hidden': 'false',
'value': 'unset',
'value': '5ba3a69c95c668daf84c2f103ebec82d273a4897',
}, {
'name': 'AdminToken',
'label': 'Admin Token',
'description': 'Admin Token',
'hidden': 'false',
'value': 'aa61677c0a270880e99293c148cefee4000b2259',
}, {
'name': 'GlancePassword',
'label': 'Glance Password',
'description': 'Glance Password',
'hidden': 'false',
'value': '16b4aaa3e056d07f796a93afb6010487b7b617e7',
}, {
'name': 'NovaPassword',
'label': 'Nova Password',
'description': 'Nova Password',
'hidden': 'false',
'value': '67d8090ff40c0c400b08ff558233091402afc9c5',
}],
})
TEST.tuskarclient_plans.add(plan_1)