Renamed LaunchJob to JobTemplate, LaunchJobStatus to Job, LaunchJobHostSummary to JobHostSummary, and LaunchJobStatusEvent to JobEvent. Updated admin, celery task, management commands accordingly.

This commit is contained in:
Chris Church 2013-04-17 18:59:21 -04:00
parent 5e6ad5a244
commit aff422c976
11 changed files with 995 additions and 326 deletions

View File

@ -16,6 +16,7 @@
import json
import urllib
from django.conf.urls import *
from django.contrib import admin
@ -51,7 +52,16 @@ admin.site.register(User, UserAdmin)
# FIXME: Hide auth.Group admin
class OrganizationAdmin(admin.ModelAdmin):
class BaseModelAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
# Automatically set created_by when saved from the admin.
# FIXME: Doesn't handle inline model instances yet.
if hasattr(obj, 'created_by') and obj.created_by is None:
obj.created_by = request.user
return super(BaseModelAdmin, self).save_model(request, obj, form, change)
class OrganizationAdmin(BaseModelAdmin):
list_display = ('name', 'description', 'active')
list_filter = ('active', 'tags')
@ -62,7 +72,7 @@ class OrganizationAdmin(admin.ModelAdmin):
(_('Tags'), {'fields': ('tags',)}),
(_('Audit Trail'), {'fields': ('creation_date', 'audit_trail',)}),
)
readonly_fields = ('creation_date', 'audit_trail')
readonly_fields = ('creation_date', 'created_by', 'audit_trail')
filter_horizontal = ('users', 'admins', 'projects', 'tags')
class InventoryHostInline(admin.StackedInline):
@ -79,7 +89,7 @@ class InventoryGroupInline(admin.StackedInline):
fields = ('name', 'description', 'active', 'parents', 'hosts', 'tags')
filter_horizontal = ('parents', 'hosts', 'tags')
class InventoryAdmin(admin.ModelAdmin):
class InventoryAdmin(BaseModelAdmin):
list_display = ('name', 'organization', 'description', 'active')
list_filter = ('organization', 'active')
@ -89,11 +99,11 @@ class InventoryAdmin(admin.ModelAdmin):
(_('Tags'), {'fields': ('tags',)}),
(_('Audit Trail'), {'fields': ('creation_date', 'audit_trail',)}),
)
readonly_fields = ('creation_date', 'audit_trail')
readonly_fields = ('creation_date', 'created_by', 'audit_trail')
filter_horizontal = ('tags',)
inlines = [InventoryHostInline, InventoryGroupInline]
class TagAdmin(admin.ModelAdmin):
class TagAdmin(BaseModelAdmin):
list_display = ('name',)
@ -111,18 +121,18 @@ class VariableDataInline(admin.StackedInline):
# FIXME: Doesn't yet work as inline due to the way the OneToOne field is
# defined.
class LaunchJobHostSummaryInline(admin.TabularInline):
class JobHostSummaryInline(admin.TabularInline):
model = LaunchJobHostSummary
model = JobHostSummary
extra = 0
can_delete = False
def has_add_permission(self, request):
return False
class LaunchJobStatusEventInline(admin.StackedInline):
class JobEventInline(admin.StackedInline):
model = LaunchJobStatusEvent
model = JobEvent
extra = 0
can_delete = False
@ -130,161 +140,197 @@ class LaunchJobStatusEventInline(admin.StackedInline):
return False
def get_event_data_display(self, obj):
return format_html('<pre class="json-display">{0}</pre>', json.dumps(obj.event_data, indent=4))
return format_html('<pre class="json-display">{0}</pre>',
json.dumps(obj.event_data, indent=4))
get_event_data_display.short_description = _('Event data')
get_event_data_display.allow_tags = True
class LaunchJobHostSummaryInlineForHost(LaunchJobHostSummaryInline):
class JobHostSummaryInlineForHost(JobHostSummaryInline):
fields = ('launch_job_status', 'changed', 'dark', 'failures', 'ok', 'processed', 'skipped')
readonly_fields = ('launch_job_status', 'changed', 'dark', 'failures', 'ok', 'processed', 'skipped')
fields = ('job', 'changed', 'dark', 'failures', 'ok', 'processed',
'skipped')
readonly_fields = ('job', 'changed', 'dark', 'failures', 'ok', 'processed',
'skipped')
class LaunchJobStatusEventInlineForHost(LaunchJobStatusEventInline):
class JobEventInlineForHost(JobEventInline):
fields = ('created', 'event', 'get_event_data_display', 'launch_job_status')
readonly_fields = ('created', 'event', 'get_event_data_display', 'launch_job_status')
fields = ('job', 'created', 'event', 'get_event_data_display')
readonly_fields = ('job', 'created', 'event', 'get_event_data_display')
class HostAdmin(admin.ModelAdmin):
class HostAdmin(BaseModelAdmin):
list_display = ('name', 'inventory', 'description', 'active')
list_filter = ('inventory', 'active')
fields = ('name', 'inventory', 'description', 'active', 'tags',
'created_by', 'audit_trail')
readonly_fields = ('creation_date', 'created_by', 'audit_trail')
filter_horizontal = ('tags',)
# FIXME: Edit reverse of many to many for groups.
#inlines = [VariableDataInline]
inlines = [LaunchJobHostSummaryInlineForHost, LaunchJobStatusEventInlineForHost]
inlines = [JobHostSummaryInlineForHost, JobEventInlineForHost]
class GroupAdmin(admin.ModelAdmin):
class GroupAdmin(BaseModelAdmin):
list_display = ('name', 'description', 'active')
filter_horizontal = ('parents', 'hosts', 'tags')
#inlines = [VariableDataInline]
class VariableDataAdmin(admin.ModelAdmin):
class VariableDataAdmin(BaseModelAdmin):
list_display = ('name', 'description', 'active')
filter_horizontal = ('tags',)
class CredentialAdmin(admin.ModelAdmin):
class CredentialAdmin(BaseModelAdmin):
list_display = ('name', 'description', 'active')
filter_horizontal = ('tags',)
class TeamAdmin(admin.ModelAdmin):
class TeamAdmin(BaseModelAdmin):
list_display = ('name', 'description', 'active')
filter_horizontal = ('projects', 'users', 'tags')
class ProjectAdmin(admin.ModelAdmin):
class ProjectAdmin(BaseModelAdmin):
list_display = ('name', 'description', 'active')
filter_horizontal = ('tags',)
class PermissionAdmin(admin.ModelAdmin):
class PermissionAdmin(BaseModelAdmin):
list_display = ('name', 'description', 'active')
filter_horizontal = ('tags',)
class LaunchJobAdmin(admin.ModelAdmin):
class JobTemplateAdmin(BaseModelAdmin):
list_display = ('name', 'description', 'active', 'get_start_link_display',
'get_statuses_link_display')
list_display = ('name', 'description', 'active', 'get_create_link_display',
'get_jobs_link_display')
fieldsets = (
(None, {'fields': ('name', 'active', 'created_by', 'description',
'get_start_link_display', 'get_statuses_link_display')}),
(None, {'fields': ('name', 'active', 'description',
'get_create_link_display', 'get_jobs_link_display')}),
(_('Job Parameters'), {'fields': ('inventory', 'project', 'credential',
'user', 'job_type')}),
(_('Tags'), {'fields': ('tags',)}),
(_('Audit Trail'), {'fields': ('creation_date', 'audit_trail',)}),
#(_('Tags'), {'fields': ('tags',)}),
(_('Audit Trail'), {'fields': ('creation_date', 'created_by',
'audit_trail',)}),
)
readonly_fields = ('creation_date', 'audit_trail', 'get_start_link_display',
'get_statuses_link_display')
filter_horizontal = ('tags',)
readonly_fields = ('creation_date', 'created_by', 'audit_trail',
'get_create_link_display', 'get_jobs_link_display')
#filter_horizontal = ('tags',)
def get_start_link_display(self, obj):
info = self.model._meta.app_label, self.model._meta.module_name
start_url = reverse('admin:%s_%s_start' % info, args=(obj.pk,),
current_app=self.admin_site.name)
return '<a href="%s">Run Job</a>' % start_url
get_start_link_display.short_description = _('Run')
get_start_link_display.allow_tags = True
def get_statuses_link_display(self, obj):
info = LaunchJobStatus._meta.app_label, LaunchJobStatus._meta.module_name
statuses_url = reverse('admin:%s_%s_changelist' % info,
current_app=self.admin_site.name)
statuses_url += '?launch_job__id__exact=%d' % obj.pk
return '<a href="%s">View Logs</a>' % statuses_url
get_statuses_link_display.short_description = _('Logs')
get_statuses_link_display.allow_tags = True
def get_urls(self):
info = self.model._meta.app_label, self.model._meta.module_name
urls = super(LaunchJobAdmin, self).get_urls()
return patterns('',
url(r'^(.+)/start/$',
self.admin_site.admin_view(self.start_job_view),
name='%s_%s_start' % info),
) + urls
def start_job_view(self, request, object_id):
obj = self.get_object(request, unquote(object_id))
ljs = obj.start()
info = ljs._meta.app_label, ljs._meta.module_name
status_url = reverse('admin:%s_%s_change' % info, args=(ljs.pk,),
def get_create_link_display(self, obj):
info = Job._meta.app_label, Job._meta.module_name
create_url = reverse('admin:%s_%s_add' % info,
current_app=self.admin_site.name)
messages.success(request, '%s has been started.' % ljs)
return HttpResponseRedirect(status_url)
create_opts = {
'job_template': obj.pk,
'job_type': obj.job_type,
}
if obj.inventory:
create_opts['inventory'] = obj.inventory.pk
if obj.project:
create_opts['project'] = obj.project.pk
if obj.credential:
create_opts['credential'] = obj.credential.pk
if obj.user:
create_opts['user'] = obj.user.pk
create_url += '?%s' % urllib.urlencode(create_opts)
return format_html('<a href="{0}">{1}</a>', create_url, 'Create Job')
get_create_link_display.short_description = _('Create Job')
get_create_link_display.allow_tags = True
class LaunchJobHostSummaryInlineForLaunchJobStatus(LaunchJobHostSummaryInline):
def get_jobs_link_display(self, obj):
info = Job._meta.app_label, Job._meta.module_name
jobs_url = reverse('admin:%s_%s_changelist' % info,
current_app=self.admin_site.name)
jobs_url += '?job_template__id__exact=%d' % obj.pk
return format_html('<a href="{0}">{1}</a>', jobs_url, 'View Jobs')
get_jobs_link_display.short_description = _('View Jobs')
get_jobs_link_display.allow_tags = True
fields = ('host', 'changed', 'dark', 'failures', 'ok', 'processed', 'skipped')
readonly_fields = ('host', 'changed', 'dark', 'failures', 'ok', 'processed', 'skipped')
class JobHostSummaryInlineForJob(JobHostSummaryInline):
class LaunchJobStatusEventInlineForLaunchJobStatus(LaunchJobStatusEventInline):
fields = ('host', 'changed', 'dark', 'failures', 'ok', 'processed',
'skipped')
readonly_fields = ('host', 'changed', 'dark', 'failures', 'ok',
'processed', 'skipped')
class JobEventInlineForJob(JobEventInline):
fields = ('created', 'event', 'get_event_data_display', 'host')
readonly_fields = ('created', 'event', 'get_event_data_display', 'host')
class LaunchJobStatusAdmin(admin.ModelAdmin):
class JobAdmin(BaseModelAdmin):
list_display = ('name', 'launch_job', 'status')
fields = ('name', 'get_launch_job_display', 'status',
'get_result_stdout_display', 'get_result_stderr_display',
'get_result_traceback_display', 'celery_task_id', 'tags',
'created_by')
readonly_fields = ('name', 'description', 'status', 'get_launch_job_display',
list_display = ('name', 'job_template', 'status')
fieldsets = (
(None, {'fields': ('name', 'job_template', 'description')}),
(_('Job Parameters'), {'fields': ('inventory', 'project', 'credential',
'user', 'job_type')}),
#(_('Tags'), {'fields': ('tags',)}),
(_('Audit Trail'), {'fields': ('creation_date', 'created_by',
'audit_trail',)}),
(_('Job Status'), {'fields': ('status', 'get_result_stdout_display',
'get_result_stderr_display',
'get_result_traceback_display',
'celery_task_id')}),
)
readonly_fields = ('status', 'get_job_template_display',
'get_result_stdout_display', 'get_result_stderr_display',
'get_result_traceback_display', 'celery_task_id',
'created_by', 'tags', 'audit_trail', 'active')
'creation_date', 'created_by', 'audit_trail',)
filter_horizontal = ('tags',)
inlines = [LaunchJobHostSummaryInlineForLaunchJobStatus,
LaunchJobStatusEventInlineForLaunchJobStatus]
inlines = [JobHostSummaryInlineForJob, JobEventInlineForJob]
def has_add_permission(self, request):
return False
def get_readonly_fields(self, request, obj=None):
ro_fields = list(super(JobAdmin, self).get_readonly_fields(request, obj))
if obj and obj.pk:
ro_fields.extend(['name', 'description', 'job_template',
'inventory', 'project', 'credential', 'user',
'job_type'])
return ro_fields
def get_launch_job_display(self, obj):
info = obj.launch_job._meta.app_label, obj.launch_job._meta.module_name
lj_url = reverse('admin:%s_%s_change' % info, args=(obj.launch_job.pk,),
current_app=self.admin_site.name)
return format_html('<a href="{0}">{1}</a>', lj_url, obj.launch_job)
get_launch_job_display.short_description = _('Launch job')
get_launch_job_display.allow_tags = True
def get_fieldsets(self, request, obj=None):
fsets = list(super(JobAdmin, self).get_fieldsets(request, obj))
if not obj or not obj.pk:
fsets = [fs for fs in fsets if
'creation_date' not in fs[1]['fields'] and
'status' not in fs[1]['fields']]
return fsets
def get_inline_instances(self, request, obj=None):
if obj and obj.pk:
return super(JobAdmin, self).get_inline_instances(request, obj)
else:
return []
def get_job_template_display(self, obj):
if obj.job_template:
info = JobTemplate._meta.app_label, JobTemplate._meta.module_name
job_template_url = reverse('admin:%s_%s_change' % info,
args=(obj.job_template.pk,),
current_app=self.admin_site.name)
return format_html('<a href="{0}">{1}</a>', job_template_url,
obj.job_template)
else:
return _('(None)')
get_job_template_display.short_description = _('Job template')
get_job_template_display.allow_tags = True
def get_result_stdout_display(self, obj):
return format_html('<pre class="result-display">{0}</pre>', obj.result_stdout or ' ')
return format_html('<pre class="result-display">{0}</pre>',
obj.result_stdout or ' ')
get_result_stdout_display.short_description = _('Stdout')
get_result_stdout_display.allow_tags = True
def get_result_stderr_display(self, obj):
return format_html('<pre class="result-display">{0}</pre>', obj.result_stderr or ' ')
return format_html('<pre class="result-display">{0}</pre>',
obj.result_stderr or ' ')
get_result_stderr_display.short_description = _('Stderr')
get_result_stderr_display.allow_tags = True
def get_result_traceback_display(self, obj):
return format_html('<pre class="result-display">{0}</pre>', obj.result_traceback or ' ')
return format_html('<pre class="result-display">{0}</pre>',
obj.result_traceback or ' ')
get_result_traceback_display.short_description = _('Traceback')
get_result_traceback_display.allow_tags = True
@ -300,5 +346,5 @@ admin.site.register(VariableData, VariableDataAdmin)
admin.site.register(Team, TeamAdmin)
admin.site.register(Project, ProjectAdmin)
admin.site.register(Credential, CredentialAdmin)
admin.site.register(LaunchJob, LaunchJobAdmin)
admin.site.register(LaunchJobStatus, LaunchJobStatusAdmin)
admin.site.register(JobTemplate, JobTemplateAdmin)
admin.site.register(Job, JobAdmin)

View File

@ -30,10 +30,10 @@ class Command(NoArgsCommand):
help = 'Ansible Commander Callback Event Capture'
option_list = NoArgsCommand.option_list + (
make_option('-i', '--launch-job-status', dest='launch_job_status_id',
make_option('-j', '--job', dest='job_id',
type='int', default=0,
help='Launch job status ID (can also be specified using '
'ACOM_LAUNCH_JOB_STATUS_ID environment variable)'),
help='Job ID (can also be specified using ACOM_JOB_ID '
'environment variable)'),
make_option('-e', '--event', dest='event_type', default=None,
help='Event type'),
make_option('-f', '--file', dest='event_data_file', default=None,
@ -44,29 +44,28 @@ class Command(NoArgsCommand):
)
def handle_noargs(self, **options):
from lib.main.models import LaunchJobStatus, LaunchJobStatusEvent
from lib.main.models import Job, JobEvent
event_type = options.get('event_type', None)
if not event_type:
raise CommandError('No event specified')
if event_type not in [x[0] for x in LaunchJobStatusEvent.EVENT_TYPES]:
if event_type not in [x[0] for x in JobEvent.EVENT_TYPES]:
raise CommandError('Unsupported event')
event_data_file = options.get('event_data_file', None)
event_data_json = options.get('event_data_json', None)
if event_data_file is None and event_data_json is None:
raise CommandError('Either --file or --data must be specified')
try:
launch_job_status_id = int(os.getenv('ACOM_LAUNCH_JOB_STATUS_ID',
options.get('launch_job_status_id', 0)))
job_id = int(os.getenv('ACOM_JOB_ID', options.get('job_id', 0)))
except ValueError:
raise CommandError('Launch job status ID must be an integer')
if not launch_job_status_id:
raise CommandError('No launch job status ID specified')
raise CommandError('Job ID must be an integer')
if not job_id:
raise CommandError('No Job ID specified')
try:
launch_job_status = LaunchJobStatus.objects.get(id=launch_job_status_id)
except LaunchJobStatus.DoesNotExist:
raise CommandError('Launch job status with ID %d not found' % launch_job_status_id)
if launch_job_status.status != 'running':
raise CommandError('Unable to add event except when launch job is running')
job = Job.objects.get(id=job_id)
except Job.DoesNotExist:
raise CommandError('Job with ID %d not found' % job_id)
if job.status != 'running':
raise CommandError('Unable to add event except when job is running')
try:
if event_data_json is None:
try:
@ -81,8 +80,7 @@ class Command(NoArgsCommand):
event_data = json.loads(event_data_json)
except ValueError:
raise CommandError('Error parsing JSON data')
launch_job_status.launch_job_status_events.create(event=event_type,
event_data=event_data)
job.job_events.create(event=event_type, event_data=event_data)
if __name__ == '__main__':
from __init__ import run_command_as_script

View File

@ -27,9 +27,9 @@ class Command(NoArgsCommand):
help = 'Ansible Commander Inventory script'
option_list = NoArgsCommand.option_list + (
make_option('-i', '--inventory', dest='inventory', type='int', default=0,
help='Inventory ID (can also be specified using '
'ACOM_INVENTORY_ID environment variable)'),
make_option('-i', '--inventory', dest='inventory_id', type='int',
default=0, help='Inventory ID (can also be specified using'
' ACOM_INVENTORY_ID environment variable)'),
make_option('--list', action='store_true', dest='list', default=False,
help='Return JSON hash of host groups.'),
make_option('--host', dest='host', default='',
@ -75,7 +75,7 @@ class Command(NoArgsCommand):
try:
# Command line argument takes precedence over environment
# variable.
inventory_id = int(options.get('inventory', 0) or \
inventory_id = int(options.get('inventory_id', 0) or \
os.getenv('ACOM_INVENTORY_ID', 0))
except ValueError:
raise CommandError('Inventory ID must be an integer')

View File

@ -0,0 +1,501 @@
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Removing unique constraint on 'LaunchJobHostSummary', fields ['launch_job_status', 'host']
db.delete_unique(u'main_launchjobhostsummary', ['launch_job_status_id', 'host_id'])
# Deleting model 'LaunchJobHostSummary'
db.delete_table(u'main_launchjobhostsummary')
# Deleting model 'LaunchJobStatusEvent'
db.delete_table(u'main_launchjobstatusevent')
# Deleting model 'LaunchJob'
db.delete_table(u'main_launchjob')
# Removing M2M table for field tags on 'LaunchJob'
db.delete_table('main_launchjob_tags')
# Removing M2M table for field audit_trail on 'LaunchJob'
db.delete_table('main_launchjob_audit_trail')
# Deleting model 'LaunchJobStatus'
db.delete_table(u'main_launchjobstatus')
# Removing M2M table for field tags on 'LaunchJobStatus'
db.delete_table('main_launchjobstatus_tags')
# Removing M2M table for field audit_trail on 'LaunchJobStatus'
db.delete_table('main_launchjobstatus_audit_trail')
# Adding model 'Job'
db.create_table(u'main_job', (
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('description', self.gf('django.db.models.fields.TextField')(default='', blank=True)),
('created_by', self.gf('django.db.models.fields.related.ForeignKey')(related_name="{'class': 'job', 'app_label': 'main'}(class)s_created", null=True, on_delete=models.SET_NULL, to=orm['auth.User'])),
('creation_date', self.gf('django.db.models.fields.DateField')(auto_now_add=True, blank=True)),
('active', self.gf('django.db.models.fields.BooleanField')(default=True)),
('name', self.gf('django.db.models.fields.CharField')(unique=True, max_length=512)),
('job_template', self.gf('django.db.models.fields.related.ForeignKey')(related_name='jobs', on_delete=models.SET_NULL, default=None, to=orm['main.JobTemplate'], blank=True, null=True)),
('job_type', self.gf('django.db.models.fields.CharField')(max_length=64)),
('inventory', self.gf('django.db.models.fields.related.ForeignKey')(related_name='jobs', null=True, on_delete=models.SET_NULL, to=orm['main.Inventory'])),
('credential', self.gf('django.db.models.fields.related.ForeignKey')(related_name='jobs', null=True, on_delete=models.SET_NULL, to=orm['main.Credential'])),
('project', self.gf('django.db.models.fields.related.ForeignKey')(related_name='jobs', null=True, on_delete=models.SET_NULL, to=orm['main.Project'])),
('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='jobs', null=True, on_delete=models.SET_NULL, to=orm['auth.User'])),
('status', self.gf('django.db.models.fields.CharField')(default='pending', max_length=20)),
('result_stdout', self.gf('django.db.models.fields.TextField')(default='', blank=True)),
('result_stderr', self.gf('django.db.models.fields.TextField')(default='', blank=True)),
('result_traceback', self.gf('django.db.models.fields.TextField')(default='', blank=True)),
('celery_task_id', self.gf('django.db.models.fields.CharField')(default='', max_length=100, blank=True)),
))
db.send_create_signal('main', ['Job'])
# Adding M2M table for field tags on 'Job'
db.create_table(u'main_job_tags', (
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
('job', models.ForeignKey(orm['main.job'], null=False)),
('tag', models.ForeignKey(orm['main.tag'], null=False))
))
db.create_unique(u'main_job_tags', ['job_id', 'tag_id'])
# Adding M2M table for field audit_trail on 'Job'
db.create_table(u'main_job_audit_trail', (
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
('job', models.ForeignKey(orm['main.job'], null=False)),
('audittrail', models.ForeignKey(orm['main.audittrail'], null=False))
))
db.create_unique(u'main_job_audit_trail', ['job_id', 'audittrail_id'])
# Adding model 'JobHostSummary'
db.create_table(u'main_jobhostsummary', (
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('job', self.gf('django.db.models.fields.related.ForeignKey')(related_name='job_host_summaries', to=orm['main.Job'])),
('host', self.gf('django.db.models.fields.related.ForeignKey')(related_name='job_host_summaries', to=orm['main.Host'])),
('changed', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
('dark', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
('failures', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
('ok', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
('processed', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
('skipped', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
))
db.send_create_signal(u'main', ['JobHostSummary'])
# Adding unique constraint on 'JobHostSummary', fields ['job', 'host']
db.create_unique(u'main_jobhostsummary', ['job_id', 'host_id'])
# Adding model 'JobTemplate'
db.create_table(u'main_jobtemplate', (
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('description', self.gf('django.db.models.fields.TextField')(default='', blank=True)),
('created_by', self.gf('django.db.models.fields.related.ForeignKey')(related_name="{'class': 'jobtemplate', 'app_label': 'main'}(class)s_created", null=True, on_delete=models.SET_NULL, to=orm['auth.User'])),
('creation_date', self.gf('django.db.models.fields.DateField')(auto_now_add=True, blank=True)),
('active', self.gf('django.db.models.fields.BooleanField')(default=True)),
('name', self.gf('django.db.models.fields.CharField')(unique=True, max_length=512)),
('job_type', self.gf('django.db.models.fields.CharField')(max_length=64)),
('inventory', self.gf('django.db.models.fields.related.ForeignKey')(related_name='job_templates', on_delete=models.SET_NULL, default=None, to=orm['main.Inventory'], blank=True, null=True)),
('credential', self.gf('django.db.models.fields.related.ForeignKey')(related_name='job_templates', on_delete=models.SET_NULL, default=None, to=orm['main.Credential'], blank=True, null=True)),
('project', self.gf('django.db.models.fields.related.ForeignKey')(related_name='job_templates', on_delete=models.SET_NULL, default=None, to=orm['main.Project'], blank=True, null=True)),
('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='job_templates', on_delete=models.SET_NULL, default=None, to=orm['auth.User'], blank=True, null=True)),
))
db.send_create_signal('main', ['JobTemplate'])
# Adding M2M table for field tags on 'JobTemplate'
db.create_table(u'main_jobtemplate_tags', (
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
('jobtemplate', models.ForeignKey(orm['main.jobtemplate'], null=False)),
('tag', models.ForeignKey(orm['main.tag'], null=False))
))
db.create_unique(u'main_jobtemplate_tags', ['jobtemplate_id', 'tag_id'])
# Adding M2M table for field audit_trail on 'JobTemplate'
db.create_table(u'main_jobtemplate_audit_trail', (
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
('jobtemplate', models.ForeignKey(orm['main.jobtemplate'], null=False)),
('audittrail', models.ForeignKey(orm['main.audittrail'], null=False))
))
db.create_unique(u'main_jobtemplate_audit_trail', ['jobtemplate_id', 'audittrail_id'])
# Adding model 'JobEvent'
db.create_table(u'main_jobevent', (
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('job', self.gf('django.db.models.fields.related.ForeignKey')(related_name='job_events', to=orm['main.Job'])),
('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
('event', self.gf('django.db.models.fields.CharField')(max_length=100)),
('event_data', self.gf('jsonfield.fields.JSONField')(default='', blank=True)),
('host', self.gf('django.db.models.fields.related.ForeignKey')(related_name='job_events', on_delete=models.SET_NULL, default=None, to=orm['main.Host'], blank=True, null=True)),
))
db.send_create_signal('main', ['JobEvent'])
def backwards(self, orm):
# Removing unique constraint on 'JobHostSummary', fields ['job', 'host']
db.delete_unique(u'main_jobhostsummary', ['job_id', 'host_id'])
# Adding model 'LaunchJobHostSummary'
db.create_table(u'main_launchjobhostsummary', (
('dark', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
('skipped', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
('host', self.gf('django.db.models.fields.related.ForeignKey')(related_name='launch_job_host_summaries', to=orm['main.Host'])),
('ok', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
('processed', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
('failures', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
('changed', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('launch_job_status', self.gf('django.db.models.fields.related.ForeignKey')(related_name='launch_job_host_summaries', to=orm['main.LaunchJobStatus'])),
))
db.send_create_signal(u'main', ['LaunchJobHostSummary'])
# Adding unique constraint on 'LaunchJobHostSummary', fields ['launch_job_status', 'host']
db.create_unique(u'main_launchjobhostsummary', ['launch_job_status_id', 'host_id'])
# Adding model 'LaunchJobStatusEvent'
db.create_table(u'main_launchjobstatusevent', (
('event', self.gf('django.db.models.fields.CharField')(max_length=100)),
('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
('event_data', self.gf('jsonfield.fields.JSONField')(default='', blank=True)),
('host', self.gf('django.db.models.fields.related.ForeignKey')(related_name='launch_job_status_events', on_delete=models.SET_NULL, default=None, to=orm['main.Host'], blank=True, null=True)),
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('launch_job_status', self.gf('django.db.models.fields.related.ForeignKey')(related_name='launch_job_status_events', to=orm['main.LaunchJobStatus'])),
))
db.send_create_signal('main', ['LaunchJobStatusEvent'])
# Adding model 'LaunchJob'
db.create_table(u'main_launchjob', (
('credential', self.gf('django.db.models.fields.related.ForeignKey')(related_name='launch_jobs', on_delete=models.SET_NULL, default=None, to=orm['main.Credential'], blank=True, null=True)),
('description', self.gf('django.db.models.fields.TextField')(default='', blank=True)),
('job_type', self.gf('django.db.models.fields.CharField')(max_length=64)),
('creation_date', self.gf('django.db.models.fields.DateField')(auto_now_add=True, blank=True)),
('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='launch_jobs', on_delete=models.SET_NULL, default=None, to=orm['auth.User'], blank=True, null=True)),
('active', self.gf('django.db.models.fields.BooleanField')(default=True)),
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('name', self.gf('django.db.models.fields.CharField')(max_length=512, unique=True)),
('created_by', self.gf('django.db.models.fields.related.ForeignKey')(related_name="{'class': 'launchjob', 'app_label': 'main'}(class)s_created", null=True, on_delete=models.SET_NULL, to=orm['auth.User'])),
('project', self.gf('django.db.models.fields.related.ForeignKey')(related_name='launch_jobs', on_delete=models.SET_NULL, default=None, to=orm['main.Project'], blank=True, null=True)),
('inventory', self.gf('django.db.models.fields.related.ForeignKey')(related_name='launch_jobs', on_delete=models.SET_NULL, default=None, to=orm['main.Inventory'], blank=True, null=True)),
))
db.send_create_signal('main', ['LaunchJob'])
# Adding M2M table for field tags on 'LaunchJob'
db.create_table(u'main_launchjob_tags', (
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
('launchjob', models.ForeignKey(orm['main.launchjob'], null=False)),
('tag', models.ForeignKey(orm['main.tag'], null=False))
))
db.create_unique(u'main_launchjob_tags', ['launchjob_id', 'tag_id'])
# Adding M2M table for field audit_trail on 'LaunchJob'
db.create_table(u'main_launchjob_audit_trail', (
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
('launchjob', models.ForeignKey(orm['main.launchjob'], null=False)),
('audittrail', models.ForeignKey(orm['main.audittrail'], null=False))
))
db.create_unique(u'main_launchjob_audit_trail', ['launchjob_id', 'audittrail_id'])
# Adding model 'LaunchJobStatus'
db.create_table(u'main_launchjobstatus', (
('status', self.gf('django.db.models.fields.CharField')(default='pending', max_length=20)),
('description', self.gf('django.db.models.fields.TextField')(default='', blank=True)),
('result_traceback', self.gf('django.db.models.fields.TextField')(default='', blank=True)),
('result_stdout', self.gf('django.db.models.fields.TextField')(default='', blank=True)),
('created_by', self.gf('django.db.models.fields.related.ForeignKey')(related_name="{'class': 'launchjobstatus', 'app_label': 'main'}(class)s_created", null=True, on_delete=models.SET_NULL, to=orm['auth.User'])),
('result_stderr', self.gf('django.db.models.fields.TextField')(default='', blank=True)),
('celery_task_id', self.gf('django.db.models.fields.CharField')(default='', max_length=100, blank=True)),
('launch_job', self.gf('django.db.models.fields.related.ForeignKey')(related_name='launch_job_statuses', null=True, on_delete=models.SET_NULL, to=orm['main.LaunchJob'])),
('active', self.gf('django.db.models.fields.BooleanField')(default=True)),
('creation_date', self.gf('django.db.models.fields.DateField')(auto_now_add=True, blank=True)),
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('name', self.gf('django.db.models.fields.CharField')(max_length=512, unique=True)),
))
db.send_create_signal('main', ['LaunchJobStatus'])
# Adding M2M table for field tags on 'LaunchJobStatus'
db.create_table(u'main_launchjobstatus_tags', (
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
('launchjobstatus', models.ForeignKey(orm['main.launchjobstatus'], null=False)),
('tag', models.ForeignKey(orm['main.tag'], null=False))
))
db.create_unique(u'main_launchjobstatus_tags', ['launchjobstatus_id', 'tag_id'])
# Adding M2M table for field audit_trail on 'LaunchJobStatus'
db.create_table(u'main_launchjobstatus_audit_trail', (
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
('launchjobstatus', models.ForeignKey(orm['main.launchjobstatus'], null=False)),
('audittrail', models.ForeignKey(orm['main.audittrail'], null=False))
))
db.create_unique(u'main_launchjobstatus_audit_trail', ['launchjobstatus_id', 'audittrail_id'])
# Deleting model 'Job'
db.delete_table(u'main_job')
# Removing M2M table for field tags on 'Job'
db.delete_table('main_job_tags')
# Removing M2M table for field audit_trail on 'Job'
db.delete_table('main_job_audit_trail')
# Deleting model 'JobHostSummary'
db.delete_table(u'main_jobhostsummary')
# Deleting model 'JobTemplate'
db.delete_table(u'main_jobtemplate')
# Removing M2M table for field tags on 'JobTemplate'
db.delete_table('main_jobtemplate_tags')
# Removing M2M table for field audit_trail on 'JobTemplate'
db.delete_table('main_jobtemplate_audit_trail')
# Deleting model 'JobEvent'
db.delete_table(u'main_jobevent')
models = {
u'auth.group': {
'Meta': {'object_name': 'Group'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
u'auth.permission': {
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
u'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'main.audittrail': {
'Meta': {'object_name': 'AuditTrail'},
'comment': ('django.db.models.fields.TextField', [], {}),
'delta': ('django.db.models.fields.TextField', [], {}),
'detail': ('django.db.models.fields.TextField', [], {}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
'resource_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
'tag': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Tag']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'})
},
'main.credential': {
'Meta': {'object_name': 'Credential'},
'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'credential_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}),
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'credential\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
'creation_date': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'default_username': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}),
'ssh_key_data': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
'ssh_key_unlock': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
'ssh_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
'sudo_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'credential_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}),
'team': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'credentials'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Team']", 'blank': 'True', 'null': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'credentials'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': u"orm['auth.User']", 'blank': 'True', 'null': 'True'})
},
'main.group': {
'Meta': {'unique_together': "(('name', 'inventory'),)", 'object_name': 'Group'},
'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'group_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}),
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'group\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
'creation_date': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
'hosts': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'groups'", 'blank': 'True', 'to': "orm['main.Host']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'groups'", 'to': "orm['main.Inventory']"}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}),
'parents': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'children'", 'blank': 'True', 'to': "orm['main.Group']"}),
'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'group_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}),
'variable_data': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'group'", 'unique': 'True', 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.VariableData']", 'blank': 'True', 'null': 'True'})
},
'main.host': {
'Meta': {'unique_together': "(('name', 'inventory'),)", 'object_name': 'Host'},
'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'host_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}),
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'host\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
'creation_date': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'hosts'", 'to': "orm['main.Inventory']"}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}),
'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'host_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}),
'variable_data': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'host'", 'unique': 'True', 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.VariableData']", 'blank': 'True', 'null': 'True'})
},
'main.inventory': {
'Meta': {'unique_together': "(('name', 'organization'),)", 'object_name': 'Inventory'},
'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'inventory_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}),
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'inventory\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
'creation_date': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}),
'organization': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'inventories'", 'to': "orm['main.Organization']"}),
'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'inventory_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"})
},
'main.job': {
'Meta': {'object_name': 'Job'},
'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'job_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}),
'celery_task_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100', 'blank': 'True'}),
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'job\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
'creation_date': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Credential']"}),
'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
'hosts': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'jobs'", 'blank': 'True', 'through': u"orm['main.JobHostSummary']", 'to': "orm['main.Host']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}),
'job_template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.JobTemplate']", 'blank': 'True', 'null': 'True'}),
'job_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}),
'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['main.Project']"}),
'result_stderr': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
'result_stdout': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
'result_traceback': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '20'}),
'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'job_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"})
},
'main.jobevent': {
'Meta': {'object_name': 'JobEvent'},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'event': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'event_data': ('jsonfield.fields.JSONField', [], {'default': "''", 'blank': 'True'}),
'host': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_events'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Host']", 'blank': 'True', 'null': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_events'", 'to': "orm['main.Job']"})
},
u'main.jobhostsummary': {
'Meta': {'ordering': "('-pk',)", 'unique_together': "[('job', 'host')]", 'object_name': 'JobHostSummary'},
'changed': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'dark': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'host': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_host_summaries'", 'to': "orm['main.Host']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_host_summaries'", 'to': "orm['main.Job']"}),
'ok': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'processed': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'skipped': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
},
'main.jobtemplate': {
'Meta': {'object_name': 'JobTemplate'},
'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'jobtemplate_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}),
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'jobtemplate\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
'creation_date': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_templates'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_templates'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Inventory']", 'blank': 'True', 'null': 'True'}),
'job_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}),
'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_templates'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': u"orm['main.Project']", 'blank': 'True', 'null': 'True'}),
'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'jobtemplate_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_templates'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': u"orm['auth.User']", 'blank': 'True', 'null': 'True'})
},
'main.organization': {
'Meta': {'object_name': 'Organization'},
'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'admins': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'admin_of_organizations'", 'blank': 'True', 'to': u"orm['auth.User']"}),
'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'organization_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}),
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'organization\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
'creation_date': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}),
'projects': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'organizations'", 'blank': 'True', 'to': u"orm['main.Project']"}),
'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'organization_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}),
'users': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'organizations'", 'blank': 'True', 'to': u"orm['auth.User']"})
},
'main.permission': {
'Meta': {'object_name': 'Permission'},
'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'permission_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}),
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'permission\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
'creation_date': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}),
'permission_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
'project': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['main.Project']"}),
'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'permission_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}),
'team': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Team']"}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"})
},
u'main.project': {
'Meta': {'object_name': 'Project'},
'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'project_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}),
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'project\', \'app_label\': u\'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
'creation_date': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'default_playbook': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'local_repository': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}),
'scm_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'project_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"})
},
'main.tag': {
'Meta': {'object_name': 'Tag'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '512'})
},
'main.team': {
'Meta': {'object_name': 'Team'},
'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'team_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}),
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'team\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
'creation_date': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}),
'organization': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'teams'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Organization']"}),
'projects': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'teams'", 'blank': 'True', 'to': u"orm['main.Project']"}),
'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'team_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}),
'users': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'teams'", 'blank': 'True', 'to': u"orm['auth.User']"})
},
'main.variabledata': {
'Meta': {'object_name': 'VariableData'},
'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'variabledata_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}),
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'variabledata\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
'creation_date': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'data': ('django.db.models.fields.TextField', [], {}),
'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}),
'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'variabledata_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"})
}
}
complete_apps = ['main']

