Merge "Make instance_action_events.traceback a non-required field"
diff --git a/releasenotes/notes/add-storyboard-in-skip-because-decorator-3e139aa8a4f7970f.yaml b/releasenotes/notes/add-storyboard-in-skip-because-decorator-3e139aa8a4f7970f.yaml
new file mode 100644
index 0000000..dd4a90b
--- /dev/null
+++ b/releasenotes/notes/add-storyboard-in-skip-because-decorator-3e139aa8a4f7970f.yaml
@@ -0,0 +1,17 @@
+---
+features:
+  - |
+    Add a new parameter called ``bug_type`` to
+    ``tempest.lib.decorators.related_bug`` and
+    ``tempest.lib.decorators.skip_because`` decorators, which accepts
+    2 values:
+
+    * launchpad
+    * storyboard
+
+    This offers the possibility of tracking bugs related to tests using
+    launchpad or storyboard references. The default value is launchpad
+    for backward compatibility.
+
+    Passing in a non-digit ``bug`` value to either decorator will raise
+    a ``InvalidParam`` exception (previously ``ValueError``).
diff --git a/tempest/api/compute/admin/test_flavors_microversions.py b/tempest/api/compute/admin/test_flavors_microversions.py
index 9f014e6..31b9217 100644
--- a/tempest/api/compute/admin/test_flavors_microversions.py
+++ b/tempest/api/compute/admin/test_flavors_microversions.py
@@ -33,14 +33,14 @@
                                            disk=10,
                                            id=flavor_id)['id']
         # Checking show API response schema
-        self.flavors_client.show_flavor(new_flavor_id)['flavor']
+        self.flavors_client.show_flavor(new_flavor_id)
         # Checking update API response schema
         self.admin_flavors_client.update_flavor(new_flavor_id,
-                                                description='new')['flavor']
+                                                description='new')
         # Checking list details API response schema
-        self.flavors_client.list_flavors(detail=True)['flavors']
+        self.flavors_client.list_flavors(detail=True)
         # Checking list API response schema
-        self.flavors_client.list_flavors()['flavors']
+        self.flavors_client.list_flavors()
 
 
 class FlavorsV261TestJSON(FlavorsV255TestJSON):
diff --git a/tempest/api/compute/admin/test_floating_ips_bulk.py b/tempest/api/compute/admin/test_floating_ips_bulk.py
index 72d09ed..2d7e1a7 100644
--- a/tempest/api/compute/admin/test_floating_ips_bulk.py
+++ b/tempest/api/compute/admin/test_floating_ips_bulk.py
@@ -25,6 +25,8 @@
 CONF = config.CONF
 
 
+# TODO(stephenfin): Remove this test class once the nova queens branch goes
+# into extended maintenance mode.
 class FloatingIPsBulkAdminTestJSON(base.BaseV2ComputeAdminTest):
     """Tests Floating IPs Bulk APIs that require admin privileges.
 
@@ -32,6 +34,7 @@
     content/ext-os-floating-ips-bulk.html
     """
     max_microversion = '2.35'
+    depends_on_nova_network = True
 
     @classmethod
     def setup_clients(cls):
diff --git a/tempest/api/compute/admin/test_live_migration.py b/tempest/api/compute/admin/test_live_migration.py
index bc38144..8350f7c 100644
--- a/tempest/api/compute/admin/test_live_migration.py
+++ b/tempest/api/compute/admin/test_live_migration.py
@@ -66,7 +66,8 @@
         kwargs = dict()
         block_migration = getattr(self, 'block_migration', None)
         if self.block_migration is None:
-            kwargs['disk_over_commit'] = False
+            if self.is_requested_microversion_compatible('2.24'):
+                kwargs['disk_over_commit'] = False
             block_migration = (CONF.compute_feature_enabled.
                                block_migration_for_live_migration and
                                not volume_backed)
diff --git a/tempest/api/compute/admin/test_live_migration_negative.py b/tempest/api/compute/admin/test_live_migration_negative.py
index deabbc2..8327a3b 100644
--- a/tempest/api/compute/admin/test_live_migration_negative.py
+++ b/tempest/api/compute/admin/test_live_migration_negative.py
@@ -32,9 +32,10 @@
 
     def _migrate_server_to(self, server_id, dest_host):
         bmflm = CONF.compute_feature_enabled.block_migration_for_live_migration
-        self.admin_servers_client.live_migrate_server(
-            server_id, host=dest_host, block_migration=bmflm,
-            disk_over_commit=False)
+        kwargs = dict(host=dest_host, block_migration=bmflm)
+        if self.is_requested_microversion_compatible('2.24'):
+            kwargs['disk_over_commit'] = False
+        self.admin_servers_client.live_migrate_server(server_id, **kwargs)
 
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('7fb7856e-ae92-44c9-861a-af62d7830bcb')
diff --git a/tempest/api/compute/admin/test_services_negative.py b/tempest/api/compute/admin/test_services_negative.py
index 993c8ec..d264829 100644
--- a/tempest/api/compute/admin/test_services_negative.py
+++ b/tempest/api/compute/admin/test_services_negative.py
@@ -74,8 +74,6 @@
     @classmethod
     def resource_setup(cls):
         super(ServicesAdminNegativeV253TestJSON, cls).resource_setup()
-        # Nova returns 400 if `binary` is not nova-compute.
-        cls.binary = 'nova-compute'
         cls.fake_service_id = data_utils.rand_uuid()
 
     @decorators.attr(type=['negative'])
diff --git a/tempest/api/compute/servers/test_device_tagging.py b/tempest/api/compute/servers/test_device_tagging.py
index 5d9bf48..40681cb 100644
--- a/tempest/api/compute/servers/test_device_tagging.py
+++ b/tempest/api/compute/servers/test_device_tagging.py
@@ -382,5 +382,8 @@
         waiters.wait_for_interface_detach(self.interfaces_client,
                                           server['id'],
                                           interface['port_id'])
