Code update for new Tuskar API

* Currently uses mock data
* Many tests have call_count asserts removed
* Many tests are removed pending additional API refactoring
* Plan creation workflow needs to be fixed; successive
  workflow steps depend on initial role-selection step
* Overcloud and plans index redirect logic may need to be rethought
* Plans need to take responsibility for image and flavor (instead of
  role)

Change-Id: I3299a66b4f11b74196d88c458b276cad0851cbab
This commit is contained in:
Tzu-Mainn Chen 2014-07-01 20:13:30 +02:00
parent b919af12f3
commit a18f40554c
46 changed files with 935 additions and 1215 deletions

View File

@ -14,13 +14,18 @@ import logging
from django.utils.translation import ugettext_lazy as _
from horizon.utils import memoized
from openstack_dashboard.api import nova
from openstack_dashboard.test.test_data import utils as test_utils
from tuskar_ui.api import tuskar
from tuskar_ui.cached_property import cached_property # noqa
from tuskar_ui.handle_errors import handle_errors # noqa
from tuskar_ui.test.test_data import flavor_data
from tuskar_ui.test.test_data import heat_data
TEST_DATA = test_utils.TestDataContainer()
flavor_data.data(TEST_DATA)
heat_data.data(TEST_DATA)
LOG = logging.getLogger(__name__)
@ -77,29 +82,27 @@ class Flavor(object):
@classmethod
def create(cls, request, name, memory, vcpus, disk, cpu_arch,
kernel_image_id, ramdisk_image_id):
extras_dict = {'cpu_arch': cpu_arch,
'baremetal:deploy_kernel_id': kernel_image_id,
'baremetal:deploy_ramdisk_id': ramdisk_image_id}
return cls(nova.flavor_create(request, name, memory, vcpus, disk,
metadata=extras_dict))
return cls(TEST_DATA.novaclient_flavors.first(),
request=request)
@classmethod
@handle_errors(_("Unable to load flavor."))
def get(cls, request, flavor_id):
return cls(nova.flavor_get(request, flavor_id))
for flavor in Flavor.list(request):
if flavor.id == flavor_id:
return flavor
@classmethod
@handle_errors(_("Unable to retrieve flavor list."), [])
def list(cls, request):
return [cls(item) for item in nova.flavor_list(request)]
flavors = TEST_DATA.novaclient_flavors.list()
return [cls(flavor) for flavor in flavors]
@classmethod
@memoized.memoized
@handle_errors(_("Unable to retrieve existing servers list."), [])
def list_deployed_ids(cls, request):
"""Get and memoize ID's of deployed flavors."""
servers = nova.server_list(request)[0]
servers = TEST_DATA.novaclient_servers.list()
deployed_ids = set(server.flavor['id'] for server in servers)
roles = tuskar.OvercloudRole.list(request)
deployed_ids |= set(role.flavor_id for role in roles)
return deployed_ids

View File

@ -10,23 +10,30 @@
# License for the specific language governing permissions and limitations
# under the License.
import heatclient
import keystoneclient.exceptions
import logging
import urlparse
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon.utils import memoized
from openstack_dashboard.api import base
from openstack_dashboard.api import heat
from openstack_dashboard.api import keystone
from openstack_dashboard.test.test_data import utils as test_utils
from tuskar_ui.api import node
from tuskar_ui.api import tuskar
from tuskar_ui.cached_property import cached_property # noqa
from tuskar_ui.handle_errors import handle_errors # noqa
from tuskar_ui.test.test_data import heat_data
from tuskar_ui import utils
TEST_DATA = test_utils.TestDataContainer()
heat_data.data(TEST_DATA)
LOG = logging.getLogger(__name__)
@ -69,25 +76,41 @@ def overcloud_keystoneclient(request, endpoint, password):
return conn
class OvercloudStack(base.APIResourceWrapper):
class Stack(base.APIResourceWrapper):
_attrs = ('id', 'stack_name', 'outputs', 'stack_status', 'parameters')
def __init__(self, apiresource, request=None, plan=None):
super(OvercloudStack, self).__init__(apiresource)
def __init__(self, apiresource, request=None):
super(Stack, self).__init__(apiresource)
self._request = request
self._plan = plan
@classmethod
def get(cls, request, stack_id, plan=None):
"""Return the Heat Stack associated with the stack_id
@handle_errors(_("Unable to retrieve heat stacks"), [])
def list(cls, request):
"""Return a list of stacks in Heat
:param request: request object
:type request: django.http.HttpRequest
:return: list of Heat stacks, or an empty list if there
are none
:rtype: list of tuskar_ui.api.heat.Stack
"""
stacks = TEST_DATA.heatclient_stacks.list()
return [cls(stack, request=request) for stack in stacks]
@classmethod
@handle_errors(_("Unable to retrieve stack"))
def get(cls, request, stack_id):
"""Return the Heat Stack associated with this Overcloud
:return: Heat Stack associated with the stack_id; or None
if no Stack is associated, or no Stack can be
found
:rtype: heatclient.v1.stacks.Stack or None
:rtype: tuskar_ui.api.heat.Stack or None
"""
stack = heat.stack_get(request, stack_id)
return cls(stack, request=request, plan=plan)
for stack in Stack.list(request):
if stack.id == stack_id:
return stack
@memoized.memoized
def resources(self, with_joins=True):
@ -100,14 +123,8 @@ class OvercloudStack(base.APIResourceWrapper):
:return: list of all Resources or an empty list if there are none
:rtype: list of tuskar_ui.api.heat.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 = []
resources = [r for r in TEST_DATA.heatclient_resources.list() if
r.stack_id == self.id]
if not with_joins:
return [Resource(r, request=self._request)
@ -144,8 +161,7 @@ class OvercloudStack(base.APIResourceWrapper):
# nova instance
resources = self.resources(with_joins)
filtered_resources = [resource for resource in resources if
(overcloud_role.is_deployed_on_node(
resource.node))]
(resource.has_role(overcloud_role))]
return filtered_resources
@ -169,6 +185,19 @@ class OvercloudStack(base.APIResourceWrapper):
resources = self.resources_by_role(overcloud_role)
return len(resources)
@cached_property
def plan(self):
"""return associated OvercloudPlan if a plan_id exists within stack
parameters.
:return: associated OvercloudPlan if plan_id exists and a matching plan
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'])
@cached_property
def is_deployed(self):
"""Check if this Stack is successfully deployed.
@ -251,9 +280,9 @@ class OvercloudStack(base.APIResourceWrapper):
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.')
self.plan.parameter_value('AdminPassword'))
except Exception:
LOG.debug('Unable to connect to overcloud keystone.')
return None
@cached_property
@ -318,9 +347,57 @@ class Resource(base.APIResourceWrapper):
matches the resource name
:rtype: tuskar_ui.api.heat.Resource
"""
resource = heat.resource_get(stack.id,
resource_name)
return cls(resource, request=request)
for r in TEST_DATA.heatclient_resources.list():
if r.stack_id == stack.id and r.resource_name == resource_name:
return cls(stack, request=request)
@classmethod
def get_by_node(cls, request, node):
"""Return the specified Heat Resource given a Node
:param request: request object
:type request: django.http.HttpRequest
:param node: node to match
:type node: tuskar_ui.api.node.Node
:return: matching Resource, or None if no Resource matches
the Node
:rtype: tuskar_ui.api.heat.Resource
"""
# TODO(tzumainn): this is terribly inefficient, but I don't see a
# better way. Maybe if Heat set some node metadata. . . ?
if node.instance_uuid:
for stack in Stack.list(request):
for resource in stack.resources(with_joins=False):
if resource.physical_resource_id == node.instance_uuid:
return resource
msg = _('Could not find resource matching node "%s"') % node.uuid
raise exceptions.NotFound(msg)
@cached_property
def role(self):
"""Return the OvercloudRole associated with this Resource
:return: OvercloudRole associated with this Resource, or None if no
OvercloudRole is associated
:rtype: tuskar_ui.api.tuskar.OvercloudRole
"""
roles = tuskar.OvercloudRole.list(self._request)
for role in roles:
if self.has_role(role):
return role
def has_role(self, role):
"""Determine whether a resources matches an overcloud role
:param role: role to check against
:type role: tuskar_ui.api.tuskar.OvercloudRole
:return: does this resource match the overcloud_role?
:rtype: bool
"""
return self.resource_type == role.provider_resource_type
@cached_property
def node(self):

View File

@ -14,17 +14,23 @@ import logging
from django.utils.translation import ugettext_lazy as _
from horizon.utils import memoized
from ironicclient.v1 import client as ironicclient
from novaclient.v1_1.contrib import baremetal
from openstack_dashboard.api import base
from openstack_dashboard.api import glance
from openstack_dashboard.api import nova
from openstack_dashboard.test.test_data import utils as test_utils
from tuskar_ui.cached_property import cached_property # noqa
from tuskar_ui.handle_errors import handle_errors # noqa
from tuskar_ui.test.test_data import heat_data
from tuskar_ui.test.test_data import node_data
from tuskar_ui import utils
TEST_DATA = test_utils.TestDataContainer()
node_data.data(TEST_DATA)
heat_data.data(TEST_DATA)
LOG = logging.getLogger(__name__)
@ -87,20 +93,7 @@ class IronicNode(base.APIResourceWrapper):
:return: the created Node object
:rtype: tuskar_ui.api.node.IronicNode
"""
node = ironicclient(request).node.create(
driver='pxe_ipmitool',
driver_info={'ipmi_address': ipmi_address,
'ipmi_username': ipmi_username,
'password': ipmi_password},
properties={'cpu': cpu,
'ram': ram,
'local_disk': local_disk})
for mac_address in mac_addresses:
ironicclient(request).port.create(
node_uuid=node.uuid,
address=mac_address
)
node = TEST_DATA.ironicclient_nodes.first()
return cls(node)
@classmethod
@ -116,8 +109,9 @@ class IronicNode(base.APIResourceWrapper):
:return: matching IronicNode, or None if no IronicNode matches the ID
:rtype: tuskar_ui.api.node.IronicNode
"""
node = ironicclient(request).nodes.get(uuid)
return cls(node)
for node in IronicNode.list(request):
if node.uuid == uuid:
return node
@classmethod
def get_by_instance_uuid(cls, request, instance_uuid):
@ -136,8 +130,9 @@ class IronicNode(base.APIResourceWrapper):
:raises: ironicclient.exc.HTTPNotFound if there is no IronicNode with
the matching instance UUID
"""
node = ironicclient(request).nodes.get_by_instance_uuid(instance_uuid)
return cls(node)
for node in IronicNode.list(request):
if node.instance_uuid == instance_uuid:
return node
@classmethod
@handle_errors(_("Unable to retrieve nodes"), [])
@ -155,8 +150,15 @@ class IronicNode(base.APIResourceWrapper):
:return: list of IronicNodes, or an empty list if there are none
:rtype: list of tuskar_ui.api.node.IronicNode
"""
nodes = ironicclient(request).nodes.list(
associated=associated)
nodes = TEST_DATA.ironicclient_nodes.list()
if associated is not None:
if associated:
nodes = [node for node in nodes
if node.instance_uuid is not None]
else:
nodes = [node for node in nodes
if node.instance_uuid is None]
return [cls(node) for node in nodes]
@classmethod
@ -170,7 +172,6 @@ class IronicNode(base.APIResourceWrapper):
:param uuid: ID of IronicNode to be removed
:type uuid: str
"""
ironicclient(request).nodes.delete(uuid)
return
@cached_property
@ -222,15 +223,7 @@ class BareMetalNode(base.APIResourceWrapper):
:return: the created BareMetalNode object
:rtype: tuskar_ui.api.node.BareMetalNode
"""
node = baremetalclient(request).create(
'undercloud',
cpu,
ram,
local_disk,
mac_addresses,
pm_address=ipmi_address,
pm_user=ipmi_username,
pm_password=ipmi_password)
node = TEST_DATA.baremetalclient_nodes.first()
return cls(node)
@classmethod
@ -247,9 +240,9 @@ class BareMetalNode(base.APIResourceWrapper):
the ID
:rtype: tuskar_ui.api.node.BareMetalNode
"""
node = baremetalclient(request).get(uuid)
return cls(node)
for node in BareMetalNode.list(request):
if node.uuid == uuid:
return node
@classmethod
def get_by_instance_uuid(cls, request, instance_uuid):
@ -268,10 +261,9 @@ class BareMetalNode(base.APIResourceWrapper):
:raises: ironicclient.exc.HTTPNotFound if there is no BareMetalNode
with the matching instance UUID
"""
nodes = baremetalclient(request).list()
node = next((n for n in nodes if instance_uuid == n.instance_uuid),
None)
return cls(node)
for node in BareMetalNode.list(request):
if node.instance_uuid == instance_uuid:
return node
@classmethod
def list(cls, request, associated=None):
@ -288,7 +280,7 @@ class BareMetalNode(base.APIResourceWrapper):
:return: list of BareMetalNodes, or an empty list if there are none
:rtype: list of tuskar_ui.api.node.BareMetalNode
"""
nodes = baremetalclient(request).list()
nodes = TEST_DATA.baremetalclient_nodes.list()
if associated is not None:
if associated:
nodes = [node for node in nodes
@ -308,7 +300,6 @@ class BareMetalNode(base.APIResourceWrapper):
:param uuid: ID of BareMetalNode to be removed
:type uuid: str
"""
baremetalclient(request).delete(uuid)
return
@cached_property
@ -427,9 +418,8 @@ class Node(base.APIResourceWrapper):
@handle_errors(_("Unable to retrieve node"))
def get(cls, request, uuid):
node = NodeClient(request).node_class.get(request, uuid)
if node.instance_uuid is not None:
server = nova.server_get(request, node.instance_uuid)
server = TEST_DATA.novaclient_servers.first()
return cls(node, instance=server, request=request)
return cls(node)
@ -439,7 +429,7 @@ class Node(base.APIResourceWrapper):
def get_by_instance_uuid(cls, request, instance_uuid):
node = NodeClient(request).node_class.get_by_instance_uuid(
request, instance_uuid)
server = nova.server_get(request, instance_uuid)
server = TEST_DATA.novaclient_servers.first()
return cls(node, instance=server, request=request)
@classmethod
@ -449,8 +439,7 @@ class Node(base.APIResourceWrapper):
request, associated=associated)
if associated is None or associated:
servers, has_more_data = nova.server_list(request)
servers = TEST_DATA.novaclient_servers.list()
servers_dict = utils.list_to_dict(servers)
nodes_with_instance = []
for n in nodes:
@ -478,7 +467,7 @@ class Node(base.APIResourceWrapper):
return self._instance
if self.instance_uuid:
server = nova.server_get(self._request, self.instance_uuid)
server = TEST_DATA.novaclient_servers.first()
return server
return None

