From ca2f3792e464a96d55a6e5a4e33584e7d0cbfecc Mon Sep 17 00:00:00 2001 From: Ladislav Smola Date: Fri, 24 Jan 2014 18:19:54 +0100 Subject: [PATCH] Adding nova-baremetal, heat and nova API Adding nova-baremetal, heat and nova API instead of mock API. nova-baremetal is supposed to be just temporary solution, replaced by ironic later. Tests were fixed accordingly. Change-Id: Iee6436196d52db626a9cfccc24ae303f5917707c --- tuskar_ui/api.py | 297 +++++++++++++----- tuskar_ui/forms.py | 6 +- tuskar_ui/infrastructure/nodes/forms.py | 31 +- tuskar_ui/infrastructure/nodes/tables.py | 24 +- .../templates/nodes/_nodes_formset_form.html | 5 +- tuskar_ui/infrastructure/nodes/tests.py | 26 +- tuskar_ui/infrastructure/overcloud/tables.py | 2 +- tuskar_ui/infrastructure/overcloud/tests.py | 15 +- tuskar_ui/test/api_tests/tuskar_tests.py | 179 ++++++++--- tuskar_ui/test/test_data/tuskar_data.py | 113 +++++-- 10 files changed, 514 insertions(+), 184 deletions(-) diff --git a/tuskar_ui/api.py b/tuskar_ui/api.py index 1ef926b56..3b074b495 100644 --- a/tuskar_ui/api.py +++ b/tuskar_ui/api.py @@ -19,6 +19,11 @@ import django.conf from horizon.utils import memoized from openstack_dashboard.api import base +from openstack_dashboard.api import glance +from openstack_dashboard.api import heat +from openstack_dashboard.api import nova + +from novaclient.v1_1.contrib import baremetal from openstack_dashboard.test.test_data import utils from tuskar_ui.cached_property import cached_property # noqa from tuskar_ui.test.test_data import tuskar_data @@ -28,6 +33,11 @@ LOG = logging.getLogger(__name__) TUSKAR_ENDPOINT_URL = getattr(django.conf.settings, 'TUSKAR_ENDPOINT_URL') +def baremetalclient(request): + nc = nova.novaclient(request) + return baremetal.BareMetalNodeManager(nc) + + # TODO(Tzu-Mainn Chen): remove test data when possible def test_data(): test_data = utils.TestDataContainer() @@ -35,8 +45,8 @@ def test_data(): return test_data -# FIXME: request isn't used right in the tuskar client right now, but looking -# at other clients, it seems like it will be in the future +# FIXME: request isn't used right in the tuskar client right now, +# but looking at other clients, it seems like it will be in the future def tuskarclient(request): c = tuskar_client.Client(TUSKAR_ENDPOINT_URL) return c @@ -57,11 +67,33 @@ def list_to_dict(object_list, key_attribute='id'): return dict((getattr(o, key_attribute), o) for o in object_list) +# FIXME(lsmola) This should be done in Horizon, they don't have caching +@memoized.memoized +def image_get(request, image_id): + """Returns an Image object with metadata + + Returns an Image object populated with metadata for image + with supplied identifier. + + :param image_id: list of objects to be put into a dict + :type object_list: list + + :return: object + :rtype: glanceclient.v1.images.Image + """ + image = glance.image_get(request, image_id) + return image + + # TODO(Tzu-Mainn Chen): change this to APIResourceWrapper once # ResourceCategory object exists in tuskar class Overcloud(base.APIDictWrapper): _attrs = ('id', 'stack_id', 'name', 'description') + def __init__(self, apiresource, request=None): + super(Overcloud, self).__init__(apiresource) + self._request = request + @classmethod def create(cls, request, overcloud_sizing): """Create an Overcloud in Tuskar @@ -82,7 +114,7 @@ class Overcloud(base.APIDictWrapper): # overcloud_sizing) overcloud = test_data().tuskarclient_overclouds.first() - return cls(overcloud) + return cls(overcloud, request=request) @classmethod def list(cls, request): @@ -98,7 +130,7 @@ class Overcloud(base.APIDictWrapper): # ocs = tuskarclient(request).overclouds.list() ocs = test_data().tuskarclient_overclouds.list() - return [cls(oc) for oc in ocs] + return [cls(oc, request=request) for oc in ocs] @classmethod def get(cls, request, overcloud_id): @@ -118,7 +150,7 @@ class Overcloud(base.APIDictWrapper): # overcloud = tuskarclient(request).overclouds.get(overcloud_id) overcloud = test_data().tuskarclient_overclouds.first() - return cls(overcloud) + return cls(overcloud, request=request) @cached_property def stack(self): @@ -132,7 +164,8 @@ class Overcloud(base.APIDictWrapper): if self.stack_id: # TODO(Tzu-Mainn Chen): remove test data when possible # stack = heatclient(request).stacks.get(self.stack_id) - stack = test_data().heatclient_stacks.first() + # stack = test_data().heatclient_stacks.first() + stack = heat.stack_get(self._request, 'overcloud') return stack return None @@ -160,11 +193,42 @@ class Overcloud(base.APIDictWrapper): False otherwise :rtype: bool """ - # TODO(rdopieralski) Actually implement it - return False + return self.stack.stack_status in ('CREATE_COMPLETE', + 'UPDATE_COMPLETE') @memoized.memoized - def resources(self, resource_category, with_joins=False): + def all_resources(self, with_joins=True): + """Return a list of all Overcloud Resources + + :param with_joins: should we also retrieve objects associated with each + retrieved Resource? + :type with_joins: bool + + :return: list of all Overcloud Resources or an empty list if there + are none + :rtype: list of tuskar_ui.api.Resource + """ + + resources = [r for r in heat.resources_list(self._request, + self.stack.stack_name)] + + if not with_joins: + return [Resource(r, request=self._request) for r in resources] + + nodes_dict = list_to_dict(Node.list(self._request, associated=True), + key_attribute='instance_uuid') + joined_resources = [] + for r in resources: + node = nodes_dict.get(r.physical_resource_id, None) + joined_resources.append(Resource(r, + node=node, + request=self._request)) + # TODO(lsmola) I want just resources with nova instance + # this could be probably filtered a better way, investigate + return [r for r in joined_resources if r.node is not None] + + @memoized.memoized + def resources(self, resource_category, with_joins=True): """Return a list of Overcloud Resources that match a Resource Category :param resource_category: category of resources to be returned @@ -178,34 +242,57 @@ class Overcloud(base.APIDictWrapper): or an empty list if there are none :rtype: list of tuskar_ui.api.Resource """ - # TODO(Tzu-Mainn Chen): uncomment when possible - #resources = tuskarclient(request).overclouds.get_resources( - # self.id, resource_category.id) - resources = [r for r in test_data().heatclient_resources.list() - if r.logical_resource_id.startswith( - resource_category.name)] + # FIXME(lsmola) with_joins is not necessary here, I need at least + # nova instance + all_resources = self.all_resources(with_joins) + filtered_resources = [resource for resource in all_resources if + (resource.image_name == + resource_category.image_name)] - if not with_joins: - return [Resource(r) for r in resources] - - nodes_dict = list_to_dict(Node.list(None, associated=True), - key_attribute='instance_uuid') - joined_resources = [] - for r in resources: - node = nodes_dict.get(r.physical_resource_id, None) - joined_resources.append(Resource(r, - node=node)) - return joined_resources + return filtered_resources class Node(base.APIResourceWrapper): - _attrs = ('uuid', 'instance_uuid', 'driver', 'driver_info', - 'properties', 'power_state') + # FIXME(lsmola) uncomment this and delete equivalent methods + #_attrs = ('uuid', 'instance_uuid', 'driver', 'driver_info', + # 'properties', 'power_state') + _attrs = ('id', 'uuid', 'instance_uuid') - def __init__(self, apiresource, instance=None): + def __init__(self, apiresource, request=None, **kwargs): + """Initialize a node + + :param apiresource: apiresource we want to wrap + :type apiresource: novaclient.v1_1.contrib.baremetal.BareMetalNode + + :param request: request + :type request: django.core.handlers.wsgi.WSGIRequest + + :param instance: instance relation we want to cache + :type instance: openstack_dashboard.api.nova.Server + + :return: Node object + :rtype: Node + """ super(Node, self).__init__(apiresource) - if instance is not None: - self._instance = instance + self._request = request + if 'instance' in kwargs: + self._instance = kwargs['instance'] + + @classmethod + def nova_baremetal_format(cls, ipmi_address, cpu, ram, local_disk, + mac_addresses, ipmi_username=None, + ipmi_password=None): + """Converts Ironic parameters to Nova-baremetal format + """ + return {'service_host': 'undercloud', + 'cpus': cpu, + 'memory_mb': ram, + 'local_gb': local_disk, + 'prov_mac_address': mac_addresses, + 'pm_address': ipmi_address, + 'pm_user': ipmi_username, + 'pm_password': ipmi_password, + 'terminal_port': None} @classmethod def create(cls, request, ipmi_address, cpu, ram, local_disk, @@ -254,7 +341,9 @@ class Node(base.APIResourceWrapper): # node_uuid=node.uuid, # address=mac_address # ) - node = test_data().ironicclient_nodes.first() + node = baremetalclient(request).create(**cls.nova_baremetal_format( + ipmi_address, cpu, ram, local_disk, mac_addresses, + ipmi_username=None, ipmi_password=None)) return cls(node) @@ -273,15 +362,12 @@ class Node(base.APIResourceWrapper): """ # TODO(Tzu-Mainn Chen): remove test data when possible # node = ironicclient(request).nodes.get(uuid) - nodes = test_data().ironicclient_nodes.list() - node = next((n for n in nodes if uuid == n.uuid), - None) + + node = baremetalclient(request).get(uuid) + if node.instance_uuid is not None: - # server = novaclient(request).servers.get(node.instance_uuid) - servers = test_data().novaclient_servers.list() - server = next((s for s in servers if node.instance_uuid == s.id), - None) - return cls(node, instance=server) + server = nova.server_get(request, node.instance_uuid) + return cls(node, instance=server, request=request) return cls(node) @@ -303,17 +389,16 @@ class Node(base.APIResourceWrapper): matching instance UUID """ # TODO(Tzu-Mainn Chen): remove test data when possible - #node = ironicclient(request).nodes.get_by_instance_uuid( + # node = ironicclient(request).nodes.get_by_instance_uuid( # instance_uuid) - #server = novaclient(request).servers.get(instance_id) - nodes = test_data().ironicclient_nodes.list() + + server = nova.server_get(request, instance_uuid) + nodes = baremetalclient(request).list() + node = next((n for n in nodes if instance_uuid == n.instance_uuid), None) - servers = test_data().novaclient_servers.list() - server = next((s for s in servers if instance_uuid == s.id), - None) - return cls(node, instance=server) + return cls(node, instance=server, request=request) @classmethod def list(cls, request, associated=None): @@ -333,7 +418,9 @@ class Node(base.APIResourceWrapper): # TODO(Tzu-Mainn Chen): remove test data when possible # nodes = ironicclient(request).nodes.list( # associated=associated) - nodes = test_data().ironicclient_nodes.list() + + # nodes = test_data().ironicclient_nodes.list() + nodes = baremetalclient(request).list() if associated is not None: if associated: @@ -342,14 +429,16 @@ class Node(base.APIResourceWrapper): else: nodes = [node for node in nodes if node.instance_uuid is None] - return [cls(node) for node in nodes] + return [cls(node, request=request) for node in nodes] - # servers = novaclient(request).servers.list(detailed=True) - servers_dict = list_to_dict(test_data().novaclient_servers.list()) + servers, has_more_data = nova.server_list(request) + + servers_dict = list_to_dict(servers) nodes_with_instance = [] for n in nodes: server = servers_dict.get(n.instance_uuid, None) - nodes_with_instance.append(cls(n, instance=server)) + nodes_with_instance.append(cls(n, instance=server, + request=request)) return nodes_with_instance @@ -366,6 +455,7 @@ class Node(base.APIResourceWrapper): """ # TODO(Tzu-Mainn Chen): uncomment when possible # ironicclient(request).nodes.delete(uuid) + baremetalclient(request).delete(uuid) return @cached_property @@ -379,18 +469,16 @@ class Node(base.APIResourceWrapper): """ if hasattr(self, '_instance'): return self._instance - if self.instance_uuid: - # TODO(Tzu-Mainn Chen): remove test data when possible - # server = novaclient(request).servers.get(self.instance_uuid) - servers = test_data().novaclient_servers.list() - server = next((s for s in servers if self.instance_uuid == s.id), - None) + if self.instance_uuid: + server = nova.server_get(self._request, self.instance_uuid) return server + return None @cached_property def addresses(self): + # FIXME(lsmola) remove when Ironic is in """Return a list of port addresses associated with this Node :return: list of port addresses associated with this Node, or @@ -400,17 +488,71 @@ class Node(base.APIResourceWrapper): """ # TODO(Tzu-Mainn Chen): uncomment when possible # ports = self.list_ports() - ports = test_data().ironicclient_ports.list()[:2] + # ports = test_data().ironicclient_ports.list()[:2] - return [port.address for port in ports] + # return [port.address for port in ports] + return [interface["address"] for interface in + self._apiresource.interfaces] + + @cached_property + def power_state(self): + # FIXME(lsmola) remove when Ironic is in + """Return a power state of this Node + + :return: power state of this node + :rtype: str + """ + return self._apiresource.task_state + + @cached_property + def properties(self): + # FIXME(lsmola) remove when Ironic is in + """Return properties of this Node + + :return: return memory, cpus and local_disk properties + of this Node + :rtype: dict of str + """ + return { + 'ram': self._apiresource.memory_mb / 1024.0, + 'cpu': self._apiresource.cpus, + 'local_disk': self._apiresource.local_gb / 1000.0 + } + + @cached_property + def driver_info(self): + # FIXME(lsmola) remove when Ironic is in + """Return driver_info this Node + + :return: return pm_address property of this Node + :rtype: dict of str + """ + return { + 'ipmi_address': self._apiresource.pm_address + } class Resource(base.APIResourceWrapper): _attrs = ('resource_name', 'resource_type', 'resource_status', 'physical_resource_id') - def __init__(self, apiresource, **kwargs): + def __init__(self, apiresource, request=None, **kwargs): + """Initialize a resource + + :param apiresource: apiresource we want to wrap + :type apiresource: heatclient.v1.resources.Resource + + :param request: request + :type request: django.core.handlers.wsgi.WSGIRequest + + :param node: node relation we want to cache + :type node: tuskar_ui.api.Node + + :return: Resource object + :rtype: Resource + """ super(Resource, self).__init__(apiresource) + self._request = request if 'node' in kwargs: self._node = kwargs['node'] @@ -431,17 +573,9 @@ class Resource(base.APIResourceWrapper): stack matches the resource name :rtype: tuskar_ui.api.Resource """ - # TODO(Tzu-Mainn Chen): uncomment when possible - # resource = heatclient(request).resources.get( - # overcloud.id, - # resource_name) - resources = test_data().heatclient_resources.list() - resource = next((r for r in resources - if overcloud['id'] == r.stack_id - and resource_name == r.resource_name), - None) - - return cls(resource) + resource = heat.resource_get(overcloud.stack.id, + resource_name) + return cls(resource, request=request) @cached_property def node(self): @@ -457,14 +591,29 @@ class Resource(base.APIResourceWrapper): if hasattr(self, '_node'): return self._node if self.physical_resource_id: - return Node.get_by_instance_uuid(None, self.physical_resource_id) + return Node.get_by_instance_uuid(self._request, + self.physical_resource_id) + return None + + @cached_property + def image_name(self): + """Return image name of resource + + Returns image name of instance associated with resource + + :return: Image name of resources + :rtype: string + """ + instance = getattr(getattr(self, 'node', None), 'instance', None) + if instance is not None: + return image_get(self._request, instance.image['id']).name return None # TODO(Tzu-Mainn Chen): change this to APIResourceWrapper once # ResourceCategory object exists in tuskar class ResourceCategory(base.APIDictWrapper): - _attrs = ('id', 'name', 'description', 'image_id') + _attrs = ('id', 'name', 'description', 'image_id', 'image_name') @classmethod def list(cls, request): diff --git a/tuskar_ui/forms.py b/tuskar_ui/forms.py index ebfd9f67a..707c40c9b 100644 --- a/tuskar_ui/forms.py +++ b/tuskar_ui/forms.py @@ -41,9 +41,13 @@ class NumberPickerInput(NumberInput): class MACField(forms.fields.Field): def clean(self, value): + class mac_dialect(netaddr.mac_eui48): + """Same validation as Nova uses.""" + word_fmt = '%.02x' + word_sep = ':' try: return str(netaddr.EUI( - value.strip(), version=48, dialect=netaddr.mac_unix)).upper() + value.strip(), version=48, dialect=mac_dialect)).upper() except (netaddr.AddrFormatError, TypeError): raise forms.ValidationError(_(u'Enter a valid MAC address.')) diff --git a/tuskar_ui/infrastructure/nodes/forms.py b/tuskar_ui/infrastructure/nodes/forms.py index fabc33a7b..ad4ed35d6 100644 --- a/tuskar_ui/infrastructure/nodes/forms.py +++ b/tuskar_ui/infrastructure/nodes/forms.py @@ -26,9 +26,9 @@ class NodeForm(django.forms.Form): required=False, widget=django.forms.HiddenInput(), ) - - ip_address = django.forms.IPAddressField( - label=_("IP Address"), + ipmi_address = django.forms.IPAddressField( + label=_("IPMI Address"), + required=False, widget=django.forms.TextInput(attrs={'class': 'input input-medium'}), ) ipmi_user = django.forms.CharField( @@ -42,26 +42,12 @@ class NodeForm(django.forms.Form): widget=django.forms.PasswordInput( render_value=False, attrs={'class': 'input input-medium'}), ) - mac_address = tuskar_ui.forms.MACField( label=_("NIC MAC Address"), - widget=django.forms.Textarea(attrs={ - 'class': 'input input-medium', - 'rows': 2, + widget=django.forms.TextInput(attrs={ + 'class': 'input input-medium' }), ) - - ipmi_user = django.forms.CharField( - label=_("IPMI User"), - required=False, - widget=django.forms.TextInput(attrs={'class': 'input input-medium'}), - ) - ipmi_password = django.forms.CharField( - label=_("IPMI Password"), - required=False, - widget=django.forms.PasswordInput( - render_value=False, attrs={'class': 'input input-medium'}), - ) cpus = django.forms.IntegerField( label=_("CPUs"), required=True, @@ -89,7 +75,8 @@ class NodeForm(django.forms.Form): def get_name(self): try: - name = self.fields['ip_address'].value() + # FIXME(lsmola) show somethign meaningful here + name = self.fields['ipmi_address'].value() except AttributeError: # when the field is not bound name = _("Undefined node") @@ -103,11 +90,11 @@ class BaseNodeFormset(django.forms.formsets.BaseFormSet): try: api.Node.create( request, - form.cleaned_data['ip_address'], + form.cleaned_data['ipmi_address'], form.cleaned_data.get('cpus'), form.cleaned_data.get('memory'), form.cleaned_data.get('local_disk'), - [form.cleaned_data['mac_address']], + form.cleaned_data['mac_address'], form.cleaned_data.get('ipmi_username'), form.cleaned_data.get('ipmi_password'), ) diff --git a/tuskar_ui/infrastructure/nodes/tables.py b/tuskar_ui/infrastructure/nodes/tables.py index 2498db1c6..80f67ba5a 100644 --- a/tuskar_ui/infrastructure/nodes/tables.py +++ b/tuskar_ui/infrastructure/nodes/tables.py @@ -17,9 +17,25 @@ from django.utils.translation import ugettext_lazy as _ from horizon import tables +from tuskar_ui import api + + +class DeleteNode(tables.BatchAction): + name = "delete" + action_present = _("Delete") + action_past = _("Deleting") + data_type_singular = _("Node") + data_type_plural = _("Nodes") + classes = ('btn-danger',) + + def allowed(self, request, obj=None): + return getattr(obj, 'instance', None) is None + + def action(self, request, obj_id): + api.Node.delete(request, obj_id) + class NodesTable(tables.DataTable): - uuid = tables.Column("uuid", link="horizon:infrastructure:nodes:detail", verbose_name=_("UUID")) @@ -51,7 +67,7 @@ class NodesTable(tables.DataTable): row_actions = () def get_object_id(self, datum): - return datum.uuid + return datum.id def get_object_display(self, datum): return datum.uuid @@ -62,8 +78,8 @@ class FreeNodesTable(NodesTable): class Meta: name = "free_nodes" verbose_name = _("Free Nodes") - table_actions = () - row_actions = () + table_actions = (DeleteNode,) + row_actions = (DeleteNode,) class DeployedNodesTable(NodesTable): diff --git a/tuskar_ui/infrastructure/nodes/templates/nodes/_nodes_formset_form.html b/tuskar_ui/infrastructure/nodes/templates/nodes/_nodes_formset_form.html index ed777d230..090fcc736 100644 --- a/tuskar_ui/infrastructure/nodes/templates/nodes/_nodes_formset_form.html +++ b/tuskar_ui/infrastructure/nodes/templates/nodes/_nodes_formset_form.html @@ -7,7 +7,7 @@

