Merge "Increase test coverage on preprov creds"
diff --git a/HACKING.rst b/HACKING.rst
index ec7ff6a..480650c 100644
--- a/HACKING.rst
+++ b/HACKING.rst
@@ -21,6 +21,7 @@
 - [T111] Check that service client names of DELETE should be consistent
 - [T112] Check that tempest.lib should not import local tempest code
 - [T113] Check that tests use data_utils.rand_uuid() instead of uuid.uuid4()
+- [T114] Check that tempest.lib does not use tempest config
 - [N322] Method's default argument shouldn't be mutable
 
 Test Data/Configuration
diff --git a/README.rst b/README.rst
index 650a1ed..725a890 100644
--- a/README.rst
+++ b/README.rst
@@ -25,8 +25,7 @@
   discover features of a cloud incorrectly, and give people an
   incorrect assessment of their cloud. Explicit is always better.
 - Tempest uses OpenStack public interfaces. Tests in Tempest should
-  only touch public interfaces, API calls (native or 3rd party),
-  or libraries.
+  only touch public OpenStack APIs.
 - Tempest should not touch private or implementation specific
   interfaces. This means not directly going to the database, not
   directly hitting the hypervisors, not testing extensions not
@@ -163,7 +162,7 @@
 Tempest also has a set of unit tests which test the Tempest code itself. These
 tests can be run by specifying the test discovery path::
 
-    $> OS_TEST_PATH=./tempest/tests testr run --parallel
+    $ OS_TEST_PATH=./tempest/tests testr run --parallel
 
 By setting OS_TEST_PATH to ./tempest/tests it specifies that test discover
 should only be run on the unit test directory. The default value of OS_TEST_PATH
@@ -214,8 +213,8 @@
 To start you need to create a configuration file. The easiest way to create a
 configuration file is to generate a sample in the ``etc/`` directory ::
 
-    $> cd $TEMPEST_ROOT_DIR
-    $> oslo-config-generator --config-file \
+    $ cd $TEMPEST_ROOT_DIR
+    $ oslo-config-generator --config-file \
         etc/config-generator.tempest.conf \
         --output-file etc/tempest.conf
 
@@ -237,21 +236,21 @@
 After setting up your configuration file, you can execute the set of Tempest
 tests by using ``testr`` ::
 
-    $> testr run --parallel
+    $ testr run --parallel
 
 To run one single test serially ::
 
-    $> testr run tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_reboot_non_existent_server
+    $ testr run tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_reboot_non_existent_server
 
 Alternatively, you can use the run_tempest.sh script which will create a venv
 and run the tests or use tox to do the same. Tox also contains several existing
 job configurations. For example::
 
-   $> tox -efull
+   $ tox -efull
 
 which will run the same set of tests as the OpenStack gate. (it's exactly how
 the gate invokes Tempest) Or::
 
-  $> tox -esmoke
+  $ tox -esmoke
 
 to run the tests tagged as smoke.
diff --git a/releasenotes/notes/remove-integrated-horizon-bb57551c1e5f5be3.yaml b/releasenotes/notes/remove-integrated-horizon-bb57551c1e5f5be3.yaml
new file mode 100644
index 0000000..294f6d9
--- /dev/null
+++ b/releasenotes/notes/remove-integrated-horizon-bb57551c1e5f5be3.yaml
@@ -0,0 +1,7 @@
+---
+upgrade:
+  - The integrated dashboard scenario test has been
+    removed and is now in a separate tempest plugin
+    tempest-horizon. The removed test coverage can be
+    used by installing tempest-horizon on the server
+    where you run tempest.
diff --git a/tempest/api/volume/admin/test_volumes_backup.py b/tempest/api/volume/admin/test_volumes_backup.py
index b09cd2c..1289297 100644
--- a/tempest/api/volume/admin/test_volumes_backup.py
+++ b/tempest/api/volume/admin/test_volumes_backup.py
@@ -13,11 +13,15 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+import base64
+import six
+
+from oslo_serialization import jsonutils as json
+
 from tempest.api.volume import base
 from tempest.common.utils import data_utils
 from tempest.common import waiters
 from tempest import config
-from tempest.lib import decorators
 from tempest import test
 
 CONF = config.CONF
@@ -41,6 +45,20 @@
         self.backups_adm_client.delete_backup(backup_id)
         self.backups_adm_client.wait_for_backup_deletion(backup_id)
 
