Merge "Remove skipping flavor_access_add/remove related tests"
diff --git a/etc/accounts.yaml.sample b/etc/accounts.yaml.sample
new file mode 100644
index 0000000..d191769
--- /dev/null
+++ b/etc/accounts.yaml.sample
@@ -0,0 +1,7 @@
+- username: 'user_1'
+  tenant_name: 'test_tenant_1'
+  password: 'test_password'
+
+- username: 'user_2'
+  tenant_name: 'test_tenant_2'
+  password: 'test_password'
diff --git a/etc/tempest.conf.sample b/etc/tempest.conf.sample
index 08f3fd4..4dcf460 100644
--- a/etc/tempest.conf.sample
+++ b/etc/tempest.conf.sample
@@ -105,6 +105,17 @@
 #syslog_log_facility=LOG_USER
 
 
+[auth]
+
+#
+# Options defined in tempest.config
+#
+
+# Path to the yaml file that contains the list of credentials
+# to use for running tests (string value)
+#test_accounts_file=etc/accounts.yaml
+
+
 [baremetal]
 
 #
@@ -452,6 +463,10 @@
 # value)
 #enable_instance_password=true
 
+# Does the test environment support dynamic network interface
+# attachment? (boolean value)
+#interface_attach=true
+
 
 [dashboard]
 
diff --git a/tempest/api/baremetal/README.rst b/tempest/api/baremetal/README.rst
new file mode 100644
index 0000000..759c937
--- /dev/null
+++ b/tempest/api/baremetal/README.rst
@@ -0,0 +1,25 @@
+Tempest Field Guide to Baremetal API tests
+==========================================
+
+
+What are these tests?
+---------------------
+
+These tests stress the OpenStack baremetal provisioning API provided by
+Ironic.
+
+
+Why are these tests in tempest?
+------------------------------
+
+The purpose of these tests is to exercise the various APIs provided by Ironic
+for managing baremetal nodes.
+
+
+Scope of these tests
+--------------------
+
+The baremetal API test perform basic CRUD operations on the Ironic node
+inventory.  They do not actually perform hardware provisioning. It is important
+to note that all Ironic API actions are admin operations meant to be used
+either by cloud operators or other OpenStack services (i.e., Nova).
diff --git a/tempest/api/baremetal/admin/__init__.py b/tempest/api/baremetal/admin/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/api/baremetal/admin/__init__.py
diff --git a/tempest/api/baremetal/base.py b/tempest/api/baremetal/admin/base.py
similarity index 100%
rename from tempest/api/baremetal/base.py
rename to tempest/api/baremetal/admin/base.py
diff --git a/tempest/api/baremetal/test_api_discovery.py b/tempest/api/baremetal/admin/test_api_discovery.py
similarity index 97%
rename from tempest/api/baremetal/test_api_discovery.py
rename to tempest/api/baremetal/admin/test_api_discovery.py
index bee10b9..7368b3e 100644
--- a/tempest/api/baremetal/test_api_discovery.py
+++ b/tempest/api/baremetal/admin/test_api_discovery.py
@@ -10,7 +10,7 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-from tempest.api.baremetal import base
+from tempest.api.baremetal.admin import base
 from tempest import test
 
 
diff --git a/tempest/api/baremetal/test_chassis.py b/tempest/api/baremetal/admin/test_chassis.py
similarity index 98%
rename from tempest/api/baremetal/test_chassis.py
rename to tempest/api/baremetal/admin/test_chassis.py
index 4ab86c2..c306c34 100644
--- a/tempest/api/baremetal/test_chassis.py
+++ b/tempest/api/baremetal/admin/test_chassis.py
@@ -11,7 +11,7 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-from tempest.api.baremetal import base
+from tempest.api.baremetal.admin import base
 from tempest.common.utils import data_utils
 from tempest import exceptions as exc
 from tempest import test
diff --git a/tempest/api/baremetal/test_drivers.py b/tempest/api/baremetal/admin/test_drivers.py
similarity index 96%
rename from tempest/api/baremetal/test_drivers.py
rename to tempest/api/baremetal/admin/test_drivers.py
index 852b6ab..649886b 100644
--- a/tempest/api/baremetal/test_drivers.py
+++ b/tempest/api/baremetal/admin/test_drivers.py
@@ -12,7 +12,7 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-from tempest.api.baremetal import base
+from tempest.api.baremetal.admin import base
 from tempest import config
 from tempest import test
 