View File

@ -150,7 +150,7 @@ class PrimordialModel(models.Model):
abstract = True
description = models.TextField(blank=True, default='')
created_by = models.ForeignKey('auth.User', on_delete=SET_NULL, null=True, related_name='%s(class)s_created') # not blank=False on purpose for admin!
created_by = models.ForeignKey('auth.User', on_delete=SET_NULL, null=True, related_name='%s(class)s_created', editable=False) # not blank=False on purpose for admin!
creation_date = models.DateField(auto_now_add=True)
tags = models.ManyToManyField('Tag', related_name='%(class)s_by_tag', blank=True)
audit_trail = models.ManyToManyField('AuditTrail', related_name='%(class)s_by_audit_trail', blank=True)
@ -707,35 +707,51 @@ class Permission(CommonModelNameNotUnique):
# TODO: other job types (later)
class LaunchJob(CommonModel):
class JobTemplate(CommonModel):
'''
A launch job is a definition for applying a project (with playbook) to an
inventory source with a given credential.
A job template is a reusable job definition for applying a project (with
playbook) to an inventory source with a given credential.
'''
class Meta:
app_label = 'main'
inventory = models.ForeignKey('Inventory', on_delete=SET_NULL, null=True, default=None, blank=True, related_name='launch_jobs')
credential = models.ForeignKey('Credential', on_delete=SET_NULL, null=True, default=None, blank=True, related_name='launch_jobs')
project = models.ForeignKey('Project', on_delete=SET_NULL, null=True, default=None, blank=True, related_name='launch_jobs')
user = models.ForeignKey('auth.User', on_delete=SET_NULL, null=True, default=None, blank=True, related_name='launch_jobs')
# JOB_TYPE_CHOICES are a subset of PERMISSION_TYPE_CHOICES
job_type = models.CharField(max_length=64, choices=JOB_TYPE_CHOICES)
def start(self):
'''
Create a new launch job status and start the task via celery.
'''
from lib.main.tasks import run_launch_job
launch_job_status = self.launch_job_statuses.create(name='Launch Job Status %s' % now().isoformat())
task_result = run_launch_job.delay(launch_job_status.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.
launch_job_status.celery_task_id = task_result.task_id
launch_job_status.save(update_fields=['celery_task_id'])
return launch_job_status
job_type = models.CharField(
max_length=64,
choices=JOB_TYPE_CHOICES,
)
inventory = models.ForeignKey(
'Inventory',
related_name='job_templates',
blank=True,
null=True,
default=None,
on_delete=models.SET_NULL,
)
credential = models.ForeignKey(
'Credential',
related_name='job_templates',
blank=True,
null=True,
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,
)
user = models.ForeignKey(
'auth.User',
related_name='job_templates',
blank=True,
null=True,
default=None,
on_delete=models.SET_NULL,
)
# project has one default playbook but really should have a list of playbooks and flags ...
@ -756,9 +772,11 @@ class LaunchJob(CommonModel):
# --list
# -- host <hostname>
class LaunchJobStatus(CommonModel):
class Job(CommonModel):
'''
Status for a single run of a launch job.
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
given parameters.
'''
STATUS_CHOICES = [
@ -771,15 +789,77 @@ class LaunchJobStatus(CommonModel):
class Meta:
app_label = 'main'
verbose_name_plural = _('launch job statuses')
launch_job = models.ForeignKey('LaunchJob', null=True, on_delete=SET_NULL, related_name='launch_job_statuses')
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
result_stdout = models.TextField(blank=True, default='')
result_stderr = models.TextField(blank=True, default='')
result_traceback = models.TextField(blank=True, default='')
celery_task_id = models.CharField(max_length=100, blank=True, default='', editable=False)
hosts = models.ManyToManyField('Host', related_name='launch_job_statuses', blank=True, through='LaunchJobHostSummary')
job_template = models.ForeignKey(
'JobTemplate',
related_name='jobs',
blank=True,
null=True,
default=None,
on_delete=models.SET_NULL,
)
job_type = models.CharField(
max_length=64,
choices=JOB_TYPE_CHOICES,
)
inventory = models.ForeignKey(
'Inventory',
related_name='jobs',
null=True,
on_delete=models.SET_NULL,
)
credential = models.ForeignKey(
'Credential',
related_name='jobs',
null=True,
on_delete=models.SET_NULL,
)
project = models.ForeignKey(
'Project',
related_name='jobs',
null=True,
on_delete=models.SET_NULL,
)
user = models.ForeignKey(
'auth.User',
related_name='jobs',
null=True,
on_delete=models.SET_NULL,
)
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='pending',
editable=False,
)
result_stdout = models.TextField(
blank=True,
default='',
editable=False,
)
result_stderr = 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',
blank=True,
editable=False,
through='JobHostSummary',
)
@property
def celery_task(self):
@ -789,24 +869,46 @@ class LaunchJobStatus(CommonModel):
except TaskMeta.DoesNotExist:
pass
def save(self, *args, **kwargs):
super(LaunchJobStatus, self).save(*args, **kwargs)
# Create a new host summary for each host in the inventory.
for host in self.launch_job.inventory.hosts.all():
# Due to the way the inventory script is called, hosts without a group won't be affected.
if host.groups.count():
self.launch_job_host_summaries.get_or_create(host=host)
def _run(self):
from lib.main.tasks import run_job
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'])
class LaunchJobHostSummary(models.Model):
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()
class JobHostSummary(models.Model):
'''
Per-host statistics for each job.
'''
class Meta:
unique_together = [('launch_job_status', 'host')]
verbose_name_plural = _('Launch Job Host Summaries')
unique_together = [('job', 'host')]
verbose_name_plural = _('Job Host Summaries')
ordering = ('-pk',)
launch_job_status = models.ForeignKey('LaunchJobStatus', on_delete=models.CASCADE, related_name='launch_job_host_summaries')
host = models.ForeignKey('Host', on_delete=models.CASCADE, related_name='launch_job_host_summaries')
# FIXME: Can't use SET_NULL for host relationship because of unique constraint.
job = models.ForeignKey(
'Job',
related_name='job_host_summaries',
on_delete=models.CASCADE,
)
host = models.ForeignKey('Host',
related_name='job_host_summaries',
on_delete=models.CASCADE,
)
changed = models.PositiveIntegerField(default=0)
dark = models.PositiveIntegerField(default=0)
@ -820,7 +922,7 @@ class LaunchJobHostSummary(models.Model):
(self.host.name, self.changed, self.dark, self.failures, self.ok,
self.processed, self.skipped)
class LaunchJobStatusEvent(models.Model):
class JobEvent(models.Model):
'''
An event/message logged from the callback when running a job.
'''
@ -849,11 +951,30 @@ class LaunchJobStatusEvent(models.Model):
class Meta:
app_label = 'main'
launch_job_status = models.ForeignKey('LaunchJobStatus', related_name='launch_job_status_events', on_delete=CASCADE)
created = models.DateTimeField(auto_now_add=True)
event = models.CharField(max_length=100, choices=EVENT_TYPES)
event_data = JSONField(blank=True, default='')
host = models.ForeignKey('Host', blank=True, null=True, default=None, on_delete=SET_NULL, related_name='launch_job_status_events')
job = models.ForeignKey(
'Job',
related_name='job_events',
on_delete=models.CASCADE,
)
created = models.DateTimeField(
auto_now_add=True,
)
event = models.CharField(
max_length=100,
choices=EVENT_TYPES,
)
event_data = JSONField(
blank=True,
default='',
)
host = models.ForeignKey(
'Host',
related_name='job_events',
blank=True,
null=True,
default=None,
on_delete=models.SET_NULL,
)
def __unicode__(self):
return u'%s @ %s' % (self.get_event_display(), self.created.isoformat())
@ -861,31 +982,36 @@ class LaunchJobStatusEvent(models.Model):
def save(self, *args, **kwargs):
try:
if not self.host and self.event_data.get('host', ''):
# Make sure we're looking at only the hosts from this launch job's associated inventory.
self.host = self.launch_job_status.launch_job.inventory.hosts.get(name=self.event_data['host'])
self.host = self.job.inventory.hosts.get(name=self.event_data['host'])
except (Host.DoesNotExist, AttributeError):
pass
super(LaunchJobStatusEvent, self).save(*args, **kwargs)
super(JobEvent, self).save(*args, **kwargs)
self.update_host_summary_from_stats()
def update_host_summary_from_stats(self):
if self.event != 'playbook_on_stats':
return
hostnames = set()
for v in self.event_data.values():
hostnames.update(v.keys())
try:
for v in self.event_data.values():
hostnames.update(v.keys())
except AttributeError: # In case event_data or v isn't a dict.
pass
for hostname in hostnames:
try:
host = self.launch_job_status.launch_job.inventory.hosts.get(name=hostname)
host = self.job.inventory.hosts.get(name=hostname)
except Host.DoesNotExist:
continue
host_summary = self.launch_job_status.launch_job_host_summaries.get_or_create(host=host)[0]
host_summary = self.job.job_host_summaries.get_or_create(host=host)[0]
host_summary_changed = False
for stat in ('changed', 'dark', 'failures', 'ok', 'processed', 'skipped'):
value = self.event_data.get(stat, {}).get(hostname, 0)
if getattr(host_summary, stat) != value:
setattr(host_summary, stat, value)
host_summary_changed = True
try:
value = self.event_data.get(stat, {}).get(hostname, 0)
if getattr(host_summary, stat) != value:
setattr(host_summary, stat, value)
host_summary_changed = True
except AttributeError: # in case event_data[stat] isn't a dict.
pass
if host_summary_changed:
host_summary.save()

