Add support for ANSIBLE_VAULT_PASSWORD_FILE

Story: 2006766
Task: 37281

Change-Id: I53fac9ac5cfb17729bf854bd9e16373dc9c2efe2
This commit is contained in:
Alex-Welsh 2022-09-21 14:14:17 +01:00 committed by Pierre Riteau
parent 05a2c1acbd
commit 4661cf7410
6 changed files with 107 additions and 31 deletions

View File

@ -31,18 +31,22 @@ can be activated by generating and then sourcing the bash completion script::
Working with Ansible Vault
--------------------------
If Ansible vault has been used to encrypt Kayobe configuration files, it will
If Ansible Vault has been used to encrypt Kayobe configuration files, it will
be necessary to provide the ``kayobe`` command with access to vault password.
There are three options for doing this:
There are four options for doing this:
Prompt
Use ``kayobe --ask-vault-pass`` to prompt for the password.
File
Use ``kayobe --vault-password-file <file>`` to read the password from a
(plain text) file.
Environment variable
Environment variable: ``KAYOBE_VAULT_PASSWORD``
Export the environment variable ``KAYOBE_VAULT_PASSWORD`` to read the
password from the environment.
Environment variable: ``ANSIBLE_VAULT_PASSWORD_FILE``
Export the environment variable ``ANSIBLE_VAULT_PASSWORD_FILE`` to read the
password from a (plain text) file, with the path to that file being read
from the environment.
Limiting Hosts
--------------

View File

