Browse Source

enough: playbook decrypts essential files when needed

Fixes: main/infrastructure#138
keep-around/9b0dec86d6a55a1cab65521f776e0ca094d2e9c5
Loïc Dachary 2 years ago
committed by Loic Dachary
parent
commit
9b0dec86d6
Signed by: dachary GPG Key ID: 992D23B392F9E4F2
  1. 2
      .gitignore
  2. 82
      enough/common/ansible_utils.py
  3. 33
      tests/enough/common/test_ansible_utils.py
  4. 1
      tests/enough/common/test_ansible_utils/domain.com.pass
  5. 6
      tests/enough/common/test_ansible_utils/domain.com/certs/encrypted.key
  6. 1
      tests/enough/common/test_ansible_utils/domain.com/certs/not-encrypted.key
  7. 6
      tests/enough/common/test_ansible_utils/domain.com/infrastructure_key
  8. 6
      tests/enough/common/test_ansible_utils/domain.com/inventory/group_vars/all/clouds.yml

2
.gitignore

@ -6,7 +6,7 @@ pytestdebug.log
docs/_build/
inventories/common/01-hosts.yml
*.pyc
infrastructure_key*
/infrastructure_key*
secret
inventories/common/group_vars/all/domain.yml
inventories/common/group_vars/all/clouds.yml

82
enough/common/ansible_utils.py