View File

@ -15,13 +15,17 @@ import logging
from django.utils.translation import ugettext_lazy as _
from openstack_dashboard.api import base
from openstack_dashboard.test.test_data import utils
from tuskarclient.v1 import client as tuskar_client
from tuskar_ui.api import heat
from tuskar_ui.cached_property import cached_property # noqa
from tuskar_ui.handle_errors import handle_errors # noqa
from tuskar_ui.test.test_data import tuskar_data
TEST_DATA = utils.TestDataContainer()
tuskar_data.data(TEST_DATA)
LOG = logging.getLogger(__name__)
TUSKAR_ENDPOINT_URL = getattr(django.conf.settings, 'TUSKAR_ENDPOINT_URL')
@ -33,66 +37,36 @@ def tuskarclient(request):
return c
def transform_sizing(overcloud_sizing):
"""Transform the sizing to simpler format
We need this till API will accept the more complex format with flavors,
then we delete this.
:param overcloud_sizing: overcloud sizing information with structure
{('overcloud_role_id',
'flavor_name'): count, ...}
:type overcloud_sizing: dict
:return: list of ('overcloud_role_id', 'num_nodes')
:rtype: list
"""
return [{
'overcloud_role_id': role,
'num_nodes': sizing,
} for (role, flavor), sizing in overcloud_sizing.items()]
class OvercloudPlan(base.APIResourceWrapper):
_attrs = ('id', 'stack_id', 'name', 'description', 'counts', 'attributes')
class OvercloudPlan(base.APIDictWrapper):
_attrs = ('id', 'name', 'description', 'created_at', 'modified_at',
'roles', 'parameters')
def __init__(self, apiresource, request=None):
super(OvercloudPlan, self).__init__(apiresource)
self._request = request
@classmethod
def create(cls, request, overcloud_sizing, overcloud_configuration):
def create(cls, request, name, description):
"""Create an OvercloudPlan in Tuskar
:param request: request object
:type request: django.http.HttpRequest
:param overcloud_sizing: overcloud sizing information with structure
{('overcloud_role_id',
'flavor_name'): count, ...}
:type overcloud_sizing: dict
:param name: plan name
:type name: string
:param overcloud_configuration: overcloud configuration with structure
{'key': 'value', ...}
:type overcloud_configuration: dict
:param description: plan description
:type description: string
:return: the created OvercloudPlan object
:rtype: tuskar_ui.api.tuskar.OvercloudPlan
"""
# TODO(lsmola) for now we have to transform the sizing to simpler
# format, till API will accept the more complex with flavors,
# then we delete this
transformed_sizing = transform_sizing(overcloud_sizing)
overcloud = tuskarclient(request).overclouds.create(
name='overcloud', description="Openstack cloud providing VMs",
counts=transformed_sizing, attributes=overcloud_configuration)
return cls(overcloud, request=request)
return cls(TEST_DATA.tuskarclient_plans.first(),
request=request)
@classmethod
def update(cls, request, overcloud_id, overcloud_sizing,
overcloud_configuration):
def update(cls, request, overcloud_id, name, description):
"""Update an OvercloudPlan in Tuskar
:param request: request object
@ -101,28 +75,17 @@ class OvercloudPlan(base.APIResourceWrapper):
:param overcloud_id: id of the overcloud we want to update
:type overcloud_id: string
:param overcloud_sizing: overcloud sizing information with structure
{('overcloud_role_id',
'flavor_name'): count, ...}
:type overcloud_sizing: dict
:param name: plan name
:type name: string
:param overcloud_configuration: overcloud configuration with structure
{'key': 'value', ...}
:type overcloud_configuration: dict
:param description: plan description
:type description: string
:return: the updated OvercloudPlan object
:rtype: tuskar_ui.api.tuskar.OvercloudPlan
"""
# TODO(lsmola) for now we have to transform the sizing to simpler
# format, till API will accept the more complex with flavors,
# then we delete this
transformed_sizing = transform_sizing(overcloud_sizing)
overcloud = tuskarclient(request).overclouds.update(
overcloud_id, counts=transformed_sizing,
attributes=overcloud_configuration)
return cls(overcloud, request=request)
return cls(TEST_DATA.tuskarclient_plans.first(),
request=request)
@classmethod
def list(cls, request):
@ -134,29 +97,26 @@ class OvercloudPlan(base.APIResourceWrapper):
:return: list of OvercloudPlans, or an empty list if there are none
:rtype: list of tuskar_ui.api.tuskar.OvercloudPlan
"""
ocs = tuskarclient(request).overclouds.list()
plans = TEST_DATA.tuskarclient_plans.list()
return [cls(oc, request=request) for oc in ocs]
return [cls(plan, request=request) for plan in plans]
@classmethod
@handle_errors(_("Unable to retrieve deployment"))
def get(cls, request, overcloud_id):
@handle_errors(_("Unable to retrieve plan"))
def get(cls, request, plan_id):
"""Return the OvercloudPlan that matches the ID
:param request: request object
:type request: django.http.HttpRequest
:param overcloud_id: id of OvercloudPlan to be retrieved
:type overcloud_id: int
:param plan_id: id of OvercloudPlan to be retrieved
:type plan_id: int
:return: matching OvercloudPlan, or None if no OvercloudPlan matches
the ID
:rtype: tuskar_ui.api.tuskar.OvercloudPlan
"""
# FIXME(lsmola) hack for Icehouse, only one Overcloud is allowed
# TODO(lsmola) uncomment when possible
# overcloud = tuskarclient(request).overclouds.get(overcloud_id)
# return cls(overcloud, request=request)
return cls.get_the_plan(request)
# TODO(lsmola) before will will support multiple overclouds, we
@ -175,47 +135,34 @@ class OvercloudPlan(base.APIResourceWrapper):
return plan
@classmethod
def delete(cls, request, overcloud_id):
def delete(cls, request, plan_id):
"""Delete an OvercloudPlan
:param request: request object
:type request: django.http.HttpRequest
:param overcloud_id: overcloud id
:type overcloud_id: int
:param plan_id: plan id
:type plan_id: int
"""
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
return
@cached_property
def stack(self):
"""Return the Heat Stack associated with this Overcloud
def role_list(self):
return [OvercloudRole(role) for role in self.roles]
: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
"""
return heat.OvercloudStack.get(self._request, self.stack_id,
plan=self)
def parameter(self, param_name):
for parameter in self.parameters:
if parameter['name'] == param_name:
return parameter
def parameter_value(self, param_name):
parameter = self.parameter(param_name)
if parameter is not None:
return parameter['value']
class OvercloudRole(base.APIResourceWrapper):
_attrs = ('id', 'name', 'description', 'image_name', 'flavor_id')
class OvercloudRole(base.APIDictWrapper):
_attrs = ('id', 'name', 'version', 'description', 'created_at')
@classmethod
@handle_errors(_("Unable to retrieve overcloud roles"), [])
@ -229,7 +176,7 @@ class OvercloudRole(base.APIResourceWrapper):
are none
:rtype: list of tuskar_ui.api.tuskar.OvercloudRole
"""
roles = tuskarclient(request).overcloud_roles.list()
roles = TEST_DATA.tuskarclient_roles.list()
return [cls(role) for role in roles]
@classmethod
@ -247,47 +194,30 @@ class OvercloudRole(base.APIResourceWrapper):
OvercloudRole can be found
:rtype: tuskar_ui.api.tuskar.OvercloudRole
"""
role = tuskarclient(request).overcloud_roles.get(role_id)
return cls(role)
@classmethod
@handle_errors(_("Unable to retrieve overcloud role"))
def get_by_node(cls, request, node):
"""Return the Tuskar OvercloudRole that is deployed on the node
:param request: request object
:type request: django.http.HttpRequest
:param node: node to check against
:type node: tuskar_ui.api.node.Node
:return: matching OvercloudRole, or None if no matching
OvercloudRole can be found
:rtype: tuskar_ui.api.tuskar.OvercloudRole
"""
roles = cls.list(request)
for role in roles:
if role.is_deployed_on_node(node):
for role in OvercloudRole.list(request):
if role.id == role_id:
return role
def update(self, request, **kwargs):
"""Update the selected attributes of Tuskar OvercloudRole.
# TODO(tzumainn): fix this once we know how a role corresponds to
# its provider resource type
@property
def provider_resource_type(self):
return self.name
:param request: request object
:type request: django.http.HttpRequest
"""
for attr in kwargs:
if attr not in self._attrs:
raise TypeError('Invalid parameter %r' % attr)
tuskarclient(request).overcloud_roles.update(self.id, **kwargs)
# TODO(tzumainn): fix this once we know how this connection can be
# made
@property
def node_count_parameter_name(self):
return self.name + 'NodeCount'
def is_deployed_on_node(self, node):
"""Determine whether a node matches an overcloud role
# TODO(tzumainn): fix this once we know how this connection can be
# made
@property
def image_id_parameter_name(self):
return self.name + 'ImageID'
:param node: node to check against
:type node: tuskar_ui.api.node.Node
:return: does this node match the overcloud_role?
:rtype: bool
"""
return self.image_name == node.image_name
# TODO(tzumainn): fix this once we know how this connection can be
# made
@property
def flavor_id_parameter_name(self):
return self.name + 'FlavorID'

View File

@ -21,6 +21,7 @@ class BasePanels(horizon.PanelGroup):
name = _("Infrastructure")
panels = (
'overcloud',
'plans',
'nodes',
'flavors',
)

View File

@ -67,19 +67,7 @@ def _prepare_create():
class FlavorsTest(test.BaseAdminViewTests):
def test_index(self):
roles = TEST_DATA.tuskarclient_overcloud_roles.list()
with contextlib.nested(
patch('openstack_dashboard.api.nova.flavor_list',
return_value=TEST_DATA.novaclient_flavors.list()),
patch('openstack_dashboard.api.nova.server_list',
return_value=([], False)),
patch('tuskar_ui.api.tuskar.OvercloudRole.list',
return_value=roles),
) as (flavors_mock, servers_mock, role_list_mock):
res = self.client.get(INDEX_URL)
self.assertEqual(flavors_mock.call_count, 1)
self.assertEqual(servers_mock.call_count, 1)
self.assertEqual(role_list_mock.call_count, 1)
res = self.client.get(INDEX_URL)
self.assertTemplateUsed(res, 'infrastructure/flavors/index.html')
@ -144,8 +132,6 @@ class FlavorsTest(test.BaseAdminViewTests):
res = self.client.post(INDEX_URL, data)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
self.assertEqual(delete_mock.call_count, 2)
self.assertEqual(server_list_mock.call_count, 1)
def test_delete_deployed_on_servers(self):
flavors = TEST_DATA.novaclient_flavors.list()
@ -175,39 +161,11 @@ class FlavorsTest(test.BaseAdminViewTests):
self.assertMessageCount(error=1, warning=0)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
self.assertEqual(delete_mock.call_count, 1)
self.assertEqual(server_list_mock.call_count, 1)
def test_delete_deployed_on_roles(self):
flavors = TEST_DATA.novaclient_flavors.list()
roles = TEST_DATA.tuskarclient_roles_with_flavors.list()
data = {'action': 'flavors__delete',
'object_ids': [flavors[0].id, flavors[1].id]}
with contextlib.nested(
patch('openstack_dashboard.api.nova.flavor_delete'),
patch('openstack_dashboard.api.nova.server_list',
return_value=([], False)),
patch('tuskar_ui.api.tuskar.OvercloudRole.list',
return_value=roles),
patch('openstack_dashboard.api.glance.image_list_detailed',
return_value=([], False)),
patch('openstack_dashboard.api.nova.flavor_list',
return_value=TEST_DATA.novaclient_flavors.list())
) as (delete_mock, server_list_mock, _role_list_mock, _glance_mock,
_flavors_mock):
res = self.client.post(INDEX_URL, data)
self.assertMessageCount(error=1, warning=0)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
self.assertEqual(delete_mock.call_count, 1)
self.assertEqual(server_list_mock.call_count, 1)
def test_details_no_overcloud(self):
flavor = api.flavor.Flavor(TEST_DATA.novaclient_flavors.first())
images = TEST_DATA.glanceclient_images.list()[:2]
roles = TEST_DATA.tuskarclient_overcloud_roles.list()
roles[0].flavor_id = flavor.id
roles = TEST_DATA.tuskarclient_roles.list()
with contextlib.nested(
patch('openstack_dashboard.api.glance.image_get',
side_effect=images),
@ -222,7 +180,6 @@ class FlavorsTest(test.BaseAdminViewTests):
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(plan_mock.call_count, 1)
self.assertTemplateUsed(res,
'infrastructure/flavors/details.html')
@ -230,11 +187,10 @@ class FlavorsTest(test.BaseAdminViewTests):
def test_details(self):
flavor = api.flavor.Flavor(TEST_DATA.novaclient_flavors.first())
images = TEST_DATA.glanceclient_images.list()[:2]
roles = TEST_DATA.tuskarclient_overcloud_roles.list()
roles[0].flavor_id = flavor.id
roles = TEST_DATA.tuskarclient_roles.list()
plan = api.tuskar.OvercloudPlan(
TEST_DATA.tuskarclient_overcloud_plans.first())
stack = api.heat.OvercloudStack(
TEST_DATA.tuskarclient_plans.first())
stack = api.heat.Stack(
TEST_DATA.heatclient_stacks.first())
with contextlib.nested(
patch('openstack_dashboard.api.glance.image_get',
@ -245,10 +201,10 @@ class FlavorsTest(test.BaseAdminViewTests):
return_value=roles),
patch('tuskar_ui.api.tuskar.OvercloudPlan.get_the_plan',
return_value=plan),
patch('tuskar_ui.api.heat.OvercloudStack.get',
patch('tuskar_ui.api.heat.Stack.get',
return_value=stack),
# __name__ is required for horizon.tables
patch('tuskar_ui.api.heat.OvercloudStack.resources_count',
patch('tuskar_ui.api.heat.Stack.resources_count',
return_value=42, __name__='')
) as (image_mock, get_mock, roles_mock, plan_mock, stack_mock,
count_mock):
@ -256,10 +212,6 @@ class FlavorsTest(test.BaseAdminViewTests):
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(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,
'infrastructure/flavors/details.html')

View File

@ -83,5 +83,6 @@ class DetailView(horizon.tables.DataTableView):
return context
def get_data(self):
return [role for role in api.tuskar.OvercloudRole.list(self.request)
if role.flavor_id == str(self.kwargs.get('flavor_id'))]
# TODO(tzumainn): fix role relation, if possible; the plan needs to be
# considered as well
return []

View File

@ -84,7 +84,7 @@ class NodesTable(tables.DataTable):
row_actions = ()
def get_object_id(self, datum):
return datum.id
return datum.uuid
def get_object_display(self, datum):
return datum.uuid

View File

@ -15,6 +15,7 @@
from django.core import urlresolvers
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import tabs
from tuskar_ui import api
@ -63,12 +64,14 @@ class DeployedTab(tabs.TableTab):
if 'errors' in self.request.GET:
return api.node.filter_nodes(deployed_nodes, healthy=False)
# TODO(tzumainn) ideally, the role should be a direct attribute
# of a node; however, that cannot be done until the tuskar api
# update that will prevent a circular dependency in the api
for node in deployed_nodes:
node.role_name = api.tuskar.OvercloudRole.get_by_node(
self.request, node).name
# TODO(tzumainn): this could probably be done more efficiently
# by getting the resource for all nodes at once
try:
resource = api.heat.Resource.get_by_node(self.request, node)
node.role_name = resource.role.name
except exceptions.NotFound:
node.role_name = '-'
return deployed_nodes

View File

@ -19,10 +19,7 @@
{% if deployed_nodes_error %}
<i class="icon-exclamation-sign"></i>
{% if deployed_nodes_error|length == 1 %}
{% comment %}
Replace id with uuid when ironicclient is used instead baremetalclient
{% endcomment %}
{% url 'horizon:infrastructure:nodes:detail' deployed_nodes_error.0.id as node_detail_url %}
{% url 'horizon:infrastructure:nodes:detail' deployed_nodes_error.0.uuid as node_detail_url %}
{% else %}
{% url 'horizon:infrastructure:nodes:index' as nodes_index_url %}
{% endif %}

View File

@ -61,13 +61,14 @@ class NodesTests(test.BaseAdminViewTests, helpers.APITestCase):
def test_free_nodes(self):
free_nodes = [api.node.Node(node)
for node in self.ironicclient_nodes.list()]
roles = TEST_DATA.tuskarclient_overcloud_roles.list()
roles = [api.tuskar.OvercloudRole(r)
for r in TEST_DATA.tuskarclient_roles.list()]
instance = TEST_DATA.novaclient_servers.first()
image = TEST_DATA.glanceclient_images.first()
with contextlib.nested(
patch('tuskar_ui.api.tuskar.OvercloudRole', **{
'spec_set': ['list', 'name', 'get_by_node'],
'spec_set': ['list', 'name'],
'list.return_value': roles,
}),
patch('tuskar_ui.api.node.Node', **{
@ -106,13 +107,14 @@ class NodesTests(test.BaseAdminViewTests, helpers.APITestCase):
def test_deployed_nodes(self):
deployed_nodes = [api.node.Node(node)
for node in self.ironicclient_nodes.list()]
roles = TEST_DATA.tuskarclient_overcloud_roles.list()
roles = [api.tuskar.OvercloudRole(r)
for r in TEST_DATA.tuskarclient_roles.list()]
instance = TEST_DATA.novaclient_servers.first()
image = TEST_DATA.glanceclient_images.first()
with contextlib.nested(
patch('tuskar_ui.api.tuskar.OvercloudRole', **{
'spec_set': ['list', 'name', 'get_by_node'],
'spec_set': ['list', 'name'],
'list.return_value': roles,
}),
patch('tuskar_ui.api.node.Node', **{

View File

@ -79,7 +79,6 @@ class DetailView(horizon_views.APIView):
redirect = reverse_lazy('horizon:infrastructure:nodes:index')
node = api.node.Node.get(request, node_uuid, _error_redirect=redirect)
context['node'] = node
if api_base.is_service_enabled(request, 'metering'):
context['meters'] = (
('cpu', _('CPU')),

View File

@ -12,12 +12,10 @@
# License for the specific language governing permissions and limitations
# under the License.
import django.forms
from django.utils.translation import ugettext_lazy as _
import horizon.exceptions
import horizon.forms
import horizon.messages
from openstack_dashboard import api as horizon_api
from tuskar_ui import api
@ -25,7 +23,8 @@ from tuskar_ui import api
class UndeployOvercloud(horizon.forms.SelfHandlingForm):
def handle(self, request, data):
try:
api.tuskar.OvercloudPlan.delete(request, self.initial['plan_id'])
stack = api.heat.Stack.get(request, self.initial['stack_id'])
api.tuskar.OvercloudPlan.delete(request, stack.plan.id)
except Exception:
horizon.exceptions.handle(request,
_("Unable to undeploy overcloud."))
@ -34,47 +33,3 @@ class UndeployOvercloud(horizon.forms.SelfHandlingForm):
msg = _('Undeployment in progress.')
horizon.messages.success(request, msg)
return True
def get_flavor_choices(request):
empty = [('', '----')]
try:
flavors = horizon_api.nova.flavor_list(request, None)
except Exception:
horizon.exceptions.handle(request,
_('Unable to retrieve flavor list.'))
return empty
return empty + [(flavor.id, flavor.name) for flavor in flavors]
class OvercloudRoleForm(horizon.forms.SelfHandlingForm):
id = django.forms.IntegerField(
widget=django.forms.HiddenInput)
name = django.forms.CharField(
label=_("Name"), required=False,
widget=django.forms.TextInput(
attrs={'readonly': 'readonly', 'disabled': 'disabled'}))
description = django.forms.CharField(
label=_("Description"), required=False,
widget=django.forms.Textarea(
attrs={'readonly': 'readonly', 'disabled': 'disabled'}))
image_name = django.forms.CharField(
label=_("Image"), required=False,
widget=django.forms.TextInput(
attrs={'readonly': 'readonly', 'disabled': 'disabled'}))
flavor_id = django.forms.ChoiceField(
label=_("Flavor"), required=False, choices=())
def __init__(self, *args, **kwargs):
super(OvercloudRoleForm, self).__init__(*args, **kwargs)
self.fields['flavor_id'].choices = get_flavor_choices(self.request)
def handle(self, request, context):
try:
role = api.tuskar.OvercloudRole.get(request, context['id'])
role.update(request, flavor_id=context['flavor_id'])
except Exception:
horizon.exceptions.handle(request,
_('Unable to update the role.'))
return False
return True

View File

@ -16,33 +16,25 @@ from django.utils.translation import ugettext_lazy as _
import heatclient
from horizon import tabs
from tuskar_ui import api
from tuskar_ui.infrastructure.overcloud import tables
from tuskar_ui import utils
def _get_role_data(plan, role):
def _get_role_data(stack, 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.
:param overcloud: Overcloud object
:type overcloud: tuskar_ui.api.Overcloud
:param stack: Stack object
:type stack: tuskar_ui.api.heat.Stack
:param role: Role object
:type role: tuskar_ui.api.OvercloudRole
:type role: tuskar_ui.api.tuskar.OvercloudRole
:return: dict with information about the role, to be used by template
:rtype: dict
"""
resources = plan.stack.resources_by_role(role, with_joins=True)
resources = stack.resources_by_role(role, with_joins=True)
nodes = [r.node for r in resources]
counts = getattr(plan, 'counts', [])
for c in counts:
if c['overcloud_role_id'] == role.id:
node_count = c['num_nodes']
break
else:
node_count = 0
node_count = len(nodes)
data = {
'role': role,
@ -82,22 +74,23 @@ class OverviewTab(tabs.Tab):
preload = False
def get_context_data(self, request, **kwargs):
plan = self.tab_group.kwargs['plan']
roles = api.tuskar.OvercloudRole.list(request)
role_data = [_get_role_data(plan, role) for role in roles]
stack = self.tab_group.kwargs['stack']
roles = stack.plan.role_list
role_data = [_get_role_data(stack, 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 = plan.stack.events
events = stack.events
last_failed_events = [e for e in events
if e.resource_status == 'CREATE_FAILED'][-3:]
return {
'plan': plan,
'stack': plan.stack,
'stack': stack,
'plan': stack.plan,
'roles': role_data,
'progress': max(5, progress),
'dashboard_urls': plan.stack.dashboard_urls,
'dashboard_urls': stack.dashboard_urls,
'last_failed_events': last_failed_events,
}
@ -109,7 +102,7 @@ class UndeployInProgressTab(tabs.Tab):
preload = False
def get_context_data(self, request, **kwargs):
plan = self.tab_group.kwargs['plan']
stack = self.tab_group.kwargs['stack']
# 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
@ -119,7 +112,7 @@ class UndeployInProgressTab(tabs.Tab):
try:
resources_count = len(
plan.stack.resources(with_joins=False))
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.
@ -131,11 +124,12 @@ class UndeployInProgressTab(tabs.Tab):
delete_progress = max(
5, 100 * (total_num_nodes_count - resources_count))
events = plan.stack.events
events = stack.events
last_failed_events = [e for e in events
if e.resource_status == 'DELETE_FAILED'][-3:]
return {
'plan': plan,
'stack': stack,
'plan': stack.plan,
'progress': delete_progress,
'last_failed_events': last_failed_events,
}
@ -149,10 +143,10 @@ class ConfigurationTab(tabs.TableTab):
preload = False
def get_configuration_data(self):
plan = self.tab_group.kwargs['plan']
stack = self.tab_group.kwargs['stack']
return [(utils.de_camel_case(key), value) for key, value in
plan.stack.parameters.items()]
stack.parameters.items()]
class LogTab(tabs.TableTab):
@ -163,8 +157,8 @@ class LogTab(tabs.TableTab):
preload = False
def get_log_data(self):
plan = self.tab_group.kwargs['plan']
return plan.stack.events
stack = self.tab_group.kwargs['stack']
return stack.events
class UndeployInProgressTabs(tabs.TabGroup):

View File

@ -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' plan.id role.role.id %}"
href="{% url 'horizon:infrastructure:overcloud:role' stack.id role.role.id %}"
>{{ role.name }} <span class="badge">({{ role.total_node_count }})</span></a>
</td>
<td>

View File

@ -1,15 +0,0 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% load url from future %}
{% block form_id %}role_edit_form{% endblock %}
{% block modal_id %}role_edit_modal{% endblock %}
{% block modal-header %}{% trans "Edit Deployment Role" %}{% endblock %}
{% block form_action %}{% url 'horizon:infrastructure:overcloud:role_edit' form.id.value %}{% endblock %}
{% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit"
value="{% trans "Apply Changes" %}" />
<a href="{% url 'horizon:infrastructure:overcloud:create' %}"
class="btn secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

@ -3,7 +3,7 @@
{% load url from future %}
{% block form_id %}provision_form{% endblock %}
{% block form_action %}{% url 'horizon:infrastructure:overcloud:undeploy_confirmation' plan_id %}{% endblock %}
{% block form_action %}{% url 'horizon:infrastructure:overcloud:undeploy_confirmation' stack_id %}{% endblock %}
{% block modal_id %}provision_modal{% endblock %}
{% block modal-header %}{% trans "Provisioning Confirmation" %}{% endblock %}

View File

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

View File

@ -19,20 +19,12 @@
<div class="row-fluid">
<div class="span12">
<div class="actions pull-right">
<a href="{% url 'horizon:infrastructure:overcloud:undeploy_confirmation' plan.id %}"
<a href="{% url 'horizon:infrastructure:overcloud:undeploy_confirmation' stack.id %}"
class="btn btn-danger ajax-modal
{% 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' plan.id %}"
class="btn ajax-modal
{% if not stack.is_deployed %}disabled{% endif %}">
<i class="icon-resize-full"></i>
{% trans "Scale deployment" %}
</a>
{% endcomment %}
</div>
{{ tab_group.render }}
</div>

View File

@ -1,11 +0,0 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}{% trans "Edit Deployment Role" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Edit Deployment Role") %}
{% endblock %}
{% block main %}
{% include "infrastructure/overcloud/_role_edit.html" %}
{% endblock %}

View File

@ -1,33 +0,0 @@
{% load i18n %}
{% load url from future%}
<noscript><h3>{{ step }}</h3></noscript>
<div class="widget muted">{{ step.get_help_text }}</div>
<div class="row-fluid">
<div class="span8">
<div class="pull-right">
<span id="free-nodes">{{ step.get_free_nodes }}</span> {% trans "free nodes" %}
</div>
<h2>{% trans "Roles" %}</h2>
<div class="widget">
{% include 'infrastructure/overcloud/node_counts.html' with form=form editable=True %}
</div>
</div>
<div class="span4">
<div class="widget">
<h2>{% trans "Configuration" %}</h2>
<p class="muted">{% trans "Configuration options will be auto-detected." %}</p>
<p><a href="#undeployed_overcloud__deployed_configuration"
data-toggle="tab">{% trans "See and change defaults." %}</a></p>
</div>
</div>
</div>
<script type="text/javascript">
(window.$ || window.addHorizonLoadEvent)(function () {
$('p > a[href="#undeployed_overcloud__deployed_configuration"]'
).click(function () {
$('li > a[href="#undeployed_overcloud__deployed_configuration"]').tab('show');
});
});
</script>

View File

@ -12,7 +12,6 @@
# License for the specific language governing permissions and limitations
# under the License.
import collections
import contextlib
from django.core import urlresolvers
@ -29,10 +28,8 @@ from tuskar_ui.test.test_data import tuskar_data
INDEX_URL = urlresolvers.reverse(
'horizon:infrastructure:overcloud:index')
CREATE_URL = urlresolvers.reverse(
'horizon:infrastructure:overcloud:create')
DETAIL_URL = urlresolvers.reverse(
'horizon:infrastructure:overcloud:detail', args=(1,))
'horizon:infrastructure:overcloud:detail', args=('stack-id-1',))
UNDEPLOY_IN_PROGRESS_URL = urlresolvers.reverse(
'horizon:infrastructure:overcloud:undeploy_in_progress',
args=('overcloud',))
@ -42,7 +39,10 @@ DETAIL_URL_CONFIGURATION_TAB = (DETAIL_URL +
"?tab=detail__configuration")
DETAIL_URL_LOG_TAB = (DETAIL_URL + "?tab=detail__log")
DELETE_URL = urlresolvers.reverse(
'horizon:infrastructure:overcloud:undeploy_confirmation', args=(1,))
'horizon:infrastructure:overcloud:undeploy_confirmation',
args=('stack-id-1',))
PLAN_CREATE_URL = urlresolvers.reverse(
'horizon:infrastructure:plans:create')
TEST_DATA = utils.TestDataContainer()
flavor_data.data(TEST_DATA)
node_data.data(TEST_DATA)
@ -53,49 +53,25 @@ tuskar_data.data(TEST_DATA)
@contextlib.contextmanager
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": "",
"Type": "String",
"NoEcho": "false",
"Description": ("If set, the public interface is a vlan with this "
"device as the raw device."),
},
"HeatPassword": {
"Default": "unset",
"Type": "String",
"NoEcho": "true",
"Description": ("The password for the Heat service account, used "
"by the Heat services.")
},
}
params = {
'spec_set': [
'counts',
'create',
'delete',
'get',
'get_the_plan',
'id',
'stack',
'update',
'template_parameters',
'parameters',
'role_list',
],
'counts': [],
'create.side_effect': lambda *args, **kwargs: plan,
'delete.return_value': None,
'get.side_effect': lambda *args, **kwargs: plan,
'get_the_plan.side_effect': lambda *args, **kwargs: plan,
'id': 1,
'stack': stack,
'update.side_effect': lambda *args, **kwargs: plan,
'template_parameters.return_value': template_parameters,
'role_list': [],
}
params.update(kwargs)
with patch(
@ -111,12 +87,12 @@ class OvercloudTests(test.BaseAdminViewTests):
'get_the_plan.return_value': None}):
res = self.client.get(INDEX_URL)
self.assertRedirectsNoFollow(res, CREATE_URL)
self.assertRedirectsNoFollow(res, PLAN_CREATE_URL)
def test_index_overcloud_deployed_stack_not_created(self):
with contextlib.nested(
_mock_plan(),
patch('tuskar_ui.api.heat.OvercloudStack.is_deployed',
patch('tuskar_ui.api.heat.Stack.is_deployed',
return_value=False),
):
res = self.client.get(INDEX_URL)
@ -128,147 +104,17 @@ class OvercloudTests(test.BaseAdminViewTests):
self.assertRedirectsNoFollow(res, DETAIL_URL)
def test_index_overcloud_deployed(self):
with _mock_plan() as Overcloud:
with _mock_plan() as OvercloudPlan:
res = self.client.get(INDEX_URL)
request = Overcloud.get_the_plan.call_args_list[0][0][0]
self.assertListEqual(Overcloud.get_the_plan.call_args_list,
request = OvercloudPlan.get_the_plan.call_args_list[0][0][0]
self.assertListEqual(OvercloudPlan.get_the_plan.call_args_list,
[call(request)])
self.assertRedirectsNoFollow(res, DETAIL_URL)
def test_create_get(self):
roles = TEST_DATA.tuskarclient_overcloud_roles.list()
with contextlib.nested(
patch('tuskar_ui.api.tuskar.OvercloudRole', **{
'spec_set': ['list'],
'list.return_value': roles,
}),
_mock_plan(),
patch('tuskar_ui.api.node.Node', **{
'spec_set': ['list'],
'list.return_value': [],
}),
patch('openstack_dashboard.api.nova', **{
'spec_set': ['flavor_list'],
'flavor_list.return_value': [],
}),
):
res = self.client.get(CREATE_URL)
self.assertTemplateUsed(
res, 'infrastructure/_fullscreen_workflow_base.html')
self.assertTemplateUsed(
res, 'infrastructure/overcloud/node_counts.html')
self.assertTemplateUsed(
res, 'infrastructure/overcloud/undeployed_overview.html')
def test_create_post(self):
node = TEST_DATA.ironicclient_nodes.first
roles = TEST_DATA.tuskarclient_overcloud_roles.list()
flavor = TEST_DATA.novaclient_flavors.first()
old_flavor_id = roles[0].flavor_id
roles[0].flavor_id = flavor.id
data = {
'count__1__%s' % flavor.id: '1',
'count__2__': '0',
'count__3__': '0',
'count__4__': '0',
}
with contextlib.nested(
patch('tuskar_ui.api.tuskar.OvercloudRole', **{
'spec_set': ['list'],
'list.return_value': roles,
}),
_mock_plan(),
patch('tuskar_ui.api.node.Node', **{
'spec_set': ['list'],
'list.return_value': [node],
}),
patch('openstack_dashboard.api.nova', **{
'spec_set': ['flavor_list'],
'flavor_list.return_value': [flavor],
}),
) as (OvercloudRole, Overcloud, Node, nova):
res = self.client.post(CREATE_URL, data)
request = Overcloud.create.call_args_list[0][0][0]
self.assertListEqual(
Overcloud.create.call_args_list,
[
call(request, {
('1', flavor.id): 1,
('2', ''): 0,
('3', ''): 0,
('4', ''): 0,
}, {
'NeutronPublicInterfaceRawDevice': '',
'HeatPassword': '',
}),
])
roles[0].flavor_id = old_flavor_id
self.assertRedirectsNoFollow(res, INDEX_URL)
def test_create_post_invalid_flavor(self):
roles = TEST_DATA.tuskarclient_overcloud_roles.list()
old_flavor_id = roles[0].flavor_id
roles[0].flavor_id = 'non-existing'
data = {
'count__1__%s' % roles[0].flavor_id: '1',
'count__2__': '0',
'count__3__': '0',
'count__4__': '0',
}
with contextlib.nested(
patch('tuskar_ui.api.tuskar.OvercloudRole', **{
'spec_set': ['list'],
'list.return_value': roles,
}),
_mock_plan(),
patch('tuskar_ui.api.node.Node', **{
'spec_set': ['list'],
'list.return_value': [],
}),
patch('openstack_dashboard.api.nova', **{
'spec_set': ['flavor_list'],
'flavor_list.return_value': [],
}),
) as (OvercloudRole, Overcloud, Node, nova):
res = self.client.post(CREATE_URL, data)
self.assertFormErrors(res)
roles[0].flavor_id = old_flavor_id
def test_create_post_not_enough_nodes(self):
node = TEST_DATA.ironicclient_nodes.first
roles = TEST_DATA.tuskarclient_overcloud_roles.list()
flavor = TEST_DATA.novaclient_flavors.first()
roles[0].flavor_id = flavor.id
data = {
'count__1__%s' % flavor.id: '2',
'count__2__': '0',
'count__3__': '0',
'count__4__': '0',
}
with contextlib.nested(
patch('tuskar_ui.api.tuskar.OvercloudRole', **{
'spec_set': ['list'],
'list.return_value': roles,
}),
_mock_plan(),
patch('tuskar_ui.api.node.Node', **{
'spec_set': ['list'],
'list.return_value': [node],
}),
patch('openstack_dashboard.api.nova', **{
'spec_set': ['flavor_list'],
'flavor_list.return_value': [flavor],
}),
):
response = self.client.post(CREATE_URL, data)
self.assertFormErrors(
response,
1,
'This configuration requires 2 nodes, but only 1 is available.')
def test_detail_get(self):
roles = TEST_DATA.tuskarclient_overcloud_roles.list()
roles = [api.tuskar.OvercloudRole(role)
for role in TEST_DATA.tuskarclient_roles.list()]
with contextlib.nested(
_mock_plan(),
@ -276,7 +122,9 @@ class OvercloudTests(test.BaseAdminViewTests):
'spec_set': ['list'],
'list.return_value': roles,
}),
) as (Overcloud, OvercloudRole):
patch('tuskar_ui.api.heat.Stack.events',
return_value=[]),
):
res = self.client.get(DETAIL_URL)
self.assertTemplateUsed(
@ -298,7 +146,11 @@ class OvercloudTests(test.BaseAdminViewTests):
res, 'horizon/common/_detail_table.html')
def test_detail_get_log_tab(self):
with _mock_plan():
with contextlib.nested(
_mock_plan(),
patch('tuskar_ui.api.heat.Stack.events',
return_value=[]),
):
res = self.client.get(DETAIL_URL_LOG_TAB)
self.assertTemplateUsed(
@ -321,10 +173,12 @@ class OvercloudTests(test.BaseAdminViewTests):
def test_undeploy_in_progress(self):
with contextlib.nested(
_mock_plan(),
patch('tuskar_ui.api.heat.OvercloudStack.is_deleting',
patch('tuskar_ui.api.heat.Stack.is_deleting',
return_value=True),
patch('tuskar_ui.api.heat.OvercloudStack.is_deployed',
patch('tuskar_ui.api.heat.Stack.is_deployed',
return_value=False),
patch('tuskar_ui.api.heat.Stack.events',
return_value=[]),
):
res = self.client.get(UNDEPLOY_IN_PROGRESS_URL)
@ -340,7 +194,7 @@ class OvercloudTests(test.BaseAdminViewTests):
'get_the_plan.return_value': None}):
res = self.client.get(UNDEPLOY_IN_PROGRESS_URL)
self.assertRedirectsNoFollow(res, CREATE_URL)
self.assertRedirectsNoFollow(res, INDEX_URL)
def test_undeploy_in_progress_invalid(self):
with _mock_plan():
@ -351,10 +205,12 @@ class OvercloudTests(test.BaseAdminViewTests):
def test_undeploy_in_progress_log_tab(self):
with contextlib.nested(
_mock_plan(),
patch('tuskar_ui.api.heat.OvercloudStack.is_deleting',
patch('tuskar_ui.api.heat.Stack.is_deleting',
return_value=True),
patch('tuskar_ui.api.heat.OvercloudStack.is_deployed',
patch('tuskar_ui.api.heat.Stack.is_deployed',
return_value=False),
patch('tuskar_ui.api.heat.Stack.events',
return_value=[]),
):
res = self.client.get(UNDEPLOY_IN_PROGRESS_URL_LOG_TAB)
@ -364,137 +220,3 @@ class OvercloudTests(test.BaseAdminViewTests):
res, 'infrastructure/overcloud/_undeploy_in_progress.html')
self.assertTemplateUsed(
res, 'horizon/common/_detail_table.html')
def test_scale_get(self):
oc = None
roles = TEST_DATA.tuskarclient_overcloud_roles.list()
with contextlib.nested(
patch('tuskar_ui.api.tuskar.OvercloudRole', **{
'spec_set': ['list'],
'list.return_value': roles,
}),
_mock_plan(counts=[{
"overcloud_role_id": role.id,
"num_nodes": 0,
} for role in roles]),
patch('openstack_dashboard.api.nova', **{
'spec_set': ['flavor_list'],
'flavor_list.return_value': [],
}),
) as (OvercloudRole, Overcloud, nova):
oc = Overcloud
url = urlresolvers.reverse(
'horizon:infrastructure:overcloud:scale', args=(oc.id,))
res = self.client.get(url)
self.assertTemplateUsed(
res, 'infrastructure/overcloud/scale_node_counts.html')
def test_scale_post(self):
node = TEST_DATA.ironicclient_nodes.first
roles = TEST_DATA.tuskarclient_overcloud_roles.list()
flavor = TEST_DATA.novaclient_flavors.first()
old_flavor_id = roles[0].flavor_id
roles[0].flavor_id = flavor.id
data = {
'plan_id': '1',
'count__1__%s' % flavor.id: '1',
'count__2__': '0',
'count__3__': '0',
'count__4__': '0',
}
with contextlib.nested(
patch('tuskar_ui.api.tuskar.OvercloudRole', **{
'spec_set': ['list'],
'list.return_value': roles,
}),
_mock_plan(counts=[{
"overcloud_role_id": role.id,
"num_nodes": 0,
} for role in roles]),
patch('tuskar_ui.api.node.Node', **{
'spec_set': ['list'],
'list.return_value': [node],
}),
patch('openstack_dashboard.api.nova', **{
'spec_set': ['flavor_list'],
'flavor_list.return_value': [flavor],
}),
) as (OvercloudRole, Overcloud, Node, nova):
url = urlresolvers.reverse(
'horizon:infrastructure:overcloud:scale', args=(Overcloud.id,))
res = self.client.post(url, data)
request = Overcloud.update.call_args_list[0][0][0]
self.assertListEqual(
Overcloud.update.call_args_list,
[
call(request, Overcloud.id, {
('1', flavor.id): 1,
('2', ''): 0,
('3', ''): 0,
('4', ''): 0,
}, {}),
])
roles[0].flavor_id = old_flavor_id
self.assertRedirectsNoFollow(res, DETAIL_URL)
def test_role_edit_get(self):
role = TEST_DATA.tuskarclient_overcloud_roles.first()
url = urlresolvers.reverse(
'horizon:infrastructure:overcloud:role_edit', args=(role.id,))
with contextlib.nested(
patch('tuskar_ui.api.tuskar.OvercloudRole', **{
'spec_set': ['get'],
'get.return_value': role,
}),
patch('openstack_dashboard.api.nova', **{
'spec_set': ['flavor_list'],
'flavor_list.return_value': [],
}),
):
res = self.client.get(url)
self.assertTemplateUsed(
res, 'infrastructure/overcloud/role_edit.html')
self.assertTemplateUsed(
res, 'infrastructure/overcloud/_role_edit.html')
def test_role_edit_post(self):
role = None
Flavor = collections.namedtuple('Flavor', 'id name')
flavor = Flavor('xxx', 'Xxx')
with contextlib.nested(
patch('tuskar_ui.api.tuskar.OvercloudRole', **{
'spec_set': [
'get',
'update',
'id',
'name',
'description',
'image_name',
'flavor_id',
],
'get.side_effect': lambda *args, **kwargs: role,
'name': 'Compute',
'description': '...',
'image_name': '',
'id': 1,
'flavor_id': '',
}),
patch('openstack_dashboard.api.nova', **{
'spec_set': ['flavor_list'],
'flavor_list.return_value': [flavor],
}),
) as (OvercloudRole, nova):
role = OvercloudRole
url = urlresolvers.reverse(
'horizon:infrastructure:overcloud:role_edit', args=(role.id,))
data = {
'id': str(role.id),
'flavor_id': flavor.id,
}
res = self.client.post(url, data)
request = OvercloudRole.update.call_args_list[0][0][0]
self.assertListEqual(
OvercloudRole.update.call_args_list,
[call(request, flavor_id=flavor.id)])
self.assertRedirectsNoFollow(res, CREATE_URL)

View File

@ -20,19 +20,14 @@ from tuskar_ui.infrastructure.overcloud import views
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<plan_id>[^/]+)/undeploy-in-progress$',
urls.url(r'^(?P<stack_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<plan_id>[^/]+)/$', views.DetailView.as_view(),
urls.url(r'^(?P<stack_id>[^/]+)/$', views.DetailView.as_view(),
name='detail'),
urls.url(r'^(?P<plan_id>[^/]+)/scale$', views.Scale.as_view(),
name='scale'),
urls.url(r'^(?P<plan_id>[^/]+)/role/(?P<role_id>[^/]+)$',
urls.url(r'^(?P<stack_id>[^/]+)/role/(?P<role_id>[^/]+)$',
views.OvercloudRoleView.as_view(), name='role'),
urls.url(r'^(?P<plan_id>[^/]+)/undeploy-confirmation$',
urls.url(r'^(?P<stack_id>[^/]+)/undeploy-confirmation$',
views.UndeployConfirmationView.as_view(),
name='undeploy_confirmation'),
)

View File

@ -11,46 +11,39 @@
# 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 novaclient
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from django.views.generic import base as base_views
import heatclient
from horizon import exceptions as horizon_exceptions
import horizon.forms
from horizon import messages
from horizon import tables as horizon_tables
from horizon import tabs as horizon_tabs
from horizon.utils import memoized
import horizon.workflows
from openstack_dashboard.api import nova
from tuskar_ui import api
from tuskar_ui.infrastructure.overcloud import forms
from tuskar_ui.infrastructure.overcloud import tables
from tuskar_ui.infrastructure.overcloud import tabs
from tuskar_ui.infrastructure.overcloud.workflows import scale
from tuskar_ui.infrastructure.overcloud.workflows import undeployed
INDEX_URL = 'horizon:infrastructure:overcloud:index'
DETAIL_URL = 'horizon:infrastructure:overcloud:detail'
CREATE_URL = 'horizon:infrastructure:overcloud:create'
PLAN_CREATE_URL = 'horizon:infrastructure:plans:create'
UNDEPLOY_IN_PROGRESS_URL = (
'horizon:infrastructure:overcloud:undeploy_in_progress')
class OvercloudPlanMixin(object):
class StackMixin(object):
@memoized.memoized
def get_plan(self, redirect=None):
def get_stack(self, redirect=None):
if redirect is None:
redirect = reverse(INDEX_URL)
plan_id = self.kwargs['plan_id']
plan = api.tuskar.OvercloudPlan.get(self.request, plan_id,
_error_redirect=redirect)
return plan
stack_id = self.kwargs['stack_id']
stack = api.heat.Stack.get(self.request, stack_id,
_error_redirect=redirect)
return stack
class OvercloudRoleMixin(object):
@ -66,42 +59,39 @@ class IndexView(base_views.RedirectView):
permanent = False
def get_redirect_url(self):
try:
# TODO(lsmola) implement this properly when supported by API
plan = api.tuskar.OvercloudPlan.get_the_plan(self.request)
except heatclient.exc.HTTPNotFound:
plan = None
plan = api.tuskar.OvercloudPlan.get_the_plan(self.request)
redirect = None
if plan is None:
redirect = reverse(CREATE_URL)
elif plan.stack.is_deleting or plan.stack.is_delete_failed:
redirect = reverse(UNDEPLOY_IN_PROGRESS_URL,
args=(plan.id,))
else:
redirect = reverse(DETAIL_URL,
args=(plan.id,))
redirect = reverse(PLAN_CREATE_URL)
if plan is not None:
stacks = api.heat.Stack.list(self.request)
for stack in stacks:
if stack.plan.id == plan.id:
break
else:
stack = None
if stack is not None:
if stack.is_deleting or stack.is_delete_failed:
redirect = reverse(UNDEPLOY_IN_PROGRESS_URL,
args=(stack.id,))
else:
redirect = reverse(DETAIL_URL,
args=(stack.id,))
return redirect
class CreateView(horizon.workflows.WorkflowView):
workflow_class = undeployed.Workflow
template_name = 'infrastructure/_fullscreen_workflow_base.html'
class DetailView(horizon_tabs.TabView, OvercloudPlanMixin):
class DetailView(horizon_tabs.TabView, StackMixin):
tab_group_class = tabs.DetailTabs
template_name = 'infrastructure/overcloud/detail.html'
def get_tabs(self, request, **kwargs):
plan = self.get_plan()
return self.tab_group_class(request, plan=plan, **kwargs)
stack = self.get_stack()
return self.tab_group_class(request, stack=stack, **kwargs)
def get_context_data(self, **kwargs):
context = super(DetailView, self).get_context_data(**kwargs)
context['plan'] = self.get_plan()
context['stack'] = self.get_plan().stack
context['stack'] = self.get_stack()
context['plan'] = self.get_stack().plan
return context
@ -115,133 +105,95 @@ class UndeployConfirmationView(horizon.forms.ModalFormView):
def get_context_data(self, **kwargs):
context = super(UndeployConfirmationView,
self).get_context_data(**kwargs)
context['plan_id'] = self.kwargs['plan_id']
context['stack_id'] = self.kwargs['stack_id']
return context
def get_initial(self, **kwargs):
initial = super(UndeployConfirmationView, self).get_initial(**kwargs)
initial['plan_id'] = self.kwargs['plan_id']
initial['stack_id'] = self.kwargs['stack_id']
return initial
class UndeployInProgressView(horizon_tabs.TabView, OvercloudPlanMixin, ):
class UndeployInProgressView(horizon_tabs.TabView, StackMixin, ):
tab_group_class = tabs.UndeployInProgressTabs
template_name = 'infrastructure/overcloud/detail.html'
def get_overcloud_plan_or_redirect(self):
try:
# TODO(lsmola) implement this properly when supported by API
plan = api.tuskar.OvercloudPlan.get_the_plan(self.request)
except heatclient.exc.HTTPNotFound:
plan = None
def get_stack_or_redirect(self):
plan = api.tuskar.OvercloudPlan.get_the_plan(self.request)
stack = None
if plan is None:
redirect = reverse(CREATE_URL)
if plan is not None:
stack = None
stacks = api.heat.Stack.list(self.request)
for s in stacks:
if s.plan.id == plan.id:
stack = s
break
if stack is None:
redirect = reverse(INDEX_URL)
messages.success(self.request,
_("Undeploying of the Overcloud has finished."))
raise horizon_exceptions.Http302(redirect)
elif plan.stack.is_deleting or plan.stack.is_delete_failed:
return plan
elif stack.is_deleting or stack.is_delete_failed:
return stack
else:
messages.error(self.request,
_("Overcloud is not being undeployed."))
redirect = reverse(DETAIL_URL,
args=(plan.id,))
args=(stack.id,))
raise horizon_exceptions.Http302(redirect)
def get_tabs(self, request, **kwargs):
plan = self.get_overcloud_plan_or_redirect()
return self.tab_group_class(request, plan=plan, **kwargs)
stack = self.get_stack_or_redirect()
return self.tab_group_class(request, stack=stack, **kwargs)
def get_context_data(self, **kwargs):
context = super(UndeployInProgressView,
self).get_context_data(**kwargs)
context['plan'] = self.get_overcloud_plan_or_redirect()
context['stack'] = self.get_stack_or_redirect()
return context
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['plan_id'] = self.kwargs['plan_id']
return context
def get_initial(self):
plan = self.get_plan()
overcloud_roles = dict((overcloud_role.id, overcloud_role)
for overcloud_role in
api.tuskar.OvercloudRole.list(self.request))
role_counts = dict((
(count['overcloud_role_id'],
overcloud_roles[count['overcloud_role_id']].flavor_id),
count['num_nodes'],
) for count in plan.counts)
return {
'plan_id': plan.id,
'role_counts': role_counts,
}
class OvercloudRoleView(horizon_tables.DataTableView,
OvercloudRoleMixin, OvercloudPlanMixin):
OvercloudRoleMixin, StackMixin):
table_class = tables.OvercloudRoleNodeTable
template_name = 'infrastructure/overcloud/overcloud_role.html'
@memoized.memoized
def _get_nodes(self, plan, role):
resources = plan.stack.resources_by_role(role, with_joins=True)
def _get_nodes(self, stack, role):
resources = stack.resources_by_role(role, with_joins=True)
nodes = [r.node for r in resources]
# TODO(akrivoka) ideally, the role should be a direct attribute
# of a node; however, that cannot be done until the tuskar api
# update that will prevent a circular dependency in the api
for node in nodes:
node.role_name = role.name
# TODO(tzumainn): this could probably be done more efficiently
# by getting the resource for all nodes at once
try:
resource = api.heat.Resource.get_by_node(self.request, node)
node.role_name = resource.role.name
except horizon_exceptions.NotFound:
node.role_name = '-'
return nodes
def get_data(self):
plan = self.get_plan()
stack = self.get_stack()
redirect = reverse(DETAIL_URL,
args=(plan.id,))
args=(stack.id,))
role = self.get_role(redirect)
return self._get_nodes(plan, role)
return self._get_nodes(stack, role)
def get_context_data(self, **kwargs):
context = super(OvercloudRoleView, self).get_context_data(**kwargs)
plan = self.get_plan()
stack = self.get_stack()
redirect = reverse(DETAIL_URL,
args=(plan.id,))
args=(stack.id,))
role = self.get_role(redirect)
context['role'] = role
context['image_name'] = role.image_name
context['nodes'] = self._get_nodes(plan, role)
# TODO(tzumainn) we need to do this from plan parameters
context['image_name'] = 'FIXME'
context['nodes'] = self._get_nodes(stack, role)
context['flavor'] = None
try:
context['flavor'] = nova.flavor_get(self.request, role.flavor_id)
except novaclient.exceptions.NotFound:
context['flavor'] = None
except Exception:
msg = _('Unable to retrieve flavor.')
horizon.exceptions.handle(self.request, msg)
return context
class OvercloudRoleEdit(horizon.forms.ModalFormView, OvercloudRoleMixin):
form_class = forms.OvercloudRoleForm
template_name = 'infrastructure/overcloud/role_edit.html'
def get_success_url(self):
return reverse(CREATE_URL)
def get_initial(self):
role = self.get_role()
return {
'id': role.id,
'name': role.name,
'description': role.description,
'image_name': role.image_name,
'flavor_id': role.flavor_id,
}

