Merge "Add missing baremetal API tests"
diff --git a/tempest/api/baremetal/admin/test_chassis.py b/tempest/api/baremetal/admin/test_chassis.py
index 254a969..2131e78 100644
--- a/tempest/api/baremetal/admin/test_chassis.py
+++ b/tempest/api/baremetal/admin/test_chassis.py
@@ -75,3 +75,9 @@
                    description=new_description))
         _, chassis = self.client.show_chassis(uuid)
         self.assertEqual(chassis['description'], new_description)
+
+    @test.attr(type='smoke')
+    def test_chassis_node_list(self):
+        _, node = self.create_node(self.chassis['uuid'])
+        _, body = self.client.list_chassis_nodes(self.chassis['uuid'])
+        self.assertIn(node['uuid'], [n['uuid'] for n in body['nodes']])
diff --git a/tempest/api/baremetal/admin/test_nodes.py b/tempest/api/baremetal/admin/test_nodes.py
index b9b9b55..8ccd36b 100644
--- a/tempest/api/baremetal/admin/test_nodes.py
+++ b/tempest/api/baremetal/admin/test_nodes.py
@@ -13,6 +13,8 @@
 import six
 
 from tempest.api.baremetal.admin import base
+from tempest.common.utils import data_utils
+from tempest.common import waiters
 from tempest import exceptions as exc
 from tempest import test
 
@@ -33,6 +35,17 @@
                 self.assertIn(key, actual)
                 self.assertEqual(value, actual[key])
 
+    def _associate_node_with_instance(self):
+        self.client.set_node_power_state(self.node['uuid'], 'power off')
+        waiters.wait_for_bm_node_status(self.client, self.node['uuid'],
+                                        'power_state', 'power off')
+        instance_uuid = data_utils.rand_uuid()
+        self.client.update_node(self.node['uuid'],
+                                instance_uuid=instance_uuid)
+        self.addCleanup(self.client.update_node,
+                        uuid=self.node['uuid'], instance_uuid=None)
+        return instance_uuid
+
     @test.attr(type='smoke')
     def test_create_node(self):
         params = {'cpu_arch': 'x86_64',
@@ -63,6 +76,34 @@
                       [i['uuid'] for i in body['nodes']])
 
     @test.attr(type='smoke')
