mirror of
https://github.com/ansible/awx.git
synced 2026-01-14 19:30:39 -03:30
1036 lines
43 KiB
Python
1036 lines
43 KiB
Python
# Copyright (c) 2013 AnsibleWorks, Inc.
|
|
# All Rights Reserved.
|
|
|
|
# Python
|
|
import json
|
|
import re
|
|
import socket
|
|
import urlparse
|
|
|
|
# PyYAML
|
|
import yaml
|
|
|
|
# Django
|
|
from django.conf import settings
|
|
from django.contrib.auth import authenticate
|
|
from django.contrib.auth.models import User
|
|
from django.core.urlresolvers import reverse
|
|
from django.core.exceptions import ObjectDoesNotExist
|
|
from django.db.models.fields import BLANK_CHOICE_DASH
|
|
from django.utils.datastructures import SortedDict
|
|
from django.utils.translation import ugettext_lazy as _
|
|
|
|
# Django REST Framework
|
|
from rest_framework.compat import get_concrete_model
|
|
from rest_framework import fields
|
|
from rest_framework import serializers
|
|
|
|
# AWX
|
|
from awx.main.models import *
|
|
from awx.main.utils import update_scm_url
|
|
|
|
BASE_FIELDS = ('id', 'url', 'related', 'summary_fields', 'created', 'modified',
|
|
'name', 'description')
|
|
|
|
# Fields that should be summarized regardless of object type.
|
|
DEFAULT_SUMMARY_FIELDS = ('name', 'description',)
|
|
|
|
# Keys are fields (foreign keys) where, if found on an instance, summary info
|
|
# should be added to the serialized data. Values are a tuple of field names on
|
|
# the related object to include in the summary data (if the field is present on
|
|
# the related object).
|
|
SUMMARIZABLE_FK_FIELDS = {
|
|
'organization': DEFAULT_SUMMARY_FIELDS,
|
|
'user': ('username', 'first_name', 'last_name'),
|
|
'team': DEFAULT_SUMMARY_FIELDS,
|
|
'inventory': DEFAULT_SUMMARY_FIELDS + ('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'),
|
|
'host': DEFAULT_SUMMARY_FIELDS + ('has_active_failures',
|
|
'has_inventory_sources'),
|
|
'group': DEFAULT_SUMMARY_FIELDS + ('has_active_failures',
|
|
'total_hosts',
|
|
'hosts_with_active_failures',
|
|
'total_groups',
|
|
'groups_with_active_failures',
|
|
'has_inventory_sources'),
|
|
'project': DEFAULT_SUMMARY_FIELDS + ('status',),
|
|
'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_host_summary': DEFAULT_SUMMARY_FIELDS + ('failed',),
|
|
'last_update': DEFAULT_SUMMARY_FIELDS + ('status', 'failed', 'license_error'),
|
|
'current_update': DEFAULT_SUMMARY_FIELDS + ('status', 'failed', 'license_error'),
|
|
'inventory_source': ('source', 'last_updated', 'status'),
|
|
}
|
|
|
|
class ChoiceField(fields.ChoiceField):
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(ChoiceField, self).__init__(*args, **kwargs)
|
|
if not self.required:
|
|
# Remove extra blank option if one is already present (for writable
|
|
# field) or if present at all for read-only fields.
|
|
if ([x[0] for x in self.choices].count(u'') > 1 or self.read_only) \
|
|
and BLANK_CHOICE_DASH[0] in self.choices:
|
|
self.choices = [x for x in self.choices
|
|
if x != BLANK_CHOICE_DASH[0]]
|
|
|
|
def metadata(self):
|
|
metadata = super(ChoiceField, self).metadata()
|
|
if self.choices:
|
|
metadata['choices'] = self.choices
|
|
return metadata
|
|
|
|
# Monkeypatch REST framework to replace default ChoiceField used by
|
|
# ModelSerializer.
|
|
serializers.ChoiceField = ChoiceField
|
|
|
|
class BaseSerializer(serializers.ModelSerializer):
|
|
|
|
# add the URL and related resources
|
|
url = serializers.SerializerMethodField('get_url')
|
|
related = serializers.SerializerMethodField('get_related')
|
|
summary_fields = serializers.SerializerMethodField('get_summary_fields')
|
|
|
|
# make certain fields read only
|
|
created = serializers.SerializerMethodField('get_created')
|
|
modified = serializers.SerializerMethodField('get_modified')
|
|
active = serializers.SerializerMethodField('get_active')
|
|
|
|
def get_fields(self):
|
|
opts = get_concrete_model(self.opts.model)._meta
|
|
ret = super(BaseSerializer, self).get_fields()
|
|
for key, field in ret.items():
|
|
if key == 'id' and not getattr(field, 'help_text', None):
|
|
field.help_text = 'Database ID for this %s.' % unicode(opts.verbose_name)
|
|
elif key == 'url':
|
|
field.help_text = 'URL for this %s.' % unicode(opts.verbose_name)
|
|
field.type_label = 'string'
|
|
elif key == 'related':
|
|
field.help_text = 'Data structure with URLs of related resources.'
|
|
field.type_label = 'object'
|
|
elif key == 'summary_fields':
|
|
field.help_text = 'Data structure with name/description for related resources.'
|
|
field.type_label = 'object'
|
|
elif key == 'created':
|
|
field.help_text = 'Timestamp when this %s was created.' % unicode(opts.verbose_name)
|
|
field.type_label = 'datetime'
|
|
elif key == 'modified':
|
|
field.help_text = 'Timestamp when this %s was last modified.' % unicode(opts.verbose_name)
|
|
field.type_label = 'datetime'
|
|
return ret
|
|
|
|
def get_url(self, obj):
|
|
if obj is None:
|
|
return ''
|
|
elif isinstance(obj, User):
|
|
return reverse('api:user_detail', args=(obj.pk,))
|
|
else:
|
|
return obj.get_absolute_url()
|
|
|
|
def get_related(self, obj):
|
|
res = SortedDict()
|
|
if getattr(obj, 'created_by', None):
|
|
res['created_by'] = reverse('api:user_detail', args=(obj.created_by.pk,))
|
|
return res
|
|
|
|
def get_summary_fields(self, obj):
|
|
# Return values for certain fields on related objects, to simplify
|
|
# displaying lists of items without additional API requests.
|
|
summary_fields = SortedDict()
|
|
for fk, related_fields in SUMMARIZABLE_FK_FIELDS.items():
|
|
try:
|
|
fkval = getattr(obj, fk, None)
|
|
if fkval is not None:
|
|
summary_fields[fk] = SortedDict()
|
|
for field in related_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 obj is None:
|
|
return None
|
|
elif isinstance(obj, User):
|
|
return obj.date_joined
|
|
else:
|
|
return obj.created
|
|
|
|
def get_modified(self, obj):
|
|
if obj is None:
|
|
return None
|
|
elif isinstance(obj, User):
|
|
return obj.last_login # Not actually exposed for User.
|
|
else:
|
|
return obj.modified
|
|
|
|
def get_active(self, obj):
|
|
if obj is None:
|
|
return False
|
|
elif isinstance(obj, User):
|
|
return obj.is_active
|
|
else:
|
|
return obj.active
|
|
|
|
def validate_description(self, attrs, source):
|
|
# Description should always be empty string, never null.
|
|
attrs[source] = attrs.get(source, None) or ''
|
|
return attrs
|
|
|
|
class UserSerializer(BaseSerializer):
|
|
|
|
password = serializers.WritableField(required=False, default='',
|
|
help_text='Write-only field used to change the password.')
|
|
ldap_dn = serializers.Field(source='profile.ldap_dn')
|
|
|
|
class Meta:
|
|
model = User
|
|
fields = ('id', 'url', 'related', 'created', 'username', 'first_name',
|
|
'last_name', 'email', 'is_superuser', 'password', 'ldap_dn')
|
|
|
|
def to_native(self, obj):
|
|
ret = super(UserSerializer, self).to_native(obj)
|
|
ret.pop('password', None)
|
|
ret.fields.pop('password', None)
|
|
return ret
|
|
|
|
def get_validation_exclusions(self):
|
|
ret = super(UserSerializer, self).get_validation_exclusions()
|
|
ret.append('password')
|
|
return ret
|
|
|
|
def restore_object(self, attrs, instance=None):
|
|
new_password = attrs.pop('password', None)
|
|
instance = super(UserSerializer, self).restore_object(attrs, instance)
|
|
instance._new_password = new_password
|
|
return instance
|
|
|
|
def save_object(self, obj, **kwargs):
|
|
new_password = getattr(obj, '_new_password', None)
|
|
# For now we're not raising an error, just not saving password for
|
|
# users managed by LDAP who already have an unusable password set.
|
|
try:
|
|
if obj.pk and obj.profile.ldap_dn and not obj.has_usable_password():
|
|
new_password = None
|
|
except AttributeError:
|
|
pass
|
|
if new_password:
|
|
obj.set_password(new_password)
|
|
if not obj.password:
|
|
obj.set_unusable_password()
|
|
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,)),
|
|
organizations = reverse('api:user_organizations_list', args=(obj.pk,)),
|
|
admin_of_organizations = reverse('api:user_admin_of_organizations_list', args=(obj.pk,)),
|
|
projects = reverse('api:user_projects_list', args=(obj.pk,)),
|
|
credentials = reverse('api:user_credentials_list', args=(obj.pk,)),
|
|
permissions = reverse('api:user_permissions_list', args=(obj.pk,)),
|
|
))
|
|
return res
|
|
|
|
def _validate_ldap_managed_field(self, attrs, source):
|
|
try:
|
|
is_ldap_user = bool(self.object.profile.ldap_dn)
|
|
except AttributeError:
|
|
is_ldap_user = False
|
|
if is_ldap_user:
|
|
ldap_managed_fields = ['username']
|
|
ldap_managed_fields.extend(getattr(settings, 'AUTH_LDAP_USER_ATTR_MAP', {}).keys())
|
|
ldap_managed_fields.extend(getattr(settings, 'AUTH_LDAP_USER_FLAGS_BY_GROUP', {}).keys())
|
|
if source in ldap_managed_fields and source in attrs:
|
|
if attrs[source] != getattr(self.object, source):
|
|
raise serializers.ValidationError('Unable to change %s on user managed by LDAP' % source)
|
|
return attrs
|
|
|
|
def validate_username(self, attrs, source):
|
|
return self._validate_ldap_managed_field(attrs, source)
|
|
|
|
def validate_first_name(self, attrs, source):
|
|
return self._validate_ldap_managed_field(attrs, source)
|
|
|
|
def validate_last_name(self, attrs, source):
|
|
return self._validate_ldap_managed_field(attrs, source)
|
|
|
|
def validate_email(self, attrs, source):
|
|
return self._validate_ldap_managed_field(attrs, source)
|
|
|
|
def validate_is_superuser(self, attrs, source):
|
|
return self._validate_ldap_managed_field(attrs, source)
|
|
|
|
class OrganizationSerializer(BaseSerializer):
|
|
|
|
class Meta:
|
|
model = Organization
|
|
fields = BASE_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,)),
|
|
))
|
|
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')
|
|
|
|
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_update_failed', 'status', 'last_updated')
|
|
|
|
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,)),
|
|
))
|
|
if obj.credential:
|
|
res['credential'] = reverse('api:credential_detail',
|
|
args=(obj.credential.pk,))
|
|
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 _get_scm_type(self, attrs, source=None):
|
|
if self.object:
|
|
return attrs.get(source or 'scm_type', self.object.scm_type) or u''
|
|
else:
|
|
return attrs.get(source or 'scm_type', u'') or u''
|
|
|
|
def validate_local_path(self, attrs, source):
|
|
# Don't allow assigning a local_path used by another project.
|
|
# Don't allow assigning a local_path when scm_type is set.
|
|
valid_local_paths = Project.get_local_path_choices()
|
|
scm_type = self._get_scm_type(attrs)
|
|
if self.object and not scm_type:
|
|
valid_local_paths.append(self.object.local_path)
|
|
if scm_type:
|
|
attrs.pop(source, None)
|
|
if source in attrs and attrs[source] not in valid_local_paths:
|
|
raise serializers.ValidationError('Invalid path choice')
|
|
return attrs
|
|
|
|
def validate_scm_type(self, attrs, source):
|
|
scm_type = self._get_scm_type(attrs, source)
|
|
attrs[source] = scm_type
|
|
return attrs
|
|
|
|
def validate_scm_url(self, attrs, source):
|
|
scm_type = self._get_scm_type(attrs)
|
|
scm_url = unicode(attrs.get(source, None) or '')
|
|
if not scm_type:
|
|
return attrs
|
|
try:
|
|
scm_url = update_scm_url(scm_type, scm_url)
|
|
except ValueError, e:
|
|
raise serializers.ValidationError((e.args or ('Invalid SCM URL',))[0])
|
|
scm_url_parts = urlparse.urlsplit(scm_url)
|
|
if scm_type and not any(scm_url_parts):
|
|
raise serializers.ValidationError('SCM URL is required')
|
|
return attrs
|
|
|
|
#def validate_scm_username(self, attrs, source):
|
|
# if self.object:
|
|
# scm_type = attrs.get('scm_type', self.object.scm_type) or ''
|
|
# scm_url = unicode(attrs.get('scm_url', self.object.scm_url) or '')
|
|
# scm_username = attrs.get('scm_username', self.object.scm_username) or ''
|
|
# else:
|
|
# scm_type = attrs.get('scm_type', '') or ''
|
|
# scm_url = unicode(attrs.get('scm_url', '') or '')
|
|
# scm_username = attrs.get('scm_username', '') or ''
|
|
# if not scm_type:
|
|
# return attrs
|
|
# try:
|
|
# if scm_url and scm_username:
|
|
# update_scm_url(scm_type, scm_url, scm_username)
|
|
# except ValueError, e:
|
|
# raise serializers.ValidationError((e.args or ('Invalid SCM username',))[0])
|
|
# return attrs
|
|
|
|
#def validate_scm_password(self, attrs, source):
|
|
# if self.object:
|
|
# scm_type = attrs.get('scm_type', self.object.scm_type) or ''
|
|
# scm_url = unicode(attrs.get('scm_url', self.object.scm_url) or '')
|
|
# scm_username = attrs.get('scm_username', self.object.scm_username) or ''
|
|
# scm_password = attrs.get('scm_password', self.object.scm_password) or ''
|
|
# else:
|
|
# scm_type = attrs.get('scm_type', '') or ''
|
|
# scm_url = unicode(attrs.get('scm_url', '') or '')
|
|
# scm_username = attrs.get('scm_username', '') or ''
|
|
# scm_password = attrs.get('scm_password', '') or ''
|
|
# if not scm_type:
|
|
# return attrs
|
|
# try:
|
|
# try:
|
|
# if scm_url and scm_username:
|
|
# update_scm_url(scm_type, scm_url, scm_username)
|
|
# except ValueError:
|
|
# pass
|
|
# else:
|
|
# if scm_url and scm_username and scm_password:
|
|
# update_scm_url(scm_type, scm_url, scm_username, '**')
|
|
# except ValueError, e:
|
|
# raise serializers.ValidationError((e.args or ('Invalid SCM password',))[0])
|
|
# return attrs
|
|
|
|
# FIXME: Validate combination of SCM URL and credential!
|
|
|
|
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 ProjectUpdateSerializer(BaseSerializer):
|
|
|
|
class Meta:
|
|
model = ProjectUpdate
|
|
fields = ('id', 'url', 'related', 'summary_fields', 'created',
|
|
'modified', 'project', 'status', 'failed', 'result_stdout',
|
|
'result_traceback', 'job_args', 'job_cwd', 'job_env')
|
|
|
|
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,)),
|
|
cancel = reverse('api:project_update_cancel', args=(obj.pk,)),
|
|
))
|
|
return res
|
|
|
|
class BaseSerializerWithVariables(BaseSerializer):
|
|
|
|
def validate_variables(self, attrs, source):
|
|
try:
|
|
json.loads(attrs.get(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', '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,)),
|
|
groups = reverse('api:inventory_groups_list', args=(obj.pk,)),
|
|
root_groups = reverse('api:inventory_root_groups_list', args=(obj.pk,)),
|
|
variable_data = reverse('api:inventory_variable_data', args=(obj.pk,)),
|
|
script = reverse('api:inventory_script_view', args=(obj.pk,)),
|
|
tree = reverse('api:inventory_tree_view', args=(obj.pk,)),
|
|
organization = reverse('api:organization_detail', args=(obj.organization.pk,)),
|
|
inventory_sources = reverse('api:inventory_inventory_sources_list', args=(obj.pk,)),
|
|
))
|
|
return res
|
|
|
|
class HostSerializer(BaseSerializerWithVariables):
|
|
|
|
class Meta:
|
|
model = Host
|
|
fields = BASE_FIELDS + ('inventory', 'enabled', 'instance_id', 'variables',
|
|
'has_active_failures', 'has_inventory_sources',
|
|
'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,)),
|
|
inventory = reverse('api:inventory_detail', args=(obj.inventory.pk,)),
|
|
groups = reverse('api:host_groups_list', args=(obj.pk,)),
|
|
all_groups = reverse('api:host_all_groups_list', args=(obj.pk,)),
|
|
job_events = reverse('api:host_job_events_list', args=(obj.pk,)),
|
|
job_host_summaries = reverse('api:host_job_host_summaries_list', args=(obj.pk,)),
|
|
#inventory_sources = reverse('api:host_inventory_sources_list', args=(obj.pk,)),
|
|
))
|
|
if obj.last_job:
|
|
res['last_job'] = reverse('api:job_detail', args=(obj.last_job.pk,))
|
|
if obj.last_job_host_summary:
|
|
res['last_job_host_summary'] = reverse('api:job_host_summary_detail', args=(obj.last_job_host_summary.pk,))
|
|
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
|
|
d['last_job']['job_template_name'] = obj.last_job.job_template.name
|
|
except (KeyError, AttributeError):
|
|
pass
|
|
d['all_groups'] = [{'id': g.id, 'name': g.name} for g in obj.all_groups.all()]
|
|
d['groups'] = [{'id': g.id, 'name': g.name} for g in obj.groups.all()]
|
|
return d
|
|
|
|
def _get_host_port_from_name(self, name):
|
|
# Allow hostname (except IPv6 for now) to specify the port # inline.
|
|
port = None
|
|
if name.count(':') == 1:
|
|
name, port = name.split(':')
|
|
try:
|
|
port = int(port)
|
|
if port < 1 or port > 65535:
|
|
raise ValueError
|
|
except ValueError:
|
|
raise serializers.ValidationError('Invalid port specification: %s' % str(port))
|
|
return name, port
|
|
|
|
def validate_name(self, attrs, source):
|
|
name = unicode(attrs.get(source, ''))
|
|
# Validate here only, update in main validate method.
|
|
host, port = self._get_host_port_from_name(name)
|
|
#for family in (socket.AF_INET, socket.AF_INET6):
|
|
# try:
|
|
# socket.inet_pton(family, name)
|
|
# return attrs
|
|
# except socket.error:
|
|
# pass
|
|
# Hostname should match the following regular expression and have at
|
|
# last one letter in the name (to catch invalid IPv4 addresses from
|
|
# above).
|
|
#valid_host_re = r'^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$'
|
|
#if re.match(valid_host_re, name) and re.match(r'^.*?[a-zA-Z].*?$', name):
|
|
# return attrs
|
|
#raise serializers.ValidationError('Invalid host name or IP')
|
|
return attrs
|
|
|
|
def validate(self, attrs):
|
|
name = unicode(attrs.get('name', ''))
|
|
host, port = self._get_host_port_from_name(name)
|
|
|
|
if port:
|
|
attrs['name'] = host
|
|
if self.object:
|
|
variables = unicode(attrs.get('variables', self.object.variables) or '')
|
|
else:
|
|
variables = unicode(attrs.get('variables', ''))
|
|
try:
|
|
vars_dict = json.loads(variables.strip() or '{}')
|
|
vars_dict['ansible_ssh_port'] = port
|
|
attrs['variables'] = json.dumps(vars_dict)
|
|
except (ValueError, TypeError):
|
|
try:
|
|
vars_dict = yaml.safe_load(variables)
|
|
vars_dict['ansible_ssh_port'] = port
|
|
attrs['variables'] = yaml.dump(vars_dict)
|
|
except (yaml.YAMLError, TypeError):
|
|
raise serializers.ValidationError('Must be valid JSON or YAML')
|
|
|
|
return attrs
|
|
|
|
|
|
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')
|
|
|
|
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,)),
|
|
hosts = reverse('api:group_hosts_list', args=(obj.pk,)),
|
|
potential_children = reverse('api:group_potential_children_list', args=(obj.pk,)),
|
|
children = reverse('api:group_children_list', args=(obj.pk,)),
|
|
all_hosts = reverse('api:group_all_hosts_list', args=(obj.pk,)),
|
|
inventory = reverse('api:inventory_detail', args=(obj.inventory.pk,)),
|
|
job_events = reverse('api:group_job_events_list', args=(obj.pk,)),
|
|
job_host_summaries = reverse('api:group_job_host_summaries_list', args=(obj.pk,)),
|
|
inventory_source = reverse('api:inventory_source_detail', args=(obj.inventory_source.pk,)),
|
|
#inventory_sources = reverse('api:group_inventory_sources_list', args=(obj.pk,)),
|
|
))
|
|
return res
|
|
|
|
def validate_name(self, attrs, source):
|
|
name = attrs.get(source, '')
|
|
if name in ('all', '_meta'):
|
|
raise serializers.ValidationError('Invalid group name')
|
|
return attrs
|
|
|
|
class GroupTreeSerializer(GroupSerializer):
|
|
|
|
children = serializers.SerializerMethodField('get_children')
|
|
|
|
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')
|
|
|
|
def get_children(self, obj):
|
|
if obj is None:
|
|
return {}
|
|
children_qs = obj.children.filter(active=True)
|
|
return GroupTreeSerializer(children_qs, many=True).data
|
|
|
|
class BaseVariableDataSerializer(BaseSerializer):
|
|
|
|
def to_native(self, obj):
|
|
if obj is None:
|
|
return {}
|
|
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 InventorySourceSerializer(BaseSerializer):
|
|
|
|
#source_password = serializers.WritableField(required=False, default='')
|
|
|
|
class Meta:
|
|
model = InventorySource
|
|
fields = ('id', 'url', 'related', 'summary_fields', 'created',
|
|
'modified', 'inventory', 'group', 'source', 'source_path',
|
|
'source_vars', 'credential', 'source_regions', 'overwrite',
|
|
'overwrite_vars', 'update_on_launch', 'update_interval',
|
|
'last_update_failed', 'status', 'last_updated')
|
|
read_only_fields = ('inventory', 'group')
|
|
|
|
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,)),
|
|
#hosts = reverse('api:inventory_source_hosts_list', args=(obj.pk,)),
|
|
#groups = reverse('api:inventory_source_groups_list', args=(obj.pk,)),
|
|
))
|
|
if obj.inventory:
|
|
res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,))
|
|
if obj.group:
|
|
res['group'] = reverse('api:group_detail', args=(obj.group.pk,))
|
|
if obj.credential:
|
|
res['credential'] = reverse('api:credential_detail',
|
|
args=(obj.credential.pk,))
|
|
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
|
|
# FIXME
|
|
return attrs
|
|
|
|
def validate_source_vars(self, attrs, source):
|
|
# source_env must be blank, a valid JSON or YAML dict, or ...
|
|
# FIXME: support key=value pairs.
|
|
try:
|
|
json.loads(attrs.get(source, '').strip() or '{}')
|
|
return attrs
|
|
except ValueError:
|
|
pass
|
|
try:
|
|
yaml.safe_load(attrs[source])
|
|
return attrs
|
|
except yaml.YAMLError:
|
|
pass
|
|
raise serializers.ValidationError('Must be valid JSON or YAML')
|
|
|
|
def validate_source_regions(self, attrs, source):
|
|
# FIXME
|
|
return attrs
|
|
|
|
class InventoryUpdateSerializer(BaseSerializer):
|
|
|
|
class Meta:
|
|
model = InventoryUpdate
|
|
fields = ('id', 'url', 'related', 'summary_fields', 'created',
|
|
'modified', 'inventory_source', 'status', 'failed',
|
|
'result_stdout', 'result_traceback', 'job_args', 'job_cwd',
|
|
'job_env', '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,)),
|
|
cancel = reverse('api:inventory_update_cancel', args=(obj.pk,)),
|
|
))
|
|
return res
|
|
|
|
class TeamSerializer(BaseSerializer):
|
|
|
|
class Meta:
|
|
model = Team
|
|
fields = BASE_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,)),
|
|
users = reverse('api:team_users_list', args=(obj.pk,)),
|
|
credentials = reverse('api:team_credentials_list', args=(obj.pk,)),
|
|
organization = reverse('api:organization_detail', args=(obj.organization.pk,)),
|
|
permissions = reverse('api: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):
|
|
if obj is None:
|
|
return {}
|
|
res = super(PermissionSerializer, self).get_related(obj)
|
|
if obj.user:
|
|
res['user'] = reverse('api:user_detail', args=(obj.user.pk,))
|
|
if obj.team:
|
|
res['team'] = reverse('api:team_detail', args=(obj.team.pk,))
|
|
if obj.project:
|
|
res['project'] = reverse('api:project_detail', args=(obj.project.pk,))
|
|
if obj.inventory:
|
|
res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,))
|
|
return res
|
|
|
|
def validate(self, attrs):
|
|
# Can only set either user or team.
|
|
if attrs['user'] and attrs['team']:
|
|
raise serializers.ValidationError('permission can only be assigned'
|
|
' to a user OR a team, not both')
|
|
# Cannot assign admit/read/write permissions for a project.
|
|
if attrs['permission_type'] in ('admin', 'read', 'write') and attrs['project']:
|
|
raise serializers.ValidationError('project cannot be assigned for '
|
|
'inventory-only permissions')
|
|
# Project is required when setting deployment permissions.
|
|
if attrs['permission_type'] in ('run', 'check') and not attrs['project']:
|
|
raise serializers.ValidationError('project is required when '
|
|
'assigning deployment permissions')
|
|
return attrs
|
|
|
|
class CredentialSerializer(BaseSerializer):
|
|
|
|
# FIXME: may want to make some of these filtered based on user accessing
|
|
|
|
password = serializers.WritableField(required=False, default='')
|
|
ssh_key_data = serializers.WritableField(required=False, default='')
|
|
ssh_key_unlock = serializers.WritableField(required=False, default='')
|
|
sudo_password = serializers.WritableField(required=False, default='')
|
|
|
|
class Meta:
|
|
model = Credential
|
|
fields = BASE_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)
|
|
# Replace the actual encrypted value with the string $encrypted$.
|
|
for field in Credential.PASSWORD_FIELDS:
|
|
if field in ret and unicode(ret[field]).startswith('$encrypted$'):
|
|
ret[field] = '$encrypted$'
|
|
return ret
|
|
|
|
def restore_object(self, attrs, instance=None):
|
|
# If the value sent to the API startswith $encrypted$, ignore it.
|
|
for field in Credential.PASSWORD_FIELDS:
|
|
if unicode(attrs.get(field, '')).startswith('$encrypted$'):
|
|
attrs.pop(field, None)
|
|
instance = super(CredentialSerializer, self).restore_object(attrs, instance)
|
|
return instance
|
|
|
|
def get_related(self, obj):
|
|
if obj is None:
|
|
return {}
|
|
res = super(CredentialSerializer, self).get_related(obj)
|
|
if obj.user:
|
|
res['user'] = reverse('api:user_detail', args=(obj.user.pk,))
|
|
if obj.team:
|
|
res['team'] = reverse('api:team_detail', args=(obj.team.pk,))
|
|
return res
|
|
|
|
class JobTemplateSerializer(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')
|
|
|
|
def get_related(self, obj):
|
|
if obj is None:
|
|
return {}
|
|
res = super(JobTemplateSerializer, self).get_related(obj)
|
|
res.update(dict(
|
|
inventory = reverse('api:inventory_detail', args=(obj.inventory.pk,)),
|
|
project = reverse('api:project_detail', args=(obj.project.pk,)),
|
|
jobs = reverse('api:job_template_jobs_list', args=(obj.pk,)),
|
|
))
|
|
if obj.credential:
|
|
res['credential'] = reverse('api:credential_detail', args=(obj.credential.pk,))
|
|
if obj.cloud_credential:
|
|
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 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='passwords_needed_to_start')
|
|
|
|
class Meta:
|
|
model = Job
|
|
fields = ('id', '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')
|
|
|
|
def get_related(self, obj):
|
|
if obj is None:
|
|
return {}
|
|
res = super(JobSerializer, self).get_related(obj)
|
|
res.update(dict(
|
|
inventory = reverse('api:inventory_detail', args=(obj.inventory.pk,)),
|
|
project = reverse('api:project_detail', args=(obj.project.pk,)),
|
|
credential = reverse('api:credential_detail', args=(obj.credential.pk,)),
|
|
job_events = reverse('api:job_job_events_list', args=(obj.pk,)),
|
|
job_host_summaries = reverse('api:job_job_host_summaries_list', args=(obj.pk,)),
|
|
))
|
|
if obj.job_template:
|
|
res['job_template'] = reverse('api:job_template_detail', args=(obj.job_template.pk,))
|
|
if obj.cloud_credential:
|
|
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:
|
|
res['cancel'] = reverse('api: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)
|
|
if job_template.cloud_credential:
|
|
data.setdefault('cloud_credential', job_template.cloud_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', 'created', 'modified',
|
|
'summary_fields', 'related', '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,)),
|
|
host=reverse('api:host_detail', args=(obj.host.pk,))
|
|
))
|
|
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
|
|
d['job']['job_template_name'] = obj.job.job_template.name
|
|
except (KeyError, AttributeError):
|
|
pass
|
|
return d
|
|
|
|
class JobEventSerializer(BaseSerializer):
|
|
|
|
event_display = serializers.Field(source='get_event_display2')
|
|
event_level = serializers.Field(source='event_level')
|
|
|
|
class Meta:
|
|
model = JobEvent
|
|
fields = ('id', 'url', 'created', 'modified', 'job', 'event',
|
|
'event_display', 'event_data', 'event_level', 'failed',
|
|
'changed', 'host', 'related', 'summary_fields', '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,)),
|
|
#children = reverse('api:job_event_children_list', args=(obj.pk,)),
|
|
))
|
|
if obj.parent:
|
|
res['parent'] = reverse('api:job_event_detail', args=(obj.parent.pk,))
|
|
if obj.children.count():
|
|
res['children'] = reverse('api:job_event_children_list', args=(obj.pk,))
|
|
if obj.host:
|
|
res['host'] = reverse('api:host_detail', args=(obj.host.pk,))
|
|
if obj.hosts.count():
|
|
res['hosts'] = reverse('api:job_event_hosts_list', args=(obj.pk,))
|
|
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
|
|
d['job']['job_template_name'] = obj.job.job_template.name
|
|
except (KeyError, AttributeError):
|
|
pass
|
|
return d
|
|
|
|
class AuthTokenSerializer(serializers.Serializer):
|
|
|
|
username = serializers.CharField()
|
|
password = serializers.CharField()
|
|
|
|
def validate(self, attrs):
|
|
username = attrs.get('username')
|
|
password = attrs.get('password')
|
|
|
|
if username and password:
|
|
user = authenticate(username=username, password=password)
|
|
if user:
|
|
if not user.is_active:
|
|
raise serializers.ValidationError('User account is disabled.')
|
|
attrs['user'] = user
|
|
return attrs
|
|
else:
|
|
raise serializers.ValidationError('Unable to login with provided credentials.')
|
|
else:
|
|
raise serializers.ValidationError('Must include "username" and "password"')
|