AC-1040 Work on unified job serializers.

This commit is contained in:
Chris Church
2014-03-25 22:04:37 -04:00
parent 62a74f9c84
commit 8ad72426b4
6 changed files with 311 additions and 256 deletions

View File

@@ -33,9 +33,6 @@ from awx.main.utils import update_scm_url, camelcase_to_underscore
logger = logging.getLogger('awx.api.serializers') logger = logging.getLogger('awx.api.serializers')
BASE_FIELDS = ('id', 'type', 'url', 'related', 'summary_fields', 'created',
'modified', 'name', 'description')
# Fields that should be summarized regardless of object type. # Fields that should be summarized regardless of object type.
DEFAULT_SUMMARY_FIELDS = ('name', 'description',) DEFAULT_SUMMARY_FIELDS = ('name', 'description',)
@@ -65,13 +62,15 @@ SUMMARIZABLE_FK_FIELDS = {
'has_inventory_sources'), 'has_inventory_sources'),
'project': DEFAULT_SUMMARY_FIELDS + ('status',), 'project': DEFAULT_SUMMARY_FIELDS + ('status',),
'credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud'), 'credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud'),
'cloud_credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud'),
'permission': DEFAULT_SUMMARY_FIELDS, 'permission': DEFAULT_SUMMARY_FIELDS,
'job': DEFAULT_SUMMARY_FIELDS + ('status', 'failed',), 'job': DEFAULT_SUMMARY_FIELDS + ('status', 'failed',),
'job_template': DEFAULT_SUMMARY_FIELDS, 'job_template': DEFAULT_SUMMARY_FIELDS,
'last_job': DEFAULT_SUMMARY_FIELDS + ('status', 'failed',), 'last_job': DEFAULT_SUMMARY_FIELDS + ('status', 'failed', 'license_error'),
'last_job_host_summary': DEFAULT_SUMMARY_FIELDS + ('failed',), 'last_job_host_summary': DEFAULT_SUMMARY_FIELDS + ('failed',),
'last_update': DEFAULT_SUMMARY_FIELDS + ('status', 'failed', 'license_error'), 'last_update': DEFAULT_SUMMARY_FIELDS + ('status', 'failed', 'license_error'),
'current_update': DEFAULT_SUMMARY_FIELDS + ('status', 'failed', 'license_error'), 'current_update': DEFAULT_SUMMARY_FIELDS + ('status', 'failed', 'license_error'),
'current_job': DEFAULT_SUMMARY_FIELDS + ('status', 'failed', 'license_error'),
'inventory_source': ('source', 'last_updated', 'status'), 'inventory_source': ('source', 'last_updated', 'status'),
} }
@@ -98,13 +97,87 @@ class ChoiceField(fields.ChoiceField):
serializers.ChoiceField = ChoiceField serializers.ChoiceField = ChoiceField
class BaseSerializerMetaclass(serializers.SerializerMetaclass):
'''
Custom metaclass to enable attribute inheritance from Meta objects on
serializer base classes.
Also allows for inheriting or updating field lists from base class(es):
class Meta:
# Inherit all fields from base class.
fields = ('*',)
# Inherit all fields from base class and add 'foo'.
fields = ('*', 'foo')
# Inherit all fields from base class except 'bar'.
fields = ('*', '-bar')
# Define fields as 'foo' and 'bar'; ignore base class fields.
fields = ('foo', 'bar')
'''
@classmethod
def _update_meta(self, base, meta, other=None):
for attr in dir(other):
if attr.startswith('_'):
continue
val = getattr(other, attr)
# Special handling for lists of strings (field names).
if isinstance(val, (list, tuple)) and all([isinstance(x, basestring) for x in val]):
new_vals = []
except_vals = []
if base: # Merge values from all bases.
new_vals.extend([x for x in getattr(meta, attr, [])])
for v in val:
if not base and v == '*': # Inherit all values from previous base(es).
new_vals.extend([x for x in getattr(meta, attr, [])])
elif not base and v.startswith('-'): # Except these values.
except_vals.append(v[1:])
else:
new_vals.append(v)
val = []
for v in new_vals:
if v not in except_vals and v not in val:
val.append(v)
setattr(meta, attr, val)
def __new__(cls, name, bases, attrs):
meta = type('Meta', (object,), {})
for base in bases:
cls._update_meta(base, meta, getattr(base, 'Meta', None))
cls._update_meta(None, meta, attrs.get('Meta', None))
attrs['Meta'] = meta
return super(BaseSerializerMetaclass, cls).__new__(cls, name, bases, attrs)
class BaseSerializerOptions(serializers.ModelSerializerOptions):
def __init__(self, meta):
super(BaseSerializerOptions, self).__init__(meta)
self.summary_fields = getattr(meta, 'summary_fields', ())
self.summarizable_fields = getattr(meta, 'summarizable_fields', ())
class BaseSerializer(serializers.ModelSerializer): class BaseSerializer(serializers.ModelSerializer):
__metaclass__ = BaseSerializerMetaclass
_options_class = BaseSerializerOptions
class Meta:
fields = ('id', 'type', 'url', 'related', 'summary_fields', 'created',
'modified', 'name', 'description')
summary_fields = () # FIXME: List of field names from this serializer that should be used when included as part of another's summary_fields.
summarizable_fields = () # FIXME: List of field names on this serializer that should be included in summary_fields.
# add the URL and related resources # add the URL and related resources
type = serializers.SerializerMethodField('get_type') type = serializers.SerializerMethodField('get_type')
url = serializers.SerializerMethodField('get_url') url = serializers.SerializerMethodField('get_url')
related = serializers.SerializerMethodField('get_related') related = serializers.SerializerMethodField('_get_related')
summary_fields = serializers.SerializerMethodField('get_summary_fields') summary_fields = serializers.SerializerMethodField('_get_summary_fields')
# make certain fields read only # make certain fields read only
created = serializers.SerializerMethodField('get_created') created = serializers.SerializerMethodField('get_created')
@@ -149,6 +222,9 @@ class BaseSerializer(serializers.ModelSerializer):
else: else:
return obj.get_absolute_url() return obj.get_absolute_url()
def _get_related(self, obj):
return {} if obj is None else self.get_related(obj)
def get_related(self, obj): def get_related(self, obj):
res = SortedDict() res = SortedDict()
if getattr(obj, 'created_by', None) and obj.created_by.is_active: if getattr(obj, 'created_by', None) and obj.created_by.is_active:
@@ -157,6 +233,9 @@ class BaseSerializer(serializers.ModelSerializer):
res['modified_by'] = reverse('api:user_detail', args=(obj.modified_by.pk,)) res['modified_by'] = reverse('api:user_detail', args=(obj.modified_by.pk,))
return res return res
def _get_summary_fields(self, obj):
return {} if obj is None else self.get_summary_fields(obj)
def get_summary_fields(self, obj): def get_summary_fields(self, obj):
# Return values for certain fields on related objects, to simplify # Return values for certain fields on related objects, to simplify
# displaying lists of items without additional API requests. # displaying lists of items without additional API requests.
@@ -166,6 +245,8 @@ class BaseSerializer(serializers.ModelSerializer):
fkval = getattr(obj, fk, None) fkval = getattr(obj, fk, None)
if fkval is None: if fkval is None:
continue continue
if fkval == obj:
continue
if hasattr(fkval, 'active') and not fkval.active: if hasattr(fkval, 'active') and not fkval.active:
continue continue
if hasattr(fkval, 'is_active') and not fkval.is_active: if hasattr(fkval, 'is_active') and not fkval.is_active:
@@ -205,6 +286,43 @@ class BaseSerializer(serializers.ModelSerializer):
return obj.active return obj.active
class UnifiedJobTemplateSerializer(BaseSerializer):
class Meta:
model = UnifiedJobTemplate
fields = ('*', 'last_job_run', 'last_job_failed', 'has_schedules',
'next_job_run', 'status')
def get_related(self, obj):
res = super(UnifiedJobTemplateSerializer, self).get_related(obj)
if obj.current_job and obj.current_job.active:
res['current_job'] = obj.current_job.get_absolute_url()
if obj.last_job and obj.last_job.active:
res['last_job'] = obj.last_job.get_absolute_url()
if obj.next_schedule and obj.next_schedule.active:
res['next_schedule'] = obj.next_schedule.get_absolute_url()
return res
class UnifiedJobSerializer(BaseSerializer):
result_stdout = serializers.Field(source='result_stdout')
class Meta:
model = UnifiedJob
fields = ('*', 'unified_job_template', 'launch_type', 'status',
'failed', 'started', 'finished', 'elapsed', 'job_args',
'job_cwd', 'job_env', 'result_stdout', 'result_traceback')
def get_related(self, obj):
res = super(UnifiedJobSerializer, self).get_related(obj)
if obj.unified_job_template and obj.unified_job_template.active:
res['unified_job_template'] = obj.unified_job_template.get_absolute_url()
if obj.schedule and obj.schedule.active:
res['schedule'] = obj.schedule.get_absolute_url()
return res
class BaseTaskSerializer(BaseSerializer): class BaseTaskSerializer(BaseSerializer):
job_env = serializers.SerializerMethodField('get_job_env') job_env = serializers.SerializerMethodField('get_job_env')
@@ -224,9 +342,9 @@ class UserSerializer(BaseSerializer):
class Meta: class Meta:
model = User model = User
fields = ('id', 'type', 'url', 'related', 'created', 'username', fields = ('*', '-name', '-description', '-modified',
'first_name', 'last_name', 'email', 'is_superuser', '-summary_fields', 'username', 'first_name', 'last_name',
'password', 'ldap_dn') 'email', 'is_superuser', 'password', 'ldap_dn')
def to_native(self, obj): def to_native(self, obj):
ret = super(UserSerializer, self).to_native(obj) ret = super(UserSerializer, self).to_native(obj)
@@ -261,8 +379,6 @@ class UserSerializer(BaseSerializer):
return super(UserSerializer, self).save_object(obj, **kwargs) return super(UserSerializer, self).save_object(obj, **kwargs)
def get_related(self, obj): def get_related(self, obj):
if obj is None:
return {}
res = super(UserSerializer, self).get_related(obj) res = super(UserSerializer, self).get_related(obj)
res.update(dict( res.update(dict(
teams = reverse('api:user_teams_list', args=(obj.pk,)), teams = reverse('api:user_teams_list', args=(obj.pk,)),
@@ -309,69 +425,32 @@ class OrganizationSerializer(BaseSerializer):
class Meta: class Meta:
model = Organization model = Organization
fields = BASE_FIELDS fields = ('*',)
def get_related(self, obj): def get_related(self, obj):
if obj is None:
return {}
res = super(OrganizationSerializer, self).get_related(obj) res = super(OrganizationSerializer, self).get_related(obj)
res.update(dict( res.update(dict(
#audit_trail = reverse('api:organization_audit_trail_list', args=(obj.pk,)),
projects = reverse('api:organization_projects_list', args=(obj.pk,)), projects = reverse('api:organization_projects_list', args=(obj.pk,)),
inventories = reverse('api:organization_inventories_list', args=(obj.pk,)), inventories = reverse('api:organization_inventories_list', args=(obj.pk,)),
users = reverse('api:organization_users_list', args=(obj.pk,)), users = reverse('api:organization_users_list', args=(obj.pk,)),
admins = reverse('api:organization_admins_list', args=(obj.pk,)), admins = reverse('api:organization_admins_list', args=(obj.pk,)),
#tags = reverse('api:organization_tags_list', args=(obj.pk,)),
teams = reverse('api:organization_teams_list', args=(obj.pk,)), teams = reverse('api:organization_teams_list', args=(obj.pk,)),
activity_stream = reverse('api:organization_activity_stream_list', args=(obj.pk,)) activity_stream = reverse('api:organization_activity_stream_list', args=(obj.pk,))
)) ))
return res return res
class ProjectSerializer(BaseSerializer): class ProjectOptionsSerializer(BaseSerializer):
playbooks = serializers.Field(source='playbooks', help_text='Array of playbooks available within this project.')
scm_delete_on_next_update = serializers.Field(source='scm_delete_on_next_update')
last_update_failed = serializers.Field(source='last_update_failed')
last_updated = serializers.Field(source='last_updated')
class Meta: class Meta:
model = Project fields = ('*', 'local_path', 'scm_type', 'scm_url', 'scm_branch',
fields = BASE_FIELDS + ('local_path', 'scm_type', 'scm_url', 'scm_clean', 'scm_delete_on_update', 'credential')
'scm_branch', 'scm_clean',
'scm_delete_on_update', 'scm_delete_on_next_update',
'scm_update_on_launch', 'credential',
'last_job_failed', 'status', 'last_job_run') +\
('last_update_failed', 'last_updated',) # Backwards compatibility
def get_related(self, obj): def get_related(self, obj):
if obj is None: res = super(ProjectOptionsSerializer, self).get_related(obj)
return {}
res = super(ProjectSerializer, self).get_related(obj)
res.update(dict(
organizations = reverse('api:project_organizations_list', args=(obj.pk,)),
teams = reverse('api:project_teams_list', args=(obj.pk,)),
playbooks = reverse('api:project_playbooks', args=(obj.pk,)),
update = reverse('api:project_update_view', args=(obj.pk,)),
project_updates = reverse('api:project_updates_list', args=(obj.pk,)),
activity_stream = reverse('api:project_activity_stream_list', args=(obj.pk,)),
))
if obj.credential and obj.credential.active: if obj.credential and obj.credential.active:
res['credential'] = reverse('api:credential_detail', res['credential'] = reverse('api:credential_detail',
args=(obj.credential.pk,)) args=(obj.credential.pk,))
if obj.current_job:
res['current_job'] = reverse('api:project_update_detail',
args=(obj.current_job.pk,))
if obj.last_job:
res['last_job'] = reverse('api:project_update_detail',
args=(obj.last_job.pk,))
# Backwards compatibility.
if obj.current_update:
res['current_update'] = reverse('api:project_update_detail',
args=(obj.current_update.pk,))
if obj.last_update:
res['last_update'] = reverse('api:project_update_detail',
args=(obj.last_update.pk,))
return res return res
def validate_local_path(self, attrs, source): def validate_local_path(self, attrs, source):
@@ -391,12 +470,44 @@ class ProjectSerializer(BaseSerializer):
return attrs return attrs
def to_native(self, obj): def to_native(self, obj):
ret = super(ProjectSerializer, self).to_native(obj) ret = super(ProjectOptionsSerializer, self).to_native(obj)
if obj is not None and 'credential' in ret and (not obj.credential or not obj.credential.active): if obj is not None and 'credential' in ret and (not obj.credential or not obj.credential.active):
ret['credential'] = None ret['credential'] = None
return ret return ret
class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer):
playbooks = serializers.Field(source='playbooks', help_text='Array of playbooks available within this project.')
scm_delete_on_next_update = serializers.Field(source='scm_delete_on_next_update')
last_update_failed = serializers.Field(source='last_update_failed')
last_updated = serializers.Field(source='last_updated')
class Meta:
model = Project
fields = ('*', 'scm_delete_on_next_update', 'scm_update_on_launch') + \
('last_update_failed', 'last_updated') # Backwards compatibility
def get_related(self, obj):
res = super(ProjectSerializer, self).get_related(obj)
res.update(dict(
organizations = reverse('api:project_organizations_list', args=(obj.pk,)),
teams = reverse('api:project_teams_list', args=(obj.pk,)),
playbooks = reverse('api:project_playbooks', args=(obj.pk,)),
update = reverse('api:project_update_view', args=(obj.pk,)),
project_updates = reverse('api:project_updates_list', args=(obj.pk,)),
activity_stream = reverse('api:project_activity_stream_list', args=(obj.pk,)),
))
# Backwards compatibility.
if obj.current_update:
res['current_update'] = reverse('api:project_update_detail',
args=(obj.current_update.pk,))
if obj.last_update:
res['last_update'] = reverse('api:project_update_detail',
args=(obj.last_update.pk,))
return res
class ProjectPlaybooksSerializer(ProjectSerializer): class ProjectPlaybooksSerializer(ProjectSerializer):
class Meta: class Meta:
@@ -408,19 +519,13 @@ class ProjectPlaybooksSerializer(ProjectSerializer):
return ret.get('playbooks', []) return ret.get('playbooks', [])
class ProjectUpdateSerializer(BaseTaskSerializer): class ProjectUpdateSerializer(UnifiedJobSerializer, ProjectOptionsSerializer):
result_stdout = serializers.Field(source='result_stdout')
class Meta: class Meta:
model = ProjectUpdate model = ProjectUpdate
fields = ('id', 'type', 'url', 'related', 'summary_fields', 'created', fields = ('*', 'project')
'modified', 'project', 'status', 'failed', 'result_stdout',
'result_traceback', 'job_args', 'job_cwd', 'job_env')
def get_related(self, obj): def get_related(self, obj):
if obj is None:
return {}
res = super(ProjectUpdateSerializer, self).get_related(obj) res = super(ProjectUpdateSerializer, self).get_related(obj)
res.update(dict( res.update(dict(
project = reverse('api:project_detail', args=(obj.project.pk,)), project = reverse('api:project_detail', args=(obj.project.pk,)),
@@ -446,17 +551,12 @@ class InventorySerializer(BaseSerializerWithVariables):
class Meta: class Meta:
model = Inventory model = Inventory
fields = BASE_FIELDS + ('organization', 'variables', fields = ('*', 'organization', 'variables', 'has_active_failures',
'has_active_failures', 'total_hosts', 'total_hosts', 'hosts_with_active_failures', 'total_groups',
'hosts_with_active_failures', 'total_groups', 'groups_with_active_failures', 'has_inventory_sources',
'groups_with_active_failures', 'total_inventory_sources', 'inventory_sources_with_failures')
'has_inventory_sources',
'total_inventory_sources',
'inventory_sources_with_failures',)
def get_related(self, obj): def get_related(self, obj):
if obj is None:
return {}
res = super(InventorySerializer, self).get_related(obj) res = super(InventorySerializer, self).get_related(obj)
res.update(dict( res.update(dict(
hosts = reverse('api:inventory_hosts_list', args=(obj.pk,)), hosts = reverse('api:inventory_hosts_list', args=(obj.pk,)),
@@ -481,19 +581,14 @@ class InventorySerializer(BaseSerializerWithVariables):
class HostSerializer(BaseSerializerWithVariables): class HostSerializer(BaseSerializerWithVariables):
# Allow the serializer to treat these fields as read-only
last_job = serializers.PrimaryKeyRelatedField(read_only=True)
last_job_host_summary = serializers.PrimaryKeyRelatedField(read_only=True)
class Meta: class Meta:
model = Host model = Host
fields = BASE_FIELDS + ('inventory', 'enabled', 'instance_id', 'variables', fields = ('*', 'inventory', 'enabled', 'instance_id', 'variables',
'has_active_failures', 'has_inventory_sources', 'has_active_failures', 'has_inventory_sources', 'last_job',
'last_job', 'last_job_host_summary') 'last_job_host_summary')
readonly_fields = ('last_job', 'last_job_host_summary')
def get_related(self, obj): def get_related(self, obj):
if obj is None:
return {}
res = super(HostSerializer, self).get_related(obj) res = super(HostSerializer, self).get_related(obj)
res.update(dict( res.update(dict(
variable_data = reverse('api:host_variable_data', args=(obj.pk,)), variable_data = reverse('api:host_variable_data', args=(obj.pk,)),
@@ -513,8 +608,6 @@ class HostSerializer(BaseSerializerWithVariables):
return res return res
def get_summary_fields(self, obj): def get_summary_fields(self, obj):
if obj is None:
return {}
d = super(HostSerializer, self).get_summary_fields(obj) d = super(HostSerializer, self).get_summary_fields(obj)
try: try:
d['last_job']['job_template_id'] = obj.last_job.job_template.id d['last_job']['job_template_id'] = obj.last_job.job_template.id
@@ -600,14 +693,11 @@ class GroupSerializer(BaseSerializerWithVariables):
class Meta: class Meta:
model = Group model = Group
fields = BASE_FIELDS + ('inventory', 'variables', 'has_active_failures', fields = ('*', 'inventory', 'variables', 'has_active_failures',
'total_hosts', 'hosts_with_active_failures', 'total_hosts', 'hosts_with_active_failures', 'total_groups',
'total_groups', 'groups_with_active_failures', 'groups_with_active_failures', 'has_inventory_sources')
'has_inventory_sources')
def get_related(self, obj): def get_related(self, obj):
if obj is None:
return {}
res = super(GroupSerializer, self).get_related(obj) res = super(GroupSerializer, self).get_related(obj)
res.update(dict( res.update(dict(
variable_data = reverse('api:group_variable_data', args=(obj.pk,)), variable_data = reverse('api:group_variable_data', args=(obj.pk,)),
@@ -645,10 +735,7 @@ class GroupTreeSerializer(GroupSerializer):
class Meta: class Meta:
model = Group model = Group
fields = BASE_FIELDS + ('inventory', 'variables', 'has_active_failures', fields = ('*', 'children')
'total_hosts', 'hosts_with_active_failures',
'total_groups', 'groups_with_active_failures',
'has_inventory_sources', 'children')
def get_children(self, obj): def get_children(self, obj):
if obj is None: if obj is None:
@@ -661,6 +748,9 @@ class GroupTreeSerializer(GroupSerializer):
class BaseVariableDataSerializer(BaseSerializer): class BaseVariableDataSerializer(BaseSerializer):
class Meta:
fields = ('variables',)
def to_native(self, obj): def to_native(self, obj):
if obj is None: if obj is None:
return {} return {}
@@ -679,78 +769,33 @@ class InventoryVariableDataSerializer(BaseVariableDataSerializer):
class Meta: class Meta:
model = Inventory model = Inventory
fields = ('variables',)
class HostVariableDataSerializer(BaseVariableDataSerializer): class HostVariableDataSerializer(BaseVariableDataSerializer):
class Meta: class Meta:
model = Host model = Host
fields = ('variables',)
class GroupVariableDataSerializer(BaseVariableDataSerializer): class GroupVariableDataSerializer(BaseVariableDataSerializer):
class Meta: class Meta:
model = Group model = Group
fields = ('variables',)
class InventorySourceSerializer(BaseSerializer): class InventorySourceOptionsSerializer(BaseSerializer):
#source_password = serializers.WritableField(required=False, default='')
last_update_failed = serializers.Field(source='last_update_failed')
last_updated = serializers.Field(source='last_updated')
class Meta: class Meta:
model = InventorySource fields = ('*', 'source', 'source_path', 'source_vars', 'credential',
fields = ('id', 'type', 'url', 'related', 'summary_fields', 'created', 'source_regions', 'overwrite', 'overwrite_vars')
'modified', 'inventory', 'group', 'source', 'source_path',
'source_vars', 'credential', 'source_regions', 'overwrite',
'overwrite_vars', 'update_on_launch', 'last_job_failed',
'status', 'last_job_run') + \
('last_update_failed', 'last_updated') # Backwards compatibility.
read_only_fields = ('inventory', 'group')
def get_related(self, obj): def get_related(self, obj):
if obj is None: res = super(InventorySourceOptionsSerializer, self).get_related(obj)
return {}
res = super(InventorySourceSerializer, self).get_related(obj)
res.update(dict(
update = reverse('api:inventory_source_update_view', args=(obj.pk,)),
inventory_updates = reverse('api:inventory_source_updates_list', args=(obj.pk,)),
activity_stream = reverse('api:inventory_activity_stream_list', args=(obj.pk,)),
#hosts = reverse('api:inventory_source_hosts_list', args=(obj.pk,)),
#groups = reverse('api:inventory_source_groups_list', args=(obj.pk,)),
))
if obj.inventory and obj.inventory.active:
res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,))
if obj.group and obj.group.active:
res['group'] = reverse('api:group_detail', args=(obj.group.pk,))
if obj.credential and obj.credential.active: if obj.credential and obj.credential.active:
res['credential'] = reverse('api:credential_detail', res['credential'] = reverse('api:credential_detail',
args=(obj.credential.pk,)) args=(obj.credential.pk,))
if obj.current_job:
res['current_job'] = reverse('api:inventory_update_detail',
args=(obj.current_job.pk,))
if obj.last_job:
res['last_job'] = reverse('api:inventory_update_detail',
args=(obj.last_job.pk,))
# Backwards compatibility.
if obj.current_update:
res['current_update'] = reverse('api:inventory_update_detail',
args=(obj.current_update.pk,))
if obj.last_update:
res['last_update'] = reverse('api:inventory_update_detail',
args=(obj.last_update.pk,))
return res return res
def get_summary_fields(self, obj):
if obj is None:
return {}
d = super(InventorySourceSerializer, self).get_summary_fields(obj)
return d
def validate_source(self, attrs, source): def validate_source(self, attrs, source):
src = attrs.get(source, '') src = attrs.get(source, '')
obj = self.object obj = self.object
@@ -777,12 +822,55 @@ class InventorySourceSerializer(BaseSerializer):
return attrs return attrs
def metadata(self): def metadata(self):
metadata = super(InventorySourceSerializer, self).metadata() metadata = super(InventorySourceOptionsSerializer, self).metadata()
field_opts = metadata.get('source_regions', {}) field_opts = metadata.get('source_regions', {})
field_opts['ec2_region_choices'] = self.opts.model.get_ec2_region_choices() field_opts['ec2_region_choices'] = self.opts.model.get_ec2_region_choices()
field_opts['rax_region_choices'] = self.opts.model.get_rax_region_choices() field_opts['rax_region_choices'] = self.opts.model.get_rax_region_choices()
return metadata return metadata
def to_native(self, obj):
ret = super(InventorySourceOptionsSerializer, self).to_native(obj)
if obj is None:
return ret
if 'credential' in ret and (not obj.credential or not obj.credential.active):
ret['credential'] = None
return ret
class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOptionsSerializer):
last_update_failed = serializers.Field(source='last_update_failed')
last_updated = serializers.Field(source='last_updated')
class Meta:
model = InventorySource
fields = ('*', 'inventory', 'group', 'update_on_launch',
'update_cache_timeout') + \
('last_update_failed', 'last_updated') # Backwards compatibility.
read_only_fields = ('*', 'name', 'inventory', 'group')
def get_related(self, obj):
res = super(InventorySourceSerializer, self).get_related(obj)
res.update(dict(
update = reverse('api:inventory_source_update_view', args=(obj.pk,)),
inventory_updates = reverse('api:inventory_source_updates_list', args=(obj.pk,)),
activity_stream = reverse('api:inventory_activity_stream_list', args=(obj.pk,)),
#hosts = reverse('api:inventory_source_hosts_list', args=(obj.pk,)),
#groups = reverse('api:inventory_source_groups_list', args=(obj.pk,)),
))
if obj.inventory and obj.inventory.active:
res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,))
if obj.group and obj.group.active:
res['group'] = reverse('api:group_detail', args=(obj.group.pk,))
# Backwards compatibility.
if obj.current_update:
res['current_update'] = reverse('api:inventory_update_detail',
args=(obj.current_update.pk,))
if obj.last_update:
res['last_update'] = reverse('api:inventory_update_detail',
args=(obj.last_update.pk,))
return res
def to_native(self, obj): def to_native(self, obj):
ret = super(InventorySourceSerializer, self).to_native(obj) ret = super(InventorySourceSerializer, self).to_native(obj)
if obj is None: if obj is None:
@@ -791,25 +879,16 @@ class InventorySourceSerializer(BaseSerializer):
ret['inventory'] = None ret['inventory'] = None
if 'group' in ret and (not obj.group or not obj.group.active): if 'group' in ret and (not obj.group or not obj.group.active):
ret['group'] = None ret['group'] = None
if 'credential' in ret and (not obj.credential or not obj.credential.active):
ret['credential'] = None
return ret return ret
class InventoryUpdateSerializer(BaseTaskSerializer): class InventoryUpdateSerializer(UnifiedJobSerializer, InventorySourceOptionsSerializer):
result_stdout = serializers.Field(source='result_stdout')
class Meta: class Meta:
model = InventoryUpdate model = InventoryUpdate
fields = ('id', 'type', 'url', 'related', 'summary_fields', 'created', fields = ('*', 'inventory_source', 'license_error')
'modified', 'inventory_source', 'status', 'failed',
'result_stdout', 'result_traceback', 'job_args', 'job_cwd',
'job_env', 'license_error')
def get_related(self, obj): def get_related(self, obj):
if obj is None:
return {}
res = super(InventoryUpdateSerializer, self).get_related(obj) res = super(InventoryUpdateSerializer, self).get_related(obj)
res.update(dict( res.update(dict(
inventory_source = reverse('api:inventory_source_detail', args=(obj.inventory_source.pk,)), inventory_source = reverse('api:inventory_source_detail', args=(obj.inventory_source.pk,)),
@@ -822,11 +901,9 @@ class TeamSerializer(BaseSerializer):
class Meta: class Meta:
model = Team model = Team
fields = BASE_FIELDS + ('organization',) fields = ('*', 'organization')
def get_related(self, obj): def get_related(self, obj):
if obj is None:
return {}
res = super(TeamSerializer, self).get_related(obj) res = super(TeamSerializer, self).get_related(obj)
res.update(dict( res.update(dict(
projects = reverse('api:team_projects_list', args=(obj.pk,)), projects = reverse('api:team_projects_list', args=(obj.pk,)),
@@ -835,7 +912,7 @@ class TeamSerializer(BaseSerializer):
permissions = reverse('api:team_permissions_list', args=(obj.pk,)), permissions = reverse('api:team_permissions_list', args=(obj.pk,)),
activity_stream = reverse('api:team_activity_stream_list', args=(obj.pk,)), activity_stream = reverse('api:team_activity_stream_list', args=(obj.pk,)),
)) ))
if obj.organization: if obj.organization and obj.organization.active:
res['organization'] = reverse('api:organization_detail', args=(obj.organization.pk,)) res['organization'] = reverse('api:organization_detail', args=(obj.organization.pk,))
return res return res
@@ -850,20 +927,18 @@ class PermissionSerializer(BaseSerializer):
class Meta: class Meta:
model = Permission model = Permission
fields = BASE_FIELDS + ('user', 'team', 'project', 'inventory', fields = ('*', 'user', 'team', 'project', 'inventory',
'permission_type',) 'permission_type')
def get_related(self, obj): def get_related(self, obj):
if obj is None:
return {}
res = super(PermissionSerializer, self).get_related(obj) res = super(PermissionSerializer, self).get_related(obj)
if obj.user: if obj.user and obj.user.is_active:
res['user'] = reverse('api:user_detail', args=(obj.user.pk,)) res['user'] = reverse('api:user_detail', args=(obj.user.pk,))
if obj.team: if obj.team and obj.team.active:
res['team'] = reverse('api:team_detail', args=(obj.team.pk,)) res['team'] = reverse('api:team_detail', args=(obj.team.pk,))
if obj.project: if obj.project and obj.project.active:
res['project'] = reverse('api:project_detail', args=(obj.project.pk,)) res['project'] = reverse('api:project_detail', args=(obj.project.pk,))
if obj.inventory: if obj.inventory and obj.inventory.active:
res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,)) res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,))
return res return res
@@ -908,9 +983,9 @@ class CredentialSerializer(BaseSerializer):
class Meta: class Meta:
model = Credential model = Credential
fields = BASE_FIELDS + ('user', 'team', 'kind', 'cloud', 'username', fields = ('*', 'user', 'team', 'kind', 'cloud', 'username',
'password', 'ssh_key_data', 'ssh_key_unlock', 'password', 'ssh_key_data', 'ssh_key_unlock',
'sudo_username', 'sudo_password',) 'sudo_username', 'sudo_password')
def to_native(self, obj): def to_native(self, obj):
ret = super(CredentialSerializer, self).to_native(obj) ret = super(CredentialSerializer, self).to_native(obj)
@@ -933,8 +1008,6 @@ class CredentialSerializer(BaseSerializer):
return instance return instance
def get_related(self, obj): def get_related(self, obj):
if obj is None:
return {}
res = super(CredentialSerializer, self).get_related(obj) res = super(CredentialSerializer, self).get_related(obj)
res.update(dict( res.update(dict(
activity_stream = reverse('api:credential_activity_stream_list', args=(obj.pk,)) activity_stream = reverse('api:credential_activity_stream_list', args=(obj.pk,))
@@ -946,25 +1019,15 @@ class CredentialSerializer(BaseSerializer):
return res return res
class JobTemplateSerializer(BaseSerializer): class JobOptionsSerializer(BaseSerializer):
class Meta: class Meta:
model = JobTemplate fields = ('*', 'job_type', 'inventory', 'project', 'playbook',
fields = BASE_FIELDS + ('job_type', 'inventory', 'project', 'playbook', 'credential', 'cloud_credential', 'forks', 'limit',
'credential', 'cloud_credential', 'forks', 'verbosity', 'extra_vars', 'job_tags')
'limit', 'verbosity', 'extra_vars', 'job_tags',
'host_config_key')
def get_related(self, obj): def get_related(self, obj):
if obj is None: res = super(JobOptionsSerializer, self).get_related(obj)
return {}
res = super(JobTemplateSerializer, self).get_related(obj)
res.update(dict(
jobs = reverse('api:job_template_jobs_list', args=(obj.pk,)),
activity_stream = reverse('api:job_template_activity_stream_list', args=(obj.pk,)),
))
if obj is None:
return ret
if obj.inventory and obj.inventory.active: if obj.inventory and obj.inventory.active:
res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,)) res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,))
if obj.project and obj.project.active: if obj.project and obj.project.active:
@@ -974,12 +1037,10 @@ class JobTemplateSerializer(BaseSerializer):
if obj.cloud_credential and obj.cloud_credential.active: if obj.cloud_credential and obj.cloud_credential.active:
res['cloud_credential'] = reverse('api:credential_detail', res['cloud_credential'] = reverse('api:credential_detail',
args=(obj.cloud_credential.pk,)) args=(obj.cloud_credential.pk,))
if obj.host_config_key:
res['callback'] = reverse('api:job_template_callback', args=(obj.pk,))
return res return res
def to_native(self, obj): def to_native(self, obj):
ret = super(JobTemplateSerializer, self).to_native(obj) ret = super(JobOptionsSerializer, self).to_native(obj)
if obj is None: if obj is None:
return ret return ret
if 'inventory' in ret and (not obj.inventory or not obj.inventory.active): if 'inventory' in ret and (not obj.inventory or not obj.inventory.active):
@@ -1002,24 +1063,32 @@ class JobTemplateSerializer(BaseSerializer):
return attrs return attrs
class JobSerializer(BaseTaskSerializer): class JobTemplateSerializer(UnifiedJobTemplateSerializer, JobOptionsSerializer):
class Meta:
model = JobTemplate
fields = ('*', 'host_config_key')
def get_related(self, obj):
res = super(JobTemplateSerializer, self).get_related(obj)
res.update(dict(
jobs = reverse('api:job_template_jobs_list', args=(obj.pk,)),
activity_stream = reverse('api:job_template_activity_stream_list', args=(obj.pk,)),
))
if obj.host_config_key:
res['callback'] = reverse('api:job_template_callback', args=(obj.pk,))
return res
class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
passwords_needed_to_start = serializers.Field(source='passwords_needed_to_start') passwords_needed_to_start = serializers.Field(source='passwords_needed_to_start')
result_stdout = serializers.Field(source='result_stdout')
class Meta: class Meta:
model = Job model = Job
fields = ('id', 'type', 'url', 'related', 'summary_fields', 'created', fields = ('*', 'job_template', 'passwords_needed_to_start')
'modified', 'job_template', 'job_type', 'inventory',
'project', 'playbook', 'credential', 'cloud_credential',
'forks', 'limit', 'verbosity', 'extra_vars',
'job_tags', 'launch_type', 'status', 'failed', 'result_stdout',
'result_traceback', 'passwords_needed_to_start', 'job_args',
'job_cwd', 'job_env')
def get_related(self, obj): def get_related(self, obj):
if obj is None:
return {}
res = super(JobSerializer, self).get_related(obj) res = super(JobSerializer, self).get_related(obj)
res.update(dict( res.update(dict(
job_events = reverse('api:job_job_events_list', args=(obj.pk,)), job_events = reverse('api:job_job_events_list', args=(obj.pk,)),
@@ -1029,18 +1098,6 @@ class JobSerializer(BaseTaskSerializer):
if obj.job_template and obj.job_template.active: if obj.job_template and obj.job_template.active:
res['job_template'] = reverse('api:job_template_detail', res['job_template'] = reverse('api:job_template_detail',
args=(obj.job_template.pk,)) args=(obj.job_template.pk,))
if obj.inventory and obj.inventory.active:
res['inventory'] = reverse('api:inventory_detail',
args=(obj.inventory.pk,))
if obj.project and obj.project.active:
res['project'] = reverse('api:project_detail',
args=(obj.project.pk,))
if obj.credential and obj.credential.active:
res['credential'] = reverse('api:credential_detail',
args=(obj.credential.pk,))
if obj.cloud_credential and obj.cloud_credential.active:
res['cloud_credential'] = reverse('api:credential_detail',
args=(obj.cloud_credential.pk,))
if obj.can_start or True: if obj.can_start or True:
res['start'] = reverse('api:job_start', args=(obj.pk,)) res['start'] = reverse('api:job_start', args=(obj.pk,))
if obj.can_cancel or True: if obj.can_cancel or True:
@@ -1080,16 +1137,6 @@ class JobSerializer(BaseTaskSerializer):
return ret return ret
if 'job_template' in ret and (not obj.job_template or not obj.job_template.active): if 'job_template' in ret and (not obj.job_template or not obj.job_template.active):
ret['job_template'] = None ret['job_template'] = None
if 'inventory' in ret and (not obj.inventory or not obj.inventory.active):
ret['inventory'] = None
if 'project' in ret and (not obj.project or not obj.project.active):
ret['project'] = None
if 'playbook' in ret:
ret['playbook'] = ''
if 'credential' in ret and (not obj.credential or not obj.credential.active):
ret['credential'] = None
if 'cloud_credential' in ret and (not obj.cloud_credential or not obj.cloud_credential.active):
ret['cloud_credential'] = None
return ret return ret
@@ -1097,26 +1144,17 @@ class JobListSerializer(JobSerializer):
class Meta: class Meta:
model = Job model = Job
fields = ('id', 'type', 'url', 'related', 'summary_fields', 'created', fields = ('*', '-result_stdout')
'modified', 'job_template', 'job_type', 'inventory',
'project', 'playbook', 'credential', 'cloud_credential',
'forks', 'limit', 'verbosity', 'extra_vars',
'job_tags', 'launch_type', 'status', 'failed',
'result_traceback', 'passwords_needed_to_start', 'job_args',
'job_cwd', 'job_env')
class JobHostSummarySerializer(BaseSerializer): class JobHostSummarySerializer(BaseSerializer):
class Meta: class Meta:
model = JobHostSummary model = JobHostSummary
fields = ('id', 'type', 'url', 'job', 'host', 'created', 'modified', fields = ('*', '-name', '-description', 'job', 'host', 'changed',
'summary_fields', 'related', 'changed', 'dark', 'failures', 'dark', 'failures', 'ok', 'processed', 'skipped', 'failed')
'ok', 'processed', 'skipped', 'failed')
def get_related(self, obj): def get_related(self, obj):
if obj is None:
return {}
res = super(JobHostSummarySerializer, self).get_related(obj) res = super(JobHostSummarySerializer, self).get_related(obj)
res.update(dict( res.update(dict(
job=reverse('api:job_detail', args=(obj.job.pk,)), job=reverse('api:job_detail', args=(obj.job.pk,)),
@@ -1125,8 +1163,6 @@ class JobHostSummarySerializer(BaseSerializer):
return res return res
def get_summary_fields(self, obj): def get_summary_fields(self, obj):
if obj is None:
return {}
d = super(JobHostSummarySerializer, self).get_summary_fields(obj) d = super(JobHostSummarySerializer, self).get_summary_fields(obj)
try: try:
d['job']['job_template_id'] = obj.job.job_template.id d['job']['job_template_id'] = obj.job.job_template.id
@@ -1143,14 +1179,11 @@ class JobEventSerializer(BaseSerializer):
class Meta: class Meta:
model = JobEvent model = JobEvent
fields = ('id', 'type', 'url', 'created', 'modified', 'job', 'event', fields = ('*', '-name', '-description', 'job', 'event',
'event_display', 'event_data', 'event_level', 'failed', 'event_display', 'event_data', 'event_level', 'failed',
'changed', 'host', 'related', 'summary_fields', 'parent', 'changed', 'host', 'parent', 'play', 'task')
'play', 'task')
def get_related(self, obj): def get_related(self, obj):
if obj is None:
return {}
res = super(JobEventSerializer, self).get_related(obj) res = super(JobEventSerializer, self).get_related(obj)
res.update(dict( res.update(dict(
job = reverse('api:job_detail', args=(obj.job.pk,)), job = reverse('api:job_detail', args=(obj.job.pk,)),
@@ -1167,8 +1200,6 @@ class JobEventSerializer(BaseSerializer):
return res return res
def get_summary_fields(self, obj): def get_summary_fields(self, obj):
if obj is None:
return {}
d = super(JobEventSerializer, self).get_summary_fields(obj) d = super(JobEventSerializer, self).get_summary_fields(obj)
try: try:
d['job']['job_template_id'] = obj.job.job_template.id d['job']['job_template_id'] = obj.job.job_template.id
@@ -1177,13 +1208,31 @@ class JobEventSerializer(BaseSerializer):
pass pass
return d return d
class ScheduleSerializer(BaseSerializer):
class Meta:
model = Schedule
fields = ('*', 'unified_job_template', 'enabled', 'dtstart', 'dtend',
'rrule', 'next_run')
def get_related(self, obj):
res = super(ScheduleSerializer, self).get_related(obj)
res.update(dict(
#unified_jobs = reverse('api:schedule_unified_jobs_list', args=(obj.pk,)),
))
if obj.unified_job_template and obj.unified_job_template.active:
res['unified_job_template'] = obj.unified_job_template.get_absolute_url()
return res
class ActivityStreamSerializer(BaseSerializer): class ActivityStreamSerializer(BaseSerializer):
changes = serializers.SerializerMethodField('get_changes') changes = serializers.SerializerMethodField('get_changes')
class Meta: class Meta:
model = ActivityStream model = ActivityStream
fields = ('id', 'type', 'url', 'related', 'summary_fields', fields = ('*', '-name', '-description', '-created', '-modified',
'timestamp', 'operation', 'changes', 'object1', 'object2') 'timestamp', 'operation', 'changes', 'object1', 'object2')
def get_fields(self): def get_fields(self):
@@ -1197,10 +1246,8 @@ class ActivityStreamSerializer(BaseSerializer):
field.help_text = 'Unpopulated for create, update, and delete events. For associate and disassociate events this is the object type that object1 is being associated with' field.help_text = 'Unpopulated for create, update, and delete events. For associate and disassociate events this is the object type that object1 is being associated with'
if key == 'operation': if key == 'operation':
field.help_text = 'The action taken with respect to the given object(s).' field.help_text = 'The action taken with respect to the given object(s).'
return ret return ret
def get_changes(self, obj): def get_changes(self, obj):
if obj is None: if obj is None:
return {} return {}
@@ -1214,8 +1261,6 @@ class ActivityStreamSerializer(BaseSerializer):
return {} return {}
def get_related(self, obj): def get_related(self, obj):
if obj is None:
return {}
rel = {} rel = {}
if obj.actor is not None: if obj.actor is not None:
rel['actor'] = reverse('api:user_detail', args=(obj.actor.pk,)) rel['actor'] = reverse('api:user_detail', args=(obj.actor.pk,))
@@ -1267,6 +1312,7 @@ class ActivityStreamSerializer(BaseSerializer):
last_name = obj.actor.last_name) last_name = obj.actor.last_name)
return summary_fields return summary_fields
class AuthTokenSerializer(serializers.Serializer): class AuthTokenSerializer(serializers.Serializer):
username = serializers.CharField() username = serializers.CharField()

View File

@@ -631,8 +631,8 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions):
@classmethod @classmethod
def _get_unified_job_field_names(cls): def _get_unified_job_field_names(cls):
return ['source', 'source_path', 'source_vars', 'credential', return ['name', 'description', 'source', 'source_path', 'source_vars',
'source_regions', 'overwrite', 'overwrite_vars'] 'credential', 'source_regions', 'overwrite', 'overwrite_vars']
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
# If update_fields has been specified, add our field names to it, # If update_fields has been specified, add our field names to it,

View File

@@ -151,9 +151,9 @@ class JobTemplate(UnifiedJobTemplate, JobOptions):
@classmethod @classmethod
def _get_unified_job_field_names(cls): def _get_unified_job_field_names(cls):
return ['job_type', 'inventory', 'project', 'playbook', 'credential', return ['name', 'description', 'job_type', 'inventory', 'project',
'cloud_credential', 'forks', 'limit', 'verbosity', 'playbook', 'credential', 'cloud_credential', 'forks',
'extra_vars', 'job_tags'] 'limit', 'verbosity', 'extra_vars', 'job_tags']
def create_job(self, **kwargs): def create_job(self, **kwargs):
''' '''

View File

@@ -219,8 +219,9 @@ class Project(UnifiedJobTemplate, ProjectOptions):
@classmethod @classmethod
def _get_unified_job_field_names(cls): def _get_unified_job_field_names(cls):
return ['local_path', 'scm_type', 'scm_url', 'scm_branch', return ['name', 'description', 'local_path', 'scm_type', 'scm_url',
'scm_clean', 'scm_delete_on_update', 'credential'] 'scm_branch', 'scm_clean', 'scm_delete_on_update',
'credential']
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
new_instance = not bool(self.pk) new_instance = not bool(self.pk)

View File

@@ -185,10 +185,16 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique):
@classmethod @classmethod
def _get_unified_job_class(cls): def _get_unified_job_class(cls):
'''
Return subclass of UnifiedJob that is created from this template.
'''
raise NotImplementedError # Implement in subclass. raise NotImplementedError # Implement in subclass.
@classmethod @classmethod
def _get_unified_job_field_names(cls): def _get_unified_job_field_names(cls):
'''
Return field names that should be copied from template to new job.
'''
raise NotImplementedError # Implement in subclass. raise NotImplementedError # Implement in subclass.
def _create_unified_job_instance(self, **kwargs): def _create_unified_job_instance(self, **kwargs):

View File

@@ -460,7 +460,9 @@ class JobTemplateTest(BaseJobTestMixin, django.test.TestCase):
'modified', 'name', 'description', 'job_type', 'modified', 'name', 'description', 'job_type',
'inventory', 'project', 'playbook', 'credential', 'inventory', 'project', 'playbook', 'credential',
'cloud_credential', 'forks', 'limit', 'verbosity', 'cloud_credential', 'forks', 'limit', 'verbosity',
'extra_vars', 'job_tags', 'host_config_key',) 'extra_vars', 'job_tags', 'host_config_key',
'status', 'next_job_run', 'has_schedules',
'last_job_run', 'last_job_failed')
def test_get_job_template_list(self): def test_get_job_template_list(self):
url = reverse('api:job_template_list') url = reverse('api:job_template_list')