@ -112,7 +112,7 @@ def _get_inventories_paths(parsed_args, env_path):
def _validate_args(parsed_args, playbooks):
"""Validate Kayobe Ansible arguments."""
vault.validate_args(parsed_args)
vault.enforce_single_password_source(parsed_args)
result = utils.is_readable_dir(parsed_args.config_path)
if not result["result"]:
LOG.error("Kayobe configuration path %s is invalid: %s",

View File

@ -81,7 +81,7 @@ def _get_inventory_path(parsed_args, inventory_filename):
def _validate_args(parsed_args, inventory_filename):
"""Validate Kayobe Ansible arguments."""
vault.validate_args(parsed_args)
vault.enforce_single_password_source(parsed_args)
result = utils.is_readable_dir(parsed_args.kolla_config_path)
if not result["result"]:
LOG.error("Kolla configuration path %s is invalid: %s",

View File

@ -34,33 +34,53 @@ class TestCase(unittest.TestCase):
universal_newlines=True)
self.assertEqual('fake-password', result)
def test_validate_args_ok(self):
def test_enforce_single_password_source_ok(self):
parser = argparse.ArgumentParser()
vault.add_args(parser)
parsed_args = parser.parse_args([])
vault.validate_args(parsed_args)
vault.enforce_single_password_source(parsed_args)
@mock.patch.dict(os.environ, {"KAYOBE_VAULT_PASSWORD": "test-pass"})
def test_validate_args_env(self):
def test_enforce_single_password_source_env_kayobe(self):
parser = argparse.ArgumentParser()
vault.add_args(parser)
parsed_args = parser.parse_args([])
vault.validate_args(parsed_args)
vault.enforce_single_password_source(parsed_args)
@mock.patch.dict(os.environ, {"ANSIBLE_VAULT_PASSWORD_FILE":
"/path/to/file"})
def test_enforce_single_password_source_env_ansible(self):
parser = argparse.ArgumentParser()
vault.add_args(parser)
parsed_args = parser.parse_args([])
vault.enforce_single_password_source(parsed_args)
@mock.patch.dict(os.environ, {"KAYOBE_VAULT_PASSWORD": "test-pass"})
def test_validate_args_ask_vault_pass(self):
def test_enforce_single_password_source_ask_vault_pass(self):
parser = argparse.ArgumentParser()
vault.add_args(parser)
parsed_args = parser.parse_args(["--ask-vault-pass"])
self.assertRaises(SystemExit, vault.validate_args, parsed_args)
self.assertRaises(SystemExit, vault.enforce_single_password_source,
parsed_args)
@mock.patch.dict(os.environ, {"KAYOBE_VAULT_PASSWORD": "test-pass"})
def test_validate_args_vault_password_file(self):
def test_enforce_single_password_source_vault_password_file(self):
parser = argparse.ArgumentParser()
vault.add_args(parser)
parsed_args = parser.parse_args(["--vault-password-file",
"/path/to/file"])
self.assertRaises(SystemExit, vault.validate_args, parsed_args)
self.assertRaises(SystemExit, vault.enforce_single_password_source,
parsed_args)
@mock.patch.dict(os.environ, {"KAYOBE_VAULT_PASSWORD": "test-pass",
"ANSIBLE_VAULT_PASSWORD_FILE":
"/path/to/file"})
def test_enforce_single_password_source_vault_both_env(self):
parser = argparse.ArgumentParser()
vault.add_args(parser)
parsed_args = parser.parse_args([])
self.assertRaises(SystemExit, vault.enforce_single_password_source,
parsed_args)
@mock.patch.object(vault.getpass, 'getpass')
def test__ask_vault_pass(self, mock_getpass):
@ -102,7 +122,7 @@ class TestCase(unittest.TestCase):
mock_ask.assert_called_once_with()
@mock.patch.object(vault, '_read_vault_password_file')
def test_update_environment_file(self, mock_read):
def test_update_environment_file_arg(self, mock_read):
mock_read.return_value = "test-pass"
parser = argparse.ArgumentParser()
vault.add_args(parser)
@ -112,3 +132,17 @@ class TestCase(unittest.TestCase):
vault.update_environment(parsed_args, env)
self.assertEqual({"KAYOBE_VAULT_PASSWORD": "test-pass"}, env)
mock_read.assert_called_once_with("/path/to/file")
@mock.patch.dict(os.environ, {"ANSIBLE_VAULT_PASSWORD_FILE":
"/path/to/file"})
@mock.patch.object(vault, '_read_vault_password_file')
def test_update_environment_file_env(self, mock_read):
mock_read.return_value = "test-pass"
parser = argparse.ArgumentParser()
vault.add_args(parser)
parsed_args = parser.parse_args([])
env = {"ANSIBLE_VAULT_PASSWORD_FILE": "/path/to/file"}
vault.update_environment(parsed_args, env)
self.assertEqual({"KAYOBE_VAULT_PASSWORD": "test-pass",
"ANSIBLE_VAULT_PASSWORD_FILE": "/path/to/file"}, env)
mock_read.assert_called_once_with("/path/to/file")

View File

@ -24,6 +24,7 @@ from kayobe import utils
LOG = logging.getLogger(__name__)
VAULT_PASSWORD_ENV = "KAYOBE_VAULT_PASSWORD"
VAULT_PASSWORD_FILE_ENV = "ANSIBLE_VAULT_PASSWORD_FILE"
def _get_vault_password_helper():
@ -45,7 +46,8 @@ def _get_default_vault_password_file():
It is possible to use an environment variable to avoid typing the vault
password.
"""
if not os.getenv(VAULT_PASSWORD_ENV):
if (VAULT_PASSWORD_ENV not in os.environ and
VAULT_PASSWORD_FILE_ENV not in os.environ):
return None
return _get_vault_password_helper()
@ -75,23 +77,48 @@ def build_args(parsed_args, password_file_arg_name):
return cmd
def validate_args(parsed_args):
"""Validate command line arguments."""
# Ensure that a password prompt or file has not been requested if the
# password environment variable is set.
if VAULT_PASSWORD_ENV not in os.environ:
return
def _validate_environment_variables():
"""Verify that only one password environment variable is set"""
invalid_source = None
password_env_var = None
if VAULT_PASSWORD_ENV in os.environ:
password_env_var = VAULT_PASSWORD_ENV
if VAULT_PASSWORD_FILE_ENV in os.environ:
invalid_source = "$" + VAULT_PASSWORD_FILE_ENV
elif (VAULT_PASSWORD_FILE_ENV in os.environ):
password_env_var = VAULT_PASSWORD_FILE_ENV
return invalid_source, password_env_var
def _validate_args(parsed_args):
"""Verify that no conflicting arguments are being used"""
helper = _get_vault_password_helper()
invalid_arg = None
invalid_source = None
if parsed_args.ask_vault_pass:
invalid_arg = "--ask-vault-pass"
invalid_source = "--ask-vault-pass"
elif parsed_args.vault_password_file != helper:
invalid_arg = "--vault-password-file"
invalid_source = "--vault-password-file"
return invalid_source
if invalid_arg:
def enforce_single_password_source(parsed_args):
"""Verify that a password is only being received from a single source"""
# Ensure that a password prompt or file has not been requested if a
# password environment variable is set, and that only one password
# environment variable is set
invalid_source, password_env_var = _validate_environment_variables()
if not password_env_var:
return
if not invalid_source and password_env_var:
invalid_source = _validate_args(parsed_args)
if invalid_source:
LOG.error("Cannot specify %s when $%s is specified" %
(invalid_arg, VAULT_PASSWORD_ENV))
(invalid_source, password_env_var))
sys.exit(1)
@ -124,15 +151,20 @@ def update_environment(parsed_args, env):
:param parsed_args: Parsed command line arguments.
:params env: Dict of environment variables to update.
"""
# If the Vault password has been specified via --vault-password-file, or a
# prompt has been requested via --ask-vault-pass, ensure the environment
# variable is set, so that it can be referenced by playbooks to generate
# the kolla-ansible passwords.yml file.
# If the Vault password has been specified via --vault-password-file, a
# prompt has been requested via --ask-vault-pass, or the
# $ANSIBLE_VAULT_PASSWORD_FILE environment variable is set, ensure the
# $KAYOBE_PASSWORD_ENV environment variable is set, so that it can be
# referenced by playbooks to generate the kolla-ansible passwords.yml
# file.
if VAULT_PASSWORD_ENV in env:
return
vault_password = None
if parsed_args.ask_vault_pass:
if VAULT_PASSWORD_FILE_ENV in os.environ:
vault_password = _read_vault_password_file(
os.environ[VAULT_PASSWORD_FILE_ENV])
elif parsed_args.ask_vault_pass:
vault_password = _ask_vault_pass()
elif parsed_args.vault_password_file:
vault_password = _read_vault_password_file(

View File

@ -0,0 +1,6 @@
---
features:
- |
Adds support for the ANSIBLE_VAULT_PASSWORD_FILE environment variable as a
source for the Ansible Vault password. See `story 2006766
<https://storyboard.openstack.org/#!/story/2006766>`__ for details.