-        self.verify_metadata_from_api(server, ssh_client,
-                                      self.verify_empty_devices)
+        # FIXME(mriedem): The assertion that the tagged devices are removed
+        # from the metadata for the server is being skipped until bug 1775947
+        # is fixed.
+        # self.verify_metadata_from_api(server, ssh_client,
+        #                               self.verify_empty_devices)
diff --git a/tempest/api/compute/servers/test_novnc.py b/tempest/api/compute/servers/test_novnc.py
index 1dfd0f9..3e44b56 100644
--- a/tempest/api/compute/servers/test_novnc.py
+++ b/tempest/api/compute/servers/test_novnc.py
@@ -58,6 +58,9 @@
     def resource_setup(cls):
         super(NoVNCConsoleTestJSON, cls).resource_setup()
         cls.server = cls.create_test_server(wait_until="ACTIVE")
+        cls.use_get_remote_console = False
+        if not cls.is_requested_microversion_compatible('2.5'):
+            cls.use_get_remote_console = True
 
     def _validate_novnc_html(self, vnc_url):
         """Verify we can connect to novnc and get back the javascript."""
@@ -170,8 +173,13 @@
 
     @decorators.idempotent_id('c640fdff-8ab4-45a4-a5d8-7e6146cbd0dc')
     def test_novnc(self):
-        body = self.client.get_vnc_console(self.server['id'],
-                                           type='novnc')['console']
+        if self.use_get_remote_console:
+            body = self.client.get_remote_console(
+                self.server['id'], console_type='novnc',
+                protocol='vnc')['remote_console']
+        else:
+            body = self.client.get_vnc_console(self.server['id'],
+                                               type='novnc')['console']
         self.assertEqual('novnc', body['type'])
         # Do the initial HTTP Request to novncproxy to get the NoVNC JavaScript
         self._validate_novnc_html(body['url'])
@@ -184,8 +192,13 @@
 
     @decorators.idempotent_id('f9c79937-addc-4aaa-9e0e-841eef02aeb7')
     def test_novnc_bad_token(self):
-        body = self.client.get_vnc_console(self.server['id'],
-                                           type='novnc')['console']
+        if self.use_get_remote_console:
+            body = self.client.get_remote_console(
+                self.server['id'], console_type='novnc',
+                protocol='vnc')['remote_console']
+        else:
+            body = self.client.get_vnc_console(self.server['id'],
+                                               type='novnc')['console']
         self.assertEqual('novnc', body['type'])
         # Do the WebSockify HTTP Request to novncproxy with a bad token
         url = body['url'].replace('token=', 'token=bad')
diff --git a/tempest/api/compute/servers/test_server_actions.py b/tempest/api/compute/servers/test_server_actions.py
index 350e8ba..961b2b7 100644
--- a/tempest/api/compute/servers/test_server_actions.py
+++ b/tempest/api/compute/servers/test_server_actions.py
@@ -527,10 +527,10 @@
 
     def _get_output(self):
         output = self.client.get_console_output(
-            self.server_id, length=10)['output']
+            self.server_id, length=3)['output']
         self.assertTrue(output, "Console output was empty.")
         lines = len(output.split('\n'))