+    def _decode_url(self, backup_url):
+        return json.loads(base64.decodestring(backup_url))
+
+    def _encode_backup(self, backup):
+        retval = json.dumps(backup)
+        if six.PY3:
+            retval = retval.encode('utf-8')
+        return base64.encodestring(retval)
+
+    def _modify_backup_url(self, backup_url, changes):
+        backup = self._decode_url(backup_url)
+        backup.update(changes)
+        return self._encode_backup(backup)
+
     @test.idempotent_id('a66eb488-8ee1-47d4-8e9f-575a095728c6')
     def test_volume_backup_create_get_detailed_list_restore_delete(self):
         # Create backup
@@ -78,9 +96,13 @@
         waiters.wait_for_volume_status(self.admin_volume_client,
                                        restore['volume_id'], 'available')
 
-    @decorators.skip_because(bug='1455043')
     @test.idempotent_id('a99c54a1-dd80-4724-8a13-13bf58d4068d')
     def test_volume_backup_export_import(self):
+        """Test backup export import functionality.
+
+        Cinder allows exporting DB backup information through its API so it can
+        be imported back in case of a DB loss.
+        """
         # Create backup
         backup_name = data_utils.rand_name('Backup')
         backup = (self.backups_adm_client.create_backup(
@@ -99,25 +121,40 @@
                         'cinder.backup.drivers'))
         self.assertIsNotNone(export_backup['backup_url'])
 
+        # NOTE(geguileo): Backups are imported with the same backup id
+        # (important for incremental backups among other things), so we cannot
+        # import the exported backup information as it is, because that Backup
+        # ID already exists.  So we'll fake the data by changing the backup id
+        # in the exported backup DB info we have retrieved before importing it
+        # back.
+        new_id = data_utils.rand_uuid()
+        new_url = self._modify_backup_url(
+            export_backup['backup_url'], {'id': new_id})
+
         # Import Backup
         import_backup = self.backups_adm_client.import_backup(
             backup_service=export_backup['backup_service'],
-            backup_url=export_backup['backup_url'])['backup']
-        self.addCleanup(self._delete_backup, import_backup['id'])
+            backup_url=new_url)['backup']
+
+        # NOTE(geguileo): We delete both backups, but only one of those
+        # deletions will delete data from the backup back-end because they
+        # were both pointing to the same backend data.
+        self.addCleanup(self._delete_backup, new_id)
         self.assertIn("id", import_backup)
+        self.assertEqual(new_id, import_backup['id'])
         self.backups_adm_client.wait_for_backup_status(import_backup['id'],
                                                        'available')
 
         # Verify Import Backup
         backups = self.backups_adm_client.list_backups(detail=True)['backups']
-        self.assertIn(import_backup['id'], [b['id'] for b in backups])
+        self.assertIn(new_id, [b['id'] for b in backups])
 
         # Restore backup
-        restore = (self.backups_adm_client.restore_backup(import_backup['id'])
-                   ['restore'])
+        restore = self.backups_adm_client.restore_backup(
+            backup['id'])['restore']
         self.addCleanup(self.admin_volume_client.delete_volume,
                         restore['volume_id'])
-        self.assertEqual(import_backup['id'], restore['backup_id'])
+        self.assertEqual(backup['id'], restore['backup_id'])
         waiters.wait_for_volume_status(self.admin_volume_client,
                                        restore['volume_id'], 'available')
 
diff --git a/tempest/common/dynamic_creds.py b/tempest/common/dynamic_creds.py
index e5d65f5..3a3e3c2 100644
--- a/tempest/common/dynamic_creds.py
+++ b/tempest/common/dynamic_creds.py
@@ -122,7 +122,9 @@
         project = self.creds_client.create_project(
             name=project_name, description=project_desc)
 
-        username = data_utils.rand_name(root) + suffix
+        # NOTE(andreaf) User and project can be distinguished from the context,
+        # having the same ID in both makes it easier to match them and debug.
+        username = project_name
         user_password = data_utils.rand_password()
         email = data_utils.rand_name(root) + suffix + "@example.com"
         user = self.creds_client.create_user(
diff --git a/tempest/config.py b/tempest/config.py
index 71b25d0..3810ceb 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -841,21 +841,6 @@
                help="Value must match heat configuration of the same name."),
 ]
 
