diff --git a/doc/source/usage.rst b/doc/source/usage.rst index 5c64c17c5..69fd42c86 100644 --- a/doc/source/usage.rst +++ b/doc/source/usage.rst @@ -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 `` 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 -------------- diff --git a/kayobe/ansible.py b/kayobe/ansible.py index 9d47a9243..ac8004a7c 100644 --- a/kayobe/ansible.py +++ b/kayobe/ansible.py @@ -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", diff --git a/kayobe/kolla_ansible.py b/kayobe/kolla_ansible.py index 8c5532b82..8ea1b830b 100644 --- a/kayobe/kolla_ansible.py +++ b/kayobe/kolla_ansible.py @@ -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", diff --git a/kayobe/tests/unit/test_vault.py b/kayobe/tests/unit/test_vault.py index 00f373af8..50a681df5 100644 --- a/kayobe/tests/unit/test_vault.py +++ b/kayobe/tests/unit/test_vault.py @@ -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") diff --git a/kayobe/vault.py b/kayobe/vault.py index 252ea97b9..82bc0f208 100644 --- a/kayobe/vault.py +++ b/kayobe/vault.py @@ -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( diff --git a/releasenotes/notes/support-ansible-vault-password-file-d00ca1e041e1bd88.yaml b/releasenotes/notes/support-ansible-vault-password-file-d00ca1e041e1bd88.yaml new file mode 100644 index 000000000..789297885 --- /dev/null +++ b/releasenotes/notes/support-ansible-vault-password-file-d00ca1e041e1bd88.yaml @@ -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 + `__ for details.