AC-1060 Add API support for vault password.

This commit is contained in:
Chris Church 2014-03-25 22:54:14 -04:00
parent 8ad72426b4
commit ac0927f430
6 changed files with 99 additions and 11 deletions

View File

@ -980,12 +980,13 @@ class CredentialSerializer(BaseSerializer):
ssh_key_data = serializers.WritableField(required=False, default='')
ssh_key_unlock = serializers.WritableField(required=False, default='')
sudo_password = serializers.WritableField(required=False, default='')
vault_password = serializers.WritableField(required=False, default='')
class Meta:
model = Credential
fields = ('*', 'user', 'team', 'kind', 'cloud', 'username',
'password', 'ssh_key_data', 'ssh_key_unlock',
'sudo_username', 'sudo_password')
'sudo_username', 'sudo_password', 'vault_password')
def to_native(self, obj):
ret = super(CredentialSerializer, self).to_native(obj)

View File

@ -164,7 +164,7 @@ class Credential(CommonModelNameNotUnique):
]
PASSWORD_FIELDS = ('password', 'ssh_key_data', 'ssh_key_unlock',
'sudo_password')
'sudo_password', 'vault_password')
class Meta:
app_label = 'main'
@ -263,10 +263,14 @@ class Credential(CommonModelNameNotUnique):
def needs_sudo_password(self):
return self.kind == 'ssh' and self.sudo_password == 'ASK'
@property
def needs_vault_password(self):
return self.kind == 'ssh' and self.vault_password == 'ASK'
@property
def passwords_needed(self):
needed = []
for field in ('password', 'sudo_password', 'ssh_key_unlock'):
for field in ('password', 'sudo_password', 'ssh_key_unlock', 'vault_password'):
if getattr(self, 'needs_%s' % field):
needed.append(field)
return needed

View File

@ -176,9 +176,13 @@ class ProjectOptions(models.Model):
# show up.
matched = False
try:
for line in file(playbook):
for n, line in enumerate(file(playbook)):
if valid_re.match(line):
matched = True
# Any YAML file can also be encrypted with vault;
# allow these to be used as the main playbook.
elif n == 0 and line.startswith('$ANSIBLE_VAULT;'):
matched = True
except IOError:
continue
if not matched:

View File

@ -5,7 +5,7 @@
import ConfigParser
import cStringIO
import datetime
import distutils.version
from distutils.version import StrictVersion as Version
import functools
import json
import logging
@ -317,6 +317,8 @@ class BaseTask(Task):
instance = self.update_model(pk)
status = instance.status
raise RuntimeError('not starting %s task' % instance.status)
# Fetch ansible version once here to support version-dependent features.
kwargs['ansible_version'] = get_ansible_version()
kwargs['private_data_file'] = self.build_private_data_file(instance, **kwargs)
kwargs['passwords'] = self.build_passwords(instance, **kwargs)
args = self.build_args(instance, **kwargs)
@ -377,12 +379,13 @@ class RunJob(BaseTask):
def build_passwords(self, job, **kwargs):
'''
Build a dictionary of passwords for SSH private key, SSH user and sudo.
Build a dictionary of passwords for SSH private key, SSH user, sudo
and ansible-vault.
'''
passwords = super(RunJob, self).build_passwords(job, **kwargs)
creds = job.credential
if creds:
for field in ('ssh_key_unlock', 'ssh_password', 'sudo_password'):
for field in ('ssh_key_unlock', 'ssh_password', 'sudo_password', 'vault_password'):
if field == 'ssh_password':
value = kwargs.get(field, decrypt_field(creds, 'password'))
else:
@ -413,8 +416,7 @@ class RunJob(BaseTask):
# When using Ansible >= 1.3, allow the inventory script to include host
# variables inline via ['_meta']['hostvars'].
try:
Version = distutils.version.StrictVersion
if Version(get_ansible_version()) >= Version('1.3'):
if Version(kwargs['ansible_version']) >= Version('1.3'):
env['INVENTORY_HOSTVARS'] = str(True)
except ValueError:
pass
@ -461,6 +463,15 @@ class RunJob(BaseTask):
args.extend(['-U', sudo_username])
if 'sudo_password' in kwargs.get('passwords', {}):
args.append('--ask-sudo-pass')
# When using Ansible >= 1.5, support prompting for a vault password.
try:
if Version(kwargs['ansible_version']) >= Version('1.5'):
if 'vault_password' in kwargs.get('passwords', {}):
args.append('--ask-vault-pass')
except ValueError:
pass
if job.forks: # FIXME: Max limit?
args.append('--forks=%d' % job.forks)
if job.limit:
@ -497,6 +508,7 @@ class RunJob(BaseTask):
d[re.compile(r'^sudo password.*:\s*?$', re.M)] = 'sudo_password'
d[re.compile(r'^SSH password:\s*?$', re.M)] = 'ssh_password'
d[re.compile(r'^Password:\s*?$', re.M)] = 'ssh_password'
d[re.compile(r'^Vault password:\s*?$', re.M)] = 'vault_password'
return d
def pre_run_check(self, job, **kwargs):
@ -591,8 +603,7 @@ class RunProjectUpdate(BaseTask):
# the git module.
if scm_type == 'git' and scm_url_parts.scheme == 'ssh':
try:
Version = distutils.version.StrictVersion
if Version(get_ansible_version()) >= Version('1.5'):
if Version(kwargs['ansible_version']) >= Version('1.5'):
extra_vars['scm_accept_hostkey'] = 'true'
except ValueError:
pass

