From 5901acb6a846db1f4a615318e8ddff19129204fa Mon Sep 17 00:00:00 2001 From: Chris Church Date: Fri, 19 Apr 2013 18:11:31 -0400 Subject: [PATCH] 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. --- lib/main/admin.py | 33 +++++++++++++--- lib/main/forms.py | 26 +++++++++++-- lib/main/models/__init__.py | 78 ++++++++++++++++++++----------------- lib/main/tests/commands.py | 4 +- lib/main/tests/tasks.py | 24 ++++++------ 5 files changed, 108 insertions(+), 57 deletions(-) diff --git a/lib/main/admin.py b/lib/main/admin.py index 9f19b3c14d..2728cb080c 100644 --- a/lib/main/admin.py +++ b/lib/main/admin.py @@ -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 [] diff --git a/lib/main/forms.py b/lib/main/forms.py index 4465006c57..bd60c31938 100644 --- a/lib/main/forms.py +++ b/lib/main/forms.py @@ -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 diff --git a/lib/main/models/__init__.py b/lib/main/models/__init__.py index 2b9ef2c84b..a311e3744c 100644 --- a/lib/main/models/__init__.py +++ b/lib/main/models/__init__.py @@ -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, diff --git a/lib/main/tests/commands.py b/lib/main/tests/commands.py index c6828f8c3a..a67d8d4d05 100644 --- a/lib/main/tests/commands.py +++ b/lib/main/tests/commands.py @@ -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)) diff --git a/lib/main/tests/tasks.py b/lib/main/tests/tasks.py index b11e4a7dfd..03b067d0a5 100644 --- a/lib/main/tests/tasks.py +++ b/lib/main/tests/tasks.py @@ -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.