From a18f40554c9d87e3134c067345acf8775922e1b9 Mon Sep 17 00:00:00 2001 From: Tzu-Mainn Chen Date: Tue, 1 Jul 2014 20:13:30 +0200 Subject: [PATCH] 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 --- tuskar_ui/api/flavor.py | 27 +- tuskar_ui/api/heat.py | 131 +++++-- tuskar_ui/api/node.py | 81 ++--- tuskar_ui/api/tuskar.py | 206 ++++------- tuskar_ui/infrastructure/dashboard.py | 1 + tuskar_ui/infrastructure/flavors/tests.py | 62 +--- tuskar_ui/infrastructure/flavors/views.py | 5 +- tuskar_ui/infrastructure/nodes/tables.py | 2 +- tuskar_ui/infrastructure/nodes/tabs.py | 13 +- .../nodes/templates/nodes/_overview.html | 5 +- tuskar_ui/infrastructure/nodes/tests.py | 10 +- tuskar_ui/infrastructure/nodes/views.py | 1 - tuskar_ui/infrastructure/overcloud/forms.py | 49 +-- tuskar_ui/infrastructure/overcloud/tabs.py | 52 ++- .../templates/overcloud/_detail_overview.html | 2 +- .../templates/overcloud/_role_edit.html | 15 - .../overcloud/_undeploy_confirmation.html | 2 +- .../overcloud/_undeploy_in_progress.html | 2 +- .../overcloud/templates/overcloud/detail.html | 10 +- .../templates/overcloud/role_edit.html | 11 - .../overcloud/undeployed_overview.html | 33 -- tuskar_ui/infrastructure/overcloud/tests.py | 342 ++---------------- tuskar_ui/infrastructure/overcloud/urls.py | 13 +- tuskar_ui/infrastructure/overcloud/views.py | 190 ++++------ .../workflows/undeployed_overview.py | 134 ------- .../workflows => plans}/__init__.py | 0 tuskar_ui/infrastructure/plans/forms.py | 66 ++++ tuskar_ui/infrastructure/plans/panel.py | 27 ++ tuskar_ui/infrastructure/plans/tables.py | 35 ++ .../plans/create_configuration.html} | 0 .../templates/plans/create_overview.html | 16 + .../templates/plans}/node_counts.html | 11 - .../templates/plans}/scale_node_counts.html | 0 tuskar_ui/infrastructure/plans/tests.py | 65 ++++ tuskar_ui/infrastructure/plans/urls.py | 26 ++ tuskar_ui/infrastructure/plans/views.py | 91 +++++ .../plans/workflows/__init__.py | 0 .../workflows/create.py} | 24 +- .../workflows/create_configuration.py} | 5 +- .../plans/workflows/create_overview.py | 53 +++ .../{overcloud => plans}/workflows/scale.py | 6 +- .../workflows/scale_node_counts.py | 8 +- tuskar_ui/test/api_tests/heat_tests.py | 89 ++--- tuskar_ui/test/api_tests/tuskar_tests.py | 77 ++-- tuskar_ui/test/test_data/heat_data.py | 21 +- tuskar_ui/test/test_data/tuskar_data.py | 131 ++++--- 46 files changed, 935 insertions(+), 1215 deletions(-) delete mode 100644 tuskar_ui/infrastructure/overcloud/templates/overcloud/_role_edit.html delete mode 100644 tuskar_ui/infrastructure/overcloud/templates/overcloud/role_edit.html delete mode 100644 tuskar_ui/infrastructure/overcloud/templates/overcloud/undeployed_overview.html delete mode 100644 tuskar_ui/infrastructure/overcloud/workflows/undeployed_overview.py rename tuskar_ui/infrastructure/{overcloud/workflows => plans}/__init__.py (100%) create mode 100644 tuskar_ui/infrastructure/plans/forms.py create mode 100644 tuskar_ui/infrastructure/plans/panel.py create mode 100644 tuskar_ui/infrastructure/plans/tables.py rename tuskar_ui/infrastructure/{overcloud/templates/overcloud/undeployed_configuration.html => plans/templates/plans/create_configuration.html} (100%) create mode 100644 tuskar_ui/infrastructure/plans/templates/plans/create_overview.html rename tuskar_ui/infrastructure/{overcloud/templates/overcloud => plans/templates/plans}/node_counts.html (71%) rename tuskar_ui/infrastructure/{overcloud/templates/overcloud => plans/templates/plans}/scale_node_counts.html (100%) create mode 100644 tuskar_ui/infrastructure/plans/tests.py create mode 100644 tuskar_ui/infrastructure/plans/urls.py create mode 100644 tuskar_ui/infrastructure/plans/views.py create mode 100644 tuskar_ui/infrastructure/plans/workflows/__init__.py rename tuskar_ui/infrastructure/{overcloud/workflows/undeployed.py => plans/workflows/create.py} (79%) rename tuskar_ui/infrastructure/{overcloud/workflows/undeployed_configuration.py => plans/workflows/create_configuration.py} (94%) create mode 100644 tuskar_ui/infrastructure/plans/workflows/create_overview.py rename tuskar_ui/infrastructure/{overcloud => plans}/workflows/scale.py (89%) rename tuskar_ui/infrastructure/{overcloud => plans}/workflows/scale_node_counts.py (82%) diff --git a/tuskar_ui/api/flavor.py b/tuskar_ui/api/flavor.py index a0d1d1b54..07966e78d 100644 --- a/tuskar_ui/api/flavor.py +++ b/tuskar_ui/api/flavor.py @@ -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 diff --git a/tuskar_ui/api/heat.py b/tuskar_ui/api/heat.py index 9ea75fd6a..6469a3c94 100644 --- a/tuskar_ui/api/heat.py +++ b/tuskar_ui/api/heat.py @@ -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): diff --git a/tuskar_ui/api/node.py b/tuskar_ui/api/node.py index 402126b67..992310e33 100644 --- a/tuskar_ui/api/node.py +++ b/tuskar_ui/api/node.py @@ -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 diff --git a/tuskar_ui/api/tuskar.py b/tuskar_ui/api/tuskar.py index 727dafb61..776f7dae3 100644 --- a/tuskar_ui/api/tuskar.py +++ b/tuskar_ui/api/tuskar.py @@ -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' diff --git a/tuskar_ui/infrastructure/dashboard.py b/tuskar_ui/infrastructure/dashboard.py index 27c24848c..93ad1c01b 100644 --- a/tuskar_ui/infrastructure/dashboard.py +++ b/tuskar_ui/infrastructure/dashboard.py @@ -21,6 +21,7 @@ class BasePanels(horizon.PanelGroup): name = _("Infrastructure") panels = ( 'overcloud', + 'plans', 'nodes', 'flavors', ) diff --git a/tuskar_ui/infrastructure/flavors/tests.py b/tuskar_ui/infrastructure/flavors/tests.py index 65fb47a0c..e6bd35b3e 100644 --- a/tuskar_ui/infrastructure/flavors/tests.py +++ b/tuskar_ui/infrastructure/flavors/tests.py @@ -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') diff --git a/tuskar_ui/infrastructure/flavors/views.py b/tuskar_ui/infrastructure/flavors/views.py index 0f368ca7b..6c113b00e 100644 --- a/tuskar_ui/infrastructure/flavors/views.py +++ b/tuskar_ui/infrastructure/flavors/views.py @@ -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 [] diff --git a/tuskar_ui/infrastructure/nodes/tables.py b/tuskar_ui/infrastructure/nodes/tables.py index e483bdb8f..2236c9bc0 100644 --- a/tuskar_ui/infrastructure/nodes/tables.py +++ b/tuskar_ui/infrastructure/nodes/tables.py @@ -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 diff --git a/tuskar_ui/infrastructure/nodes/tabs.py b/tuskar_ui/infrastructure/nodes/tabs.py index c177c582b..7b45a5061 100644 --- a/tuskar_ui/infrastructure/nodes/tabs.py +++ b/tuskar_ui/infrastructure/nodes/tabs.py @@ -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 diff --git a/tuskar_ui/infrastructure/nodes/templates/nodes/_overview.html b/tuskar_ui/infrastructure/nodes/templates/nodes/_overview.html index 069525001..c21f8dac3 100644 --- a/tuskar_ui/infrastructure/nodes/templates/nodes/_overview.html +++ b/tuskar_ui/infrastructure/nodes/templates/nodes/_overview.html @@ -19,10 +19,7 @@ {% if deployed_nodes_error %} {% 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 %} diff --git a/tuskar_ui/infrastructure/nodes/tests.py b/tuskar_ui/infrastructure/nodes/tests.py index b4fae4508..dfc03cd3d 100644 --- a/tuskar_ui/infrastructure/nodes/tests.py +++ b/tuskar_ui/infrastructure/nodes/tests.py @@ -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', **{ diff --git a/tuskar_ui/infrastructure/nodes/views.py b/tuskar_ui/infrastructure/nodes/views.py index 5d60d36b8..54788fa3e 100644 --- a/tuskar_ui/infrastructure/nodes/views.py +++ b/tuskar_ui/infrastructure/nodes/views.py @@ -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')), diff --git a/tuskar_ui/infrastructure/overcloud/forms.py b/tuskar_ui/infrastructure/overcloud/forms.py index 4027e79d7..2b04e715d 100644 --- a/tuskar_ui/infrastructure/overcloud/forms.py +++ b/tuskar_ui/infrastructure/overcloud/forms.py @@ -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 diff --git a/tuskar_ui/infrastructure/overcloud/tabs.py b/tuskar_ui/infrastructure/overcloud/tabs.py index c840e05e9..3c2beedf9 100644 --- a/tuskar_ui/infrastructure/overcloud/tabs.py +++ b/tuskar_ui/infrastructure/overcloud/tabs.py @@ -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): diff --git a/tuskar_ui/infrastructure/overcloud/templates/overcloud/_detail_overview.html b/tuskar_ui/infrastructure/overcloud/templates/overcloud/_detail_overview.html index 751d54355..f8b0b1bd8 100644 --- a/tuskar_ui/infrastructure/overcloud/templates/overcloud/_detail_overview.html +++ b/tuskar_ui/infrastructure/overcloud/templates/overcloud/_detail_overview.html @@ -191,7 +191,7 @@ nova flavor-create m1.tiny 1 512 2 1 {% for role in roles %} {{ role.name }} ({{ role.total_node_count }}) diff --git a/tuskar_ui/infrastructure/overcloud/templates/overcloud/_role_edit.html b/tuskar_ui/infrastructure/overcloud/templates/overcloud/_role_edit.html deleted file mode 100644 index c8eae3646..000000000 --- a/tuskar_ui/infrastructure/overcloud/templates/overcloud/_role_edit.html +++ /dev/null @@ -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 %} - - {% trans "Cancel" %} -{% endblock %} diff --git a/tuskar_ui/infrastructure/overcloud/templates/overcloud/_undeploy_confirmation.html b/tuskar_ui/infrastructure/overcloud/templates/overcloud/_undeploy_confirmation.html index 9742d9ea1..a0f29085d 100644 --- a/tuskar_ui/infrastructure/overcloud/templates/overcloud/_undeploy_confirmation.html +++ b/tuskar_ui/infrastructure/overcloud/templates/overcloud/_undeploy_confirmation.html @@ -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 %} diff --git a/tuskar_ui/infrastructure/overcloud/templates/overcloud/_undeploy_in_progress.html b/tuskar_ui/infrastructure/overcloud/templates/overcloud/_undeploy_in_progress.html index 468b8dbf7..a231f4fdd 100644 --- a/tuskar_ui/infrastructure/overcloud/templates/overcloud/_undeploy_in_progress.html +++ b/tuskar_ui/infrastructure/overcloud/templates/overcloud/_undeploy_in_progress.html @@ -4,7 +4,7 @@
- {% if plan.stack.is_deleting %} + {% if stack.is_deleting %}
diff --git a/tuskar_ui/infrastructure/overcloud/templates/overcloud/detail.html b/tuskar_ui/infrastructure/overcloud/templates/overcloud/detail.html index 45ba22a46..dfc780896 100644 --- a/tuskar_ui/infrastructure/overcloud/templates/overcloud/detail.html +++ b/tuskar_ui/infrastructure/overcloud/templates/overcloud/detail.html @@ -19,20 +19,12 @@
- {% trans "Undeploy" %} - {% comment %} no scaling for Icehouse, uncomment when ready - - - {% trans "Scale deployment" %} - - {% endcomment %}
{{ tab_group.render }}
diff --git a/tuskar_ui/infrastructure/overcloud/templates/overcloud/role_edit.html b/tuskar_ui/infrastructure/overcloud/templates/overcloud/role_edit.html deleted file mode 100644 index 4cc1075c6..000000000 --- a/tuskar_ui/infrastructure/overcloud/templates/overcloud/role_edit.html +++ /dev/null @@ -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 %} diff --git a/tuskar_ui/infrastructure/overcloud/templates/overcloud/undeployed_overview.html b/tuskar_ui/infrastructure/overcloud/templates/overcloud/undeployed_overview.html deleted file mode 100644 index 5c02e2ecd..000000000 --- a/tuskar_ui/infrastructure/overcloud/templates/overcloud/undeployed_overview.html +++ /dev/null @@ -1,33 +0,0 @@ -{% load i18n %} -{% load url from future%} - - -
{{ step.get_help_text }}
- -
-
-
- {{ step.get_free_nodes }} {% trans "free nodes" %} -
-

