From 0018c7cccffefed96992158c07e04f610cce06bd Mon Sep 17 00:00:00 2001 From: Doug Hellmann <doug.hellmann@dreamhost.com> Date: Mon, 12 Mar 2012 14:08:19 -0400 Subject: [PATCH] Move the password generation logic to its own class and out of the config parser to cut down on the number of places that have component-specific configuration knowledge. Add a --no-prompt-passwords flag to stack for users that want auto-generated passwords without having to press enter for each one. Pork: - Ignore the emacs TAGS file. --- .gitignore | 1 + devstack/cfg.py | 42 +++++++----------- devstack/component.py | 1 + devstack/components/db.py | 26 ++++++----- devstack/components/glance.py | 4 +- devstack/components/keystone.py | 27 +++++++++--- devstack/components/nova.py | 2 +- devstack/components/rabbit.py | 4 +- devstack/env_rc.py | 24 +++++----- devstack/image/creator.py | 5 ++- devstack/opts.py | 7 +++ devstack/passwords.py | 78 +++++++++++++++++++++++++++++++++ devstack/progs/actions.py | 40 +++++++++++------ devstack/shell.py | 36 --------------- stack | 11 +++-- tests/test_passwords.py | 10 +++++ 16 files changed, 203 insertions(+), 115 deletions(-) create mode 100644 devstack/passwords.py create mode 100644 tests/test_passwords.py diff --git a/.gitignore b/.gitignore index 05bdf812..d97522d1 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ pidfile *.komodoproject .coverage .DS_Store +TAGS diff --git a/devstack/cfg.py b/devstack/cfg.py index 1bfe795e..46c46ec6 100644 --- a/devstack/cfg.py +++ b/devstack/cfg.py @@ -30,16 +30,6 @@ PW_TMPL = "Enter a password for %s: " ENV_PAT = re.compile(r"^\s*\$\{([\w\d]+):\-(.*)\}\s*$") SUB_MATCH = re.compile(r"(?:\$\(([\w\d]+):([\w\d]+))\)") CACHE_MSG = "(value will now be internally cached)" -PW_SECTIONS = ['passwords'] -DEF_PW_MSG = "[or press enter to get a generated one]" -PW_PROMPTS = { - 'horizon_keystone_admin': "Enter a password to use for horizon and keystone (20 chars or less) %s: " % (DEF_PW_MSG), - 'service_token': 'Enter a token to use for the service admin token %s: ' % (DEF_PW_MSG), - 'sql': 'Enter a password to use for your sql database user %s: ' % (DEF_PW_MSG), - 'rabbit': 'Enter a password to use for your rabbit user %s: ' % (DEF_PW_MSG), - 'old_sql': "Please enter your current mysql password so we that can reset it for next time: ", - 'service_password': "Enter a service password to use for the service authentication %s:" % (DEF_PW_MSG), -} class IgnoreMissingConfigParser(ConfigParser.RawConfigParser): @@ -91,56 +81,54 @@ def make_id(section, option): class StackConfigParser(IgnoreMissingConfigParser): def __init__(self): IgnoreMissingConfigParser.__init__(self) - self.pws = dict() self.configs_fetched = dict() self.db_dsns = dict() - def _resolve_value(self, section, option, value_gotten, auto_pw): + def _resolve_value(self, section, option, value_gotten): key = make_id(section, option) - if section in PW_SECTIONS and key not in self.pws and value_gotten: - self.pws[key] = value_gotten if section == 'host' and option == 'ip': LOG.debug("Host ip from configuration/environment was empty, programatically attempting to determine it.") value_gotten = utils.get_host_ip() LOG.debug("Determined your host ip to be: [%s]" % (value_gotten)) - if section in PW_SECTIONS and auto_pw and not value_gotten: - LOG.debug("Being forced to ask for password for [%s] since the configuration value is empty.", key) - value_gotten = sh.password(PW_PROMPTS.get(option, PW_TMPL % (key))) - self.pws[key] = value_gotten return value_gotten - def getdefaulted(self, section, option, default_val, auto_pw=True): - val = self.get(section, option, auto_pw) + def getdefaulted(self, section, option, default_val): + val = self.get(section, option) if not val or not val.strip(): LOG.debug("Value [%s] found was not good enough, returning provided default [%s]" % (val, default_val)) return default_val return val - def get(self, section, option, auto_pw=True): + def get(self, section, option): key = make_id(section, option) if key in self.configs_fetched: value = self.configs_fetched.get(key) LOG.debug("Fetched cached value [%s] for param [%s]" % (value, key)) else: LOG.debug("Fetching value for param [%s]" % (key)) - gotten_value = self._get_bashed(section, option, auto_pw) - value = self._resolve_value(section, option, gotten_value, auto_pw) + gotten_value = self._get_bashed(section, option) + value = self._resolve_value(section, option, gotten_value) LOG.debug("Fetched [%s] for [%s] %s" % (value, key, CACHE_MSG)) self.configs_fetched[key] = value return value - def _resolve_replacements(self, value, auto_pw): + def set(self, section, option, value): + key = make_id(section, option) + self.configs_fetched[key] = value + return IgnoreMissingConfigParser.set(self, section, option, value) + + def _resolve_replacements(self, value): LOG.debug("Performing simple replacement on [%s]", value) #allow for our simple replacement to occur def replacer(match): section = match.group(1) option = match.group(2) - return self.getdefaulted(section, option, '', auto_pw) + return self.getdefaulted(section, option, '') return SUB_MATCH.sub(replacer, value) - def _get_bashed(self, section, option, auto_pw): + def _get_bashed(self, section, option): value = IgnoreMissingConfigParser.get(self, section, option) if value is None: return value @@ -155,7 +143,7 @@ class StackConfigParser(IgnoreMissingConfigParser): env_value = env.get_key(env_key) if env_value is None: LOG.debug("Extracting value from config provided default value [%s]" % (def_val)) - extracted_val = self._resolve_replacements(def_val, auto_pw) + extracted_val = self._resolve_replacements(def_val) LOG.debug("Using config provided default value [%s] (no environment key)" % (extracted_val)) else: extracted_val = env_value diff --git a/devstack/component.py b/devstack/component.py index f9c6d4ea..56fc1fc9 100644 --- a/devstack/component.py +++ b/devstack/component.py @@ -54,6 +54,7 @@ class ComponentBase(object): def __init__(self, component_name, **kargs): self.component_name = component_name self.cfg = kargs.get("config") + self.password_generator = kargs.get('password_generator') self.packager = kargs.get("packager") self.distro = kargs.get("distro") self.instances = kargs.get("instances") or dict() diff --git a/devstack/components/db.py b/devstack/components/db.py index b07da1bf..f00c54ac 100644 --- a/devstack/components/db.py +++ b/devstack/components/db.py @@ -85,6 +85,8 @@ REQ_PKGS = ['db.json'] #config keys we warm up so u won't be prompted later WARMUP_PWS = ['sql'] +PASSWORD_DESCRIPTION = 'the database user' + class DBUninstaller(comp.PkgUninstallComponent): def __init__(self, *args, **kargs): @@ -93,7 +95,8 @@ class DBUninstaller(comp.PkgUninstallComponent): def warm_configs(self): for pw_key in WARMUP_PWS: - self.cfg.get("passwords", pw_key) + self.password_generator.get_password( + 'passwords', pw_key, PASSWORD_DESCRIPTION) def pre_uninstall(self): dbtype = self.cfg.get("db", "type") @@ -106,8 +109,10 @@ class DBUninstaller(comp.PkgUninstallComponent): if pwd_cmd: LOG.info("Ensuring your database is started before we operate on it.") self.runtime.restart() + old_pw = self.password_generator.get_password( + 'passwords', 'sql', PASSWORD_DESCRIPTION) params = { - 'OLD_PASSWORD': self.cfg.get("passwords", 'sql'), + 'OLD_PASSWORD': old_pw, 'NEW_PASSWORD': RESET_BASE_PW, 'USER': self.cfg.getdefaulted("db", "sql_user", 'root'), } @@ -129,7 +134,8 @@ class DBInstaller(comp.PkgInstallComponent): #in pre-install and post-install sections host_ip = self.cfg.get('host', 'ip') out = { - 'PASSWORD': self.cfg.get("passwords", "sql"), + 'PASSWORD': self.password_generator.get_password( + "passwords", "sql", PASSWORD_DESCRIPTION), 'BOOT_START': ("%s" % (True)).lower(), 'USER': self.cfg.getdefaulted("db", "sql_user", 'root'), 'SERVICE_HOST': host_ip, @@ -139,7 +145,8 @@ class DBInstaller(comp.PkgInstallComponent): def warm_configs(self): for pw_key in WARMUP_PWS: - self.cfg.get("passwords", pw_key) + self.password_generator.get_password( + 'passwords', pw_key, PASSWORD_DESCRIPTION) def _configure_db_confs(self): dbtype = self.cfg.get("db", "type") @@ -192,7 +199,8 @@ class DBInstaller(comp.PkgInstallComponent): LOG.info("Ensuring your database is started before we operate on it.") self.runtime.restart() params = { - 'NEW_PASSWORD': self.cfg.get("passwords", "sql"), + 'NEW_PASSWORD': self.password_generator.get_password( + "passwords", "sql", PASSWORD_DESCRIPTION), 'USER': self.cfg.getdefaulted("db", "sql_user", 'root'), 'OLD_PASSWORD': RESET_BASE_PW, } @@ -211,13 +219,11 @@ class DBInstaller(comp.PkgInstallComponent): LOG.info("Ensuring your database is started before we operate on it.") self.runtime.restart() params = { - 'PASSWORD': self.cfg.get("passwords", "sql"), + 'PASSWORD': self.password_generator.get_password( + "passwords", "sql", PASSWORD_DESCRIPTION), 'USER': user, } - cmds = list() - cmds.append({ - 'cmd': grant_cmd, - }) + cmds = [{'cmd': grant_cmd}] #shell seems to be needed here #since python escapes this to much... utils.execute_template(*cmds, params=params, shell=True) diff --git a/devstack/components/glance.py b/devstack/components/glance.py index 3dc13875..a33b2dee 100644 --- a/devstack/components/glance.py +++ b/devstack/components/glance.py @@ -187,7 +187,7 @@ class GlanceInstaller(comp.PythonInstallComponent): mp['SQL_CONN'] = self.cfg.get_dbdsn(DB_NAME) mp['SERVICE_HOST'] = self.cfg.get('host', 'ip') mp['HOST_IP'] = self.cfg.get('host', 'ip') - mp.update(keystone.get_shared_params(self.cfg, 'glance')) + mp.update(keystone.get_shared_params(self.cfg, self.password_generator, 'glance')) return mp @@ -224,4 +224,4 @@ class GlanceRuntime(comp.PythonRuntime): # TODO: make this less cheesy - need to wait till glance goes online LOG.info("Waiting %s seconds so that glance can start up before image install." % (WAIT_ONLINE_TO)) sh.sleep(WAIT_ONLINE_TO) - creator.ImageCreationService(self.cfg).install() + creator.ImageCreationService(self.cfg, self.password_generator).install() diff --git a/devstack/components/keystone.py b/devstack/components/keystone.py index 91050f63..f7f7353e 100644 --- a/devstack/components/keystone.py +++ b/devstack/components/keystone.py @@ -191,7 +191,7 @@ class KeystoneInstaller(comp.PythonInstallComponent): return comp.PythonInstallComponent._get_source_config(self, config_fn) def warm_configs(self): - get_shared_params(self.cfg) + get_shared_params(self.cfg, self.password_generator) def _get_param_map(self, config_fn): #these be used to fill in the configuration/cmds + @@ -204,9 +204,9 @@ class KeystoneInstaller(comp.PythonInstallComponent): if config_fn == ROOT_CONF: mp['SQL_CONN'] = self.cfg.get_dbdsn(DB_NAME) mp['KEYSTONE_DIR'] = self.appdir - mp.update(get_shared_params(self.cfg)) + mp.update(get_shared_params(self.cfg, self.password_generator)) elif config_fn == MANAGE_DATA_CONF: - mp.update(get_shared_params(self.cfg)) + mp.update(get_shared_params(self.cfg, self.password_generator)) return mp @@ -248,7 +248,8 @@ class KeystoneRuntime(comp.PythonRuntime): return APP_OPTIONS.get(app) -def get_shared_params(config, service_user_name=None): +def get_shared_params(config, password_generator, service_user_name=None): + LOG.debug('password_generator %s', password_generator) mp = dict() host_ip = config.get('host', 'ip') @@ -262,9 +263,21 @@ def get_shared_params(config, service_user_name=None): mp['DEMO_TENANT_NAME'] = mp['DEMO_USER_NAME'] #tokens and passwords - mp['SERVICE_TOKEN'] = config.get("passwords", "service_token") - mp['ADMIN_PASSWORD'] = config.get('passwords', 'horizon_keystone_admin') - mp['SERVICE_PASSWORD'] = config.get('passwords', 'service_password') + mp['SERVICE_TOKEN'] = password_generator.get_password( + 'passwords', + "service_token", + 'the service admin token', + ) + mp['ADMIN_PASSWORD'] = password_generator.get_password( + 'passwords', + 'horizon_keystone_admin', + 'the horizon and keystone admin', + 20) + mp['SERVICE_PASSWORD'] = password_generator.get_password( + 'passwords', + 'service_password', + 'service authentication', + ) #components of the auth endpoint keystone_auth_host = config.getdefaulted('keystone', 'keystone_auth_host', host_ip) diff --git a/devstack/components/nova.py b/devstack/components/nova.py index 104d9adf..1144bc71 100644 --- a/devstack/components/nova.py +++ b/devstack/components/nova.py @@ -408,7 +408,7 @@ class NovaInstaller(comp.PythonInstallComponent): mp['FIXED_NETWORK_SIZE'] = self.cfg.getdefaulted('nova', 'fixed_network_size', '256') mp['FIXED_RANGE'] = self.cfg.getdefaulted('nova', 'fixed_range', '10.0.0.0/24') else: - mp.update(keystone.get_shared_params(self.cfg, 'nova')) + mp.update(keystone.get_shared_params(self.cfg, self.password_generator, 'nova')) return mp def configure(self): diff --git a/devstack/components/rabbit.py b/devstack/components/rabbit.py index 34e5b1f8..6eb89f88 100644 --- a/devstack/components/rabbit.py +++ b/devstack/components/rabbit.py @@ -68,12 +68,12 @@ class RabbitInstaller(comp.PkgInstallComponent): def warm_configs(self): for pw_key in WARMUP_PWS: - self.cfg.get("passwords", pw_key) + self.password_generator.get_password("passwords", pw_key, 'the rabbit user') def _setup_pw(self): LOG.info("Setting up your rabbit-mq guest password.") self.runtime.restart() - passwd = self.cfg.get("passwords", "rabbit") + passwd = self.password_generator.get_password('passwords', "rabbit", 'the rabbit user') cmd = PWD_CMD + [passwd] sh.execute(*cmd, run_as_root=True) LOG.info("Restarting so that your rabbit-mq guest password is reflected.") diff --git a/devstack/env_rc.py b/devstack/env_rc.py index d9ebe98a..22adb266 100644 --- a/devstack/env_rc.py +++ b/devstack/env_rc.py @@ -19,12 +19,15 @@ import re from devstack import date from devstack import env +from devstack import log as logging from devstack import settings from devstack import shell as sh from devstack import utils from devstack.components import keystone +LOG = logging.getLogger('devstack.env_rc') + #general extraction cfg keys CFG_MAKE = { 'ADMIN_PASSWORD': ('passwords', 'horizon_keystone_admin'), @@ -48,8 +51,9 @@ QUOTED_PAT = re.compile(r"^\s*[\"](.*)[\"]\s*$") class RcWriter(object): - def __init__(self, cfg): + def __init__(self, cfg, password_generator): self.cfg = cfg + self.password_generator = password_generator def _make_export(self, export_name, value): escaped_val = sh.shellquote(value) @@ -68,11 +72,11 @@ class RcWriter(object): to_set = dict() ip = self.cfg.get('host', 'ip') ec2_url_default = urlunparse(('http', "%s:%s" % (ip, EC2_PORT), "services/Cloud", '', '', '')) - to_set['EC2_URL'] = self.cfg.getdefaulted('extern', 'ec2_url', ec2_url_default, auto_pw=False) + to_set['EC2_URL'] = self.cfg.getdefaulted('extern', 'ec2_url', ec2_url_default) s3_url_default = urlunparse(('http', "%s:%s" % (ip, S3_PORT), "services/Cloud", '', '', '')) - to_set['S3_URL'] = self.cfg.getdefaulted('extern', 's3_url', s3_url_default, auto_pw=False) - to_set['EC2_CERT'] = self.cfg.get('extern', 'ec2_cert_fn', auto_pw=False) - to_set['EC2_USER_ID'] = self.cfg.get('extern', 'ec2_user_id', auto_pw=False) + to_set['S3_URL'] = self.cfg.getdefaulted('extern', 's3_url', s3_url_default) + to_set['EC2_CERT'] = self.cfg.get('extern', 'ec2_cert_fn') + to_set['EC2_USER_ID'] = self.cfg.get('extern', 'ec2_user_id') return to_set def _generate_ec2_env(self): @@ -86,7 +90,7 @@ class RcWriter(object): to_set = dict() for (out_name, cfg_data) in CFG_MAKE.items(): (section, key) = (cfg_data) - to_set[out_name] = self.cfg.get(section, key, auto_pw=False) + to_set[out_name] = self.cfg.get(section, key) return to_set def _generate_general(self): @@ -149,7 +153,7 @@ class RcWriter(object): sh.write_file(fn, contents) def _get_os_envs(self): - key_params = keystone.get_shared_params(self.cfg) + key_params = keystone.get_shared_params(self.cfg, self.password_generator) to_set = dict() to_set['OS_PASSWORD'] = key_params['ADMIN_PASSWORD'] to_set['OS_TENANT_NAME'] = key_params['DEMO_TENANT_NAME'] @@ -179,7 +183,7 @@ alias ec2-upload-bundle="ec2-upload-bundle -a ${EC2_ACCESS_KEY} -s ${EC2_SECRET_ def _get_euca_envs(self): to_set = dict() - to_set['EUCALYPTUS_CERT'] = self.cfg.get('extern', 'nova_cert_fn', auto_pw=False) + to_set['EUCALYPTUS_CERT'] = self.cfg.get('extern', 'nova_cert_fn') return to_set def _generate_euca_env(self): @@ -191,8 +195,8 @@ alias ec2-upload-bundle="ec2-upload-bundle -a ${EC2_ACCESS_KEY} -s ${EC2_SECRET_ def _get_nova_envs(self): to_set = dict() - to_set['NOVA_VERSION'] = self.cfg.get('nova', 'nova_version', auto_pw=False) - to_set['NOVA_CERT'] = self.cfg.get('extern', 'nova_cert_fn', auto_pw=False) + to_set['NOVA_VERSION'] = self.cfg.get('nova', 'nova_version') + to_set['NOVA_CERT'] = self.cfg.get('extern', 'nova_cert_fn') return to_set def _generate_nova_env(self): diff --git a/devstack/image/creator.py b/devstack/image/creator.py index 996cd2ae..25411368 100644 --- a/devstack/image/creator.py +++ b/devstack/image/creator.py @@ -211,13 +211,14 @@ class ImageRegistry: class ImageCreationService: - def __init__(self, cfg): + def __init__(self, cfg, password_generator): self.cfg = cfg + self.password_generator = password_generator def _get_token(self): LOG.info("Fetching your keystone admin token so that we can perform image uploads.") - key_params = keystone.get_shared_params(self.cfg) + key_params = keystone.get_shared_params(self.cfg, self.password_generator) keystone_service_url = key_params['SERVICE_ENDPOINT'] keystone_token_url = "%s/tokens" % (keystone_service_url) diff --git a/devstack/opts.py b/devstack/opts.py index 286dc6f4..3e9e60a7 100644 --- a/devstack/opts.py +++ b/devstack/opts.py @@ -77,6 +77,12 @@ def parse(): action="store_false", dest="ensure_deps", help="ignore dependencies when performing ACTION") + base_group.add_option("--no-prompt-passwords", + action="store_false", + dest="prompt_for_passwords", + default=True, + help="do not prompt the user for passwords", + ) base_group.add_option("-e", "--ensure-deps", action="store_true", dest="ensure_deps", @@ -118,5 +124,6 @@ def parse(): output['keep_old'] = options.keep_old output['extras'] = args output['verbosity'] = len(options.verbosity) + output['prompt_for_passwords'] = options.prompt_for_passwords return output diff --git a/devstack/passwords.py b/devstack/passwords.py new file mode 100644 index 00000000..47fdd70a --- /dev/null +++ b/devstack/passwords.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python + +import binascii +import ConfigParser +import getpass +import logging +import os +import re + +from devstack.cfg import make_id + +LOG = logging.getLogger("devstack.passwords") + + +def generate_random(length): + """Returns a randomly generated password of the specified length.""" + LOG.debug("Generating a pseudo-random password of %d characters", + length) + return binascii.hexlify(os.urandom((length + 1) / 2))[:length] + + +class PasswordGenerator(object): + + def __init__(self, cfg, prompt_user=True): + self.cfg = cfg + self.prompt_user = prompt_user + # Store the values accessed by the caller + # so the main script can print them out + # at the end. + self.accessed = {} + + def _prompt_user(self, prompt_text): + LOG.debug('Asking the user for a %r password', prompt_text) + message = ("Enter a password to use for %s " + "[or press enter to get a generated one] " % prompt_text + ) + rc = "" + while True: + rc = getpass.getpass(message) + if len(rc) == 0: + break + # FIXME: More efficient way to look for whitespace? + if re.match(r"^(\s+)$", rc): + LOG.warning("Whitespace not allowed as a password!") + elif re.match(r"^(\s+)(\S+)(\s+)$", rc) or \ + re.match(r"^(\S+)(\s+)$", rc) or \ + re.match(r"^(\s+)(\S+)$", rc): + LOG.warning("Whitespace can not start or end a password!") + else: + break + return rc + + # FIXME: Remove the "section" argument, since it is always the same. + def get_password(self, section, option, prompt_text, length=8): + """Returns a password identified by the configuration location.""" + LOG.debug('looking for password %s (%s)', option, prompt_text) + + # Look in the configuration file(s) + try: + password = self.cfg.get(section, option) + except ConfigParser.Error: + password = '' + + # Optionally ask the user + if not password and self.prompt_user: + password = self._prompt_user(prompt_text) + self.accessed[make_id(section, option)] = password + + # If we still don't have a value, make one up. + if not password: + LOG.debug('no configured password for %s (%s)', + option, prompt_text) + password = generate_random(length) + + # Update the configration cache so that other parts of the + # code can find the value. + self.cfg.set(section, option, password) + return password diff --git a/devstack/progs/actions.py b/devstack/progs/actions.py index e4fc745a..274f7fee 100644 --- a/devstack/progs/actions.py +++ b/devstack/progs/actions.py @@ -31,7 +31,8 @@ _REVERSE_ACTIONS = [settings.UNINSTALL, settings.STOP] # For these actions we will attempt to make an rc file if it does not exist _RC_FILE_MAKE_ACTIONS = [settings.INSTALL] -# The order of which uninstalls happen + message of what is happening (before and after) +# The order of which uninstalls happen + message of what is happening +# (before and after) UNINSTALL_ORDERING = [ ( "Unconfiguring {name}.", @@ -55,7 +56,8 @@ UNINSTALL_ORDERING = [ ), ] -# The order of which starts happen + message of what is happening (before and after) +# The order of which starts happen + message of what is happening +# (before and after) STARTS_ORDERING = [ ( "Configuring runner for {name}.", @@ -79,7 +81,8 @@ STARTS_ORDERING = [ ), ] -# The order of which stops happen + message of what is happening (before and after) +# The order of which stops happen + message of what is happening +# (before and after) STOPS_ORDERING = [ ( "Stopping {name}.", @@ -88,7 +91,8 @@ STOPS_ORDERING = [ ), ] -# The order of which install happen + message of what is happening (before and after) +# The order of which install happen + message of what is happening +# (before and after) INSTALL_ORDERING = [ ( "Downloading {name}.", @@ -125,7 +129,8 @@ ACTION_MP = { settings.UNINSTALL: UNINSTALL_ORDERING, } -# These actions must have there prerequisite action accomplished (if determined by the boolean lambda to be needed) +# These actions must have there prerequisite action accomplished (if +# determined by the boolean lambda to be needed) PREQ_ACTIONS = { settings.START: ((lambda instance: (not instance.is_installed())), settings.INSTALL), settings.UNINSTALL: ((lambda instance: (instance.is_started())), settings.STOP), @@ -133,11 +138,14 @@ PREQ_ACTIONS = { class ActionRunner(object): - def __init__(self, distro, action, directory, config, pkg_manager, **kargs): + def __init__(self, distro, action, directory, config, + password_generator, pkg_manager, + **kargs): self.distro = distro self.action = action self.directory = directory self.cfg = config + self.password_generator = password_generator self.pkg_manager = pkg_manager self.kargs = kargs self.components = dict() @@ -186,14 +194,18 @@ class ActionRunner(object): all_instances = dict() for component in components.keys(): cls = common.get_action_cls(self.action, component, self.distro) + # FIXME: Instead of passing some of these options, + # pass a reference to the runner itself and let + # the component keep a weakref to it. instance = cls(instances=all_instances, - distro=self.distro, - packager=self.pkg_manager, - config=self.cfg, - root=self.directory, - opts=components.get(component, list()), - keep_old=self.kargs.get("keep_old") - ) + distro=self.distro, + packager=self.pkg_manager, + config=self.cfg, + password_generator=self.password_generator, + root=self.directory, + opts=components.get(component, list()), + keep_old=self.kargs.get("keep_old") + ) all_instances[component] = instance return all_instances @@ -233,7 +245,7 @@ class ActionRunner(object): inst = instances[component] inst.warm_configs() if self.gen_rc and self.rc_file: - writer = env_rc.RcWriter(self.cfg) + writer = env_rc.RcWriter(self.cfg, self.password_generator) if not sh.isfile(self.rc_file): LOG.info("Generating a file at [%s] that will contain your environment settings." % (self.rc_file)) writer.write(self.rc_file) diff --git a/devstack/shell.py b/devstack/shell.py index a69349db..b5c639f6 100644 --- a/devstack/shell.py +++ b/devstack/shell.py @@ -22,8 +22,6 @@ import pwd import shutil import subprocess import sys -import random -import re import time from devstack import env @@ -45,7 +43,6 @@ SHELL_QUOTE_REPLACERS = { } SHELL_WRAPPER = "\"%s\"" ROOT_PATH = os.sep -RANDOMIZER = random.SystemRandom() DRYRUN = None @@ -239,32 +236,6 @@ def _get_suids(): return (uid, gid) -def _gen_password(pw_len): - if pw_len <= 0: - msg = "Password length %s can not be less than or equal to zero" % (pw_len) - raise excp.BadParamException(msg) - LOG.debug("Generating you a pseudo-random password of byte length: %s" % (pw_len)) - random_num = RANDOMIZER.getrandbits(pw_len * 8) - pw = "%x" % (random_num) - LOG.debug("Generated you a pseudo-random password [%s]" % (pw)) - return pw - - -def prompt_password(pw_prompt): - rc = "" - while True: - rc = getpass.getpass(pw_prompt) - if len(rc) == 0: - break - if re.match(r"^(\s+)$", rc): - LOG.warning("Whitespace not allowed as a password!") - elif re.match(r"^(\s+)(\S+)(\s+)$", rc) or \ - re.match(r"^(\S+)(\s+)$", rc) or \ - re.match(r"^(\s+)(\S+)$", rc): - LOG.warning("Whitespace can not start or end a password!") - else: - break - return rc def chown_r(path, uid, gid, run_as_root=True): @@ -282,13 +253,6 @@ def chown_r(path, uid, gid, run_as_root=True): LOG.debug("Changing ownership of %s to %s:%s" % (joinpths(root, f), uid, gid)) -def password(pw_prompt, pw_len=8): - pw = prompt_password(pw_prompt) - if len(pw) == 0: - return _gen_password(pw_len) - else: - return pw - def _explode_path(path): parts = list() diff --git a/stack b/stack index efb51c14..6b12160d 100755 --- a/stack +++ b/stack @@ -26,6 +26,7 @@ from devstack import colorlog from devstack import date from devstack import log as lg from devstack import opts +from devstack import passwords from devstack import settings from devstack import shell as sh from devstack import utils @@ -45,7 +46,7 @@ _WELCOME_MAP = { } -def dump_config(config_obj): +def dump_config(config_obj, password_generator): def item_format(key, value): return "\t%s=%s" % (str(key), str(value)) @@ -55,7 +56,7 @@ def dump_config(config_obj): value = mp.get(key) LOG.info(item_format(key, value)) - passwords_gotten = config_obj.pws + passwords_gotten = password_generator.accessed full_cfgs = config_obj.configs_fetched db_dsns = config_obj.db_dsns if passwords_gotten or full_cfgs or db_dsns: @@ -97,14 +98,16 @@ def run(args): dryrun = args.get('dryrun') if dryrun: sh.set_dryrun(dryrun) + password_generator = passwords.PasswordGenerator(config, args['prompt_for_passwords']) pkg_manager = common.get_packager(distro, args.get('keep_old')) components = utils.parse_components(args.pop("components")) - runner = actions.ActionRunner(distro, action, rootdir, config, pkg_manager, components=components, **args) + runner = actions.ActionRunner(distro, action, rootdir, config, password_generator, + pkg_manager, components=components, **args) LOG.info("Starting action [%s] on %s for distro [%s]" % (action, date.rcf8222date(), distro)) runner.run() LOG.info("It took (%s) to complete action [%s]" % (common.format_secs_taken((time.time() - start_time)), action)) LOG.info("After action [%s] your settings which were created or read are:" % (action)) - dump_config(config) + dump_config(config, password_generator) return True diff --git a/tests/test_passwords.py b/tests/test_passwords.py new file mode 100644 index 00000000..7179c995 --- /dev/null +++ b/tests/test_passwords.py @@ -0,0 +1,10 @@ + +from devstack import passwords + + +def test_generate_random(): + def check_one(i): + p = passwords.generate_random(i) + assert len(p) == i + for i in range(1, 9): + yield check_one, i