+    def test_list_nodes_association(self):
+        _, body = self.client.list_nodes(associated=True)
+        self.assertNotIn(self.node['uuid'],
+                         [n['uuid'] for n in body['nodes']])
+
+        self._associate_node_with_instance()
+
+        _, body = self.client.list_nodes(associated=True)
+        self.assertIn(self.node['uuid'], [n['uuid'] for n in body['nodes']])
+
+        _, body = self.client.list_nodes(associated=False)
+        self.assertNotIn(self.node['uuid'], [n['uuid'] for n in body['nodes']])
+
+    @test.attr(type='smoke')
+    def test_node_port_list(self):
+        _, port = self.create_port(self.node['uuid'],
+                                   data_utils.rand_mac_address())
+        _, body = self.client.list_node_ports(self.node['uuid'])
+        self.assertIn(port['uuid'],
+                      [p['uuid'] for p in body['ports']])
+
+    @test.attr(type='smoke')
+    def test_node_port_list_no_ports(self):
+        _, node = self.create_node(self.chassis['uuid'])
+        _, body = self.client.list_node_ports(node['uuid'])
+        self.assertEmpty(body['ports'])
+
+    @test.attr(type='smoke')
     def test_update_node(self):
         props = {'cpu_arch': 'x86_64',
                  'cpu_num': '12',
@@ -120,3 +161,10 @@
 
         _, body = self.client.get_console(self.node['uuid'])
         self.assertEqual(True, body['console_enabled'])
+
+    @test.attr(type='smoke')
+    def test_get_node_by_instance_uuid(self):
+        instance_uuid = self._associate_node_with_instance()
+        _, body = self.client.show_node_by_instance_uuid(instance_uuid)
+        self.assertEqual(len(body['nodes']), 1)
+        self.assertIn(self.node['uuid'], [n['uuid'] for n in body['nodes']])
diff --git a/tempest/common/waiters.py b/tempest/common/waiters.py
index c4f1214..52568cb 100644
--- a/tempest/common/waiters.py
+++ b/tempest/common/waiters.py
@@ -131,3 +131,31 @@
             if caller:
                 message = '(%s) %s' % (caller, message)
             raise exceptions.TimeoutException(message)
+
+
+def wait_for_bm_node_status(client, node_id, attr, status):
+    """Waits for a baremetal node attribute to reach given status.
+
+    The client should have a show_node(node_uuid) method to get the node.
+    """
+    _, node = client.show_node(node_id)
+    start = int(time.time())
+
+    while node[attr] != status:
+        time.sleep(client.build_interval)
+        _, node = client.show_node(node_id)
+        if node[attr] == status:
+            return
+
+        if int(time.time()) - start >= client.build_timeout:
+            message = ('Node %(node_id)s failed to reach %(attr)s=%(status)s '
+                       'within the required time (%(timeout)s s).' %
+                       {'node_id': node_id,
+                        'attr': attr,
+                        'status': status,
+                        'timeout': client.build_timeout})
+            message += ' Current state of %s: %s.' % (attr, node[attr])
+            caller = misc_utils.find_test_caller()
+            if caller:
+                message = '(%s) %s' % (caller, message)
+            raise exceptions.TimeoutException(message)
diff --git a/tempest/services/baremetal/base.py b/tempest/services/baremetal/base.py
index 0b97f74..4933300 100644
--- a/tempest/services/baremetal/base.py
+++ b/tempest/services/baremetal/base.py
@@ -95,9 +95,13 @@
                     for ch in get_change(value, path + '%s/' % name):
                         yield ch
                 else:
-                    yield {'path': path + name,
-                           'value': value,
-                           'op': 'replace'}
+                    if value is None:
+                        yield {'path': path + name,
+                               'op': 'remove'}
+                    else:
+                        yield {'path': path + name,
+                               'value': value,
+                               'op': 'replace'}
 
         patch = [ch for ch in get_change(kw)
                  if ch['path'].lstrip('/') in allowed_attributes]
diff --git a/tempest/services/baremetal/v1/base_v1.py b/tempest/services/baremetal/v1/base_v1.py
index 032e1da..9359808 100644
--- a/tempest/services/baremetal/v1/base_v1.py
+++ b/tempest/services/baremetal/v1/base_v1.py
@@ -27,9 +27,9 @@
         self.uri_prefix = 'v%s' % self.version
 
     @base.handle_errors
-    def list_nodes(self):
+    def list_nodes(self, **kwargs):
         """List all existing nodes."""
-        return self._list_request('nodes')
+        return self._list_request('nodes', **kwargs)
 
     @base.handle_errors
     def list_chassis(self):
@@ -37,11 +37,21 @@
         return self._list_request('chassis')
 
     @base.handle_errors
+    def list_chassis_nodes(self, chassis_uuid):
+        """List all nodes associated with a chassis."""
+        return self._list_request('/chassis/%s/nodes' % chassis_uuid)
+
+    @base.handle_errors
     def list_ports(self, **kwargs):
         """List all existing ports."""
         return self._list_request('ports', **kwargs)
 
     @base.handle_errors
+    def list_node_ports(self, uuid):
+        """List all ports associated with the node."""
+        return self._list_request('/nodes/%s/ports' % uuid)
+
+    @base.handle_errors
     def list_nodestates(self, uuid):
         """List all existing states."""
         return self._list_request('/nodes/%s/states' % uuid)
@@ -68,6 +78,21 @@
         return self._show_request('nodes', uuid)
 
     @base.handle_errors
+    def show_node_by_instance_uuid(self, instance_uuid):
+        """
+        Gets a node associated with given instance uuid.
+
+        :param uuid: Unique identifier of the node in UUID format.
+        :return: Serialized node as a dictionary.
+
+        """
+        uri = '/nodes/detail?instance_uuid=%s' % instance_uuid
+
+        return self._show_request('nodes',
+                                  uuid=None,
+                                  uri=uri)
+
+    @base.handle_errors
     def show_chassis(self, uuid):
         """
         Gets a specific chassis.
@@ -203,7 +228,8 @@
                            'properties/cpu_num',
                            'properties/storage',
                            'properties/memory',
-                           'driver')
+                           'driver',
+                           'instance_uuid')
 
         patch = self._make_patch(node_attributes, **kwargs)