diff --git a/config_tempest/main.py b/config_tempest/main.py index eceeeead..c1ac2d3e 100755 --- a/config_tempest/main.py +++ b/config_tempest/main.py @@ -37,6 +37,9 @@ obtained by querying the cloud. import argparse import logging import os +import re +import secrets +import string import sys import openstack @@ -258,6 +261,18 @@ def get_arg_parser(): $ discover-tempest-config \\ identity.username myname \\ identity.password mypass""") + parser.add_argument('--password-regex', default=None, + help="""Define the password regex required to + generate a password that meets these + conditions. NOTE: In regular expression use + double curly brackets `{{`instead of single + curly bracket `{` to make the command pass. + """) + parser.add_argument('--password-length', default=8, + help="""Specify the password length in combination + with the --password-regex option to generate + a password that meets these conditions. + """) parser.add_argument('--debug', action='store_true', default=False, help='Print debugging information.') parser.add_argument('--verbose', '-v', action='store_true', default=False, @@ -451,6 +466,30 @@ def parse_overrides(overrides): return new_overrides +def generate_password_by_regex(regex, length): + """Generate a new password that meets the required regex and length. + + :type regex: string + :type length: int + """ + characters = string.ascii_letters + string.digits + string.punctuation + regex = regex.replace('{{', '{').replace('}}', '}') + + try: + regex_object = re.compile(regex) + except re.error: + raise Exception("The provided regex %s is invalid." % regex) + + for _ in range(200): + password = ''.join(secrets.choice(characters) for i in range(length)) + if regex_object.fullmatch(password): + return password + + raise Exception("Could not generate a password matching the specified" + " regex. Check if the regex %s is possible with the" + " given length %d." % (regex, length)) + + def set_cloud_config_values(non_admin, cloud_creds, conf): """Set values from client's cloud config file. @@ -553,6 +592,12 @@ def config_tempest(**kwargs): clients = ClientManager(conf, credentials) if kwargs.get('create', False) and kwargs.get('test_accounts') is None: + if kwargs.get('password_regex') is not None: + password_regex = kwargs.get('password_regex') + password_length = kwargs.get('password_length') + new_password = generate_password_by_regex(password_regex, + int(password_length)) + conf.set('identity', 'password', new_password) users = Users(clients.projects, clients.roles, clients.users, conf) users.create_tempest_users() @@ -632,6 +677,8 @@ def main(): os_cloud=args.os_cloud, out=args.out, overrides=args.overrides, + password_regex=args.password_regex, + password_length=args.password_length, remove=args.remove, test_accounts=args.test_accounts, verbose=args.verbose, diff --git a/config_tempest/tests/test_config_tempest.py b/config_tempest/tests/test_config_tempest.py index e330c374..c688c3ee 100644 --- a/config_tempest/tests/test_config_tempest.py +++ b/config_tempest/tests/test_config_tempest.py @@ -15,6 +15,7 @@ # License for the specific language governing permissions and limitations # under the License. +import re from unittest import mock from fixtures import MonkeyPatch @@ -119,3 +120,38 @@ class TestOsClientConfigSupport(BaseConfigTempestTest): self.conf.get('auth', 'admin_username'), self.conf.get('auth', 'admin_password'), self.conf.get('auth', 'admin_project_name')) + + +class TestGeneratePassword(BaseConfigTempestTest): + + def test_generate_password_success_easy(self): + regex = r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$' + length = 12 + password = tool.generate_password_by_regex(regex, length) + + self.assertIsNotNone(password) + self.assertEqual(length, len(password)) + self.assertTrue(re.fullmatch(regex, password)) + + def test_generate_password_success_hard(self): + regex = r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*_+-=]).{8}$' + length = 8 + password = tool.generate_password_by_regex(regex, length) + + self.assertIsNotNone(password) + self.assertEqual(length, len(password)) + self.assertTrue(re.fullmatch(regex, password)) + + def test_generate_password_invalid_regex(self): + invalid_regex = r'[' + exc = Exception + self.assertRaises(exc, + tool.generate_password_by_regex, + invalid_regex, 8) + + def test_generate_password_impossible(self): + impossible_regex = r'^[a-z]{10}$' + exc = Exception + self.assertRaises(exc, + tool.generate_password_by_regex, + impossible_regex, 8) diff --git a/releasenotes/notes/add-password-regex-and-length-587c29a517338102.yaml b/releasenotes/notes/add-password-regex-and-length-587c29a517338102.yaml new file mode 100644 index 00000000..bcf528da --- /dev/null +++ b/releasenotes/notes/add-password-regex-and-length-587c29a517338102.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + New parameters, ``--password-regex`` and ``--password-length`` have been + added to allow users to generate a password following these constraints. + It is helpful for meeting password requirements given by services (e.g. + Keystone). When using both parameters, ensure the value for + ``--password-length`` does not conflict with any length defined within + the regular expression. Furthermore, to use a regex from the command + line, you must modify it by changing all single curly brackets ({}) to + double curly brackets ({{}}).