-        self.assertEqual(lines, 10)
+        self.assertEqual(lines, 3)
 
     @decorators.idempotent_id('4b8867e6-fffa-4d54-b1d1-6fdda57be2f3')
     @testtools.skipUnless(CONF.compute_feature_enabled.console_output,
@@ -561,8 +561,8 @@
 
             # NOTE: This test tries to get full length console log, and the
             # length should be bigger than the one of test_get_console_output.
-            self.assertGreater(lines, 10, "Cannot get enough console log "
-                                          "length. (lines: %s)" % lines)
+            self.assertGreater(lines, 3, "Cannot get enough console log "
+                                         "length. (lines: %s)" % lines)
 
         self.wait_for(_check_full_length_console_log)
 
@@ -690,8 +690,13 @@
         # Get the VNC console of type 'novnc' and 'xvpvnc'
         console_types = ['novnc', 'xvpvnc']
         for console_type in console_types:
-            body = self.client.get_vnc_console(self.server_id,
-                                               type=console_type)['console']
+            if self.is_requested_microversion_compatible('2.5'):
+                body = self.client.get_vnc_console(
+                    self.server_id, type=console_type)['console']
+            else:
+                body = self.client.get_remote_console(
+                    self.server_id, console_type=console_type,
+                    protocol='vnc')['remote_console']
             self.assertEqual(console_type, body['type'])
             self.assertNotEqual('', body['url'])
             self._validate_url(body['url'])
diff --git a/tempest/api/compute/servers/test_server_group.py b/tempest/api/compute/servers/test_server_group.py
index 5286c8f..1b7cb96 100644
--- a/tempest/api/compute/servers/test_server_group.py
+++ b/tempest/api/compute/servers/test_server_group.py
@@ -47,8 +47,16 @@
         super(ServerGroupTestJSON, cls).resource_setup()
         cls.policy = ['affinity']
 
-        cls.created_server_group = cls.create_test_server_group(
-            policy=cls.policy)
+    def setUp(self):
+        super(ServerGroupTestJSON, self).setUp()
+        # TODO(zhufl): After microversion 2.13 project_id and user_id are
+        # added to the body of server_group, and microversion is not used
+        # in resource_setup for now, so we should create server group in setUp
+        # in order to use the same microversion as in testcases till
+        # microversion support in resource_setup is fulfilled.
+        if not hasattr(self, 'created_server_group'):
+            self.__class__.created_server_group = \
+                self.create_test_server_group(policy=self.policy)
 
     def _create_server_group(self, name, policy):
         # create the test server-group with given policy
diff --git a/tempest/api/network/admin/test_external_network_extension.py b/tempest/api/network/admin/test_external_network_extension.py
index 49a9cdb..39d03e7 100644
--- a/tempest/api/network/admin/test_external_network_extension.py
+++ b/tempest/api/network/admin/test_external_network_extension.py
@@ -95,6 +95,7 @@
         self.assertEqual(self.network['id'], show_net['id'])
         self.assertFalse(show_net['router:external'])
 
+    @decorators.skip_because(bug="1749820")
     @decorators.idempotent_id('82068503-2cf2-4ed4-b3be-ecb89432e4bb')
     @testtools.skipUnless(CONF.network_feature_enabled.floating_ips,
                           'Floating ips are not availabled')
diff --git a/tempest/api/volume/admin/test_volume_services_negative.py b/tempest/api/volume/admin/test_volume_services_negative.py
index 6f3dbc6..3a863a1 100644
--- a/tempest/api/volume/admin/test_volume_services_negative.py
+++ b/tempest/api/volume/admin/test_volume_services_negative.py
@@ -23,10 +23,9 @@
     @classmethod
     def resource_setup(cls):
         super(VolumeServicesNegativeTest, cls).resource_setup()
-        cls.services = cls.admin_volume_services_client.list_services()[
-            'services']
-        cls.host = cls.services[0]['host']
-        cls.binary = cls.services[0]['binary']
+        services = cls.admin_volume_services_client.list_services()['services']
+        cls.host = services[0]['host']
+        cls.binary = services[0]['binary']
 
     @decorators.attr(type='negative')
     @decorators.idempotent_id('3246ce65-ba70-4159-aa3b-082c28e4b484')
diff --git a/tempest/api/volume/test_volumes_extend.py b/tempest/api/volume/test_volumes_extend.py
index 5d339c4..ac9a9c7 100644
--- a/tempest/api/volume/test_volumes_extend.py
+++ b/tempest/api/volume/test_volumes_extend.py
@@ -33,7 +33,7 @@
     def test_volume_extend(self):
         # Extend Volume Test.
         volume = self.create_volume(image_ref=self.image_ref)
-        extend_size = volume['size'] + 1
+        extend_size = volume['size'] * 2
         self.volumes_client.extend_volume(volume['id'],
                                           new_size=extend_size)
         waiters.wait_for_volume_resource_status(self.volumes_client,
@@ -48,7 +48,7 @@
         volume = self.create_volume()
         self.create_snapshot(volume['id'])
 
-        extend_size = volume['size'] + 1
+        extend_size = volume['size'] * 2
         self.volumes_client.extend_volume(volume['id'], new_size=extend_size)
 
         waiters.wait_for_volume_resource_status(self.volumes_client,
diff --git a/tempest/api/volume/test_volumes_negative.py b/tempest/api/volume/test_volumes_negative.py
index f139283..866bd87 100644
--- a/tempest/api/volume/test_volumes_negative.py
+++ b/tempest/api/volume/test_volumes_negative.py
@@ -103,21 +103,24 @@
     def test_create_volume_with_nonexistent_volume_type(self):
         # Should not be able to create volume with non-existent volume type
         self.assertRaises(lib_exc.NotFound, self.volumes_client.create_volume,
-                          size='1', volume_type=data_utils.rand_uuid())
+                          size=CONF.volume.volume_size,
+                          volume_type=data_utils.rand_uuid())
 
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('0c36f6ae-4604-4017-b0a9-34fdc63096f9')
     def test_create_volume_with_nonexistent_snapshot_id(self):
         # Should not be able to create volume with non-existent snapshot
         self.assertRaises(lib_exc.NotFound, self.volumes_client.create_volume,
-                          size='1', snapshot_id=data_utils.rand_uuid())
+                          size=CONF.volume.volume_size,
+                          snapshot_id=data_utils.rand_uuid())
 
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('47c73e08-4be8-45bb-bfdf-0c4e79b88344')
     def test_create_volume_with_nonexistent_source_volid(self):
         # Should not be able to create volume with non-existent source volume
         self.assertRaises(lib_exc.NotFound, self.volumes_client.create_volume,
-                          size='1', source_volid=data_utils.rand_uuid())
+                          size=CONF.volume.volume_size,
+                          source_volid=data_utils.rand_uuid())
 
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('0186422c-999a-480e-a026-6a665744c30c')
diff --git a/tempest/api/volume/test_volumes_snapshots_negative.py b/tempest/api/volume/test_volumes_snapshots_negative.py
index ea5f036..0453c0a 100644
--- a/tempest/api/volume/test_volumes_snapshots_negative.py
+++ b/tempest/api/volume/test_volumes_snapshots_negative.py
@@ -50,7 +50,7 @@
     @decorators.idempotent_id('677863d1-34f9-456d-b6ac-9924f667a7f4')
     def test_volume_from_snapshot_decreasing_size(self):
         # Creates a volume a snapshot passing a size different from the source
-        src_size = CONF.volume.volume_size + 1
+        src_size = CONF.volume.volume_size * 2
 
         src_vol = self.create_volume(size=src_size)
         src_snap = self.create_snapshot(src_vol['id'])
@@ -58,7 +58,7 @@
         # Destination volume smaller than source
         self.assertRaises(lib_exc.BadRequest,
                           self.volumes_client.create_volume,
-                          size=src_size - 1,
+                          size=CONF.volume.volume_size,
                           snapshot_id=src_snap['id'])
 
     @decorators.attr(type=['negative'])
diff --git a/tempest/lib/decorators.py b/tempest/lib/decorators.py
index e99dd24..b399aa0 100644
--- a/tempest/lib/decorators.py
+++ b/tempest/lib/decorators.py
@@ -19,39 +19,83 @@
 import six
 import testtools
 
+from tempest.lib import exceptions as lib_exc
+
 LOG = logging.getLogger(__name__)
 
