diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 3a20effbe7..4e8256356b 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -33,9 +33,6 @@ from awx.main.utils import update_scm_url, camelcase_to_underscore 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. DEFAULT_SUMMARY_FIELDS = ('name', 'description',) @@ -65,13 +62,15 @@ SUMMARIZABLE_FK_FIELDS = { 'has_inventory_sources'), 'project': DEFAULT_SUMMARY_FIELDS + ('status',), 'credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud'), + 'cloud_credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud'), 'permission': DEFAULT_SUMMARY_FIELDS, 'job': DEFAULT_SUMMARY_FIELDS + ('status', 'failed',), '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_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'), } @@ -98,13 +97,87 @@ class ChoiceField(fields.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): + __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 type = serializers.SerializerMethodField('get_type') url = serializers.SerializerMethodField('get_url') - related = serializers.SerializerMethodField('get_related') - summary_fields = serializers.SerializerMethodField('get_summary_fields') + related = serializers.SerializerMethodField('_get_related') + summary_fields = serializers.SerializerMethodField('_get_summary_fields') # make certain fields read only created = serializers.SerializerMethodField('get_created') @@ -149,6 +222,9 @@ class BaseSerializer(serializers.ModelSerializer): else: 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): res = SortedDict() 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,)) 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): # Return values for certain fields on related objects, to simplify # displaying lists of items without additional API requests. @@ -166,6 +245,8 @@ class BaseSerializer(serializers.ModelSerializer): fkval = getattr(obj, fk, None) if fkval is None: continue + if fkval == obj: + continue if hasattr(fkval, 'active') and not fkval.active: continue if hasattr(fkval, 'is_active') and not fkval.is_active: @@ -205,6 +286,43 @@ class BaseSerializer(serializers.ModelSerializer): 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): job_env = serializers.SerializerMethodField('get_job_env') @@ -224,9 +342,9 @@ class UserSerializer(BaseSerializer): class Meta: model = User - fields = ('id', 'type', 'url', 'related', 'created', 'username', - 'first_name', 'last_name', 'email', 'is_superuser', - 'password', 'ldap_dn') + fields = ('*', '-name', '-description', '-modified', + '-summary_fields', 'username', 'first_name', 'last_name', + 'email', 'is_superuser', 'password', 'ldap_dn') def to_native(self, obj): ret = super(UserSerializer, self).to_native(obj) @@ -261,8 +379,6 @@ class UserSerializer(BaseSerializer): return super(UserSerializer, self).save_object(obj, **kwargs) def get_related(self, obj): - if obj is None: - return {} res = super(UserSerializer, self).get_related(obj) res.update(dict( teams = reverse('api:user_teams_list', args=(obj.pk,)), @@ -309,69 +425,32 @@ class OrganizationSerializer(BaseSerializer): class Meta: model = Organization - fields = BASE_FIELDS + fields = ('*',) def get_related(self, obj): - if obj is None: - return {} res = super(OrganizationSerializer, self).get_related(obj) res.update(dict( - #audit_trail = reverse('api:organization_audit_trail_list', args=(obj.pk,)), projects = reverse('api:organization_projects_list', args=(obj.pk,)), inventories = reverse('api:organization_inventories_list', args=(obj.pk,)), users = reverse('api:organization_users_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,)), activity_stream = reverse('api:organization_activity_stream_list', args=(obj.pk,)) )) return res -class ProjectSerializer(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 ProjectOptionsSerializer(BaseSerializer): + class Meta: - model = Project - fields = BASE_FIELDS + ('local_path', 'scm_type', 'scm_url', - '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 + fields = ('*', 'local_path', 'scm_type', 'scm_url', 'scm_branch', + 'scm_clean', 'scm_delete_on_update', 'credential') def get_related(self, obj): - if obj is None: - 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,)), - )) + res = super(ProjectOptionsSerializer, self).get_related(obj) if obj.credential and obj.credential.active: res['credential'] = reverse('api:credential_detail', 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 def validate_local_path(self, attrs, source): @@ -391,12 +470,44 @@ class ProjectSerializer(BaseSerializer): return attrs 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): ret['credential'] = None 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 Meta: @@ -408,19 +519,13 @@ class ProjectPlaybooksSerializer(ProjectSerializer): return ret.get('playbooks', []) -class ProjectUpdateSerializer(BaseTaskSerializer): - - result_stdout = serializers.Field(source='result_stdout') +class ProjectUpdateSerializer(UnifiedJobSerializer, ProjectOptionsSerializer): class Meta: model = ProjectUpdate - fields = ('id', 'type', 'url', 'related', 'summary_fields', 'created', - 'modified', 'project', 'status', 'failed', 'result_stdout', - 'result_traceback', 'job_args', 'job_cwd', 'job_env') + fields = ('*', 'project') def get_related(self, obj): - if obj is None: - return {} res = super(ProjectUpdateSerializer, self).get_related(obj) res.update(dict( project = reverse('api:project_detail', args=(obj.project.pk,)), @@ -446,17 +551,12 @@ class InventorySerializer(BaseSerializerWithVariables): class Meta: model = Inventory - fields = BASE_FIELDS + ('organization', 'variables', - 'has_active_failures', 'total_hosts', - 'hosts_with_active_failures', 'total_groups', - 'groups_with_active_failures', - 'has_inventory_sources', - 'total_inventory_sources', - 'inventory_sources_with_failures',) + fields = ('*', 'organization', 'variables', 'has_active_failures', + 'total_hosts', 'hosts_with_active_failures', 'total_groups', + 'groups_with_active_failures', 'has_inventory_sources', + 'total_inventory_sources', 'inventory_sources_with_failures') def get_related(self, obj): - if obj is None: - return {} res = super(InventorySerializer, self).get_related(obj) res.update(dict( hosts = reverse('api:inventory_hosts_list', args=(obj.pk,)), @@ -481,19 +581,14 @@ class InventorySerializer(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: model = Host - fields = BASE_FIELDS + ('inventory', 'enabled', 'instance_id', 'variables', - 'has_active_failures', 'has_inventory_sources', - 'last_job', 'last_job_host_summary') + fields = ('*', 'inventory', 'enabled', 'instance_id', 'variables', + 'has_active_failures', 'has_inventory_sources', 'last_job', + 'last_job_host_summary') + readonly_fields = ('last_job', 'last_job_host_summary') def get_related(self, obj): - if obj is None: - return {} res = super(HostSerializer, self).get_related(obj) res.update(dict( variable_data = reverse('api:host_variable_data', args=(obj.pk,)), @@ -513,8 +608,6 @@ class HostSerializer(BaseSerializerWithVariables): return res def get_summary_fields(self, obj): - if obj is None: - return {} d = super(HostSerializer, self).get_summary_fields(obj) try: d['last_job']['job_template_id'] = obj.last_job.job_template.id @@ -600,14 +693,11 @@ class GroupSerializer(BaseSerializerWithVariables): class Meta: model = Group - fields = BASE_FIELDS + ('inventory', 'variables', 'has_active_failures', - 'total_hosts', 'hosts_with_active_failures', - 'total_groups', 'groups_with_active_failures', - 'has_inventory_sources') + fields = ('*', 'inventory', 'variables', 'has_active_failures', + 'total_hosts', 'hosts_with_active_failures', 'total_groups', + 'groups_with_active_failures', 'has_inventory_sources') def get_related(self, obj): - if obj is None: - return {} res = super(GroupSerializer, self).get_related(obj) res.update(dict( variable_data = reverse('api:group_variable_data', args=(obj.pk,)), @@ -645,10 +735,7 @@ class GroupTreeSerializer(GroupSerializer): class Meta: model = Group - fields = BASE_FIELDS + ('inventory', 'variables', 'has_active_failures', - 'total_hosts', 'hosts_with_active_failures', - 'total_groups', 'groups_with_active_failures', - 'has_inventory_sources', 'children') + fields = ('*', 'children') def get_children(self, obj): if obj is None: @@ -661,6 +748,9 @@ class GroupTreeSerializer(GroupSerializer): class BaseVariableDataSerializer(BaseSerializer): + class Meta: + fields = ('variables',) + def to_native(self, obj): if obj is None: return {} @@ -679,78 +769,33 @@ class InventoryVariableDataSerializer(BaseVariableDataSerializer): class Meta: model = Inventory - fields = ('variables',) class HostVariableDataSerializer(BaseVariableDataSerializer): class Meta: model = Host - fields = ('variables',) class GroupVariableDataSerializer(BaseVariableDataSerializer): class Meta: 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: - model = InventorySource - fields = ('id', 'type', 'url', 'related', 'summary_fields', 'created', - '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') + fields = ('*', 'source', 'source_path', 'source_vars', 'credential', + 'source_regions', 'overwrite', 'overwrite_vars') def get_related(self, obj): - if obj is None: - 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,)) + res = super(InventorySourceOptionsSerializer, self).get_related(obj) if obj.credential and obj.credential.active: res['credential'] = reverse('api:credential_detail', 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 - 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): src = attrs.get(source, '') obj = self.object @@ -777,12 +822,55 @@ class InventorySourceSerializer(BaseSerializer): return attrs def metadata(self): - metadata = super(InventorySourceSerializer, self).metadata() + metadata = super(InventorySourceOptionsSerializer, self).metadata() field_opts = metadata.get('source_regions', {}) field_opts['ec2_region_choices'] = self.opts.model.get_ec2_region_choices() field_opts['rax_region_choices'] = self.opts.model.get_rax_region_choices() 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): ret = super(InventorySourceSerializer, self).to_native(obj) if obj is None: @@ -791,25 +879,16 @@ class InventorySourceSerializer(BaseSerializer): ret['inventory'] = None if 'group' in ret and (not obj.group or not obj.group.active): ret['group'] = None - if 'credential' in ret and (not obj.credential or not obj.credential.active): - ret['credential'] = None return ret -class InventoryUpdateSerializer(BaseTaskSerializer): - - result_stdout = serializers.Field(source='result_stdout') +class InventoryUpdateSerializer(UnifiedJobSerializer, InventorySourceOptionsSerializer): class Meta: model = InventoryUpdate - fields = ('id', 'type', 'url', 'related', 'summary_fields', 'created', - 'modified', 'inventory_source', 'status', 'failed', - 'result_stdout', 'result_traceback', 'job_args', 'job_cwd', - 'job_env', 'license_error') + fields = ('*', 'inventory_source', 'license_error') def get_related(self, obj): - if obj is None: - return {} res = super(InventoryUpdateSerializer, self).get_related(obj) res.update(dict( inventory_source = reverse('api:inventory_source_detail', args=(obj.inventory_source.pk,)), @@ -822,11 +901,9 @@ class TeamSerializer(BaseSerializer): class Meta: model = Team - fields = BASE_FIELDS + ('organization',) + fields = ('*', 'organization') def get_related(self, obj): - if obj is None: - return {} res = super(TeamSerializer, self).get_related(obj) res.update(dict( 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,)), 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,)) return res @@ -850,20 +927,18 @@ class PermissionSerializer(BaseSerializer): class Meta: model = Permission - fields = BASE_FIELDS + ('user', 'team', 'project', 'inventory', - 'permission_type',) + fields = ('*', 'user', 'team', 'project', 'inventory', + 'permission_type') def get_related(self, obj): - if obj is None: - return {} 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,)) - if obj.team: + if obj.team and obj.team.active: 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,)) - if obj.inventory: + if obj.inventory and obj.inventory.active: res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,)) return res @@ -908,9 +983,9 @@ class CredentialSerializer(BaseSerializer): class Meta: model = Credential - fields = BASE_FIELDS + ('user', 'team', 'kind', 'cloud', 'username', - 'password', 'ssh_key_data', 'ssh_key_unlock', - 'sudo_username', 'sudo_password',) + fields = ('*', 'user', 'team', 'kind', 'cloud', 'username', + 'password', 'ssh_key_data', 'ssh_key_unlock', + 'sudo_username', 'sudo_password') def to_native(self, obj): ret = super(CredentialSerializer, self).to_native(obj) @@ -933,8 +1008,6 @@ class CredentialSerializer(BaseSerializer): return instance def get_related(self, obj): - if obj is None: - return {} res = super(CredentialSerializer, self).get_related(obj) res.update(dict( activity_stream = reverse('api:credential_activity_stream_list', args=(obj.pk,)) @@ -946,25 +1019,15 @@ class CredentialSerializer(BaseSerializer): return res -class JobTemplateSerializer(BaseSerializer): +class JobOptionsSerializer(BaseSerializer): class Meta: - model = JobTemplate - fields = BASE_FIELDS + ('job_type', 'inventory', 'project', 'playbook', - 'credential', 'cloud_credential', 'forks', - 'limit', 'verbosity', 'extra_vars', 'job_tags', - 'host_config_key') + fields = ('*', 'job_type', 'inventory', 'project', 'playbook', + 'credential', 'cloud_credential', 'forks', 'limit', + 'verbosity', 'extra_vars', 'job_tags') def get_related(self, obj): - if obj is None: - 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 + res = super(JobOptionsSerializer, self).get_related(obj) if obj.inventory and obj.inventory.active: res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,)) if obj.project and obj.project.active: @@ -974,12 +1037,10 @@ class JobTemplateSerializer(BaseSerializer): if obj.cloud_credential and obj.cloud_credential.active: res['cloud_credential'] = reverse('api:credential_detail', args=(obj.cloud_credential.pk,)) - if obj.host_config_key: - res['callback'] = reverse('api:job_template_callback', args=(obj.pk,)) return res def to_native(self, obj): - ret = super(JobTemplateSerializer, self).to_native(obj) + ret = super(JobOptionsSerializer, self).to_native(obj) if obj is None: return ret if 'inventory' in ret and (not obj.inventory or not obj.inventory.active): @@ -1002,24 +1063,32 @@ class JobTemplateSerializer(BaseSerializer): 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') - result_stdout = serializers.Field(source='result_stdout') class Meta: model = Job - fields = ('id', 'type', 'url', 'related', 'summary_fields', 'created', - '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') + fields = ('*', 'job_template', 'passwords_needed_to_start') def get_related(self, obj): - if obj is None: - return {} res = super(JobSerializer, self).get_related(obj) res.update(dict( 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: res['job_template'] = reverse('api:job_template_detail', 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: res['start'] = reverse('api:job_start', args=(obj.pk,)) if obj.can_cancel or True: @@ -1080,16 +1137,6 @@ class JobSerializer(BaseTaskSerializer): return ret if 'job_template' in ret and (not obj.job_template or not obj.job_template.active): 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 @@ -1097,26 +1144,17 @@ class JobListSerializer(JobSerializer): class Meta: model = Job - fields = ('id', 'type', 'url', 'related', 'summary_fields', 'created', - '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') + fields = ('*', '-result_stdout') class JobHostSummarySerializer(BaseSerializer): class Meta: model = JobHostSummary - fields = ('id', 'type', 'url', 'job', 'host', 'created', 'modified', - 'summary_fields', 'related', 'changed', 'dark', 'failures', - 'ok', 'processed', 'skipped', 'failed') + fields = ('*', '-name', '-description', 'job', 'host', 'changed', + 'dark', 'failures', 'ok', 'processed', 'skipped', 'failed') def get_related(self, obj): - if obj is None: - return {} res = super(JobHostSummarySerializer, self).get_related(obj) res.update(dict( job=reverse('api:job_detail', args=(obj.job.pk,)), @@ -1125,8 +1163,6 @@ class JobHostSummarySerializer(BaseSerializer): return res def get_summary_fields(self, obj): - if obj is None: - return {} d = super(JobHostSummarySerializer, self).get_summary_fields(obj) try: d['job']['job_template_id'] = obj.job.job_template.id @@ -1143,14 +1179,11 @@ class JobEventSerializer(BaseSerializer): class Meta: model = JobEvent - fields = ('id', 'type', 'url', 'created', 'modified', 'job', 'event', + fields = ('*', '-name', '-description', 'job', 'event', 'event_display', 'event_data', 'event_level', 'failed', - 'changed', 'host', 'related', 'summary_fields', 'parent', - 'play', 'task') + 'changed', 'host', 'parent', 'play', 'task') def get_related(self, obj): - if obj is None: - return {} res = super(JobEventSerializer, self).get_related(obj) res.update(dict( job = reverse('api:job_detail', args=(obj.job.pk,)), @@ -1167,8 +1200,6 @@ class JobEventSerializer(BaseSerializer): return res def get_summary_fields(self, obj): - if obj is None: - return {} d = super(JobEventSerializer, self).get_summary_fields(obj) try: d['job']['job_template_id'] = obj.job.job_template.id @@ -1177,13 +1208,31 @@ class JobEventSerializer(BaseSerializer): pass 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): changes = serializers.SerializerMethodField('get_changes') class Meta: model = ActivityStream - fields = ('id', 'type', 'url', 'related', 'summary_fields', + fields = ('*', '-name', '-description', '-created', '-modified', 'timestamp', 'operation', 'changes', 'object1', 'object2') 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' if key == 'operation': field.help_text = 'The action taken with respect to the given object(s).' - return ret - def get_changes(self, obj): if obj is None: return {} @@ -1214,8 +1261,6 @@ class ActivityStreamSerializer(BaseSerializer): return {} def get_related(self, obj): - if obj is None: - return {} rel = {} if obj.actor is not None: rel['actor'] = reverse('api:user_detail', args=(obj.actor.pk,)) @@ -1267,6 +1312,7 @@ class ActivityStreamSerializer(BaseSerializer): last_name = obj.actor.last_name) return summary_fields + class AuthTokenSerializer(serializers.Serializer): username = serializers.CharField() diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 73b9df814d..391b34b45b 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -631,8 +631,8 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions): @classmethod def _get_unified_job_field_names(cls): - return ['source', 'source_path', 'source_vars', 'credential', - 'source_regions', 'overwrite', 'overwrite_vars'] + return ['name', 'description', 'source', 'source_path', 'source_vars', + 'credential', 'source_regions', 'overwrite', 'overwrite_vars'] def save(self, *args, **kwargs): # If update_fields has been specified, add our field names to it, diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 067ea2e759..26214f9752 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -151,9 +151,9 @@ class JobTemplate(UnifiedJobTemplate, JobOptions): @classmethod def _get_unified_job_field_names(cls): - return ['job_type', 'inventory', 'project', 'playbook', 'credential', - 'cloud_credential', 'forks', 'limit', 'verbosity', - 'extra_vars', 'job_tags'] + return ['name', 'description', 'job_type', 'inventory', 'project', + 'playbook', 'credential', 'cloud_credential', 'forks', + 'limit', 'verbosity', 'extra_vars', 'job_tags'] def create_job(self, **kwargs): ''' diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index 30464e29a1..b46beb6d27 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -219,8 +219,9 @@ class Project(UnifiedJobTemplate, ProjectOptions): @classmethod def _get_unified_job_field_names(cls): - return ['local_path', 'scm_type', 'scm_url', 'scm_branch', - 'scm_clean', 'scm_delete_on_update', 'credential'] + return ['name', 'description', 'local_path', 'scm_type', 'scm_url', + 'scm_branch', 'scm_clean', 'scm_delete_on_update', + 'credential'] def save(self, *args, **kwargs): new_instance = not bool(self.pk) diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index a78603421e..7cc6fb1259 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -185,10 +185,16 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique): @classmethod def _get_unified_job_class(cls): + ''' + Return subclass of UnifiedJob that is created from this template. + ''' raise NotImplementedError # Implement in subclass. @classmethod def _get_unified_job_field_names(cls): + ''' + Return field names that should be copied from template to new job. + ''' raise NotImplementedError # Implement in subclass. def _create_unified_job_instance(self, **kwargs): diff --git a/awx/main/tests/jobs.py b/awx/main/tests/jobs.py index e6cbf68807..d0914e40b6 100644 --- a/awx/main/tests/jobs.py +++ b/awx/main/tests/jobs.py @@ -460,7 +460,9 @@ class JobTemplateTest(BaseJobTestMixin, django.test.TestCase): 'modified', 'name', 'description', 'job_type', 'inventory', 'project', 'playbook', 'credential', '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): url = reverse('api:job_template_list')