diff --git a/tempest/api/baremetal/test_nodes.py b/tempest/api/baremetal/admin/test_nodes.py
similarity index 98%
rename from tempest/api/baremetal/test_nodes.py
rename to tempest/api/baremetal/admin/test_nodes.py
index 1572840..fc67854 100644
--- a/tempest/api/baremetal/test_nodes.py
+++ b/tempest/api/baremetal/admin/test_nodes.py
@@ -12,7 +12,7 @@
 
 import six
 
-from tempest.api.baremetal import base
+from tempest.api.baremetal.admin import base
 from tempest import exceptions as exc
 from tempest import test
 
diff --git a/tempest/api/baremetal/test_nodestates.py b/tempest/api/baremetal/admin/test_nodestates.py
similarity index 97%
rename from tempest/api/baremetal/test_nodestates.py
rename to tempest/api/baremetal/admin/test_nodestates.py
index 3044bc6..f24f490 100644
--- a/tempest/api/baremetal/test_nodestates.py
+++ b/tempest/api/baremetal/admin/test_nodestates.py
@@ -12,7 +12,7 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-from tempest.api.baremetal import base
+from tempest.api.baremetal.admin import base
 from tempest import exceptions
 from tempest.openstack.common import timeutils
 from tempest import test
diff --git a/tempest/api/baremetal/test_ports.py b/tempest/api/baremetal/admin/test_ports.py
similarity index 99%
rename from tempest/api/baremetal/test_ports.py
rename to tempest/api/baremetal/admin/test_ports.py
index 4ac7e29..d4adba9 100644
--- a/tempest/api/baremetal/test_ports.py
+++ b/tempest/api/baremetal/admin/test_ports.py
@@ -10,7 +10,7 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-from tempest.api.baremetal import base
+from tempest.api.baremetal.admin import base
 from tempest.common.utils import data_utils
 from tempest import exceptions as exc
 from tempest import test
diff --git a/tempest/api/baremetal/test_ports_negative.py b/tempest/api/baremetal/admin/test_ports_negative.py
similarity index 99%
rename from tempest/api/baremetal/test_ports_negative.py
rename to tempest/api/baremetal/admin/test_ports_negative.py
index 3e77a5f..7646677 100644
--- a/tempest/api/baremetal/test_ports_negative.py
+++ b/tempest/api/baremetal/admin/test_ports_negative.py
@@ -10,7 +10,7 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-from tempest.api.baremetal import base
+from tempest.api.baremetal.admin import base
 from tempest.common.utils import data_utils
 from tempest import exceptions as exc
 from tempest import test
diff --git a/tempest/api/compute/servers/test_attach_interfaces.py b/tempest/api/compute/servers/test_attach_interfaces.py
index 067d721..d1192c0 100644
--- a/tempest/api/compute/servers/test_attach_interfaces.py
+++ b/tempest/api/compute/servers/test_attach_interfaces.py
@@ -29,6 +29,8 @@
     def setUpClass(cls):
         if not CONF.service_available.neutron:
             raise cls.skipException("Neutron is required")
+        if not CONF.compute_feature_enabled.interface_attach:
+            raise cls.skipException("Interface attachment is not available.")
         # This test class requires network and subnet
         cls.set_network_resources(network=True, subnet=True)
         super(AttachInterfacesTestJSON, cls).setUpClass()
diff --git a/tempest/api/compute/servers/test_server_rescue_negative.py b/tempest/api/compute/servers/test_server_rescue_negative.py
index b35e55c..4582a46 100644
--- a/tempest/api/compute/servers/test_server_rescue_negative.py
+++ b/tempest/api/compute/servers/test_server_rescue_negative.py
@@ -34,7 +34,7 @@
 
         cls.set_network_resources(network=True, subnet=True, router=True)
         super(ServerRescueNegativeTestJSON, cls).setUpClass()