View File

@ -1,134 +0,0 @@
# -*- 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 django.forms
from django.utils.translation import ugettext_lazy as _
from horizon.utils import memoized
import horizon.workflows
from openstack_dashboard import api as horizon_api
from tuskar_ui import api
import tuskar_ui.forms
def get_role_id_and_flavor_id_from_field_name(field_name):
"""Extract the ids of overcloud role and flavor from the field
name.
"""
_count, role_id, flavor_id = field_name.split('__', 2)
return role_id, flavor_id
def get_field_name_from_role_id_and_flavor_id(role_id, flavor_id=''):
"""Compose the ids of overcloud role and flavor into a field name."""
return 'count__%s__%s' % (role_id, flavor_id)
class Action(horizon.workflows.Action):
class Meta:
slug = 'undeployed_overview'
name = _("Overview")
def _get_flavor_names(self):
# Get all flavors in one call, instead of getting them one by one.
try:
flavors = horizon_api.nova.flavor_list(self.request, None)
except Exception:
horizon.exceptions.handle(self.request,
_('Unable to retrieve flavor list.'))
flavors = []
return dict((str(flavor.id), flavor.name) for flavor in flavors)
def _get_flavors(self, role, flavor_names):
# TODO(rdopieralski) Get a list of flavors for each
# role here, when we support multiple flavors per role.
if role.flavor_id and role.flavor_id in flavor_names:
flavors = [(
role.flavor_id,
flavor_names[role.flavor_id],
)]
else:
flavors = []
return flavors
def __init__(self, *args, **kwargs):
super(Action, self).__init__(*args, **kwargs)
flavor_names = self._get_flavor_names()
for role in self._get_roles():
if role.name == 'Controller':
initial = 1
attrs = {'readonly': 'readonly'}
else:
initial = 0
attrs = {}
flavors = self._get_flavors(role, flavor_names)
if not flavors:
name = get_field_name_from_role_id_and_flavor_id(str(role.id))
attrs = {'readonly': 'readonly'}
self.fields[name] = django.forms.IntegerField(
label='', initial=initial, min_value=initial,
widget=tuskar_ui.forms.NumberPickerInput(attrs=attrs))
for flavor_id, label in flavors:
name = get_field_name_from_role_id_and_flavor_id(
str(role.id), flavor_id)
self.fields[name] = django.forms.IntegerField(
label=label, initial=initial, min_value=initial,
widget=tuskar_ui.forms.NumberPickerInput(attrs=attrs))
def roles_fieldset(self):
"""Iterates over lists of fields for each role."""
for role in self._get_roles():
yield (
role.id,
role.name,
list(tuskar_ui.forms.fieldset(
self, prefix=get_field_name_from_role_id_and_flavor_id(
str(role.id)))),
)
@memoized.memoized
def _get_roles(self):
"""Retrieve the list of all overcloud roles."""
return api.tuskar.OvercloudRole.list(self.request)
def clean(self):
for key, value in self.cleaned_data.iteritems():
if not key.startswith('count_'):
continue
role_id, flavor = get_role_id_and_flavor_id_from_field_name(key)
if int(value) and not flavor:
raise django.forms.ValidationError(
_("Can't deploy nodes without a flavor assigned."))
return self.cleaned_data
class Step(horizon.workflows.Step):
action_class = Action
contributes = ('role_counts',)
template_name = 'infrastructure/overcloud/undeployed_overview.html'
help_text = _("Nothing deployed yet. Design your first deployment.")
def get_free_nodes(self):
"""Get the count of nodes that are not assigned yet."""
return len(api.node.Node.list(self.workflow.request, False))
def contribute(self, data, context):
counts = {}
for key, value in data.iteritems():
if not key.startswith('count_'):
continue
count, role_id, flavor = key.split('__', 2)
counts[role_id, flavor] = int(value)
context['role_counts'] = counts
return context

