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:
Chris Church
2013-04-19 18:11:31 -04:00
parent bc1f3e320e
commit 5901acb6a8
5 changed files with 108 additions and 57 deletions

View File

@@ -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 []

View File

@@ -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

View File

@@ -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,

View File

@@ -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))

View File

@@ -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.