+_SUPPORTED_BUG_TYPES = {
+    'launchpad': 'https://launchpad.net/bugs/%s',
+    'storyboard': 'https://storyboard.openstack.org/#!/story/%s',
+}
+
+
+def _validate_bug_and_bug_type(bug, bug_type):
+    """Validates ``bug`` and ``bug_type`` values.
+
+    :param bug: bug number causing the test to skip (launchpad or storyboard)
+    :param bug_type: 'launchpad' or 'storyboard', default 'launchpad'
+    :raises: InvalidParam if ``bug`` is not a digit or ``bug_type`` is not
+        a valid value
+    """
+    if not bug.isdigit():
+        invalid_param = '%s must be a valid %s number' % (bug, bug_type)
+        raise lib_exc.InvalidParam(invalid_param=invalid_param)
+    if bug_type not in _SUPPORTED_BUG_TYPES:
+        invalid_param = 'bug_type "%s" must be one of: %s' % (
+            bug_type, ', '.join(_SUPPORTED_BUG_TYPES.keys()))
+        raise lib_exc.InvalidParam(invalid_param=invalid_param)
+
+
+def _get_bug_url(bug, bug_type='launchpad'):
+    """Get the bug URL based on the ``bug_type`` and ``bug``
+
+    :param bug: The launchpad/storyboard bug number causing the test
+    :param bug_type: 'launchpad' or 'storyboard', default 'launchpad'
+    :returns: Bug URL corresponding to ``bug_type`` value
+    """
+    _validate_bug_and_bug_type(bug, bug_type)
+    return _SUPPORTED_BUG_TYPES[bug_type] % bug
+
 
 def skip_because(*args, **kwargs):
     """A decorator useful to skip tests hitting known bugs
 
-    @param bug: bug number causing the test to skip
-    @param condition: optional condition to be True for the skip to have place
+    ``bug`` must be a number and ``condition`` must be true for the test to
+    skip.
+
+    :param bug: bug number causing the test to skip (launchpad or storyboard)
+    :param bug_type: 'launchpad' or 'storyboard', default 'launchpad'
+    :param condition: optional condition to be True for the skip to have place
+    :raises: testtools.TestCase.skipException if ``condition`` is True and
+        ``bug`` is included
     """
     def decorator(f):
         @functools.wraps(f)
         def wrapper(*func_args, **func_kwargs):
             skip = False
+            msg = ''
             if "condition" in kwargs:
                 if kwargs["condition"] is True:
                     skip = True
             else:
                 skip = True
             if "bug" in kwargs and skip is True:
-                if not kwargs['bug'].isdigit():
-                    raise ValueError('bug must be a valid bug number')
-                msg = "Skipped until Bug: %s is resolved." % kwargs["bug"]
+                bug = kwargs['bug']
+                bug_type = kwargs.get('bug_type', 'launchpad')
+                bug_url = _get_bug_url(bug, bug_type)
+                msg = "Skipped until bug: %s is resolved." % bug_url
                 raise testtools.TestCase.skipException(msg)
             return f(*func_args, **func_kwargs)
         return wrapper
     return decorator
 
 
-def related_bug(bug, status_code=None):
-    """A decorator useful to know solutions from launchpad bug reports
+def related_bug(bug, status_code=None, bug_type='launchpad'):
+    """A decorator useful to know solutions from launchpad/storyboard reports
 
-    @param bug: The launchpad bug number causing the test
-    @param status_code: The status code related to the bug report
+    :param bug: The launchpad/storyboard bug number causing the test bug
+    :param bug_type: 'launchpad' or 'storyboard', default 'launchpad'
+    :param status_code: The status code related to the bug report
     """
     def decorator(f):
         @functools.wraps(f)
@@ -61,9 +105,10 @@
             except Exception as exc:
                 exc_status_code = getattr(exc, 'status_code', None)
                 if status_code is None or status_code == exc_status_code:
-                    LOG.error('Hints: This test was made for the bug %s. '
-                              'The failure could be related to '
-                              'https://launchpad.net/bugs/%s', bug, bug)
+                    if bug:
+                        LOG.error('Hints: This test was made for the bug_type '
+                                  '%s. The failure could be related to '
+                                  '%s', bug, _get_bug_url(bug, bug_type))
                 raise exc
         return wrapper
     return decorator
diff --git a/tempest/scenario/manager.py b/tempest/scenario/manager.py
index c5d41a0..9db7f92 100644
--- a/tempest/scenario/manager.py
+++ b/tempest/scenario/manager.py
@@ -543,7 +543,7 @@
                                                 volume['id'], 'available')
 
     def ping_ip_address(self, ip_address, should_succeed=True,
-                        ping_timeout=None, mtu=None):
+                        ping_timeout=None, mtu=None, server=None):
         timeout = ping_timeout or CONF.validation.ping_timeout
         cmd = ['ping', '-c1', '-w1']
 
@@ -577,12 +577,16 @@
                       'caller': caller, 'ip': ip_address, 'timeout': timeout,
                       'result': 'expected' if result else 'unexpected'
                   })
+        if server:
+            self._log_console_output([server])
         return result
 
     def check_vm_connectivity(self, ip_address,
                               username=None,
                               private_key=None,
                               should_connect=True,
+                              extra_msg="",
+                              server=None,
                               mtu=None):
         """Check server connectivity
 
@@ -592,43 +596,36 @@
         :param should_connect: True/False indicates positive/negative test
             positive - attempt ping and ssh
             negative - attempt ping and fail if succeed
