Merge "test_encrypted_cinder_volumes: Do not create an image for this test"
diff --git a/releasenotes/notes/merge-tempest-horizon-plugin-39d555339ab8c7ce.yaml b/releasenotes/notes/merge-tempest-horizon-plugin-39d555339ab8c7ce.yaml
new file mode 100644
index 0000000..ff406fb
--- /dev/null
+++ b/releasenotes/notes/merge-tempest-horizon-plugin-39d555339ab8c7ce.yaml
@@ -0,0 +1,6 @@
+---
+prelude: >
+    The integrated horizon dashboard test is now moved
+    from tempest-horizon plugin into Tempest. You do not need
+    to install tempest-horizon to run the horizon test which
+    can be run using Tempest itself.
diff --git a/releasenotes/notes/random-bytes-size-limit-ee94a8c6534fe916.yaml b/releasenotes/notes/random-bytes-size-limit-ee94a8c6534fe916.yaml
new file mode 100644
index 0000000..42322e4
--- /dev/null
+++ b/releasenotes/notes/random-bytes-size-limit-ee94a8c6534fe916.yaml
@@ -0,0 +1,9 @@
+---
+upgrade:
+  - |
+    The ``tempest.lib.common.utils.data_utils.random_bytes()`` helper
+    function will no longer allow a ``size`` of more than 1MiB. Tests
+    generally do not need to generate and use large payloads for
+    feature verification and it is easy to lose track of and duplicate
+    large buffers. The sum total of such errors can become problematic
+    in paralllelized and constrained CI environments.
diff --git a/tempest/api/identity/admin/v3/test_roles.py b/tempest/api/identity/admin/v3/test_roles.py
index dd7d5af..e5137f4 100644
--- a/tempest/api/identity/admin/v3/test_roles.py
+++ b/tempest/api/identity/admin/v3/test_roles.py
@@ -142,6 +142,26 @@
         self.roles_client.delete_role_from_user_on_domain(
             self.domain['id'], self.user_body['id'], self.role['id'])
 
+    @testtools.skipIf(CONF.identity_feature_enabled.immutable_user_source,
+                      'Skipped because environment has an immutable user '
+                      'source and solely provides read-only access to users.')
+    @decorators.idempotent_id('e5a81737-d294-424d-8189-8664858aae4c')
+    def test_grant_list_revoke_role_to_user_on_system(self):
+        self.roles_client.create_user_role_on_system(
+            self.user_body['id'], self.role['id'])
+
+        roles = self.roles_client.list_user_roles_on_system(
+            self.user_body['id'])['roles']
+
+        self.assertEqual(1, len(roles))
+        self.assertEqual(self.role['id'], roles[0]['id'])
+
+        self.roles_client.check_user_role_existence_on_system(
+            self.user_body['id'], self.role['id'])
+
+        self.roles_client.delete_role_from_user_on_system(
+            self.user_body['id'], self.role['id'])
+
     @decorators.idempotent_id('cbf11737-1904-4690-9613-97bcbb3df1c4')
     @testtools.skipIf(CONF.identity_feature_enabled.immutable_user_source,
                       'Skipped because environment has an immutable user '
@@ -197,6 +217,23 @@
         self.roles_client.delete_role_from_group_on_domain(
             self.domain['id'], self.group_body['id'], self.role['id'])
 
+    @decorators.idempotent_id('c888fe4f-8018-48db-b959-542225c1b4b6')
+    def test_grant_list_revoke_role_to_group_on_system(self):
+        self.roles_client.create_group_role_on_system(
+            self.group_body['id'], self.role['id'])
+
+        roles = self.roles_client.list_group_roles_on_system(
+            self.group_body['id'])['roles']
+
+        self.assertEqual(1, len(roles))
+        self.assertEqual(self.role['id'], roles[0]['id'])
+
+        self.roles_client.check_role_from_group_on_system_existence(
+            self.group_body['id'], self.role['id'])
+
+        self.roles_client.delete_role_from_group_on_system(
+            self.group_body['id'], self.role['id'])
+
     @decorators.idempotent_id('f5654bcc-08c4-4f71-88fe-05d64e06de94')
     def test_list_roles(self):
         """Test listing roles"""
diff --git a/tempest/common/utils/__init__.py b/tempest/common/utils/__init__.py
index 914acf7..38881ee 100644
--- a/tempest/common/utils/__init__.py
+++ b/tempest/common/utils/__init__.py
@@ -59,6 +59,7 @@
         # So we should set this True here.
         'identity': True,
         'object_storage': CONF.service_available.swift,
+        'dashboard': CONF.service_available.horizon,
     }
     return service_list
 
