AC-626 Removed support for prompting for password and ssh_key_unlock for scm/cloud credentials.

This commit is contained in:
Chris Church
2013-11-16 17:59:51 -05:00
parent f254f8bc92
commit 621cbb9f66
9 changed files with 48 additions and 144 deletions

View File

@@ -1,18 +1,13 @@
# Update Inventory Source # Update Inventory Source
Make a GET request to this resource to determine if the group can be updated Make a GET request to this resource to determine if the group can be updated
from its inventory source and whether any passwords are required for the from its inventory source. The response will include the following field:
update. The response will include the following fields:
* `can_start`: Flag indicating if this job can be started (boolean, read-only) * `can_update`: Flag indicating if this inventory source can be updated
* `passwords_needed_to_update`: Password names required to update from the (boolean, read-only)
inventory source (array, read-only)
Make a POST request to this resource to update the inventory source. If any Make a POST request to this resource to update the inventory source. If
passwords are required, they must be passed via POST data. successful, the response status code will be 202. If the inventory source is
not defined or cannot be updated, a 405 status code will be returned.
If successful, the response status code will be 202. If any required passwords
are not provided, a 400 status code will be returned. If the inventory source
is not defined or cannot be updated, a 405 status code will be returned.
{% include "api/_new_in_awx.md" %} {% include "api/_new_in_awx.md" %}

View File

@@ -5,7 +5,8 @@ whether any passwords are required to start the job. The response will include
the following fields: the following fields:
* `can_start`: Flag indicating if this job can be started (boolean, read-only) * `can_start`: Flag indicating if this job can be started (boolean, read-only)
* `passwords_needed_to_start`: Password names required to start the job (array, read-only) * `passwords_needed_to_start`: Password names required to start the job (array,
read-only)
Make a POST request to this resource to start the job. If any passwords are Make a POST request to this resource to start the job. If any passwords are
required, they must be passed via POST data. required, they must be passed via POST data.

View File

@@ -1,19 +1,12 @@
# Update Project # Update Project
Make a GET request to this resource to determine if the project can be updated Make a GET request to this resource to determine if the project can be updated
from its SCM source and whether any passwords are required for the update. The from its SCM source. The response will include the following field:
response will include the following fields:
* `can_update`: Flag indicating if this project can be updated (boolean, * `can_update`: Flag indicating if this project can be updated (boolean,
read-only) read-only)
* `passwords_needed_to_update`: Password names required to update the project
(array, read-only)
Make a POST request to this resource to update the project. If any passwords Make a POST request to this resource to update the project. If the project
are required, they must be passed via POST data. cannot be updated, a 405 status code will be returned.
If successful, the response status code will be 202. If any required passwords
are not provided, a 400 status code will be returned. If the project cannot be
updated, a 405 status code will be returned.
{% include "api/_new_in_awx.md" %} {% include "api/_new_in_awx.md" %}

View File

@@ -299,8 +299,6 @@ class ProjectUpdateView(GenericAPIView):
data = dict( data = dict(
can_update=obj.can_update, can_update=obj.can_update,
) )
if obj.scm_type:
data['passwords_needed_to_update'] = obj.scm_passwords_needed
return Response(data) return Response(data)
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
@@ -308,8 +306,7 @@ class ProjectUpdateView(GenericAPIView):
if obj.can_update: if obj.can_update:
project_update = obj.update(**request.DATA) project_update = obj.update(**request.DATA)
if not project_update: if not project_update:
data = dict(passwords_needed_to_update=obj.scm_passwords_needed) return Response({}, status=status.HTTP_400_BAD_REQUEST)
return Response(data, status=status.HTTP_400_BAD_REQUEST)
else: else:
headers = {'Location': project_update.get_absolute_url()} headers = {'Location': project_update.get_absolute_url()}
return Response(status=status.HTTP_202_ACCEPTED, headers=headers) return Response(status=status.HTTP_202_ACCEPTED, headers=headers)

View File