View File

@ -21,12 +21,13 @@ from celery import task
from django.conf import settings
from lib.main.models import *
@task(name='run_launch_job')
def run_launch_job(launch_job_status_pk):
launch_job_status = LaunchJobStatus.objects.get(pk=launch_job_status_pk)
launch_job_status.status = 'running'
launch_job_status.save()
launch_job = launch_job_status.launch_job
__all__ = ['run_job']
@task(name='run_job')
def run_job(job_pk):
job = Job.objects.get(pk=job_pk)
job.status = 'running'
job.save(update_fields=['status'])
try:
status, stdout, stderr, tb = 'error', '', '', ''
@ -40,17 +41,18 @@ def run_launch_job(launch_job_status_pk):
'acom_callback_event.py'))
env = dict(os.environ.items())
# question: when running over CLI, generate a random ID or grab next, etc?
env['ACOM_LAUNCH_JOB_STATUS_ID'] = str(launch_job_status.pk)
env['ACOM_INVENTORY_ID'] = str(launch_job.inventory.pk)
# answer: TBD
env['ACOM_JOB_ID'] = str(job.pk)
env['ACOM_INVENTORY_ID'] = str(job.inventory.pk)
env['ANSIBLE_CALLBACK_PLUGINS'] = plugin_dir
env['ACOM_CALLBACK_EVENT_SCRIPT'] = callback_script
if hasattr(settings, 'ANSIBLE_TRANSPORT'):
env['ANSIBLE_TRANSPORT'] = getattr(settings, 'ANSIBLE_TRANSPORT')
playbook = launch_job.project.default_playbook
playbook = job.project.default_playbook
cmdline = ['ansible-playbook', '-i', inventory_script]
if launch_job.job_type == 'check':
if job.job_type == 'check':
cmdline.append('--check')
cmdline.append(playbook)
@ -63,9 +65,10 @@ def run_launch_job(launch_job_status_pk):
tb = traceback.format_exc()
# Reload from database before updating/saving.
launch_job_status = LaunchJobStatus.objects.get(pk=launch_job_status_pk)
launch_job_status.status = status
launch_job_status.result_stdout = stdout
launch_job_status.result_stderr = stderr
launch_job_status.result_traceback = tb
launch_job_status.save()
job = Job.objects.get(pk=job_pk)
job.status = status
job.result_stdout = stdout
job.result_stderr = stderr
job.result_traceback = tb
job.save(update_fields=['status', 'result_stdout', 'result_stderr',
'result_traceback'])

