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.core.urlresolvers import reverse
from django.http import HttpResponseRedirect
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from django.utils.html import format_html
from lib.main.models import *
@ -195,7 +196,15 @@ class VariableDataAdmin(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',)
class TeamAdmin(BaseModelAdmin):
@ -246,12 +255,16 @@ class JobTemplateAdmin(BaseModelAdmin):
#filter_horizontal = ('tags',)
def get_create_link_display(self, obj):
if not obj or not obj.pk:
return ''
info = Job._meta.app_label, Job._meta.module_name
create_url = reverse('admin:%s_%s_add' % info,
current_app=self.admin_site.name)
create_opts = {
'job_template': obj.pk,
'job_type': obj.job_type,
'description': obj.description,
'name': '%s %s' % (obj.name, now().isoformat()),
}
if obj.inventory:
create_opts['inventory'] = obj.inventory.pk
@ -267,6 +280,8 @@ class JobTemplateAdmin(BaseModelAdmin):
get_create_link_display.allow_tags = True
def get_jobs_link_display(self, obj):
if not obj or not obj.pk:
return ''
info = Job._meta.app_label, Job._meta.module_name
jobs_url = reverse('admin:%s_%s_changelist' % info,
current_app=self.admin_site.name)
@ -293,7 +308,8 @@ class JobAdmin(BaseModelAdmin):
fieldsets = (
(None, {'fields': ('name', 'job_template', 'description')}),
(_('Job Parameters'), {'fields': ('inventory', 'project', 'playbook',
'credential', 'job_type')}),
'credential', 'job_type',
'start_job')}),
#(_('Tags'), {'fields': ('tags',)}),
(_('Audit Trail'), {'fields': ('creation_date', 'created_by',
'audit_trail',)}),
@ -312,7 +328,7 @@ class JobAdmin(BaseModelAdmin):
def get_readonly_fields(self, request, obj=None):
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',
'inventory', 'project', 'playbook', 'credential',
'job_type'])
@ -320,14 +336,21 @@ class JobAdmin(BaseModelAdmin):
def get_fieldsets(self, request, obj=None):
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
'creation_date' not in fs[1]['fields'] and
'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
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)
else:
return []

View File

@ -71,8 +71,7 @@ class GroupForm(forms.ModelForm):
class JobTemplateAdminForm(forms.ModelForm):
'''Custom admin form for creating/editing JobTemplates.'''
playbook = forms.ChoiceField(choices=[EMPTY_CHOICE], required=False,
widget=PlaybookSelect)
playbook = forms.ChoiceField(choices=[EMPTY_CHOICE], widget=PlaybookSelect)
class Meta:
model = JobTemplate
@ -89,9 +88,30 @@ class JobTemplateAdminForm(forms.ModelForm):
class JobAdminForm(JobTemplateAdminForm):
'''Custom admin form for creating Jobs.'''
start_job = forms.BooleanField(initial=False, required=False)
class Meta:
model = Job
def __init__(self, *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',
related_name='job_templates',
blank=True,
null=True,
default=None,
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',
related_name='job_templates',
@ -788,19 +796,27 @@ class JobTemplate(CommonModel):
default=None,
on_delete=models.SET_NULL,
)
project = models.ForeignKey(
'Project',
related_name='job_templates',
blank=True,
null=True,
default=None,
on_delete=models.SET_NULL,
)
playbook = models.CharField(
max_length=1024,
blank=True,
default='',
)
def create_job(self, **kwargs):
'''
Create a new job based on this template.
'''
start_job = kwargs.pop('start', False)
save_job = kwargs.pop('save', True) or start_job # Start implies save.
kwargs['job_template'] = self
kwargs.setdefault('name', '%s %s' % (self.name, now().isoformat()))
kwargs.setdefault('description', self.description)
kwargs.setdefault('job_type', self.job_type)
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 ...
# ssh-agent bash
@ -903,11 +919,12 @@ class Job(CommonModel):
'''
STATUS_CHOICES = [
('pending', _('Pending')),
('running', _('Running')),
('successful', _('Successful')),
('failed', _('Failed')),
('error', _('Error')),
('new', _('New')), # Job has been created, but not started.
('pending', _('Pending')), # Job has been queued, but is not yet running.
('running', _('Running')), # Job is currently running.
('successful', _('Successful')), # Job completed successfully.
('failed', _('Failed')), # Job completed, but with failures.
('error', _('Error')), # The job was unable to run.
]
class Meta:
@ -949,7 +966,7 @@ class Job(CommonModel):
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='pending',
default='new',
editable=False,
)
result_stdout = models.TextField(
@ -989,27 +1006,18 @@ class Job(CommonModel):
except TaskMeta.DoesNotExist:
pass
def _run(self):
def start(self):
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)
# 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.
self.celery_task_id = task_result.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
def successful_hosts(self):
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):
# 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',
**self.valid_kwargs)
self.assertTrue(isinstance(result, CommandError))

View File

@ -80,20 +80,21 @@ class RunJobTest(BaseCeleryTest):
def create_test_job(self, **kwargs):
opts = {
'name': 'test-job',
'name': 'test-job-template',
'inventory': self.inventory,
'project': self.project,
'playbook': self.project.available_playbooks[0],
}
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):
self.create_test_project(TEST_PLAYBOOK)
job = self.create_test_job()
self.assertEqual(job.status, 'new')
job.start()
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)
#print 'stdout:', job.result_stdout
#print 'stderr:', job.result_stderr
@ -102,8 +103,6 @@ class RunJobTest(BaseCeleryTest):
self.assertEqual(job.status, 'successful')
self.assertTrue(job.result_stdout)
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_play_start').count(), 1)
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'):
self.assertEqual(evt.host, self.host)
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.failed_hosts.count(), 0)
self.assertEqual(job.changed_hosts.count(), 1)
@ -122,9 +120,9 @@ class RunJobTest(BaseCeleryTest):
def test_check_job(self):
self.create_test_project(TEST_PLAYBOOK)
job = self.create_test_job(job_type='check')
self.assertEqual(job.status, 'new')
job.start()
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)
self.assertEqual(job.status, 'successful')
self.assertTrue(job.result_stdout)
@ -146,9 +144,9 @@ class RunJobTest(BaseCeleryTest):
def test_run_job_that_fails(self):
self.create_test_project(TEST_PLAYBOOK2)
job = self.create_test_job()
self.assertEqual(job.status, 'new')
job.start()
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)
self.assertEqual(job.status, 'failed')
self.assertTrue(job.result_stdout)
@ -169,9 +167,9 @@ class RunJobTest(BaseCeleryTest):
def test_check_job_where_task_would_fail(self):
self.create_test_project(TEST_PLAYBOOK2)
job = self.create_test_job(job_type='check')
self.assertEqual(job.status, 'new')
job.start()
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)
# Since we don't actually run the task, the --check should indicate
# everything is successful.