@@ -143,8 +143,6 @@ class JobTemplate(CommonModel):
needed.append('ssh_password') needed.append('ssh_password')
else: else:
needed.append(pw) needed.append(pw)
if self.project.scm_update_on_launch:
needed.extend(self.project.scm_passwords_needed)
return bool(self.credential and not len(needed)) return bool(self.credential and not len(needed))
class Job(CommonTask): class Job(CommonTask):
@@ -262,8 +260,6 @@ class Job(CommonTask):
needed.append('ssh_password') needed.append('ssh_password')
else: else:
needed.append(pw) needed.append(pw)
if self.project.scm_update_on_launch:
needed.extend(self.project.scm_passwords_needed)
return needed return needed
def _get_task_class(self): def _get_task_class(self):

View File

@@ -200,7 +200,8 @@ class Credential(CommonModelNameNotUnique):
default='', default='',
max_length=1024, max_length=1024,
verbose_name=_('Password'), verbose_name=_('Password'),
help_text=_('Password for this credential.'), help_text=_('Password for this credential (or "ASK" to prompt the '
'user for machine credentials).'),
) )
ssh_key_data = models.TextField( ssh_key_data = models.TextField(
blank=True, blank=True,
@@ -214,7 +215,7 @@ class Credential(CommonModelNameNotUnique):
default='', default='',
verbose_name=_('SSH key unlock'), verbose_name=_('SSH key unlock'),
help_text=_('Passphrase to unlock SSH private key if encrypted (or ' help_text=_('Passphrase to unlock SSH private key if encrypted (or '
'"ASK" to prompt the user).'), '"ASK" to prompt the user for machine credentials).'),
) )
sudo_username = models.CharField( sudo_username = models.CharField(
max_length=1024, max_length=1024,
@@ -231,16 +232,16 @@ class Credential(CommonModelNameNotUnique):
@property @property
def needs_password(self): def needs_password(self):
return not self.ssh_key_data and self.password == 'ASK' return self.kind == 'ssh' and self.password == 'ASK'
@property @property
def needs_ssh_key_unlock(self): def needs_ssh_key_unlock(self):
return 'ENCRYPTED' in decrypt_field(self, 'ssh_key_data') and \ return self.kind == 'ssh' and self.ssh_key_unlock == 'ASK' and \
(not self.ssh_key_unlock or self.ssh_key_unlock == 'ASK') 'ENCRYPTED' in decrypt_field(self, 'ssh_key_data')
@property @property
def needs_sudo_password(self): def needs_sudo_password(self):
return self.sudo_password == 'ASK' return self.kind == 'ssh' and self.sudo_password == 'ASK'
@property @property
def passwords_needed(self): def passwords_needed(self):
@@ -321,7 +322,8 @@ class Credential(CommonModelNameNotUnique):
# If update_fields has been specified, add our field names to it, # If update_fields has been specified, add our field names to it,
# if hit hasn't been specified, then we're just doing a normal save. # if hit hasn't been specified, then we're just doing a normal save.
for field in self.PASSWORD_FIELDS: for field in self.PASSWORD_FIELDS:
encrypted = encrypt_field(self, field, bool(field != 'ssh_key_data')) ask = bool(self.kind == 'ssh' and field != 'ssh_key_data')
encrypted = encrypt_field(self, field, ask)
setattr(self, field, encrypted) setattr(self, field, encrypted)
if field not in update_fields: if field not in update_fields:
update_fields.append(field) update_fields.append(field)

View File

@@ -186,27 +186,10 @@ class Project(CommonModel):
update_fields.append('local_path') update_fields.append('local_path')
if update_fields: if update_fields:
self.save(update_fields=update_fields) self.save(update_fields=update_fields)
# If we just created a new project with SCM and it doesn't require any # If we just created a new project with SCM, start the initial update.
# passwords to update, start the initial update. if new_instance and self.scm_type:
if new_instance and self.scm_type and not self.scm_passwords_needed:
self.update() self.update()
@property
def needs_scm_password(self):
return self.credential and self.credential.needs_password
@property
def needs_scm_key_unlock(self):
return self.credential and self.credential.needs_ssh_key_unlock
@property
def scm_passwords_needed(self):
needed = []
for field in ('scm_password', 'scm_key_unlock'):
if getattr(self, 'needs_%s' % field):
needed.append(field)
return needed
def set_status_and_last_updated(self, save=True): def set_status_and_last_updated(self, save=True):
# Determine current status. # Determine current status.
if self.scm_type: if self.scm_type:
@@ -256,12 +239,8 @@ class Project(CommonModel):
def update(self, **kwargs): def update(self, **kwargs):
if self.can_update: if self.can_update:
needed = self.scm_passwords_needed
opts = dict([(field, kwargs.get(field, '')) for field in needed])
if not all(opts.values()):
return
project_update = self.project_updates.create() project_update = self.project_updates.create()
project_update.start(**opts) project_update.start()
return project_update return project_update
def get_absolute_url(self): def get_absolute_url(self):
@@ -332,9 +311,6 @@ class ProjectUpdate(CommonTask):
from awx.main.tasks import RunProjectUpdate from awx.main.tasks import RunProjectUpdate
return RunProjectUpdate return RunProjectUpdate
def _get_passwords_needed_to_start(self):
return self.project.scm_passwords_needed
def _update_parent_instance(self): def _update_parent_instance(self):
parent_instance = self._get_parent_instance() parent_instance = self._get_parent_instance()
if parent_instance: if parent_instance:

View File

@@ -440,9 +440,7 @@ class RunJob(BaseTask):
try: try:
project_update = pu_qs[0] project_update = pu_qs[0]
except IndexError: except IndexError:
kw = dict([(k,v) for k,v in kwargs.items() project_update = project.update()
if k.startswith('scm_')])
project_update = project.update(**kw)
if not project_update: if not project_update:
msg = 'Unable to update project before launch.' msg = 'Unable to update project before launch.'
job = self.update_model(pk, status='error', job = self.update_model(pk, status='error',
@@ -460,10 +458,7 @@ class RunJob(BaseTask):
try: try:
inventory_update = iu_qs.filter(inventory_source=inventory_source)[0] inventory_update = iu_qs.filter(inventory_source=inventory_source)[0]
except IndexError: except IndexError:
# FIXME: Doesn't support multiple sources!!! inventory_update = inventory_source.update()
kw = dict([(k,v) for k,v in kwargs.items()
if k.startswith('source_')])
inventory_update = inventory_source.update(**kw)
if not inventory_update: if not inventory_update:
msgs.append('Unable to update inventory source %d before launch' % inventory_source.pk) msgs.append('Unable to update inventory source %d before launch' % inventory_source.pk)
continue continue
@@ -528,12 +523,12 @@ class RunProjectUpdate(BaseTask):
**kwargs) **kwargs)
project = project_update.project project = project_update.project
if project.credential: if project.credential:
value = kwargs.get('scm_key_unlock', decrypt_field(project.credential, 'ssh_key_unlock')) value = decrypt_field(project.credential, 'ssh_key_unlock')
if value not in ('', 'ASK'): if value not in ('', 'ASK'):
passwords['scm_key_unlock'] = value passwords['scm_key_unlock'] = value
passwords['scm_username'] = project.credential.username passwords['scm_username'] = project.credential.username
passwords['scm_password'] = kwargs.get('scm_password', passwords['scm_password'] = decrypt_field(project.credential,
decrypt_field(project.credential, 'password')) 'password')
return passwords return passwords
def build_env(self, project_update, **kwargs): def build_env(self, project_update, **kwargs):

View File

@@ -918,7 +918,7 @@ class ProjectUpdatesTest(BaseTransactionTest):
scm_password = kwargs.get('scm_password', scm_password = kwargs.get('scm_password',
decrypt_field(project.credential, decrypt_field(project.credential,
'password')) 'password'))
if scm_password not in ('', 'ASK'): if scm_password:
self.assertFalse(scm_password in pu.job_args, pu.job_args) self.assertFalse(scm_password in pu.job_args, pu.job_args)
self.assertFalse(scm_password in json.dumps(pu.job_env), self.assertFalse(scm_password in json.dumps(pu.job_env),
json.dumps(pu.job_env)) json.dumps(pu.job_env))
@@ -932,7 +932,7 @@ class ProjectUpdatesTest(BaseTransactionTest):
scm_key_unlock = kwargs.get('scm_key_unlock', scm_key_unlock = kwargs.get('scm_key_unlock',
decrypt_field(project.credential, decrypt_field(project.credential,
'ssh_key_unlock')) 'ssh_key_unlock'))
if scm_key_unlock not in ('', 'ASK'): if scm_key_unlock:
self.assertFalse(scm_key_unlock in pu.job_args, pu.job_args) self.assertFalse(scm_key_unlock in pu.job_args, pu.job_args)
self.assertFalse(scm_key_unlock in json.dumps(pu.job_env), self.assertFalse(scm_key_unlock in json.dumps(pu.job_env),
json.dumps(pu.job_env)) json.dumps(pu.job_env))
@@ -968,7 +968,7 @@ class ProjectUpdatesTest(BaseTransactionTest):
project_path = project.get_project_path(check_if_exists=False) project_path = project.get_project_path(check_if_exists=False)
# If project could be auto-updated on creation, the project dir should # If project could be auto-updated on creation, the project dir should
# already exist, otherwise run an initial checkout. # already exist, otherwise run an initial checkout.
if project.scm_type and not project.scm_passwords_needed: if project.scm_type:
self.assertTrue(project.last_update) self.assertTrue(project.last_update)
self.check_project_update(project, self.check_project_update(project,
project_udpate=project.last_update) project_udpate=project.last_update)
@@ -1035,7 +1035,8 @@ class ProjectUpdatesTest(BaseTransactionTest):
# Change username/password for private projects and verify the update # Change username/password for private projects and verify the update
# fails (but doesn't cause the task to hang). # fails (but doesn't cause the task to hang).
scm_url_parts = urlparse.urlsplit(project.scm_url) scm_url_parts = urlparse.urlsplit(project.scm_url)
if 0 and project.scm_username and project.scm_password not in ('', 'ASK'): # FIXME: Implement these tests again with new credentials!
if 0 and project.scm_username and project.scm_password:
scm_username = project.scm_username scm_username = project.scm_username
should_still_fail = not (getpass.getuser() == scm_username and should_still_fail = not (getpass.getuser() == scm_username and
scm_url_parts.hostname == 'localhost' and scm_url_parts.hostname == 'localhost' and
@@ -1344,59 +1345,6 @@ class ProjectUpdatesTest(BaseTransactionTest):
) )
self.check_project_scm(project) self.check_project_scm(project)
def test_prompt_for_scm_password_on_update(self):
scm_url = getattr(settings, 'TEST_GIT_PUBLIC_HTTPS',
'https://github.com/ansible/ansible.github.com.git')
if not all([scm_url]):
self.skipTest('no public git repo defined for https!')
project = self.create_project(
name='my public git project over https',
scm_type='git',
scm_url=scm_url,
scm_username='nobody',
scm_password='ASK',
)
url = reverse('api:project_update_view', args=(project.pk,))
with self.current_user(self.super_django_user):
response = self.get(url, expect=200)
self.assertTrue(response['can_update'])
self.assertTrue('scm_password' in response['passwords_needed_to_update'])
with self.current_user(self.super_django_user):
response = self.post(url, {}, expect=400)
self.assertTrue('scm_password' in response['passwords_needed_to_update'])
with self.current_user(self.super_django_user):
response = self.post(url, {'scm_password': 'blah1234'}, expect=202)
project_update = project.project_updates.order_by('-pk')[0]
self.check_project_update(project, should_fail=False,
scm_password='blah1234',
project_update=project_update)
def test_prompt_for_scm_key_unlock_on_update(self):
scm_url = 'git@github.com:ansible/ansible.github.com.git'
project = self.create_project(
name='my public git project over ssh',
scm_type='git',
scm_url=scm_url,
scm_key_data=TEST_SSH_KEY_DATA_LOCKED,
scm_key_unlock='ASK',
)
url = reverse('api:project_update_view', args=(project.pk,))
with self.current_user(self.super_django_user):
response = self.get(url, expect=200)
self.assertTrue(response['can_update'])
self.assertTrue('scm_key_unlock' in response['passwords_needed_to_update'])
with self.current_user(self.super_django_user):
response = self.post(url, {}, expect=400)
self.assertTrue('scm_key_unlock' in response['passwords_needed_to_update'])
with self.current_user(self.super_django_user):
response = self.post(url, {'scm_key_unlock': TEST_SSH_KEY_DATA_UNLOCK}, expect=202)
project_update = project.project_updates.order_by('-pk')[0]
self.check_project_update(project, should_fail=None,
scm_key_unlock=TEST_SSH_KEY_DATA_UNLOCK,
project_update=project_update)
# Verify that we responded to ssh-agent prompt.
self.assertTrue('Identity added' in project_update.result_stdout)
def create_test_job_template(self, **kwargs): def create_test_job_template(self, **kwargs):
opts = { opts = {
'name': 'test-job-template %s' % str(now()), 'name': 'test-job-template %s' % str(now()),
@@ -1491,33 +1439,34 @@ class ProjectUpdatesTest(BaseTransactionTest):
scm_type='git', scm_type='git',
scm_url=scm_url, scm_url=scm_url,
scm_username=scm_username, scm_username=scm_username,
scm_password='ASK', scm_password=scm_password,
scm_update_on_launch=True, scm_update_on_launch=True,
) )
self.check_project_update(self.project, scm_password=scm_password) self.check_project_update(self.project)
self.assertEqual(self.project.project_updates.count(), 1) self.assertEqual(self.project.project_updates.count(), 2)
job_template = self.create_test_job_template() job_template = self.create_test_job_template()
job = self.create_test_job(job_template=job_template) job = self.create_test_job(job_template=job_template)
self.assertEqual(job.status, 'new') self.assertEqual(job.status, 'new')
self.assertTrue(job.passwords_needed_to_start) self.assertFalse(job.passwords_needed_to_start)
self.assertTrue('scm_password' in job.passwords_needed_to_start) self.assertTrue(job.start())
self.assertTrue(job.start(**{'scm_password': scm_password}))
self.assertEqual(job.status, 'pending') self.assertEqual(job.status, 'pending')
job = Job.objects.get(pk=job.pk) job = Job.objects.get(pk=job.pk)
self.assertTrue(job.status in ('successful', 'failed'), self.assertTrue(job.status in ('successful', 'failed'),
job.result_stdout + job.result_traceback) job.result_stdout + job.result_traceback)
self.assertEqual(self.project.project_updates.count(), 2) self.assertEqual(self.project.project_updates.count(), 3)
# Try again but with a bad password - the job should flag an error # Try again but set a bad project password - the job should flag an
# because the project update failed. # error because the project update failed.
cred = self.project.credential
cred.password = 'bad scm password'
cred.save()
job = self.create_test_job(job_template=job_template) job = self.create_test_job(job_template=job_template)
self.assertEqual(job.status, 'new') self.assertEqual(job.status, 'new')
self.assertTrue(job.passwords_needed_to_start) self.assertFalse(job.passwords_needed_to_start)
self.assertTrue('scm_password' in job.passwords_needed_to_start) self.assertTrue(job.start())
self.assertTrue(job.start(**{'scm_password': 'lasdkfjlsdkfj'}))
self.assertEqual(job.status, 'pending') self.assertEqual(job.status, 'pending')
job = Job.objects.get(pk=job.pk) job = Job.objects.get(pk=job.pk)
# FIXME: Not quite sure why the project update still returns successful # FIXME: Not quite sure why the project update still returns successful
# in this case? # in this case?
#self.assertEqual(job.status, 'error', #self.assertEqual(job.status, 'error',
# '\n'.join([job.result_stdout, job.result_traceback])) # '\n'.join([job.result_stdout, job.result_traceback]))
self.assertEqual(self.project.project_updates.count(), 3) self.assertEqual(self.project.project_updates.count(), 4)