{% trans "Roles" %}

-
- {% include 'infrastructure/overcloud/node_counts.html' with form=form editable=True %} -
-
-
-
-

{% trans "Configuration" %}

-

{% trans "Configuration options will be auto-detected." %}

-

{% trans "See and change defaults." %}

-
-
-
- diff --git a/tuskar_ui/infrastructure/overcloud/tests.py b/tuskar_ui/infrastructure/overcloud/tests.py index 652071ec0..fc026963e 100644 --- a/tuskar_ui/infrastructure/overcloud/tests.py +++ b/tuskar_ui/infrastructure/overcloud/tests.py @@ -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) diff --git a/tuskar_ui/infrastructure/overcloud/urls.py b/tuskar_ui/infrastructure/overcloud/urls.py index 1365bca3b..6eb5ec9a4 100644 --- a/tuskar_ui/infrastructure/overcloud/urls.py +++ b/tuskar_ui/infrastructure/overcloud/urls.py @@ -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[^/]+)/undeploy-in-progress$', + urls.url(r'^(?P[^/]+)/undeploy-in-progress$', views.UndeployInProgressView.as_view(), name='undeploy_in_progress'), - urls.url(r'^create/role-edit/(?P[^/]+)$', - views.OvercloudRoleEdit.as_view(), name='role_edit'), - urls.url(r'^(?P[^/]+)/$', views.DetailView.as_view(), + urls.url(r'^(?P[^/]+)/$', views.DetailView.as_view(), name='detail'), - urls.url(r'^(?P[^/]+)/scale$', views.Scale.as_view(), - name='scale'), - urls.url(r'^(?P[^/]+)/role/(?P[^/]+)$', + urls.url(r'^(?P[^/]+)/role/(?P[^/]+)$', views.OvercloudRoleView.as_view(), name='role'), - urls.url(r'^(?P[^/]+)/undeploy-confirmation$', + urls.url(r'^(?P[^/]+)/undeploy-confirmation$', views.UndeployConfirmationView.as_view(), name='undeploy_confirmation'), ) diff --git a/tuskar_ui/infrastructure/overcloud/views.py b/tuskar_ui/infrastructure/overcloud/views.py index 15990ee0c..68bfc1079 100644 --- a/tuskar_ui/infrastructure/overcloud/views.py +++ b/tuskar_ui/infrastructure/overcloud/views.py @@ -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, - } diff --git a/tuskar_ui/infrastructure/overcloud/workflows/undeployed_overview.py b/tuskar_ui/infrastructure/overcloud/workflows/undeployed_overview.py deleted file mode 100644 index 6bb01b1ee..000000000 --- a/tuskar_ui/infrastructure/overcloud/workflows/undeployed_overview.py +++ /dev/null @@ -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 diff --git a/tuskar_ui/infrastructure/overcloud/workflows/__init__.py b/tuskar_ui/infrastructure/plans/__init__.py similarity index 100% rename from tuskar_ui/infrastructure/overcloud/workflows/__init__.py rename to tuskar_ui/infrastructure/plans/__init__.py diff --git a/tuskar_ui/infrastructure/plans/forms.py b/tuskar_ui/infrastructure/plans/forms.py new file mode 100644 index 000000000..5d04b348f --- /dev/null +++ b/tuskar_ui/infrastructure/plans/forms.py @@ -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 diff --git a/tuskar_ui/infrastructure/plans/panel.py b/tuskar_ui/infrastructure/plans/panel.py new file mode 100644 index 000000000..87247f997 --- /dev/null +++ b/tuskar_ui/infrastructure/plans/panel.py @@ -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) diff --git a/tuskar_ui/infrastructure/plans/tables.py b/tuskar_ui/infrastructure/plans/tables.py new file mode 100644 index 000000000..2be5d6f91 --- /dev/null +++ b/tuskar_ui/infrastructure/plans/tables.py @@ -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] diff --git a/tuskar_ui/infrastructure/overcloud/templates/overcloud/undeployed_configuration.html b/tuskar_ui/infrastructure/plans/templates/plans/create_configuration.html similarity index 100% rename from tuskar_ui/infrastructure/overcloud/templates/overcloud/undeployed_configuration.html rename to tuskar_ui/infrastructure/plans/templates/plans/create_configuration.html diff --git a/tuskar_ui/infrastructure/plans/templates/plans/create_overview.html b/tuskar_ui/infrastructure/plans/templates/plans/create_overview.html new file mode 100644 index 000000000..bb8e889b8 --- /dev/null +++ b/tuskar_ui/infrastructure/plans/templates/plans/create_overview.html @@ -0,0 +1,16 @@ +{% load i18n %} +{% load url from future%} + + +
{{ step.get_help_text }}
+ +
+
+