-        cls.device = 'vdf'
+        cls.device = CONF.compute.volume_device_name
 
         # Create a volume and wait for it to become ready for attach
         resp, cls.volume = cls.volumes_extensions_client.create_volume(
diff --git a/tempest/api/compute/v3/admin/test_servers.py b/tempest/api/compute/v3/admin/test_servers.py
index 366cfc6..d99c329 100644
--- a/tempest/api/compute/v3/admin/test_servers.py
+++ b/tempest/api/compute/v3/admin/test_servers.py
@@ -51,7 +51,6 @@
         self.assertEqual('200', resp['status'])
         self.assertEqual([], servers)
 
-    @test.skip_because(bug='1265416')
     @test.attr(type='gate')
     def test_list_servers_by_admin_with_all_tenants(self):
         # Listing servers by admin user with all tenants parameter
diff --git a/tempest/api/compute/v3/servers/test_attach_interfaces.py b/tempest/api/compute/v3/servers/test_attach_interfaces.py
index 43440c1..c2cf7e0 100644
--- a/tempest/api/compute/v3/servers/test_attach_interfaces.py
+++ b/tempest/api/compute/v3/servers/test_attach_interfaces.py
@@ -29,6 +29,8 @@
     def setUpClass(cls):
         if not CONF.service_available.neutron:
             raise cls.skipException("Neutron is required")
+        if not CONF.compute_feature_enabled.interface_attach:
+            raise cls.skipException("Interface attachment is not available.")
         # This test class requires network and subnet
         cls.set_network_resources(network=True, subnet=True)
         super(AttachInterfacesV3Test, cls).setUpClass()
diff --git a/tempest/api/compute/v3/servers/test_server_rescue_negative.py b/tempest/api/compute/v3/servers/test_server_rescue_negative.py
index 5eb6c9a..83fe128 100644
--- a/tempest/api/compute/v3/servers/test_server_rescue_negative.py
+++ b/tempest/api/compute/v3/servers/test_server_rescue_negative.py
@@ -33,7 +33,7 @@
             raise cls.skipException(msg)
 
         super(ServerRescueNegativeV3Test, cls).setUpClass()
-        cls.device = 'vdf'
+        cls.device = CONF.compute.volume_device_name
 
         # Create a volume and wait for it to become ready for attach
         resp, cls.volume = cls.volumes_client.create_volume(
diff --git a/tempest/api/network/test_routers.py b/tempest/api/network/test_routers.py
index d38633f..dca4c17 100644
--- a/tempest/api/network/test_routers.py
+++ b/tempest/api/network/test_routers.py
@@ -110,6 +110,36 @@
                         create_body['router']['id'])
         self.assertEqual(tenant_id, create_body['router']['tenant_id'])
 
+    @test.requires_ext(extension='ext-gw-mode', service='network')
+    @test.attr(type='smoke')
+    def test_create_router_with_default_snat_value(self):
+        # Create a router with default snat rule
+        name = data_utils.rand_name('router')
+        router = self._create_router(name,
+                 external_network_id=CONF.network.public_network_id)
+        self._verify_router_gateway(router['id'],
+              {'network_id': CONF.network.public_network_id,
+              'enable_snat': True})
+
+    @test.requires_ext(extension='ext-gw-mode', service='network')
+    @test.attr(type='smoke')
+    def test_create_router_with_snat_explicit(self):
+        name = data_utils.rand_name('snat-router')
+        # Create a router enabling snat attributes
+        enable_snat_states = [False, True]
+        for enable_snat in enable_snat_states:
+            external_gateway_info = {
+                'network_id': CONF.network.public_network_id,
+                'enable_snat': enable_snat}
+            resp, create_body = self.admin_client.create_router(
+                name, external_gateway_info=external_gateway_info)
+            self.assertEqual('201', resp['status'])
+            self.addCleanup(self.admin_client.delete_router,
+                    create_body['router']['id'])
+            # Verify snat attributes after router creation
+            self._verify_router_gateway(create_body['router']['id'],
+                    exp_ext_gw_info=external_gateway_info)
+
     @test.attr(type='smoke')
     def test_add_remove_router_interface_with_subnet_id(self):
         network = self.create_network()
@@ -151,7 +181,7 @@
                          router['id'])
 
     def _verify_router_gateway(self, router_id, exp_ext_gw_info=None):
-        resp, show_body = self.client.show_router(router_id)
+        resp, show_body = self.admin_client.show_router(router_id)
         self.assertEqual('200', resp['status'])
         actual_ext_gw_info = show_body['router']['external_gateway_info']
         if exp_ext_gw_info is None:
diff --git a/tempest/api_schema/response/compute/v2/flavors.py b/tempest/api_schema/response/compute/v2/flavors.py
index 66710b2..811ea84 100644
--- a/tempest/api_schema/response/compute/v2/flavors.py
+++ b/tempest/api_schema/response/compute/v2/flavors.py
@@ -18,7 +18,7 @@
 
 list_flavors_details = copy.deepcopy(flavors.common_flavor_list_details)
 