diff --git a/tempest/config.py b/tempest/config.py
index 7c4539c..956b593 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -845,6 +845,18 @@
                     'This value will be increased in case of conflict.')
 ]
 
+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.BoolOpt('disable_ssl_certificate_validation',
+                default=False,
+                help="Set to True if using self-signed SSL certificates."),
+]
+
 validation_group = cfg.OptGroup(name='validation',
                                 title='SSH Validation options')
 
@@ -1190,6 +1202,9 @@
     cfg.BoolOpt('nova',
                 default=True,
                 help="Whether or not nova is expected to be available"),
+    cfg.BoolOpt('horizon',
+                default=True,
+                help="Whether or not horizon is expected to be available"),
 ]
 
 debug_group = cfg.OptGroup(name="debug",
@@ -1253,6 +1268,7 @@
     (image_feature_group, ImageFeaturesGroup),
     (network_group, NetworkGroup),
     (network_feature_group, NetworkFeaturesGroup),
+    (dashboard_group, DashboardGroup),
     (validation_group, ValidationGroup),
     (volume_group, VolumeGroup),
     (volume_feature_group, VolumeFeaturesGroup),
@@ -1320,6 +1336,7 @@
         self.image_feature_enabled = _CONF['image-feature-enabled']
         self.network = _CONF.network
         self.network_feature_enabled = _CONF['network-feature-enabled']
+        self.dashboard = _CONF.dashboard
         self.validation = _CONF.validation
         self.volume = _CONF.volume
         self.volume_feature_enabled = _CONF['volume-feature-enabled']
diff --git a/tempest/lib/common/dynamic_creds.py b/tempest/lib/common/dynamic_creds.py
index 0a2817b..d9f4f46 100644
--- a/tempest/lib/common/dynamic_creds.py
+++ b/tempest/lib/common/dynamic_creds.py
@@ -144,9 +144,9 @@
             # need a different token scope for them.
             if self.default_admin_creds.system:
                 scope = 'system'
-            elif (self.default_admin_creds.domain_id or
-                    self.default_admin_creds.domain_name or
-                    self.identity_admin_domain_scope):
+            elif (self.identity_admin_domain_scope and
+                  (self.default_admin_creds.domain_id or
+                   self.default_admin_creds.domain_name)):
                 scope = 'domain'
             else:
                 scope = 'project'
diff --git a/tempest/lib/common/utils/data_utils.py b/tempest/lib/common/utils/data_utils.py
index 44b55eb..b6671b5 100644
--- a/tempest/lib/common/utils/data_utils.py
+++ b/tempest/lib/common/utils/data_utils.py
@@ -169,6 +169,8 @@
     :return: size randomly bytes
     :rtype: string
     """
+    if size > 1 << 20:
+        raise RuntimeError('Size should be less than 1MiB')
     return b''.join([six.int2byte(random.randint(0, 255))
                      for i in range(size)])
 
diff --git a/tempest/lib/services/identity/v3/roles_client.py b/tempest/lib/services/identity/v3/roles_client.py
index 0d7593a..e41dc28 100644
--- a/tempest/lib/services/identity/v3/roles_client.py
+++ b/tempest/lib/services/identity/v3/roles_client.py
@@ -89,6 +89,13 @@
         self.expected_success(204, resp.status)
         return rest_client.ResponseBody(resp, body)
 
+    def create_user_role_on_system(self, user_id, role_id):
+        """Add roles to a user on the system."""
+        resp, body = self.put('system/users/%s/roles/%s' %
+                              (user_id, role_id), None)
+        self.expected_success(204, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
     def list_user_roles_on_project(self, project_id, user_id):
         """list roles of a user on a project."""
         resp, body = self.get('projects/%s/users/%s/roles' %
@@ -105,6 +112,13 @@
         body = json.loads(body)
         return rest_client.ResponseBody(resp, body)
 
+    def list_user_roles_on_system(self, user_id):
+        """list roles of a user on the system."""
+        resp, body = self.get('system/users/%s/roles' % user_id)
+        self.expected_success(200, resp.status)
+        body = json.loads(body)
+        return rest_client.ResponseBody(resp, body)
+
     def delete_role_from_user_on_project(self, project_id, user_id, role_id):
         """Delete role of a user on a project."""
         resp, body = self.delete('projects/%s/users/%s/roles/%s' %
@@ -119,6 +133,13 @@
         self.expected_success(204, resp.status)
         return rest_client.ResponseBody(resp, body)
 
+    def delete_role_from_user_on_system(self, user_id, role_id):
+        """Delete role of a user on the system."""
+        resp, body = self.delete('system/users/%s/roles/%s' %
+                                 (user_id, role_id))
+        self.expected_success(204, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
     def check_user_role_existence_on_project(self, project_id,
                                              user_id, role_id):
         """Check role of a user on a project."""
@@ -135,6 +156,12 @@
         self.expected_success(204, resp.status)
         return rest_client.ResponseBody(resp)
 
+    def check_user_role_existence_on_system(self, user_id, role_id):
+        """Check role of a user on the system."""
+        resp, body = self.head('system/users/%s/roles/%s' % (user_id, role_id))
+        self.expected_success(204, resp.status)
+        return rest_client.ResponseBody(resp)
+
     def create_group_role_on_project(self, project_id, group_id, role_id):
         """Add roles to a group on a project."""
         resp, body = self.put('projects/%s/groups/%s/roles/%s' %
@@ -149,6 +176,13 @@
         self.expected_success(204, resp.status)
         return rest_client.ResponseBody(resp, body)
 
+    def create_group_role_on_system(self, group_id, role_id):
+        """Add roles to a group on the system."""
+        resp, body = self.put('system/groups/%s/roles/%s' %
+                              (group_id, role_id), None)
+        self.expected_success(204, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
     def list_group_roles_on_project(self, project_id, group_id):
         """list roles of a group on a project."""
         resp, body = self.get('projects/%s/groups/%s/roles' %
@@ -165,6 +199,13 @@
         body = json.loads(body)
         return rest_client.ResponseBody(resp, body)
 
+    def list_group_roles_on_system(self, group_id):
+        """list roles of a group on the system."""
+        resp, body = self.get('system/groups/%s/roles' % group_id)
+        self.expected_success(200, resp.status)
+        body = json.loads(body)
+        return rest_client.ResponseBody(resp, body)
+
     def delete_role_from_group_on_project(self, project_id, group_id, role_id):
         """Delete role of a group on a project."""
         resp, body = self.delete('projects/%s/groups/%s/roles/%s' %
@@ -179,6 +220,13 @@
         self.expected_success(204, resp.status)
         return rest_client.ResponseBody(resp, body)
 
+    def delete_role_from_group_on_system(self, group_id, role_id):
+        """Delete role of a group on the system."""
+        resp, body = self.delete('system/groups/%s/roles/%s' %
+                                 (group_id, role_id))
+        self.expected_success(204, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
     def check_role_from_group_on_project_existence(self, project_id,
                                                    group_id, role_id):
         """Check role of a group on a project."""
@@ -195,6 +243,13 @@
         self.expected_success(204, resp.status)
         return rest_client.ResponseBody(resp)
 
+    def check_role_from_group_on_system_existence(self, group_id, role_id):
+        """Check role of a group on the system."""
+        resp, body = self.head('system/groups/%s/roles/%s' %
+                               (group_id, role_id))
+        self.expected_success(204, resp.status)
+        return rest_client.ResponseBody(resp)
+
     def create_role_inference_rule(self, prior_role, implies_role):
         """Create a role inference rule."""
         resp, body = self.put('roles/%s/implies/%s' %
diff --git a/tempest/scenario/test_dashboard_basic_ops.py b/tempest/scenario/test_dashboard_basic_ops.py
new file mode 100644
index 0000000..b1098fa
--- /dev/null
+++ b/tempest/scenario/test_dashboard_basic_ops.py
@@ -0,0 +1,141 @@
+#    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 html.parser
+import ssl
+from urllib import parse
+from urllib import request
+
+from tempest.common import utils
+from tempest import config
+from tempest.lib import decorators
+from tempest import test
+
+CONF = config.CONF
+
+
+class HorizonHTMLParser(html.parser.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(test.BaseTestCase):
+
+    """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
+    """
+    opener = None
+
+    credentials = ['primary']
+
+    @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 = self._get_opener().open(CONF.dashboard.dashboard_url).read()
+        self.assertIn("id_username", response.decode("utf-8"))
+
+    def user_login(self, username, password):
+        response = self._get_opener().open(CONF.dashboard.dashboard_url).read()
+
+        # Grab the CSRF token and default region
+        parser = HorizonHTMLParser()
+        parser.feed(response.decode("utf-8"))
+
+        # 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._get_opener().open(req, parse.urlencode(params).encode())
+
+    def check_home_page(self):
+        response = self._get_opener().open(CONF.dashboard.dashboard_url).read()
+        self.assertIn('Overview', response.decode("utf-8"))
+
+    def _get_opener(self):
+        if not self.opener:
+            if (CONF.dashboard.disable_ssl_certificate_validation and
+                    self._ssl_default_context_supported()):
+                ctx = ssl.create_default_context()
+                ctx.check_hostname = False
+                ctx.verify_mode = ssl.CERT_NONE
+                self.opener = request.build_opener(
+                    request.HTTPSHandler(context=ctx),
+                    request.HTTPCookieProcessor())
+            else:
+                self.opener = request.build_opener(
+                    request.HTTPCookieProcessor())
+        return self.opener
+
+    def _ssl_default_context_supported(self):
+        return (hasattr(ssl, 'create_default_context'))
+
+    @decorators.attr(type='smoke')
+    @decorators.idempotent_id('4f8851b1-0e69-482b-b63b-84c6e76f6c80')
+    @utils.services('dashboard')
+    def test_basic_scenario(self):
+        creds = self.os_primary.credentials
+        self.check_login_page()
+        self.user_login(creds.username, creds.password)
+        self.check_home_page()
diff --git a/tempest/scenario/test_server_basic_ops.py b/tempest/scenario/test_server_basic_ops.py
index 02bc692..60242d5 100644
--- a/tempest/scenario/test_server_basic_ops.py
+++ b/tempest/scenario/test_server_basic_ops.py
@@ -67,7 +67,10 @@
     def verify_metadata(self):
         if self.run_ssh and CONF.compute_feature_enabled.metadata_service:
             # Verify metadata service
-            md_url = 'http://169.254.169.254/latest/meta-data/public-ipv4'
+            if CONF.network.public_network_id:
+                md_url = 'http://169.254.169.254/latest/meta-data/public-ipv4'
+            else:
+                md_url = 'http://169.254.169.254/latest/meta-data/local-ipv4'
 
             def exec_cmd_and_verify_output():
                 cmd = 'curl ' + md_url
diff --git a/tempest/tests/lib/services/identity/v3/test_roles_client.py b/tempest/tests/lib/services/identity/v3/test_roles_client.py
index 8d6bb42..e963310 100644
--- a/tempest/tests/lib/services/identity/v3/test_roles_client.py
+++ b/tempest/tests/lib/services/identity/v3/test_roles_client.py
@@ -225,6 +225,16 @@
             role_id="1234",
             status=204)
 
+    def _test_create_user_role_on_system(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.create_user_role_on_system,
+            'tempest.lib.common.rest_client.RestClient.put',
+            {},
+            bytes_body,
+            user_id="123",
+            role_id="1234",
+            status=204)
+
     def _test_list_user_roles_on_project(self, bytes_body=False):
         self.check_service_client_function(
             self.client.list_user_roles_on_project,
@@ -243,6 +253,14 @@
             domain_id="b344506af7644f6794d9cb316600b020",
             user_id="123")
 
+    def _test_list_user_roles_on_system(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.list_user_roles_on_system,
+            'tempest.lib.common.rest_client.RestClient.get',
+            self.FAKE_LIST_ROLES,
+            bytes_body,
+            user_id="123")
+
     def _test_create_group_role_on_project(self, bytes_body=False):
         self.check_service_client_function(
             self.client.create_group_role_on_project,
@@ -265,6 +283,16 @@
             role_id="1234",
             status=204)
 
+    def _test_create_group_role_on_system(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.create_group_role_on_system,
+            'tempest.lib.common.rest_client.RestClient.put',
+            {},
+            bytes_body,
+            group_id="123",
+            role_id="1234",
+            status=204)
+
     def _test_list_group_roles_on_project(self, bytes_body=False):
         self.check_service_client_function(
             self.client.list_group_roles_on_project,
@@ -283,6 +311,15 @@
             domain_id="b344506af7644f6794d9cb316600b020",
             group_id="123")
 
+    def _test_list_group_roles_on_system(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.list_group_roles_on_system,
+            'tempest.lib.common.rest_client.RestClient.get',
+            self.FAKE_LIST_ROLES,
+            bytes_body,
+            domain_id="b344506af7644f6794d9cb316600b020",
+            group_id="123")
+
     def _test_create_role_inference_rule(self, bytes_body=False):
         self.check_service_client_function(
             self.client.create_role_inference_rule,
@@ -405,6 +442,15 @@
             role_id="1234",
             status=204)
 
+    def test_delete_role_from_user_on_system(self):
+        self.check_service_client_function(
+            self.client.delete_role_from_user_on_system,
+            'tempest.lib.common.rest_client.RestClient.delete',
+            {},
+            user_id="123",
+            role_id="1234",
+            status=204)
+
     def test_delete_role_from_group_on_project(self):
         self.check_service_client_function(
             self.client.delete_role_from_group_on_project,
@@ -425,6 +471,15 @@
             role_id="1234",
             status=204)
 
+    def test_delete_role_from_group_on_system(self):
+        self.check_service_client_function(
+            self.client.delete_role_from_group_on_system,
+            'tempest.lib.common.rest_client.RestClient.delete',
+            {},
+            group_id="123",
+            role_id="1234",
+            status=204)
+
     def test_check_user_role_existence_on_project(self):
         self.check_service_client_function(
             self.client.check_user_role_existence_on_project,
@@ -445,6 +500,15 @@
             role_id="1234",
             status=204)
 
+    def test_check_user_role_existence_on_system(self):
+        self.check_service_client_function(
+            self.client.check_user_role_existence_on_system,
+            'tempest.lib.common.rest_client.RestClient.head',
+            {},
+            user_id="123",
+            role_id="1234",
+            status=204)
+
     def test_check_role_from_group_on_project_existence(self):
         self.check_service_client_function(
             self.client.check_role_from_group_on_project_existence,
@@ -465,6 +529,15 @@
             role_id="1234",
             status=204)
 
+    def test_check_role_from_group_on_system_existence(self):
+        self.check_service_client_function(
+            self.client.check_role_from_group_on_system_existence,
+            'tempest.lib.common.rest_client.RestClient.head',
+            {},
+            group_id="123",
+            role_id="1234",
+            status=204)
+
     def test_create_role_inference_rule_with_str_body(self):
         self._test_create_role_inference_rule()
 
diff --git a/zuul.d/integrated-gate.yaml b/zuul.d/integrated-gate.yaml
index 4c1ee5a..27bbf64 100644
--- a/zuul.d/integrated-gate.yaml
+++ b/zuul.d/integrated-gate.yaml
@@ -69,6 +69,8 @@
       Former names for this job where:
         * legacy-tempest-dsvm-py35
         * gate-tempest-dsvm-py35
+    required-projects:
+      - openstack/horizon
     vars:
       tox_envlist: full
       devstack_localrc:
@@ -89,6 +91,8 @@
             network-feature-enabled:
               qos_placement_physnet: public
       devstack_services:
+        # Enbale horizon so that we can run horizon test.
+        horizon: true
         s-account: false
         s-container: false
         s-object: false