Power Management

- {% include 'infrastructure/nodes/_nodes_formset_field.html' with field=form.ip_address required=True %} + {% include 'infrastructure/nodes/_nodes_formset_field.html' with field=form.ipmi_address %} {% include 'infrastructure/nodes/_nodes_formset_field.html' with field=form.ipmi_user %} {% include 'infrastructure/nodes/_nodes_formset_field.html' with field=form.ipmi_password %}
@@ -38,7 +38,8 @@ var $nav_link = $('a[href="#' + $form.attr('id') + '"]'); var undefined_name = '{{ form.get_name|escapejs }}'; - $form.find('input[name$="-ip_address"]').change(function () { + // FIXME(lsmola) what is this good for exactly? + $form.find('input[name$="-ipmi_address"]').change(function () { $nav_link.html($(this).val() || undefined_name); }); }); diff --git a/tuskar_ui/infrastructure/nodes/tests.py b/tuskar_ui/infrastructure/nodes/tests.py index 3cc84f929..7f0be0676 100644 --- a/tuskar_ui/infrastructure/nodes/tests.py +++ b/tuskar_ui/infrastructure/nodes/tests.py @@ -31,7 +31,15 @@ tuskar_data.data(TEST_DATA) class NodesTests(test.BaseAdminViewTests): def test_index_get(self): - res = self.client.get(INDEX_URL) + + with patch('tuskar_ui.api.Node', **{ + 'spec_set': ['list'], # Only allow these attributes + 'list.return_value': [], + }) as mock: + res = self.client.get(INDEX_URL) + # FIXME(lsmola) optimize, this should call 1 time, what the hell + self.assertEqual(mock.list.call_count, 8) + self.assertTemplateUsed( res, 'infrastructure/nodes/index.html') self.assertTemplateUsed(res, 'infrastructure/nodes/_overview.html') @@ -102,13 +110,13 @@ class NodesTests(test.BaseAdminViewTests): 'register_nodes-INITIAL_FORMS': 1, 'register_nodes-MAX_NUM_FORMS': 1000, - 'register_nodes-0-ip_address': '127.0.0.1', + 'register_nodes-0-ipmi_address': '127.0.0.1', 'register_nodes-0-mac_address': 'de:ad:be:ef:ca:fe', 'register_nodes-0-cpus': '1', 'register_nodes-0-memory': '2', 'register_nodes-0-local_disk': '3', - 'register_nodes-1-ip_address': '127.0.0.2', + 'register_nodes-1-ipmi_address': '127.0.0.2', 'register_nodes-1-mac_address': 'de:ad:be:ef:ca:ff', 'register_nodes-1-cpus': '4', 'register_nodes-1-memory': '5', @@ -122,9 +130,9 @@ class NodesTests(test.BaseAdminViewTests): request = Node.create.call_args_list[0][0][0] # This is a hack. self.assertListEqual(Node.create.call_args_list, [ call(request, '127.0.0.1', 1, 2, 3, - ['DE:AD:BE:EF:CA:FE'], None, u''), + 'DE:AD:BE:EF:CA:FE', None, u''), call(request, '127.0.0.2', 4, 5, 6, - ['DE:AD:BE:EF:CA:FF'], None, u''), + 'DE:AD:BE:EF:CA:FF', None, u''), ]) self.assertRedirectsNoFollow(res, INDEX_URL) @@ -134,13 +142,13 @@ class NodesTests(test.BaseAdminViewTests): 'register_nodes-INITIAL_FORMS': 1, 'register_nodes-MAX_NUM_FORMS': 1000, - 'register_nodes-0-ip_address': '127.0.0.1', + 'register_nodes-0-ipmi_address': '127.0.0.1', 'register_nodes-0-mac_address': 'de:ad:be:ef:ca:fe', 'register_nodes-0-cpus': '1', 'register_nodes-0-memory': '2', 'register_nodes-0-local_disk': '3', - 'register_nodes-1-ip_address': '127.0.0.2', + 'register_nodes-1-ipmi_address': '127.0.0.2', 'register_nodes-1-mac_address': 'de:ad:be:ef:ca:ff', 'register_nodes-1-cpus': '4', 'register_nodes-1-memory': '5', @@ -154,9 +162,9 @@ class NodesTests(test.BaseAdminViewTests): request = Node.create.call_args_list[0][0][0] # This is a hack. self.assertListEqual(Node.create.call_args_list, [ call(request, '127.0.0.1', 1, 2, 3, - ['DE:AD:BE:EF:CA:FE'], None, u''), + 'DE:AD:BE:EF:CA:FE', None, u''), call(request, '127.0.0.2', 4, 5, 6, - ['DE:AD:BE:EF:CA:FF'], None, u''), + 'DE:AD:BE:EF:CA:FF', None, u''), ]) self.assertTemplateUsed( res, 'infrastructure/nodes/register.html') diff --git a/tuskar_ui/infrastructure/overcloud/tables.py b/tuskar_ui/infrastructure/overcloud/tables.py index 5f2f6ce10..b0a8cbe58 100644 --- a/tuskar_ui/infrastructure/overcloud/tables.py +++ b/tuskar_ui/infrastructure/overcloud/tables.py @@ -41,7 +41,7 @@ class ResourceCategoryNodeTable(tables.DataTable): )) def get_object_id(self, datum): - return datum.uuid + return datum.id class Meta: name = "resource_category__nodetable" diff --git a/tuskar_ui/infrastructure/overcloud/tests.py b/tuskar_ui/infrastructure/overcloud/tests.py index d3cf2737e..c4044d07f 100644 --- a/tuskar_ui/infrastructure/overcloud/tests.py +++ b/tuskar_ui/infrastructure/overcloud/tests.py @@ -26,7 +26,7 @@ INDEX_URL = urlresolvers.reverse( CREATE_URL = urlresolvers.reverse( 'horizon:infrastructure:overcloud:create') DETAIL_URL = urlresolvers.reverse( - 'horizon:infrastructure:overcloud:create') + 'horizon:infrastructure:overcloud:detail', args=(1,)) TEST_DATA = utils.TestDataContainer() tuskar_data.data(TEST_DATA) @@ -34,12 +34,13 @@ tuskar_data.data(TEST_DATA) class OvercloudTests(test.BaseAdminViewTests): def test_index_overcloud_undeployed_get(self): - oc = api.Overcloud(TEST_DATA.tuskarclient_overclouds.first()) + oc = None with patch('tuskar_ui.api.Overcloud', **{ 'spec_set': ['get', 'is_deployed'], 'is_deployed': False, - 'get.return_value': oc, + 'get.side_effect': lambda request, overcloud_id: oc, }) as Overcloud: + oc = api.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, @@ -82,12 +83,14 @@ class OvercloudTests(test.BaseAdminViewTests): self.assertRedirectsNoFollow(res, INDEX_URL) def test_index_overcloud_deployed(self): - oc = api.Overcloud(TEST_DATA.tuskarclient_overclouds.first()) + oc = None with patch('tuskar_ui.api.Overcloud', **{ - 'spec_set': ['get', 'is_deployed'], + 'spec_set': ['get', 'is_deployed', 'id'], 'is_deployed': True, - 'get.return_value': oc, + 'id': 1, + 'get.side_effect': lambda request, overcloud_id: oc, }) as Overcloud: + oc = 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, diff --git a/tuskar_ui/test/api_tests/tuskar_tests.py b/tuskar_ui/test/api_tests/tuskar_tests.py index 649474c4d..c16fd0602 100644 --- a/tuskar_ui/test/api_tests/tuskar_tests.py +++ b/tuskar_ui/test/api_tests/tuskar_tests.py @@ -14,6 +14,8 @@ from __future__ import absolute_import +from mock import patch # noqa + from glanceclient.v1 import images from heatclient.v1 import events from heatclient.v1 import stacks @@ -27,16 +29,11 @@ from tuskar_ui.test import helpers as test class TuskarAPITests(test.APITestCase): - def test_overcloud_create(self): - #overcloud = self.tuskarclient_overclouds.first() - ret_val = api.Overcloud.create(self.request, []) self.assertIsInstance(ret_val, api.Overcloud) def test_overcloud_list(self): - #overclouds = self.tuskarclient_overclouds.list() - ret_val = api.Overcloud.list(self.request) for oc in ret_val: self.assertIsInstance(oc, api.Overcloud) @@ -49,10 +46,12 @@ class TuskarAPITests(test.APITestCase): self.assertIsInstance(ret_val, api.Overcloud) def test_overcloud_stack(self): - overcloud = self.tuskarclient_overclouds.first() - - ret_val = api.Overcloud(overcloud).stack - self.assertIsInstance(ret_val, stacks.Stack) + stack = self.heatclient_stacks.first() + oc = api.Overcloud(self.tuskarclient_overclouds.first(), request=None) + with patch('openstack_dashboard.api.heat.stack_get', + return_value=stack): + ret_val = oc.stack + self.assertIsInstance(ret_val, stacks.Stack) def test_overcloud_stack_events(self): overcloud = self.tuskarclient_overclouds.first() @@ -70,65 +69,150 @@ class TuskarAPITests(test.APITestCase): self.assertListEqual([], ret_val) def test_overcloud_is_deployed(self): - overcloud = self.tuskarclient_overclouds.first() + stack = self.heatclient_stacks.first() + oc = api.Overcloud(self.tuskarclient_overclouds.first(), request=None) + with patch('openstack_dashboard.api.heat.stack_get', + return_value=stack): + ret_val = oc.is_deployed + self.assertFalse(ret_val) - ret_val = api.Overcloud(overcloud).is_deployed - self.assertFalse(ret_val) + def test_overcloud_all_resources(self): + oc = api.Overcloud(self.tuskarclient_overclouds.first(), request=None) - def test_overcloud_resources(self): - overcloud = self.tuskarclient_overclouds.first() - category = self.tuskarclient_resource_categories.first() + # FIXME(lsmola) the stack call should not be tested in this unit test + # anybody has idea how to do it? + stack = self.heatclient_stacks.first() + resources = self.heatclient_resources.list() + nodes = self.ironicclient_nodes.list() + instances = [] + + with patch('openstack_dashboard.api.heat.resources_list', + return_value=resources): + with patch('openstack_dashboard.api.nova.server_list', + return_value=(instances, None)): + with patch('novaclient.v1_1.contrib.baremetal.' + 'BareMetalNodeManager.list', + return_value=nodes): + with patch('openstack_dashboard.api.heat.stack_get', + return_value=stack): + ret_val = oc.all_resources() - ret_val = api.Overcloud(overcloud).resources( - api.ResourceCategory(category)) for i in ret_val: self.assertIsInstance(i, api.Resource) - self.assertEqual(1, len(ret_val)) + self.assertEqual(4, len(ret_val)) + + def test_overcloud_resources(self): + oc = api.Overcloud(self.tuskarclient_overclouds.first(), request=None) + category = api.ResourceCategory(self.tuskarclient_resource_categories. + first()) + + # FIXME(lsmola) only all_resources and image_name should be tested + # here, anybody has idea how to do that? + image = self.glanceclient_images.first() + stack = self.heatclient_stacks.first() + resources = self.heatclient_resources.list() + instances = self.novaclient_servers.list() + nodes = self.ironicclient_nodes.list() + with patch('openstack_dashboard.api.heat.resources_list', + return_value=resources) as resource_list: + with patch('openstack_dashboard.api.nova.server_list', + return_value=(instances, None)) as server_list: + with patch('openstack_dashboard.api.glance.image_get', + return_value=image) as image_get: + with patch('novaclient.v1_1.contrib.baremetal.' + 'BareMetalNodeManager.list', + return_value=nodes) as node_list: + with patch('openstack_dashboard.api.heat.stack_get', + return_value=stack) as stack_get: + ret_val = oc.resources(category) + self.assertEqual(resource_list.call_count, 1) + self.assertEqual(server_list.call_count, 1) + # TODO(lsmola) isn't it better to call image_list? + # this will call image_get for every unique image + # used that should not be much (4 images should be + # there for start) + # FIXME(lsmola) testing caching here is bad, + # because it gets cached for the whole tests run + self.assertEqual(image_get.call_count, 2) + # FIXME(lsmola) optimize this, it's enough to call + # node_list once + self.assertEqual(node_list.call_count, 1) + self.assertEqual(stack_get.call_count, 1) + + for i in ret_val: + self.assertIsInstance(i, api.Resource) + self.assertEqual(4, len(ret_val)) def test_node_create(self): - node = self.ironicclient_nodes.first() + node = api.Node(self.ironicclient_nodes.first()) - ret_val = api.Node.create( - self.request, - node.driver_info['ipmi_address'], - node.properties['cpu'], - node.properties['ram'], - node.properties['local_disk'], - ['aa:aa:aa:aa:aa:aa'], - ipmi_username='admin', - ipmi_password='password') - ret_val.instance_uuid = None + # FIXME(lsmola) this should be mocking client call no Node + with patch('novaclient.v1_1.contrib.baremetal.' + 'BareMetalNodeManager.create', + return_value=node): + ret_val = api.Node.create( + self.request, + node.driver_info['ipmi_address'], + node.properties['cpu'], + node.properties['ram'], + node.properties['local_disk'], + ['aa:aa:aa:aa:aa:aa'], + ipmi_username='admin', + ipmi_password='password') self.assertIsInstance(ret_val, api.Node) - self.assertIsNone(ret_val.instance) def test_node_get(self): node = self.ironicclient_nodes.first() + instance = self.novaclient_servers.first() + + with patch('openstack_dashboard.api.nova.server_get', + return_value=instance): + with patch('novaclient.v1_1.contrib.baremetal.' + 'BareMetalNodeManager.get', + return_value=node): + ret_val = api.Node.get(self.request, node.uuid) - ret_val = api.Node.get(self.request, node.uuid) self.assertIsInstance(ret_val, api.Node) self.assertIsInstance(ret_val.instance, servers.Server) def test_node_get_by_instance_uuid(self): + instance = self.novaclient_servers.first() node = self.ironicclient_nodes.first() + nodes = self.ironicclient_nodes.list() + + with patch('openstack_dashboard.api.nova.server_get', + return_value=instance): + with patch('novaclient.v1_1.contrib.baremetal.' + 'BareMetalNodeManager.list', + return_value=nodes): + ret_val = api.Node.get_by_instance_uuid(self.request, + node.instance_uuid) - ret_val = api.Node.get_by_instance_uuid(self.request, - node.instance_uuid) self.assertIsInstance(ret_val, api.Node) self.assertIsInstance(ret_val.instance, servers.Server) def test_node_list(self): - #nodes = self.tuskarclient_overclouds.list() + instances = self.novaclient_servers.list() + nodes = self.ironicclient_nodes.list() + + with patch('openstack_dashboard.api.nova.server_list', + return_value=(instances, None)): + with patch('novaclient.v1_1.contrib.baremetal.' + 'BareMetalNodeManager.list', + return_value=nodes): + ret_val = api.Node.list(self.request) - ret_val = api.Node.list(self.request) for node in ret_val: self.assertIsInstance(node, api.Node) self.assertEqual(5, len(ret_val)) def test_node_delete(self): node = self.ironicclient_nodes.first() - - api.Node.delete(self.request, node.uuid) + with patch('novaclient.v1_1.contrib.baremetal.' + 'BareMetalNodeManager.delete', + return_value=None): + api.Node.delete(self.request, node.uuid) def test_node_addresses(self): node = self.ironicclient_nodes.first() @@ -137,17 +221,30 @@ class TuskarAPITests(test.APITestCase): self.assertEqual(2, len(ret_val)) def test_resource_get(self): - overcloud = self.tuskarclient_overclouds.first() + stack = self.heatclient_stacks.first() + overcloud = api.Overcloud(self.tuskarclient_overclouds.first(), + request=None) resource = self.heatclient_resources.first() - ret_val = api.Resource.get(self.request, overcloud, - resource.resource_name) + 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.Resource.get(None, overcloud, + resource.resource_name) self.assertIsInstance(ret_val, api.Resource) def test_resource_node(self): resource = self.heatclient_resources.first() + nodes = self.ironicclient_nodes.list() + instance = self.novaclient_servers.first() - ret_val = api.Resource(resource).node + with patch('openstack_dashboard.api.nova.server_get', + return_value=instance): + with patch('novaclient.v1_1.contrib.baremetal.' + 'BareMetalNodeManager.list', + return_value=nodes): + ret_val = api.Resource(resource, request=None).node self.assertIsInstance(ret_val, api.Node) self.assertIsInstance(ret_val.instance, servers.Server) diff --git a/tuskar_ui/test/test_data/tuskar_data.py b/tuskar_ui/test/test_data/tuskar_data.py index e49a25501..7163e894e 100644 --- a/tuskar_ui/test/test_data/tuskar_data.py +++ b/tuskar_ui/test/test_data/tuskar_data.py @@ -105,7 +105,8 @@ def data(TEST): TEST.ironicclient_nodes = test_data_utils.TestDataContainer() node_1 = node.Node( node.NodeManager(None), - {'uuid': 'aa-11', + {'id': '1', + 'uuid': 'aa-11', 'instance_uuid': 'aa', 'driver': 'pxe_ipmitool', 'driver_info': { @@ -118,10 +119,23 @@ def data(TEST): 'ram': '16', 'local_disk': '10', }, - 'power_state': 'on'}) + 'power_state': 'on', + + # FIXME(lsmola) nova-baremetal test attrs, delete when Ironic is in + "pm_address": None, + "pm_user": None, + "task_state": "active", + "interfaces": [{"address": "52:54:00:90:38:01"}, + {"address": "52:54:00:90:38:01"}], + "cpus": 1, + "memory_mb": 4096, + "service_host": "undercloud", + "local_gb": 20, + }) node_2 = node.Node( node.NodeManager(None), - {'uuid': 'bb-22', + {'id': '2', + 'uuid': 'bb-22', 'instance_uuid': 'bb', 'driver': 'pxe_ipmitool', 'driver_info': { @@ -134,10 +148,22 @@ def data(TEST): 'ram': '32', 'local_disk': '100', }, - 'power_state': 'on'}) + 'power_state': 'on', + + # FIXME(lsmola) nova-baremetal test attrs, delete when Ironic is in + "pm_address": None, + "pm_user": None, + "task_state": "active", + "interfaces": [{"address": "52:54:00:90:38:01"}], + "cpus": 1, + "memory_mb": 4096, + "service_host": "undercloud", + "local_gb": 20, + }) node_3 = node.Node( node.NodeManager(None), - {'uuid': 'cc-33', + {'id': '3', + 'uuid': 'cc-33', 'instance_uuid': None, 'driver': 'pxe_ipmitool', 'driver_info': { @@ -150,10 +176,22 @@ def data(TEST): 'ram': '64', 'local_disk': '1', }, - 'power_state': 'rebooting'}) + 'power_state': 'rebooting', + + # FIXME(lsmola) nova-baremetal test attrs, delete when Ironic is in + "pm_address": None, + "pm_user": None, + "task_state": "active", + "interfaces": [{"address": "52:54:00:90:38:01"}], + "cpus": 1, + "memory_mb": 4096, + "service_host": "undercloud", + "local_gb": 20, + }) node_4 = node.Node( node.NodeManager(None), - {'uuid': 'cc-44', + {'id': '4', + 'uuid': 'cc-44', 'instance_uuid': 'cc', 'driver': 'pxe_ipmitool', 'driver_info': { @@ -166,10 +204,22 @@ def data(TEST): 'ram': '16', 'local_disk': '10', }, - 'power_state': 'on'}) + 'power_state': 'on', + + # FIXME(lsmola) nova-baremetal test attrs, delete when Ironic is in + "pm_address": None, + "pm_user": None, + "task_state": "active", + "interfaces": [{"address": "52:54:00:90:38:01"}], + "cpus": 1, + "memory_mb": 4096, + "service_host": "undercloud", + "local_gb": 20, + }) node_5 = node.Node( node.NodeManager(None), - {'uuid': 'dd-55', + {'id': '5', + 'uuid': 'dd-55', 'instance_uuid': 'dd', 'driver': 'pxe_ipmitool', 'driver_info': { @@ -182,7 +232,18 @@ def data(TEST): 'ram': '16', 'local_disk': '10', }, - 'power_state': 'on'}) + 'power_state': 'on', + + # FIXME(lsmola) nova-baremetal test attrs, delete when Ironic is in + "pm_address": None, + "pm_user": None, + "task_state": "active", + "interfaces": [{"address": "52:54:00:90:38:01"}], + "cpus": 1, + "memory_mb": 4096, + "service_host": "undercloud", + "local_gb": 20, + }) TEST.ironicclient_nodes.add(node_1, node_2, node_3, node_4, node_5) # Ports @@ -258,25 +319,25 @@ def data(TEST): servers.ServerManager(None), {'id': 'aa', 'name': 'Compute', - 'image': 'compute-image', + 'image': {'id': 1}, 'status': 'ACTIVE'}) s_2 = servers.Server( servers.ServerManager(None), {'id': 'bb', 'name': 'Controller', - 'image': 'controller-image', + 'image': {'id': 2}, 'status': 'ACTIVE'}) s_3 = servers.Server( servers.ServerManager(None), {'id': 'cc', 'name': 'Compute', - 'image': 'compute-image', + 'image': {'id': 1}, 'status': 'BUILD'}) s_4 = servers.Server( servers.ServerManager(None), {'id': 'dd', 'name': 'Compute', - 'image': 'compute-image', + 'image': {'id': 1}, 'status': 'ERROR'}) TEST.novaclient_servers.add(s_1, s_2, s_3, s_4) @@ -297,37 +358,41 @@ def data(TEST): rc_1 = {'id': 1, 'name': 'Controller', 'description': 'controller resource category', - 'image_id': 'image-id-1'} + 'image_id': '2', + 'image_name': 'overcloud-control'} rc_2 = {'id': 2, 'name': 'Compute', 'description': 'compute resource category', - 'image_id': 'image-id-2'} + 'image_id': '1', + 'image_name': 'overcloud-compute'} rc_3 = {'id': 3, 'name': 'Object Storage', 'description': 'object storage resource category', - 'image_id': 'image-id-3'} + 'image_id': '3', + 'image_name': 'overcloud-object-storage'} rc_4 = {'id': 4, 'name': 'Block Storage', 'description': 'block storage resource category', - 'image_id': 'image-id-4'} + 'image_id': '4', + 'image_name': 'overcloud-block-storage'} TEST.tuskarclient_resource_categories.add(rc_1, rc_2, rc_3, rc_4) # Image TEST.glanceclient_images = test_data_utils.TestDataContainer() image_1 = images.Image( images.ImageManager(None), - {'id': 'image-id-1', - 'name': 'Controller Image'}) + {'id': '2', + 'name': 'overcloud-control'}) image_2 = images.Image( images.ImageManager(None), - {'id': 'image-id-2', - 'name': 'Compute Image'}) + {'id': '1', + 'name': 'overcloud-compute'}) image_3 = images.Image( images.ImageManager(None), - {'id': 'image-id-3', + {'id': '3', 'name': 'Object Storage Image'}) image_4 = images.Image( images.ImageManager(None), - {'id': 'image-id-4', + {'id': '4', 'name': 'Block Storage Image'}) TEST.glanceclient_images.add(image_1, image_2, image_3, image_4)