Code update for new Tuskar API
* Currently uses mock data * Many tests have call_count asserts removed * Many tests are removed pending additional API refactoring * Plan creation workflow needs to be fixed; successive workflow steps depend on initial role-selection step * Overcloud and plans index redirect logic may need to be rethought * Plans need to take responsibility for image and flavor (instead of role) Change-Id: I3299a66b4f11b74196d88c458b276cad0851cbab
This commit is contained in:
parent
b919af12f3
commit
a18f40554c
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -21,6 +21,7 @@ class BasePanels(horizon.PanelGroup):
|
||||
name = _("Infrastructure")
|
||||
panels = (
|
||||
'overcloud',
|
||||
'plans',
|
||||
'nodes',
|
||||
'flavors',
|
||||
)
|
||||
|
@ -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')
|
||||
|
@ -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 []
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -19,10 +19,7 @@
|
||||
{% if deployed_nodes_error %}
|
||||
<i class="icon-exclamation-sign"></i>
|
||||
{% if deployed_nodes_error|length == 1 %}
|
||||
{% comment %}
|
||||
Replace id with uuid when ironicclient is used instead baremetalclient
|
||||
{% endcomment %}
|
||||
{% url 'horizon:infrastructure:nodes:detail' deployed_nodes_error.0.id as node_detail_url %}
|
||||
{% url 'horizon:infrastructure:nodes:detail' deployed_nodes_error.0.uuid as node_detail_url %}
|
||||
{% else %}
|
||||
{% url 'horizon:infrastructure:nodes:index' as nodes_index_url %}
|
||||
{% endif %}
|
||||
|
@ -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', **{
|
||||
|
@ -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')),
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -191,7 +191,7 @@ nova flavor-create m1.tiny 1 512 2 1
|
||||
{% for role in roles %}
|
||||
<tr>
|
||||
<td><a
|
||||
href="{% url 'horizon:infrastructure:overcloud:role' plan.id role.role.id %}"
|
||||
href="{% url 'horizon:infrastructure:overcloud:role' stack.id role.role.id %}"
|
||||
>{{ role.name }} <span class="badge">({{ role.total_node_count }})</span></a>
|
||||
</td>
|
||||
<td>
|
||||
|
@ -1,15 +0,0 @@
|
||||
{% extends "horizon/common/_modal_form.html" %}
|
||||
{% load i18n %}
|
||||
{% load url from future %}
|
||||
|
||||
{% block form_id %}role_edit_form{% endblock %}
|
||||
{% block modal_id %}role_edit_modal{% endblock %}
|
||||
{% block modal-header %}{% trans "Edit Deployment Role" %}{% endblock %}
|
||||
{% block form_action %}{% url 'horizon:infrastructure:overcloud:role_edit' form.id.value %}{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<input class="btn btn-primary pull-right" type="submit"
|
||||
value="{% trans "Apply Changes" %}" />
|
||||
<a href="{% url 'horizon:infrastructure:overcloud:create' %}"
|
||||
class="btn secondary cancel close">{% trans "Cancel" %}</a>
|
||||
{% endblock %}
|
@ -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 %}
|
||||
|
@ -4,7 +4,7 @@
|
||||
<div class="span12">
|
||||
<div class="actions pull-right">
|
||||
</div>
|
||||
{% if plan.stack.is_deleting %}
|
||||
{% if stack.is_deleting %}
|
||||
<div class="alert alert-error">
|
||||
<div class="row-fluid">
|
||||
<div class="span2">
|
||||
|
@ -19,20 +19,12 @@
|
||||
<div class="row-fluid">
|
||||
<div class="span12">
|
||||
<div class="actions pull-right">
|
||||
<a href="{% url 'horizon:infrastructure:overcloud:undeploy_confirmation' plan.id %}"
|
||||
<a href="{% url 'horizon:infrastructure:overcloud:undeploy_confirmation' stack.id %}"
|
||||
class="btn btn-danger ajax-modal
|
||||
{% if not stack.is_deployed and not stack.is_failed %}disabled{% endif %}">
|
||||
<i class="icon-fire icon-white"></i>
|
||||
{% trans "Undeploy" %}
|
||||
</a>
|
||||
{% comment %} no scaling for Icehouse, uncomment when ready
|
||||
<a href="{% url 'horizon:infrastructure:overcloud:scale' plan.id %}"
|
||||
class="btn ajax-modal
|
||||
{% if not stack.is_deployed %}disabled{% endif %}">
|
||||
<i class="icon-resize-full"></i>
|
||||
{% trans "Scale deployment" %}
|
||||
</a>
|
||||
{% endcomment %}
|
||||
</div>
|
||||
{{ tab_group.render }}
|
||||
</div>
|
||||
|
@ -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 %}
|
@ -1,33 +0,0 @@
|
||||
{% load i18n %}
|
||||
{% load url from future%}
|
||||
|
||||
<noscript><h3>{{ step }}</h3></noscript>
|
||||
<div class="widget muted">{{ step.get_help_text }}</div>
|
||||
|
||||
<div class="row-fluid">
|
||||
<div class="span8">
|
||||
<div class="pull-right">
|
||||
<span id="free-nodes">{{ step.get_free_nodes }}</span> {% trans "free nodes" %}
|
||||
</div>
|
||||
<h2>{% trans "Roles" %}</h2>
|
||||
<div class="widget">
|
||||
{% include 'infrastructure/overcloud/node_counts.html' with form=form editable=True %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="span4">
|
||||
<div class="widget">
|
||||
<h2>{% trans "Configuration" %}</h2>
|
||||
<p class="muted">{% trans "Configuration options will be auto-detected." %}</p>
|
||||
<p><a href="#undeployed_overcloud__deployed_configuration"
|
||||
data-toggle="tab">{% trans "See and change defaults." %}</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
(window.$ || window.addHorizonLoadEvent)(function () {
|
||||
$('p > a[href="#undeployed_overcloud__deployed_configuration"]'
|
||||
).click(function () {
|
||||
$('li > a[href="#undeployed_overcloud__deployed_configuration"]').tab('show');
|
||||
});
|
||||
});
|
||||
</script>
|
@ -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)
|
||||
|
@ -20,19 +20,14 @@ from tuskar_ui.infrastructure.overcloud import views
|
||||
urlpatterns = urls.patterns(
|
||||
'',
|
||||
urls.url(r'^$', views.IndexView.as_view(), name='index'),
|
||||
urls.url(r'^create/$', views.CreateView.as_view(), name='create'),
|
||||
urls.url(r'^(?P<plan_id>[^/]+)/undeploy-in-progress$',
|
||||
urls.url(r'^(?P<stack_id>[^/]+)/undeploy-in-progress$',
|
||||
views.UndeployInProgressView.as_view(),
|
||||
name='undeploy_in_progress'),
|
||||
urls.url(r'^create/role-edit/(?P<role_id>[^/]+)$',
|
||||
views.OvercloudRoleEdit.as_view(), name='role_edit'),
|
||||
urls.url(r'^(?P<plan_id>[^/]+)/$', views.DetailView.as_view(),
|
||||
urls.url(r'^(?P<stack_id>[^/]+)/$', views.DetailView.as_view(),
|
||||
name='detail'),
|
||||
urls.url(r'^(?P<plan_id>[^/]+)/scale$', views.Scale.as_view(),
|
||||
name='scale'),
|
||||
urls.url(r'^(?P<plan_id>[^/]+)/role/(?P<role_id>[^/]+)$',
|
||||
urls.url(r'^(?P<stack_id>[^/]+)/role/(?P<role_id>[^/]+)$',
|
||||
views.OvercloudRoleView.as_view(), name='role'),
|
||||
urls.url(r'^(?P<plan_id>[^/]+)/undeploy-confirmation$',
|
||||
urls.url(r'^(?P<stack_id>[^/]+)/undeploy-confirmation$',
|
||||
views.UndeployConfirmationView.as_view(),
|
||||
name='undeploy_confirmation'),
|
||||
)
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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
|
66
tuskar_ui/infrastructure/plans/forms.py
Normal file
66
tuskar_ui/infrastructure/plans/forms.py
Normal file
@ -0,0 +1,66 @@
|
||||
# -*- coding: utf8 -*-
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import django.forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
import horizon.exceptions
|
||||
import horizon.forms
|
||||
import horizon.messages
|
||||
from openstack_dashboard import api as horizon_api
|
||||
|
||||
from tuskar_ui import api
|
||||
|
||||
|
||||
def get_flavor_choices(request):
|
||||
empty = [('', '----')]
|
||||
try:
|
||||
flavors = horizon_api.nova.flavor_list(request, None)
|
||||
except Exception:
|
||||
horizon.exceptions.handle(request,
|
||||
_('Unable to retrieve flavor list.'))
|
||||
return empty
|
||||
return empty + [(flavor.id, flavor.name) for flavor in flavors]
|
||||
|
||||
|
||||
class OvercloudRoleForm(horizon.forms.SelfHandlingForm):
|
||||
id = django.forms.IntegerField(
|
||||
widget=django.forms.HiddenInput)
|
||||
name = django.forms.CharField(
|
||||
label=_("Name"), required=False,
|
||||
widget=django.forms.TextInput(
|
||||
attrs={'readonly': 'readonly', 'disabled': 'disabled'}))
|
||||
description = django.forms.CharField(
|
||||
label=_("Description"), required=False,
|
||||
widget=django.forms.Textarea(
|
||||
attrs={'readonly': 'readonly', 'disabled': 'disabled'}))
|
||||
image_name = django.forms.CharField(
|
||||
label=_("Image"), required=False,
|
||||
widget=django.forms.TextInput(
|
||||
attrs={'readonly': 'readonly', 'disabled': 'disabled'}))
|
||||
flavor_id = django.forms.ChoiceField(
|
||||
label=_("Flavor"), required=False, choices=())
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(OvercloudRoleForm, self).__init__(*args, **kwargs)
|
||||
self.fields['flavor_id'].choices = get_flavor_choices(self.request)
|
||||
|
||||
def handle(self, request, context):
|
||||
try:
|
||||
role = api.tuskar.OvercloudRole.get(request, context['id'])
|
||||
role.update(request, flavor_id=context['flavor_id'])
|
||||
except Exception:
|
||||
horizon.exceptions.handle(request,
|
||||
_('Unable to update the role.'))
|
||||
return False
|
||||
return True
|
27
tuskar_ui/infrastructure/plans/panel.py
Normal file
27
tuskar_ui/infrastructure/plans/panel.py
Normal file
@ -0,0 +1,27 @@
|
||||
# -*- coding: utf8 -*-
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
import horizon
|
||||
|
||||
from tuskar_ui.infrastructure import dashboard
|
||||
|
||||
|
||||
class Plans(horizon.Panel):
|
||||
name = _("Plans")
|
||||
slug = "plans"
|
||||
|
||||
|
||||
dashboard.Infrastructure.register(Plans)
|
35
tuskar_ui/infrastructure/plans/tables.py
Normal file
35
tuskar_ui/infrastructure/plans/tables.py
Normal file
@ -0,0 +1,35 @@
|
||||
# -*- coding: utf8 -*-
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from horizon import tables
|
||||
|
||||
|
||||
class ConfigurationTable(tables.DataTable):
|
||||
|
||||
key = tables.Column(lambda parameter: parameter[0],
|
||||
verbose_name=_("Attribute Name"))
|
||||
value = tables.Column(lambda parameter: parameter[1],
|
||||
verbose_name=_("Attribute Value"))
|
||||
|
||||
class Meta:
|
||||
name = "configuration"
|
||||
verbose_name = _("Configuration")
|
||||
multi_select = False
|
||||
table_actions = ()
|
||||
row_actions = ()
|
||||
|
||||
def get_object_id(self, datum):
|
||||
return datum[0]
|
@ -0,0 +1,16 @@
|
||||
{% load i18n %}
|
||||
{% load url from future%}
|
||||
|
||||
<noscript><h3>{{ step }}</h3></noscript>
|
||||
<div class="widget muted">{{ step.get_help_text }}</div>
|
||||
|
||||
<div class="row-fluid">
|
||||
<div class="span8">
|
||||
<h2>{% trans "Roles" %}</h2>
|
||||
<div class="widget">
|
||||
<td class="actions">
|
||||
{% include "horizon/common/_form_fields.html" %}
|
||||
</td>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -17,23 +17,12 @@
|
||||
<tr>
|
||||
{% if forloop.first %}
|
||||
<td rowspan="{{ fields|length }}">
|
||||
{% if editable %}
|
||||
<a
|
||||
href="{% url 'horizon:infrastructure:overcloud:role_edit' role_id %}"
|
||||
class="ajax-modal"
|
||||
><i class="icon-pencil"></i></a>
|
||||
{% endif %}
|
||||
{{ label }}
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
{% if field.field.label %}
|
||||
{{ field.label }}
|
||||
{% elif editable %}
|
||||
(<a
|
||||
href="{% url 'horizon:infrastructure:overcloud:role_edit' role_id %}"
|
||||
class="ajax-modal"
|
||||
>{% trans "Add a flavor" %}</a>)
|
||||
{% else %}
|
||||
({% trans "No flavor" %})
|
||||
{% endif %}
|
65
tuskar_ui/infrastructure/plans/tests.py
Normal file
65
tuskar_ui/infrastructure/plans/tests.py
Normal file
@ -0,0 +1,65 @@
|
||||
# -*- coding: utf8 -*-
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import contextlib
|
||||
|
||||
from django.core import urlresolvers
|
||||
from mock import patch, call # noqa
|
||||
from openstack_dashboard.test.test_data import utils
|
||||
|
||||
from tuskar_ui import api
|
||||
from tuskar_ui.test import helpers as test
|
||||
from tuskar_ui.test.test_data import flavor_data
|
||||
from tuskar_ui.test.test_data import heat_data
|
||||
from tuskar_ui.test.test_data import node_data
|
||||
from tuskar_ui.test.test_data import tuskar_data
|
||||
|
||||
|
||||
INDEX_URL = urlresolvers.reverse(
|
||||
'horizon:infrastructure:plans:index')
|
||||
CREATE_URL = urlresolvers.reverse(
|
||||
'horizon:infrastructure:plans:create')
|
||||
OVERCLOUD_INDEX_URL = urlresolvers.reverse(
|
||||
'horizon:infrastructure:overcloud:index')
|
||||
|
||||
TEST_DATA = utils.TestDataContainer()
|
||||
flavor_data.data(TEST_DATA)
|
||||
node_data.data(TEST_DATA)
|
||||
heat_data.data(TEST_DATA)
|
||||
tuskar_data.data(TEST_DATA)
|
||||
|
||||
|
||||
class OvercloudTests(test.BaseAdminViewTests):
|
||||
|
||||
def test_index_no_plan_get(self):
|
||||
with contextlib.nested(
|
||||
patch('tuskar_ui.api.tuskar.OvercloudPlan.get_the_plan',
|
||||
return_value=None),
|
||||
):
|
||||
res = self.client.get(INDEX_URL)
|
||||
self.assertRedirectsNoFollow(res, CREATE_URL)
|
||||
|
||||
def test_index_with_plan_get(self):
|
||||
plan = api.tuskar.OvercloudPlan(TEST_DATA.tuskarclient_plans.first())
|
||||
with contextlib.nested(
|
||||
patch('tuskar_ui.api.tuskar.OvercloudPlan.get_the_plan',
|
||||
return_value=plan),
|
||||
):
|
||||
res = self.client.get(INDEX_URL)
|
||||
request = api.tuskar.OvercloudPlan.get_the_plan. \
|
||||
call_args_list[0][0][0]
|
||||
self.assertListEqual(
|
||||
api.tuskar.OvercloudPlan.get_the_plan.call_args_list,
|
||||
[call(request)])
|
||||
self.assertRedirectsNoFollow(res, OVERCLOUD_INDEX_URL)
|
26
tuskar_ui/infrastructure/plans/urls.py
Normal file
26
tuskar_ui/infrastructure/plans/urls.py
Normal file
@ -0,0 +1,26 @@
|
||||
# -*- coding: utf8 -*-
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.conf import urls
|
||||
|
||||
from tuskar_ui.infrastructure.plans import views
|
||||
|
||||
|
||||
urlpatterns = urls.patterns(
|
||||
'',
|
||||
urls.url(r'^$', views.IndexView.as_view(), name='index'),
|
||||
urls.url(r'^create/$', views.CreateView.as_view(), name='create'),
|
||||
urls.url(r'^(?P<plan_id>[^/]+)/scale$', views.Scale.as_view(),
|
||||
name='scale'),
|
||||
)
|
91
tuskar_ui/infrastructure/plans/views.py
Normal file
91
tuskar_ui/infrastructure/plans/views.py
Normal file
@ -0,0 +1,91 @@
|
||||
# -*- coding: utf8 -*-
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.views.generic import base as base_views
|
||||
|
||||
from horizon.utils import memoized
|
||||
import horizon.workflows
|
||||
|
||||
from tuskar_ui import api
|
||||
from tuskar_ui.infrastructure.plans.workflows import create
|
||||
from tuskar_ui.infrastructure.plans.workflows import scale
|
||||
|
||||
|
||||
INDEX_URL = 'horizon:infrastructure:plans:index'
|
||||
CREATE_URL = 'horizon:infrastructure:plans:create'
|
||||
OVERCLOUD_INDEX_URL = 'horizon:infrastructure:overcloud:index'
|
||||
|
||||
|
||||
class OvercloudPlanMixin(object):
|
||||
@memoized.memoized
|
||||
def get_plan(self, redirect=None):
|
||||
if redirect is None:
|
||||
redirect = reverse(INDEX_URL)
|
||||
plan_id = self.kwargs['plan_id']
|
||||
plan = api.tuskar.OvercloudPlan.get(self.request, plan_id,
|
||||
_error_redirect=redirect)
|
||||
return plan
|
||||
|
||||
|
||||
class OvercloudRoleMixin(object):
|
||||
@memoized.memoized
|
||||
def get_role(self, redirect=None):
|
||||
role_id = self.kwargs['role_id']
|
||||
role = api.tuskar.OvercloudRole.get(self.request, role_id,
|
||||
_error_redirect=redirect)
|
||||
return role
|
||||
|
||||
|
||||
class IndexView(base_views.RedirectView):
|
||||
permanent = False
|
||||
|
||||
def get_redirect_url(self):
|
||||
plan = api.tuskar.OvercloudPlan.get_the_plan(self.request)
|
||||
|
||||
if plan is None:
|
||||
redirect = reverse(CREATE_URL)
|
||||
else:
|
||||
redirect = reverse(OVERCLOUD_INDEX_URL)
|
||||
|
||||
return redirect
|
||||
|
||||
|
||||
class CreateView(horizon.workflows.WorkflowView):
|
||||
workflow_class = create.Workflow
|
||||
template_name = 'infrastructure/_fullscreen_workflow_base.html'
|
||||
|
||||
|
||||
class Scale(horizon.workflows.WorkflowView, OvercloudPlanMixin):
|
||||
workflow_class = scale.Workflow
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(Scale, self).get_context_data(**kwargs)
|
||||
context['plan_id'] = self.kwargs['plan_id']
|
||||
return context
|
||||
|
||||
def get_initial(self):
|
||||
plan = self.get_plan()
|
||||
overcloud_roles = dict((overcloud_role.id, overcloud_role)
|
||||
for overcloud_role in
|
||||
api.tuskar.OvercloudRole.list(self.request))
|
||||
|
||||
role_counts = dict((
|
||||
(count['overcloud_role_id'],
|
||||
overcloud_roles[count['overcloud_role_id']].flavor_id),
|
||||
count['num_nodes'],
|
||||
) for count in plan.counts)
|
||||
return {
|
||||
'plan_id': plan.id,
|
||||
'role_counts': role_counts,
|
||||
}
|
@ -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
|
@ -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
|
53
tuskar_ui/infrastructure/plans/workflows/create_overview.py
Normal file
53
tuskar_ui/infrastructure/plans/workflows/create_overview.py
Normal file
@ -0,0 +1,53 @@
|
||||
# -*- coding: utf8 -*-
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from horizon import forms
|
||||
import horizon.workflows
|
||||
|
||||
from tuskar_ui import api
|
||||
|
||||
|
||||
class Action(horizon.workflows.Action):
|
||||
|
||||
role_ids = forms.MultipleChoiceField(
|
||||
label=_("Roles"),
|
||||
required=True,
|
||||
widget=forms.CheckboxSelectMultiple(),
|
||||
help_text=_("Select roles for this plan."))
|
||||
|
||||
class Meta:
|
||||
slug = 'create_overview'
|
||||
name = _("Overview")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Action, self).__init__(*args, **kwargs)
|
||||
|
||||
role_ids_choices = []
|
||||
roles = api.tuskar.OvercloudRole.list(self.request)
|
||||
for r in roles:
|
||||
role_ids_choices.append((r.id, r.name))
|
||||
self.fields['role_ids'].choices = sorted(
|
||||
role_ids_choices)
|
||||
|
||||
|
||||
class Step(horizon.workflows.Step):
|
||||
action_class = Action
|
||||
contributes = ('role_ids',)
|
||||
template_name = 'infrastructure/plans/create_overview.html'
|
||||
help_text = _("Nothing deployed yet. Design your first deployment.")
|
||||
|
||||
def contribute(self, data, context):
|
||||
context = super(Step, self).contribute(data, context)
|
||||
return context
|
@ -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")
|
@ -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():
|
@ -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()
|
||||
|
@ -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')
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user