+        :param extra_msg: Message to help with debugging if ``ping_ip_address``
+            fails
+        :param server: The server whose console to log for debugging
         :param mtu: network MTU to use for connectivity validation
 
         :raises: AssertError if the result of the connectivity check does
             not match the value of the should_connect param
         """
+        LOG.debug('checking network connections to IP %s with user: %s',
+                  ip_address, username)
         if should_connect:
             msg = "Timed out waiting for %s to become reachable" % ip_address
         else:
             msg = "ip address %s is reachable" % ip_address
+        if extra_msg:
+            msg = "%s\n%s" % (extra_msg, msg)
         self.assertTrue(self.ping_ip_address(ip_address,
                                              should_succeed=should_connect,
-                                             mtu=mtu),
+                                             mtu=mtu, server=server),
                         msg=msg)
         if should_connect:
             # no need to check ssh for negative connectivity
-            self.get_remote_client(ip_address, username, private_key)
-
-    def check_public_network_connectivity(self, ip_address, username,
-                                          private_key, should_connect=True,
-                                          msg=None, servers=None, mtu=None):
-        # The target login is assumed to have been configured for
-        # key-based authentication by cloud-init.
-        LOG.debug('checking network connections to IP %s with user: %s',
-                  ip_address, username)
-        try:
-            self.check_vm_connectivity(ip_address,
-                                       username,
-                                       private_key,
-                                       should_connect=should_connect,
-                                       mtu=mtu)
-        except Exception:
-            ex_msg = 'Public network connectivity check failed'
-            if msg:
-                ex_msg += ": " + msg
-            LOG.exception(ex_msg)
-            self._log_console_output(servers)
-            raise
+            try:
+                self.get_remote_client(ip_address, username, private_key,
+                                       server=server)
+            except Exception:
+                if not extra_msg:
+                    extra_msg = 'Failed to ssh to %s' % ip_address
+                LOG.exception(extra_msg)
+                raise
 
     def create_floating_ip(self, thing, pool_name=None):
         """Create a floating IP and associates to a server on Nova"""
diff --git a/tempest/scenario/test_network_advanced_server_ops.py b/tempest/scenario/test_network_advanced_server_ops.py
index 87ce951..b0e4669 100644
--- a/tempest/scenario/test_network_advanced_server_ops.py
+++ b/tempest/scenario/test_network_advanced_server_ops.py
@@ -90,9 +90,10 @@
         floating_ip_addr = floating_ip['floating_ip_address']
         # Check FloatingIP status before checking the connectivity
         self.check_floating_ip_status(floating_ip, 'ACTIVE')
-        self.check_public_network_connectivity(floating_ip_addr, username,
-                                               private_key, should_connect,
-                                               servers=[server])
+        self.check_vm_connectivity(floating_ip_addr, username,
+                                   private_key, should_connect,
+                                   'Public network connectivity check failed',
+                                   server)
 
     def _wait_server_status_and_check_network_connectivity(self, server,
                                                            keypair,
diff --git a/tempest/scenario/test_network_basic_ops.py b/tempest/scenario/test_network_basic_ops.py
index 8212e75..c1132cf 100644
--- a/tempest/scenario/test_network_basic_ops.py
+++ b/tempest/scenario/test_network_basic_ops.py
@@ -175,7 +175,7 @@
     def _get_server_key(self, server):
         return self.keypairs[server['key_name']]['private_key']
 
-    def check_public_network_connectivity(
+    def _check_public_network_connectivity(
             self, should_connect=True, msg=None,
             should_check_floating_ip_status=True, mtu=None):
         """Verifies connectivty to a VM via public network and floating IP
@@ -199,13 +199,18 @@
         if should_connect:
             private_key = self._get_server_key(server)
             floatingip_status = 'ACTIVE'
+
         # Check FloatingIP Status before initiating a connection
         if should_check_floating_ip_status:
             self.check_floating_ip_status(floating_ip, floatingip_status)
-        # call the common method in the parent class
-        super(TestNetworkBasicOps, self).check_public_network_connectivity(
-            ip_address, ssh_login, private_key, should_connect, msg,
-            self.servers, mtu=mtu)
+
+        message = 'Public network connectivity check failed'
+        if msg:
+            message += '. Reason: %s' % msg
+
+        self.check_vm_connectivity(
+            ip_address, ssh_login, private_key, should_connect,
+            message, server, mtu=mtu)
 
     def _disassociate_floating_ips(self):
         floating_ip, _ = self.floating_ip_tuple
@@ -404,17 +409,17 @@
 
         """
         self._setup_network_and_servers()
-        self.check_public_network_connectivity(should_connect=True)
+        self._check_public_network_connectivity(should_connect=True)
         self._check_network_internal_connectivity(network=self.network)
         self._check_network_external_connectivity()
         self._disassociate_floating_ips()
-        self.check_public_network_connectivity(should_connect=False,
-                                               msg="after disassociate "
-                                                   "floating ip")
+        self._check_public_network_connectivity(should_connect=False,
+                                                msg="after disassociate "
+                                                    "floating ip")
         self._reassociate_floating_ips()
-        self.check_public_network_connectivity(should_connect=True,
-                                               msg="after re-associate "
-                                                   "floating ip")
+        self._check_public_network_connectivity(should_connect=True,
+                                                msg="after re-associate "
+                                                    "floating ip")
 
     @decorators.idempotent_id('b158ea55-472e-4086-8fa9-c64ac0c6c1d0')
     @testtools.skipUnless(utils.is_extension_enabled('net-mtu', 'network'),
@@ -425,10 +430,10 @@
         """Validate that network MTU sized frames fit through."""
         self._setup_network_and_servers()
         # first check that connectivity works in general for the instance
-        self.check_public_network_connectivity(should_connect=True)
+        self._check_public_network_connectivity(should_connect=True)
         # now that we checked general connectivity, test that full size frames
         # can also pass between nodes
-        self.check_public_network_connectivity(
+        self._check_public_network_connectivity(
             should_connect=True, mtu=self.network['mtu'])
 
     @decorators.idempotent_id('1546850e-fbaa-42f5-8b5f-03d8a6a95f15')
@@ -467,7 +472,7 @@
 
         """
         self._setup_network_and_servers()
-        self.check_public_network_connectivity(should_connect=True)
+        self._check_public_network_connectivity(should_connect=True)
         self._check_network_internal_connectivity(network=self.network)
         self._check_network_external_connectivity()
         self._create_new_network(create_gateway=True)
@@ -502,7 +507,7 @@
 
         """
         self._setup_network_and_servers()
-        self.check_public_network_connectivity(should_connect=True)
+        self._check_public_network_connectivity(should_connect=True)
         self._create_new_network()
         self._hotplug_server()
         self._check_network_internal_connectivity(network=self.new_net)
@@ -524,19 +529,19 @@
                 admin_state_up attribute of router to True
         """
         self._setup_network_and_servers()