View File

@ -19,4 +19,4 @@ from lib.main.tests.users import UsersTest
from lib.main.tests.inventory import InventoryTest
from lib.main.tests.projects import ProjectsTest
from lib.main.tests.commands import *
from lib.main.tests.tasks import RunLaunchJobTest
from lib.main.tests.tasks import RunJobTest

View File

@ -138,7 +138,7 @@ class AcomInventoryTest(BaseCommandTest):
def test_list_with_inventory_id_as_argument(self):
inventory = self.inventories[0]
result, stdout, stderr = self.run_command('acom_inventory', list=True,
inventory=inventory.pk)
inventory_id=inventory.pk)
self.assertEqual(result, None)
data = json.loads(stdout)
self.assertEqual(set(data.keys()),
@ -156,7 +156,7 @@ class AcomInventoryTest(BaseCommandTest):
invalid_id = [x for x in xrange(9999) if x not in inventory_pks][0]
os.environ['ACOM_INVENTORY_ID'] = str(invalid_id)
result, stdout, stderr = self.run_command('acom_inventory', list=True,
inventory=inventory.pk)
inventory_id=inventory.pk)
self.assertEqual(result, None)
data = json.loads(stdout)
@ -274,40 +274,38 @@ class AcomCallbackEventTest(BaseCommandTest):
self.group = self.inventory.groups.create(name='test-group',
inventory=self.inventory)
self.group.hosts.add(self.host)
self.launch_job = LaunchJob.objects.create(name='test-launch-job',
inventory=self.inventory,
project=self.project)
self.launch_job_status = self.launch_job.launch_job_statuses.create(
name='launch-job-status-%s' % now().isoformat())
self.job = Job.objects.create(name='job-%s' % now().isoformat(),
inventory=self.inventory,
project=self.project)
self.valid_kwargs = {
'launch_job_status_id': self.launch_job_status.id,
'job_id': self.job.id,
'event_type': 'playbook_on_start',
'event_data_json': json.dumps({'test_event_data': [2,4,6]}),
}
def test_with_launch_job_status_not_running(self):
# Events can only be added when the launch job is running.
self.assertEqual(self.launch_job_status.status, 'pending')
def test_with_job_status_not_running(self):
# Events can only be added when the job is running.
self.assertEqual(self.job.status, 'pending')
result, stdout, stderr = self.run_command('acom_callback_event',
**self.valid_kwargs)
self.assertTrue(isinstance(result, CommandError))
self.assertTrue('unable to add event ' in str(result).lower())
self.launch_job_status.status = 'successful'
self.launch_job_status.save()
self.job.status = 'successful'
self.job.save()
result, stdout, stderr = self.run_command('acom_callback_event',
**self.valid_kwargs)
self.assertTrue(isinstance(result, CommandError))
self.assertTrue('unable to add event ' in str(result).lower())
self.launch_job_status.status = 'failed'
self.launch_job_status.save()
self.job.status = 'failed'
self.job.save()
result, stdout, stderr = self.run_command('acom_callback_event',
**self.valid_kwargs)
self.assertTrue(isinstance(result, CommandError))
self.assertTrue('unable to add event ' in str(result).lower())
def test_with_invalid_args(self):
self.launch_job_status.status = 'running'
self.launch_job_status.save()
self.job.status = 'running'
self.job.save()
# Event type not given.
kwargs = dict(self.valid_kwargs.items())
kwargs.pop('event_type')
@ -326,21 +324,21 @@ class AcomCallbackEventTest(BaseCommandTest):
result, stdout, stderr = self.run_command('acom_callback_event', **kwargs)
self.assertTrue(isinstance(result, CommandError))
self.assertTrue('either --file or --data' in str(result).lower())
# Non-integer launch job status ID.
# Non-integer job ID.
kwargs = dict(self.valid_kwargs.items())
kwargs['launch_job_status_id'] = 'foo'
kwargs['job_id'] = 'foo'
result, stdout, stderr = self.run_command('acom_callback_event', **kwargs)
self.assertTrue(isinstance(result, CommandError))
self.assertTrue('id must be an integer' in str(result).lower())
# No launch job status ID.
# No job ID.
kwargs = dict(self.valid_kwargs.items())
kwargs.pop('launch_job_status_id')
kwargs.pop('job_id')
result, stdout, stderr = self.run_command('acom_callback_event', **kwargs)
self.assertTrue(isinstance(result, CommandError))
self.assertTrue('no launch job status id' in str(result).lower())
# Invalid launch job status ID.
self.assertTrue('no job id' in str(result).lower())
# Invalid job ID.
kwargs = dict(self.valid_kwargs.items())
kwargs['launch_job_status_id'] = 9999
kwargs['job_id'] = 9999
result, stdout, stderr = self.run_command('acom_callback_event', **kwargs)
self.assertTrue(isinstance(result, CommandError))
self.assertTrue('not found' in str(result).lower())
@ -362,21 +360,21 @@ class AcomCallbackEventTest(BaseCommandTest):
self.assertTrue('reading from' in str(result).lower())
def test_with_valid_args(self):
self.launch_job_status.status = 'running'
self.launch_job_status.save()
self.job.status = 'running'
self.job.save()
# Default valid args.
kwargs = dict(self.valid_kwargs.items())
result, stdout, stderr = self.run_command('acom_callback_event', **kwargs)
self.assertEqual(result, None)
self.assertEqual(self.launch_job_status.launch_job_status_events.count(), 1)
# Pass launch job status in environment instead.
self.assertEqual(self.job.job_events.count(), 1)
# Pass job ID in environment instead.
kwargs = dict(self.valid_kwargs.items())
kwargs.pop('launch_job_status_id')
os.environ['ACOM_LAUNCH_JOB_STATUS_ID'] = str(self.launch_job_status.id)
kwargs.pop('job_id')
os.environ['ACOM_JOB_ID'] = str(self.job.id)
result, stdout, stderr = self.run_command('acom_callback_event', **kwargs)
self.assertEqual(result, None)
self.assertEqual(self.launch_job_status.launch_job_status_events.count(), 2)
os.environ.pop('ACOM_LAUNCH_JOB_STATUS_ID', None)
self.assertEqual(self.job.job_events.count(), 2)
os.environ.pop('ACOM_JOB_ID', None)
# Test with JSON data in a file instead.
kwargs = dict(self.valid_kwargs.items())
kwargs.pop('event_data_json')
@ -388,7 +386,7 @@ class AcomCallbackEventTest(BaseCommandTest):
kwargs['event_data_file'] = tf
result, stdout, stderr = self.run_command('acom_callback_event', **kwargs)
self.assertEqual(result, None)
self.assertEqual(self.launch_job_status.launch_job_status_events.count(), 3)
self.assertEqual(self.job.job_events.count(), 3)
# Test with JSON data from stdin.
kwargs = dict(self.valid_kwargs.items())
kwargs.pop('event_data_json')
@ -396,4 +394,4 @@ class AcomCallbackEventTest(BaseCommandTest):
kwargs['stdin_fileobj'] = StringIO.StringIO(json.dumps({'blah': 'bleep'}))
result, stdout, stderr = self.run_command('acom_callback_event', **kwargs)
self.assertEqual(result, None)
self.assertEqual(self.launch_job_status.launch_job_status_events.count(), 4)
self.assertEqual(self.job.job_events.count(), 4)