View File

@ -0,0 +1,66 @@
# -*- 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 django.forms
from django.utils.translation import ugettext_lazy as _
import horizon.exceptions
import horizon.forms
import horizon.messages
from openstack_dashboard import api as horizon_api
from tuskar_ui import api
def get_flavor_choices(request):
empty = [('', '----')]
try:
flavors = horizon_api.nova.flavor_list(request, None)
except Exception:
horizon.exceptions.handle(request,
_('Unable to retrieve flavor list.'))
return empty
return empty + [(flavor.id, flavor.name) for flavor in flavors]
class OvercloudRoleForm(horizon.forms.SelfHandlingForm):
id = django.forms.IntegerField(
widget=django.forms.HiddenInput)
name = django.forms.CharField(
label=_("Name"), required=False,
widget=django.forms.TextInput(
attrs={'readonly': 'readonly', 'disabled': 'disabled'}))
description = django.forms.CharField(
label=_("Description"), required=False,
widget=django.forms.Textarea(
attrs={'readonly': 'readonly', 'disabled': 'disabled'}))
image_name = django.forms.CharField(
label=_("Image"), required=False,
widget=django.forms.TextInput(
attrs={'readonly': 'readonly', 'disabled': 'disabled'}))
flavor_id = django.forms.ChoiceField(
label=_("Flavor"), required=False, choices=())
def __init__(self, *args, **kwargs):
super(OvercloudRoleForm, self).__init__(*args, **kwargs)
self.fields['flavor_id'].choices = get_flavor_choices(self.request)
def handle(self, request, context):
try:
role = api.tuskar.OvercloudRole.get(request, context['id'])
role.update(request, flavor_id=context['flavor_id'])
except Exception:
horizon.exceptions.handle(request,
_('Unable to update the role.'))
return False
return True