@ -9,6 +9,8 @@ import textwrap
from enough import settings
log = logging.getLogger(__name__)
def parse_output(output):
json_result = re.sub(r'.*?=> ', '', output)
@ -19,12 +21,11 @@ def bake_ansible_playbook():
args = ['-i', 'inventory']
if settings.CONFIG_DIR != '.':
args.extend(['-i', f'{settings.CONFIG_DIR}/inventory'])
logger = logging.getLogger(__name__)
return sh.ansible_playbook.bake(
*args,
_tee=True,
_out=lambda x: logger.info(x.strip()),
_err=lambda x: logger.info(x.strip()),
_out=lambda x: log.info(x.strip()),
_err=lambda x: log.info(x.strip()),
_truncate_exc=False,
_cwd=settings.SHARE_DIR,
_env={'ANSIBLE_NOCOLOR': 'true'},
@ -62,9 +63,59 @@ def get_variable(role, variable, host):
class Playbook(object):
class NoPasswordException(Exception):
pass
def __init__(self, **kwargs):
self.args = kwargs
self.sharedir = settings.SHARE_DIR
self.share_dir = settings.SHARE_DIR
self.config_dir = settings.CONFIG_DIR
@staticmethod
def is_encrypted(p):
if not os.path.exists(p):
return False
c = open(p).read()
return c.startswith('$ANSIBLE_VAULT')
@staticmethod
def encrypted_files(d):
return [
f'{d}/infrastructure_key',
f'{d}/inventory/group_vars/all/clouds.yml',
] + glob.glob(f'{d}/certs/*.key')
def ensure_decrypted(self):
encrypted = [f for f in self.encrypted_files(self.config_dir)
if self.is_encrypted(f)]
if len(encrypted) == 0:
return False
vault_password_option = self.vault_password_option()
if not vault_password_option:
raise Playbook.NoPasswordException(
f'{encrypted} are encrypted but {self.config_dir}.pass does not exist')
for f in encrypted:
log.info(f'decrypt {f}')
sh.ansible_vault.decrypt(
vault_password_option,
f,
_tee=True,
_out=lambda x: log.info(x.strip()),
_err=lambda x: log.info(x.strip()),
_truncate_exc=False,
_env={
'ANSIBLE_NOCOLOR': 'true',
}
)
return True
def vault_password_option(self):
password_file = f'{self.config_dir}.pass'
if os.path.exists(password_file):
return f'--vault-password-file={password_file}'
else:
log.info(f'no decryption because {password_file} does not exist')
return None
@staticmethod
def roles_path(d):
@ -74,13 +125,12 @@ class Playbook(object):
def bake(self):
args = [
'-i', f'{settings.SHARE_DIR}/inventory',
'-i', f'{self.share_dir}/inventory',
]
password_file = f'{settings.CONFIG_DIR}.pass'
if os.path.exists(password_file):
args.extend(['--vault-password-file', password_file])
if settings.SHARE_DIR != settings.CONFIG_DIR:
args.extend(['-i', f'{settings.CONFIG_DIR}/inventory'])
if self.vault_password_option():
args.append(self.vault_password_option())
if self.share_dir != self.config_dir:
args.extend(['-i', f'{self.config_dir}/inventory'])
logger = logging.getLogger(__name__)
return sh.ansible_playbook.bake(
*args,
@ -89,20 +139,20 @@ class Playbook(object):
_err=lambda x: logger.info(x.strip()),
_truncate_exc=False,
_env={
'SHARE_DIR': settings.SHARE_DIR,
'CONFIG_DIR': settings.CONFIG_DIR,
'ANSIBLE_ROLES_PATH': self.roles_path(settings.SHARE_DIR),
'SHARE_DIR': self.share_dir,
'CONFIG_DIR': self.config_dir,
'ANSIBLE_ROLES_PATH': self.roles_path(self.share_dir),
'ANSIBLE_NOCOLOR': 'true',
},
)
def run(self):
self.ensure_decrypted()
if not self.args['args']:
args = [
'--private-key', f'{settings.CONFIG_DIR}/infrastructure_key',
f'{settings.CONFIG_DIR}/enough-playbook.yml'
'--private-key', f'{self.config_dir}/infrastructure_key',
f'{self.config_dir}/enough-playbook.yml'
]
else:
args = self.args['args'][1:]
self.bake()(*args)
print("OK")

33
tests/enough/common/test_ansible_utils.py

@ -1,5 +1,7 @@
from enough.common import ansible_utils
import json
import pytest
import shutil
import yaml
@ -22,6 +24,37 @@ def test_playbook_roles_path():
assert '/infrastructure/' in r
def test_playbook_ensure_decrypted(tmpdir):
p = ansible_utils.Playbook()
shutil.copytree('tests/enough/common/test_ansible_utils/domain.com',
f'{tmpdir}/domain.com')
c = f'{tmpdir}/domain.com'
p.config_dir = c
#
# decryption is needed but no password is found
#
with pytest.raises(ansible_utils.Playbook.NoPasswordException):
assert p.ensure_decrypted() is False
shutil.copyfile('tests/enough/common/test_ansible_utils/domain.com.pass',
f'{tmpdir}/domain.com.pass')
#
# all files are decrypted
#
for f in p.encrypted_files(c):
if f.endswith('not-encrypted.key'):
continue
assert p.is_encrypted(f)
assert p.ensure_decrypted() is True
for f in p.encrypted_files(c):
assert not p.is_encrypted(f)
#
# nothing to do
#
assert p.ensure_decrypted() is False
def test_playbook_run_with_args(capsys, caplog):
kwargs = {
'args': ['--', 'tests/enough/common/test_ansible_utils/playbook-ok.yml'],

1
tests/enough/common/test_ansible_utils/domain.com.pass

@ -0,0 +1 @@
PASSWORD

6
tests/enough/common/test_ansible_utils/domain.com/certs/encrypted.key

@ -0,0 +1,6 @@
$ANSIBLE_VAULT;1.1;AES256
63366165353634313437346634653732386434636638623965366461306363383830643465306630
3036323438613461306335346563343866613931613936630a376562636134326632306138663735
30343165326230386432656531643263643536303230623932616237353138646365356632346431
3461353937306531660a613937393036326437316332323561663164316261623161386434666161
6134

1
tests/enough/common/test_ansible_utils/domain.com/certs/not-encrypted.key

@ -0,0 +1 @@
SOMETHINGELSE

6
tests/enough/common/test_ansible_utils/domain.com/infrastructure_key

@ -0,0 +1,6 @@
$ANSIBLE_VAULT;1.1;AES256
62353061346563616139313939393030356430333233326432623734623737623830613064326239
3563323430303162373861396538343561633137393637330a373233326332353264623539333235
39333363363430646336363532666236366466663861303839343837643332336539363135326631
3232373965666565310a653031623862666536626637636639633465653432336630383035326332
6233

6
tests/enough/common/test_ansible_utils/domain.com/inventory/group_vars/all/clouds.yml

@ -0,0 +1,6 @@
$ANSIBLE_VAULT;1.1;AES256
66336136613834646663376166613936323664633164353036656536613338323365303638373330
3362303961343530333466666132623964356332386362660a316130346164363639633461386231
32316638366639346363353238373733656364383965633662303439363237316563303865313535
6638393430636430300a313962363565303665356134316161336238306436333936343562306662
6132
Loading…
Cancel
Save