mirror of
https://github.com/ansible/awx.git
synced 2026-06-29 02:18:01 -02:30
AC-537 Work in progress on cloud credentials.
This commit is contained in:
@@ -44,7 +44,7 @@ __all__ = ['PrimordialModel', 'Organization', 'Team', 'Project',
|
||||
'Job', 'JobHostSummary', 'JobEvent', 'AuthToken',
|
||||
'PERM_INVENTORY_ADMIN', 'PERM_INVENTORY_READ',
|
||||
'PERM_INVENTORY_WRITE', 'PERM_INVENTORY_DEPLOY',
|
||||
'PERM_INVENTORY_CHECK', 'JOB_STATUS_CHOICES',
|
||||
'PERM_INVENTORY_CHECK', 'TASK_STATUS_CHOICES',
|
||||
'CLOUD_INVENTORY_SOURCES']
|
||||
|
||||
logger = logging.getLogger('awx.main.models')
|
||||
@@ -71,7 +71,7 @@ PERMISSION_TYPE_CHOICES = [
|
||||
(PERM_INVENTORY_CHECK, _('Deploy To Inventory (Dry Run)')),
|
||||
]
|
||||
|
||||
JOB_STATUS_CHOICES = [
|
||||
TASK_STATUS_CHOICES = [
|
||||
('new', _('New')), # Job has been created, but not started.
|
||||
('pending', _('Pending')), # Job has been queued, but is not yet running.
|
||||
('waiting', _('Waiting')), # Job is waiting on an update/dependency.
|
||||
@@ -187,6 +187,145 @@ class CommonModelNameNotUnique(PrimordialModel):
|
||||
|
||||
name = models.CharField(max_length=512, unique=False)
|
||||
|
||||
class CommonTask(PrimordialModel):
|
||||
'''
|
||||
Common fields for models run by the task engine.
|
||||
'''
|
||||
|
||||
class Meta:
|
||||
app_label = 'main'
|
||||
abstract = True
|
||||
|
||||
cancel_flag = models.BooleanField(
|
||||
blank=True,
|
||||
default=False,
|
||||
editable=False,
|
||||
)
|
||||
status = models.CharField(
|
||||
max_length=20,
|
||||
choices=TASK_STATUS_CHOICES,
|
||||
default='new',
|
||||
editable=False,
|
||||
)
|
||||
failed = models.BooleanField(
|
||||
default=False,
|
||||
editable=False,
|
||||
)
|
||||
job_args = models.TextField(
|
||||
blank=True,
|
||||
default='',
|
||||
editable=False,
|
||||
)
|
||||
job_cwd = models.CharField(
|
||||
max_length=1024,
|
||||
blank=True,
|
||||
default='',
|
||||
editable=False,
|
||||
)
|
||||
job_env = JSONField(
|
||||
blank=True,
|
||||
default={},
|
||||
editable=False,
|
||||
)
|
||||
result_stdout = models.TextField(
|
||||
blank=True,
|
||||
default='',
|
||||
editable=False,
|
||||
)
|
||||
result_traceback = models.TextField(
|
||||
blank=True,
|
||||
default='',
|
||||
editable=False,
|
||||
)
|
||||
celery_task_id = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
default='',
|
||||
editable=False,
|
||||
)
|
||||
|
||||
def __unicode__(self):
|
||||
return u'%s-%s-%s' % (self.created, self.id, self.status)
|
||||
|
||||
def _get_parent_instance(self):
|
||||
return None
|
||||
|
||||
def _update_parent_instance(self):
|
||||
parent_instance = self._get_parent_instance()
|
||||
if parent_instance:
|
||||
if self.status in ('pending', 'waiting', 'running'):
|
||||
if parent_instance.current_update != self:
|
||||
parent_instance.current_update = self
|
||||
parent_instance.save(update_fields=['current_update'])
|
||||
elif self.status in ('successful', 'failed', 'error', 'canceled'):
|
||||
if parent_instance.current_update == self:
|
||||
parent_instance.current_update = None
|
||||
parent_instance.last_update = self
|
||||
parent_instance.last_update_failed = self.failed
|
||||
parent_instance.save(update_fields=['current_update',
|
||||
'last_update',
|
||||
'last_update_failed'])
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Get status before save...
|
||||
status_before = self.status or 'new'
|
||||
if self.pk:
|
||||
self_before = self.__class__.objects.get(pk=self.pk)
|
||||
if self_before.status != self.status:
|
||||
status_before = self_before.status
|
||||
self.failed = bool(self.status in ('failed', 'error', 'canceled'))
|
||||
super(CommonTask, self).save(*args, **kwargs)
|
||||
# If status changed, update parent instance....
|
||||
if self.status != status_before:
|
||||
self._update_parent_instance()
|
||||
|
||||
@property
|
||||
def celery_task(self):
|
||||
try:
|
||||
if self.celery_task_id:
|
||||
return TaskMeta.objects.get(task_id=self.celery_task_id)
|
||||
except TaskMeta.DoesNotExist:
|
||||
pass
|
||||
|
||||
@property
|
||||
def can_start(self):
|
||||
return bool(self.status == 'new')
|
||||
|
||||
def _get_task_class(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def _get_passwords_needed_to_start(self):
|
||||
return []
|
||||
|
||||
def start(self, **kwargs):
|
||||
task_class = self._get_task_class()
|
||||
needed = self._get_passwords_needed_to_start
|
||||
opts = dict([(field, kwargs.get(field, '')) for field in needed])
|
||||
if not all(opts.values()):
|
||||
return False
|
||||
self.status = 'pending'
|
||||
self.save(update_fields=['status'])
|
||||
task_result = task_class().delay(self.pk, **opts)
|
||||
# Reload instance from database so we don't clobber results from task
|
||||
# (mainly from tests when using Django 1.4.x).
|
||||
instance = self.__class__.objects.get(pk=self.pk)
|
||||
# The TaskMeta instance in the database isn't created until the worker
|
||||
# starts processing the task, so we can only store the task ID here.
|
||||
instance.celery_task_id = task_result.task_id
|
||||
instance.save(update_fields=['celery_task_id'])
|
||||
return True
|
||||
|
||||
@property
|
||||
def can_cancel(self):
|
||||
return bool(self.status in ('pending', 'waiting', 'running'))
|
||||
|
||||
def cancel(self):
|
||||
if self.can_cancel:
|
||||
if not self.cancel_flag:
|
||||
self.cancel_flag = True
|
||||
self.save(update_fields=['cancel_flag'])
|
||||
return self.cancel_flag
|
||||
|
||||
class Organization(CommonModel):
|
||||
'''
|
||||
organizations are the basic unit of multi-tenancy divisions
|
||||
@@ -333,7 +472,7 @@ class Host(CommonModelNameNotUnique):
|
||||
|
||||
class Meta:
|
||||
app_label = 'main'
|
||||
unique_together = (("name", "inventory"),)
|
||||
unique_together = (("name", "inventory"),) # FIXME: Add ('instance_id', 'inventory') after migration.
|
||||
|
||||
inventory = models.ForeignKey(
|
||||
'Inventory',
|
||||
@@ -344,6 +483,11 @@ class Host(CommonModelNameNotUnique):
|
||||
default=True,
|
||||
help_text=_('Is this host online and available for running jobs?'),
|
||||
)
|
||||
instance_id = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
default='',
|
||||
)
|
||||
variables = models.TextField(
|
||||
blank=True,
|
||||
default='',
|
||||
@@ -629,7 +773,7 @@ class InventorySource(PrimordialModel):
|
||||
('ec2', _('Amazon EC2')),
|
||||
]
|
||||
|
||||
PASSWORD_FIELDS = ('source_password',)
|
||||
#PASSWORD_FIELDS = ('source_password',)
|
||||
|
||||
INVENTORY_SOURCE_STATUS_CHOICES = [
|
||||
('none', _('No External Source')),
|
||||
@@ -671,16 +815,23 @@ class InventorySource(PrimordialModel):
|
||||
default='',
|
||||
help_text=_('Inventory source variables in YAML or JSON format.'),
|
||||
)
|
||||
source_username = models.CharField(
|
||||
max_length=1024,
|
||||
credential = models.ForeignKey(
|
||||
'Credential',
|
||||
related_name='inventory_sources',
|
||||
null=True,
|
||||
default=None,
|
||||
blank=True,
|
||||
default='',
|
||||
)
|
||||
source_password = models.CharField(
|
||||
max_length=1024,
|
||||
blank=True,
|
||||
default='',
|
||||
)
|
||||
#source_username = models.CharField( # FIXME: Remove after migration
|
||||
# max_length=1024,
|
||||
# blank=True,
|
||||
# default='',
|
||||
#)
|
||||
#source_password = models.CharField( # FIXME: Remove after migration
|
||||
# max_length=1024,
|
||||
# blank=True,
|
||||
# default='',
|
||||
#)
|
||||
source_regions = models.CharField(
|
||||
max_length=1024,
|
||||
blank=True,
|
||||
@@ -745,19 +896,19 @@ class InventorySource(PrimordialModel):
|
||||
update_fields = kwargs.get('update_fields', [])
|
||||
# When first saving to the database, don't store any password field
|
||||
# values, but instead save them until after the instance is created.
|
||||
if new_instance:
|
||||
for field in self.PASSWORD_FIELDS:
|
||||
value = getattr(self, field, '')
|
||||
setattr(self, '_saved_%s' % field, value)
|
||||
setattr(self, field, '')
|
||||
#if new_instance:
|
||||
# for field in self.PASSWORD_FIELDS:
|
||||
# value = getattr(self, field, '')
|
||||
# setattr(self, '_saved_%s' % field, value)
|
||||
# setattr(self, field, '')
|
||||
# Otherwise, store encrypted values to the database.
|
||||
else:
|
||||
for field in self.PASSWORD_FIELDS:
|
||||
encrypted = encrypt_field(self, field, True)
|
||||
if getattr(self, field) != encrypted:
|
||||
setattr(self, field, encrypted)
|
||||
if field not in update_fields:
|
||||
update_fields.append(field)
|
||||
#else:
|
||||
# for field in self.PASSWORD_FIELDS:
|
||||
# encrypted = encrypt_field(self, field, True)
|
||||
# if getattr(self, field) != encrypted:
|
||||
# setattr(self, field, encrypted)
|
||||
# if field not in update_fields:
|
||||
# update_fields.append(field)
|
||||
# Update status and last_updated fields.
|
||||
updated_fields = self.set_status_and_last_updated(save=False)
|
||||
for field in updated_fields:
|
||||
@@ -772,15 +923,15 @@ class InventorySource(PrimordialModel):
|
||||
super(InventorySource, self).save(*args, **kwargs)
|
||||
# After saving a new instance for the first time (to get a primary
|
||||
# key), set the password fields and save again.
|
||||
if new_instance:
|
||||
update_fields=[]
|
||||
for field in self.PASSWORD_FIELDS:
|
||||
saved_value = getattr(self, '_saved_%s' % field, '')
|
||||
if getattr(self, field) != saved_value:
|
||||
setattr(self, field, saved_value)
|
||||
update_fields.append(field)
|
||||
if update_fields:
|
||||
self.save(update_fields=update_fields)
|
||||
#if new_instance:
|
||||
# update_fields=[]
|
||||
# for field in self.PASSWORD_FIELDS:
|
||||
# saved_value = getattr(self, '_saved_%s' % field, '')
|
||||
# if getattr(self, field) != saved_value:
|
||||
# setattr(self, field, saved_value)
|
||||
# update_fields.append(field)
|
||||
# if update_fields:
|
||||
# self.save(update_fields=update_fields)
|
||||
|
||||
source_vars_dict = VarsDictProperty('source_vars')
|
||||
|
||||
@@ -843,7 +994,7 @@ class InventorySource(PrimordialModel):
|
||||
def get_absolute_url(self):
|
||||
return reverse('main:inventory_source_detail', args=(self.pk,))
|
||||
|
||||
class InventoryUpdate(PrimordialModel):
|
||||
class InventoryUpdate(CommonTask):
|
||||
'''
|
||||
Internal job for tracking inventory updates from external sources.
|
||||
'''
|
||||
@@ -857,125 +1008,33 @@ class InventoryUpdate(PrimordialModel):
|
||||
on_delete=models.CASCADE,
|
||||
editable=False,
|
||||
)
|
||||
cancel_flag = models.BooleanField(
|
||||
blank=True,
|
||||
license_error = models.BooleanField(
|
||||
default=False,
|
||||
editable=False,
|
||||
)
|
||||
status = models.CharField(
|
||||
max_length=20,
|
||||
choices=JOB_STATUS_CHOICES,
|
||||
default='new',
|
||||
editable=False,
|
||||
)
|
||||
failed = models.BooleanField(
|
||||
default=False,
|
||||
editable=False,
|
||||
)
|
||||
job_args = models.TextField(
|
||||
blank=True,
|
||||
default='',
|
||||
editable=False,
|
||||
)
|
||||
job_cwd = models.CharField(
|
||||
max_length=1024,
|
||||
blank=True,
|
||||
default='',
|
||||
editable=False,
|
||||
)
|
||||
job_env = JSONField(
|
||||
blank=True,
|
||||
default={},
|
||||
editable=False,
|
||||
)
|
||||
result_stdout = models.TextField(
|
||||
blank=True,
|
||||
default='',
|
||||
editable=False,
|
||||
)
|
||||
result_traceback = models.TextField(
|
||||
blank=True,
|
||||
default='',
|
||||
editable=False,
|
||||
)
|
||||
celery_task_id = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
default='',
|
||||
editable=False,
|
||||
)
|
||||
|
||||
def __unicode__(self):
|
||||
return u'%s-%s-%s' % (self.created, self.id, self.status)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Get status before save...
|
||||
status_before = self.status or 'new'
|
||||
if self.pk:
|
||||
inventory_update_before = InventoryUpdate.objects.get(pk=self.pk)
|
||||
if inventory_update_before.status != self.status:
|
||||
status_before = inventory_update_before.status
|
||||
self.failed = bool(self.status in ('failed', 'error', 'canceled'))
|
||||
update_fields = kwargs.get('update_fields', [])
|
||||
if bool('license' in self.result_stdout and
|
||||
'exceeded' in self.result_stdout and not self.license_error):
|
||||
self.license_error = True
|
||||
if 'license_error' not in update_fields:
|
||||
update_fields.append('license_error')
|
||||
super(InventoryUpdate, self).save(*args, **kwargs)
|
||||
# If status changed, update inventory.
|
||||
if self.status != status_before:
|
||||
if self.status in ('pending', 'waiting', 'running'):
|
||||
inventory_source = self.inventory_source
|
||||
if inventory_source.current_update != self:
|
||||
inventory_source.current_update = self
|
||||
inventory_source.save(update_fields=['current_update'])
|
||||
elif self.status in ('successful', 'failed', 'error', 'canceled'):
|
||||
inventory_source = self.inventory_source
|
||||
if inventory_source.current_update == self:
|
||||
inventory_source.current_update = None
|
||||
inventory_source.last_update = self
|
||||
inventory_source.last_update_failed = self.failed
|
||||
inventory_source.save(update_fields=['current_update', 'last_update',
|
||||
'last_update_failed'])
|
||||
|
||||
def _get_parent_instance(self):
|
||||
return self.inventory_source
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('main:inventory_update_detail', args=(self.pk,))
|
||||
|
||||
@property
|
||||
def celery_task(self):
|
||||
try:
|
||||
if self.celery_task_id:
|
||||
return TaskMeta.objects.get(task_id=self.celery_task_id)
|
||||
except TaskMeta.DoesNotExist:
|
||||
pass
|
||||
|
||||
@property
|
||||
def can_start(self):
|
||||
return bool(self.status == 'new')
|
||||
|
||||
def start(self, **kwargs):
|
||||
def _get_task_class(self):
|
||||
from awx.main.tasks import RunInventoryUpdate
|
||||
needed = self.inventory_source.source_passwords_needed
|
||||
opts = dict([(field, kwargs.get(field, '')) for field in needed])
|
||||
if not all(opts.values()):
|
||||
return False
|
||||
self.status = 'pending'
|
||||
self.save(update_fields=['status'])
|
||||
task_result = RunInventoryUpdate().delay(self.pk, **opts)
|
||||
# Reload inventory update from database so we don't clobber results
|
||||
# from RunInventoryUpdate (mainly from tests when using Django 1.4.x).
|
||||
inventory_update = InventoryUpdate.objects.get(pk=self.pk)
|
||||
# The TaskMeta instance in the database isn't created until the worker
|
||||
# starts processing the task, so we can only store the task ID here.
|
||||
inventory_update.celery_task_id = task_result.task_id
|
||||
inventory_update.save(update_fields=['celery_task_id'])
|
||||
return True
|
||||
return RunInventoryUpdate
|
||||
|
||||
@property
|
||||
def can_cancel(self):
|
||||
return bool(self.status in ('pending', 'waiting', 'running'))
|
||||
def _get_passwords_needed_to_start(self):
|
||||
return self.inventory_source.source_passwords_needed
|
||||
|
||||
def cancel(self):
|
||||
if self.can_cancel:
|
||||
if not self.cancel_flag:
|
||||
self.cancel_flag = True
|
||||
self.save(update_fields=['cancel_flag'])
|
||||
return self.cancel_flag
|
||||
|
||||
class Credential(CommonModelNameNotUnique):
|
||||
'''
|
||||
@@ -984,27 +1043,68 @@ class Credential(CommonModelNameNotUnique):
|
||||
If used with sudo, a sudo password should be set if required.
|
||||
'''
|
||||
|
||||
PASSWORD_FIELDS = ('ssh_password', 'ssh_key_data', 'ssh_key_unlock', 'sudo_password')
|
||||
KIND_CHOICES = [
|
||||
('ssh', _('Machine')),
|
||||
('scm', _('SCM')),
|
||||
('aws', _('AWS')),
|
||||
('rax', _('Rackspace')),
|
||||
]
|
||||
|
||||
PASSWORD_FIELDS = ('ssh_password', 'password', 'ssh_key_data', 'ssh_key_unlock', 'sudo_password')
|
||||
|
||||
class Meta:
|
||||
app_label = 'main'
|
||||
unique_together = [('user', 'team', 'kind', 'name')]
|
||||
|
||||
user = models.ForeignKey('auth.User', null=True, default=None, blank=True, on_delete=SET_NULL, related_name='credentials')
|
||||
team = models.ForeignKey('Team', null=True, default=None, blank=True, on_delete=SET_NULL, related_name='credentials')
|
||||
|
||||
ssh_username = models.CharField(
|
||||
user = models.ForeignKey(
|
||||
'auth.User',
|
||||
null=True,
|
||||
default=None,
|
||||
blank=True,
|
||||
default='',
|
||||
max_length=1024,
|
||||
verbose_name=_('SSH username'),
|
||||
help_text=_('SSH username for a job using this credential.'),
|
||||
on_delete=models.CASCADE,
|
||||
related_name='credentials',
|
||||
)
|
||||
ssh_password = models.CharField(
|
||||
team = models.ForeignKey(
|
||||
'Team',
|
||||
null=True,
|
||||
default=None,
|
||||
blank=True,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='credentials',
|
||||
)
|
||||
kind = models.CharField(
|
||||
max_length=32,
|
||||
choices=KIND_CHOICES,
|
||||
default='machine',
|
||||
)
|
||||
|
||||
#ssh_username = models.CharField(
|
||||
# blank=True,
|
||||
# default='',
|
||||
# max_length=1024,
|
||||
# verbose_name=_('SSH username'),
|
||||
# help_text=_('SSH username for a job using this credential.'),
|
||||
#)
|
||||
username = models.CharField(
|
||||
blank=True,
|
||||
default='',
|
||||
max_length=1024,
|
||||
verbose_name=_('SSH password'),
|
||||
help_text=_('SSH password (or "ASK" to prompt the user).'),
|
||||
verbose_name=_('Username'),
|
||||
help_text=_('Username for this credential.'),
|
||||
)
|
||||
#ssh_password = models.CharField(
|
||||
# blank=True,
|
||||
# default='',
|
||||
# max_length=1024,
|
||||
# verbose_name=_('SSH password'),
|
||||
# help_text=_('SSH password (or "ASK" to prompt the user).'),
|
||||
#)
|
||||
password = models.CharField(
|
||||
blank=True,
|
||||
default='',
|
||||
max_length=1024,
|
||||
verbose_name=_('Password'),
|
||||
help_text=_('Password for this credential.'),
|
||||
)
|
||||
ssh_key_data = models.TextField(
|
||||
blank=True,
|
||||
@@ -1087,13 +1187,14 @@ class Credential(CommonModelNameNotUnique):
|
||||
update_fields.append(field)
|
||||
self.save(update_fields=update_fields)
|
||||
|
||||
class Team(CommonModel):
|
||||
class Team(CommonModelNameNotUnique):
|
||||
'''
|
||||
A team is a group of users that work on common projects.
|
||||
'''
|
||||
|
||||
class Meta:
|
||||
app_label = 'main'
|
||||
unique_together = [('organization', 'name')]
|
||||
|
||||
projects = models.ManyToManyField('Project', blank=True, related_name='teams')
|
||||
users = models.ManyToManyField('auth.User', blank=True, related_name='teams')
|
||||
@@ -1116,7 +1217,7 @@ class Project(CommonModel):
|
||||
('successful', 'Successful'),
|
||||
]
|
||||
|
||||
PASSWORD_FIELDS = ('scm_password', 'scm_key_data', 'scm_key_unlock')
|
||||
#PASSWORD_FIELDS = ('scm_password', 'scm_key_data', 'scm_key_unlock')
|
||||
|
||||
SCM_TYPE_CHOICES = [
|
||||
('', _('Manual')),
|
||||
@@ -1188,38 +1289,45 @@ class Project(CommonModel):
|
||||
scm_update_on_launch = models.BooleanField(
|
||||
default=False,
|
||||
)
|
||||
scm_username = models.CharField(
|
||||
credential = models.ForeignKey(
|
||||
'Credential',
|
||||
related_name='projects',
|
||||
blank=True,
|
||||
null=True,
|
||||
default='',
|
||||
max_length=256,
|
||||
verbose_name=_('Username'),
|
||||
help_text=_('SCM username for this project.'),
|
||||
)
|
||||
scm_password = models.CharField(
|
||||
blank=True,
|
||||
null=True,
|
||||
default='',
|
||||
max_length=1024,
|
||||
verbose_name=_('Password'),
|
||||
help_text=_('SCM password (or "ASK" to prompt the user).'),
|
||||
)
|
||||
scm_key_data = models.TextField(
|
||||
blank=True,
|
||||
null=True,
|
||||
default='',
|
||||
verbose_name=_('SSH private key'),
|
||||
help_text=_('RSA or DSA private key to be used instead of password.'),
|
||||
)
|
||||
scm_key_unlock = models.CharField(
|
||||
max_length=1024,
|
||||
null=True,
|
||||
blank=True,
|
||||
default='',
|
||||
verbose_name=_('SSH key unlock'),
|
||||
help_text=_('Passphrase to unlock SSH private key if encrypted (or '
|
||||
'"ASK" to prompt the user).'),
|
||||
default=None,
|
||||
)
|
||||
#scm_username = models.CharField(
|
||||
# blank=True,
|
||||
# null=True,
|
||||
# default='',
|
||||
# max_length=256,
|
||||
# verbose_name=_('Username'),
|
||||
# help_text=_('SCM username for this project.'),
|
||||
#)
|
||||
#scm_password = models.CharField(
|
||||
# blank=True,
|
||||
# null=True,
|
||||
# default='',
|
||||
# max_length=1024,
|
||||
# verbose_name=_('Password'),
|
||||
# help_text=_('SCM password (or "ASK" to prompt the user).'),
|
||||
#)
|
||||
#scm_key_data = models.TextField(
|
||||
# blank=True,
|
||||
# null=True,
|
||||
# default='',
|
||||
# verbose_name=_('SSH private key'),
|
||||
# help_text=_('RSA or DSA private key to be used instead of password.'),
|
||||
#)
|
||||
#scm_key_unlock = models.CharField(
|
||||
# max_length=1024,
|
||||
# null=True,
|
||||
# blank=True,
|
||||
# default='',
|
||||
# verbose_name=_('SSH key unlock'),
|
||||
# help_text=_('Passphrase to unlock SSH private key if encrypted (or '
|
||||
# '"ASK" to prompt the user).'),
|
||||
#)
|
||||
current_update = models.ForeignKey(
|
||||
'ProjectUpdate',
|
||||
null=True,
|
||||
@@ -1258,19 +1366,19 @@ class Project(CommonModel):
|
||||
update_fields = kwargs.get('update_fields', [])
|
||||
# When first saving to the database, don't store any password field
|
||||
# values, but instead save them until after the instance is created.
|
||||
if new_instance:
|
||||
for field in self.PASSWORD_FIELDS:
|
||||
value = getattr(self, field, '')
|
||||
setattr(self, '_saved_%s' % field, value)
|
||||
setattr(self, field, '')
|
||||
#if new_instance:
|
||||
# for field in self.PASSWORD_FIELDS:
|
||||
# value = getattr(self, field, '')
|
||||
# setattr(self, '_saved_%s' % field, value)
|
||||
# setattr(self, field, '')
|
||||
# Otherwise, store encrypted values to the database.
|
||||
else:
|
||||
for field in self.PASSWORD_FIELDS:
|
||||
encrypted = encrypt_field(self, field, bool(field != 'scm_key_data'))
|
||||
if getattr(self, field) != encrypted:
|
||||
setattr(self, field, encrypted)
|
||||
if field not in update_fields:
|
||||
update_fields.append(field)
|
||||
#else:
|
||||
# for field in self.PASSWORD_FIELDS:
|
||||
# encrypted = encrypt_field(self, field, bool(field != 'scm_key_data'))
|
||||
# if getattr(self, field) != encrypted:
|
||||
# setattr(self, field, encrypted)
|
||||
# if field not in update_fields:
|
||||
# update_fields.append(field)
|
||||
# Check if scm_type or scm_url changes.
|
||||
if self.pk:
|
||||
project_before = Project.objects.get(pk=self.pk)
|
||||
@@ -1298,11 +1406,11 @@ class Project(CommonModel):
|
||||
# Generate local_path for SCM after initial save (so we have a PK).
|
||||
if self.scm_type and not self.local_path.startswith('_'):
|
||||
update_fields.append('local_path')
|
||||
for field in self.PASSWORD_FIELDS:
|
||||
saved_value = getattr(self, '_saved_%s' % field, '')
|
||||
if getattr(self, field) != saved_value:
|
||||
setattr(self, field, saved_value)
|
||||
update_fields.append(field)
|
||||
# for field in self.PASSWORD_FIELDS:
|
||||
# saved_value = getattr(self, '_saved_%s' % field, '')
|
||||
# if getattr(self, field) != saved_value:
|
||||
# setattr(self, field, saved_value)
|
||||
# update_fields.append(field)
|
||||
if update_fields:
|
||||
self.save(update_fields=update_fields)
|
||||
# If we just created a new project with SCM and it doesn't require any
|
||||
@@ -1429,7 +1537,7 @@ class Project(CommonModel):
|
||||
results.append(playbook)
|
||||
return results
|
||||
|
||||
class ProjectUpdate(PrimordialModel):
|
||||
class ProjectUpdate(CommonTask):
|
||||
'''
|
||||
Internal job for tracking project updates from SCM.
|
||||
'''
|
||||
@@ -1443,128 +1551,17 @@ class ProjectUpdate(PrimordialModel):
|
||||
on_delete=models.CASCADE,
|
||||
editable=False,
|
||||
)
|
||||
cancel_flag = models.BooleanField(
|
||||
blank=True,
|
||||
default=False,
|
||||
editable=False,
|
||||
)
|
||||
status = models.CharField(
|
||||
max_length=20,
|
||||
choices=JOB_STATUS_CHOICES,
|
||||
default='new',
|
||||
editable=False,
|
||||
)
|
||||
failed = models.BooleanField(
|
||||
default=False,
|
||||
editable=False,
|
||||
)
|
||||
job_args = models.TextField(
|
||||
blank=True,
|
||||
default='',
|
||||
editable=False,
|
||||
)
|
||||
job_cwd = models.CharField(
|
||||
max_length=1024,
|
||||
blank=True,
|
||||
default='',
|
||||
editable=False,
|
||||
)
|
||||
job_env = JSONField(
|
||||
blank=True,
|
||||
default={},
|
||||
editable=False,
|
||||
)
|
||||
result_stdout = models.TextField(
|
||||
blank=True,
|
||||
default='',
|
||||
editable=False,
|
||||
)
|
||||
result_traceback = models.TextField(
|
||||
blank=True,
|
||||
default='',
|
||||
editable=False,
|
||||
)
|
||||
celery_task_id = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
default='',
|
||||
editable=False,
|
||||
)
|
||||
|
||||
def __unicode__(self):
|
||||
return u'%s-%s-%s' % (self.created, self.id, self.status)
|
||||
def _get_parent_instance(self):
|
||||
return self.project
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Get status before save...
|
||||
status_before = self.status or 'new'
|
||||
if self.pk:
|
||||
project_update_before = ProjectUpdate.objects.get(pk=self.pk)
|
||||
if project_update_before.status != self.status:
|
||||
status_before = project_update_before.status
|
||||
self.failed = bool(self.status in ('failed', 'error', 'canceled'))
|
||||
super(ProjectUpdate, self).save(*args, **kwargs)
|
||||
# If status changed, update project.
|
||||
if self.status != status_before:
|
||||
if self.status in ('pending', 'waiting', 'running'):
|
||||
project = self.project
|
||||
if project.current_update != self:
|
||||
project.current_update = self
|
||||
project.save(update_fields=['current_update'])
|
||||
elif self.status in ('successful', 'failed', 'error', 'canceled'):
|
||||
project = self.project
|
||||
if project.current_update == self:
|
||||
project.current_update = None
|
||||
project.last_update = self
|
||||
project.last_update_failed = self.failed
|
||||
if not self.failed and project.scm_delete_on_next_update:
|
||||
project.scm_delete_on_next_update = False
|
||||
project.save(update_fields=['current_update', 'last_update',
|
||||
'last_update_failed',
|
||||
'scm_delete_on_next_update'])
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('main:project_update_detail', args=(self.pk,))
|
||||
|
||||
@property
|
||||
def celery_task(self):
|
||||
try:
|
||||
if self.celery_task_id:
|
||||
return TaskMeta.objects.get(task_id=self.celery_task_id)
|
||||
except TaskMeta.DoesNotExist:
|
||||
pass
|
||||
|
||||
@property
|
||||
def can_start(self):
|
||||
return bool(self.status == 'new')
|
||||
|
||||
def start(self, **kwargs):
|
||||
def _get_task_class(self):
|
||||
from awx.main.tasks import RunProjectUpdate
|
||||
needed = self.project.scm_passwords_needed
|
||||
opts = dict([(field, kwargs.get(field, '')) for field in needed])
|
||||
if not all(opts.values()):
|
||||
return False
|
||||
self.status = 'pending'
|
||||
self.save(update_fields=['status'])
|
||||
task_result = RunProjectUpdate().delay(self.pk, **opts)
|
||||
# Reload project update from database so we don't clobber results
|
||||
# from RunProjectUpdate (mainly from tests when using Django 1.4.x).
|
||||
project_update = ProjectUpdate.objects.get(pk=self.pk)
|
||||
# The TaskMeta instance in the database isn't created until the worker
|
||||
# starts processing the task, so we can only store the task ID here.
|
||||
project_update.celery_task_id = task_result.task_id
|
||||
project_update.save(update_fields=['celery_task_id'])
|
||||
return True
|
||||
return RunProjectUpdate
|
||||
|
||||
@property
|
||||
def can_cancel(self):
|
||||
return bool(self.status in ('pending', 'waiting', 'running'))
|
||||
def _get_passwords_needed_to_start(self):
|
||||
return self.project.scm_passwords_needed
|
||||
|
||||
def cancel(self):
|
||||
if self.can_cancel:
|
||||
if not self.cancel_flag:
|
||||
self.cancel_flag = True
|
||||
self.save(update_fields=['cancel_flag'])
|
||||
return self.cancel_flag
|
||||
|
||||
class Permission(CommonModelNameNotUnique):
|
||||
'''
|
||||
@@ -1648,6 +1645,14 @@ class JobTemplate(CommonModel):
|
||||
default=None,
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
cloud_credential = models.ForeignKey(
|
||||
'Credential',
|
||||
related_name='job_templates_as_cloud_credential+',
|
||||
blank=True,
|
||||
null=True,
|
||||
default=None,
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
forks = models.PositiveIntegerField(
|
||||
blank=True,
|
||||
default=0,
|
||||
@@ -1692,6 +1697,7 @@ class JobTemplate(CommonModel):
|
||||
kwargs.setdefault('project', self.project)
|
||||
kwargs.setdefault('playbook', self.playbook)
|
||||
kwargs.setdefault('credential', self.credential)
|
||||
kwargs.setdefault('cloud_credential', self.cloud_credential)
|
||||
kwargs.setdefault('forks', self.forks)
|
||||
kwargs.setdefault('limit', self.limit)
|
||||
kwargs.setdefault('verbosity', self.verbosity)
|
||||
@@ -1721,7 +1727,7 @@ class JobTemplate(CommonModel):
|
||||
needed.append(pw)
|
||||
return bool(self.credential and not len(needed))
|
||||
|
||||
class Job(CommonModelNameNotUnique):
|
||||
class Job(CommonTask):
|
||||
'''
|
||||
A job applies a project (with playbook) to an inventory source with a given
|
||||
credential. It represents a single invocation of ansible-playbook with the
|
||||
@@ -1761,6 +1767,14 @@ class Job(CommonModelNameNotUnique):
|
||||
null=True,
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
cloud_credential = models.ForeignKey(
|
||||
'Credential',
|
||||
related_name='jobs_as_cloud_credential+',
|
||||
blank=True,
|
||||
null=True,
|
||||
default=None,
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
project = models.ForeignKey(
|
||||
'Project',
|
||||
related_name='jobs',
|
||||
@@ -1792,59 +1806,12 @@ class Job(CommonModelNameNotUnique):
|
||||
blank=True,
|
||||
default='',
|
||||
)
|
||||
cancel_flag = models.BooleanField(
|
||||
blank=True,
|
||||
default=False,
|
||||
editable=False,
|
||||
)
|
||||
launch_type = models.CharField(
|
||||
max_length=20,
|
||||
choices=LAUNCH_TYPE_CHOICES,
|
||||
default='manual',
|
||||
editable=False,
|
||||
)
|
||||
status = models.CharField(
|
||||
max_length=20,
|
||||
choices=JOB_STATUS_CHOICES,
|
||||
default='new',
|
||||
editable=False,
|
||||
)
|
||||
failed = models.BooleanField(
|
||||
default=False,
|
||||
editable=False,
|
||||
)
|
||||
job_args = models.TextField(
|
||||
blank=True,
|
||||
default='',
|
||||
editable=False,
|
||||
)
|
||||
job_cwd = models.CharField(
|
||||
max_length=1024,
|
||||
blank=True,
|
||||
default='',
|
||||
editable=False,
|
||||
)
|
||||
job_env = JSONField(
|
||||
blank=True,
|
||||
default={},
|
||||
editable=False,
|
||||
)
|
||||
result_stdout = models.TextField(
|
||||
blank=True,
|
||||
default='',
|
||||
editable=False,
|
||||
)
|
||||
result_traceback = models.TextField(
|
||||
blank=True,
|
||||
default='',
|
||||
editable=False,
|
||||
)
|
||||
celery_task_id = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
default='',
|
||||
editable=False,
|
||||
)
|
||||
hosts = models.ManyToManyField(
|
||||
'Host',
|
||||
related_name='jobs',
|
||||
@@ -1856,23 +1823,8 @@ class Job(CommonModelNameNotUnique):
|
||||
def get_absolute_url(self):
|
||||
return reverse('main:job_detail', args=(self.pk,))
|
||||
|
||||
def __unicode__(self):
|
||||
return u'%s-%s-%s' % (self.name, self.id, self.status)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.failed = bool(self.status in ('failed', 'error', 'canceled'))
|
||||
super(Job, self).save(*args, **kwargs)
|
||||
|
||||
extra_vars_dict = VarsDictProperty('extra_vars', True)
|
||||
|
||||
@property
|
||||
def celery_task(self):
|
||||
try:
|
||||
if self.celery_task_id:
|
||||
return TaskMeta.objects.get(task_id=self.celery_task_id)
|
||||
except TaskMeta.DoesNotExist:
|
||||
pass
|
||||
|
||||
@property
|
||||
def task_auth_token(self):
|
||||
'''Return temporary auth token used for task requests via API.'''
|
||||
@@ -1894,40 +1846,12 @@ class Job(CommonModelNameNotUnique):
|
||||
needed.append(pw)
|
||||
return needed
|
||||
|
||||
@property
|
||||
def can_start(self):
|
||||
return bool(self.status == 'new')
|
||||
|
||||
def start(self, **kwargs):
|
||||
def _get_task_class(self):
|
||||
from awx.main.tasks import RunJob
|
||||
if not self.can_start:
|
||||
return False
|
||||
needed = self.passwords_needed_to_start
|
||||
opts = dict([(field, kwargs.get(field, '')) for field in needed])
|
||||
if not all(opts.values()):
|
||||
return False
|
||||
self.status = 'pending'
|
||||
self.save(update_fields=['status'])
|
||||
task_result = RunJob().delay(self.pk, **opts)
|
||||
# Reload job from database so we don't clobber results from RunJob
|
||||
# (mainly from tests when using Django 1.4.x).
|
||||
job = Job.objects.get(pk=self.pk)
|
||||
# The TaskMeta instance in the database isn't created until the worker
|
||||
# starts processing the task, so we can only store the task ID here.
|
||||
job.celery_task_id = task_result.task_id
|
||||
job.save(update_fields=['celery_task_id'])
|
||||
return True
|
||||
|
||||
@property
|
||||
def can_cancel(self):
|
||||
return bool(self.status in ('pending', 'waiting', 'running'))
|
||||
|
||||
def cancel(self):
|
||||
if self.can_cancel:
|
||||
if not self.cancel_flag:
|
||||
self.cancel_flag = True
|
||||
self.save(update_fields=['cancel_flag'])
|
||||
return self.cancel_flag
|
||||
return RunJob
|
||||
|
||||
def _get_passwords_needed_to_start(self):
|
||||
return self.passwords_needed_to_start
|
||||
|
||||
@property
|
||||
def successful_hosts(self):
|
||||
|
||||
Reference in New Issue
Block a user