View File

@ -0,0 +1,27 @@
# -*- 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.
from django.utils.translation import ugettext_lazy as _
import horizon
from tuskar_ui.infrastructure import dashboard
class Plans(horizon.Panel):
name = _("Plans")
slug = "plans"
dashboard.Infrastructure.register(Plans)

View File

@ -0,0 +1,35 @@
# -*- 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.
from django.utils.translation import ugettext_lazy as _
from horizon import tables
class ConfigurationTable(tables.DataTable):
key = tables.Column(lambda parameter: parameter[0],
verbose_name=_("Attribute Name"))
value = tables.Column(lambda parameter: parameter[1],
verbose_name=_("Attribute Value"))
class Meta:
name = "configuration"
verbose_name = _("Configuration")
multi_select = False
table_actions = ()
row_actions = ()
def get_object_id(self, datum):
return datum[0]

View File

@ -0,0 +1,16 @@
{% load i18n %}
{% load url from future%}
<noscript><h3>{{ step }}</h3></noscript>
<div class="widget muted">{{ step.get_help_text }}</div>
<div class="row-fluid">
<div class="span8">
<h2>{% trans "Roles" %}</h2>
<div class="widget">
<td class="actions">
{% include "horizon/common/_form_fields.html" %}
</td>
</div>
</div>
</div>