-
-dashboard_group = cfg.OptGroup(name="dashboard",
-                               title="Dashboard options")
-
-DashboardGroup = [
-    cfg.StrOpt('dashboard_url',
-               default='http://localhost/',
-               help="Where the dashboard can be found"),
-    cfg.StrOpt('login_url',
-               default='http://localhost/auth/login/',
-               help="Login page for the dashboard",
-               deprecated_for_removal=True),
-]
-
-
 data_processing_group = cfg.OptGroup(name="data-processing",
                                      title="Data Processing options")
 
@@ -986,9 +971,6 @@
     cfg.BoolOpt('heat',
                 default=False,
                 help="Whether or not Heat is expected to be available"),
-    cfg.BoolOpt('horizon',
-                default=True,
-                help="Whether or not Horizon is expected to be available"),
     cfg.BoolOpt('sahara',
                 default=False,
                 help="Whether or not Sahara is expected to be available"),
@@ -1131,7 +1113,6 @@
     (object_storage_feature_group, ObjectStoreFeaturesGroup),
     (database_group, DatabaseGroup),
     (orchestration_group, OrchestrationGroup),
-    (dashboard_group, DashboardGroup),
     (data_processing_group, DataProcessingGroup),
     (data_processing_feature_group, DataProcessingFeaturesGroup),
     (stress_group, StressGroup),
@@ -1199,7 +1180,6 @@
             'object-storage-feature-enabled']
         self.database = _CONF.database
         self.orchestration = _CONF.orchestration
-        self.dashboard = _CONF.dashboard
         self.data_processing = _CONF['data-processing']
         self.data_processing_feature_enabled = _CONF[
             'data-processing-feature-enabled']
diff --git a/tempest/hacking/checks.py b/tempest/hacking/checks.py
index aff9dee..09106d1 100644
--- a/tempest/hacking/checks.py
+++ b/tempest/hacking/checks.py
@@ -256,6 +256,23 @@
     yield (0, msg)
 
 
+def dont_use_config_in_tempest_lib(logical_line, filename):
+    """Check that tempest.lib doesn't use tempest config
+
+    T114
+    """
+
+    if 'tempest/lib/' not in filename:
+        return
+
+    if ('tempest.config' in logical_line
+        or 'from tempest import config' in logical_line
+        or 'oslo_config' in logical_line):
+        msg = ('T114: tempest.lib can not have any dependency on tempest '
+               'config.')
+        yield(0, msg)
+
+
 def factory(register):
     register(import_no_clients_in_api_and_scenario_tests)
     register(scenario_tests_need_service_tags)
@@ -268,4 +285,5 @@
     register(get_resources_on_service_clients)
     register(delete_resources_on_service_clients)
     register(dont_import_local_tempest_into_lib)
+    register(dont_use_config_in_tempest_lib)
     register(use_rand_uuid_instead_of_uuid4)