View File

@ -46,13 +46,13 @@ class BaseCeleryTest(BaseTransactionTest):
'''
@override_settings(ANSIBLE_TRANSPORT='local')
class RunLaunchJobTest(BaseCeleryTest):
class RunJobTest(BaseCeleryTest):
'''
Test cases for run_launch_job celery task.
Test cases for run_job celery task.
'''
def setUp(self):
super(RunLaunchJobTest, self).setUp()
super(RunJobTest, self).setUp()
self.setup_users()
self.organization = self.make_organizations(self.super_django_user, 1)[0]
self.project = self.make_projects(self.normal_django_user, 1)[0]
@ -65,14 +65,11 @@ class RunLaunchJobTest(BaseCeleryTest):
self.group = self.inventory.groups.create(name='test-group',
inventory=self.inventory)
self.group.hosts.add(self.host)
self.launch_job = LaunchJob.objects.create(name='test-launch-job',
inventory=self.inventory,
project=self.project)
# Pass test database name in environment for use by the inventory script.
os.environ['ACOM_TEST_DATABASE_NAME'] = settings.DATABASES['default']['NAME']
def tearDown(self):
super(RunLaunchJobTest, self).tearDown()
super(RunJobTest, self).tearDown()
os.environ.pop('ACOM_TEST_DATABASE_NAME', None)
os.remove(self.test_playbook)
@ -84,84 +81,84 @@ class RunLaunchJobTest(BaseCeleryTest):
self.project.default_playbook = self.test_playbook
self.project.save()
def test_run_launch_job(self):
def test_run_job(self):
self.create_test_playbook(TEST_PLAYBOOK)
launch_job_status = self.launch_job.start()
self.assertEqual(launch_job_status.status, 'pending')
self.assertEqual(set(launch_job_status.hosts.values_list('pk', flat=True)),
job = Job.objects.create(name='test-job', inventory=self.inventory,
project=self.project)
self.assertEqual(job.status, 'pending')
self.assertEqual(set(job.hosts.values_list('pk', flat=True)),
set([self.host.pk]))
launch_job_status = LaunchJobStatus.objects.get(pk=launch_job_status.pk)
job = Job.objects.get(pk=job.pk)
#print 'stdout:', launch_job_status.result_stdout
#print 'stderr:', launch_job_status.result_stderr
#print launch_job_status.status
#print settings.DATABASES
self.assertEqual(launch_job_status.status, 'successful')
self.assertTrue(launch_job_status.result_stdout)
launch_job_status_events = launch_job_status.launch_job_status_events.all()
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(launch_job_status_events.filter(event='playbook_on_start').count(), 1)
self.assertEqual(launch_job_status_events.filter(event='playbook_on_play_start').count(), 1)
self.assertEqual(launch_job_status_events.filter(event='playbook_on_task_start').count(), 2)
self.assertEqual(launch_job_status_events.filter(event='runner_on_ok').count(), 2)
for evt in launch_job_status_events.filter(event='runner_on_ok'):
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)
self.assertEqual(job_events.filter(event='runner_on_ok').count(), 2)
for evt in job_events.filter(event='runner_on_ok'):
self.assertEqual(evt.host, self.host)
self.assertEqual(launch_job_status_events.filter(event='playbook_on_stats').count(), 1)
self.assertEqual(job_events.filter(event='playbook_on_stats').count(), 1)
def test_check_launch_job(self):
def test_check_job(self):
self.create_test_playbook(TEST_PLAYBOOK)
self.launch_job.job_type = 'check'
self.launch_job.save()
launch_job_status = self.launch_job.start()
self.assertEqual(launch_job_status.status, 'pending')
self.assertEqual(set(launch_job_status.hosts.values_list('pk', flat=True)),
job = Job.objects.create(name='test-job', inventory=self.inventory,
project=self.project, job_type='check')
self.assertEqual(job.status, 'pending')
self.assertEqual(set(job.hosts.values_list('pk', flat=True)),
set([self.host.pk]))
launch_job_status = LaunchJobStatus.objects.get(pk=launch_job_status.pk)
self.assertEqual(launch_job_status.status, 'successful')
self.assertTrue(launch_job_status.result_stdout)
launch_job_status_events = launch_job_status.launch_job_status_events.all()
self.assertEqual(launch_job_status_events.filter(event='playbook_on_start').count(), 1)
self.assertEqual(launch_job_status_events.filter(event='playbook_on_play_start').count(), 1)
self.assertEqual(launch_job_status_events.filter(event='playbook_on_task_start').count(), 2)
self.assertEqual(launch_job_status_events.filter(event='runner_on_skipped').count(), 2)
for evt in launch_job_status_events.filter(event='runner_on_skipped'):
job = Job.objects.get(pk=job.pk)
self.assertEqual(job.status, 'successful')
self.assertTrue(job.result_stdout)
job_events = job.job_events.all()
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)
self.assertEqual(job_events.filter(event='runner_on_skipped').count(), 2)
for evt in job_events.filter(event='runner_on_skipped'):
self.assertEqual(evt.host, self.host)
self.assertEqual(launch_job_status_events.filter(event='playbook_on_stats').count(), 1)
self.assertEqual(job_events.filter(event='playbook_on_stats').count(), 1)
def test_run_launch_job_that_fails(self):
def test_run_job_that_fails(self):
self.create_test_playbook(TEST_PLAYBOOK2)
launch_job_status = self.launch_job.start()
self.assertEqual(launch_job_status.status, 'pending')
self.assertEqual(set(launch_job_status.hosts.values_list('pk', flat=True)),
job = Job.objects.create(name='test-job', inventory=self.inventory,
project=self.project)
self.assertEqual(job.status, 'pending')
self.assertEqual(set(job.hosts.values_list('pk', flat=True)),
set([self.host.pk]))
launch_job_status = LaunchJobStatus.objects.get(pk=launch_job_status.pk)
self.assertEqual(launch_job_status.status, 'failed')
self.assertTrue(launch_job_status.result_stdout)
launch_job_status_events = launch_job_status.launch_job_status_events.all()
self.assertEqual(launch_job_status_events.filter(event='playbook_on_start').count(), 1)
self.assertEqual(launch_job_status_events.filter(event='playbook_on_play_start').count(), 1)
self.assertEqual(launch_job_status_events.filter(event='playbook_on_task_start').count(), 1)
self.assertEqual(launch_job_status_events.filter(event='runner_on_failed').count(), 1)
self.assertEqual(launch_job_status_events.get(event='runner_on_failed').host, self.host)
self.assertEqual(launch_job_status_events.filter(event='playbook_on_stats').count(), 1)
job = Job.objects.get(pk=job.pk)
self.assertEqual(job.status, 'failed')
self.assertTrue(job.result_stdout)
job_events = job.job_events.all()
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(), 1)
self.assertEqual(job_events.filter(event='runner_on_failed').count(), 1)
self.assertEqual(job_events.get(event='runner_on_failed').host, self.host)
self.assertEqual(job_events.filter(event='playbook_on_stats').count(), 1)
def test_check_launch_job_where_task_would_fail(self):
def test_check_job_where_task_would_fail(self):
self.create_test_playbook(TEST_PLAYBOOK2)
self.launch_job.job_type = 'check'
self.launch_job.save()
launch_job_status = self.launch_job.start()
self.assertEqual(launch_job_status.status, 'pending')
self.assertEqual(set(launch_job_status.hosts.values_list('pk', flat=True)),
job = Job.objects.create(name='test-job', inventory=self.inventory,
project=self.project, job_type='check')
self.assertEqual(job.status, 'pending')
self.assertEqual(set(job.hosts.values_list('pk', flat=True)),
set([self.host.pk]))
launch_job_status = LaunchJobStatus.objects.get(pk=launch_job_status.pk)
job = Job.objects.get(pk=job.pk)
# Since we don't actually run the task, the --check should indicate
# everything is successful.
self.assertEqual(launch_job_status.status, 'successful')
self.assertTrue(launch_job_status.result_stdout)
launch_job_status_events = launch_job_status.launch_job_status_events.all()
self.assertEqual(launch_job_status_events.filter(event='playbook_on_start').count(), 1)
self.assertEqual(launch_job_status_events.filter(event='playbook_on_play_start').count(), 1)
self.assertEqual(launch_job_status_events.filter(event='playbook_on_task_start').count(), 1)
self.assertEqual(launch_job_status_events.filter(event='runner_on_skipped').count(), 1)
self.assertEqual(launch_job_status_events.get(event='runner_on_skipped').host, self.host)
self.assertEqual(launch_job_status_events.filter(event='playbook_on_stats').count(), 1)
self.assertEqual(job.status, 'successful')
self.assertTrue(job.result_stdout)
job_events = job.job_events.all()
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(), 1)
self.assertEqual(job_events.filter(event='runner_on_skipped').count(), 1)
self.assertEqual(job_events.get(event='runner_on_skipped').host, self.host)
self.assertEqual(job_events.filter(event='playbook_on_stats').count(), 1)

View File

@ -1,34 +1,36 @@
# Copyright (c) 2013 AnsibleWorks, Inc.
# This file is a utility Ansible plugin that is not part of Ansible Commander or Ansible.
# (it does not import any ansible-commander code, nor does it's license apply to Ansible
# or Ansible Commander)
# This file is a utility Ansible plugin that is not part of Ansible Commander
# or Ansible. It does not import any ansible-commander code, nor does its
# license apply to Ansible or Ansible Commander.
#
# Copyright (c) 2013, AnsibleWorks Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# Redistributions of source code must retain the above copyright notice, this list
# of conditions and the following disclaimer.
# Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
# Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# Neither the name of the <ORGANIZATION> nor the names of its contributors may be
# used to endorse or promote products derived from this software without specific
# prior written permission.
# Neither the name of the <ORGANIZATION> nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
# OF SUCH DAMAGE.
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import json
import os
@ -41,11 +43,11 @@ class CallbackModule(object):
'''
def __init__(self):
self.acom_callback_event_script = os.getenv('ACOM_CALLBACK_EVENT_SCRIPT')
self.callback_script = os.getenv('ACOM_CALLBACK_EVENT_SCRIPT')
def _log_event(self, event, **event_data):
event_data_json = json.dumps(event_data)
cmdline = [self.acom_callback_event_script, '-e', event, '-d', event_data_json]
cmdline = [self.callback_script, '-e', event, '-d', event_data_json]
subprocess.check_call(cmdline)
def on_any(self, *args, **kwargs):
@ -117,5 +119,3 @@ class CallbackModule(object):
for attr in ('changed', 'dark', 'failures', 'ok', 'processed', 'skipped'):
d[attr] = getattr(stats, attr)
self._log_event('playbook_on_stats', **d)

View File

@ -116,10 +116,10 @@ pre.result-display {
max-height: 300px;
overflow: auto;
}
#launch_job_host_summaries-group table td.original p {
#job_host_summaries-group table td.original p {
display: none
}
#launch_job_host_summaries-group table tr.has_original td {
#job_host_summaries-group table tr.has_original td {
padding-top: 5px;
}
</style>