View File

@ -17,23 +17,12 @@
<tr>
{% if forloop.first %}
<td rowspan="{{ fields|length }}">
{% if editable %}
<a
href="{% url 'horizon:infrastructure:overcloud:role_edit' role_id %}"
class="ajax-modal"
><i class="icon-pencil"></i></a>
{% endif %}
{{ label }}
</td>
{% endif %}
<td>
{% if field.field.label %}
{{ field.label }}
{% elif editable %}
(<a
href="{% url 'horizon:infrastructure:overcloud:role_edit' role_id %}"
class="ajax-modal"
>{% trans "Add a flavor" %}</a>)
{% else %}
({% trans "No flavor" %})
{% endif %}

View File

@ -0,0 +1,65 @@
# -*- 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 contextlib
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
from tuskar_ui.test.test_data import node_data
from tuskar_ui.test.test_data import tuskar_data
INDEX_URL = urlresolvers.reverse(
'horizon:infrastructure:plans:index')
CREATE_URL = urlresolvers.reverse(
'horizon:infrastructure:plans:create')
OVERCLOUD_INDEX_URL = urlresolvers.reverse(
'horizon:infrastructure:overcloud:index')
TEST_DATA = utils.TestDataContainer()
flavor_data.data(TEST_DATA)
node_data.data(TEST_DATA)
heat_data.data(TEST_DATA)
tuskar_data.data(TEST_DATA)
class OvercloudTests(test.BaseAdminViewTests):
def test_index_no_plan_get(self):
with contextlib.nested(
patch('tuskar_ui.api.tuskar.OvercloudPlan.get_the_plan',
return_value=None),
):
res = self.client.get(INDEX_URL)
self.assertRedirectsNoFollow(res, CREATE_URL)
def test_index_with_plan_get(self):
plan = api.tuskar.OvercloudPlan(TEST_DATA.tuskarclient_plans.first())
with contextlib.nested(
patch('tuskar_ui.api.tuskar.OvercloudPlan.get_the_plan',
return_value=plan),
):
res = self.client.get(INDEX_URL)
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, OVERCLOUD_INDEX_URL)

