Merge "Remove DataGenerator from admin V3 identity tests"
diff --git a/requirements.txt b/requirements.txt
index 216dd50..fdcb3d5 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -8,7 +8,6 @@
 paramiko>=2.0 # LGPLv2.1+
 netaddr!=0.7.16,>=0.7.12 # BSD
 testrepository>=0.0.18 # Apache-2.0/BSD
-pyOpenSSL>=0.14 # Apache-2.0
 oslo.concurrency>=3.8.0 # Apache-2.0
 oslo.config>=3.12.0 # Apache-2.0
 oslo.i18n>=2.1.0 # Apache-2.0
diff --git a/tempest/api/compute/servers/test_attach_interfaces.py b/tempest/api/compute/servers/test_attach_interfaces.py
index fdf55e5..d02f86f 100644
--- a/tempest/api/compute/servers/test_attach_interfaces.py
+++ b/tempest/api/compute/servers/test_attach_interfaces.py
@@ -16,7 +16,9 @@
 import time
 
 from tempest.api.compute import base
+from tempest.common import compute
 from tempest.common.utils import net_utils
+from tempest.common import waiters
 from tempest import config
 from tempest import exceptions
 from tempest.lib import exceptions as lib_exc
@@ -48,6 +50,7 @@
         cls.networks_client = cls.os.networks_client
         cls.subnets_client = cls.os.subnets_client
         cls.ports_client = cls.os.ports_client
+        cls.servers_client = cls.servers_client
 
     def wait_for_interface_status(self, server, port_id, status):
         """Waits for an interface to reach a given status."""
@@ -73,6 +76,34 @@
 
         return body
 
+    # TODO(mriedem): move this into a common waiters utility module
+    def wait_for_port_detach(self, port_id):
+        """Waits for the port's device_id to be unset.
+
+        :param port_id: The id of the port being detached.
+        :returns: The final port dict from the show_port response.
+        """
+        port = self.ports_client.show_port(port_id)['port']
+        device_id = port['device_id']
+        start = int(time.time())
+
+        # NOTE(mriedem): Nova updates the port's device_id to '' rather than
+        # None, but it's not contractual so handle Falsey either way.
+        while device_id:
+            time.sleep(self.build_interval)
+            port = self.ports_client.show_port(port_id)['port']
+            device_id = port['device_id']
+
+            timed_out = int(time.time()) - start >= self.build_timeout
+
+            if device_id and timed_out:
+                message = ('Port %s failed to detach (device_id %s) within '
+                           'the required time (%s s).' %
+                           (port_id, device_id, self.build_timeout))
+                raise exceptions.TimeoutException(message)
+
+        return port
+
     def _check_interface(self, iface, port_id=None, network_id=None,
                          fixed_ip=None, mac_addr=None):
         self.assertIn('port_state', iface)
@@ -240,3 +271,40 @@
             if fixed_ip is not None:
                 break
         self.servers_client.remove_fixed_ip(server['id'], address=fixed_ip)
+
+    @test.idempotent_id('2f3a0127-95c7-4977-92d2-bc5aec602fb4')
+    def test_reassign_port_between_servers(self):
+        """Tests the following:
+
+        1. Create a port in Neutron.
+        2. Create two servers in Nova.
+        3. Attach the port to the first server.
+        4. Detach the port from the first server.
+        5. Attach the port to the second server.
+        6. Detach the port from the second server.
+        """
+        network = self.get_tenant_network()
+        network_id = network['id']
+        port = self.ports_client.create_port(network_id=network_id)
+        port_id = port['port']['id']
+        self.addCleanup(self.ports_client.delete_port, port_id)
+
+        # create two servers
+        _, servers = compute.create_test_server(
+            self.os, tenant_network=network, wait_until='ACTIVE', min_count=2)
+        # add our cleanups for the servers since we bypassed the base class
+        for server in servers:
+            self.addCleanup(waiters.wait_for_server_termination,
+                            self.servers_client, server['id'])
+            self.addCleanup(self.servers_client.delete_server, server['id'])
+
+        for server in servers:
+            # attach the port to the server
+            iface = self.client.create_interface(
+                server['id'], port_id=port_id)['interfaceAttachment']
+            self._check_interface(iface, port_id=port_id)
+
+            # detach the port from the server; this is a cast in the compute
+            # API so we have to poll the port until the device_id is unset.
+            self.client.delete_interface(server['id'], port_id)
+            self.wait_for_port_detach(port_id)
diff --git a/tempest/api/compute/servers/test_server_actions.py b/tempest/api/compute/servers/test_server_actions.py
index 0d06119..21e7d10 100644
--- a/tempest/api/compute/servers/test_server_actions.py
+++ b/tempest/api/compute/servers/test_server_actions.py
@@ -110,6 +110,10 @@
                 servers_client=self.client)
             boot_time = linux_client.get_boot_time()
 