-# 'swap' attributes comes as integre value but if it is empty it comes as "".
+# 'swap' attributes comes as integer value but if it is empty it comes as "".
 # So defining type of as string and integer.
 list_flavors_details['response_body']['properties']['flavors']['items'][
     'properties']['swap'] = {'type': ['string', 'integer']}
@@ -38,7 +38,7 @@
 
 create_get_flavor_details = copy.deepcopy(flavors.common_flavor_details)
 
-# 'swap' attributes comes as integre value but if it is empty it comes as "".
+# 'swap' attributes comes as integer value but if it is empty it comes as "".
 # So defining type of as string and integer.
 create_get_flavor_details['response_body']['properties']['flavor'][
     'properties']['swap'] = {'type': ['string', 'integer']}
diff --git a/tempest/common/accounts.py b/tempest/common/accounts.py
new file mode 100644
index 0000000..80f5f91
--- /dev/null
+++ b/tempest/common/accounts.py
@@ -0,0 +1,130 @@
+# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
+#
+#    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 hashlib
+import os
+import yaml
+
+from tempest import auth
+from tempest.common import cred_provider
+from tempest import config
+from tempest import exceptions
+from tempest.openstack.common import lockutils
+from tempest.openstack.common import log as logging
+
+CONF = config.CONF
+LOG = logging.getLogger(__name__)
+
+
+def read_accounts_yaml(path):
+    yaml_file = open(path, 'r')
+    accounts = yaml.load(yaml_file)
+    return accounts
+
+
+class Accounts(cred_provider.CredentialProvider):
+
+    def __init__(self, name):
+        super(Accounts, self).__init__(name)
+        accounts = read_accounts_yaml(CONF.auth.test_accounts_file)
+        self.hash_dict = self.get_hash_dict(accounts)
+        self.accounts_dir = os.path.join(CONF.lock_path, 'test_accounts')
+        self.isolated_creds = {}
+
+    @classmethod
+    def get_hash_dict(cls, accounts):
+        hash_dict = {}
+        for account in accounts:
+            temp_hash = hashlib.md5()
+            temp_hash.update(str(account))
+            hash_dict[temp_hash.hexdigest()] = account
+        return hash_dict
+
+    def _create_hash_file(self, hash):
+        path = os.path.join(os.path.join(self.accounts_dir, hash))
+        if not os.path.isfile(path):
+            open(path, 'w').close()
+            return True
+        return False
+
+    @lockutils.synchronized('test_accounts_io', external=True)
+    def _get_free_hash(self, hashes):
+        if not os.path.isdir(self.accounts_dir):
+            os.mkdir(self.accounts_dir)
+            # Create File from first hash (since none are in use)
+            self._create_hash_file(hashes[0])
+            return hashes[0]
+        for hash in hashes:
+            res = self._create_hash_file(hash)
+            if res:
+                return hash
+        msg = 'Insufficient number of users provided'
+        raise exceptions.InvalidConfiguration(msg)
+
+    def _get_creds(self):
+        free_hash = self._get_free_hash(self.hashes.keys())
+        return self.hash_dict[free_hash]
+
+    @lockutils.synchronized('test_accounts_io', external=True)
+    def remove_hash(self, hash):
+        hash_path = os.path.join(self.accounts_dir, hash)
+        if not os.path.isfile(hash_path):
+            LOG.warning('Expected an account lock file %s to remove, but '
+                        'one did not exist')
+        else:
+            os.remove(hash_path)
+            if not os.listdir(self.accounts_dir):
+                os.rmdir(self.accounts_dir)
+
+    def get_hash(self, creds):
+        for hash in self.hash_dict:
+            # NOTE(mtreinish) Assuming with v3 that username, tenant, password
+            # is unique enough
+            cred_dict = {
+                'username': creds.username,
+                'tenant_name': creds.tenant_name,
+                'password': creds.password
+            }
+            if self.hash_dict[hash] == cred_dict:
+                return hash
+        raise AttributeError('Invalid credentials %s' % creds)
+
+    def remove_credentials(self, creds):
+        hash = self.get_hash(creds)
+        self.remove_hash(hash, self.accounts_dir)
+
+    def get_primary_creds(self):
+        if self.credentials.get('primary'):
+            return self.credentials.get('primary')
+        creds = self._get_creds()
+        primary_credential = auth.get_credentials(**creds)
+        self.credentials['primary'] = primary_credential
+        return primary_credential
+
+    def get_alt_creds(self):
+        if self.credentials.get('alt'):
+            return self.credentials.get('alt')
+        creds = self._get_creds()
+        alt_credential = auth.get_credentials(**creds)
+        self.credentials['alt'] = alt_credential
+        return alt_credential
+
+    def clear_isolated_creds(self):
+        for creds in self.credentials.values():
+            self.remove_credentials(creds)
+
+    def get_admin_creds(self):
+        msg = ('If admin credentials are available tenant_isolation should be'
+               ' used instead')
+        raise NotImplementedError(msg)
diff --git a/tempest/config.py b/tempest/config.py
index db54269..3b61700 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -29,6 +29,18 @@
         conf.register_opt(opt, group=opt_group.name)
 
 
