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_data = serializers.WritableField(required=False, default='')
ssh_key_unlock = serializers.WritableField(required=False, default='') ssh_key_unlock = serializers.WritableField(required=False, default='')
sudo_password = serializers.WritableField(required=False, default='') sudo_password = serializers.WritableField(required=False, default='')
vault_password = serializers.WritableField(required=False, default='')
class Meta: class Meta:
model = Credential model = Credential
fields = ('*', 'user', 'team', 'kind', 'cloud', 'username', fields = ('*', 'user', 'team', 'kind', 'cloud', 'username',
'password', 'ssh_key_data', 'ssh_key_unlock', 'password', 'ssh_key_data', 'ssh_key_unlock',
'sudo_username', 'sudo_password') 'sudo_username', 'sudo_password', 'vault_password')
def to_native(self, obj): def to_native(self, obj):
ret = super(CredentialSerializer, self).to_native(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', PASSWORD_FIELDS = ('password', 'ssh_key_data', 'ssh_key_unlock',
'sudo_password') 'sudo_password', 'vault_password')
class Meta: class Meta:
app_label = 'main' app_label = 'main'
@@ -263,10 +263,14 @@ class Credential(CommonModelNameNotUnique):
def needs_sudo_password(self): def needs_sudo_password(self):
return self.kind == 'ssh' and self.sudo_password == 'ASK' 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 @property
def passwords_needed(self): def passwords_needed(self):
needed = [] 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): if getattr(self, 'needs_%s' % field):
needed.append(field) needed.append(field)
return needed return needed

View File

@@ -176,9 +176,13 @@ class ProjectOptions(models.Model):
# show up. # show up.
matched = False matched = False
try: try:
for line in file(playbook): for n, line in enumerate(file(playbook)):
if valid_re.match(line): if valid_re.match(line):
matched = True 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: except IOError:
continue continue
if not matched: if not matched:

View File

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

View File

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

View File

@@ -2,6 +2,7 @@
# All Rights Reserved. # All Rights Reserved.
# Python # Python
from distutils.version import StrictVersion as Version
import os import os
import shutil import shutil
import tempfile import tempfile
@@ -87,6 +88,19 @@ TEST_ASYNC_NOWAIT_PLAYBOOK = '''
poll: 0 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----- TEST_SSH_KEY_DATA = '''-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAyQ8F5bbgjHvk4SZJsKI9OmJKMFxZqRhvx4LaqjLTKbBwRBsY MIIEpQIBAAKCAQEAyQ8F5bbgjHvk4SZJsKI9OmJKMFxZqRhvx4LaqjLTKbBwRBsY
1/C00NPiZn70dKbeyV7RNVZxuzM6yd3D3lwTdbDu/eJ0x72t3ch+TdLt/aenyy10 1/C00NPiZn70dKbeyV7RNVZxuzM6yd3D3lwTdbDu/eJ0x72t3ch+TdLt/aenyy10
@@ -198,6 +212,7 @@ class RunJobTest(BaseCeleryTest):
'password': '', 'password': '',
'sudo_username': '', 'sudo_username': '',
'sudo_password': '', 'sudo_password': '',
'vault_password': '',
} }
opts.update(kwargs) opts.update(kwargs)
self.credential = Credential.objects.create(**opts) self.credential = Credential.objects.create(**opts)
@@ -845,6 +860,56 @@ class RunJobTest(BaseCeleryTest):
self.assertTrue('ssh-agent' in job.job_args) self.assertTrue('ssh-agent' in job.job_args)
self.assertTrue('Bad passphrase' not in job.result_stdout) 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): def _test_cloud_credential_environment_variables(self, kind):
if kind == 'aws': if kind == 'aws':
env_var1 = 'AWS_ACCESS_KEY' env_var1 = 'AWS_ACCESS_KEY'