Fix configuration dump with inline encrypted variables
If inline Ansible vault encryption is used to define an encrypted variable in kayobe-config, running 'kayobe configuration dump -l <host>' fails with the following: Failed to decode config dump YAML file /tmp/tmp_fg1bv_j/localhost.yml: ConstructorError(None, None, "could not determine a constructor for the tag '!vault'", <yaml.error.Mark object at 0x7f1e5c7404c0>) This change fixes the error by using the Ansible YAML loader which supports the vault tag. Any vault encrypted variables are sanitised in the dump output. Note that variables in vault encrypted files are not sanitised. Change-Id: I4830500d3c927b0689b6f0bca32c28137916420b Closes-Bug: #2031390 (cherry picked from commit 78702d0e3094e6d6a16a31eaf2517d4e0f25d1c7)
This commit is contained in:
parent
698c281a59
commit
2711492262
@ -21,6 +21,8 @@ import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
from ansible.parsing.yaml.objects import AnsibleVaultEncryptedUnicode
|
||||
|
||||
from kayobe import exception
|
||||
from kayobe import utils
|
||||
from kayobe import vault
|
||||
@ -257,6 +259,18 @@ def run_playbook(parsed_args, playbook, *args, **kwargs):
|
||||
return run_playbooks(parsed_args, [playbook], *args, **kwargs)
|
||||
|
||||
|
||||
def _sanitise_hostvar(var):
|
||||
"""Sanitise a host variable."""
|
||||
if isinstance(var, AnsibleVaultEncryptedUnicode):
|
||||
return "******"
|
||||
# Recursively sanitise dicts and lists.
|
||||
if isinstance(var, dict):
|
||||
return {k: _sanitise_hostvar(v) for k, v in var.items()}
|
||||
if isinstance(var, list):
|
||||
return [_sanitise_hostvar(v) for v in var]
|
||||
return var
|
||||
|
||||
|
||||
def config_dump(parsed_args, host=None, hosts=None, var_name=None,
|
||||
facts=None, extra_vars=None, tags=None, verbose_level=None):
|
||||
dump_dir = tempfile.mkdtemp()
|
||||
@ -282,7 +296,8 @@ def config_dump(parsed_args, host=None, hosts=None, var_name=None,
|
||||
LOG.debug("Found dump file %s", path)
|
||||
inventory_hostname, ext = os.path.splitext(path)
|
||||
if ext == ".yml":
|
||||
hvars = utils.read_yaml_file(os.path.join(dump_dir, path))
|
||||
dump_file = os.path.join(dump_dir, path)
|
||||
hvars = utils.read_config_dump_yaml_file(dump_file)
|
||||
if host:
|
||||
return hvars
|
||||
else:
|
||||
@ -290,7 +305,7 @@ def config_dump(parsed_args, host=None, hosts=None, var_name=None,
|
||||
else:
|
||||
LOG.warning("Unexpected extension on config dump file %s",
|
||||
path)
|
||||
return hostvars
|
||||
return {k: _sanitise_hostvar(v) for k, v in hostvars.items()}
|
||||
finally:
|
||||
shutil.rmtree(dump_dir)
|
||||
|
||||
|
@ -402,7 +402,7 @@ class TestCase(unittest.TestCase):
|
||||
ansible.run_playbooks, parsed_args, ["command"])
|
||||
|
||||
@mock.patch.object(shutil, 'rmtree')
|
||||
@mock.patch.object(utils, 'read_yaml_file')
|
||||
@mock.patch.object(utils, 'read_config_dump_yaml_file')
|
||||
@mock.patch.object(os, 'listdir')
|
||||
@mock.patch.object(ansible, 'run_playbook')
|
||||
@mock.patch.object(tempfile, 'mkdtemp')
|
||||
@ -440,6 +440,70 @@ class TestCase(unittest.TestCase):
|
||||
mock.call(os.path.join(dump_dir, "host2.yml")),
|
||||
])
|
||||
|
||||
@mock.patch.object(shutil, 'rmtree')
|
||||
@mock.patch.object(utils, 'read_file')
|
||||
@mock.patch.object(os, 'listdir')
|
||||
@mock.patch.object(ansible, 'run_playbook')
|
||||
@mock.patch.object(tempfile, 'mkdtemp')
|
||||
def test_config_dump_vaulted(self, mock_mkdtemp, mock_run, mock_listdir,
|
||||
mock_read, mock_rmtree):
|
||||
parser = argparse.ArgumentParser()
|
||||
parsed_args = parser.parse_args([])
|
||||
dump_dir = "/path/to/dump"
|
||||
mock_mkdtemp.return_value = dump_dir
|
||||
mock_listdir.return_value = ["host1.yml", "host2.yml"]
|
||||
config = """---
|
||||
key1: !vault |
|
||||
$ANSIBLE_VAULT;1.1;AES256
|
||||
633230623736383232323862393364323037343430393530316636363961626361393133646437
|
||||
643438663261356433656365646138666133383032376532310a63323432306431303437623637
|
||||
346236316161343635636230613838316566383933313338636237616338326439616536316639
|
||||
6334343462333062363334300a3930313762313463613537626531313230303731343365643766
|
||||
666436333037
|
||||
key2: value2
|
||||
key3:
|
||||
- !vault |
|
||||
$ANSIBLE_VAULT;1.1;AES256
|
||||
633230623736383232323862393364323037343430393530316636363961626361393133646437
|
||||
643438663261356433656365646138666133383032376532310a63323432306431303437623637
|
||||
346236316161343635636230613838316566383933313338636237616338326439616536316639
|
||||
6334343462333062363334300a3930313762313463613537626531313230303731343365643766
|
||||
666436333037
|
||||
"""
|
||||
config_nested = """---
|
||||
key1:
|
||||
key2: !vault |
|
||||
$ANSIBLE_VAULT;1.1;AES256
|
||||
633230623736383232323862393364323037343430393530316636363961626361393133646437
|
||||
643438663261356433656365646138666133383032376532310a63323432306431303437623637
|
||||
346236316161343635636230613838316566383933313338636237616338326439616536316639
|
||||
6334343462333062363334300a3930313762313463613537626531313230303731343365643766
|
||||
666436333037
|
||||
"""
|
||||
mock_read.side_effect = [config, config_nested]
|
||||
result = ansible.config_dump(parsed_args)
|
||||
expected_result = {
|
||||
"host1": {"key1": "******", "key2": "value2", "key3": ["******"]},
|
||||
"host2": {"key1": {"key2": "******"}},
|
||||
}
|
||||
self.assertEqual(result, expected_result)
|
||||
dump_config_path = utils.get_data_files_path(
|
||||
"ansible", "dump-config.yml")
|
||||
mock_run.assert_called_once_with(parsed_args,
|
||||
dump_config_path,
|
||||
extra_vars={
|
||||
"dump_path": dump_dir,
|
||||
},
|
||||
check_output=True, tags=None,
|
||||
verbose_level=None, check=False,
|
||||
list_tasks=False, diff=False)
|
||||
mock_rmtree.assert_called_once_with(dump_dir)
|
||||
mock_listdir.assert_any_call(dump_dir)
|
||||
mock_read.assert_has_calls([
|
||||
mock.call(os.path.join(dump_dir, "host1.yml")),
|
||||
mock.call(os.path.join(dump_dir, "host2.yml")),
|
||||
])
|
||||
|
||||
@mock.patch.object(utils, 'galaxy_role_install', autospec=True)
|
||||
@mock.patch.object(utils, 'is_readable_file', autospec=True)
|
||||
@mock.patch.object(os, 'makedirs', autospec=True)
|
||||
|
@ -17,6 +17,7 @@ import subprocess
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
from ansible.parsing.yaml.objects import AnsibleVaultEncryptedUnicode
|
||||
import yaml
|
||||
|
||||
from kayobe import exception
|
||||
@ -125,6 +126,59 @@ key2: value2
|
||||
mock_read.return_value = "[1{!"
|
||||
self.assertRaises(SystemExit, utils.read_yaml_file, "/path/to/file")
|
||||
|
||||
@mock.patch.object(utils, "read_file")
|
||||
def test_read_config_dump_yaml_file(self, mock_read):
|
||||
config = """---
|
||||
key1: value1
|
||||
key2: value2
|
||||
"""
|
||||
mock_read.return_value = config
|
||||
result = utils.read_config_dump_yaml_file("/path/to/file")
|
||||
self.assertEqual(result, {"key1": "value1", "key2": "value2"})
|
||||
mock_read.assert_called_once_with("/path/to/file")
|
||||
|
||||
@mock.patch.object(utils, "read_file")
|
||||
def test_read_config_dump_yaml_file_vaulted(self, mock_read):
|
||||
config = """---
|
||||
key1: !vault |
|
||||
$ANSIBLE_VAULT;1.1;AES256
|
||||
633230623736383232323862393364323037343430393530316636363961626361393133646437
|
||||
643438663261356433656365646138666133383032376532310a63323432306431303437623637
|
||||
346236316161343635636230613838316566383933313338636237616338326439616536316639
|
||||
6334343462333062363334300a3930313762313463613537626531313230303731343365643766
|
||||
666436333037
|
||||
key2: value2
|
||||
key3:
|
||||
- !vault |
|
||||
$ANSIBLE_VAULT;1.1;AES256
|
||||
633230623736383232323862393364323037343430393530316636363961626361393133646437
|
||||
643438663261356433656365646138666133383032376532310a63323432306431303437623637
|
||||
346236316161343635636230613838316566383933313338636237616338326439616536316639
|
||||
6334343462333062363334300a3930313762313463613537626531313230303731343365643766
|
||||
666436333037
|
||||
"""
|
||||
mock_read.return_value = config
|
||||
result = utils.read_config_dump_yaml_file("/path/to/file")
|
||||
# Can't read the value without an encryption key, so just check type.
|
||||
self.assertTrue(isinstance(result["key1"],
|
||||
AnsibleVaultEncryptedUnicode))
|
||||
self.assertEqual(result["key2"], "value2")
|
||||
self.assertTrue(isinstance(result["key3"][0],
|
||||
AnsibleVaultEncryptedUnicode))
|
||||
mock_read.assert_called_once_with("/path/to/file")
|
||||
|
||||
@mock.patch.object(utils, "read_file")
|
||||
def test_read_config_dump_yaml_file_open_failure(self, mock_read):
|
||||
mock_read.side_effect = IOError
|
||||
self.assertRaises(SystemExit, utils.read_config_dump_yaml_file,
|
||||
"/path/to/file")
|
||||
|
||||
@mock.patch.object(utils, "read_file")
|
||||
def test_read_config_dump_yaml_file_not_yaml(self, mock_read):
|
||||
mock_read.return_value = "[1{!"
|
||||
self.assertRaises(SystemExit, utils.read_config_dump_yaml_file,
|
||||
"/path/to/file")
|
||||
|
||||
@mock.patch.object(subprocess, "check_call")
|
||||
def test_run_command(self, mock_call):
|
||||
output = utils.run_command(["command", "to", "run"])
|
||||
|
@ -25,6 +25,7 @@ import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from ansible.parsing.yaml.loader import AnsibleLoader
|
||||
import yaml
|
||||
|
||||
from kayobe import exception
|
||||
@ -154,11 +155,28 @@ def read_yaml_file(path):
|
||||
try:
|
||||
content = read_file(path)
|
||||
except IOError as e:
|
||||
print("Failed to open config dump file %s: %s" %
|
||||
print("Failed to open YAML file %s: %s" %
|
||||
(path, repr(e)))
|
||||
sys.exit(1)
|
||||
try:
|
||||
return yaml.safe_load(content)
|
||||
except yaml.YAMLError as e:
|
||||
print("Failed to decode YAML file %s: %s" %
|
||||
(path, repr(e)))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def read_config_dump_yaml_file(path):
|
||||
"""Read and decode a configuration dump YAML file."""
|
||||
try:
|
||||
content = read_file(path)
|
||||
except IOError as e:
|
||||
print("Failed to open config dump file %s: %s" %
|
||||
(path, repr(e)))
|
||||
sys.exit(1)
|
||||
try:
|
||||
# AnsibleLoader supports loading vault encrypted variables.
|
||||
return AnsibleLoader(content).get_single_data()
|
||||
except yaml.YAMLError as e:
|
||||
print("Failed to decode config dump YAML file %s: %s" %
|
||||
(path, repr(e)))
|
||||
|
@ -0,0 +1,7 @@
|
||||
---
|
||||
fixes:
|
||||
- |
|
||||
Fixes an issue where ``kayobe configuration dump`` would fail when
|
||||
variables are encrypted using Ansible Vault. Encrypted variables are now
|
||||
sanitised in the dump output. `LP#2031390
|
||||
<https://bugs.launchpad.net/kayobe/+bug/2031390>`__
|
Loading…
x
Reference in New Issue
Block a user