-        self.check_public_network_connectivity(
+        self._check_public_network_connectivity(
             should_connect=True, msg="before updating "
             "admin_state_up of router to False")
         self._update_router_admin_state(self.router, False)
         # TODO(alokmaurya): Remove should_check_floating_ip_status=False check
         # once bug 1396310 is fixed
 
-        self.check_public_network_connectivity(
+        self._check_public_network_connectivity(
             should_connect=False, msg="after updating "
             "admin_state_up of router to False",
             should_check_floating_ip_status=False)
         self._update_router_admin_state(self.router, True)
-        self.check_public_network_connectivity(
+        self._check_public_network_connectivity(
             should_connect=True, msg="after updating "
             "admin_state_up of router to True")
 
@@ -581,7 +586,7 @@
         renew_timeout = CONF.network.build_timeout
 
         self._setup_network_and_servers(dns_nameservers=[initial_dns_server])
-        self.check_public_network_connectivity(should_connect=True)
+        self._check_public_network_connectivity(should_connect=True)
 
         floating_ip, server = self.floating_ip_tuple
         ip_address = floating_ip['floating_ip_address']
@@ -656,20 +661,20 @@
                                             private_key=private_key,
                                             server=server2)
 
-        self.check_public_network_connectivity(
+        self._check_public_network_connectivity(
             should_connect=True, msg="before updating "
             "admin_state_up of instance port to False")
         self.check_remote_connectivity(ssh_client, dest=server_pip,
                                        should_succeed=True)
         self.ports_client.update_port(port_id, admin_state_up=False)
-        self.check_public_network_connectivity(
+        self._check_public_network_connectivity(
             should_connect=False, msg="after updating "
             "admin_state_up of instance port to False",
             should_check_floating_ip_status=False)
         self.check_remote_connectivity(ssh_client, dest=server_pip,
                                        should_succeed=False)
         self.ports_client.update_port(port_id, admin_state_up=True)
-        self.check_public_network_connectivity(
+        self._check_public_network_connectivity(
             should_connect=True, msg="after updating "
             "admin_state_up of instance port to True")
         self.check_remote_connectivity(ssh_client, dest=server_pip,
@@ -766,7 +771,7 @@
             msg = "Rescheduling test does not apply to distributed routers."
             raise self.skipException(msg)
 
-        self.check_public_network_connectivity(should_connect=True)
+        self._check_public_network_connectivity(should_connect=True)
 
         # remove resource from agents
         hosting_agents = set(a["id"] for a in
@@ -783,7 +788,7 @@
                              'unscheduling router failed')
 
         # verify resource is un-functional
-        self.check_public_network_connectivity(
+        self._check_public_network_connectivity(
             should_connect=False,
             msg='after router unscheduling',
         )
@@ -800,7 +805,7 @@
             "target agent")
 
         # verify resource is functional
-        self.check_public_network_connectivity(
+        self._check_public_network_connectivity(
             should_connect=True,
             msg='After router rescheduling')
 
@@ -834,7 +839,7 @@
 
         # Create server
         self._setup_network_and_servers()
-        self.check_public_network_connectivity(should_connect=True)
+        self._check_public_network_connectivity(should_connect=True)
         self._create_new_network()
         self._hotplug_server()
         fip, server = self.floating_ip_tuple
diff --git a/tempest/tests/lib/services/volume/v2/test_volumes_client.py b/tempest/tests/lib/services/volume/v2/test_volumes_client.py
deleted file mode 100644
index d7b042e..0000000
--- a/tempest/tests/lib/services/volume/v2/test_volumes_client.py
+++ /dev/null
@@ -1,127 +0,0 @@
-# Copyright 2017 FiberHome Telecommunication Technologies CO.,LTD
-# All Rights Reserved.
-#
-#    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 oslo_serialization import jsonutils as json
-
-from tempest.lib.services.volume.v2 import volumes_client
-from tempest.tests.lib import fake_auth_provider
-from tempest.tests.lib.services import base
-
-
-class TestVolumesClient(base.BaseServiceTest):
-
-    FAKE_VOLUME_METADATA_ITEM = {
-        "meta": {
-            "key1": "value1"
-        }
-    }
-
-    FAKE_VOLUME_IMAGE_METADATA = {
-        "metadata": {
-            "container_format": "bare",
-            "min_ram": "0",
-            "disk_format": "raw",
-            "image_name": "xly-ubuntu16-server",
-            "image_id": "3e087b0c-10c5-4255-b147-6e8e9dbad6fc",
-            "checksum": "008f5d22fe3cb825d714da79607a90f9",
-            "min_disk": "0",
-            "size": "8589934592"
-        }
-    }
-
-    def setUp(self):
-        super(TestVolumesClient, self).setUp()
-        fake_auth = fake_auth_provider.FakeAuthProvider()
-        self.client = volumes_client.VolumesClient(fake_auth,
-                                                   'volume',
-                                                   'regionOne')
-
-    def _test_retype_volume(self, bytes_body=False):
-        kwargs = {
-            "new_type": "dedup-tier-replication",
-            "migration_policy": "never"
-        }
-
-        self.check_service_client_function(
-            self.client.retype_volume,
-            'tempest.lib.common.rest_client.RestClient.post',
-            {},
-            to_utf=bytes_body,
-            status=202,
-            volume_id="a3be971b-8de5-4bdf-bdb8-3d8eb0fb69f8",
-            **kwargs
-        )
-
-    def _test_force_detach_volume(self, bytes_body=False):
-        kwargs = {
-            'attachment_id': '6980e295-920f-412e-b189-05c50d605acd',
-            'connector': {
-                'initiator': 'iqn.2017-04.org.fake:01'
-            }
-        }
-
-        self.check_service_client_function(
-            self.client.force_detach_volume,
-            'tempest.lib.common.rest_client.RestClient.post',
-            {},
-            to_utf=bytes_body,
-            status=202,
-            volume_id="a3be971b-8de5-4bdf-bdb8-3d8eb0fb69f8",
-            **kwargs
-        )
-
-    def _test_show_volume_metadata_item(self, bytes_body=False):
-        self.check_service_client_function(
-            self.client.show_volume_metadata_item,
-            'tempest.lib.common.rest_client.RestClient.get',
-            self.FAKE_VOLUME_METADATA_ITEM,
-            to_utf=bytes_body,
-            volume_id="a3be971b-8de5-4bdf-bdb8-3d8eb0fb69f8",
-            id="key1")
-
-    def _test_show_volume_image_metadata(self, bytes_body=False):
-        fake_volume_id = "a3be971b-8de5-4bdf-bdb8-3d8eb0fb69f8"
-        self.check_service_client_function(
-            self.client.show_volume_image_metadata,
-            'tempest.lib.common.rest_client.RestClient.post',
-            self.FAKE_VOLUME_IMAGE_METADATA,
-            to_utf=bytes_body,
-            mock_args=['volumes/%s/action' % fake_volume_id,
-                       json.dumps({"os-show_image_metadata": {}})],
-            volume_id=fake_volume_id)
-
-    def test_force_detach_volume_with_str_body(self):
-        self._test_force_detach_volume()
-
-    def test_force_detach_volume_with_bytes_body(self):
-        self._test_force_detach_volume(bytes_body=True)
-
-    def test_show_volume_metadata_item_with_str_body(self):
-        self._test_show_volume_metadata_item()
-
-    def test_show_volume_metadata_item_with_bytes_body(self):
-        self._test_show_volume_metadata_item(bytes_body=True)
-
-    def test_show_volume_image_metadata_with_str_body(self):
-        self._test_show_volume_image_metadata()
-
-    def test_show_volume_image_metadata_with_bytes_body(self):
-        self._test_show_volume_image_metadata(bytes_body=True)
-
-    def test_retype_volume_with_str_body(self):
-        self._test_retype_volume()
-
-    def test_retype_volume_with_bytes_body(self):
-        self._test_retype_volume(bytes_body=True)
diff --git a/tempest/tests/lib/services/volume/v3/test_volumes_client.py b/tempest/tests/lib/services/volume/v3/test_volumes_client.py
index a515fd3..1250536 100644
--- a/tempest/tests/lib/services/volume/v3/test_volumes_client.py
+++ b/tempest/tests/lib/services/volume/v3/test_volumes_client.py
@@ -13,6 +13,8 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+from oslo_serialization import jsonutils as json
+
 from tempest.lib.services.volume.v3 import volumes_client
 from tempest.tests.lib import fake_auth_provider
 from tempest.tests.lib.services import base
@@ -27,6 +29,25 @@
         }
     }
 
+    FAKE_VOLUME_METADATA_ITEM = {
+        "meta": {
+            "key1": "value1"
+        }
+    }
+
+    FAKE_VOLUME_IMAGE_METADATA = {
+        "metadata": {
+            "container_format": "bare",
+            "min_ram": "0",
+            "disk_format": "raw",
+            "image_name": "xly-ubuntu16-server",
+            "image_id": "3e087b0c-10c5-4255-b147-6e8e9dbad6fc",
+            "checksum": "008f5d22fe3cb825d714da79607a90f9",
+            "min_disk": "0",
+            "size": "8589934592"
+        }
+    }
+
     def setUp(self):
         super(TestVolumesClient, self).setUp()
         fake_auth = fake_auth_provider.FakeAuthProvider()
@@ -34,6 +55,60 @@
                                                    'volume',
                                                    'regionOne')
 
+    def _test_retype_volume(self, bytes_body=False):
+        kwargs = {
+            "new_type": "dedup-tier-replication",
+            "migration_policy": "never"
+        }
+
+        self.check_service_client_function(
+            self.client.retype_volume,
+            'tempest.lib.common.rest_client.RestClient.post',
+            {},
+            to_utf=bytes_body,
+            status=202,
+            volume_id="a3be971b-8de5-4bdf-bdb8-3d8eb0fb69f8",
+            **kwargs
+        )
+
+    def _test_force_detach_volume(self, bytes_body=False):
+        kwargs = {
+            'attachment_id': '6980e295-920f-412e-b189-05c50d605acd',
+            'connector': {
+                'initiator': 'iqn.2017-04.org.fake:01'
+            }
+        }
+
+        self.check_service_client_function(
+            self.client.force_detach_volume,
+            'tempest.lib.common.rest_client.RestClient.post',
+            {},
+            to_utf=bytes_body,
+            status=202,
+            volume_id="a3be971b-8de5-4bdf-bdb8-3d8eb0fb69f8",
+            **kwargs
+        )
+
+    def _test_show_volume_metadata_item(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.show_volume_metadata_item,
+            'tempest.lib.common.rest_client.RestClient.get',
+            self.FAKE_VOLUME_METADATA_ITEM,
+            to_utf=bytes_body,
+            volume_id="a3be971b-8de5-4bdf-bdb8-3d8eb0fb69f8",
+            id="key1")
+
+    def _test_show_volume_image_metadata(self, bytes_body=False):
+        fake_volume_id = "a3be971b-8de5-4bdf-bdb8-3d8eb0fb69f8"
+        self.check_service_client_function(
+            self.client.show_volume_image_metadata,
+            'tempest.lib.common.rest_client.RestClient.post',
+            self.FAKE_VOLUME_IMAGE_METADATA,
+            to_utf=bytes_body,
+            mock_args=['volumes/%s/action' % fake_volume_id,
+                       json.dumps({"os-show_image_metadata": {}})],
+            volume_id=fake_volume_id)
+
     def _test_show_volume_summary(self, bytes_body=False):
         self.check_service_client_function(
             self.client.show_volume_summary,
@@ -41,6 +116,30 @@
             self.FAKE_VOLUME_SUMMARY,
             bytes_body)
 
+    def test_force_detach_volume_with_str_body(self):
+        self._test_force_detach_volume()
+
+    def test_force_detach_volume_with_bytes_body(self):
+        self._test_force_detach_volume(bytes_body=True)
+
+    def test_show_volume_metadata_item_with_str_body(self):
+        self._test_show_volume_metadata_item()
+
+    def test_show_volume_metadata_item_with_bytes_body(self):
+        self._test_show_volume_metadata_item(bytes_body=True)
+
+    def test_show_volume_image_metadata_with_str_body(self):
+        self._test_show_volume_image_metadata()
+
+    def test_show_volume_image_metadata_with_bytes_body(self):
+        self._test_show_volume_image_metadata(bytes_body=True)
+
+    def test_retype_volume_with_str_body(self):
+        self._test_retype_volume()
+
+    def test_retype_volume_with_bytes_body(self):
+        self._test_retype_volume(bytes_body=True)
+
     def test_show_volume_summary_with_str_body(self):
         self._test_show_volume_summary()
 
diff --git a/tempest/tests/lib/test_decorators.py b/tempest/tests/lib/test_decorators.py
index ed0eea3..0b1a599 100644
--- a/tempest/tests/lib/test_decorators.py
+++ b/tempest/tests/lib/test_decorators.py
@@ -19,6 +19,7 @@
 from tempest.lib import base as test
 from tempest.lib.common.utils import data_utils
 from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
 from tempest.tests import base
 
 
@@ -62,21 +63,40 @@
 
         t = TestFoo('test_bar')
         if expected_to_skip:
-            self.assertRaises(testtools.TestCase.skipException, t.test_bar)
+            e = self.assertRaises(testtools.TestCase.skipException, t.test_bar)
+            bug = decorator_args['bug']
+            bug_type = decorator_args.get('bug_type', 'launchpad')
+            self.assertRegex(
+                str(e),
+                r'Skipped until bug\: %s.*' % decorators._get_bug_url(
+                    bug, bug_type)
+            )
         else:
             # assert that test_bar returned 0
             self.assertEqual(TestFoo('test_bar').test_bar(), 0)
 
-    def test_skip_because_bug(self):
+    def test_skip_because_launchpad_bug(self):
         self._test_skip_because_helper(bug='12345')
 
-    def test_skip_because_bug_and_condition_true(self):
+    def test_skip_because_launchpad_bug_and_condition_true(self):
         self._test_skip_because_helper(bug='12348', condition=True)
 
-    def test_skip_because_bug_and_condition_false(self):
+    def test_skip_because_launchpad_bug_and_condition_false(self):
         self._test_skip_because_helper(expected_to_skip=False,
                                        bug='12349', condition=False)
 
+    def test_skip_because_storyboard_bug(self):
+        self._test_skip_because_helper(bug='1992', bug_type='storyboard')
+
+    def test_skip_because_storyboard_bug_and_condition_true(self):
+        self._test_skip_because_helper(bug='1992', bug_type='storyboard',
+                                       condition=True)
+
+    def test_skip_because_storyboard_bug_and_condition_false(self):
+        self._test_skip_because_helper(expected_to_skip=False,
+                                       bug='1992', bug_type='storyboard',
+                                       condition=False)
+
     def test_skip_because_bug_without_bug_never_skips(self):
         """Never skip without a bug parameter."""
         self._test_skip_because_helper(expected_to_skip=False,
@@ -84,8 +104,8 @@
         self._test_skip_because_helper(expected_to_skip=False)
 
     def test_skip_because_invalid_bug_number(self):
-        """Raise ValueError if with an invalid bug number"""
-        self.assertRaises(ValueError, self._test_skip_because_helper,
+        """Raise InvalidParam if with an invalid bug number"""
+        self.assertRaises(lib_exc.InvalidParam, self._test_skip_because_helper,
                           bug='critical_bug')
 
 
@@ -126,6 +146,13 @@
 
 
 class TestRelatedBugDecorator(base.TestCase):
+
+    def _get_my_exception(self):
+        class MyException(Exception):
+            def __init__(self, status_code):
+                self.status_code = status_code
+        return MyException
+
     def test_relatedbug_when_no_exception(self):
         f = mock.Mock()
         sentinel = object()
@@ -137,10 +164,9 @@
         test_foo(sentinel)
         f.assert_called_once_with(sentinel)
 
-    def test_relatedbug_when_exception(self):
-        class MyException(Exception):
-            def __init__(self, status_code):
-                self.status_code = status_code
+    def test_relatedbug_when_exception_with_launchpad_bug_type(self):
+        """Validate related_bug decorator with bug_type == 'launchpad'"""
+        MyException = self._get_my_exception()
 
         def f(self):
             raise MyException(status_code=500)
@@ -152,4 +178,53 @@
         with mock.patch.object(decorators.LOG, 'error') as m_error:
             self.assertRaises(MyException, test_foo, object())
 
-        m_error.assert_called_once_with(mock.ANY, '1234', '1234')
+        m_error.assert_called_once_with(
+            mock.ANY, '1234', 'https://launchpad.net/bugs/1234')
+
+    def test_relatedbug_when_exception_with_storyboard_bug_type(self):
+        """Validate related_bug decorator with bug_type == 'storyboard'"""
+        MyException = self._get_my_exception()
+
+        def f(self):
+            raise MyException(status_code=500)
+
+        @decorators.related_bug(bug="1234", status_code=500,
+                                bug_type='storyboard')
+        def test_foo(self):
+            f(self)
+
+        with mock.patch.object(decorators.LOG, 'error') as m_error:
+            self.assertRaises(MyException, test_foo, object())
+
+        m_error.assert_called_once_with(
+            mock.ANY, '1234', 'https://storyboard.openstack.org/#!/story/1234')
+
+    def test_relatedbug_when_exception_invalid_bug_type(self):
+        """Check related_bug decorator raises exc when bug_type is not valid"""
+        MyException = self._get_my_exception()
+
+        def f(self):
+            raise MyException(status_code=500)
+
+        @decorators.related_bug(bug="1234", status_code=500,
+                                bug_type=mock.sentinel.invalid)
+        def test_foo(self):
+            f(self)
+
+        with mock.patch.object(decorators.LOG, 'error'):
+            self.assertRaises(lib_exc.InvalidParam, test_foo, object())
+
+    def test_relatedbug_when_exception_invalid_bug_number(self):
+        """Check related_bug decorator raises exc when bug_number != digit"""
+        MyException = self._get_my_exception()
+
+        def f(self):
+            raise MyException(status_code=500)
+
+        @decorators.related_bug(bug="not a digit", status_code=500,
+                                bug_type='launchpad')
+        def test_foo(self):
+            f(self)
+
+        with mock.patch.object(decorators.LOG, 'error'):
+            self.assertRaises(lib_exc.InvalidParam, test_foo, object())