View File

@ -0,0 +1,26 @@
# -*- 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.
from django.conf import urls
from tuskar_ui.infrastructure.plans import views
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<plan_id>[^/]+)/scale$', views.Scale.as_view(),
name='scale'),
)

View File

@ -0,0 +1,91 @@
# -*- 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.
from django.core.urlresolvers import reverse
from django.views.generic import base as base_views
from horizon.utils import memoized
import horizon.workflows
from tuskar_ui import api
from tuskar_ui.infrastructure.plans.workflows import create
from tuskar_ui.infrastructure.plans.workflows import scale
INDEX_URL = 'horizon:infrastructure:plans:index'
CREATE_URL = 'horizon:infrastructure:plans:create'
OVERCLOUD_INDEX_URL = 'horizon:infrastructure:overcloud:index'
class OvercloudPlanMixin(object):
@memoized.memoized
def get_plan(self, redirect=None):
if redirect is None:
redirect = reverse(INDEX_URL)
plan_id = self.kwargs['plan_id']
plan = api.tuskar.OvercloudPlan.get(self.request, plan_id,
_error_redirect=redirect)
return plan
class OvercloudRoleMixin(object):
@memoized.memoized
def get_role(self, redirect=None):
role_id = self.kwargs['role_id']
role = api.tuskar.OvercloudRole.get(self.request, role_id,
_error_redirect=redirect)
return role
class IndexView(base_views.RedirectView):
permanent = False
def get_redirect_url(self):
plan = api.tuskar.OvercloudPlan.get_the_plan(self.request)
if plan is None:
redirect = reverse(CREATE_URL)
else:
redirect = reverse(OVERCLOUD_INDEX_URL)
return redirect
class CreateView(horizon.workflows.WorkflowView):
workflow_class = create.Workflow
template_name = 'infrastructure/_fullscreen_workflow_base.html'
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['plan_id'] = self.kwargs['plan_id']
return context
def get_initial(self):
plan = self.get_plan()
overcloud_roles = dict((overcloud_role.id, overcloud_role)
for overcloud_role in
api.tuskar.OvercloudRole.list(self.request))
role_counts = dict((
(count['overcloud_role_id'],
overcloud_roles[count['overcloud_role_id']].flavor_id),
count['num_nodes'],
) for count in plan.counts)
return {
'plan_id': plan.id,
'role_counts': role_counts,
}

View File

@ -19,9 +19,8 @@ from django.utils.translation import ugettext_lazy as _
import horizon.workflows
from tuskar_ui import api
from tuskar_ui.infrastructure.overcloud.workflows\
import undeployed_configuration
from tuskar_ui.infrastructure.overcloud.workflows import undeployed_overview
from tuskar_ui.infrastructure.plans.workflows import create_configuration
from tuskar_ui.infrastructure.plans.workflows import create_overview
LOG = logging.getLogger(__name__)
@ -46,18 +45,18 @@ class DeploymentValidationMixin(object):
free)
m2 %= {'free': free}
message = unicode(translation.string_concat(m1, m2))
self.add_error_to_step(message, 'undeployed_overview')
self.add_error_to_step(message, 'create_overview')
self.add_error_to_step(message, 'scale_node_counts')
return False
return super(DeploymentValidationMixin, self).validate(context)
class Workflow(DeploymentValidationMixin, horizon.workflows.Workflow):
slug = 'undeployed_overcloud'
name = _("My OpenStack Deployment")
slug = 'create_plan'
name = _("My OpenStack Deployment Plan")
default_steps = (
undeployed_overview.Step,
undeployed_configuration.Step,
create_overview.Step,
create_configuration.Step,
)
finalize_button_name = _("Deploy")
success_message = _("OpenStack deployment launched")
@ -66,14 +65,13 @@ class Workflow(DeploymentValidationMixin, horizon.workflows.Workflow):
def handle(self, request, context):
try:
api.tuskar.OvercloudPlan.create(
self.request, context['role_counts'],
context['configuration'])
self.request, 'overcloud', 'overcloud')
except Exception as e:
# Showing error in both workflow tabs, because from the exception
# type we can't recognize where it should show
msg = unicode(e)
self.add_error_to_step(msg, 'undeployed_overview')
self.add_error_to_step(msg, 'deployed_configuration')
LOG.exception('Error creating overcloud')
self.add_error_to_step(msg, 'create_overview')
self.add_error_to_step(msg, 'create_configuration')
LOG.exception('Error creating overcloud plan')
raise django.forms.ValidationError(msg)
return True

View File

@ -16,7 +16,6 @@ from django.utils.translation import ugettext_lazy as _
import horizon.workflows
from openstack_dashboard.api import neutron
from tuskar_ui import api
from tuskar_ui import utils
@ -57,7 +56,7 @@ class Action(horizon.workflows.Action):
def __init__(self, request, *args, **kwargs):
super(Action, self).__init__(request, *args, **kwargs)
params = api.tuskar.OvercloudPlan.template_parameters(request).items()
params = []
params.sort()
for name, data in params:
@ -83,7 +82,7 @@ class Action(horizon.workflows.Action):
class Step(horizon.workflows.Step):
action_class = Action
contributes = ('configuration',)
template_name = 'infrastructure/overcloud/undeployed_configuration.html'
template_name = 'infrastructure/plans/create_configuration.html'
def contribute(self, data, context):
context['configuration'] = data

View File

@ -0,0 +1,53 @@
# -*- 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.
from django.utils.translation import ugettext_lazy as _
from horizon import forms
import horizon.workflows
from tuskar_ui import api
class Action(horizon.workflows.Action):
role_ids = forms.MultipleChoiceField(
label=_("Roles"),
required=True,
widget=forms.CheckboxSelectMultiple(),
help_text=_("Select roles for this plan."))
class Meta:
slug = 'create_overview'
name = _("Overview")
def __init__(self, *args, **kwargs):
super(Action, self).__init__(*args, **kwargs)
role_ids_choices = []
roles = api.tuskar.OvercloudRole.list(self.request)
for r in roles:
role_ids_choices.append((r.id, r.name))
self.fields['role_ids'].choices = sorted(
role_ids_choices)
class Step(horizon.workflows.Step):
action_class = Action
contributes = ('role_ids',)
template_name = 'infrastructure/plans/create_overview.html'
help_text = _("Nothing deployed yet. Design your first deployment.")
def contribute(self, data, context):
context = super(Step, self).contribute(data, context)
return context

View File

@ -18,11 +18,11 @@ from horizon import exceptions
import horizon.workflows
from tuskar_ui import api
from tuskar_ui.infrastructure.overcloud.workflows import scale_node_counts
from tuskar_ui.infrastructure.overcloud.workflows import undeployed
from tuskar_ui.infrastructure.plans.workflows import create
from tuskar_ui.infrastructure.plans.workflows import scale_node_counts
class Workflow(undeployed.DeploymentValidationMixin,
class Workflow(create.DeploymentValidationMixin,
horizon.workflows.Workflow):
slug = 'scale_overcloud'
name = _("Scale Deployment")

View File

@ -14,19 +14,19 @@
from django.utils.translation import ugettext_lazy as _
from tuskar_ui.infrastructure.overcloud.workflows import undeployed_overview
from tuskar_ui.infrastructure.plans.workflows import create_overview
class Action(undeployed_overview.Action):
class Action(create_overview.Action):
class Meta:
slug = 'scale_node_counts'
name = _("Node Counts")
class Step(undeployed_overview.Step):
class Step(create_overview.Step):
action_class = Action
contributes = ('role_counts', 'plan_id')
template_name = 'infrastructure/overcloud/scale_node_counts.html'
template_name = 'infrastructure/plans/scale_node_counts.html'
def prepare_action_context(self, request, context):
for (role_id, flavor_id), count in context['role_counts'].items():

View File

@ -24,34 +24,40 @@ 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_stack_list(self):
ret_val = api.heat.Stack.list(self.request)
for stack in ret_val:
self.assertIsInstance(stack, api.heat.Stack)
self.assertEqual(1, len(ret_val))
def test_overcloud_stack_events(self):
def test_stack_get(self):
stack = self.heatclient_stacks.first()
ret_val = api.heat.Stack.get(self.request, stack.id)
self.assertIsInstance(ret_val, api.heat.Stack)
def test_stack_plan(self):
stack = api.heat.Stack(self.heatclient_stacks.first())
ret_val = stack.plan
self.assertIsInstance(ret_val, api.tuskar.OvercloudPlan)
def test_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
ret_val = api.heat.Stack(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())
def test_stack_is_deployed(self):
stack = api.heat.Stack(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())
def test_stack_resources(self):
stack = api.heat.Stack(self.heatclient_stacks.first())
resources = self.heatclient_resources.list()
nodes = self.baremetalclient_nodes.list()
@ -70,49 +76,30 @@ class HeatAPITests(test.APITestCase):
for i in ret_val:
self.assertIsInstance(i, api.heat.Resource)
self.assertEqual(4, len(ret_val))
self.assertEqual(3, len(ret_val))
def test_overcloud_stack_resources_no_ironic(self):
stack = api.heat.OvercloudStack(self.heatclient_stacks.first())
def test_stack_resources_no_ironic(self):
stack = api.heat.Stack(self.heatclient_stacks.first())
role = api.tuskar.OvercloudRole(
self.tuskarclient_overcloud_roles.first())
self.tuskarclient_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)
ret_val = stack.resources_by_role(role)
for i in ret_val:
self.assertIsInstance(i, api.heat.Resource)
self.assertEqual(4, len(ret_val))
self.assertEqual(1, len(ret_val))
def test_overcloud_stack_keystone_ip(self):
stack = api.heat.OvercloudStack(self.heatclient_stacks.first())
def test_stack_keystone_ip(self):
stack = api.heat.Stack(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())
def test_stack_dashboard_url(self):
stack = api.heat.Stack(self.heatclient_stacks.first())
mocked_service = mock.Mock(id='horizon_id')
mocked_service.name = 'horizon'
@ -141,14 +128,16 @@ class HeatAPITests(test.APITestCase):
stack = self.heatclient_stacks.first()
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, stack,
resource.resource_name)
ret_val = api.heat.Resource.get(None, stack,
resource.resource_name)
self.assertIsInstance(ret_val, api.heat.Resource)
def test_resource_role(self):
resource = api.heat.Resource(self.heatclient_resources.first())
ret_val = resource.role
self.assertIsInstance(ret_val, api.tuskar.OvercloudRole)
self.assertEqual('Compute', ret_val.name)
def test_resource_node_no_ironic(self):
resource = self.heatclient_resources.first()
nodes = self.baremetalclient_nodes.list()

