mirror of
https://github.com/ansible/awx.git
synced 2026-02-20 04:30:05 -03:30
Added separate method to start Job independently from creating it; Jobs no longer start automatically when first saved. Added method on JobTemplate to create a new Job with defaults copied from the template.
This commit is contained in:
@@ -24,6 +24,7 @@ from django.contrib.admin.util import unquote
|
|||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from lib.main.models import *
|
from lib.main.models import *
|
||||||
@@ -195,7 +196,15 @@ class VariableDataAdmin(BaseModelAdmin):
|
|||||||
|
|
||||||
class CredentialAdmin(BaseModelAdmin):
|
class CredentialAdmin(BaseModelAdmin):
|
||||||
|
|
||||||
list_display = ('name', 'description', 'active')
|
fieldsets = (
|
||||||
|
(None, {'fields': (('name', 'active'), ('user', 'team'), 'description')}),
|
||||||
|
(_('Auth Info'), {'fields': ('default_username', 'ssh_key_data',
|
||||||
|
'ssh_key_unlock', 'ssh_password',
|
||||||
|
'sudo_password')}),
|
||||||
|
#(_('Tags'), {'fields': ('tags',)}),
|
||||||
|
(_('Audit Trail'), {'fields': ('creation_date', 'created_by', 'audit_trail',)}),
|
||||||
|
)
|
||||||
|
readonly_fields = ('creation_date', 'created_by', 'audit_trail')
|
||||||
filter_horizontal = ('tags',)
|
filter_horizontal = ('tags',)
|
||||||
|
|
||||||
class TeamAdmin(BaseModelAdmin):
|
class TeamAdmin(BaseModelAdmin):
|
||||||
@@ -246,12 +255,16 @@ class JobTemplateAdmin(BaseModelAdmin):
|
|||||||
#filter_horizontal = ('tags',)
|
#filter_horizontal = ('tags',)
|
||||||
|
|
||||||
def get_create_link_display(self, obj):
|
def get_create_link_display(self, obj):
|
||||||
|
if not obj or not obj.pk:
|
||||||
|
return ''
|
||||||
info = Job._meta.app_label, Job._meta.module_name
|
info = Job._meta.app_label, Job._meta.module_name
|
||||||
create_url = reverse('admin:%s_%s_add' % info,
|
create_url = reverse('admin:%s_%s_add' % info,
|
||||||
current_app=self.admin_site.name)
|
current_app=self.admin_site.name)
|
||||||
create_opts = {
|
create_opts = {
|
||||||
'job_template': obj.pk,
|
'job_template': obj.pk,
|
||||||
'job_type': obj.job_type,
|
'job_type': obj.job_type,
|
||||||
|
'description': obj.description,
|
||||||
|
'name': '%s %s' % (obj.name, now().isoformat()),
|
||||||
}
|
}
|
||||||
if obj.inventory:
|
if obj.inventory:
|
||||||
create_opts['inventory'] = obj.inventory.pk
|
create_opts['inventory'] = obj.inventory.pk
|
||||||
@@ -267,6 +280,8 @@ class JobTemplateAdmin(BaseModelAdmin):
|
|||||||
get_create_link_display.allow_tags = True
|
get_create_link_display.allow_tags = True
|
||||||
|
|
||||||
def get_jobs_link_display(self, obj):
|
def get_jobs_link_display(self, obj):
|
||||||
|
if not obj or not obj.pk:
|
||||||
|
return ''
|
||||||
info = Job._meta.app_label, Job._meta.module_name
|
info = Job._meta.app_label, Job._meta.module_name
|
||||||
jobs_url = reverse('admin:%s_%s_changelist' % info,
|
jobs_url = reverse('admin:%s_%s_changelist' % info,
|
||||||
current_app=self.admin_site.name)
|
current_app=self.admin_site.name)
|
||||||
@@ -293,7 +308,8 @@ class JobAdmin(BaseModelAdmin):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {'fields': ('name', 'job_template', 'description')}),
|
(None, {'fields': ('name', 'job_template', 'description')}),
|
||||||
(_('Job Parameters'), {'fields': ('inventory', 'project', 'playbook',
|
(_('Job Parameters'), {'fields': ('inventory', 'project', 'playbook',
|
||||||
'credential', 'job_type')}),
|
'credential', 'job_type',
|
||||||
|
'start_job')}),
|
||||||
#(_('Tags'), {'fields': ('tags',)}),
|
#(_('Tags'), {'fields': ('tags',)}),
|
||||||
(_('Audit Trail'), {'fields': ('creation_date', 'created_by',
|
(_('Audit Trail'), {'fields': ('creation_date', 'created_by',
|
||||||
'audit_trail',)}),
|
'audit_trail',)}),
|
||||||
@@ -312,7 +328,7 @@ class JobAdmin(BaseModelAdmin):
|
|||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
def get_readonly_fields(self, request, obj=None):
|
||||||
ro_fields = list(super(JobAdmin, self).get_readonly_fields(request, obj))
|
ro_fields = list(super(JobAdmin, self).get_readonly_fields(request, obj))
|
||||||
if obj and obj.pk:
|
if obj and obj.pk and obj.status != 'new':
|
||||||
ro_fields.extend(['name', 'description', 'job_template',
|
ro_fields.extend(['name', 'description', 'job_template',
|
||||||
'inventory', 'project', 'playbook', 'credential',
|
'inventory', 'project', 'playbook', 'credential',
|
||||||
'job_type'])
|
'job_type'])
|
||||||
@@ -320,14 +336,21 @@ class JobAdmin(BaseModelAdmin):
|
|||||||
|
|
||||||
def get_fieldsets(self, request, obj=None):
|
def get_fieldsets(self, request, obj=None):
|
||||||
fsets = list(super(JobAdmin, self).get_fieldsets(request, obj))
|
fsets = list(super(JobAdmin, self).get_fieldsets(request, obj))
|
||||||
if not obj or not obj.pk:
|
if not obj or not obj.pk or obj.status == 'new':
|
||||||
fsets = [fs for fs in fsets if
|
fsets = [fs for fs in fsets if
|
||||||
'creation_date' not in fs[1]['fields'] and
|
'creation_date' not in fs[1]['fields'] and
|
||||||
'status' not in fs[1]['fields']]
|
'status' not in fs[1]['fields']]
|
||||||
|
elif obj and obj.pk and obj.status != 'new':
|
||||||
|
#print obj, obj.pk, obj.status
|
||||||
|
for fs in fsets:
|
||||||
|
# FIXME: Show start job on add view
|
||||||
|
if 'start_job' in fs[1]['fields']:
|
||||||
|
fs[1]['fields'] = [x for x in fs[1]['fields']
|
||||||
|
if x != 'start_job']
|
||||||
return fsets
|
return fsets
|
||||||
|
|
||||||
def get_inline_instances(self, request, obj=None):
|
def get_inline_instances(self, request, obj=None):
|
||||||
if obj and obj.pk:
|
if obj and obj.pk and obj.status != 'new':
|
||||||
return super(JobAdmin, self).get_inline_instances(request, obj)
|
return super(JobAdmin, self).get_inline_instances(request, obj)
|
||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -71,8 +71,7 @@ class GroupForm(forms.ModelForm):
|
|||||||
class JobTemplateAdminForm(forms.ModelForm):
|
class JobTemplateAdminForm(forms.ModelForm):
|
||||||
'''Custom admin form for creating/editing JobTemplates.'''
|
'''Custom admin form for creating/editing JobTemplates.'''
|
||||||
|
|
||||||
playbook = forms.ChoiceField(choices=[EMPTY_CHOICE], required=False,
|
playbook = forms.ChoiceField(choices=[EMPTY_CHOICE], widget=PlaybookSelect)
|
||||||
widget=PlaybookSelect)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = JobTemplate
|
model = JobTemplate
|
||||||
@@ -89,9 +88,30 @@ class JobTemplateAdminForm(forms.ModelForm):
|
|||||||
class JobAdminForm(JobTemplateAdminForm):
|
class JobAdminForm(JobTemplateAdminForm):
|
||||||
'''Custom admin form for creating Jobs.'''
|
'''Custom admin form for creating Jobs.'''
|
||||||
|
|
||||||
|
start_job = forms.BooleanField(initial=False, required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Job
|
model = Job
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(JobAdminForm, self).__init__(*args, **kwargs)
|
super(JobAdminForm, self).__init__(*args, **kwargs)
|
||||||
self.fields['playbook'].required = True
|
if self.instance.pk and self.instance.status != 'new':
|
||||||
|
self.fields.pop('playbook', None)
|
||||||
|
|
||||||
|
def clean_start_job(self):
|
||||||
|
return self.cleaned_data.get('start_job', False)
|
||||||
|
|
||||||
|
def save(self, commit=True):
|
||||||
|
instance = super(JobAdminForm, self).save(commit)
|
||||||
|
save_m2m = getattr(self, 'save_m2m', lambda: None)
|
||||||
|
should_start = bool(self.cleaned_data.get('start_job', '') and
|
||||||
|
instance.status == 'new')
|
||||||
|
def new_save_m2m():
|
||||||
|
save_m2m()
|
||||||
|
if should_start:
|
||||||
|
instance.start()
|
||||||
|
if commit:
|
||||||
|
new_save_m2m()
|
||||||
|
else:
|
||||||
|
self.save_m2m = new_save_m2m
|
||||||
|
return instance
|
||||||
|
|||||||
@@ -775,11 +775,19 @@ class JobTemplate(CommonModel):
|
|||||||
inventory = models.ForeignKey(
|
inventory = models.ForeignKey(
|
||||||
'Inventory',
|
'Inventory',
|
||||||
related_name='job_templates',
|
related_name='job_templates',
|
||||||
blank=True,
|
|
||||||
null=True,
|
null=True,
|
||||||
default=None,
|
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
)
|
)
|
||||||
|
project = models.ForeignKey(
|
||||||
|
'Project',
|
||||||
|
related_name='job_templates',
|
||||||
|
null=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
)
|
||||||
|
playbook = models.CharField(
|
||||||
|
max_length=1024,
|
||||||
|
default='',
|
||||||
|
)
|
||||||
credential = models.ForeignKey(
|
credential = models.ForeignKey(
|
||||||
'Credential',
|
'Credential',
|
||||||
related_name='job_templates',
|
related_name='job_templates',
|
||||||
@@ -788,19 +796,27 @@ class JobTemplate(CommonModel):
|
|||||||
default=None,
|
default=None,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
)
|
)
|
||||||
project = models.ForeignKey(
|
|
||||||
'Project',
|
def create_job(self, **kwargs):
|
||||||
related_name='job_templates',
|
'''
|
||||||
blank=True,
|
Create a new job based on this template.
|
||||||
null=True,
|
'''
|
||||||
default=None,
|
start_job = kwargs.pop('start', False)
|
||||||
on_delete=models.SET_NULL,
|
save_job = kwargs.pop('save', True) or start_job # Start implies save.
|
||||||
)
|
kwargs['job_template'] = self
|
||||||
playbook = models.CharField(
|
kwargs.setdefault('name', '%s %s' % (self.name, now().isoformat()))
|
||||||
max_length=1024,
|
kwargs.setdefault('description', self.description)
|
||||||
blank=True,
|
kwargs.setdefault('job_type', self.job_type)
|
||||||
default='',
|
kwargs.setdefault('inventory', self.inventory)
|
||||||
)
|
kwargs.setdefault('project', self.project)
|
||||||
|
kwargs.setdefault('playbook', self.playbook)
|
||||||
|
kwargs.setdefault('credential', self.credential)
|
||||||
|
job = Job(**kwargs)
|
||||||
|
if save_job:
|
||||||
|
job.save()
|
||||||
|
if start_job:
|
||||||
|
job.start()
|
||||||
|
return job
|
||||||
|
|
||||||
# project has one default playbook but really should have a list of playbooks and flags ...
|
# project has one default playbook but really should have a list of playbooks and flags ...
|
||||||
# ssh-agent bash
|
# ssh-agent bash
|
||||||
@@ -903,11 +919,12 @@ class Job(CommonModel):
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
STATUS_CHOICES = [
|
STATUS_CHOICES = [
|
||||||
('pending', _('Pending')),
|
('new', _('New')), # Job has been created, but not started.
|
||||||
('running', _('Running')),
|
('pending', _('Pending')), # Job has been queued, but is not yet running.
|
||||||
('successful', _('Successful')),
|
('running', _('Running')), # Job is currently running.
|
||||||
('failed', _('Failed')),
|
('successful', _('Successful')), # Job completed successfully.
|
||||||
('error', _('Error')),
|
('failed', _('Failed')), # Job completed, but with failures.
|
||||||
|
('error', _('Error')), # The job was unable to run.
|
||||||
]
|
]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -949,7 +966,7 @@ class Job(CommonModel):
|
|||||||
status = models.CharField(
|
status = models.CharField(
|
||||||
max_length=20,
|
max_length=20,
|
||||||
choices=STATUS_CHOICES,
|
choices=STATUS_CHOICES,
|
||||||
default='pending',
|
default='new',
|
||||||
editable=False,
|
editable=False,
|
||||||
)
|
)
|
||||||
result_stdout = models.TextField(
|
result_stdout = models.TextField(
|
||||||
@@ -989,27 +1006,18 @@ class Job(CommonModel):
|
|||||||
except TaskMeta.DoesNotExist:
|
except TaskMeta.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _run(self):
|
def start(self):
|
||||||
from lib.main.tasks import run_job
|
from lib.main.tasks import run_job
|
||||||
|
if self.status != 'new':
|
||||||
|
return
|
||||||
|
self.status = 'pending'
|
||||||
|
self.save(update_fields=['status'])
|
||||||
task_result = run_job.delay(self.pk)
|
task_result = run_job.delay(self.pk)
|
||||||
# The TaskMeta instance in the database isn't created until the worker
|
# 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.
|
# starts processing the task, so we can only store the task ID here.
|
||||||
self.celery_task_id = task_result.task_id
|
self.celery_task_id = task_result.task_id
|
||||||
self.save(update_fields=['celery_task_id'])
|
self.save(update_fields=['celery_task_id'])
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
created = not bool(self.pk)
|
|
||||||
super(Job, self).save(*args, **kwargs)
|
|
||||||
# Create a new host summary for each host in the inventory.
|
|
||||||
for host in self.inventory.hosts.all():
|
|
||||||
# Due to the way the inventory script is called, hosts without a
|
|
||||||
# group won't be included.
|
|
||||||
if host.groups.count():
|
|
||||||
self.job_host_summaries.get_or_create(host=host)
|
|
||||||
# Start job running (but only if just created).
|
|
||||||
if created and self.status == 'pending':
|
|
||||||
self._run()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def successful_hosts(self):
|
def successful_hosts(self):
|
||||||
return Host.objects.filter(job_host_summaries__job__pk=self.pk,
|
return Host.objects.filter(job_host_summaries__job__pk=self.pk,
|
||||||
|
|||||||
@@ -285,7 +285,9 @@ class AcomCallbackEventTest(BaseCommandTest):
|
|||||||
|
|
||||||
def test_with_job_status_not_running(self):
|
def test_with_job_status_not_running(self):
|
||||||
# Events can only be added when the job is running.
|
# Events can only be added when the job is running.
|
||||||
self.assertEqual(self.job.status, 'pending')
|
self.assertEqual(self.job.status, 'new')
|
||||||
|
self.job.status = 'pending'
|
||||||
|
self.job.save()
|
||||||
result, stdout, stderr = self.run_command('acom_callback_event',
|
result, stdout, stderr = self.run_command('acom_callback_event',
|
||||||
**self.valid_kwargs)
|
**self.valid_kwargs)
|
||||||
self.assertTrue(isinstance(result, CommandError))
|
self.assertTrue(isinstance(result, CommandError))
|
||||||
|
|||||||
@@ -80,20 +80,21 @@ class RunJobTest(BaseCeleryTest):
|
|||||||
|
|
||||||
def create_test_job(self, **kwargs):
|
def create_test_job(self, **kwargs):
|
||||||
opts = {
|
opts = {
|
||||||
'name': 'test-job',
|
'name': 'test-job-template',
|
||||||
'inventory': self.inventory,
|
'inventory': self.inventory,
|
||||||
'project': self.project,
|
'project': self.project,
|
||||||
'playbook': self.project.available_playbooks[0],
|
'playbook': self.project.available_playbooks[0],
|
||||||
}
|
}
|
||||||
opts.update(kwargs)
|
opts.update(kwargs)
|
||||||
return Job.objects.create(**opts)
|
self.job_template = JobTemplate.objects.create(**opts)
|
||||||
|
return self.job_template.create_job()
|
||||||
|
|
||||||
def test_run_job(self):
|
def test_run_job(self):
|
||||||
self.create_test_project(TEST_PLAYBOOK)
|
self.create_test_project(TEST_PLAYBOOK)
|
||||||
job = self.create_test_job()
|
job = self.create_test_job()
|
||||||
|
self.assertEqual(job.status, 'new')
|
||||||
|
job.start()
|
||||||
self.assertEqual(job.status, 'pending')
|
self.assertEqual(job.status, 'pending')
|
||||||
self.assertEqual(set(job.hosts.values_list('pk', flat=True)),
|
|
||||||
set([self.host.pk]))
|
|
||||||
job = Job.objects.get(pk=job.pk)
|
job = Job.objects.get(pk=job.pk)
|
||||||
#print 'stdout:', job.result_stdout
|
#print 'stdout:', job.result_stdout
|
||||||
#print 'stderr:', job.result_stderr
|
#print 'stderr:', job.result_stderr
|
||||||
@@ -102,8 +103,6 @@ class RunJobTest(BaseCeleryTest):
|
|||||||
self.assertEqual(job.status, 'successful')
|
self.assertEqual(job.status, 'successful')
|
||||||
self.assertTrue(job.result_stdout)
|
self.assertTrue(job.result_stdout)
|
||||||
job_events = job.job_events.all()
|
job_events = job.job_events.all()
|
||||||
#for ev in launch_job_status_events:
|
|
||||||
# print ev.event, ev.event_data
|
|
||||||
self.assertEqual(job_events.filter(event='playbook_on_start').count(), 1)
|
self.assertEqual(job_events.filter(event='playbook_on_start').count(), 1)
|
||||||
self.assertEqual(job_events.filter(event='playbook_on_play_start').count(), 1)
|
self.assertEqual(job_events.filter(event='playbook_on_play_start').count(), 1)
|
||||||
self.assertEqual(job_events.filter(event='playbook_on_task_start').count(), 2)
|
self.assertEqual(job_events.filter(event='playbook_on_task_start').count(), 2)
|
||||||
@@ -111,7 +110,6 @@ class RunJobTest(BaseCeleryTest):
|
|||||||
for evt in job_events.filter(event='runner_on_ok'):
|
for evt in job_events.filter(event='runner_on_ok'):
|
||||||
self.assertEqual(evt.host, self.host)
|
self.assertEqual(evt.host, self.host)
|
||||||
self.assertEqual(job_events.filter(event='playbook_on_stats').count(), 1)
|
self.assertEqual(job_events.filter(event='playbook_on_stats').count(), 1)
|
||||||
#print job_events.get(event='playbook_on_stats').event_data
|
|
||||||
self.assertEqual(job.successful_hosts.count(), 1)
|
self.assertEqual(job.successful_hosts.count(), 1)
|
||||||
self.assertEqual(job.failed_hosts.count(), 0)
|
self.assertEqual(job.failed_hosts.count(), 0)
|
||||||
self.assertEqual(job.changed_hosts.count(), 1)
|
self.assertEqual(job.changed_hosts.count(), 1)
|
||||||
@@ -122,9 +120,9 @@ class RunJobTest(BaseCeleryTest):
|
|||||||
def test_check_job(self):
|
def test_check_job(self):
|
||||||
self.create_test_project(TEST_PLAYBOOK)
|
self.create_test_project(TEST_PLAYBOOK)
|
||||||
job = self.create_test_job(job_type='check')
|
job = self.create_test_job(job_type='check')
|
||||||
|
self.assertEqual(job.status, 'new')
|
||||||
|
job.start()
|
||||||
self.assertEqual(job.status, 'pending')
|
self.assertEqual(job.status, 'pending')
|
||||||
self.assertEqual(set(job.hosts.values_list('pk', flat=True)),
|
|
||||||
set([self.host.pk]))
|
|
||||||
job = Job.objects.get(pk=job.pk)
|
job = Job.objects.get(pk=job.pk)
|
||||||
self.assertEqual(job.status, 'successful')
|
self.assertEqual(job.status, 'successful')
|
||||||
self.assertTrue(job.result_stdout)
|
self.assertTrue(job.result_stdout)
|
||||||
@@ -146,9 +144,9 @@ class RunJobTest(BaseCeleryTest):
|
|||||||
def test_run_job_that_fails(self):
|
def test_run_job_that_fails(self):
|
||||||
self.create_test_project(TEST_PLAYBOOK2)
|
self.create_test_project(TEST_PLAYBOOK2)
|
||||||
job = self.create_test_job()
|
job = self.create_test_job()
|
||||||
|
self.assertEqual(job.status, 'new')
|
||||||
|
job.start()
|
||||||
self.assertEqual(job.status, 'pending')
|
self.assertEqual(job.status, 'pending')
|
||||||
self.assertEqual(set(job.hosts.values_list('pk', flat=True)),
|
|
||||||
set([self.host.pk]))
|
|
||||||
job = Job.objects.get(pk=job.pk)
|
job = Job.objects.get(pk=job.pk)
|
||||||
self.assertEqual(job.status, 'failed')
|
self.assertEqual(job.status, 'failed')
|
||||||
self.assertTrue(job.result_stdout)
|
self.assertTrue(job.result_stdout)
|
||||||
@@ -169,9 +167,9 @@ class RunJobTest(BaseCeleryTest):
|
|||||||
def test_check_job_where_task_would_fail(self):
|
def test_check_job_where_task_would_fail(self):
|
||||||
self.create_test_project(TEST_PLAYBOOK2)
|
self.create_test_project(TEST_PLAYBOOK2)
|
||||||
job = self.create_test_job(job_type='check')
|
job = self.create_test_job(job_type='check')
|
||||||
|
self.assertEqual(job.status, 'new')
|
||||||
|
job.start()
|
||||||
self.assertEqual(job.status, 'pending')
|
self.assertEqual(job.status, 'pending')
|
||||||
self.assertEqual(set(job.hosts.values_list('pk', flat=True)),
|
|
||||||
set([self.host.pk]))
|
|
||||||
job = Job.objects.get(pk=job.pk)
|
job = Job.objects.get(pk=job.pk)
|
||||||
# Since we don't actually run the task, the --check should indicate
|
# Since we don't actually run the task, the --check should indicate
|
||||||
# everything is successful.
|
# everything is successful.
|
||||||
|
|||||||
Reference in New Issue
Block a user