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