View File

@ -13,7 +13,6 @@
from __future__ import absolute_import
import contextlib
from mock import patch # noqa
from tuskar_ui import api
@ -21,72 +20,44 @@ from tuskar_ui.test import helpers as test
class TuskarAPITests(test.APITestCase):
def test_overcloud_plan_create(self):
plan = self.tuskarclient_overcloud_plans.first()
with patch('tuskarclient.v1.overclouds.OvercloudManager.create',
return_value=plan):
ret_val = api.tuskar.OvercloudPlan.create(self.request, {}, {})
def test_plan_create(self):
ret_val = api.tuskar.OvercloudPlan.create(self.request, {}, {})
self.assertIsInstance(ret_val, api.tuskar.OvercloudPlan)
def test_overcloud_plan_list(self):
plans = self.tuskarclient_overcloud_plans.list()
with patch('tuskarclient.v1.overclouds.OvercloudManager.list',
return_value=plans):
ret_val = api.tuskar.OvercloudPlan.list(self.request)
def test_plan_list(self):
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_plan_get(self):
plan = self.tuskarclient_overcloud_plans.first()
with patch('tuskarclient.v1.overclouds.OvercloudManager.list',
return_value=[plan]):
ret_val = api.tuskar.OvercloudPlan.get(self.request, plan.id)
def test_plan_get(self):
plan = self.tuskarclient_plans.first()
ret_val = api.tuskar.OvercloudPlan.get(self.request, plan['id'])
self.assertIsInstance(ret_val, api.tuskar.OvercloudPlan)
def test_overcloud_plan_delete(self):
plan = self.tuskarclient_overcloud_plans.first()
with patch('tuskarclient.v1.overclouds.OvercloudManager.delete',
return_value=None):
api.tuskar.OvercloudPlan.delete(self.request, plan.id)
def test_plan_delete(self):
plan = self.tuskarclient_plans.first()
api.tuskar.OvercloudPlan.delete(self.request, plan['id'])
def test_overcloud_role_list(self):
roles = self.tuskarclient_overcloud_roles.list()
def test_plan_role_list(self):
plan = api.tuskar.OvercloudPlan(self.tuskarclient_plans.first())
with patch('tuskarclient.v1.overcloud_roles.OvercloudRoleManager.list',
return_value=roles):
ret_val = api.tuskar.OvercloudRole.list(self.request)
ret_val = plan.role_list
self.assertEqual(4, len(ret_val))
for r in ret_val:
self.assertIsInstance(r, api.tuskar.OvercloudRole)
def test_role_list(self):
ret_val = api.tuskar.OvercloudRole.list(self.request)
for r in ret_val:
self.assertIsInstance(r, api.tuskar.OvercloudRole)
self.assertEqual(4, len(ret_val))
self.assertEqual(5, len(ret_val))
def test_overcloud_role_get(self):
role = self.tuskarclient_overcloud_roles.first()
with patch('tuskarclient.v1.overcloud_roles.OvercloudRoleManager.get',
return_value=role):
ret_val = api.tuskar.OvercloudRole.get(self.request, role.id)
def test_role_get(self):
role = self.tuskarclient_roles.first()
ret_val = api.tuskar.OvercloudRole.get(self.request, role['id'])
self.assertIsInstance(ret_val, api.tuskar.OvercloudRole)
def test_overcloud_role_get_by_node(self):
node = api.node.Node(
api.node.BareMetalNode(self.baremetalclient_nodes.first()))
instance = self.novaclient_servers.first()
image = self.glanceclient_images.first()
roles = self.tuskarclient_overcloud_roles.list()
with contextlib.nested(
patch('tuskarclient.v1.overcloud_roles.'
'OvercloudRoleManager.list',
return_value=roles),
patch('openstack_dashboard.api.nova.server_get',
return_value=instance),
patch('openstack_dashboard.api.glance.image_get',
return_value=image),
):
ret_val = api.tuskar.OvercloudRole.get_by_node(self.request,
node)
self.assertEqual(ret_val.name, 'Controller')

View File

@ -33,6 +33,7 @@ def data(TEST):
'output_value': 'http://192.0.2.23:5000/v2',
}],
'parameters': {
'plan_id': 'plan-1',
'one': 'one',
'two': 'two',
}})
@ -117,7 +118,7 @@ def data(TEST):
'logical_resource_id': 'Compute0',
'physical_resource_id': 'aa',
'resource_status': 'CREATE_COMPLETE',
'resource_type': 'AWS::EC2::Instance'})
'resource_type': 'Compute'})
resource_2 = resources.Resource(
resources.ResourceManager(None),
{'id': '2-resource-id',
@ -126,7 +127,7 @@ def data(TEST):
'logical_resource_id': 'Controller',
'physical_resource_id': 'bb',
'resource_status': 'CREATE_COMPLETE',
'resource_type': 'AWS::EC2::Instance'})
'resource_type': 'Controller'})
resource_3 = resources.Resource(
resources.ResourceManager(None),
{'id': '3-resource-id',
@ -135,7 +136,7 @@ def data(TEST):
'logical_resource_id': 'Compute1',
'physical_resource_id': 'cc',
'resource_status': 'CREATE_COMPLETE',
'resource_type': 'AWS::EC2::Instance'})
'resource_type': 'Compute'})
resource_4 = resources.Resource(
resources.ResourceManager(None),
{'id': '4-resource-id',
@ -144,7 +145,7 @@ def data(TEST):
'logical_resource_id': 'Compute2',
'physical_resource_id': 'dd',
'resource_status': 'CREATE_COMPLETE',
'resource_type': 'AWS::EC2::Instance'})
'resource_type': 'Compute'})
TEST.heatclient_resources.add(resource_1,
resource_2,
resource_3,
@ -157,24 +158,36 @@ def data(TEST):
{'id': 'aa',
'name': 'Compute',
'image': {'id': 1},
'flavor': {
'id': '1',
},
'status': 'ACTIVE'})
s_2 = servers.Server(
servers.ServerManager(None),
{'id': 'bb',
'name': 'Controller',
'image': {'id': 2},
'flavor': {
'id': '2',
},
'status': 'ACTIVE'})
s_3 = servers.Server(
servers.ServerManager(None),
{'id': 'cc',
'name': 'Compute',
'image': {'id': 1},
'flavor': {
'id': '1',
},
'status': 'BUILD'})
s_4 = servers.Server(
servers.ServerManager(None),
{'id': 'dd',
'name': 'Compute',
'image': {'id': 1},
'flavor': {
'id': '1',
},
'status': 'ERROR'})
TEST.novaclient_servers.add(s_1, s_2, s_3, s_4)

View File

@ -12,68 +12,79 @@
from openstack_dashboard.test.test_data import utils as test_data_utils
from tuskarclient.v1 import overcloud_roles
from tuskarclient.v1 import overclouds
def data(TEST):
# Overcloud
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,
'name': 'overcloud',
'description': 'overcloud',
'stack_id': 'stack-id-1',
'attributes': {
'AdminPassword': "unset"
}})
TEST.tuskarclient_overcloud_plans.add(oc_1)
# OvercloudPlan
TEST.tuskarclient_plans = test_data_utils.TestDataContainer()
plan_1 = {
'id': 'plan-1',
'name': 'overcloud',
'description': 'this is an overcloud deployment plan',
'created_at': '2014-05-27T21:11:09Z',
'modified_at': '2014-05-30T21:11:09Z',
'roles': [{
'id': 'role-1',
'name': 'Controller',
'version': 1,
}, {
'id': 'role-2',
'name': 'Compute',
'version': 1,
}, {
'id': 'role-3',
'name': 'Object Storage',
'version': 1,
}, {
'id': 'role-5',
'name': 'Block Storage',
'version': 2,
}],
'parameters': [{
'name': 'AdminPassword',
'label': 'Admin Password',
'description': 'Admin password',
'hidden': 'false',
'value': 'unset',
}],
}
TEST.tuskarclient_plans.add(plan_1)
# OvercloudRole
TEST.tuskarclient_overcloud_roles = test_data_utils.TestDataContainer()
r_1 = overcloud_roles.OvercloudRole(
overcloud_roles.OvercloudRoleManager(None),
{
'id': 1,
'name': 'Controller',
'description': 'controller overcloud role',
'image_name': 'overcloud-control',
'flavor_id': '',
})
r_2 = overcloud_roles.OvercloudRole(
overcloud_roles.OvercloudRoleManager(None),
{'id': 2,
'name': 'Compute',
'description': 'compute overcloud role',
'flavor_id': '',
'image_name': 'overcloud-compute'})
r_3 = overcloud_roles.OvercloudRole(
overcloud_roles.OvercloudRoleManager(None),
{'id': 3,
'name': 'Object Storage',
'description': 'object storage overcloud role',
'flavor_id': '',
'image_name': 'overcloud-object-storage'})
r_4 = overcloud_roles.OvercloudRole(
overcloud_roles.OvercloudRoleManager(None),
{'id': 4,
'name': 'Block Storage',
'description': 'block storage overcloud role',
'flavor_id': '',
'image_name': 'overcloud-block-storage'})
TEST.tuskarclient_overcloud_roles.add(r_1, r_2, r_3, r_4)
# OvercloudRoles with flavors associated
TEST.tuskarclient_roles_with_flavors = test_data_utils.TestDataContainer()
role_with_flavor = overcloud_roles.OvercloudRole(
overcloud_roles.OvercloudRoleManager(None),
{'id': 5,
'name': 'Block Storage',
'description': 'block storage overcloud role',
'flavor_id': '1',
'image_name': 'overcloud-block-storage'})
TEST.tuskarclient_roles_with_flavors.add(role_with_flavor)
TEST.tuskarclient_roles = test_data_utils.TestDataContainer()
r_1 = {
'id': 'role-1',
'name': 'Controller',
'version': 1,
'description': 'controller role',
'created_at': '2014-05-27T21:11:09Z',
}
r_2 = {
'id': 'role-2',
'name': 'Compute',
'version': 1,
'description': 'compute role',
'created_at': '2014-05-27T21:11:09Z',
}
r_3 = {
'id': 'role-3',
'name': 'Object Storage',
'version': 1,
'description': 'object storage role',
'created_at': '2014-05-27T21:11:09Z',
}
r_4 = {
'id': 'role-4',
'name': 'Block Storage',
'version': 1,
'description': 'block storage role',
'created_at': '2014-05-27T21:11:09Z',
}
r_5 = {
'id': 'role-5',
'name': 'Block Storage',
'version': 2,
'description': 'block storage role',
'created_at': '2014-05-28T21:11:09Z',
}
TEST.tuskarclient_roles.add(r_1, r_2, r_3, r_4, r_5)