+auth_group = cfg.OptGroup(name='auth',
+                          title="Options for authentication and credentials")
+
+
+AuthGroup = [
+    cfg.StrOpt('test_accounts_file',
+               default='etc/accounts.yaml',
+               help="Path to the yaml file that contains the list of "
+                    "credentials to use for running tests"),
+]
+
+
 identity_group = cfg.OptGroup(name='identity',
                               title="Keystone Configuration Options")
 
@@ -324,7 +336,11 @@
                 default=True,
                 help='Enables returning of the instance password by the '
                      'relevant server API calls such as create, rebuild '
-                     'or rescue.')
+                     'or rescue.'),
+    cfg.BoolOpt('interface_attach',
+                default=True,
+                help='Does the test environment support dynamic network '
+                     'interface attachment?')
 ]
 
 
@@ -1008,6 +1024,7 @@
 
 
 def register_opts():
+    register_opt_group(cfg.CONF, auth_group, AuthGroup)
     register_opt_group(cfg.CONF, compute_group, ComputeGroup)
     register_opt_group(cfg.CONF, compute_features_group,
                        ComputeFeaturesGroup)
@@ -1056,6 +1073,7 @@
     DEFAULT_CONFIG_FILE = "tempest.conf"
 
     def _set_attrs(self):
+        self.auth = cfg.CONF.auth
         self.compute = cfg.CONF.compute
         self.compute_feature_enabled = cfg.CONF['compute-feature-enabled']
         self.identity = cfg.CONF.identity
diff --git a/tempest/scenario/manager.py b/tempest/scenario/manager.py
index 3cfc698..606208e 100644
--- a/tempest/scenario/manager.py
+++ b/tempest/scenario/manager.py
@@ -1085,7 +1085,8 @@
             try:
                 source.ping_host(dest)
             except exceptions.SSHExecCommandFailed:
-                LOG.exception('Failed to ping host via ssh connection')
+                LOG.warn('Failed to ping IP: %s via a ssh connection from: %s.'
+                         % (dest, source.ssh_client.host))
                 return not should_succeed
             return should_succeed
 
diff --git a/tempest/scenario/test_network_basic_ops.py b/tempest/scenario/test_network_basic_ops.py
index 7dc817d..8c7af3d 100644
--- a/tempest/scenario/test_network_basic_ops.py
+++ b/tempest/scenario/test_network_basic_ops.py
@@ -14,8 +14,8 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 import collections
-
 import re
+import testtools
 
 from tempest.api.network import common as net_common
 from tempest.common import debug
@@ -347,6 +347,8 @@
                                                 msg="after re-associate "
                                                     "floating ip")
 
+    @testtools.skipUnless(CONF.compute_feature_enabled.interface_attach,
+                          'NIC hotplug not available')
     @test.attr(type='smoke')
     @test.services('compute', 'network')
     def test_hotplug_nic(self):