diff --git a/tempest/lib/common/rest_client.py b/tempest/lib/common/rest_client.py
index 179db17..627143d 100644
--- a/tempest/lib/common/rest_client.py
+++ b/tempest/lib/common/rest_client.py
@@ -221,6 +221,10 @@
         :raises exceptions.InvalidHttpSuccessCode: if the read code isn't an
                                                    expected http success code
         """
+        if not isinstance(read_code, int):
+            raise TypeError("'read_code' must be an int instead of (%s)"
+                            % type(read_code))
+
         assert_msg = ("This function only allowed to use for HTTP status"
                       "codes which explicitly defined in the RFC 7231 & 4918."
                       "{0} is not a defined Success Code!"
diff --git a/tempest/scenario/test_dashboard_basic_ops.py b/tempest/scenario/test_dashboard_basic_ops.py
deleted file mode 100644
index 5d4f7b3..0000000
--- a/tempest/scenario/test_dashboard_basic_ops.py
+++ /dev/null
@@ -1,120 +0,0 @@
-# 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 six.moves import html_parser as HTMLParser
-from six.moves.urllib import parse
-from six.moves.urllib import request
-
-from tempest import config
-from tempest.scenario import manager
-from tempest import test
-
-CONF = config.CONF
-
-
-class HorizonHTMLParser(HTMLParser.HTMLParser):
-    csrf_token = None
-    region = None
-    login = None
-
-    def _find_name(self, attrs, name):
-        for attrpair in attrs:
-            if attrpair[0] == 'name' and attrpair[1] == name:
-                return True
-        return False
-
-    def _find_value(self, attrs):
-        for attrpair in attrs:
-            if attrpair[0] == 'value':
-                return attrpair[1]
-        return None
-
-    def _find_attr_value(self, attrs, attr_name):
-        for attrpair in attrs:
-            if attrpair[0] == attr_name:
-                return attrpair[1]
-        return None
-
-    def handle_starttag(self, tag, attrs):
-        if tag == 'input':
-            if self._find_name(attrs, 'csrfmiddlewaretoken'):
-                self.csrf_token = self._find_value(attrs)
-            if self._find_name(attrs, 'region'):
-                self.region = self._find_value(attrs)
-        if tag == 'form':
-            self.login = self._find_attr_value(attrs, 'action')
-
-
-class TestDashboardBasicOps(manager.ScenarioTest):
-
-    """The test suite for dashboard basic operations
-
-    This is a basic scenario test:
-    * checks that the login page is available
-    * logs in as a regular user
-    * checks that the user home page loads without error
-    """
-
-    @classmethod
-    def skip_checks(cls):
-        super(TestDashboardBasicOps, cls).skip_checks()
-        if not CONF.service_available.horizon:
-            raise cls.skipException("Horizon support is required")
-
-    @classmethod
-    def setup_credentials(cls):
-        cls.set_network_resources()
-        super(TestDashboardBasicOps, cls).setup_credentials()
-
-    def check_login_page(self):
-        response = request.urlopen(CONF.dashboard.dashboard_url)
-        self.assertIn("id_username", response.read())
-
-    def user_login(self, username, password):
-        self.opener = request.build_opener(request.HTTPCookieProcessor())
-        response = self.opener.open(CONF.dashboard.dashboard_url).read()
-
-        # Grab the CSRF token and default region
-        parser = HorizonHTMLParser()
-        parser.feed(response)
-
-        # construct login url for dashboard, discovery accommodates non-/ web
-        # root for dashboard
-        login_url = parse.urljoin(CONF.dashboard.dashboard_url, parser.login)
-
-        # Prepare login form request
-        req = request.Request(login_url)
-        req.add_header('Content-type', 'application/x-www-form-urlencoded')
-        req.add_header('Referer', CONF.dashboard.dashboard_url)
-
-        # Pass the default domain name regardless of the auth version in order
-        # to test the scenario of when horizon is running with keystone v3
-        params = {'username': username,
-                  'password': password,
-                  'region': parser.region,
-                  'domain': CONF.auth.default_credentials_domain_name,
-                  'csrfmiddlewaretoken': parser.csrf_token}
-        self.opener.open(req, parse.urlencode(params))
-
-    def check_home_page(self):
-        response = self.opener.open(CONF.dashboard.dashboard_url)
-        self.assertIn('Overview', response.read())
-
-    @test.idempotent_id('4f8851b1-0e69-482b-b63b-84c6e76f6c80')
-    @test.services('dashboard')
-    def test_basic_scenario(self):
-        creds = self.os.credentials
-        self.check_login_page()
-        self.user_login(creds.username, creds.password)
-        self.check_home_page()
diff --git a/tempest/test.py b/tempest/test.py
index aefe1a9..d31c509 100644
--- a/tempest/test.py
+++ b/tempest/test.py
@@ -77,7 +77,6 @@
         'network': True,
         'identity': True,
         'object_storage': CONF.service_available.swift,
-        'dashboard': CONF.service_available.horizon,
         'data_processing': CONF.service_available.sahara,
         'database': CONF.service_available.trove
     }
@@ -92,8 +91,8 @@
     """
     def decorator(f):
         services = ['compute', 'image', 'baremetal', 'volume', 'orchestration',
-                    'network', 'identity', 'object_storage', 'dashboard',
-                    'data_processing', 'database']
+                    'network', 'identity', 'object_storage', 'data_processing',
+                    'database']
         for service in args:
             if service not in services:
                 raise exceptions.InvalidServiceTag('%s is not a valid '
diff --git a/tempest/tests/lib/test_rest_client.py b/tempest/tests/lib/test_rest_client.py
index 2a6fad5..106a1e5 100644
--- a/tempest/tests/lib/test_rest_client.py
+++ b/tempest/tests/lib/test_rest_client.py
@@ -693,6 +693,24 @@
         self.assertRaises(AssertionError, self.rest_client.expected_success,
                           expected_code, read_code)
 
+    def test_non_success_read_code_as_string(self):
+        expected_code = 202
+        read_code = '202'
+        self.assertRaises(TypeError, self.rest_client.expected_success,
+                          expected_code, read_code)
+
+    def test_non_success_read_code_as_list(self):
+        expected_code = 202
+        read_code = [202]
+        self.assertRaises(TypeError, self.rest_client.expected_success,
+                          expected_code, read_code)
+
+    def test_non_success_expected_code_as_non_int(self):
+        expected_code = ['201', 202]
+        read_code = 202
+        self.assertRaises(AssertionError, self.rest_client.expected_success,
+                          expected_code, read_code)
+
 
 class TestResponseBody(base.TestCase):
 
diff --git a/tempest/tests/test_hacking.py b/tempest/tests/test_hacking.py
index aba2aab..f005c21 100644
--- a/tempest/tests/test_hacking.py
+++ b/tempest/tests/test_hacking.py
@@ -167,3 +167,16 @@
         self.assertEqual(1, len(list(checks.dont_import_local_tempest_into_lib(
             "import tempest.exception",
             './tempest/lib/common/compute.py'))))
+
+    def test_dont_use_config_in_tempest_lib(self):
+        self.assertFalse(list(checks.dont_use_config_in_tempest_lib(
+            'from tempest import config', './tempest/common/compute.py')))
+        self.assertFalse(list(checks.dont_use_config_in_tempest_lib(
+            'from oslo_concurrency import lockutils',
+            './tempest/lib/auth.py')))
+        self.assertTrue(list(checks.dont_use_config_in_tempest_lib(
+            'from tempest import config', './tempest/lib/auth.py')))
+        self.assertTrue(list(checks.dont_use_config_in_tempest_lib(
+            'from oslo_config import cfg', './tempest/lib/decorators.py')))
+        self.assertTrue(list(checks.dont_use_config_in_tempest_lib(
+            'import tempest.config', './tempest/lib/common/rest_client.py')))
diff --git a/tools/check_logs.py b/tools/check_logs.py
index e34dec3..caad85c 100755
--- a/tools/check_logs.py
+++ b/tools/check_logs.py
@@ -20,8 +20,8 @@
 import os
 import re
 import six
+import six.moves.urllib.request as urlreq
 import sys
-import urllib2
 
 import yaml
 
@@ -67,9 +67,9 @@
                 logs_with_errors.append(name)
     for (name, url) in url_specs:
         whitelist = whitelists.get(name, [])
-        req = urllib2.Request(url)
+        req = urlreq.Request(url)
         req.add_header('Accept-Encoding', 'gzip')
-        page = urllib2.urlopen(req)
+        page = urlreq.urlopen(req)
         buf = six.StringIO(page.read())
         f = gzip.GzipFile(fileobj=buf)
         if scan_content(name, f.read().splitlines(), regexp, whitelist):
@@ -95,7 +95,7 @@
 
 
 def collect_url_logs(url):
-    page = urllib2.urlopen(url)
+    page = urlreq.urlopen(url)
     content = page.read()
     logs = re.findall('(screen-[\w-]+\.txt\.gz)</a>', content)
     return logs
diff --git a/tools/find_stack_traces.py b/tools/find_stack_traces.py
index 49a42fe..f2da27a 100755
--- a/tools/find_stack_traces.py
+++ b/tools/find_stack_traces.py
@@ -19,8 +19,8 @@
 import pprint
 import re
 import six
+import six.moves.urllib.request as urlreq
 import sys
-import urllib2
 
 
 pp = pprint.PrettyPrinter()
@@ -65,9 +65,9 @@
 
 def hunt_for_stacktrace(url):
     """Return TRACE or ERROR lines out of logs."""
-    req = urllib2.Request(url)
+    req = urlreq.Request(url)
     req.add_header('Accept-Encoding', 'gzip')
-    page = urllib2.urlopen(req)
+    page = urlreq.urlopen(req)
     buf = six.StringIO(page.read())
     f = gzip.GzipFile(fileobj=buf)
     content = f.read()
@@ -105,7 +105,7 @@
 
 
 def collect_logs(url):
-    page = urllib2.urlopen(url)
+    page = urlreq.urlopen(url)
     content = page.read()
     logs = re.findall('(screen-[\w-]+\.txt\.gz)</a>', content)
     return logs