Merge "Add support for blacklist file for Zuul jobs"
diff --git a/.zuul.yaml b/.zuul.yaml
index 67d1f5e..4f17959 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -93,6 +93,13 @@
       tox_envlist: plugin-sanity-check
     voting: false
     timeout: 5000
+    irrelevant-files:
+      - ^.*\.rst$
+      - ^doc/.*$
+      - ^etc/.*$
+      - ^releasenotes/.*$
+      - ^tempest/hacking/.*$
+      - ^tempest/tests/.*$
     required-projects:
       - openstack/almanach
       - openstack/aodh
diff --git a/playbooks/post-tempest.yaml b/playbooks/post-tempest.yaml
index ab7a1bb..4dde2c9 100644
--- a/playbooks/post-tempest.yaml
+++ b/playbooks/post-tempest.yaml
@@ -1,7 +1,6 @@
 - hosts: all
   become: true
   roles:
-    - role: process-test-results
-      test_results_dir: '{{ devstack_base_dir }}/tempest'
-      tox_envdir: tempest
+    - role: fetch-subunit-output
+      zuul_work_dir: '{{ devstack_base_dir }}/tempest'
     - role: process-stackviz
diff --git a/releasenotes/notes/add-port-profile-config-option-2610b2fa67027960.yaml b/releasenotes/notes/add-port-profile-config-option-2610b2fa67027960.yaml
new file mode 100644
index 0000000..b54ee8b
--- /dev/null
+++ b/releasenotes/notes/add-port-profile-config-option-2610b2fa67027960.yaml
@@ -0,0 +1,11 @@
+---
+prelude: >
+    When using OVS HW offload feature we need to create
+    Neutron port with a certain capability. This is done
+    by creating Neutron port with binding profile. To be
+    able to test this we need profile capability support
+    in Tempest as well.
+features:
+  - A new config option 'port_profile' is added to the section
+    'network' to specify capabilities of the port.
+    By default this is set to {}.
diff --git a/releasenotes/notes/start-of-queens-support-fea9051ba1d85fc7.yaml b/releasenotes/notes/start-of-queens-support-fea9051ba1d85fc7.yaml
new file mode 100644
index 0000000..10b2300
--- /dev/null
+++ b/releasenotes/notes/start-of-queens-support-fea9051ba1d85fc7.yaml
@@ -0,0 +1,12 @@
+---
+prelude: >
+    This release marks the start of Queens release support in Tempest.
+    This release also marks the end of support for Newton in Tempest.
+other:
+    - OpenStack Releases supported after this release are **Queens**, **Pike**,
+      and **Ocata**.
+
+      The release under current development of this tag is Rocky, meaning
+      that every Tempest commit is also tested against master during the Rocky
+      cycle. However, this does not necessarily mean that using Tempest as of
+      this tag will work against a Rocky (or future release) cloud.
diff --git a/releasenotes/notes/tempest-run-fix-updates-564b41706decbba1.yaml b/releasenotes/notes/tempest-run-fix-updates-564b41706decbba1.yaml
new file mode 100644
index 0000000..265853d
--- /dev/null
+++ b/releasenotes/notes/tempest-run-fix-updates-564b41706decbba1.yaml
@@ -0,0 +1,9 @@
+---
+features:
+  - |
+    Adds a new CLI arg in tempest run, --black-regex, which is a regex to
+    exclude the tests that match it.
+fixes:
+  - |
+    Fixes tempest run CLI args mutually exclusive behavior which should not
+    be the case anymore (Bug#1751201).
diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst
index df1de46..d968a44 100644
--- a/releasenotes/source/index.rst
+++ b/releasenotes/source/index.rst
@@ -6,6 +6,7 @@
     :maxdepth: 1
 
     unreleased
+    v18.0.0
     v17.0.0
     v16.1.0
     v16.0.0
diff --git a/releasenotes/source/v18.0.0.rst b/releasenotes/source/v18.0.0.rst
new file mode 100644
index 0000000..aa05db3
--- /dev/null
+++ b/releasenotes/source/v18.0.0.rst
@@ -0,0 +1,6 @@
+=====================
+v18.0.0 Release Notes
+=====================
+
+.. release-notes:: 18.0.0 Release Notes
+   :version: 18.0.0
diff --git a/roles/process-stackviz/README.rst b/roles/process-stackviz/README.rst
index 54c217b..a8447d2 100644
--- a/roles/process-stackviz/README.rst
+++ b/roles/process-stackviz/README.rst
@@ -16,7 +16,7 @@
    The stage directory where the input data can be found and
    the output will be produced.
 
-.. zuul:rolevar:: test_results_stage_name
-   :default: test_results
+.. zuul:rolevar:: zuul_work_dir
+   :default: {{ devstack_base_dir }}/tempest
 
-   The name of the subunit file to be used as input.
+   Directory to work in. It has to be a fully qualified path.
diff --git a/roles/process-stackviz/defaults/main.yaml b/roles/process-stackviz/defaults/main.yaml
index c6a64d1..f3bc32b 100644
--- a/roles/process-stackviz/defaults/main.yaml
+++ b/roles/process-stackviz/defaults/main.yaml
@@ -1,3 +1,3 @@
 devstack_base_dir: /opt/stack
 stage_dir: "{{ ansible_user_dir }}"
-test_results_stage_name: test_results
+zuul_work_dir: "{{ devstack_base_dir }}/tempest"
diff --git a/roles/process-stackviz/tasks/main.yaml b/roles/process-stackviz/tasks/main.yaml
index 09de606..82f8f3d 100644
--- a/roles/process-stackviz/tasks/main.yaml
+++ b/roles/process-stackviz/tasks/main.yaml
@@ -9,11 +9,11 @@
 
 - name: Check if subunit data exists
   stat:
-    path: "{{ stage_dir }}/{{ test_results_stage_name }}.subunit"
+    path: "{{ zuul_work_dir }}/testrepository.subunit"
   register: subunit_input
 
 - debug:
-    msg: "Subunit file could not be found at {{ stage_dir }}/{{ test_results_stage_name }}.subunit"
+    msg: "Subunit file could not be found at {{ zuul_work_dir }}/testrepository.subunit"
   when: not subunit_input.stat.exists
 
 - name: Install stackviz
diff --git a/setup.cfg b/setup.cfg
index 04bb29f..c981370 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -49,7 +49,8 @@
 
 [build_sphinx]
 all-files = 1
-warning-is-error = 1
+# warning can be generated by using GENERATE_TEMPEST_PLUGIN_LIST='False'
+warning-is-error = 0
 build-dir = doc/build
 source-dir = doc/source
 
diff --git a/tempest/api/compute/admin/test_servers_on_multinodes.py b/tempest/api/compute/admin/test_servers_on_multinodes.py
index 18c974a..d32a5b4 100644
--- a/tempest/api/compute/admin/test_servers_on_multinodes.py
+++ b/tempest/api/compute/admin/test_servers_on_multinodes.py
@@ -43,6 +43,28 @@
         return cls.os_admin.servers_client.show_server(
             server_id)['server']['OS-EXT-SRV-ATTR:host']
 
+    def _create_servers_with_group(self, policy):
+        group_id = self.create_test_server_group(policy=[policy])['id']
+        hints = {'group': group_id}
+        reservation_id = self.create_test_server(
+            scheduler_hints=hints, wait_until='ACTIVE', min_count=2,
+            return_reservation_id=True)['reservation_id']
+
+        # Get the servers using the reservation_id.
+        servers = self.servers_client.list_servers(
+            detail=True, reservation_id=reservation_id)['servers']
+        self.assertEqual(2, len(servers))
+
+        # Assert the servers are in the group.
+        server_group = self.server_groups_client.show_server_group(
+            group_id)['server_group']
+        hosts = {}
+        for server in servers:
+            self.assertIn(server['id'], server_group['members'])
+            hosts[server['id']] = self._get_host(server['id'])
+
+        return hosts
+
     @decorators.idempotent_id('26a9d5df-6890-45f2-abc4-a659290cb130')
     @testtools.skipUnless(
         compute.is_scheduler_filter_enabled("SameHostFilter"),
@@ -87,28 +109,8 @@
         Creates two servers in an anti-affinity server group and
         asserts the servers are in the group and on different hosts.
         """
-        group_id = self.create_test_server_group(
-            policy=['anti-affinity'])['id']
-        hints = {'group': group_id}
-        reservation_id = self.create_test_server(
-            scheduler_hints=hints, wait_until='ACTIVE', min_count=2,
-            return_reservation_id=True)['reservation_id']
-
-        # Get the servers using the reservation_id.
-        servers = self.servers_client.list_servers(
-            detail=True, reservation_id=reservation_id)['servers']
-        self.assertEqual(2, len(servers))
-
-        # Assert the servers are in the group.
-        server_group = self.server_groups_client.show_server_group(
-            group_id)['server_group']
-        hosts = {}
-        for server in servers:
-            self.assertIn(server['id'], server_group['members'])
-            hosts[server['id']] = self._get_host(server['id'])
-
-        # Assert the servers are on different hosts.
-        hostnames = list(hosts.values())
+        hosts = self._create_servers_with_group('anti-affinity')
+        hostnames = hosts.values()
         self.assertNotEqual(hostnames[0], hostnames[1],
                             'Servers are on the same host: %s' % hosts)
 
@@ -122,26 +124,7 @@
         Creates two servers in an affinity server group and
         asserts the servers are in the group and on same host.
         """
-        group_id = self.create_test_server_group(policy=['affinity'])['id']
-        hints = {'group': group_id}
-        reservation_id = self.create_test_server(
-            scheduler_hints=hints, wait_until='ACTIVE', min_count=2,
-            return_reservation_id=True)['reservation_id']
-
-        # Get the servers using the reservation_id.
-        servers = self.servers_client.list_servers(
-            detail=True, reservation_id=reservation_id)['servers']
-        self.assertEqual(2, len(servers))
-
-        # Assert the servers are in the group.
-        server_group = self.server_groups_client.show_server_group(
-            group_id)['server_group']
-        hosts = {}
-        for server in servers:
-            self.assertIn(server['id'], server_group['members'])
-            hosts[server['id']] = self._get_host(server['id'])
-
-        # Assert the servers are on same host.
+        hosts = self._create_servers_with_group('affinity')
         hostnames = hosts.values()
         self.assertEqual(hostnames[0], hostnames[1],
                          'Servers are on the different hosts: %s' % hosts)
diff --git a/tempest/api/compute/servers/test_attach_interfaces.py b/tempest/api/compute/servers/test_attach_interfaces.py
index 0e8f681..7509ac6 100644
--- a/tempest/api/compute/servers/test_attach_interfaces.py
+++ b/tempest/api/compute/servers/test_attach_interfaces.py
@@ -185,7 +185,7 @@
 
     @decorators.idempotent_id('73fe8f02-590d-4bf1-b184-e9ca81065051')
     @utils.services('network')
-    def test_create_list_show_delete_interfaces(self):
+    def test_create_list_show_delete_interfaces_by_network_port(self):
         server, ifs = self._create_server_get_interfaces()
         interface_count = len(ifs)
         self.assertGreater(interface_count, 0)
@@ -206,6 +206,42 @@
         iface = self._test_create_interface_by_port_id(server, ifs)
         ifs.append(iface)
 
+        _ifs = (self.interfaces_client.list_interfaces(server['id'])
+                ['interfaceAttachments'])
+        self._compare_iface_list(ifs, _ifs)
+
+        self._test_show_interface(server, ifs)
+
+        _ifs = self._test_delete_interface(server, ifs)
+        self.assertEqual(len(ifs) - 1, len(_ifs))
+
+    @decorators.idempotent_id('d290c06c-f5b3-11e7-8ec8-002293781009')
+    @utils.services('network')
+    def test_create_list_show_delete_interfaces_by_fixed_ip(self):
+        # NOTE(zhufl) By default only project that is admin or network owner
+        # or project with role advsvc is authorised to create interfaces with
+        # fixed-ip, so if we don't create network for each project, do not
+        # test _test_create_interface_by_fixed_ips.
+        if not (CONF.auth.use_dynamic_credentials and
+                CONF.auth.create_isolated_networks and
+                not CONF.network.shared_physical_network):
+                raise self.skipException("Only owner network supports "
+                                         "creating interface by fixed ip.")
+
+        server, ifs = self._create_server_get_interfaces()
+        interface_count = len(ifs)
+        self.assertGreater(interface_count, 0)
+
+        try:
+            iface = self._test_create_interface(server)
+        except lib_exc.BadRequest as e:
+            msg = ('Multiple possible networks found, use a Network ID to be '
+                   'more specific.')
+            if not CONF.compute.fixed_network_name and six.text_type(e) == msg:
+                raise
+        else:
+            ifs.append(iface)
+
         iface = self._test_create_interface_by_fixed_ips(server, ifs)
         ifs.append(iface)
 
diff --git a/tempest/api/image/v2/admin/test_images.py b/tempest/api/image/v2/admin/test_images.py
index 4ff0268..dbb8c58 100644
--- a/tempest/api/image/v2/admin/test_images.py
+++ b/tempest/api/image/v2/admin/test_images.py
@@ -31,3 +31,21 @@
         self.addCleanup(self.admin_client.delete_image, image['id'])
         image_info = self.admin_client.show_image(image['id'])
         self.assertEqual(random_id, image_info['owner'])
+
+    @decorators.related_bug('1420008')
+    @decorators.idempotent_id('525ba546-10ef-4aad-bba1-1858095ce553')
+    def test_update_image_owner_param(self):
+        random_id_1 = data_utils.rand_uuid_hex()
+        image = self.admin_client.create_image(
+            container_format='bare', disk_format='raw', owner=random_id_1)
+        self.addCleanup(self.admin_client.delete_image, image['id'])
+        created_image_info = self.admin_client.show_image(image['id'])
+
+        random_id_2 = data_utils.rand_uuid_hex()
+        self.admin_client.update_image(
+            image['id'], [dict(replace="/owner", value=random_id_2)])
+        updated_image_info = self.admin_client.show_image(image['id'])
+
+        self.assertEqual(random_id_2, updated_image_info['owner'])
+        self.assertNotEqual(created_image_info['owner'],
+                            updated_image_info['owner'])
diff --git a/tempest/cmd/run.py b/tempest/cmd/run.py
index 49fb1bc..72ee715 100644
--- a/tempest/cmd/run.py
+++ b/tempest/cmd/run.py
@@ -153,7 +153,7 @@
             if not os.path.isfile('.stestr.conf'):
                 self._create_stestr_conf()
         # local execution with config file mode
-        elif parsed_args.config_file:
+        elif parsed_args.config_file and not os.path.isfile('.stestr.conf'):
             self._create_stestr_conf()
         elif not os.path.isfile('.stestr.conf'):
             print("No .stestr.conf file was found for local execution")
@@ -164,13 +164,14 @@
             pass
 
         regex = self._build_regex(parsed_args)
+        return_code = 0
         if parsed_args.list_tests:
             return_code = commands.list_command(
                 filters=regex, whitelist_file=parsed_args.whitelist_file,
                 blacklist_file=parsed_args.blacklist_file,
                 black_regex=parsed_args.black_regex)
 
-        elif not (parsed_args.config_file or parsed_args.workspace):
+        else:
             serial = not parsed_args.parallel
             return_code = commands.run_command(
                 filters=regex, subunit_out=parsed_args.subunit,
diff --git a/tempest/config.py b/tempest/config.py
index 340a27e..8a6370a 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -609,10 +609,14 @@
                      " for subnet creation"),
     cfg.StrOpt('port_vnic_type',
                choices=[None, 'normal', 'direct', 'macvtap'],
-               help="vnic_type to use when Launching instances"
+               help="vnic_type to use when launching instances"
                     " with pre-configured ports."
                     " Supported ports are:"
                     " ['normal','direct','macvtap']"),
+    cfg.DictOpt('port_profile',
+                default={},
+                help="port profile to use when launching instances"
+                     " with pre-configured ports."),
     cfg.ListOpt('default_network',
                 default=["1.0.0.0/16", "2.0.0.0/16"],
                 help="List of ip pools"
diff --git a/tempest/scenario/manager.py b/tempest/scenario/manager.py
index 06aa531..ef277fb 100644
--- a/tempest/scenario/manager.py
+++ b/tempest/scenario/manager.py
@@ -139,13 +139,15 @@
             name = data_utils.rand_name(self.__class__.__name__ + "-server")
 
         vnic_type = CONF.network.port_vnic_type
+        profile = CONF.network.port_profile
 
         # If vnic_type is configured create port for
         # every network
         if vnic_type:
             ports = []
 
-            create_port_body = {'binding:vnic_type': vnic_type}
+            create_port_body = {'binding:vnic_type': vnic_type,
+                                'binding:profile': profile}
             if kwargs:
                 # Convert security group names to security group ids
                 # to pass to create_port
diff --git a/tempest/tests/cmd/test_run.py b/tempest/tests/cmd/test_run.py
index bc10eb7..6cc356e 100644
--- a/tempest/tests/cmd/test_run.py
+++ b/tempest/tests/cmd/test_run.py
@@ -140,6 +140,11 @@
         self.assertRunExit(['tempest', 'run', '--whitelist-file=%s' % path,
                             '--regex', 'fail'], 1)
 
+    def test_tempest_run_passes_with_config_file(self):
+        self.assertRunExit(['tempest', 'run',
+                            '--config-file', self.stestr_conf_file,
+                            '--regex', 'passing'], 0)
+
 
 class TestTakeAction(base.TestCase):
     def test_workspace_not_registered(self):
@@ -168,3 +173,27 @@
         self.assertRaises(Exception_, tempest_run.take_action, parsed_args)
         exit_msg = m_exit.call_args[0][0]
         self.assertIn(workspace, exit_msg)
+
+    def test_config_file_specified(self):
+        # Setup test dirs
+        self.directory = tempfile.mkdtemp(prefix='tempest-unit')
+        self.addCleanup(shutil.rmtree, self.directory)
+        self.test_dir = os.path.join(self.directory, 'tests')
+        os.mkdir(self.test_dir)
+        # Change directory, run wrapper and check result
+        self.addCleanup(os.chdir, os.path.abspath(os.curdir))
+        os.chdir(self.directory)
+
+        tempest_run = run.TempestRun(app=mock.Mock(), app_args=mock.Mock())
+        parsed_args = mock.Mock()
+        parsed_args.config_file = []
+
+        parsed_args.workspace = None
+        parsed_args.state = None
+        parsed_args.list_tests = False
+        parsed_args.config_file = '.stestr.conf'
+
+        with mock.patch('stestr.commands.run_command') as m:
+            m.return_value = 0
+            self.assertEqual(0, tempest_run.take_action(parsed_args))
+            m.assert_called()
diff --git a/tempest/tests/files/setup.cfg b/tempest/tests/files/setup.cfg
index f6f9f73..bd68708 100644
--- a/tempest/tests/files/setup.cfg
+++ b/tempest/tests/files/setup.cfg
@@ -4,7 +4,7 @@
 summary = Fake Project for testing wrapper scripts
 author = OpenStack
 author-email = openstack-dev@lists.openstack.org
-home-page = http://www.openstack.org/
+home-page = https://docs.openstack.org/tempest/latest/
 classifier =
     Intended Audience :: Information Technology
     Intended Audience :: System Administrators
diff --git a/tempest/tests/services/object_storage/test_object_client.py b/tempest/tests/services/object_storage/test_object_client.py
deleted file mode 100644
index 86535f9..0000000
--- a/tempest/tests/services/object_storage/test_object_client.py
+++ /dev/null
@@ -1,108 +0,0 @@
-# Copyright 2016 IBM Corp.
-# 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.
-
-
-import mock
-
-from tempest.lib import exceptions
-from tempest.services.object_storage import object_client
-from tempest.tests import base
-from tempest.tests.lib import fake_auth_provider
-
-
-class TestObjectClient(base.TestCase):
-
-    def setUp(self):
-        super(TestObjectClient, self).setUp()
-        self.fake_auth = fake_auth_provider.FakeAuthProvider()
-        self.url = self.fake_auth.base_url(None)
-        self.object_client = object_client.ObjectClient(self.fake_auth,
-                                                        'swift', 'region1')
-
-    @mock.patch.object(object_client, '_create_connection')
-    def test_create_object_continue_no_data(self, mock_poc):
-        self._validate_create_object_continue(None, mock_poc)
-
-    @mock.patch.object(object_client, '_create_connection')
-    def test_create_object_continue_with_data(self, mock_poc):
-        self._validate_create_object_continue('hello', mock_poc)
-
-    @mock.patch.object(object_client, '_create_connection')
-    def test_create_continue_with_no_continue_received(self, mock_poc):
-        self._validate_create_object_continue('hello', mock_poc,
-                                              initial_status=201)
-
-    def _validate_create_object_continue(self, req_data,
-                                         mock_poc, initial_status=100):
-
-        expected_hdrs = {
-            'X-Auth-Token': self.fake_auth.get_token(),
-            'content-length': 0 if req_data is None else len(req_data),
-            'Expect': '100-continue'}
-
-        # Setup the Mocks prior to invoking the object creation
-        mock_resp_cls = mock.Mock()
-        mock_resp_cls._read_status.return_value = ("1", initial_status, "OK")
-
-        mock_poc.return_value.response_class.return_value = mock_resp_cls
-
-        # This is the final expected return value
-        mock_poc.return_value.getresponse.return_value.status = 201
-        mock_poc.return_value.getresponse.return_value.reason = 'OK'
-
-        # Call method to PUT object using expect:100-continue
-        cnt = "container1"
-        obj = "object1"
-        path = "/%s/%s" % (cnt, obj)
-
-        # If the expected initial status is not 100, then an exception
-        # should be thrown and the connection closed
-        if initial_status is 100:
-            status, reason = \
-                self.object_client.create_object_continue(cnt, obj, req_data)
-        else:
-            self.assertRaises(exceptions.UnexpectedResponseCode,
-                              self.object_client.create_object_continue, cnt,
-                              obj, req_data)
-            mock_poc.return_value.close.assert_called_once_with()
-
-        # Verify that putrequest is called 1 time with the appropriate values
-        mock_poc.return_value.putrequest.assert_called_once_with('PUT', path)
-
-        # Verify that headers were written, including "Expect:100-continue"
-        calls = []
-
-        for header, value in expected_hdrs.items():
-            calls.append(mock.call(header, value))
-
-        mock_poc.return_value.putheader.assert_has_calls(calls, False)
-        mock_poc.return_value.endheaders.assert_called_once_with()
-
-        # The following steps are only taken if the initial status is 100
-        if initial_status is 100:
-            # Verify that the method returned what it was supposed to
-            self.assertEqual(status, 201)
-
-            # Verify that _safe_read was called once to remove the CRLF
-            # after the 100 response
-            mock_rc = mock_poc.return_value.response_class.return_value
-            mock_rc._safe_read.assert_called_once_with(2)
-
-            # Verify the actual data was written via send
-            mock_poc.return_value.send.assert_called_once_with(req_data)
-
-            # Verify that the getresponse method was called to receive
-            # the final
-            mock_poc.return_value.getresponse.assert_called_once_with()
diff --git a/tox.ini b/tox.ini
index 5644641..9103175 100644
--- a/tox.ini
+++ b/tox.ini
@@ -146,7 +146,7 @@
   -r{toxinidir}/doc/requirements.txt
 commands =
   rm -rf doc/build
-  sphinx-build -b html doc/source doc/build/html
+  sphinx-build -W -b html doc/source doc/build/html
 whitelist_externals = rm
 
 [testenv:pep8]