diff --git a/tempest/tests/common/test_accounts.py b/tempest/tests/common/test_accounts.py
new file mode 100644
index 0000000..c24bfb6
--- /dev/null
+++ b/tempest/tests/common/test_accounts.py
@@ -0,0 +1,187 @@
+# Copyright 2014 Hewlett-Packard Development Company, L.P.
+#
+#    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 hashlib
+import os
+import tempfile
+
+import mock
+from oslo.config import cfg
+from oslotest import mockpatch
+
+from tempest import auth
+from tempest.common import accounts
+from tempest.common import http
+from tempest import config
+from tempest import exceptions
+from tempest.tests import base
+from tempest.tests import fake_config
+from tempest.tests import fake_identity
+
+
+class TestAccount(base.TestCase):
+
+    def setUp(self):
+        super(TestAccount, self).setUp()
+        self.useFixture(fake_config.ConfigFixture())
+        self.stubs.Set(config, 'TempestConfigPrivate', fake_config.FakePrivate)
+        self.temp_dir = tempfile.mkdtemp()
+        cfg.CONF.set_default('lock_path', self.temp_dir)
+        self.addCleanup(os.rmdir, self.temp_dir)
+        self.test_accounts = [
+            {'username': 'test_user1', 'tenant_name': 'test_tenant1',
+             'password': 'p'},
+            {'username': 'test_user2', 'tenant_name': 'test_tenant2',
+             'password': 'p'},
+            {'username': 'test_user3', 'tenant_name': 'test_tenant3',
+             'password': 'p'},
+            {'username': 'test_user4', 'tenant_name': 'test_tenant4',
+             'password': 'p'},
+            {'username': 'test_user5', 'tenant_name': 'test_tenant5',
+             'password': 'p'},
+            {'username': 'test_user6', 'tenant_name': 'test_tenant6',
+             'password': 'p'},
+        ]
+        self.useFixture(mockpatch.Patch(
+            'tempest.common.accounts.read_accounts_yaml',
+            return_value=self.test_accounts))
+        cfg.CONF.set_default('test_accounts_file', '', group='auth')
+
+    def _get_hash_list(self, accounts_list):
+        hash_list = []
+        for account in accounts_list:
+            hash = hashlib.md5()
+            hash.update(str(account))
+            hash_list.append(hash.hexdigest())
+        return hash_list
+
+    def test_get_hash(self):
+        self.stubs.Set(http.ClosingHttp, 'request',
+                       fake_identity._fake_v2_response)
+        test_account_class = accounts.Accounts('test_name')
+        hash_list = self._get_hash_list(self.test_accounts)
+        test_cred_dict = self.test_accounts[3]
+        test_creds = auth.get_credentials(**test_cred_dict)
+        results = test_account_class.get_hash(test_creds)
+        self.assertEqual(hash_list[3], results)
+
+    def test_get_hash_dict(self):
+        test_account_class = accounts.Accounts('test_name')
+        hash_dict = test_account_class.get_hash_dict(self.test_accounts)
+        hash_list = self._get_hash_list(self.test_accounts)
+        for hash in hash_list:
+            self.assertIn(hash, hash_dict.keys())
+            self.assertIn(hash_dict[hash], self.test_accounts)
+
+    def test_create_hash_file_previous_file(self):
+        # Emulate the lock existing on the filesystem
+        self.useFixture(mockpatch.Patch('os.path.isfile', return_value=True))
+        with mock.patch('__builtin__.open', mock.mock_open(), create=True):
+            test_account_class = accounts.Accounts('test_name')
+            res = test_account_class._create_hash_file('12345')
+        self.assertFalse(res, "_create_hash_file should return False if the "
+                         "pseudo-lock file already exists")
+
+    def test_create_hash_file_no_previous_file(self):
+        # Emulate the lock not existing on the filesystem
+        self.useFixture(mockpatch.Patch('os.path.isfile', return_value=False))
+        with mock.patch('__builtin__.open', mock.mock_open(), create=True):
+            test_account_class = accounts.Accounts('test_name')
+            res = test_account_class._create_hash_file('12345')
+        self.assertTrue(res, "_create_hash_file should return True if the "
+                        "pseudo-lock doesn't already exist")
+
+    @mock.patch('tempest.openstack.common.lockutils.lock')
+    def test_get_free_hash_no_previous_accounts(self, lock_mock):
+        # Emulate no pre-existing lock
+        self.useFixture(mockpatch.Patch('os.path.isdir', return_value=False))
+        hash_list = self._get_hash_list(self.test_accounts)
+        mkdir_mock = self.useFixture(mockpatch.Patch('os.mkdir'))
+        self.useFixture(mockpatch.Patch('os.path.isfile', return_value=False))
+        test_account_class = accounts.Accounts('test_name')
+        with mock.patch('__builtin__.open', mock.mock_open(),
+                        create=True) as open_mock:
+            test_account_class._get_free_hash(hash_list)
+            lock_path = os.path.join(accounts.CONF.lock_path, 'test_accounts',
+                                     hash_list[0])
+            open_mock.assert_called_once_with(lock_path, 'w')
+        mkdir_path = os.path.join(accounts.CONF.lock_path, 'test_accounts')
+        mkdir_mock.mock.assert_called_once_with(mkdir_path)
+
+    @mock.patch('tempest.openstack.common.lockutils.lock')
+    def test_get_free_hash_no_free_accounts(self, lock_mock):
+        hash_list = self._get_hash_list(self.test_accounts)
+        # Emulate pre-existing lock dir
+        self.useFixture(mockpatch.Patch('os.path.isdir', return_value=True))
+        # Emulate all lcoks in list are in use
+        self.useFixture(mockpatch.Patch('os.path.isfile', return_value=True))
+        test_account_class = accounts.Accounts('test_name')
+        self.assertRaises(exceptions.InvalidConfiguration,
+                          test_account_class._get_free_hash, hash_list)
+
+    @mock.patch('tempest.openstack.common.lockutils.lock')
+    def test_get_free_hash_some_in_use_accounts(self, lock_mock):
+        # Emulate no pre-existing lock
+        self.useFixture(mockpatch.Patch('os.path.isdir', return_value=True))
+        hash_list = self._get_hash_list(self.test_accounts)
+        test_account_class = accounts.Accounts('test_name')
+
+        def _fake_is_file(path):
+            # Fake isfile() to return that the path exists unless a specific
+            # hash is in the path
+            if hash_list[3] in path:
+                return False
+            return True
+
+        self.stubs.Set(os.path, 'isfile', _fake_is_file)
+        with mock.patch('__builtin__.open', mock.mock_open(),
+                        create=True) as open_mock:
+            test_account_class._get_free_hash(hash_list)
+            lock_path = os.path.join(accounts.CONF.lock_path, 'test_accounts',
+                                     hash_list[3])
+            open_mock.assert_called_once_with(lock_path, 'w')
+
+    @mock.patch('tempest.openstack.common.lockutils.lock')
+    def test_remove_hash_last_account(self, lock_mock):
+        hash_list = self._get_hash_list(self.test_accounts)
+        # Pretend the pseudo-lock is there
+        self.useFixture(mockpatch.Patch('os.path.isfile', return_value=True))
+        # Pretend the lock dir is empty
+        self.useFixture(mockpatch.Patch('os.listdir', return_value=[]))
+        test_account_class = accounts.Accounts('test_name')
+        remove_mock = self.useFixture(mockpatch.Patch('os.remove'))
+        rmdir_mock = self.useFixture(mockpatch.Patch('os.rmdir'))
+        test_account_class.remove_hash(hash_list[2])
+        hash_path = os.path.join(accounts.CONF.lock_path, 'test_accounts',
+                                 hash_list[2])
+        lock_path = os.path.join(accounts.CONF.lock_path, 'test_accounts')
+        remove_mock.mock.assert_called_once_with(hash_path)
+        rmdir_mock.mock.assert_called_once_with(lock_path)
+
+    @mock.patch('tempest.openstack.common.lockutils.lock')
+    def test_remove_hash_not_last_account(self, lock_mock):
+        hash_list = self._get_hash_list(self.test_accounts)
+        # Pretend the pseudo-lock is there
+        self.useFixture(mockpatch.Patch('os.path.isfile', return_value=True))
+        # Pretend the lock dir is empty
+        self.useFixture(mockpatch.Patch('os.listdir', return_value=[
+            hash_list[1], hash_list[4]]))
+        test_account_class = accounts.Accounts('test_name')
+        remove_mock = self.useFixture(mockpatch.Patch('os.remove'))
+        rmdir_mock = self.useFixture(mockpatch.Patch('os.rmdir'))
+        test_account_class.remove_hash(hash_list[2])
+        hash_path = os.path.join(accounts.CONF.lock_path, 'test_accounts',
+                                 hash_list[2])
+        remove_mock.mock.assert_called_once_with(hash_path)
+        rmdir_mock.mock.assert_not_called()
diff --git a/tempest/tests/fake_config.py b/tempest/tests/fake_config.py
index 536cbcf..9e56916 100644
--- a/tempest/tests/fake_config.py
+++ b/tempest/tests/fake_config.py
@@ -61,3 +61,4 @@
     def __init__(self, parse_conf=True, config_path=None):
         cfg.CONF([], default_config_files=[])
         self._set_attrs()
+        self.lock_path = cfg.CONF.lock_path