Separate Overcloud into plan and stack

This patch separates Overcloud out into OvercloudPlan in api/tuskar.py
and OvercloudStack in api/heat.py.  This is in preparation for the
split that will be needed since tuskar is being re-defined as a
planning service.

Change-Id: I9ec9a0be1fded8b6918480858ffbb9a4887a3f70
This commit is contained in:
Tzu-Mainn Chen 2014-06-23 17:17:42 +02:00
parent 3957c964a8
commit fe3caf88e9
23 changed files with 631 additions and 654 deletions

View File

@ -20,6 +20,7 @@ from tuskar_ui.api import tuskar
from tuskar_ui.cached_property import cached_property # noqa
from tuskar_ui.handle_errors import handle_errors # noqa
LOG = logging.getLogger(__name__)

View File

@ -10,18 +10,271 @@
# License for the specific language governing permissions and limitations
# under the License.
import heatclient
import keystoneclient.exceptions
import logging
import urlparse
from horizon.utils import memoized
from openstack_dashboard.api import base
from openstack_dashboard.api import heat
from openstack_dashboard.api import keystone
from tuskar_ui.api import node
from tuskar_ui.cached_property import cached_property # noqa
from tuskar_ui.handle_errors import handle_errors # noqa
from tuskar_ui import utils
LOG = logging.getLogger(__name__)
def overcloud_keystoneclient(request, endpoint, password):
"""Returns a client connected to the Keystone backend.
Several forms of authentication are supported:
* Username + password -> Unscoped authentication
* Username + password + tenant id -> Scoped authentication
* Unscoped token -> Unscoped authentication
* Unscoped token + tenant id -> Scoped authentication
* Scoped token -> Scoped authentication
Available services and data from the backend will vary depending on
whether the authentication was scoped or unscoped.
Lazy authentication if an ``endpoint`` parameter is provided.
Calls requiring the admin endpoint should have ``admin=True`` passed in
as a keyword argument.
The client is cached so that subsequent API calls during the same
request/response cycle don't have to be re-authenticated.
"""
api_version = keystone.VERSIONS.get_active_version()
# TODO(lsmola) add support of certificates and secured http and rest of
# parameters according to horizon and add configuration to local settings
# (somehow plugin based, we should not maintain a copy of settings)
LOG.debug("Creating a new keystoneclient connection to %s." % endpoint)
# TODO(lsmola) we should create tripleo-admin user for this purpose
# this needs to be done first on tripleo side
conn = api_version['client'].Client(username="admin",
password=password,
tenant_name="admin",
auth_url=endpoint)
return conn
class OvercloudStack(base.APIResourceWrapper):
_attrs = ('id', 'stack_name', 'outputs', 'stack_status', 'parameters')
def __init__(self, apiresource, request=None, plan=None):
super(OvercloudStack, self).__init__(apiresource)
self._request = request
self._plan = plan
@classmethod
def get(cls, request, stack_id, plan=None):
"""Return the Heat Stack associated with this Overcloud
:return: Heat Stack associated with this Overcloud; or None
if no Stack is associated, or no Stack can be
found
:rtype: heatclient.v1.stacks.Stack or None
"""
stack = heat.stack_get(request, stack_id)
return cls(stack, request=request, plan=plan)
@memoized.memoized
def resources(self, with_joins=True):
"""Return a list of all Overcloud Resources
:param with_joins: should we also retrieve objects associated with each
retrieved Resource?
:type with_joins: bool
:return: list of all Overcloud Resources or an empty list if there
are none
:rtype: list of tuskar_ui.api.Resource
"""
try:
resources = [r for r in heat.resources_list(self._request,
self.stack_name)]
except heatclient.exc.HTTPInternalServerError:
# TODO(lsmola) There is a weird bug in heat, that after
# stack-create it returns 500 for a little while. This can be
# removed once the bug is fixed.
resources = []
if not with_joins:
return [Resource(r, request=self._request)
for r in resources]
nodes_dict = utils.list_to_dict(node.Node.list(self._request,
associated=True),
key_attribute='instance_uuid')
joined_resources = []
for r in resources:
joined_resources.append(
Resource(r, node=nodes_dict.get(r.physical_resource_id, None),
request=self._request))
# TODO(lsmola) I want just resources with nova instance
# this could be probably filtered a better way, investigate
return [r for r in joined_resources if r.node is not None]
@memoized.memoized
def resources_by_role(self, overcloud_role, with_joins=True):
"""Return a list of Overcloud Resources that match an Overcloud Role
:param overcloud_role: role of resources to be returned
:type overcloud_role: tuskar_ui.api.OvercloudRole
:param with_joins: should we also retrieve objects associated with each
retrieved Resource?
:type with_joins: bool
:return: list of Overcloud Resources that match the Overcloud Role,
or an empty list if there are none
:rtype: list of tuskar_ui.api.Resource
"""
# FIXME(lsmola) with_joins is not necessary here, I need at least
# nova instance
resources = self.resources(with_joins)
filtered_resources = [resource for resource in resources if
(overcloud_role.is_deployed_on_node(
resource.node))]
return filtered_resources
@memoized.memoized
def resources_count(self, overcloud_role=None):
"""Return count of Overcloud Resources
:param overcloud_role: role of resources to be counted, None means all
:type overcloud_role: tuskar_ui.api.OvercloudRole
:return: Number of matches resources
:rtype: int
"""
# TODO(dtantsur): there should be better way to do it, rather than
# fetching and calling len()
# FIXME(dtantsur): should also be able to use with_joins=False
# but unable due to bug #1289505
if overcloud_role is None:
resources = self.resources()
else:
resources = self.resources_by_role(overcloud_role)
return len(resources)
@cached_property
def is_deployed(self):
"""Check if this Overcloud is successfully deployed.
:return: True if this Overcloud is successfully deployed;
False otherwise
:rtype: bool
"""
return self.stack_status in ('CREATE_COMPLETE',
'UPDATE_COMPLETE')
@cached_property
def is_deploying(self):
"""Check if this Overcloud is currently deploying or updating.
:return: True if deployment is in progress, False otherwise.
:rtype: bool
"""
return self.stack_status in ('CREATE_IN_PROGRESS',
'UPDATE_IN_PROGRESS')
@cached_property
def is_failed(self):
"""Check if this Overcloud failed to update or deploy.
:return: True if deployment there was an error, False otherwise.
:rtype: bool
"""
return self.stack_status in ('CREATE_FAILED',
'UPDATE_FAILED',)
@cached_property
def is_deleting(self):
"""Check if this Overcloud is deleting.
:return: True if Overcloud is deleting, False otherwise.
:rtype: bool
"""
return self.stack_status in ('DELETE_IN_PROGRESS', )
@cached_property
def is_delete_failed(self):
"""Check if this Overcloud deleting has failed.
:return: True if Overcloud deleting has failed, False otherwise.
:rtype: bool
"""
return self.stack_status in ('DELETE_FAILED', )
@cached_property
def events(self):
"""Return the Heat Events associated with this Overcloud
:return: list of Heat Events associated with this Overcloud;
or an empty list if there is no Stack associated with
this Overcloud, or there are no Events
:rtype: list of heatclient.v1.events.Event
"""
return heat.events_list(self._request,
self.stack_name)
return []
@cached_property
def keystone_ip(self):
for output in self.outputs:
if output['output_key'] == 'KeystoneURL':
return urlparse.urlparse(output['output_value']).hostname
@cached_property
def overcloud_keystone(self):
for output in self.outputs:
if output['output_key'] == 'KeystoneURL':
break
else:
return None
try:
return overcloud_keystoneclient(
self._request,
output['output_value'],
self._plan.attributes.get('AdminPassword', None))
except keystoneclient.exceptions.Unauthorized:
LOG.debug('Unable to connect overcloud keystone.')
return None
@cached_property
def dashboard_urls(self):
client = self.overcloud_keystone
if not client:
return []
services = client.services.list()
for service in services:
if service.name == 'horizon':
break
else:
return []
admin_urls = [endpoint.adminurl for endpoint
in client.endpoints.list()
if endpoint.service_id == service.id]
return admin_urls
class Resource(base.APIResourceWrapper):
_attrs = ('resource_name', 'resource_type', 'resource_status',
'physical_resource_id')
@ -47,7 +300,7 @@ class Resource(base.APIResourceWrapper):
self._node = kwargs['node']
@classmethod
def get(cls, request, overcloud, resource_name):
def get(cls, request, stack, resource_name):
"""Return the specified Heat Resource within an Overcloud
:param request: request object
@ -63,7 +316,7 @@ class Resource(base.APIResourceWrapper):
stack matches the resource name
:rtype: tuskar_ui.api.Resource
"""
resource = heat.resource_get(overcloud.stack.id,
resource = heat.resource_get(stack.id,
resource_name)
return cls(resource, request=request)

View File

@ -23,6 +23,7 @@ from openstack_dashboard.api import nova
from tuskar_ui.cached_property import cached_property # noqa
from tuskar_ui.handle_errors import handle_errors # noqa
LOG = logging.getLogger(__name__)

View File

@ -11,66 +11,21 @@
# under the License.
import django.conf
import heatclient
import keystoneclient.exceptions
import logging
import urlparse
from django.utils.translation import ugettext_lazy as _
from horizon.utils import memoized
from openstack_dashboard.api import base
from openstack_dashboard.api import heat
from openstack_dashboard.api import keystone
from tuskarclient.v1 import client as tuskar_client
from tuskar_ui.api import heat as tuskar_heat
from tuskar_ui.api import node
from tuskar_ui.api import heat
from tuskar_ui.cached_property import cached_property # noqa
from tuskar_ui.handle_errors import handle_errors # noqa
LOG = logging.getLogger(__name__)
TUSKAR_ENDPOINT_URL = getattr(django.conf.settings, 'TUSKAR_ENDPOINT_URL')
def overcloud_keystoneclient(request, endpoint, password):
"""Returns a client connected to the Keystone backend.
Several forms of authentication are supported:
* Username + password -> Unscoped authentication
* Username + password + tenant id -> Scoped authentication
* Unscoped token -> Unscoped authentication
* Unscoped token + tenant id -> Scoped authentication
* Scoped token -> Scoped authentication
Available services and data from the backend will vary depending on
whether the authentication was scoped or unscoped.
Lazy authentication if an ``endpoint`` parameter is provided.
Calls requiring the admin endpoint should have ``admin=True`` passed in
as a keyword argument.
The client is cached so that subsequent API calls during the same
request/response cycle don't have to be re-authenticated.
"""
api_version = keystone.VERSIONS.get_active_version()
# TODO(lsmola) add support of certificates and secured http and rest of
# parameters according to horizon and add configuration to local settings
# (somehow plugin based, we should not maintain a copy of settings)
LOG.debug("Creating a new keystoneclient connection to %s." % endpoint)
# TODO(lsmola) we should create tripleo-admin user for this purpose
# this needs to be done first on tripleo side
conn = api_version['client'].Client(username="admin",
password=password,
tenant_name="admin",
auth_url=endpoint)
return conn
# FIXME: request isn't used right in the tuskar client right now,
# but looking at other clients, it seems like it will be in the future
def tuskarclient(request):
@ -78,21 +33,6 @@ def tuskarclient(request):
return c
def list_to_dict(object_list, key_attribute='id'):
"""Converts an object list to a dict
:param object_list: list of objects to be put into a dict
:type object_list: list
:param key_attribute: object attribute used as index by dict
:type key_attribute: str
:return: dict containing the objects in the list
:rtype: dict
"""
return dict((getattr(o, key_attribute), o) for o in object_list)
def transform_sizing(overcloud_sizing):
"""Transform the sizing to simpler format
@ -113,29 +53,13 @@ def transform_sizing(overcloud_sizing):
} for (role, flavor), sizing in overcloud_sizing.items()]
class Overcloud(base.APIResourceWrapper):
class OvercloudPlan(base.APIResourceWrapper):
_attrs = ('id', 'stack_id', 'name', 'description', 'counts', 'attributes')
def __init__(self, apiresource, request=None):
super(Overcloud, self).__init__(apiresource)
super(OvercloudPlan, self).__init__(apiresource)
self._request = request
@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.attributes.get('AdminPassword', None))
except keystoneclient.exceptions.Unauthorized:
LOG.debug('Unable to connect overcloud keystone.')
return None
@classmethod
def create(cls, request, overcloud_sizing, overcloud_configuration):
"""Create an Overcloud in Tuskar
@ -214,21 +138,6 @@ class Overcloud(base.APIResourceWrapper):
return [cls(oc, request=request) for oc in ocs]
@classmethod
def template_parameters(cls, request):
"""Return a list of needed template parameters
:param request: request object
:type request: django.http.HttpRequest
:return: dict with key/value parameters
:rtype: dict
"""
parameters = tuskarclient(request).overclouds.template_parameters()
# FIXME(lsmola) python client is converting the result to
# object, we have to return it better from client or API
return parameters._info
@classmethod
@handle_errors(_("Unable to retrieve deployment"))
def get(cls, request, overcloud_id):
@ -248,7 +157,7 @@ class Overcloud(base.APIResourceWrapper):
# TODO(lsmola) uncomment when possible
# overcloud = tuskarclient(request).overclouds.get(overcloud_id)
# return cls(overcloud, request=request)
return cls.get_the_overcloud(request)
return cls.get_the_plan(request)
# TODO(lsmola) before will will support multiple overclouds, we
# can work only with overcloud that is named overcloud. Delete
@ -259,20 +168,11 @@ class Overcloud(base.APIResourceWrapper):
# with situations that overcloud is deleted, but stack is still
# there. So overcloud will pretend to exist when stack exist.
@classmethod
def get_the_overcloud(cls, request):
overcloud_list = cls.list(request)
for overcloud in overcloud_list:
if overcloud.name == 'overcloud':
return overcloud
the_overcloud = cls(object(), request=request)
# I need to mock attributes of overcloud that is being deleted.
the_overcloud.id = "overcloud"
if the_overcloud.stack and the_overcloud.is_deleting:
return the_overcloud
else:
raise heatclient.exc.HTTPNotFound()
def get_the_plan(cls, request):
plan_list = cls.list(request)
for plan in plan_list:
if plan.name == 'overcloud':
return plan
@classmethod
def delete(cls, request, overcloud_id):
@ -286,6 +186,21 @@ class Overcloud(base.APIResourceWrapper):
"""
tuskarclient(request).overclouds.delete(overcloud_id)
@classmethod
def template_parameters(cls, request):
"""Return a list of needed template parameters
:param request: request object
:type request: django.http.HttpRequest
:return: dict with key/value parameters
:rtype: dict
"""
parameters = tuskarclient(request).overclouds.template_parameters()
# FIXME(lsmola) python client is converting the result to
# object, we have to return it better from client or API
return parameters._info
@cached_property
def stack(self):
"""Return the Heat Stack associated with this Overcloud
@ -295,183 +210,8 @@ class Overcloud(base.APIResourceWrapper):
found
:rtype: heatclient.v1.stacks.Stack or None
"""
return heat.stack_get(self._request, 'overcloud')
@cached_property
def stack_events(self):
"""Return the Heat Events associated with this Overcloud
:return: list of Heat Events associated with this Overcloud;
or an empty list if there is no Stack associated with
this Overcloud, or there are no Events
:rtype: list of heatclient.v1.events.Event
"""
if self.stack:
return heat.events_list(self._request,
self.stack.stack_name)
return []
@cached_property
def is_deployed(self):
"""Check if this Overcloud is successfully deployed.
:return: True if this Overcloud is successfully deployed;
False otherwise
:rtype: bool
"""
return self.stack.stack_status in ('CREATE_COMPLETE',
'UPDATE_COMPLETE')
@cached_property
def is_deploying(self):
"""Check if this Overcloud is currently deploying or updating.
:return: True if deployment is in progress, False otherwise.
:rtype: bool
"""
return self.stack.stack_status in ('CREATE_IN_PROGRESS',
'UPDATE_IN_PROGRESS')
@cached_property
def is_failed(self):
"""Check if this Overcloud failed to update or deploy.
:return: True if deployment there was an error, False otherwise.
:rtype: bool
"""
return self.stack.stack_status in ('CREATE_FAILED',
'UPDATE_FAILED',)
@cached_property
def is_deleting(self):
"""Check if this Overcloud is deleting.
:return: True if Overcloud is deleting, False otherwise.
:rtype: bool
"""
return self.stack.stack_status in ('DELETE_IN_PROGRESS', )
@cached_property
def is_delete_failed(self):
"""Check if this Overcloud deleting has failed.
:return: True if Overcloud deleting has failed, False otherwise.
:rtype: bool
"""
return self.stack.stack_status in ('DELETE_FAILED', )
@memoized.memoized
def all_resources(self, with_joins=True):
"""Return a list of all Overcloud Resources
:param with_joins: should we also retrieve objects associated with each
retrieved Resource?
:type with_joins: bool
:return: list of all Overcloud Resources or an empty list if there
are none
:rtype: list of tuskar_ui.api.Resource
"""
try:
resources = [r for r in heat.resources_list(self._request,
self.stack.stack_name)]
except heatclient.exc.HTTPInternalServerError:
# TODO(lsmola) There is a weird bug in heat, that after
# stack-create it returns 500 for a little while. This can be
# removed once the bug is fixed.
resources = []
if not with_joins:
return [tuskar_heat.Resource(r, request=self._request)
for r in resources]
nodes_dict = list_to_dict(node.Node.list(self._request,
associated=True),
key_attribute='instance_uuid')
joined_resources = []
for r in resources:
joined_resources.append(
tuskar_heat.Resource(r,
node=nodes_dict.get(
r.physical_resource_id, None),
request=self._request))
# TODO(lsmola) I want just resources with nova instance
# this could be probably filtered a better way, investigate
return [r for r in joined_resources if r.node is not None]
@memoized.memoized
def resources(self, overcloud_role, with_joins=True):
"""Return a list of Overcloud Resources that match an Overcloud Role
:param overcloud_role: role of resources to be returned
:type overcloud_role: tuskar_ui.api.OvercloudRole
:param with_joins: should we also retrieve objects associated with each
retrieved Resource?
:type with_joins: bool
:return: list of Overcloud Resources that match the Overcloud Role,
or an empty list if there are none
:rtype: list of tuskar_ui.api.Resource
"""
# FIXME(lsmola) with_joins is not necessary here, I need at least
# nova instance
all_resources = self.all_resources(with_joins)
filtered_resources = [resource for resource in all_resources if
(overcloud_role.is_deployed_on_node(
resource.node))]
return filtered_resources
@memoized.memoized
def resources_count(self, overcloud_role=None):
"""Return count of Overcloud Resources
:param overcloud_role: role of resources to be counted, None means all
:type overcloud_role: tuskar_ui.api.OvercloudRole
:return: Number of matches resources
:rtype: int
"""
# TODO(dtantsur): there should be better way to do it, rather than
# fetching and calling len()
# FIXME(dtantsur): should also be able to use with_joins=False
# but unable due to bug #1289505
if overcloud_role is None:
resources = self.all_resources()
else:
resources = self.resources(overcloud_role)
return len(resources)
@cached_property
def stack_outputs(self):
return getattr(self.stack, 'outputs', [])
@cached_property
def keystone_ip(self):
for output in self.stack_outputs:
if output['output_key'] == 'KeystoneURL':
return urlparse.urlparse(output['output_value']).hostname
@cached_property
def dashboard_urls(self):
client = self.overcloud_keystone
if not client:
return []
services = client.services.list()
for service in services:
if service.name == 'horizon':
break
else:
return []
admin_urls = [endpoint.adminurl for endpoint
in client.endpoints.list()
if endpoint.service_id == service.id]
return admin_urls
return heat.OvercloudStack.get(self._request, self.stack_id,
plan=self)
class OvercloudRole(base.APIResourceWrapper):

View File

@ -87,11 +87,11 @@ class FlavorRolesTable(tables.DataTable):
def __init__(self, request, *args, **kwargs):
# TODO(dtantsur): support multiple overclouds
try:
overcloud = api.tuskar.Overcloud.get_the_overcloud(request)
stack = api.tuskar.OvercloudPlan.get_the_plan(request).stack
except Exception:
count_getter = lambda role: _("Not deployed")
else:
count_getter = overcloud.resources_count
count_getter = stack.resources_count
self._columns['count'] = tables.Column(
count_getter,
verbose_name=_("Instances Count")

View File

@ -215,15 +215,15 @@ class FlavorsTest(test.BaseAdminViewTests):
return_value=flavor),
patch('tuskar_ui.api.tuskar.OvercloudRole.list',
return_value=roles),
patch('tuskar_ui.api.tuskar.Overcloud.get_the_overcloud',
patch('tuskar_ui.api.tuskar.OvercloudPlan.get_the_plan',
side_effect=Exception)
) as (image_mock, get_mock, roles_mock, overcloud_mock):
) as (image_mock, get_mock, roles_mock, plan_mock):
res = self.client.get(urlresolvers.reverse(DETAILS_VIEW,
args=(flavor.id,)))
self.assertEqual(image_mock.call_count, 1) # memoized
self.assertEqual(get_mock.call_count, 1)
self.assertEqual(roles_mock.call_count, 1)
self.assertEqual(overcloud_mock.call_count, 1)
self.assertEqual(plan_mock.call_count, 1)
self.assertTemplateUsed(res,
'infrastructure/flavors/details.html')
@ -232,8 +232,10 @@ class FlavorsTest(test.BaseAdminViewTests):
images = TEST_DATA.glanceclient_images.list()[:2]
roles = TEST_DATA.tuskarclient_overcloud_roles.list()
roles[0].flavor_id = flavor.id
overcloud = api.tuskar.Overcloud(
TEST_DATA.tuskarclient_overclouds.first())
plan = api.tuskar.OvercloudPlan(
TEST_DATA.tuskarclient_overcloud_plans.first())
stack = api.heat.OvercloudStack(
TEST_DATA.heatclient_stacks.first())
with contextlib.nested(
patch('openstack_dashboard.api.glance.image_get',
side_effect=images),
@ -241,18 +243,22 @@ class FlavorsTest(test.BaseAdminViewTests):
return_value=flavor),
patch('tuskar_ui.api.tuskar.OvercloudRole.list',
return_value=roles),
patch('tuskar_ui.api.tuskar.Overcloud.get_the_overcloud',
return_value=overcloud),
patch('tuskar_ui.api.tuskar.OvercloudPlan.get_the_plan',
return_value=plan),
patch('tuskar_ui.api.heat.OvercloudStack.get',
return_value=stack),
# __name__ is required for horizon.tables
patch('tuskar_ui.api.tuskar.Overcloud.resources_count',
patch('tuskar_ui.api.heat.OvercloudStack.resources_count',
return_value=42, __name__='')
) as (image_mock, get_mock, roles_mock, overcloud_mock, count_mock):
) as (image_mock, get_mock, roles_mock, plan_mock, stack_mock,
count_mock):
res = self.client.get(urlresolvers.reverse(DETAILS_VIEW,
args=(flavor.id,)))
self.assertEqual(image_mock.call_count, 1) # memoized
self.assertEqual(get_mock.call_count, 1)
self.assertEqual(roles_mock.call_count, 1)
self.assertEqual(overcloud_mock.call_count, 1)
self.assertEqual(plan_mock.call_count, 1)
self.assertEqual(stack_mock.call_count, 1)
self.assertEqual(count_mock.call_count, 1)
self.assertListEqual(count_mock.call_args_list, [call(roles[0])])
self.assertTemplateUsed(res,

View File

@ -25,7 +25,7 @@ from tuskar_ui import api
class UndeployOvercloud(horizon.forms.SelfHandlingForm):
def handle(self, request, data):
try:
api.tuskar.Overcloud.delete(request, self.initial['overcloud_id'])
api.tuskar.OvercloudPlan.delete(request, self.initial['plan_id'])
except Exception:
horizon.exceptions.handle(request,
_("Unable to undeploy overcloud."))

View File

@ -21,7 +21,7 @@ from tuskar_ui.infrastructure.overcloud import tables
from tuskar_ui import utils
def _get_role_data(overcloud, role):
def _get_role_data(plan, role):
"""Gathers data about a single deployment role from the related Overcloud
and OvercloudRole objects, and presents it in the form convenient for use
from the template.
@ -33,9 +33,9 @@ def _get_role_data(overcloud, role):
:return: dict with information about the role, to be used by template
:rtype: dict
"""
resources = overcloud.resources(role, with_joins=True)
resources = plan.stack.resources_by_role(role, with_joins=True)
nodes = [r.node for r in resources]
counts = getattr(overcloud, 'counts', [])
counts = getattr(plan, 'counts', [])
for c in counts:
if c['overcloud_role_id'] == role.id:
@ -82,21 +82,21 @@ class OverviewTab(tabs.Tab):
preload = False
def get_context_data(self, request, **kwargs):
overcloud = self.tab_group.kwargs['overcloud']
plan = self.tab_group.kwargs['plan']
roles = api.tuskar.OvercloudRole.list(request)
role_data = [_get_role_data(overcloud, role) for role in roles]
role_data = [_get_role_data(plan, role) for role in roles]
total = sum(d['total_node_count'] for d in role_data)
progress = 100 * sum(d.get('deployed_node_count', 0)
for d in role_data) // (total or 1)
events = overcloud.stack_events
events = plan.stack.events
last_failed_events = [e for e in events
if e.resource_status == 'CREATE_FAILED'][-3:]
return {
'overcloud': overcloud,
'plan': plan,
'roles': role_data,
'progress': max(5, progress),
'dashboard_urls': overcloud.dashboard_urls,
'dashboard_urls': plan.stack.dashboard_urls,
'last_failed_events': last_failed_events,
}
@ -108,7 +108,7 @@ class UndeployInProgressTab(tabs.Tab):
preload = False
def get_context_data(self, request, **kwargs):
overcloud = self.tab_group.kwargs['overcloud']
plan = self.tab_group.kwargs['plan']
# TODO(lsmola) since at this point we don't have total number of nodes
# we will hack this around, till API can show this information. So it
@ -117,24 +117,24 @@ class UndeployInProgressTab(tabs.Tab):
total_num_nodes_count = 10
try:
all_resources_count = len(
overcloud.all_resources(with_joins=False))
resources_count = len(
plan.stack.resources(with_joins=False))
except heatclient.exc.HTTPNotFound:
# Immediately after undeploying has started, heat returns this
# exception so we can take it as kind of init of undeploying.
all_resources_count = total_num_nodes_count
resources_count = total_num_nodes_count
# TODO(lsmola) same as hack above
total_num_nodes_count = max(all_resources_count, total_num_nodes_count)
total_num_nodes_count = max(resources_count, total_num_nodes_count)
delete_progress = max(
5, 100 * (total_num_nodes_count - all_resources_count))
5, 100 * (total_num_nodes_count - resources_count))
events = overcloud.stack_events
events = plan.stack.events
last_failed_events = [e for e in events
if e.resource_status == 'DELETE_FAILED'][-3:]
return {
'overcloud': overcloud,
'plan': plan,
'progress': delete_progress,
'last_failed_events': last_failed_events,
}
@ -148,10 +148,10 @@ class ConfigurationTab(tabs.TableTab):
preload = False
def get_configuration_data(self):
overcloud = self.tab_group.kwargs['overcloud']
plan = self.tab_group.kwargs['plan']
return [(utils.de_camel_case(key), value) for key, value in
overcloud.stack.parameters.items()]
plan.stack.parameters.items()]
class LogTab(tabs.TableTab):
@ -162,8 +162,8 @@ class LogTab(tabs.TableTab):
preload = False
def get_log_data(self):
overcloud = self.tab_group.kwargs['overcloud']
return overcloud.stack_events
plan = self.tab_group.kwargs['plan']
return plan.stack.events
class UndeployInProgressTabs(tabs.TabGroup):

View File

@ -1,8 +1,8 @@
{% load i18n %}
{% load url from future%}
{% if overcloud.is_deploying or overcloud.is_failed %}
{% if overcloud.is_deploying %}
{% if stack.is_deploying or stack.is_failed %}
{% if stack.is_deploying %}
<div class="alert alert-info">
<div class="row-fluid">
<div class="span2">
@ -45,7 +45,7 @@
</div>
{% endif %}
{% if not dashboard_urls and not overcloud.is_deploying and not overcloud.is_failed %}
{% if not dashboard_urls and not stack.is_deploying and not stack.is_failed %}
<div class="row-fluid">
<div class="span8">
{% blocktrans %}
@ -61,7 +61,7 @@
#!/usr/bin/bash
# You need to run the following commands from a machine where you have a checkout of the tripleo source code
# and direct access via SSH to your Overcloud control node ({{overcloud.keystone_ip}}).
# and direct access via SSH to your Overcloud control node ({{stack.keystone_ip}}).
set -eux
@ -71,17 +71,17 @@ cd $TRIPLEO_ROOT
# Be careful to source tripleorc here, some variables are rewritten below
source $TRIPLEO_ROOT/tripleorc
export OVERCLOUD_IP={{overcloud.keystone_ip}}
export OVERCLOUD_IP={{stack.keystone_ip}}
export OVERCLOUD_ADMIN_TOKEN={{overcloud.attributes.AdminToken}}
export OVERCLOUD_ADMIN_PASSWORD={{overcloud.attributes.AdminPassword}}
export OVERCLOUD_CINDER_PASSWORD={{overcloud.attributes.CinderPassword}}
export OVERCLOUD_GLANCE_PASSWORD={{overcloud.attributes.GlancePassword}}
export OVERCLOUD_HEAT_PASSWORD={{overcloud.attributes.HeatPassword}}
export OVERCLOUD_NEUTRON_PASSWORD={{overcloud.attributes.NeutronPassword}}
export OVERCLOUD_NOVA_PASSWORD={{overcloud.attributes.NovaPassword}}
export OVERCLOUD_SWIFT_PASSWORD={{overcloud.attributes.SwiftPassword}}
export OVERCLOUD_SWIFT_HASH={{overcloud.attributes.SwiftHashSuffix}}
export OVERCLOUD_ADMIN_TOKEN={{plan.attributes.AdminToken}}
export OVERCLOUD_ADMIN_PASSWORD={{plan.attributes.AdminPassword}}
export OVERCLOUD_CINDER_PASSWORD={{plan.attributes.CinderPassword}}
export OVERCLOUD_GLANCE_PASSWORD={{plan.attributes.GlancePassword}}
export OVERCLOUD_HEAT_PASSWORD={{plan.attributes.HeatPassword}}
export OVERCLOUD_NEUTRON_PASSWORD={{plan.attributes.NeutronPassword}}
export OVERCLOUD_NOVA_PASSWORD={{plan.attributes.NovaPassword}}
export OVERCLOUD_SWIFT_PASSWORD={{plan.attributes.SwiftPassword}}
export OVERCLOUD_SWIFT_HASH={{plan.attributes.SwiftHashSuffix}}
OVERCLOUD_ENDPOINT="http://$OVERCLOUD_IP:5000/v2.0"
NEW_JSON=$(jq '.overcloud.password="'${OVERCLOUD_ADMIN_PASSWORD}'" | .overcloud.endpoint="'${OVERCLOUD_ENDPOINT}'" | .overcloud.endpointhost="'${OVERCLOUD_IP}'"' $TE_DATAFILE)
@ -115,7 +115,7 @@ keystone role-create --name heat_stack_user
set -eux
# You need to run the following commands from a Undercloud node where you have
# direct access via SSH to your Overcloud control node ({{overcloud.keystone_ip}}).
# direct access via SSH to your Overcloud control node ({{stack.keystone_ip}}).
# Run these commands as the user you used to install the undercloud, likely the stack
# user if you followed the recommendations from http://openstack.redhat.com/Deploying_RDO_using_Instack.
@ -125,18 +125,18 @@ set -eux
# This file was created when you followed http://openstack.redhat.com/Deploying_an_RDO_Overcloud_with_Instack
source deploy-overcloudrc
export OVERCLOUD_IP={{overcloud.keystone_ip}}
export OVERCLOUD_IP={{stack.keystone_ip}}
cat &gt; tripleo-overcloud-passwords &lt;&lt;EOF
export OVERCLOUD_ADMIN_TOKEN={{overcloud.attributes.AdminToken}}
export OVERCLOUD_ADMIN_PASSWORD={{overcloud.attributes.AdminPassword}}
export OVERCLOUD_CINDER_PASSWORD={{overcloud.attributes.CinderPassword}}
export OVERCLOUD_GLANCE_PASSWORD={{overcloud.attributes.GlancePassword}}
export OVERCLOUD_HEAT_PASSWORD={{overcloud.attributes.HeatPassword}}
export OVERCLOUD_NEUTRON_PASSWORD={{overcloud.attributes.NeutronPassword}}
export OVERCLOUD_NOVA_PASSWORD={{overcloud.attributes.NovaPassword}}
export OVERCLOUD_SWIFT_PASSWORD={{overcloud.attributes.SwiftPassword}}
export OVERCLOUD_SWIFT_HASH={{overcloud.attributes.SwiftHashSuffix}}
export OVERCLOUD_ADMIN_TOKEN={{plan.attributes.AdminToken}}
export OVERCLOUD_ADMIN_PASSWORD={{plan.attributes.AdminPassword}}
export OVERCLOUD_CINDER_PASSWORD={{plan.attributes.CinderPassword}}
export OVERCLOUD_GLANCE_PASSWORD={{plan.attributes.GlancePassword}}
export OVERCLOUD_HEAT_PASSWORD={{plan.attributes.HeatPassword}}
export OVERCLOUD_NEUTRON_PASSWORD={{plan.attributes.NeutronPassword}}
export OVERCLOUD_NOVA_PASSWORD={{plan.attributes.NovaPassword}}
export OVERCLOUD_SWIFT_PASSWORD={{plan.attributes.SwiftPassword}}
export OVERCLOUD_SWIFT_HASH={{plan.attributes.SwiftHashSuffix}}
EOF
source tripleo-overcloud-passwords
@ -145,8 +145,8 @@ JSONFILE=nodes.json
if [ ! -f $JSONFILE ]; then
echo '{}' &gt; $JSONFILE
fi
OVERCLOUD_ENDPOINT="http://{{overcloud.keystone_ip}}:5000/v2.0"
NEW_JSON=$(jq '.overcloud.password="'{{overcloud.attributes.AdminPassword}}'" | .overcloud.endpoint="'${OVERCLOUD_ENDPOINT}'" | .overcloud.endpointhost="'{{overcloud.keystone_ip}}'"' $JSONFILE)
OVERCLOUD_ENDPOINT="http://{{stack.keystone_ip}}:5000/v2.0"
NEW_JSON=$(jq '.overcloud.password="'{{plan.attributes.AdminPassword}}'" | .overcloud.endpoint="'${OVERCLOUD_ENDPOINT}'" | .overcloud.endpointhost="'{{stack.keystone_ip}}'"' $JSONFILE)
echo $NEW_JSON &gt; $JSONFILE
export TE_DATAFILE=$JSONFILE
@ -191,7 +191,7 @@ nova flavor-create m1.tiny 1 512 2 1
{% for role in roles %}
<tr>
<td><a
href="{% url 'horizon:infrastructure:overcloud:role' overcloud.id role.role.id %}"
href="{% url 'horizon:infrastructure:overcloud:role' plan.id role.role.id %}"
>{{ role.name }} <span class="badge">({{ role.total_node_count }})</span></a>
</td>
<td>

View File

@ -4,7 +4,7 @@
<div class="span12">
<div class="actions pull-right">
</div>
{% if overcloud.is_deleting %}
{% if plan.stack.is_deleting %}
<div class="alert alert-error">
<div class="row-fluid">
<div class="span2">

View File

@ -5,7 +5,7 @@
{% block title %}{% trans 'My OpenStack Deployment' %}{% endblock %}
{% block css %}
{% if overcloud.is_deploying %}
{% if stack.is_deploying %}
<meta http-equiv="refresh" content="30">
{% endif %}
{{ block.super }}
@ -19,16 +19,16 @@
<div class="row-fluid">
<div class="span12">
<div class="actions pull-right">
<a href="{% url 'horizon:infrastructure:overcloud:undeploy_confirmation' overcloud.id %}"
<a href="{% url 'horizon:infrastructure:overcloud:undeploy_confirmation' plan.id %}"
class="btn btn-danger ajax-modal
{% if not overcloud.is_deployed and not overcloud.is_failed %}disabled{% endif %}">
{% if not stack.is_deployed and not stack.is_failed %}disabled{% endif %}">
<i class="icon-fire icon-white"></i>
{% trans "Undeploy" %}
</a>
{% comment %} no scaling for Icehouse, uncomment when ready
<a href="{% url 'horizon:infrastructure:overcloud:scale' overcloud.id %}"
<a href="{% url 'horizon:infrastructure:overcloud:scale' plan.id %}"
class="btn ajax-modal
{% if not overcloud.is_deployed %}disabled{% endif %}">
{% if not stack.is_deployed %}disabled{% endif %}">
<i class="icon-resize-full"></i>
{% trans "Scale deployment" %}
</a>

View File

@ -19,6 +19,7 @@ from django.core import urlresolvers
from mock import patch, call # noqa
from openstack_dashboard.test.test_data import utils
from tuskar_ui import api
from tuskar_ui.test import helpers as test
from tuskar_ui.test.test_data import flavor_data
from tuskar_ui.test.test_data import heat_data
@ -50,9 +51,13 @@ tuskar_data.data(TEST_DATA)
@contextlib.contextmanager
def _mock_overcloud(**kwargs):
oc = None
stack = TEST_DATA.heatclient_stacks.first()
def _mock_plan(**kwargs):
plan = None
stack = api.heat.OvercloudStack(TEST_DATA.heatclient_stacks.first())
stack.events = []
stack.resources_by_role = lambda *args, **kwargs: []
stack.resources = lambda *args, **kwargs: []
stack.overcloud_keystone = None
template_parameters = {
"NeutronPublicInterfaceRawDevice": {
"Default": "",
@ -74,70 +79,59 @@ def _mock_overcloud(**kwargs):
'spec_set': [
'counts',
'create',
'dashboard_urls',
'delete',
'get',
'get_the_overcloud',
'get_the_plan',
'id',
'is_deployed',
'is_deploying',
'is_deleting',
'is_delete_failed',
'is_failed',
'all_resources',
'resources',
'stack',
'stack_events',
'update',
'template_parameters',
],
'counts': [],
'create.side_effect': lambda *args, **kwargs: oc,
'dashboard_urls': '',
'create.side_effect': lambda *args, **kwargs: plan,
'delete.return_value': None,
'get.side_effect': lambda *args, **kwargs: oc,
'get_the_overcloud.side_effect': lambda *args, **kwargs: oc,
'get.side_effect': lambda *args, **kwargs: plan,
'get_the_plan.side_effect': lambda *args, **kwargs: plan,
'id': 1,
'is_deployed': True,
'is_deploying': False,
'is_deleting': False,
'is_delete_failed': False,
'is_failed': False,
'all_resources.return_value': [],
'resources.return_value': [],
'stack_events': [],
'stack': stack,
'update.side_effect': lambda *args, **kwargs: oc,
'update.side_effect': lambda *args, **kwargs: plan,
'template_parameters.return_value': template_parameters,
}
params.update(kwargs)
with patch('tuskar_ui.api.tuskar.Overcloud', **params) as Overcloud:
oc = Overcloud
yield Overcloud
with patch(
'tuskar_ui.api.tuskar.OvercloudPlan', **params) as OvercloudPlan:
plan = OvercloudPlan
yield OvercloudPlan
class OvercloudTests(test.BaseAdminViewTests):
def test_index_overcloud_undeployed_get(self):
with _mock_overcloud(**{'get_the_overcloud.side_effect': None,
'get_the_overcloud.return_value': None}):
with _mock_plan(**{'get_the_plan.side_effect': None,
'get_the_plan.return_value': None}):
res = self.client.get(INDEX_URL)
self.assertRedirectsNoFollow(res, CREATE_URL)
def test_index_overcloud_deployed_stack_not_created(self):
with _mock_overcloud(is_deployed=False, stack=None) as Overcloud:
with contextlib.nested(
_mock_plan(),
patch('tuskar_ui.api.heat.OvercloudStack.is_deployed',
return_value=False),
):
res = self.client.get(INDEX_URL)
request = Overcloud.get_the_overcloud.call_args_list[0][0][0]
self.assertListEqual(Overcloud.get_the_overcloud.call_args_list,
[call(request)])
request = api.tuskar.OvercloudPlan.get_the_plan. \
call_args_list[0][0][0]
self.assertListEqual(
api.tuskar.OvercloudPlan.get_the_plan.call_args_list,
[call(request)])
self.assertRedirectsNoFollow(res, DETAIL_URL)
def test_index_overcloud_deployed(self):
with _mock_overcloud() as Overcloud:
with _mock_plan() as Overcloud:
res = self.client.get(INDEX_URL)
request = Overcloud.get_the_overcloud.call_args_list[0][0][0]
self.assertListEqual(Overcloud.get_the_overcloud.call_args_list,
request = Overcloud.get_the_plan.call_args_list[0][0][0]
self.assertListEqual(Overcloud.get_the_plan.call_args_list,
[call(request)])
self.assertRedirectsNoFollow(res, DETAIL_URL)
@ -149,7 +143,7 @@ class OvercloudTests(test.BaseAdminViewTests):
'spec_set': ['list'],
'list.return_value': roles,
}),
_mock_overcloud(),
_mock_plan(),
patch('tuskar_ui.api.node.Node', **{
'spec_set': ['list'],
'list.return_value': [],
@ -184,7 +178,7 @@ class OvercloudTests(test.BaseAdminViewTests):
'spec_set': ['list'],
'list.return_value': roles,
}),
_mock_overcloud(),
_mock_plan(),
patch('tuskar_ui.api.node.Node', **{
'spec_set': ['list'],
'list.return_value': [node],
@ -227,7 +221,7 @@ class OvercloudTests(test.BaseAdminViewTests):
'spec_set': ['list'],
'list.return_value': roles,
}),
_mock_overcloud(),
_mock_plan(),
patch('tuskar_ui.api.node.Node', **{
'spec_set': ['list'],
'list.return_value': [],
@ -257,7 +251,7 @@ class OvercloudTests(test.BaseAdminViewTests):
'spec_set': ['list'],
'list.return_value': roles,
}),
_mock_overcloud(),
_mock_plan(),
patch('tuskar_ui.api.node.Node', **{
'spec_set': ['list'],
'list.return_value': [node],
@ -275,8 +269,9 @@ class OvercloudTests(test.BaseAdminViewTests):
def test_detail_get(self):
roles = TEST_DATA.tuskarclient_overcloud_roles.list()
with contextlib.nested(
_mock_overcloud(),
_mock_plan(),
patch('tuskar_ui.api.tuskar.OvercloudRole', **{
'spec_set': ['list'],
'list.return_value': roles,
@ -292,7 +287,7 @@ class OvercloudTests(test.BaseAdminViewTests):
res, 'infrastructure/overcloud/_detail_overview.html')
def test_detail_get_configuration_tab(self):
with _mock_overcloud():
with _mock_plan():
res = self.client.get(DETAIL_URL_CONFIGURATION_TAB)
self.assertTemplateUsed(
@ -303,7 +298,7 @@ class OvercloudTests(test.BaseAdminViewTests):
res, 'horizon/common/_detail_table.html')
def test_detail_get_log_tab(self):
with _mock_overcloud():
with _mock_plan():
res = self.client.get(DETAIL_URL_LOG_TAB)
self.assertTemplateUsed(
@ -319,12 +314,18 @@ class OvercloudTests(test.BaseAdminViewTests):
res, 'infrastructure/overcloud/undeploy_confirmation.html')
def test_delete_post(self):
with _mock_overcloud():
with _mock_plan():
res = self.client.post(DELETE_URL)
self.assertRedirectsNoFollow(res, INDEX_URL)
def test_undeploy_in_progress(self):
with _mock_overcloud(is_deleting=True, is_deployed=False):
with contextlib.nested(
_mock_plan(),
patch('tuskar_ui.api.heat.OvercloudStack.is_deleting',
return_value=True),
patch('tuskar_ui.api.heat.OvercloudStack.is_deployed',
return_value=False),
):
res = self.client.get(UNDEPLOY_IN_PROGRESS_URL)
self.assertTemplateUsed(
@ -335,20 +336,26 @@ class OvercloudTests(test.BaseAdminViewTests):
res, 'horizon/common/_detail_table.html')
def test_undeploy_in_progress_finished(self):
with _mock_overcloud(**{'get_the_overcloud.side_effect': None,
'get_the_overcloud.return_value': None}):
with _mock_plan(**{'get_the_plan.side_effect': None,
'get_the_plan.return_value': None}):
res = self.client.get(UNDEPLOY_IN_PROGRESS_URL)
self.assertRedirectsNoFollow(res, CREATE_URL)
def test_undeploy_in_progress_invalid(self):
with _mock_overcloud():
with _mock_plan():
res = self.client.get(UNDEPLOY_IN_PROGRESS_URL)
self.assertRedirectsNoFollow(res, DETAIL_URL)
def test_undeploy_in_progress_log_tab(self):
with _mock_overcloud(is_deleting=True, is_deployed=False):
with contextlib.nested(
_mock_plan(),
patch('tuskar_ui.api.heat.OvercloudStack.is_deleting',
return_value=True),
patch('tuskar_ui.api.heat.OvercloudStack.is_deployed',
return_value=False),
):
res = self.client.get(UNDEPLOY_IN_PROGRESS_URL_LOG_TAB)
self.assertTemplateUsed(
@ -366,7 +373,7 @@ class OvercloudTests(test.BaseAdminViewTests):
'spec_set': ['list'],
'list.return_value': roles,
}),
_mock_overcloud(counts=[{
_mock_plan(counts=[{
"overcloud_role_id": role.id,
"num_nodes": 0,
} for role in roles]),
@ -389,7 +396,7 @@ class OvercloudTests(test.BaseAdminViewTests):
old_flavor_id = roles[0].flavor_id
roles[0].flavor_id = flavor.id
data = {
'overcloud_id': '1',
'plan_id': '1',
'count__1__%s' % flavor.id: '1',
'count__2__': '0',
'count__3__': '0',
@ -400,7 +407,7 @@ class OvercloudTests(test.BaseAdminViewTests):
'spec_set': ['list'],
'list.return_value': roles,
}),
_mock_overcloud(counts=[{
_mock_plan(counts=[{
"overcloud_role_id": role.id,
"num_nodes": 0,
} for role in roles]),

View File

@ -21,18 +21,18 @@ urlpatterns = urls.patterns(
'',
urls.url(r'^$', views.IndexView.as_view(), name='index'),
urls.url(r'^create/$', views.CreateView.as_view(), name='create'),
urls.url(r'^(?P<overcloud_id>[^/]+)/undeploy-in-progress$',
urls.url(r'^(?P<plan_id>[^/]+)/undeploy-in-progress$',
views.UndeployInProgressView.as_view(),
name='undeploy_in_progress'),
urls.url(r'^create/role-edit/(?P<role_id>[^/]+)$',
views.OvercloudRoleEdit.as_view(), name='role_edit'),
urls.url(r'^(?P<overcloud_id>[^/]+)/$', views.DetailView.as_view(),
urls.url(r'^(?P<plan_id>[^/]+)/$', views.DetailView.as_view(),
name='detail'),
urls.url(r'^(?P<overcloud_id>[^/]+)/scale$', views.Scale.as_view(),
urls.url(r'^(?P<plan_id>[^/]+)/scale$', views.Scale.as_view(),
name='scale'),
urls.url(r'^(?P<overcloud_id>[^/]+)/role/(?P<role_id>[^/]+)$',
urls.url(r'^(?P<plan_id>[^/]+)/role/(?P<role_id>[^/]+)$',
views.OvercloudRoleView.as_view(), name='role'),
urls.url(r'^(?P<overcloud_id>[^/]+)/undeploy-confirmation$',
urls.url(r'^(?P<plan_id>[^/]+)/undeploy-confirmation$',
views.UndeployConfirmationView.as_view(),
name='undeploy_confirmation'),
)

View File

@ -42,15 +42,15 @@ UNDEPLOY_IN_PROGRESS_URL = (
'horizon:infrastructure:overcloud:undeploy_in_progress')
class OvercloudMixin(object):
class OvercloudPlanMixin(object):
@memoized.memoized
def get_overcloud(self, redirect=None):
def get_plan(self, redirect=None):
if redirect is None:
redirect = reverse(INDEX_URL)
overcloud_id = self.kwargs['overcloud_id']
overcloud = api.tuskar.Overcloud.get(self.request, overcloud_id,
_error_redirect=redirect)
return overcloud
plan_id = self.kwargs['plan_id']
plan = api.tuskar.OvercloudPlan.get(self.request, plan_id,
_error_redirect=redirect)
return plan
class OvercloudRoleMixin(object):
@ -68,19 +68,19 @@ class IndexView(base_views.RedirectView):
def get_redirect_url(self):
try:
# TODO(lsmola) implement this properly when supported by API
overcloud = api.tuskar.Overcloud.get_the_overcloud(self.request)
plan = api.tuskar.OvercloudPlan.get_the_plan(self.request)
except heatclient.exc.HTTPNotFound:
overcloud = None
plan = None
redirect = None
if overcloud is None:
if plan is None:
redirect = reverse(CREATE_URL)
elif overcloud.is_deleting or overcloud.is_delete_failed:
elif plan.stack.is_deleting or plan.stack.is_delete_failed:
redirect = reverse(UNDEPLOY_IN_PROGRESS_URL,
args=(overcloud.id,))
args=(plan.id,))
else:
redirect = reverse(DETAIL_URL,
args=(overcloud.id,))
args=(plan.id,))
return redirect
@ -90,17 +90,18 @@ class CreateView(horizon.workflows.WorkflowView):
template_name = 'infrastructure/_fullscreen_workflow_base.html'
class DetailView(horizon_tabs.TabView, OvercloudMixin):
class DetailView(horizon_tabs.TabView, OvercloudPlanMixin):
tab_group_class = tabs.DetailTabs
template_name = 'infrastructure/overcloud/detail.html'
def get_tabs(self, request, **kwargs):
overcloud = self.get_overcloud()
return self.tab_group_class(request, overcloud=overcloud, **kwargs)
plan = self.get_plan()
return self.tab_group_class(request, plan=plan, **kwargs)
def get_context_data(self, **kwargs):
context = super(DetailView, self).get_context_data(**kwargs)
context['overcloud'] = self.get_overcloud()
context['plan'] = self.get_plan()
context['stack'] = self.get_plan().stack
return context
@ -114,61 +115,61 @@ class UndeployConfirmationView(horizon.forms.ModalFormView):
def get_context_data(self, **kwargs):
context = super(UndeployConfirmationView,
self).get_context_data(**kwargs)
context['overcloud_id'] = self.kwargs['overcloud_id']
context['plan_id'] = self.kwargs['plan_id']
return context
def get_initial(self, **kwargs):
initial = super(UndeployConfirmationView, self).get_initial(**kwargs)
initial['overcloud_id'] = self.kwargs['overcloud_id']
initial['plan_id'] = self.kwargs['plan_id']
return initial
class UndeployInProgressView(horizon_tabs.TabView, OvercloudMixin, ):
class UndeployInProgressView(horizon_tabs.TabView, OvercloudPlanMixin, ):
tab_group_class = tabs.UndeployInProgressTabs
template_name = 'infrastructure/overcloud/detail.html'
def get_overcloud_or_redirect(self):
def get_overcloud_plan_or_redirect(self):
try:
# TODO(lsmola) implement this properly when supported by API
overcloud = api.tuskar.Overcloud.get_the_overcloud(self.request)
plan = api.tuskar.OvercloudPlan.get_the_plan(self.request)
except heatclient.exc.HTTPNotFound:
overcloud = None
plan = None
if overcloud is None:
if plan is None:
redirect = reverse(CREATE_URL)
messages.success(self.request,
_("Undeploying of the Overcloud has finished."))
raise horizon_exceptions.Http302(redirect)
elif overcloud.is_deleting or overcloud.is_delete_failed:
return overcloud
elif plan.stack.is_deleting or plan.stack.is_delete_failed:
return plan
else:
messages.error(self.request,
_("Overcloud is not being undeployed."))
redirect = reverse(DETAIL_URL,
args=(overcloud.id,))
args=(plan.id,))
raise horizon_exceptions.Http302(redirect)
def get_tabs(self, request, **kwargs):
overcloud = self.get_overcloud_or_redirect()
return self.tab_group_class(request, overcloud=overcloud, **kwargs)
plan = self.get_overcloud_plan_or_redirect()
return self.tab_group_class(request, plan=plan, **kwargs)
def get_context_data(self, **kwargs):
context = super(UndeployInProgressView,
self).get_context_data(**kwargs)
context['overcloud'] = self.get_overcloud_or_redirect()
context['plan'] = self.get_overcloud_plan_or_redirect()
return context
class Scale(horizon.workflows.WorkflowView, OvercloudMixin):
class Scale(horizon.workflows.WorkflowView, OvercloudPlanMixin):
workflow_class = scale.Workflow
def get_context_data(self, **kwargs):
context = super(Scale, self).get_context_data(**kwargs)
context['overcloud_id'] = self.kwargs['overcloud_id']
context['plan_id'] = self.kwargs['plan_id']
return context
def get_initial(self):
overcloud = self.get_overcloud()
plan = self.get_plan()
overcloud_roles = dict((overcloud_role.id, overcloud_role)
for overcloud_role in
api.tuskar.OvercloudRole.list(self.request))
@ -177,40 +178,40 @@ class Scale(horizon.workflows.WorkflowView, OvercloudMixin):
(count['overcloud_role_id'],
overcloud_roles[count['overcloud_role_id']].flavor_id),
count['num_nodes'],
) for count in overcloud.counts)
) for count in plan.counts)
return {
'overcloud_id': overcloud.id,
'plan_id': plan.id,
'role_counts': role_counts,
}
class OvercloudRoleView(horizon_tables.DataTableView,
OvercloudRoleMixin, OvercloudMixin):
OvercloudRoleMixin, OvercloudPlanMixin):
table_class = tables.OvercloudRoleNodeTable
template_name = 'infrastructure/overcloud/overcloud_role.html'
@memoized.memoized
def _get_nodes(self, overcloud, role):
resources = overcloud.resources(role, with_joins=True)
def _get_nodes(self, plan, role):
resources = plan.stack.resources_by_role(role, with_joins=True)
return [r.node for r in resources]
def get_data(self):
overcloud = self.get_overcloud()
plan = self.get_plan()
redirect = reverse(DETAIL_URL,
args=(overcloud.id,))
args=(plan.id,))
role = self.get_role(redirect)
return self._get_nodes(overcloud, role)
return self._get_nodes(plan, role)
def get_context_data(self, **kwargs):
context = super(OvercloudRoleView, self).get_context_data(**kwargs)
overcloud = self.get_overcloud()
plan = self.get_plan()
redirect = reverse(DETAIL_URL,
args=(overcloud.id,))
args=(plan.id,))
role = self.get_role(redirect)
context['role'] = role
context['image_name'] = role.image_name
context['nodes'] = self._get_nodes(overcloud, role)
context['nodes'] = self._get_nodes(plan, role)
try:
context['flavor'] = nova.flavor_get(self.request, role.flavor_id)

View File

@ -32,18 +32,18 @@ class Workflow(undeployed.DeploymentValidationMixin,
finalize_button_name = _("Apply Changes")
def handle(self, request, context):
overcloud_id = context['overcloud_id']
plan_id = context['plan_id']
try:
# TODO(lsmola) when updates are fixed in Heat, figure out whether
# we need to send also parameters, right now we send {}
api.tuskar.Overcloud.update(request, overcloud_id,
context['role_counts'], {})
api.tuskar.OvercloudPlan.update(request, plan_id,
context['role_counts'], {})
except Exception:
exceptions.handle(request, _('Unable to update deployment.'))
return False
return True
def get_success_url(self):
overcloud_id = self.context.get('overcloud_id', 1)
plan_id = self.context.get('plan_id', 1)
return reverse('horizon:infrastructure:overcloud:detail',
args=(overcloud_id,))
args=(plan_id,))

View File

@ -25,7 +25,7 @@ class Action(undeployed_overview.Action):
class Step(undeployed_overview.Step):
action_class = Action
contributes = ('role_counts', 'overcloud_id')
contributes = ('role_counts', 'plan_id')
template_name = 'infrastructure/overcloud/scale_node_counts.html'
def prepare_action_context(self, request, context):

View File

@ -65,8 +65,9 @@ class Workflow(DeploymentValidationMixin, horizon.workflows.Workflow):
def handle(self, request, context):
try:
api.tuskar.Overcloud.create(self.request, context['role_counts'],
context['configuration'])
api.tuskar.OvercloudPlan.create(
self.request, context['role_counts'],
context['configuration'])
except Exception as e:
# Showing error in both workflow tabs, because from the exception
# type we can't recognize where it should show

View File

@ -56,10 +56,10 @@ class Action(horizon.workflows.Action):
def __init__(self, request, *args, **kwargs):
super(Action, self).__init__(request, *args, **kwargs)
parameters = api.tuskar.Overcloud.template_parameters(request).items()
parameters.sort()
params = api.tuskar.OvercloudPlan.template_parameters(request).items()
params.sort()
for name, data in parameters:
for name, data in params:
self.fields[name] = make_field(name, **data)
def clean(self):

View File

@ -13,8 +13,10 @@
from __future__ import absolute_import
import mock
from mock import patch # noqa
from heatclient.v1 import events
from novaclient.v1_1 import servers
from tuskar_ui import api
@ -22,18 +24,128 @@ from tuskar_ui.test import helpers as test
class HeatAPITests(test.APITestCase):
def test_overcloud_stack(self):
stack = self.heatclient_stacks.first()
ocs = api.heat.OvercloudStack(
self.tuskarclient_overcloud_plans.first(),
request=object())
with patch('openstack_dashboard.api.heat.stack_get',
return_value=stack):
ret_val = ocs
self.assertIsInstance(ret_val, api.heat.OvercloudStack)
def test_overcloud_stack_events(self):
event_list = self.heatclient_events.list()
stack = self.heatclient_stacks.first()
with patch('openstack_dashboard.api.heat.events_list',
return_value=event_list):
ret_val = api.heat.OvercloudStack(stack).events
for e in ret_val:
self.assertIsInstance(e, events.Event)
self.assertEqual(8, len(ret_val))
def test_overcloud_stack_is_deployed(self):
stack = api.heat.OvercloudStack(self.heatclient_stacks.first())
ret_val = stack.is_deployed
self.assertFalse(ret_val)
def test_overcloud_stack_resources(self):
stack = api.heat.OvercloudStack(self.heatclient_stacks.first())
resources = self.heatclient_resources.list()
nodes = self.baremetalclient_nodes.list()
instances = []
with patch('openstack_dashboard.api.base.is_service_enabled',
return_value=False):
with patch('openstack_dashboard.api.heat.resources_list',
return_value=resources):
with patch('openstack_dashboard.api.nova.server_list',
return_value=(instances, None)):
with patch('novaclient.v1_1.contrib.baremetal.'
'BareMetalNodeManager.list',
return_value=nodes):
ret_val = stack.resources()
for i in ret_val:
self.assertIsInstance(i, api.heat.Resource)
self.assertEqual(4, len(ret_val))
def test_overcloud_stack_resources_no_ironic(self):
stack = api.heat.OvercloudStack(self.heatclient_stacks.first())
role = api.tuskar.OvercloudRole(
self.tuskarclient_overcloud_roles.first())
# FIXME(lsmola) only resources and image_name should be tested
# here, anybody has idea how to do that?
image = self.glanceclient_images.first()
resources = self.heatclient_resources.list()
instances = self.novaclient_servers.list()
nodes = self.baremetalclient_nodes.list()
with patch('openstack_dashboard.api.base.is_service_enabled',
return_value=False):
with patch('openstack_dashboard.api.heat.resources_list',
return_value=resources) as resource_list:
with patch('openstack_dashboard.api.nova.server_list',
return_value=(instances, None)) as server_list:
with patch('openstack_dashboard.api.glance.image_get',
return_value=image) as image_get:
with patch('novaclient.v1_1.contrib.baremetal.'
'BareMetalNodeManager.list',
return_value=nodes) as node_list:
ret_val = stack.resources_by_role(role)
self.assertEqual(resource_list.call_count, 1)
self.assertEqual(server_list.call_count, 1)
self.assertEqual(image_get.call_count, 2)
self.assertEqual(node_list.call_count, 1)
for i in ret_val:
self.assertIsInstance(i, api.heat.Resource)
self.assertEqual(4, len(ret_val))
def test_overcloud_stack_keystone_ip(self):
stack = api.heat.OvercloudStack(self.heatclient_stacks.first())
self.assertEqual('192.0.2.23', stack.keystone_ip)
def test_overcloud_stack_dashboard_url(self):
stack = api.heat.OvercloudStack(self.heatclient_stacks.first())
stack._plan = api.tuskar.OvercloudPlan(
self.tuskarclient_overcloud_plans.first())
mocked_service = mock.Mock(id='horizon_id')
mocked_service.name = 'horizon'
services = [mocked_service]
endpoints = [mock.Mock(service_id='horizon_id',
adminurl='http://192.0.2.23:/admin'), ]
services_obj = mock.Mock(
**{'list.return_value': services, })
endpoints_obj = mock.Mock(
**{'list.return_value': endpoints, })
overcloud_keystone_client = mock.Mock(
services=services_obj,
endpoints=endpoints_obj)
with patch('tuskar_ui.api.heat.overcloud_keystoneclient',
return_value=overcloud_keystone_client) as client_get:
self.assertEqual(['http://192.0.2.23:/admin'],
stack.dashboard_urls)
self.assertEqual(client_get.call_count, 1)
def test_resource_get(self):
stack = self.heatclient_stacks.first()
overcloud = api.tuskar.Overcloud(self.tuskarclient_overclouds.first(),
request=object())
resource = self.heatclient_resources.first()
with patch('openstack_dashboard.api.heat.resource_get',
return_value=resource):
with patch('openstack_dashboard.api.heat.stack_get',
return_value=stack):
ret_val = api.heat.Resource.get(None, overcloud,
ret_val = api.heat.Resource.get(None, stack,
resource.resource_name)
self.assertIsInstance(ret_val, api.heat.Resource)

View File

@ -22,7 +22,6 @@ from tuskar_ui.test import helpers as test
class NodeAPITests(test.APITestCase):
def test_node_create(self):
node = api.node.BareMetalNode(self.baremetalclient_nodes.first())

View File

@ -14,201 +14,42 @@
from __future__ import absolute_import
import contextlib
import mock
from mock import patch # noqa
from heatclient.v1 import events
from heatclient.v1 import stacks
from tuskar_ui import api
from tuskar_ui.test import helpers as test
class TuskarAPITests(test.APITestCase):
def test_overcloud_create(self):
overcloud = self.tuskarclient_overclouds.first()
def test_overcloud_plan_create(self):
plan = self.tuskarclient_overcloud_plans.first()
with patch('tuskarclient.v1.overclouds.OvercloudManager.create',
return_value=overcloud):
ret_val = api.tuskar.Overcloud.create(self.request, {}, {})
self.assertIsInstance(ret_val, api.tuskar.Overcloud)
return_value=plan):
ret_val = api.tuskar.OvercloudPlan.create(self.request, {}, {})
self.assertIsInstance(ret_val, api.tuskar.OvercloudPlan)
def test_overcloud_list(self):
overclouds = self.tuskarclient_overclouds.list()
def test_overcloud_plan_list(self):
plans = self.tuskarclient_overcloud_plans.list()
with patch('tuskarclient.v1.overclouds.OvercloudManager.list',
return_value=overclouds):
ret_val = api.tuskar.Overcloud.list(self.request)
for oc in ret_val:
self.assertIsInstance(oc, api.tuskar.Overcloud)
return_value=plans):
ret_val = api.tuskar.OvercloudPlan.list(self.request)
for plan in ret_val:
self.assertIsInstance(plan, api.tuskar.OvercloudPlan)
self.assertEqual(1, len(ret_val))
def test_overcloud_get(self):
overcloud = self.tuskarclient_overclouds.first()
def test_overcloud_plan_get(self):
plan = self.tuskarclient_overcloud_plans.first()
with patch('tuskarclient.v1.overclouds.OvercloudManager.list',
return_value=[overcloud]):
ret_val = api.tuskar.Overcloud.get(self.request, overcloud.id)
return_value=[plan]):
ret_val = api.tuskar.OvercloudPlan.get(self.request, plan.id)
self.assertIsInstance(ret_val, api.tuskar.Overcloud)
self.assertIsInstance(ret_val, api.tuskar.OvercloudPlan)
def test_overcloud_delete(self):
overcloud = self.tuskarclient_overclouds.first()
def test_overcloud_plan_delete(self):
plan = self.tuskarclient_overcloud_plans.first()
with patch('tuskarclient.v1.overclouds.OvercloudManager.delete',
return_value=None):
api.tuskar.Overcloud.delete(self.request, overcloud.id)
def test_overcloud_stack(self):
stack = self.heatclient_stacks.first()
oc = api.tuskar.Overcloud(self.tuskarclient_overclouds.first(),
request=object())
with patch('openstack_dashboard.api.heat.stack_get',
return_value=stack):
ret_val = oc.stack
self.assertIsInstance(ret_val, stacks.Stack)
def test_overcloud_stack_events(self):
overcloud = self.tuskarclient_overclouds.first()
event_list = self.heatclient_events.list()
stack = self.heatclient_stacks.first()
with patch('openstack_dashboard.api.heat.stack_get',
return_value=stack):
with patch('openstack_dashboard.api.heat.events_list',
return_value=event_list):
ret_val = api.tuskar.Overcloud(overcloud).stack_events
for e in ret_val:
self.assertIsInstance(e, events.Event)
self.assertEqual(8, len(ret_val))
def test_overcloud_stack_events_empty(self):
overcloud = self.tuskarclient_overclouds.first()
event_list = self.heatclient_events.list()
overcloud.stack_id = None
with patch('openstack_dashboard.api.heat.stack_get',
return_value=None):
with patch('openstack_dashboard.api.heat.events_list',
return_value=event_list):
ret_val = api.tuskar.Overcloud(overcloud).stack_events
self.assertListEqual([], ret_val)
def test_overcloud_is_deployed(self):
stack = self.heatclient_stacks.first()
oc = api.tuskar.Overcloud(self.tuskarclient_overclouds.first(),
request=object())
with patch('openstack_dashboard.api.heat.stack_get',
return_value=stack):
ret_val = oc.is_deployed
self.assertFalse(ret_val)
def test_overcloud_all_resources(self):
oc = api.tuskar.Overcloud(self.tuskarclient_overclouds.first(),
request=object())
# FIXME(lsmola) the stack call should not be tested in this unit test
# anybody has idea how to do it?
stack = self.heatclient_stacks.first()
resources = self.heatclient_resources.list()
nodes = self.baremetalclient_nodes.list()
instances = []
with patch('openstack_dashboard.api.base.is_service_enabled',
return_value=False):
with patch('openstack_dashboard.api.heat.resources_list',
return_value=resources):
with patch('openstack_dashboard.api.nova.server_list',
return_value=(instances, None)):
with patch('novaclient.v1_1.contrib.baremetal.'
'BareMetalNodeManager.list',
return_value=nodes):
with patch('openstack_dashboard.api.heat.stack_get',
return_value=stack):
ret_val = oc.all_resources()
for i in ret_val:
self.assertIsInstance(i, api.heat.Resource)
self.assertEqual(4, len(ret_val))
def test_overcloud_resources_no_ironic(self):
oc = api.tuskar.Overcloud(self.tuskarclient_overclouds.first(),
request=object())
role = api.tuskar.OvercloudRole(
self.tuskarclient_overcloud_roles.first())
# FIXME(lsmola) only all_resources and image_name should be tested
# here, anybody has idea how to do that?
image = self.glanceclient_images.first()
stack = self.heatclient_stacks.first()
resources = self.heatclient_resources.list()
instances = self.novaclient_servers.list()
nodes = self.baremetalclient_nodes.list()
with patch('openstack_dashboard.api.base.is_service_enabled',
return_value=False):
with patch('openstack_dashboard.api.heat.resources_list',
return_value=resources) as resource_list:
with patch('openstack_dashboard.api.nova.server_list',
return_value=(instances, None)) as server_list:
with patch('openstack_dashboard.api.glance.image_get',
return_value=image) as image_get:
with patch('novaclient.v1_1.contrib.baremetal.'
'BareMetalNodeManager.list',
return_value=nodes) as node_list:
with patch(
'openstack_dashboard.api.heat.stack_get',
return_value=stack) as stack_get:
ret_val = oc.resources(role)
self.assertEqual(resource_list.call_count, 1)
self.assertEqual(server_list.call_count, 1)
self.assertEqual(image_get.call_count, 2)
self.assertEqual(node_list.call_count, 1)
self.assertEqual(stack_get.call_count, 1)
for i in ret_val:
self.assertIsInstance(i, api.heat.Resource)
self.assertEqual(4, len(ret_val))
def test_overcloud_keystone_ip(self):
oc = api.tuskar.Overcloud(self.tuskarclient_overclouds.first(),
request=object())
stack = self.heatclient_stacks.first()
with contextlib.nested(
patch('openstack_dashboard.api.heat.stack_get',
return_value=stack)) as (stack_get, ):
self.assertEqual('192.0.2.23', oc.keystone_ip)
self.assertEqual(stack_get.call_count, 1)
def test_overcloud_dashboard_url(self):
oc = api.tuskar.Overcloud(self.tuskarclient_overclouds.first(),
request=object())
stack = self.heatclient_stacks.first()
mocked_service = mock.Mock(id='horizon_id')
mocked_service.name = 'horizon'
services = [mocked_service]
endpoints = [mock.Mock(service_id='horizon_id',
adminurl='http://192.0.2.23:/admin'), ]
services_obj = mock.Mock(
**{'list.return_value': services, })
endpoints_obj = mock.Mock(
**{'list.return_value': endpoints, })
overcloud_keystone_client = mock.Mock(
services=services_obj,
endpoints=endpoints_obj)
with contextlib.nested(
patch('openstack_dashboard.api.heat.stack_get',
return_value=stack),
patch('tuskar_ui.api.tuskar.overcloud_keystoneclient',
return_value=overcloud_keystone_client)
) as (stack_get, client_get):
self.assertEqual(['http://192.0.2.23:/admin'],
oc.dashboard_urls)
self.assertEqual(stack_get.call_count, 1)
self.assertEqual(client_get.call_count, 1)
api.tuskar.OvercloudPlan.delete(self.request, plan.id)
def test_overcloud_role_list(self):
roles = self.tuskarclient_overcloud_roles.list()

View File

@ -19,19 +19,19 @@ from tuskarclient.v1 import overclouds
def data(TEST):
# Overcloud
TEST.tuskarclient_overclouds = test_data_utils.TestDataContainer()
TEST.tuskarclient_overcloud_plans = test_data_utils.TestDataContainer()
# TODO(Tzu-Mainn Chen): fix these to create Tuskar Overcloud objects
# once the api supports it
oc_1 = overclouds.Overcloud(
overclouds.OvercloudManager(None),
{'id': 1,
'stack_id': 'stack-id-1',
'name': 'overcloud',
'description': 'overcloud',
'stack_id': 'stack-id-1',
'attributes': {
'AdminPassword': "unset"
}})
TEST.tuskarclient_overclouds.add(oc_1)
TEST.tuskarclient_overcloud_plans.add(oc_1)
# OvercloudRole
TEST.tuskarclient_overcloud_roles = test_data_utils.TestDataContainer()

View File

@ -19,3 +19,18 @@ CAMEL_RE = re.compile(r'([a-z]|SSL)([A-Z])')
def de_camel_case(text):
"""Convert CamelCase names to human-readable format."""
return CAMEL_RE.sub(lambda m: m.group(1) + ' ' + m.group(2), text)
def list_to_dict(object_list, key_attribute='id'):
"""Converts an object list to a dict
:param object_list: list of objects to be put into a dict
:type object_list: list
:param key_attribute: object attribute used as index by dict
:type key_attribute: str
:return: dict containing the objects in the list
:rtype: dict
"""
return dict((getattr(o, key_attribute), o) for o in object_list)