+            # NOTE: This sync is for avoiding the loss of pub key data
+            # in a server
+            linux_client.exec_command("sync")
+
         self.client.reboot_server(self.server_id, type=reboot_type)
         waiters.wait_for_server_status(self.client, self.server_id, 'ACTIVE')
 
diff --git a/tempest/cmd/run.py b/tempest/cmd/run.py
index b6b70d7..1c0d9c4 100644
--- a/tempest/cmd/run.py
+++ b/tempest/cmd/run.py
@@ -25,7 +25,7 @@
 
 There are also the **--blacklist_file** and **--whitelist_file** options that
 let you pass a filepath to tempest run with the file format being a line
-seperated regex, with '#' used to signify the start of a comment on a line.
+separated regex, with '#' used to signify the start of a comment on a line.
 For example::
 
     # Regex file
@@ -193,7 +193,7 @@
         list_selector = parser.add_mutually_exclusive_group()
         list_selector.add_argument('--whitelist_file',
                                    help="Path to a whitelist file, this file "
-                                        "contains a seperate regex on each "
+                                        "contains a separate regex on each "
                                         "newline.")
         list_selector.add_argument('--blacklist_file',
                                    help='Path to a blacklist file, this file '
diff --git a/tempest/lib/services/image/v2/images_client.py b/tempest/lib/services/image/v2/images_client.py
index 4276847..996ce94 100644
--- a/tempest/lib/services/image/v2/images_client.py
+++ b/tempest/lib/services/image/v2/images_client.py
@@ -54,24 +54,44 @@
         return rest_client.ResponseBody(resp, body)
 
     def deactivate_image(self, image_id):
+        """Deactivate image.
+
+         Available params: see http://developer.openstack.org/
+                               api-ref-image-v2.html#deactivateImage-v2
+         """
         url = 'images/%s/actions/deactivate' % image_id
         resp, body = self.post(url, None)
         self.expected_success(204, resp.status)
         return rest_client.ResponseBody(resp, body)
 
     def reactivate_image(self, image_id):
+        """Reactivate image.
+
+         Available params: see http://developer.openstack.org/
+                               api-ref-image-v2.html#reactivateImage-v2
+         """
         url = 'images/%s/actions/reactivate' % image_id
         resp, body = self.post(url, None)
         self.expected_success(204, resp.status)
         return rest_client.ResponseBody(resp, body)
 
     def delete_image(self, image_id):
+        """Delete image.
+
+         Available params: see http://developer.openstack.org/
+                               api-ref-image-v2.html#deleteImage-v2
+         """
         url = 'images/%s' % image_id
         resp, _ = self.delete(url)
         self.expected_success(204, resp.status)
         return rest_client.ResponseBody(resp)
 
     def list_images(self, params=None):
+        """List images.
+
+        Available params: see http://developer.openstack.org/
+                              api-ref-image-v2.html#listImages-v2
+        """
         url = 'images'
 
         if params:
@@ -83,6 +103,11 @@
         return rest_client.ResponseBody(resp, body)
 
     def show_image(self, image_id):
+        """Show image details.
+
+        Available params: http://developer.openstack.org/
+                          api-ref-image-v2.html#showImage-v2
+        """
         url = 'images/%s' % image_id
         resp, body = self.get(url)
         self.expected_success(200, resp.status)
@@ -102,6 +127,11 @@
         return 'image'
 
     def store_image_file(self, image_id, data):
+        """Upload binary image data.
+
+        Available params: http://developer.openstack.org/
+                          api-ref-image-v2.html#storeImageFile-v2
+        """
         url = 'images/%s/file' % image_id
 
         # We are going to do chunked transfert, so split the input data
@@ -115,7 +145,7 @@
         return rest_client.ResponseBody(resp, body)
 
     def show_image_file(self, image_id):
-        """Show an image file.
+        """Download binary image data.
 
         Available params: http://developer.openstack.org/
                           api-ref-image-v2.html#showImageFile-v2
diff --git a/tempest/scenario/test_network_basic_ops.py b/tempest/scenario/test_network_basic_ops.py
index 402a70c..9c48080 100644
--- a/tempest/scenario/test_network_basic_ops.py
+++ b/tempest/scenario/test_network_basic_ops.py
@@ -642,6 +642,8 @@
 
         Nova should unbind the port from the instance on delete if the port was
         not created by Nova as part of the boot request.
+
+        We should also be able to boot another server with the same port.
         """
         # Setup the network, create a port and boot the server from that port.
         self._setup_network_and_servers(boot_with_port=True)
@@ -670,6 +672,17 @@
         self.assertEqual('', port['device_id'])
         self.assertEqual('', port['device_owner'])
 
+        # Boot another server with the same port to make sure nothing was
+        # left around that could cause issues.
+        name = data_utils.rand_name('reuse-port')
+        server = self._create_server(name, self.network, port['id'])
+        port_list = self._list_ports(device_id=server['id'],
+                                     network_id=self.network['id'])
+        self.assertEqual(1, len(port_list),
+                         'There should only be one port created for '
+                         'server %s.' % server['id'])
+        self.assertEqual(port['id'], port_list[0]['id'])
+
     @test.requires_ext(service='network', extension='l3_agent_scheduler')
     @test.idempotent_id('2e788c46-fb3f-4ac9-8f82-0561555bea73')
     @test.services('compute', 'network')
diff --git a/tempest/scenario/test_shelve_instance.py b/tempest/scenario/test_shelve_instance.py
index 6d3ecd4..4b9c61c 100644
--- a/tempest/scenario/test_shelve_instance.py
+++ b/tempest/scenario/test_shelve_instance.py
@@ -13,8 +13,6 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-import testtools
-
 from tempest.common import compute
 from tempest.common import waiters
 from tempest import config
@@ -35,6 +33,12 @@
 
     """
 
+    @classmethod
+    def skip_checks(cls):
+        super(TestShelveInstance, cls).skip_checks()
+        if not CONF.compute_feature_enabled.shelve:
+            raise cls.skipException("Shelve is not available.")
+
     def _shelve_then_unshelve_server(self, server):
         compute.shelve_server(self.servers_client, server['id'],
                               force_shelve_offload=True)
@@ -83,15 +87,11 @@
         self.assertEqual(timestamp, timestamp2)
 
     @test.idempotent_id('1164e700-0af0-4a4c-8792-35909a88743c')
-    @testtools.skipUnless(CONF.compute_feature_enabled.shelve,
-                          'Shelve is not available.')
     @test.services('compute', 'network', 'image')
     def test_shelve_instance(self):
         self._create_server_then_shelve_and_unshelve()
 
     @test.idempotent_id('c1b6318c-b9da-490b-9c67-9339b627271f')
-    @testtools.skipUnless(CONF.compute_feature_enabled.shelve,
-                          'Shelve is not available.')
     @test.services('compute', 'volume', 'network', 'image')
     def test_shelve_volume_backed_instance(self):
         self._create_server_then_shelve_and_unshelve(boot_from_volume=True)
diff --git a/tempest/stress/actions/unit_test.py b/tempest/stress/actions/unit_test.py
index 3b27885..e016c61 100644
--- a/tempest/stress/actions/unit_test.py
+++ b/tempest/stress/actions/unit_test.py
@@ -80,8 +80,6 @@
 
             try:
                 self.run_core()
-            except Exception as e:
-                raise e
             finally:
                 if (CONF.stress.leave_dirty_stack is False
                     and self.class_setup_per == SetUpClassRunTime.action):