# Copyright (c) 2013 AnsibleWorks, Inc. # All Rights Reserved. # Python import json # PyYAML import yaml # Django from django.contrib.auth.models import User from django.core.urlresolvers import reverse from django.core.exceptions import ObjectDoesNotExist # Django REST Framework from rest_framework import serializers # AnsibleWorks from ansibleworks.main.models import * BASE_FIELDS = ('id', 'url', 'related', 'summary_fields', 'created', 'name', 'description') # objects that if found we should add summary info for them SUMMARIZABLE_FKS = ( 'organization', 'host', 'group', 'inventory', 'project', 'team', 'job', 'job_template', 'credential', 'permission', ) # fields that should be summarized regardless of object type SUMMARIZABLE_FIELDS = ( 'name', 'username', 'first_name', 'last_name', 'description', ) class BaseSerializer(serializers.ModelSerializer): # add the URL and related resources url = serializers.SerializerMethodField('get_absolute_url') related = serializers.SerializerMethodField('get_related') summary_fields = serializers.SerializerMethodField('get_summary_fields') # make certain fields read only created = serializers.SerializerMethodField('get_created') active = serializers.SerializerMethodField('get_active') def get_absolute_url(self, obj): if isinstance(obj, User): return reverse('main:user_detail', args=(obj.pk,)) else: return obj.get_absolute_url() def get_related(self, obj): res = dict() if getattr(obj, 'created_by', None): res['created_by'] = reverse('main:user_detail', args=(obj.created_by.pk,)) return res def get_summary_fields(self, obj): # return the names (at least) for various fields, so we don't have to write this # method for each object. summary_fields = {} for fk in SUMMARIZABLE_FKS: try: fkval = getattr(obj, fk, None) if fkval is not None: summary_fields[fk] = {} for field in SUMMARIZABLE_FIELDS: fval = getattr(fkval, field, None) if fval is not None: summary_fields[fk][field] = fval # Can be raised by the reverse accessor for a OneToOneField. except ObjectDoesNotExist: pass return summary_fields def get_created(self, obj): if isinstance(obj, User): return obj.date_joined else: return obj.created def get_active(self, obj): if isinstance(obj, User): return obj.is_active else: return obj.active class OrganizationSerializer(BaseSerializer): class Meta: model = Organization fields = BASE_FIELDS def get_related(self, obj): res = super(OrganizationSerializer, self).get_related(obj) res.update(dict( #audit_trail = reverse('main:organization_audit_trail_list', args=(obj.pk,)), projects = reverse('main:organization_projects_list', args=(obj.pk,)), inventories = reverse('main:organization_inventories_list', args=(obj.pk,)), users = reverse('main:organization_users_list', args=(obj.pk,)), admins = reverse('main:organization_admins_list', args=(obj.pk,)), #tags = reverse('main:organization_tags_list', args=(obj.pk,)), teams = reverse('main:organization_teams_list', args=(obj.pk,)), )) return res class ProjectSerializer(BaseSerializer): playbooks = serializers.Field(source='playbooks') class Meta: model = Project fields = BASE_FIELDS + ('local_path',) def get_related(self, obj): res = super(ProjectSerializer, self).get_related(obj) res.update(dict( organizations = reverse('main:project_organizations_list', args=(obj.pk,)), playbooks = reverse('main:project_detail_playbooks', args=(obj.pk,)), )) return res class ProjectPlaybooksSerializer(ProjectSerializer): class Meta: model = Project fields = ('playbooks',) def to_native(self, obj): ret = super(ProjectPlaybooksSerializer, self).to_native(obj) return ret.get('playbooks', []) class BaseSerializerWithVariables(BaseSerializer): def validate_variables(self, attrs, source): try: json.loads(attrs[source].strip() or '{}') except ValueError: try: yaml.safe_load(attrs[source]) except yaml.YAMLError: raise serializers.ValidationError('Must be valid JSON or YAML') return attrs class InventorySerializer(BaseSerializerWithVariables): class Meta: model = Inventory fields = BASE_FIELDS + ('organization', 'variables', 'has_active_failures') def get_related(self, obj): res = super(InventorySerializer, self).get_related(obj) res.update(dict( hosts = reverse('main:inventory_hosts_list', args=(obj.pk,)), groups = reverse('main:inventory_groups_list', args=(obj.pk,)), root_groups = reverse('main:inventory_root_groups_list', args=(obj.pk,)), variable_data = reverse('main:inventory_variable_detail', args=(obj.pk,)), organization = reverse('main:organization_detail', args=(obj.organization.pk,)), )) return res class HostSerializer(BaseSerializerWithVariables): class Meta: model = Host fields = BASE_FIELDS + ('inventory', 'variables', 'has_active_failures') def get_related(self, obj): res = super(HostSerializer, self).get_related(obj) res.update(dict( variable_data = reverse('main:host_variable_detail', args=(obj.pk,)), inventory = reverse('main:inventory_detail', args=(obj.inventory.pk,)), groups = reverse('main:host_groups_list', args=(obj.pk,)), all_groups = reverse('main:host_all_groups_list', args=(obj.pk,)), job_events = reverse('main:host_job_events_list', args=(obj.pk,)), job_host_summaries = reverse('main:host_job_host_summaries_list', args=(obj.pk,)), )) if obj.last_job: res['last_job'] = reverse('main:job_detail', args=(obj.last_job.pk,)) if obj.last_job_host_summary: res['last_job_host_summary'] = reverse('main:job_host_summary_detail', args=(obj.last_job_host_summary.pk,)) return res class GroupSerializer(BaseSerializerWithVariables): class Meta: model = Group fields = BASE_FIELDS + ('inventory', 'variables', 'has_active_failures') def get_related(self, obj): res = super(GroupSerializer, self).get_related(obj) res.update(dict( variable_data = reverse('main:group_variable_detail', args=(obj.pk,)), hosts = reverse('main:group_hosts_list', args=(obj.pk,)), children = reverse('main:group_children_list', args=(obj.pk,)), all_hosts = reverse('main:group_all_hosts_list', args=(obj.pk,)), inventory = reverse('main:inventory_detail', args=(obj.inventory.pk,)), job_events = reverse('main:group_job_events_list', args=(obj.pk,)), job_host_summaries = reverse('main:group_job_host_summaries_list', args=(obj.pk,)), )) return res class BaseVariableDataSerializer(BaseSerializer): def to_native(self, obj): ret = super(BaseVariableDataSerializer, self).to_native(obj) try: return json.loads(ret.get('variables', '') or '{}') except ValueError: return yaml.safe_load(ret.get('variables', '')) def from_native(self, data, files): data = {'variables': json.dumps(data)} return super(BaseVariableDataSerializer, self).from_native(data, files) 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 TeamSerializer(BaseSerializer): class Meta: model = Team fields = BASE_FIELDS + ('organization',) def get_related(self, obj): res = super(TeamSerializer, self).get_related(obj) res.update(dict( projects = reverse('main:team_projects_list', args=(obj.pk,)), users = reverse('main:team_users_list', args=(obj.pk,)), credentials = reverse('main:team_credentials_list', args=(obj.pk,)), organization = reverse('main:organization_detail', args=(obj.organization.pk,)), permissions = reverse('main:team_permissions_list', args=(obj.pk,)), )) return res class PermissionSerializer(BaseSerializer): class Meta: model = Permission fields = BASE_FIELDS + ('user', 'team', 'project', 'inventory', 'permission_type',) def get_related(self, obj): res = super(PermissionSerializer, self).get_related(obj) if obj.user: res['user'] = reverse('main:user_detail', args=(obj.user.pk,)) if obj.team: res['team'] = reverse('main:team_detail', args=(obj.team.pk,)) if obj.project: res['project'] = reverse('main:project_detail', args=(obj.project.pk,)) if obj.inventory: res['inventory'] = reverse('main:inventory_detail', args=(obj.inventory.pk,)) return res class CredentialSerializer(BaseSerializer): # FIXME: may want to make some of these filtered based on user accessing class Meta: model = Credential fields = BASE_FIELDS + ('ssh_username', 'ssh_password', 'ssh_key_data', 'ssh_key_unlock', 'sudo_username', 'sudo_password', 'user', 'team',) def get_related(self, obj): res = super(CredentialSerializer, self).get_related(obj) if obj.user: res['user'] = reverse('main:user_detail', args=(obj.user.pk,)) if obj.team: res['team'] = reverse('main:team_detail', args=(obj.team.pk,)) return res def validate(self, attrs): ''' some fields cannot be changed once written ''' if self.object is not None: # this is an update if self.object.user != attrs['user']: raise serializers.ValidationError("user cannot be changed") if self.object.team != attrs['team']: raise serializers.ValidationError("team cannot be changed") return attrs class UserSerializer(BaseSerializer): class Meta: model = User fields = ('id', 'url', 'related', 'created', 'username', 'first_name', 'last_name', 'email', 'is_active', 'is_superuser',) def get_related(self, obj): res = super(UserSerializer, self).get_related(obj) res.update(dict( teams = reverse('main:user_teams_list', args=(obj.pk,)), organizations = reverse('main:user_organizations_list', args=(obj.pk,)), admin_of_organizations = reverse('main:user_admin_of_organizations_list', args=(obj.pk,)), projects = reverse('main:user_projects_list', args=(obj.pk,)), credentials = reverse('main:user_credentials_list', args=(obj.pk,)), permissions = reverse('main:user_permissions_list', args=(obj.pk,)), )) return res class JobTemplateSerializer(BaseSerializer): class Meta: model = JobTemplate fields = BASE_FIELDS + ('job_type', 'inventory', 'project', 'playbook', 'credential', 'forks', 'limit', 'verbosity', 'extra_vars', 'job_tags', 'host_config_key') def get_related(self, obj): res = super(JobTemplateSerializer, self).get_related(obj) res.update(dict( inventory = reverse('main:inventory_detail', args=(obj.inventory.pk,)), project = reverse('main:project_detail', args=(obj.project.pk,)), jobs = reverse('main:job_template_jobs_list', args=(obj.pk,)), )) if obj.credential: res['credential'] = reverse('main:credential_detail', args=(obj.credential.pk,)) return res def validate_playbook(self, attrs, source): project = attrs.get('project', None) playbook = attrs.get('playbook', '') if project and playbook and playbook not in project.playbooks: raise serializers.ValidationError('Playbook not found for project') return attrs class JobSerializer(BaseSerializer): passwords_needed_to_start = serializers.Field(source='get_passwords_needed_to_start') class Meta: model = Job fields = BASE_FIELDS + ('job_template', 'job_type', 'inventory', 'project', 'playbook', 'credential', 'forks', 'limit', 'verbosity', 'extra_vars', 'job_tags', 'status', 'failed', 'result_stdout', 'result_traceback', 'passwords_needed_to_start') def get_related(self, obj): res = super(JobSerializer, self).get_related(obj) res.update(dict( inventory = reverse('main:inventory_detail', args=(obj.inventory.pk,)), project = reverse('main:project_detail', args=(obj.project.pk,)), credential = reverse('main:credential_detail', args=(obj.credential.pk,)), job_events = reverse('main:job_job_events_list', args=(obj.pk,)), job_host_summaries = reverse('main:job_job_host_summaries_list', args=(obj.pk,)), )) if obj.job_template: res['job_template'] = reverse('main:job_template_detail', args=(obj.job_template.pk,)) if obj.can_start or True: res['start'] = reverse('main:job_start', args=(obj.pk,)) if obj.can_cancel or True: res['cancel'] = reverse('main:job_cancel', args=(obj.pk,)) return res def from_native(self, data, files): # When creating a new job and a job template is specified, populate any # fields not provided in data from the job template. if not self.object and isinstance(data, dict) and 'job_template' in data: try: job_template = JobTemplate.objects.get(pk=data['job_template']) except JobTemplate.DoesNotExist: self._errors = {'job_template': 'Invalid job template'} return # Don't auto-populate name or description. data.setdefault('job_type', job_template.job_type) data.setdefault('inventory', job_template.inventory.pk) data.setdefault('project', job_template.project.pk) data.setdefault('playbook', job_template.playbook) if job_template.credential: data.setdefault('credential', job_template.credential.pk) data.setdefault('forks', job_template.forks) data.setdefault('limit', job_template.limit) data.setdefault('verbosity', job_template.verbosity) data.setdefault('extra_vars', job_template.extra_vars) data.setdefault('job_tags', job_template.job_tags) return super(JobSerializer, self).from_native(data, files) class JobHostSummarySerializer(BaseSerializer): class Meta: model = JobHostSummary fields = ('id', 'url', 'job', 'host', 'summary_fields', 'related', 'changed', 'dark', 'failures', 'ok', 'processed', 'skipped', 'failed') def get_related(self, obj): res = super(JobHostSummarySerializer, self).get_related(obj) res.update(dict( job=reverse('main:job_detail', args=(obj.job.pk,)), host=reverse('main:host_detail', args=(obj.host.pk,)) )) return res class JobEventSerializer(BaseSerializer): event_display = serializers.Field(source='get_event_display2') class Meta: model = JobEvent fields = ('id', 'url', 'created', 'job', 'event', 'event_display', 'event_data', 'failed', 'host', 'related', 'summary_fields', 'parent') def get_related(self, obj): res = super(JobEventSerializer, self).get_related(obj) res.update(dict( job = reverse('main:job_detail', args=(obj.job.pk,)), #children = reverse('main:job_event_children_list', args=(obj.pk,)), )) if obj.parent: res['parent'] = reverse('main:job_event_detail', args=(obj.parent.pk,)) if obj.children.count(): res['children'] = reverse('main:job_event_children_list', args=(obj.pk,)) if obj.host: res['host'] = reverse('main:host_detail', args=(obj.host.pk,)) if obj.hosts.count(): res['hosts'] = reverse('main:job_event_hosts_list', args=(obj.pk,)) return res