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 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. 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 Prompt
Use ``kayobe --ask-vault-pass`` to prompt for the password. Use ``kayobe --ask-vault-pass`` to prompt for the password.
File File
Use ``kayobe --vault-password-file <file>`` to read the password from a Use ``kayobe --vault-password-file <file>`` to read the password from a
(plain text) file. (plain text) file.
Environment variable Environment variable: ``KAYOBE_VAULT_PASSWORD``
Export the environment variable ``KAYOBE_VAULT_PASSWORD`` to read the Export the environment variable ``KAYOBE_VAULT_PASSWORD`` to read the
password from the environment. 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 Limiting Hosts
-------------- --------------

View File

@ -112,7 +112,7 @@ def _get_inventories_paths(parsed_args, env_path):
def _validate_args(parsed_args, playbooks): def _validate_args(parsed_args, playbooks):
"""Validate Kayobe Ansible arguments.""" """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) result = utils.is_readable_dir(parsed_args.config_path)
if not result["result"]: if not result["result"]:
LOG.error("Kayobe configuration path %s is invalid: %s", 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): def _validate_args(parsed_args, inventory_filename):
"""Validate Kayobe Ansible arguments.""" """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) result = utils.is_readable_dir(parsed_args.kolla_config_path)
if not result["result"]: if not result["result"]:
LOG.error("Kolla configuration path %s is invalid: %s", LOG.error("Kolla configuration path %s is invalid: %s",

View File

@ -34,33 +34,53 @@ class TestCase(unittest.TestCase):
universal_newlines=True) universal_newlines=True)
self.assertEqual('fake-password', result) self.assertEqual('fake-password', result)
def test_validate_args_ok(self): def test_enforce_single_password_source_ok(self):
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
vault.add_args(parser) vault.add_args(parser)
parsed_args = parser.parse_args([]) 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"}) @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() parser = argparse.ArgumentParser()
vault.add_args(parser) vault.add_args(parser)
parsed_args = parser.parse_args([]) 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"}) @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() parser = argparse.ArgumentParser()
vault.add_args(parser) vault.add_args(parser)
parsed_args = parser.parse_args(["--ask-vault-pass"]) 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"}) @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() parser = argparse.ArgumentParser()
vault.add_args(parser) vault.add_args(parser)
parsed_args = parser.parse_args(["--vault-password-file", parsed_args = parser.parse_args(["--vault-password-file",
"/path/to/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') @mock.patch.object(vault.getpass, 'getpass')
def test__ask_vault_pass(self, mock_getpass): def test__ask_vault_pass(self, mock_getpass):
@ -102,7 +122,7 @@ class TestCase(unittest.TestCase):
mock_ask.assert_called_once_with() mock_ask.assert_called_once_with()
@mock.patch.object(vault, '_read_vault_password_file') @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" mock_read.return_value = "test-pass"
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
vault.add_args(parser) vault.add_args(parser)
@ -112,3 +132,17 @@ class TestCase(unittest.TestCase):
vault.update_environment(parsed_args, env) vault.update_environment(parsed_args, env)
self.assertEqual({"KAYOBE_VAULT_PASSWORD": "test-pass"}, env) self.assertEqual({"KAYOBE_VAULT_PASSWORD": "test-pass"}, env)
mock_read.assert_called_once_with("/path/to/file") 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__) LOG = logging.getLogger(__name__)
VAULT_PASSWORD_ENV = "KAYOBE_VAULT_PASSWORD" VAULT_PASSWORD_ENV = "KAYOBE_VAULT_PASSWORD"
VAULT_PASSWORD_FILE_ENV = "ANSIBLE_VAULT_PASSWORD_FILE"
def _get_vault_password_helper(): 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 It is possible to use an environment variable to avoid typing the vault
password. 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 None
return _get_vault_password_helper() return _get_vault_password_helper()
@ -75,23 +77,48 @@ def build_args(parsed_args, password_file_arg_name):
return cmd return cmd
def validate_args(parsed_args): def _validate_environment_variables():
"""Validate command line arguments.""" """Verify that only one password environment variable is set"""
# Ensure that a password prompt or file has not been requested if the
# password environment variable is set. invalid_source = None
if VAULT_PASSWORD_ENV not in os.environ: password_env_var = None
return 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() helper = _get_vault_password_helper()
invalid_arg = None invalid_source = None
if parsed_args.ask_vault_pass: if parsed_args.ask_vault_pass:
invalid_arg = "--ask-vault-pass" invalid_source = "--ask-vault-pass"
elif parsed_args.vault_password_file != helper: 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" % LOG.error("Cannot specify %s when $%s is specified" %
(invalid_arg, VAULT_PASSWORD_ENV)) (invalid_source, password_env_var))
sys.exit(1) sys.exit(1)
@ -124,15 +151,20 @@ def update_environment(parsed_args, env):
:param parsed_args: Parsed command line arguments. :param parsed_args: Parsed command line arguments.
:params env: Dict of environment variables to update. :params env: Dict of environment variables to update.
""" """
# If the Vault password has been specified via --vault-password-file, or a # If the Vault password has been specified via --vault-password-file, a
# prompt has been requested via --ask-vault-pass, ensure the environment # prompt has been requested via --ask-vault-pass, or the
# variable is set, so that it can be referenced by playbooks to generate # $ANSIBLE_VAULT_PASSWORD_FILE environment variable is set, ensure the
# the kolla-ansible passwords.yml file. # $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: if VAULT_PASSWORD_ENV in env:
return return
vault_password = None 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() vault_password = _ask_vault_pass()
elif parsed_args.vault_password_file: elif parsed_args.vault_password_file:
vault_password = _read_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.