diff --git a/tuskar_ui/api.py b/tuskar_ui/api.py
index 2404c7b74..f123e07e0 100644
--- a/tuskar_ui/api.py
+++ b/tuskar_ui/api.py
@@ -40,8 +40,10 @@ def tuskarclient(request):
return c
-class Overcloud(base.APIResourceWrapper):
- _attrs = ('id', 'stack_name', 'stack_status')
+# TODO(Tzu-Mainn Chen): change this to APIResourceWrapper once
+# ResourceCategory object exists in tuskar
+class Overcloud(base.APIDictWrapper):
+ _attrs = ('id', 'stack_id', 'name', 'description')
@classmethod
def create(cls, request, overcloud_sizing):
@@ -64,43 +66,42 @@ class Overcloud(base.APIResourceWrapper):
# overcloud = tuskarclient(request).overclouds.create(
# 'overcloud',
# overcloud_sizing)
- overcloud = test_data().heatclient_stacks.first()
+ overcloud = test_data().tuskarclient_overclouds.first()
return cls(overcloud)
@classmethod
- def get(cls, request):
- # Assumptions:
- # * hard-coded stack name ('overcloud')
+ def list(cls, request):
+ # Return:
+ # * a list of Overclouds in Tuskar
+
+ # TODO(Tzu-Mainn Chen): remove test data when possible
+ # ocs = tuskarclient(request).overclouds.list()
+ ocs = test_data().tuskarclient_overclouds.list()
+
+ return [cls(oc) for oc in ocs]
+
+ @classmethod
+ def get(cls, request, overcloud_id):
+ # Required:
+ # * overcloud_id
# Return:
# * the 'overcloud' stack object
# TODO(Tzu-Mainn Chen): remove test data when possible
- # overcloud = heatclient(request).stacks.get('overcloud')
- overcloud = test_data().heatclient_stacks.first()
+ # overcloud = tuskarclient(request).overclouds.get(overcloud_id)
+ overcloud = test_data().tuskarclient_overclouds.first()
return cls(overcloud)
@cached_property
- def resources(self):
- # Assumptions:
- # * hard-coded stack name ('overcloud')
+ def stack(self, request):
# Return:
- # * a list of Resources associated with the Overcloud
+ # * the Heat stack associated with this overcoud
# TODO(Tzu-Mainn Chen): remove test data when possible
- # resources = heatclient(request).resources.list(self.id)
- resources = test_data().heatclient_resources.list
-
- return [Resource(r) for r in resources]
-
- @cached_property
- def nodes(self):
- # Assumptions:
- # * hard-coded stack name ('overcloud')
- # Return:
- # * a list of Nodes indirectly associated with the Overcloud
-
- return [resource.node for resource in self.resources]
+ # stack = heatclient(request).stacks.get(self.stack_id)
+ stack = test_data().heatclient_stacks.first()
+ return stack
@cached_property
def is_deployed(self):
@@ -256,20 +257,22 @@ class Resource(base.APIResourceWrapper):
if self.physical_resource_id == n.instance_uuid),
None)
- @cached_property
- def resource_category(self):
- # Questions:
- # * is a resource_type mapped directly to a ResourceCategory?
- # * can we assume that the resource_type equals the category
- # name?
+
+# TODO(Tzu-Mainn Chen): change this to APIResourceWrapper once
+# ResourceCategory object exists in tuskar
+class ResourceCategory(base.APIDictWrapper):
+ _attrs = ('id', 'name', 'description', 'image_id')
+
+ @classmethod
+ def list(cls, request):
# Return:
- # * the ResourceCategory matching this resource
+ # * a list of Resource Categories in Tuskar.
- return ResourceCategory({'name': self.resource_type})
+ # TODO(Tzu-Mainn Chen): remove test data when possible
+ # categories = tuskarclient(request).resource_categories.list()
-
-class ResourceCategory(base.APIResourceWrapper):
- _attrs = ('name')
+ rcs = test_data().tuskarclient_resource_categories.list()
+ return [cls(rc) for rc in rcs]
@cached_property
def image(self):
@@ -284,15 +287,37 @@ class ResourceCategory(base.APIResourceWrapper):
return "image_name"
- @cached_property
def resources(self, overcloud):
- # Questions:
- # * can we assume that the resource_type equals the
- # category name?
# Required:
# * overcloud
# Return:
# * the resources within the stack that match the
# resource category
- return [r for r in overcloud.resources if r.resource_type == self.name]
+ # TODO(Tzu-Mainn Chen): uncomment when possible
+ #resources = tuskarclient(request).overclouds.get_resources(
+ # overcloud.id, self.id)
+
+ return [r for r in test_data().heatclient_resources.list()
+ if r.logical_resource_id == self.name]
+
+ def instances(self, overcloud):
+ # Required:
+ # * overcloud
+ # Return:
+ # * the instances corresponding to the resources within the
+ # stack that match the resource category
+ #resources = tuskarclient(request).overclouds.get_resources(
+ # overcloud.id, self.id)
+
+ # TODO(Tzu-Mainn Chen): uncomment real api calls and remove test
+ # data when possible
+ instances = []
+ all_instances = test_data().novaclient_servers.list()
+ for r in self.resources(overcloud):
+ #instance = novaclient(request).servers.get(r.physical_resource_id)
+ instance = next((i for i in all_instances
+ if i.id == r.physical_resource_id),
+ None)
+ instances.append(instance)
+ return instances
diff --git a/tuskar_ui/infrastructure/overcloud/tabs.py b/tuskar_ui/infrastructure/overcloud/tabs.py
new file mode 100644
index 000000000..71910c2c9
--- /dev/null
+++ b/tuskar_ui/infrastructure/overcloud/tabs.py
@@ -0,0 +1,77 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 Nebula, Inc.
+#
+# 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 exceptions
+from horizon import tabs
+
+from tuskar_ui import api
+
+
+class OverviewTab(tabs.Tab):
+ name = _("Overview")
+ slug = "overview"
+ template_name = ("infrastructure/overcloud/_detail_overview.html")
+
+ def get_context_data(self, request):
+ context = {}
+ overcloud = self.tab_group.kwargs['overcloud']
+
+ try:
+ categories = api.ResourceCategory.list(request)
+ except Exception:
+ categories = {}
+ exceptions.handle(request,
+ _('Unable to retrieve resource categories.'))
+
+ context['categories'] = []
+ for category in categories:
+ context['categories'].append(
+ self._get_category_data(overcloud, category))
+
+ # also get expected instance counts
+ return context
+
+ def _get_category_data(self, overcloud, category):
+ instances = category.instances(overcloud)
+ category.instance_count = len(instances)
+ if category.instance_count > 0:
+ category.running_instance_count = len(
+ [i for i in instances if i.status == 'ACTIVE'])
+ category.error_instance_count = len(
+ [i for i in instances if i.status == 'ERROR'])
+ category.other_instance_count = category.instance_count - \
+ (category.running_instance_count +
+ category.error_instance_count)
+ category.running_instance_percentage = 100 * \
+ category.running_instance_count / category.instance_count
+ return category
+
+
+class ConfigurationTab(tabs.Tab):
+ name = _("Configuration")
+ slug = "configuration"
+ template_name = ("infrastructure/overcloud/_detail_configuration.html")
+
+ def get_context_data(self, request):
+ return {}
+
+
+class DetailTabs(tabs.TabGroup):
+ slug = "detail"
+ tabs = (OverviewTab, ConfigurationTab)
+ sticky = True
diff --git a/tuskar_ui/infrastructure/overcloud/templates/overcloud/_detail_configuration.html b/tuskar_ui/infrastructure/overcloud/templates/overcloud/_detail_configuration.html
new file mode 100644
index 000000000..d5b3c89a1
--- /dev/null
+++ b/tuskar_ui/infrastructure/overcloud/templates/overcloud/_detail_configuration.html
@@ -0,0 +1,5 @@
+{% load i18n %}
+{% load url from future%}
+
+
diff --git a/tuskar_ui/infrastructure/overcloud/templates/overcloud/_detail_overview.html b/tuskar_ui/infrastructure/overcloud/templates/overcloud/_detail_overview.html
new file mode 100644
index 000000000..1fc57a2f7
--- /dev/null
+++ b/tuskar_ui/infrastructure/overcloud/templates/overcloud/_detail_overview.html
@@ -0,0 +1,28 @@
+{% load i18n %}
+{% load url from future%}
+
+
+
+
{% trans "Resource Categories" %}
+
+ {% for category in categories %}
+ {{ category.instance_count }} {{ category.name }}
+ {% if category.error_instance_count > 0 %}
+ {{ category.error_instance_count }} {% trans "instances are down" %}
+
+ {% endif %}
+ {% if category.other_instance_count > 0 %}
+ {{ category.other_instance_count }} {% trans "instances are building" %}
+
+ {% endif %}
+ {% if category.running_instance_percentage %}
+ {{ category.running_instance_percentage }} {% trans "% of instances are running" %}
+
+ {% endif %}
+
+ {% endfor %}
+
+
+
{% trans "Resource Categories Distribution" %}
+
+
diff --git a/tuskar_ui/infrastructure/overcloud/templates/overcloud/detail.html b/tuskar_ui/infrastructure/overcloud/templates/overcloud/detail.html
new file mode 100644
index 000000000..b10ffeb3a
--- /dev/null
+++ b/tuskar_ui/infrastructure/overcloud/templates/overcloud/detail.html
@@ -0,0 +1,16 @@
+{% extends 'infrastructure/base.html' %}
+{% load i18n %}
+{% load url from future %}
+{% block title %}{% trans 'My Openstack Deployment' %}{% endblock %}
+
+{% block page_header %}
+ {% include 'horizon/common/_domain_page_header.html' with title=_('My Openstack Deployment') %}
+{% endblock page_header %}
+
+{% block main %}
+
+
+ {{ tab_group.render }}
+
+
+{% endblock %}
diff --git a/tuskar_ui/infrastructure/overcloud/tests.py b/tuskar_ui/infrastructure/overcloud/tests.py
index 9c2899aa3..eef9bf51c 100644
--- a/tuskar_ui/infrastructure/overcloud/tests.py
+++ b/tuskar_ui/infrastructure/overcloud/tests.py
@@ -34,28 +34,30 @@ tuskar_data.data(TEST_DATA)
class OvercloudTests(test.BaseAdminViewTests):
def test_index_overcloud_undeployed(self):
- stack = api.Overcloud(TEST_DATA.heatclient_stacks.first)
+ oc = api.Overcloud(TEST_DATA.tuskarclient_overclouds.first())
with patch('tuskar_ui.api.Overcloud', **{
'spec_set': ['get', 'is_deployed'],
'is_deployed': False,
- 'get.return_value': stack,
+ 'get.return_value': oc,
}) as Overcloud:
res = self.client.get(INDEX_URL)
request = Overcloud.get.call_args_list[0][0][0] # This is a hack.
- self.assertListEqual(Overcloud.get.call_args_list, [call(request)])
+ self.assertListEqual(Overcloud.get.call_args_list,
+ [call(request, 1)])
self.assertRedirectsNoFollow(res, CREATE_URL)
def test_index_overcloud_deployed(self):
- stack = api.Overcloud(TEST_DATA.heatclient_stacks.first)
+ oc = api.Overcloud(TEST_DATA.tuskarclient_overclouds.first())
with patch('tuskar_ui.api.Overcloud', **{
'spec_set': ['get', 'is_deployed'],
'is_deployed': True,
- 'get.return_value': stack,
+ 'get.return_value': oc,
}) as Overcloud:
res = self.client.get(INDEX_URL)
request = Overcloud.get.call_args_list[0][0][0] # This is a hack.
- self.assertListEqual(Overcloud.get.call_args_list, [call(request)])
+ self.assertListEqual(Overcloud.get.call_args_list,
+ [call(request, 1)])
self.assertRedirectsNoFollow(res, DETAIL_URL)
diff --git a/tuskar_ui/infrastructure/overcloud/views.py b/tuskar_ui/infrastructure/overcloud/views.py
index 91e176ef7..ada54f02d 100644
--- a/tuskar_ui/infrastructure/overcloud/views.py
+++ b/tuskar_ui/infrastructure/overcloud/views.py
@@ -13,12 +13,15 @@
# under the License.
from django.core.urlresolvers import reverse
+from django.utils.translation import ugettext_lazy as _
from django.views.generic import base as base_views
+from horizon import exceptions
+from horizon import tabs as horizon_tabs
import horizon.workflows
from tuskar_ui import api
-from tuskar_ui.infrastructure.overcloud.workflows import deployed
+from tuskar_ui.infrastructure.overcloud import tabs
from tuskar_ui.infrastructure.overcloud.workflows import undeployed
@@ -26,7 +29,7 @@ class IndexView(base_views.RedirectView):
permanent = False
def get_redirect_url(self):
- overcloud = api.Overcloud.get(self.request)
+ overcloud = api.Overcloud.get(self.request, 1)
if overcloud is not None and overcloud.is_deployed:
redirect = reverse('horizon:infrastructure:overcloud:detail',
args=(overcloud.id,))
@@ -40,6 +43,19 @@ class CreateView(horizon.workflows.WorkflowView):
template_name = 'infrastructure/_fullscreen_workflow_base.html'
-class DetailView(horizon.workflows.WorkflowView):
- workflow_class = deployed.Workflow
- template_name = 'infrastructure/_fullscreen_workflow_base.html'
+class DetailView(horizon_tabs.TabView):
+ tab_group_class = tabs.DetailTabs
+ template_name = 'infrastructure/overcloud/detail.html'
+
+ def get_data(self, request, **kwargs):
+ overcloud_id = kwargs['overcloud_id']
+ try:
+ return api.Overcloud.get(request, overcloud_id)
+ except Exception:
+ msg = _("Unable to retrieve deployment.")
+ redirect = reverse('horizon:infrastructure:overcloud:index')
+ exceptions.handle(request, msg, redirect=redirect)
+
+ def get_tabs(self, request, **kwargs):
+ overcloud = self.get_data(request, **kwargs)
+ return self.tab_group_class(request, overcloud=overcloud, **kwargs)
diff --git a/tuskar_ui/infrastructure/overcloud/workflows/deployed.py b/tuskar_ui/infrastructure/overcloud/workflows/deployed.py
deleted file mode 100644
index ada0c9855..000000000
--- a/tuskar_ui/infrastructure/overcloud/workflows/deployed.py
+++ /dev/null
@@ -1,34 +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.
-
-from django.utils.translation import ugettext_lazy as _
-import horizon.workflows
-
-from tuskar_ui.infrastructure.overcloud.workflows import deployed_configuration
-from tuskar_ui.infrastructure.overcloud.workflows import deployed_overview
-
-
-class Workflow(horizon.workflows.Workflow):
- slug = 'deployed_overcloud'
- name = _("My Openstack Deployment")
- default_steps = (
- deployed_overview.Step,
- deployed_configuration.Step,
- )
- finalize_button_name = _("Scale Deployment")
- # TODO(rdopierqalski) Point this to the scaling forms.
- success_url = 'horizon:infrastructure:overcloud:index'
-
- def handle(self, request, context):
- pass
diff --git a/tuskar_ui/infrastructure/overcloud/workflows/deployed_configuration.py b/tuskar_ui/infrastructure/overcloud/workflows/deployed_configuration.py
deleted file mode 100644
index 2ea9d24fa..000000000
--- a/tuskar_ui/infrastructure/overcloud/workflows/deployed_configuration.py
+++ /dev/null
@@ -1,27 +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.
-
-from django.utils.translation import ugettext_lazy as _
-import horizon.workflows
-
-
-class Action(horizon.workflows.Action):
- class Meta:
- slug = 'deployed_configuration'
- name = _("Configuration")
-
-
-class Step(horizon.workflows.Step):
- action_class = Action
- contributes = ()
diff --git a/tuskar_ui/infrastructure/overcloud/workflows/deployed_overview.py b/tuskar_ui/infrastructure/overcloud/workflows/deployed_overview.py
deleted file mode 100644
index f12355c74..000000000
--- a/tuskar_ui/infrastructure/overcloud/workflows/deployed_overview.py
+++ /dev/null
@@ -1,27 +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.
-
-from django.utils.translation import ugettext_lazy as _
-import horizon.workflows
-
-
-class Action(horizon.workflows.Action):
- class Meta:
- slug = 'deployed_overview'
- name = _("Overview")
-
-
-class Step(horizon.workflows.Step):
- action_class = Action
- contributes = ()
diff --git a/tuskar_ui/test/test_data/tuskar_data.py b/tuskar_ui/test/test_data/tuskar_data.py
index 92ede8ecd..47ae8e679 100644
--- a/tuskar_ui/test/test_data/tuskar_data.py
+++ b/tuskar_ui/test/test_data/tuskar_data.py
@@ -16,6 +16,7 @@ from heatclient.v1 import resources
from heatclient.v1 import stacks
from ironicclient.v1 import node
from ironicclient.v1 import port
+from novaclient.v1_1 import servers
def data(TEST):
@@ -24,7 +25,7 @@ def data(TEST):
TEST.heatclient_stacks = test_data_utils.TestDataContainer()
stack_1 = stacks.Stack(
stacks.StackManager(None),
- {'id': '1',
+ {'id': 'stack-id-1',
'stack_name': 'overcloud',
'stack_status': 'RUNNING'})
TEST.heatclient_stacks.add(stack_1)
@@ -79,7 +80,39 @@ def data(TEST):
'local_disk': '1',
},
'power_state': 'rebooting'})
- TEST.ironicclient_nodes.add(node_1, node_2, node_3)
+ node_4 = node.Node(
+ node.NodeManager(None),
+ {'uuid': 'cc-44',
+ 'instance_uuid': 'cc',
+ 'driver': 'pxe_ipmitool',
+ 'driver_info': {
+ 'ipmi_address': '4.4.4.4',
+ 'ipmi_username': 'admin',
+ 'ipmi_password': 'password',
+ },
+ 'properties': {
+ 'cpu': '8',
+ 'ram': '16',
+ 'local_disk': '10',
+ },
+ 'power_state': 'on'})
+ node_5 = node.Node(
+ node.NodeManager(None),
+ {'uuid': 'dd-55',
+ 'instance_uuid': 'dd',
+ 'driver': 'pxe_ipmitool',
+ 'driver_info': {
+ 'ipmi_address': '5.5.5.5',
+ 'ipmi_username': 'admin',
+ 'ipmi_password': 'password',
+ },
+ 'properties': {
+ 'cpu': '8',
+ 'ram': '16',
+ 'local_disk': '10',
+ },
+ 'power_state': 'on'})
+ TEST.ironicclient_nodes.add(node_1, node_2, node_3, node_4, node_5)
# Ports
TEST.ironicclient_ports = test_data_utils.TestDataContainer()
@@ -109,7 +142,8 @@ def data(TEST):
TEST.heatclient_resources = test_data_utils.TestDataContainer()
resource_1 = resources.Resource(
resources.ResourceManager(None),
- {'stack_id': '1',
+ {'id': '1-resource-id',
+ 'stack_id': 'stack-id-1',
'resource_name': 'Compute',
'logical_resource_id': 'Compute',
'physical_resource_id': 'aa',
@@ -117,12 +151,92 @@ def data(TEST):
'resource_type': 'AWS::EC2::Instance'})
resource_2 = resources.Resource(
resources.ResourceManager(None),
- {'stack_id': '1',
- 'resource_name': 'Control',
- 'logical_resource_id': 'Control',
+ {'id': '2-resource-id',
+ 'stack_id': 'stack-id-1',
+ 'resource_name': 'Controller',
+ 'logical_resource_id': 'Controller',
'physical_resource_id': 'bb',
'resource_status': 'CREATE_COMPLETE',
'resource_type': 'AWS::EC2::Instance'})
- TEST.heatclient_resources.add(resource_1, resource_2)
+ resource_3 = resources.Resource(
+ resources.ResourceManager(None),
+ {'id': '3-resource-id',
+ 'stack_id': 'stack-id-1',
+ 'resource_name': 'Compute',
+ 'logical_resource_id': 'Compute',
+ 'physical_resource_id': 'cc',
+ 'resource_status': 'CREATE_COMPLETE',
+ 'resource_type': 'AWS::EC2::Instance'})
+ resource_4 = resources.Resource(
+ resources.ResourceManager(None),
+ {'id': '4-resource-id',
+ 'stack_id': 'stack-id-4',
+ 'resource_name': 'Compute',
+ 'logical_resource_id': 'Compute',
+ 'physical_resource_id': 'dd',
+ 'resource_status': 'CREATE_COMPLETE',
+ 'resource_type': 'AWS::EC2::Instance'})
+ TEST.heatclient_resources.add(resource_1,
+ resource_2,
+ resource_3,
+ resource_4)
+
+ # Server
+ TEST.novaclient_servers = test_data_utils.TestDataContainer()
+ s_1 = servers.Server(
+ servers.ServerManager(None),
+ {'id': 'aa',
+ 'name': 'Compute',
+ 'image': 'compute-image',
+ 'status': 'ACTIVE'})
+ s_2 = servers.Server(
+ servers.ServerManager(None),
+ {'id': 'bb',
+ 'name': 'Controller',
+ 'image': 'controller-image',
+ 'status': 'ACTIVE'})
+ s_3 = servers.Server(
+ servers.ServerManager(None),
+ {'id': 'cc',
+ 'name': 'Compute',
+ 'image': 'compute-image',
+ 'status': 'BUILD'})
+ s_4 = servers.Server(
+ servers.ServerManager(None),
+ {'id': 'dd',
+ 'name': 'Compute',
+ 'image': 'compute-image',
+ 'status': 'ERROR'})
+ TEST.novaclient_servers.add(s_1, s_2, s_3, s_4)
+
+ # Overcloud
+ TEST.tuskarclient_overclouds = test_data_utils.TestDataContainer()
+ # TODO(Tzu-Mainn Chen): fix these to create Tuskar Overcloud objects
+ # once the api supports it
+ oc_1 = {'id': 1,
+ 'stack_id': 'stack-id-1',
+ 'name': 'overcloud',
+ 'description': 'overcloud'}
+ TEST.tuskarclient_overclouds.add(oc_1)
# ResourceCategory
+ TEST.tuskarclient_resource_categories = test_data_utils.TestDataContainer()
+ # TODO(Tzu-Mainn Chen): fix these to create Tuskar ResourceCategory objects
+ # once the api supports it
+ rc_1 = {'id': 1,
+ 'name': 'Controller',
+ 'description': 'controller resource category',
+ 'image_id': 'image-id-1'}
+ rc_2 = {'id': 2,
+ 'name': 'Compute',
+ 'description': 'compute resource category',
+ 'image_id': 'image-id-2'}
+ rc_3 = {'id': 3,
+ 'name': 'Object Storage',
+ 'description': 'object storage resource category',
+ 'image_id': 'image-id-3'}
+ rc_4 = {'id': 4,
+ 'name': 'Block Storage',
+ 'description': 'block storage resource category',
+ 'image_id': 'image-id-4'}
+ TEST.tuskarclient_resource_categories.add(rc_1, rc_2, rc_3, rc_4)