View File

@ -27,6 +27,7 @@ from awx.main.models import *
from awx.main.backend import LDAPSettings
from awx.main.management.commands.run_callback_receiver import run_subscriber
from awx.main.management.commands.run_task_system import run_taskmanager
from awx.main.utils import get_ansible_version
class BaseTestMixin(object):
@ -40,6 +41,8 @@ class BaseTestMixin(object):
self._temp_project_dirs = []
self._current_auth = None
self._user_passwords = {}
self.ansible_version = get_ansible_version()
self.assertNotEqual(self.ansible_version, 'unknown')
# Wrap settings so we can redefine them within each test.
self._wrapped = settings._wrapped
settings._wrapped = UserSettingsHolder(settings._wrapped)

View File

@ -2,6 +2,7 @@
# All Rights Reserved.
# Python
from distutils.version import StrictVersion as Version
import os
import shutil
import tempfile
@ -87,6 +88,19 @@ TEST_ASYNC_NOWAIT_PLAYBOOK = '''
poll: 0
'''
TEST_VAULT_PLAYBOOK = '''$ANSIBLE_VAULT;1.1;AES256
35623233333035633365383330323835353564346534363762366465316263363463396162656432
6562643539396330616265616532656466353639303338650a313466333663646431646663333739
32623935316439343636633462373633653039646336376361386439386661366434333830383634
6266613530626633390a363532373562353262323863343830343865303663306335643430396239
63393963623537326366663332656132653465646332343234656237316537643135313932623237
66313863396463343232383131633531363239396636363165646562396261626633326561313837
32383634326230656230386237333561373630343233353239613463626538356338326633386434
36396639313030336165366266646431306665336662663732313762663938666239663233393964
30393733393331383132306463656636396566373961383865643562383564356363'''
TEST_VAULT_PASSWORD = '1234'
TEST_SSH_KEY_DATA = '''-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAyQ8F5bbgjHvk4SZJsKI9OmJKMFxZqRhvx4LaqjLTKbBwRBsY
1/C00NPiZn70dKbeyV7RNVZxuzM6yd3D3lwTdbDu/eJ0x72t3ch+TdLt/aenyy10
@ -198,6 +212,7 @@ class RunJobTest(BaseCeleryTest):
'password': '',
'sudo_username': '',
'sudo_password': '',
'vault_password': '',
}
opts.update(kwargs)
self.credential = Credential.objects.create(**opts)
@ -845,6 +860,56 @@ class RunJobTest(BaseCeleryTest):
self.assertTrue('ssh-agent' in job.job_args)
self.assertTrue('Bad passphrase' not in job.result_stdout)
def test_vault_password(self):
self.create_test_credential(vault_password=TEST_VAULT_PASSWORD)
self.create_test_project(TEST_VAULT_PLAYBOOK)
job_template = self.create_test_job_template()
job = self.create_test_job(job_template=job_template)
self.assertEqual(job.status, 'new')
self.assertFalse(job.passwords_needed_to_start)
self.assertTrue(job.signal_start())
job = Job.objects.get(pk=job.pk)
if Version(self.ansible_version) >= Version('1.5'):
self.check_job_result(job, 'successful')
self.assertTrue('--ask-vault-pass' in job.job_args)
else:
self.check_job_result(job, 'failed')
self.assertFalse('--ask-vault-pass' in job.job_args)
def test_vault_ask_password(self):
self.create_test_credential(vault_password='ASK')
self.create_test_project(TEST_VAULT_PLAYBOOK)
job_template = self.create_test_job_template()
job = self.create_test_job(job_template=job_template)
self.assertEqual(job.status, 'new')
self.assertTrue(job.passwords_needed_to_start)
self.assertTrue('vault_password' in job.passwords_needed_to_start)
self.assertFalse(job.signal_start())
self.assertEqual(job.status, 'new')
self.assertTrue(job.signal_start(vault_password=TEST_VAULT_PASSWORD))
job = Job.objects.get(pk=job.pk)
if Version(self.ansible_version) >= Version('1.5'):
self.check_job_result(job, 'successful')
self.assertTrue('--ask-vault-pass' in job.job_args)
else:
self.check_job_result(job, 'failed')
self.assertFalse('--ask-vault-pass' in job.job_args)
def test_vault_bad_password(self):
self.create_test_credential(vault_password='not it')
self.create_test_project(TEST_VAULT_PLAYBOOK)
job_template = self.create_test_job_template()
job = self.create_test_job(job_template=job_template)
self.assertEqual(job.status, 'new')
self.assertFalse(job.passwords_needed_to_start)
self.assertTrue(job.signal_start())
job = Job.objects.get(pk=job.pk)
self.check_job_result(job, 'failed')
if Version(self.ansible_version) >= Version('1.5'):
self.assertTrue('--ask-vault-pass' in job.job_args)
else:
self.assertFalse('--ask-vault-pass' in job.job_args)
def _test_cloud_credential_environment_variables(self, kind):
if kind == 'aws':
env_var1 = 'AWS_ACCESS_KEY'