{% trans "Roles" %}

+
+ + {% include "horizon/common/_form_fields.html" %} + +
+
+
diff --git a/tuskar_ui/infrastructure/overcloud/templates/overcloud/node_counts.html b/tuskar_ui/infrastructure/plans/templates/plans/node_counts.html similarity index 71% rename from tuskar_ui/infrastructure/overcloud/templates/overcloud/node_counts.html rename to tuskar_ui/infrastructure/plans/templates/plans/node_counts.html index cb4956a42..e8c8ff063 100644 --- a/tuskar_ui/infrastructure/overcloud/templates/overcloud/node_counts.html +++ b/tuskar_ui/infrastructure/plans/templates/plans/node_counts.html @@ -17,23 +17,12 @@ {% if forloop.first %} - {% if editable %} - - {% endif %} {{ label }} {% endif %} {% if field.field.label %} {{ field.label }} - {% elif editable %} - ({% trans "Add a flavor" %}) {% else %} ({% trans "No flavor" %}) {% endif %} diff --git a/tuskar_ui/infrastructure/overcloud/templates/overcloud/scale_node_counts.html b/tuskar_ui/infrastructure/plans/templates/plans/scale_node_counts.html similarity index 100% rename from tuskar_ui/infrastructure/overcloud/templates/overcloud/scale_node_counts.html rename to tuskar_ui/infrastructure/plans/templates/plans/scale_node_counts.html diff --git a/tuskar_ui/infrastructure/plans/tests.py b/tuskar_ui/infrastructure/plans/tests.py new file mode 100644 index 000000000..1c1e6a572 --- /dev/null +++ b/tuskar_ui/infrastructure/plans/tests.py @@ -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) diff --git a/tuskar_ui/infrastructure/plans/urls.py b/tuskar_ui/infrastructure/plans/urls.py new file mode 100644 index 000000000..7ffa5d271 --- /dev/null +++ b/tuskar_ui/infrastructure/plans/urls.py @@ -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[^/]+)/scale$', views.Scale.as_view(), + name='scale'), +) diff --git a/tuskar_ui/infrastructure/plans/views.py b/tuskar_ui/infrastructure/plans/views.py new file mode 100644 index 000000000..a8958d863 --- /dev/null +++ b/tuskar_ui/infrastructure/plans/views.py @@ -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, + } diff --git a/tuskar_ui/infrastructure/plans/workflows/__init__.py b/tuskar_ui/infrastructure/plans/workflows/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tuskar_ui/infrastructure/overcloud/workflows/undeployed.py b/tuskar_ui/infrastructure/plans/workflows/create.py similarity index 79% rename from tuskar_ui/infrastructure/overcloud/workflows/undeployed.py rename to tuskar_ui/infrastructure/plans/workflows/create.py index a0ca717a6..fe083544d 100644 --- a/tuskar_ui/infrastructure/overcloud/workflows/undeployed.py +++ b/tuskar_ui/infrastructure/plans/workflows/create.py @@ -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 diff --git a/tuskar_ui/infrastructure/overcloud/workflows/undeployed_configuration.py b/tuskar_ui/infrastructure/plans/workflows/create_configuration.py similarity index 94% rename from tuskar_ui/infrastructure/overcloud/workflows/undeployed_configuration.py rename to tuskar_ui/infrastructure/plans/workflows/create_configuration.py index bc6697d5c..41a9f84de 100644 --- a/tuskar_ui/infrastructure/overcloud/workflows/undeployed_configuration.py +++ b/tuskar_ui/infrastructure/plans/workflows/create_configuration.py @@ -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 diff --git a/tuskar_ui/infrastructure/plans/workflows/create_overview.py b/tuskar_ui/infrastructure/plans/workflows/create_overview.py new file mode 100644 index 000000000..45284694d --- /dev/null +++ b/tuskar_ui/infrastructure/plans/workflows/create_overview.py @@ -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 diff --git a/tuskar_ui/infrastructure/overcloud/workflows/scale.py b/tuskar_ui/infrastructure/plans/workflows/scale.py similarity index 89% rename from tuskar_ui/infrastructure/overcloud/workflows/scale.py rename to tuskar_ui/infrastructure/plans/workflows/scale.py index fdbed0e62..da91d9a6d 100644 --- a/tuskar_ui/infrastructure/overcloud/workflows/scale.py +++ b/tuskar_ui/infrastructure/plans/workflows/scale.py @@ -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") diff --git a/tuskar_ui/infrastructure/overcloud/workflows/scale_node_counts.py b/tuskar_ui/infrastructure/plans/workflows/scale_node_counts.py similarity index 82% rename from tuskar_ui/infrastructure/overcloud/workflows/scale_node_counts.py rename to tuskar_ui/infrastructure/plans/workflows/scale_node_counts.py index 5bbbe5a67..c001d1ee3 100644 --- a/tuskar_ui/infrastructure/overcloud/workflows/scale_node_counts.py +++ b/tuskar_ui/infrastructure/plans/workflows/scale_node_counts.py @@ -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(): diff --git a/tuskar_ui/test/api_tests/heat_tests.py b/tuskar_ui/test/api_tests/heat_tests.py index 33dfb0d33..ba63cdb51 100644 --- a/tuskar_ui/test/api_tests/heat_tests.py +++ b/tuskar_ui/test/api_tests/heat_tests.py @@ -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() diff --git a/tuskar_ui/test/api_tests/tuskar_tests.py b/tuskar_ui/test/api_tests/tuskar_tests.py index 4db50f481..4f14e33cf 100644 --- a/tuskar_ui/test/api_tests/tuskar_tests.py +++ b/tuskar_ui/test/api_tests/tuskar_tests.py @@ -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') diff --git a/tuskar_ui/test/test_data/heat_data.py b/tuskar_ui/test/test_data/heat_data.py index 7ae87af1f..0d50886d0 100644 --- a/tuskar_ui/test/test_data/heat_data.py +++ b/tuskar_ui/test/test_data/heat_data.py @@ -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) diff --git a/tuskar_ui/test/test_data/tuskar_data.py b/tuskar_ui/test/test_data/tuskar_data.py index 69c591d73..5fee10136 100644 --- a/tuskar_ui/test/test_data/tuskar_data.py +++ b/tuskar_ui/test/test_data/tuskar_data.py @@ -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)