Source code for cansible.variables
"""
Ansible variables file reader with vault support
Why not use the Ansible Python API? We don't have a lot to do here, and the CLI
are less likely to be subject to changes.
"""
import cli2
import functools
import subprocess
import yaml
from pathlib import Path
[docs]
class AnsibleVariablesError(Exception):
pass
[docs]
class PathNotFoundError(AnsibleVariablesError):
pass
[docs]
class UnresolvablePathError(AnsibleVariablesError):
pass
[docs]
class VaultPasswordFileRequiredError(AnsibleVariablesError):
pass
[docs]
class VaultPasswordFileNotFoundError(AnsibleVariablesError):
pass
[docs]
class Vault(yaml.YAMLObject):
yaml_tag = '!vault'
[docs]
@classmethod
def from_yaml(cls, loader, node):
"""
Convert a representation node to a Python object.
"""
return subprocess.check_output(
f'echo \'{node.value}\''
f' | {cls.ansible_vault}'
f' decrypt --vault-password-file {cls.pass_path}',
shell=True,
).decode().strip()
[docs]
class Variables(dict):
"""
Ansible variables reader.
In general, it should be instanciated with :py:attr:`root_path` and
:py:attr:`pass_path` to fully function correctly.
Example:
.. code-block:: python
import cansible
variables = cansible.Variables(
root_path=Path(__file__).parent,
pass_path='~/.vault_password',
)
print(variables['playbooks/vars/example.yml'])
Every file read is cached in the variables object.
.. py:attribute:: root_path
Unless you feed this with only absolute path, you'll need a root_path
so that relative paths can be resolved. This should be your collection
root.
.. py:attribute:: pass_path
Unless you don't use ansible-vault, you'll need to give the pass to the
vault password here.
"""
def __init__(self, root_path=None, pass_path=None):
self.root_path = Path(root_path) if root_path else None
self.pass_path = Path(pass_path) if pass_path else None
def __getitem__(self, key):
if key not in self:
self.read(key)
return super().__getitem__(key)
@functools.cached_property
def ansible_vault(self):
return cli2.which('ansible-vault')
[docs]
def read(self, path):
"""
Read an ansible YAML variable file.
:param path: Absolute path or path relative to :py:attr:`root_path`
"""
key = path
path = Path(path)
if path.is_absolute():
path = path
elif self.root_path:
path = self.root_path / path
else:
raise UnresolvablePathError(
f'{path} must be absolute if root_path not set'
)
if not path.exists():
raise PathNotFoundError(f'{path} does not exist')
with path.open('r') as f:
content = f.read()
if content.strip().startswith('$ANSIBLE_VAULT'):
if not self.pass_path:
raise VaultPasswordFileRequiredError(
'Vault password required in pass_path'
)
if not self.pass_path.exists():
raise VaultPasswordFileNotFoundError(
f'{self.pass_path} does not exist'
)
args = [
self.ansible_vault,
'view',
'--vault-password-file',
str(self.pass_path),
str(path),
]
content = subprocess.check_output(args)
# todo: find a thread safe way to use our YAMLObject
Vault.ansible_vault = self.ansible_vault
Vault.pass_path = self.pass_path
self[key] = yaml.load(content, Loader=yaml.FullLoader)
return self[key]