mirror of
https://github.com/ZwareBear/awx.git
synced 2026-04-24 00:41:48 -05:00
remove /api/v1 and deprecated credential fields
This commit is contained in:
@@ -24,20 +24,6 @@ from rest_framework.filters import BaseFilterBackend
|
||||
# AWX
|
||||
from awx.main.utils import get_type_for_model, to_python_boolean
|
||||
from awx.main.utils.db import get_all_field_names
|
||||
from awx.main.models.credential import CredentialType
|
||||
|
||||
|
||||
class V1CredentialFilterBackend(BaseFilterBackend):
|
||||
'''
|
||||
For /api/v1/ requests, filter out v2 (custom) credentials
|
||||
'''
|
||||
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
# TODO: remove in 3.3
|
||||
from awx.api.versioning import get_request_version
|
||||
if get_request_version(request) == 1:
|
||||
queryset = queryset.filter(credential_type__managed_by_tower=True)
|
||||
return queryset
|
||||
|
||||
|
||||
class TypeFilterBackend(BaseFilterBackend):
|
||||
@@ -292,39 +278,6 @@ class FieldLookupBackend(BaseFilterBackend):
|
||||
key = key[5:]
|
||||
q_not = True
|
||||
|
||||
# Make legacy v1 Job/Template fields work for backwards compatability
|
||||
# TODO: remove after API v1 deprecation period
|
||||
if queryset.model._meta.object_name in ('JobTemplate', 'Job') and key in (
|
||||
'credential', 'vault_credential', 'cloud_credential', 'network_credential'
|
||||
) or queryset.model._meta.object_name in ('InventorySource', 'InventoryUpdate') and key == 'credential':
|
||||
key = 'credentials'
|
||||
|
||||
# Make legacy v1 Credential fields work for backwards compatability
|
||||
# TODO: remove after API v1 deprecation period
|
||||
#
|
||||
# convert v1 `Credential.kind` queries to `Credential.credential_type__pk`
|
||||
if queryset.model._meta.object_name == 'Credential' and key == 'kind':
|
||||
key = key.replace('kind', 'credential_type')
|
||||
|
||||
if 'ssh' in values:
|
||||
# In 3.2, SSH and Vault became separate credential types, but in the v1 API,
|
||||
# they're both still "kind=ssh"
|
||||
# under the hood, convert `/api/v1/credentials/?kind=ssh` to
|
||||
# `/api/v1/credentials/?or__credential_type=<ssh_pk>&or__credential_type=<vault_pk>`
|
||||
values = set(values)
|
||||
values.add('vault')
|
||||
values = list(values)
|
||||
q_or = True
|
||||
|
||||
for i, kind in enumerate(values):
|
||||
if kind == 'vault':
|
||||
type_ = CredentialType.objects.get(kind=kind)
|
||||
else:
|
||||
type_ = CredentialType.from_v1_kind(kind)
|
||||
if type_ is None:
|
||||
raise ParseError(_('cannot filter on kind %s') % kind)
|
||||
values[i] = type_.pk
|
||||
|
||||
# Convert value(s) to python and add to the appropriate list.
|
||||
for value in values:
|
||||
if q_int:
|
||||
|
||||
@@ -34,7 +34,7 @@ from rest_framework.negotiation import DefaultContentNegotiation
|
||||
# AWX
|
||||
from awx.api.filters import FieldLookupBackend
|
||||
from awx.main.models import (
|
||||
UnifiedJob, UnifiedJobTemplate, User, Role
|
||||
UnifiedJob, UnifiedJobTemplate, User, Role, Credential
|
||||
)
|
||||
from awx.main.access import access_registry
|
||||
from awx.main.utils import (
|
||||
@@ -46,7 +46,7 @@ from awx.main.utils import (
|
||||
)
|
||||
from awx.main.utils.db import get_all_field_names
|
||||
from awx.api.serializers import ResourceAccessListElementSerializer, CopySerializer, UserSerializer
|
||||
from awx.api.versioning import URLPathVersioning, get_request_version
|
||||
from awx.api.versioning import URLPathVersioning
|
||||
from awx.api.metadata import SublistAttachDetatchMetadata, Metadata
|
||||
|
||||
__all__ = ['APIView', 'GenericAPIView', 'ListAPIView', 'SimpleListAPIView',
|
||||
@@ -288,12 +288,6 @@ class APIView(views.APIView):
|
||||
template_list.append('api/%s.md' % template_basename)
|
||||
context = self.get_description_context()
|
||||
|
||||
# "v2" -> 2
|
||||
default_version = int(settings.REST_FRAMEWORK['DEFAULT_VERSION'].lstrip('v'))
|
||||
request_version = get_request_version(self.request)
|
||||
if request_version is not None and request_version < default_version:
|
||||
context['deprecated'] = True
|
||||
|
||||
description = render_to_string(template_list, context)
|
||||
if context.get('deprecated') and context.get('swagger_method') is None:
|
||||
# render deprecation messages at the very top
|
||||
@@ -842,10 +836,6 @@ class CopyAPIView(GenericAPIView):
|
||||
new_in_330 = True
|
||||
new_in_api_v2 = True
|
||||
|
||||
def v1_not_allowed(self):
|
||||
return Response({'detail': 'Action only possible starting with v2 API.'},
|
||||
status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
def _get_copy_return_serializer(self, *args, **kwargs):
|
||||
if not self.copy_return_serializer_class:
|
||||
return self.get_serializer(*args, **kwargs)
|
||||
@@ -859,15 +849,15 @@ class CopyAPIView(GenericAPIView):
|
||||
def _decrypt_model_field_if_needed(obj, field_name, field_val):
|
||||
if field_name in getattr(type(obj), 'REENCRYPTION_BLACKLIST_AT_COPY', []):
|
||||
return field_val
|
||||
if isinstance(field_val, dict):
|
||||
if isinstance(obj, Credential) and field_name == 'inputs':
|
||||
for secret in obj.credential_type.secret_fields:
|
||||
if secret in field_val:
|
||||
field_val[secret] = decrypt_field(obj, secret)
|
||||
elif isinstance(field_val, dict):
|
||||
for sub_field in field_val:
|
||||
if isinstance(sub_field, str) \
|
||||
and isinstance(field_val[sub_field], str):
|
||||
try:
|
||||
field_val[sub_field] = decrypt_field(obj, field_name, sub_field)
|
||||
except AttributeError:
|
||||
# Catching the corner case with v1 credential fields
|
||||
field_val[sub_field] = decrypt_field(obj, sub_field)
|
||||
field_val[sub_field] = decrypt_field(obj, field_name, sub_field)
|
||||
elif isinstance(field_val, str):
|
||||
try:
|
||||
field_val = decrypt_field(obj, field_name)
|
||||
@@ -952,8 +942,6 @@ class CopyAPIView(GenericAPIView):
|
||||
return ret
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if get_request_version(request) < 2:
|
||||
return self.v1_not_allowed()
|
||||
obj = self.get_object()
|
||||
if not request.user.can_access(obj.__class__, 'read', obj):
|
||||
raise PermissionDenied()
|
||||
@@ -968,8 +956,6 @@ class CopyAPIView(GenericAPIView):
|
||||
return Response({'can_copy': can_copy})
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if get_request_version(request) < 2:
|
||||
return self.v1_not_allowed()
|
||||
obj = self.get_object()
|
||||
create_kwargs = self._build_create_dict(obj)
|
||||
create_kwargs_check = {}
|
||||
|
||||
@@ -232,19 +232,6 @@ class RoleMetadata(Metadata):
|
||||
return metadata
|
||||
|
||||
|
||||
# TODO: Tower 3.3 remove class and all uses in views.py when API v1 is removed
|
||||
class JobTypeMetadata(Metadata):
|
||||
def get_field_info(self, field):
|
||||
res = super(JobTypeMetadata, self).get_field_info(field)
|
||||
|
||||
if field.field_name == 'job_type':
|
||||
res['choices'] = [
|
||||
choice for choice in res['choices']
|
||||
if choice[0] != 'scan'
|
||||
]
|
||||
return res
|
||||
|
||||
|
||||
class SublistAttachDetatchMetadata(Metadata):
|
||||
|
||||
def determine_actions(self, request, view):
|
||||
|
||||
@@ -54,7 +54,7 @@ from awx.main.models import (
|
||||
OAuth2AccessToken, OAuth2Application, Organization, Project,
|
||||
ProjectUpdate, ProjectUpdateEvent, RefreshToken, Role, Schedule,
|
||||
SystemJob, SystemJobEvent, SystemJobTemplate, Team, UnifiedJob,
|
||||
UnifiedJobTemplate, V1Credential, WorkflowJob, WorkflowJobNode,
|
||||
UnifiedJobTemplate, WorkflowJob, WorkflowJobNode,
|
||||
WorkflowJobTemplate, WorkflowJobTemplateNode, StdoutMaxBytesExceeded
|
||||
)
|
||||
from awx.main.models.base import VERBOSITY_CHOICES, NEW_JOB_TYPE_CHOICES
|
||||
@@ -72,7 +72,7 @@ from awx.main.redact import UriCleaner, REPLACE_STR
|
||||
|
||||
from awx.main.validators import vars_validate_or_raise
|
||||
|
||||
from awx.api.versioning import reverse, get_request_version
|
||||
from awx.api.versioning import reverse
|
||||
from awx.api.fields import (BooleanNullField, CharNullField, ChoiceNullField,
|
||||
VerbatimField, DeprecatedCredentialField)
|
||||
|
||||
@@ -113,7 +113,6 @@ SUMMARIZABLE_FK_FIELDS = {
|
||||
'source_project': DEFAULT_SUMMARY_FIELDS + ('status', 'scm_type'),
|
||||
'project_update': DEFAULT_SUMMARY_FIELDS + ('status', 'failed',),
|
||||
'credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud', 'credential_type_id'),
|
||||
'vault_credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud', 'credential_type_id'),
|
||||
'job': DEFAULT_SUMMARY_FIELDS + ('status', 'failed', 'elapsed', 'type'),
|
||||
'job_template': DEFAULT_SUMMARY_FIELDS,
|
||||
'workflow_job_template': DEFAULT_SUMMARY_FIELDS,
|
||||
@@ -144,7 +143,7 @@ def reverse_gfk(content_object, request):
|
||||
Returns a dictionary of the form
|
||||
{ '<type>': reverse(<type detail>) }
|
||||
for example
|
||||
{ 'organization': '/api/v1/organizations/1/' }
|
||||
{ 'organization': '/api/v2/organizations/1/' }
|
||||
'''
|
||||
if content_object is None or not hasattr(content_object, 'get_absolute_url'):
|
||||
return {}
|
||||
@@ -301,10 +300,7 @@ class BaseSerializer(serializers.ModelSerializer, metaclass=BaseSerializerMetacl
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
"""
|
||||
The request version component of the URL as an integer i.e., 1 or 2
|
||||
"""
|
||||
return get_request_version(self.context.get('request')) or 1
|
||||
return 2
|
||||
|
||||
def get_type(self, obj):
|
||||
return get_type_for_model(self.Meta.model)
|
||||
@@ -359,10 +355,9 @@ class BaseSerializer(serializers.ModelSerializer, metaclass=BaseSerializerMetacl
|
||||
if view and (hasattr(view, 'retrieve') or view.request.method == 'POST') and \
|
||||
type(obj) in settings.NAMED_URL_GRAPH:
|
||||
original_url = self.get_url(obj)
|
||||
if not original_url.startswith('/api/v1'):
|
||||
res['named_url'] = self._generate_named_url(
|
||||
original_url, obj, settings.NAMED_URL_GRAPH[type(obj)]
|
||||
)
|
||||
res['named_url'] = self._generate_named_url(
|
||||
original_url, obj, settings.NAMED_URL_GRAPH[type(obj)]
|
||||
)
|
||||
if getattr(obj, 'created_by', None):
|
||||
res['created_by'] = self.reverse('api:user_detail', kwargs={'pk': obj.created_by.pk})
|
||||
if getattr(obj, 'modified_by', None):
|
||||
@@ -396,8 +391,6 @@ class BaseSerializer(serializers.ModelSerializer, metaclass=BaseSerializerMetacl
|
||||
continue
|
||||
summary_fields[fk] = OrderedDict()
|
||||
for field in related_fields:
|
||||
if self.version < 2 and field == 'credential_type_id': # TODO: remove version check in 3.3
|
||||
continue
|
||||
|
||||
fval = getattr(fkval, field, None)
|
||||
|
||||
@@ -884,10 +877,10 @@ class UserSerializer(BaseSerializer):
|
||||
'username', 'first_name', 'last_name',
|
||||
'email', 'is_superuser', 'is_system_auditor', 'password', 'ldap_dn', 'last_login', 'external_account')
|
||||
|
||||
def to_representation(self, obj): # TODO: Remove in 3.3
|
||||
def to_representation(self, obj):
|
||||
ret = super(UserSerializer, self).to_representation(obj)
|
||||
ret.pop('password', None)
|
||||
if obj and type(self) is UserSerializer or self.version == 1:
|
||||
if obj and type(self) is UserSerializer:
|
||||
ret['auth'] = obj.social_auth.values('provider', 'uid')
|
||||
return ret
|
||||
|
||||
@@ -1364,9 +1357,9 @@ class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer):
|
||||
notification_templates_error = self.reverse('api:project_notification_templates_error_list', kwargs={'pk': obj.pk}),
|
||||
access_list = self.reverse('api:project_access_list', kwargs={'pk': obj.pk}),
|
||||
object_roles = self.reverse('api:project_object_roles_list', kwargs={'pk': obj.pk}),
|
||||
copy = self.reverse('api:project_copy', kwargs={'pk': obj.pk})
|
||||
|
||||
))
|
||||
if self.version > 1:
|
||||
res['copy'] = self.reverse('api:project_copy', kwargs={'pk': obj.pk})
|
||||
if obj.organization:
|
||||
res['organization'] = self.reverse('api:organization_detail',
|
||||
kwargs={'pk': obj.organization.pk})
|
||||
@@ -1561,9 +1554,8 @@ class InventorySerializer(BaseSerializerWithVariables):
|
||||
access_list = self.reverse('api:inventory_access_list', kwargs={'pk': obj.pk}),
|
||||
object_roles = self.reverse('api:inventory_object_roles_list', kwargs={'pk': obj.pk}),
|
||||
instance_groups = self.reverse('api:inventory_instance_groups_list', kwargs={'pk': obj.pk}),
|
||||
copy = self.reverse('api:inventory_copy', kwargs={'pk': obj.pk})
|
||||
))
|
||||
if self.version > 1:
|
||||
res['copy'] = self.reverse('api:inventory_copy', kwargs={'pk': obj.pk})
|
||||
if obj.insights_credential:
|
||||
res['insights_credential'] = self.reverse('api:credential_detail', kwargs={'pk': obj.insights_credential.pk})
|
||||
if obj.organization:
|
||||
@@ -1615,20 +1607,6 @@ class InventorySerializer(BaseSerializerWithVariables):
|
||||
return super(InventorySerializer, self).validate(attrs)
|
||||
|
||||
|
||||
# TODO: Remove entire serializer in 3.3, replace with normal serializer
|
||||
class InventoryDetailSerializer(InventorySerializer):
|
||||
|
||||
def get_fields(self):
|
||||
fields = super(InventoryDetailSerializer, self).get_fields()
|
||||
if self.version == 1:
|
||||
fields['can_run_ad_hoc_commands'] = serializers.SerializerMethodField()
|
||||
return fields
|
||||
|
||||
def get_can_run_ad_hoc_commands(self, obj):
|
||||
view = self.context.get('view', None)
|
||||
return bool(obj and view and view.request and view.request.user and view.request.user.can_access(Inventory, 'run_ad_hoc_commands', obj))
|
||||
|
||||
|
||||
class InventoryScriptSerializer(InventorySerializer):
|
||||
|
||||
class Meta:
|
||||
@@ -1668,19 +1646,15 @@ class HostSerializer(BaseSerializerWithVariables):
|
||||
smart_inventories = self.reverse('api:host_smart_inventories_list', kwargs={'pk': obj.pk}),
|
||||
ad_hoc_commands = self.reverse('api:host_ad_hoc_commands_list', kwargs={'pk': obj.pk}),
|
||||
ad_hoc_command_events = self.reverse('api:host_ad_hoc_command_events_list', kwargs={'pk': obj.pk}),
|
||||
insights = self.reverse('api:host_insights', kwargs={'pk': obj.pk}),
|
||||
ansible_facts = self.reverse('api:host_ansible_facts_detail', kwargs={'pk': obj.pk}),
|
||||
))
|
||||
if self.version > 1:
|
||||
res['insights'] = self.reverse('api:host_insights', kwargs={'pk': obj.pk})
|
||||
if obj.inventory:
|
||||
res['inventory'] = self.reverse('api:inventory_detail', kwargs={'pk': obj.inventory.pk})
|
||||
if obj.last_job:
|
||||
res['last_job'] = self.reverse('api:job_detail', kwargs={'pk': obj.last_job.pk})
|
||||
if obj.last_job_host_summary:
|
||||
res['last_job_host_summary'] = self.reverse('api:job_host_summary_detail', kwargs={'pk': obj.last_job_host_summary.pk})
|
||||
if self.version > 1:
|
||||
res.update(dict(
|
||||
ansible_facts = self.reverse('api:host_ansible_facts_detail', kwargs={'pk': obj.pk}),
|
||||
))
|
||||
return res
|
||||
|
||||
def get_summary_fields(self, obj):
|
||||
@@ -1766,6 +1740,7 @@ class AnsibleFactsSerializer(BaseSerializer):
|
||||
|
||||
|
||||
class GroupSerializer(BaseSerializerWithVariables):
|
||||
show_capabilities = ['copy', 'edit', 'delete']
|
||||
capabilities_prefetch = ['inventory.admin', 'inventory.adhoc']
|
||||
groups_with_active_failures = serializers.IntegerField(
|
||||
read_only=True,
|
||||
@@ -1779,13 +1754,6 @@ class GroupSerializer(BaseSerializerWithVariables):
|
||||
'total_hosts', 'hosts_with_active_failures', 'total_groups',
|
||||
'groups_with_active_failures', 'has_inventory_sources')
|
||||
|
||||
@property
|
||||
def show_capabilities(self): # TODO: consolidate in 3.3
|
||||
if self.version == 1:
|
||||
return ['copy', 'edit', 'start', 'schedule', 'delete']
|
||||
else:
|
||||
return ['copy', 'edit', 'delete']
|
||||
|
||||
def build_relational_field(self, field_name, relation_info):
|
||||
field_class, field_kwargs = super(GroupSerializer, self).build_relational_field(field_name, relation_info)
|
||||
# Inventory is read-only unless creating a new group.
|
||||
@@ -1794,20 +1762,6 @@ class GroupSerializer(BaseSerializerWithVariables):
|
||||
field_kwargs.pop('queryset', None)
|
||||
return field_class, field_kwargs
|
||||
|
||||
def get_summary_fields(self, obj): # TODO: remove in 3.3
|
||||
summary_fields = super(GroupSerializer, self).get_summary_fields(obj)
|
||||
if self.version == 1:
|
||||
try:
|
||||
inv_src = obj.deprecated_inventory_source
|
||||
summary_fields['inventory_source'] = {}
|
||||
for field in SUMMARIZABLE_FK_FIELDS['inventory_source']:
|
||||
fval = getattr(inv_src, field, None)
|
||||
if fval is not None:
|
||||
summary_fields['inventory_source'][field] = fval
|
||||
except Group.deprecated_inventory_source.RelatedObjectDoesNotExist:
|
||||
pass
|
||||
return summary_fields
|
||||
|
||||
def get_related(self, obj):
|
||||
res = super(GroupSerializer, self).get_related(obj)
|
||||
res.update(dict(
|
||||
@@ -1822,24 +1776,10 @@ class GroupSerializer(BaseSerializerWithVariables):
|
||||
inventory_sources = self.reverse('api:group_inventory_sources_list', kwargs={'pk': obj.pk}),
|
||||
ad_hoc_commands = self.reverse('api:group_ad_hoc_commands_list', kwargs={'pk': obj.pk}),
|
||||
))
|
||||
if self.version == 1: # TODO: remove in 3.3
|
||||
try:
|
||||
res['inventory_source'] = self.reverse('api:inventory_source_detail',
|
||||
kwargs={'pk': obj.deprecated_inventory_source.pk})
|
||||
except Group.deprecated_inventory_source.RelatedObjectDoesNotExist:
|
||||
pass
|
||||
if obj.inventory:
|
||||
res['inventory'] = self.reverse('api:inventory_detail', kwargs={'pk': obj.inventory.pk})
|
||||
return res
|
||||
|
||||
def create(self, validated_data): # TODO: remove in 3.3
|
||||
instance = super(GroupSerializer, self).create(validated_data)
|
||||
if self.version == 1: # TODO: remove in 3.3
|
||||
manual_src = InventorySource(deprecated_group=instance, inventory=instance.inventory)
|
||||
manual_src.v1_group_name = instance.name
|
||||
manual_src.save()
|
||||
return instance
|
||||
|
||||
def validate_name(self, value):
|
||||
if value in ('all', '_meta'):
|
||||
raise serializers.ValidationError(_('Invalid group name.'))
|
||||
@@ -1941,9 +1881,8 @@ class CustomInventoryScriptSerializer(BaseSerializer):
|
||||
res = super(CustomInventoryScriptSerializer, self).get_related(obj)
|
||||
res.update(dict(
|
||||
object_roles = self.reverse('api:inventory_script_object_roles_list', kwargs={'pk': obj.pk}),
|
||||
copy = self.reverse('api:inventory_script_copy', kwargs={'pk': obj.pk}),
|
||||
))
|
||||
if self.version > 1:
|
||||
res['copy'] = self.reverse('api:inventory_script_copy', kwargs={'pk': obj.pk})
|
||||
|
||||
if obj.organization:
|
||||
res['organization'] = self.reverse('api:organization_detail', kwargs={'pk': obj.organization.pk})
|
||||
@@ -2004,27 +1943,6 @@ class InventorySourceOptionsSerializer(BaseSerializer):
|
||||
|
||||
return super(InventorySourceOptionsSerializer, self).validate(attrs)
|
||||
|
||||
# TODO: remove when old 'credential' fields are removed
|
||||
def get_summary_fields(self, obj):
|
||||
summary_fields = super(InventorySourceOptionsSerializer, self).get_summary_fields(obj)
|
||||
all_creds = []
|
||||
if 'credential' in summary_fields:
|
||||
cred = obj.get_cloud_credential()
|
||||
if cred:
|
||||
summarized_cred = {
|
||||
'id': cred.id, 'name': cred.name, 'description': cred.description,
|
||||
'kind': cred.kind, 'cloud': True
|
||||
}
|
||||
summary_fields['credential'] = summarized_cred
|
||||
all_creds.append(summarized_cred)
|
||||
if self.version > 1:
|
||||
summary_fields['credential']['credential_type_id'] = cred.credential_type_id
|
||||
else:
|
||||
summary_fields.pop('credential')
|
||||
if self.version > 1:
|
||||
summary_fields['credentials'] = all_creds
|
||||
return summary_fields
|
||||
|
||||
|
||||
class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOptionsSerializer):
|
||||
|
||||
@@ -2036,14 +1954,12 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt
|
||||
{'admin': 'inventory.admin'},
|
||||
{'start': 'inventory.update'}
|
||||
]
|
||||
group = serializers.SerializerMethodField(
|
||||
help_text=_('Automatic group relationship, will be removed in 3.3'))
|
||||
|
||||
class Meta:
|
||||
model = InventorySource
|
||||
fields = ('*', 'name', 'inventory', 'update_on_launch', 'update_cache_timeout',
|
||||
'source_project', 'update_on_project_update') + \
|
||||
('last_update_failed', 'last_updated', 'group') # Backwards compatibility.
|
||||
('last_update_failed', 'last_updated') # Backwards compatibility.
|
||||
|
||||
def get_related(self, obj):
|
||||
res = super(InventorySourceSerializer, self).get_related(obj)
|
||||
@@ -2069,30 +1985,10 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt
|
||||
if obj.last_update:
|
||||
res['last_update'] = self.reverse('api:inventory_update_detail',
|
||||
kwargs={'pk': obj.last_update.pk})
|
||||
if self.version == 1: # TODO: remove in 3.3
|
||||
if obj.deprecated_group:
|
||||
res['group'] = self.reverse('api:group_detail', kwargs={'pk': obj.deprecated_group.pk})
|
||||
else:
|
||||
res['credentials'] = self.reverse('api:inventory_source_credentials_list', kwargs={'pk': obj.pk})
|
||||
return res
|
||||
|
||||
def get_fields(self): # TODO: remove in 3.3
|
||||
fields = super(InventorySourceSerializer, self).get_fields()
|
||||
if self.version > 1:
|
||||
fields.pop('group', None)
|
||||
return fields
|
||||
|
||||
def get_summary_fields(self, obj): # TODO: remove in 3.3
|
||||
summary_fields = super(InventorySourceSerializer, self).get_summary_fields(obj)
|
||||
if self.version == 1 and obj.deprecated_group_id:
|
||||
g = obj.deprecated_group
|
||||
summary_fields['group'] = {}
|
||||
for field in SUMMARIZABLE_FK_FIELDS['group']:
|
||||
fval = getattr(g, field, None)
|
||||
if fval is not None:
|
||||
summary_fields['group'][field] = fval
|
||||
return summary_fields
|
||||
|
||||
def get_group(self, obj): # TODO: remove in 3.3
|
||||
if obj.deprecated_group:
|
||||
return obj.deprecated_group.id
|
||||
@@ -2127,12 +2023,6 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt
|
||||
raise serializers.ValidationError(_("Cannot use manual project for SCM-based inventory."))
|
||||
return value
|
||||
|
||||
def validate_source(self, value):
|
||||
if value == '':
|
||||
raise serializers.ValidationError(_(
|
||||
"Manual inventory sources are created automatically when a group is created in the v1 API."))
|
||||
return value
|
||||
|
||||
def validate_update_on_project_update(self, value):
|
||||
if value and self.instance and self.instance.schedules.exists():
|
||||
raise serializers.ValidationError(_("Setting not compatible with existing schedules."))
|
||||
@@ -2253,8 +2143,7 @@ class InventoryUpdateSerializer(UnifiedJobSerializer, InventorySourceOptionsSeri
|
||||
if obj.inventory:
|
||||
res['inventory'] = self.reverse('api:inventory_detail', kwargs={'pk': obj.inventory.pk})
|
||||
|
||||
if self.version > 1:
|
||||
res['credentials'] = self.reverse('api:inventory_update_credentials_list', kwargs={'pk': obj.pk})
|
||||
res['credentials'] = self.reverse('api:inventory_update_credentials_list', kwargs={'pk': obj.pk})
|
||||
|
||||
return res
|
||||
|
||||
@@ -2562,67 +2451,22 @@ class CredentialTypeSerializer(BaseSerializer):
|
||||
return fields
|
||||
|
||||
|
||||
# TODO: remove when API v1 is removed
|
||||
class V1CredentialFields(BaseSerializer, metaclass=BaseSerializerMetaclass):
|
||||
|
||||
class Meta:
|
||||
model = Credential
|
||||
fields = ('*', 'kind', 'cloud', 'host', 'username',
|
||||
'password', 'security_token', 'project', 'domain',
|
||||
'ssh_key_data', 'ssh_key_unlock', 'become_method',
|
||||
'become_username', 'become_password', 'vault_password',
|
||||
'subscription', 'tenant', 'secret', 'client', 'authorize',
|
||||
'authorize_password')
|
||||
|
||||
def build_field(self, field_name, info, model_class, nested_depth):
|
||||
if field_name in V1Credential.FIELDS:
|
||||
return self.build_standard_field(field_name,
|
||||
V1Credential.FIELDS[field_name])
|
||||
return super(V1CredentialFields, self).build_field(field_name, info, model_class, nested_depth)
|
||||
|
||||
|
||||
class V2CredentialFields(BaseSerializer, metaclass=BaseSerializerMetaclass):
|
||||
|
||||
class Meta:
|
||||
model = Credential
|
||||
fields = ('*', 'credential_type', 'inputs')
|
||||
|
||||
extra_kwargs = {
|
||||
'credential_type': {
|
||||
'label': _('Credential Type'),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class CredentialSerializer(BaseSerializer):
|
||||
show_capabilities = ['edit', 'delete', 'copy', 'use']
|
||||
capabilities_prefetch = ['admin', 'use']
|
||||
|
||||
class Meta:
|
||||
model = Credential
|
||||
fields = ('*', 'organization')
|
||||
|
||||
def get_fields(self):
|
||||
fields = super(CredentialSerializer, self).get_fields()
|
||||
|
||||
# TODO: remove when API v1 is removed
|
||||
if self.version == 1:
|
||||
fields.update(V1CredentialFields().get_fields())
|
||||
else:
|
||||
fields.update(V2CredentialFields().get_fields())
|
||||
return fields
|
||||
fields = ('*', 'organization', 'credential_type', 'inputs', 'kind', 'cloud')
|
||||
extra_kwargs = {
|
||||
'credential_type': {
|
||||
'label': _('Credential Type'),
|
||||
},
|
||||
}
|
||||
|
||||
def to_representation(self, data):
|
||||
value = super(CredentialSerializer, self).to_representation(data)
|
||||
|
||||
# TODO: remove when API v1 is removed
|
||||
if self.version == 1:
|
||||
if value.get('kind') == 'vault':
|
||||
value['kind'] = 'ssh'
|
||||
for field in V1Credential.PASSWORD_FIELDS:
|
||||
if field in value and force_text(value[field]).startswith('$encrypted$'):
|
||||
value[field] = '$encrypted$'
|
||||
|
||||
if 'inputs' in value:
|
||||
value['inputs'] = data.display_inputs()
|
||||
return value
|
||||
@@ -2639,16 +2483,10 @@ class CredentialSerializer(BaseSerializer):
|
||||
object_roles = self.reverse('api:credential_object_roles_list', kwargs={'pk': obj.pk}),
|
||||
owner_users = self.reverse('api:credential_owner_users_list', kwargs={'pk': obj.pk}),
|
||||
owner_teams = self.reverse('api:credential_owner_teams_list', kwargs={'pk': obj.pk}),
|
||||
copy = self.reverse('api:credential_copy', kwargs={'pk': obj.pk}),
|
||||
input_sources = self.reverse('api:credential_input_source_sublist', kwargs={'pk': obj.pk}),
|
||||
credential_type = self.reverse('api:credential_type_detail', kwargs={'pk': obj.credential_type.pk}),
|
||||
))
|
||||
if self.version > 1:
|
||||
res['copy'] = self.reverse('api:credential_copy', kwargs={'pk': obj.pk})
|
||||
res['input_sources'] = self.reverse('api:credential_input_source_sublist', kwargs={'pk': obj.pk})
|
||||
|
||||
# TODO: remove when API v1 is removed
|
||||
if self.version > 1:
|
||||
res.update(dict(
|
||||
credential_type = self.reverse('api:credential_type_detail', kwargs={'pk': obj.credential_type.pk}),
|
||||
))
|
||||
|
||||
parents = [role for role in obj.admin_role.parents.all() if role.object_id is not None]
|
||||
if parents:
|
||||
@@ -2684,54 +2522,12 @@ class CredentialSerializer(BaseSerializer):
|
||||
return summary_dict
|
||||
|
||||
def get_validation_exclusions(self, obj=None):
|
||||
# CredentialType is now part of validation; legacy v1 fields (e.g.,
|
||||
# 'username', 'password') in JSON POST payloads use the
|
||||
# CredentialType's inputs definition to determine their validity
|
||||
ret = super(CredentialSerializer, self).get_validation_exclusions(obj)
|
||||
for field in ('credential_type', 'inputs'):
|
||||
if field in ret:
|
||||
ret.remove(field)
|
||||
return ret
|
||||
|
||||
def to_internal_value(self, data):
|
||||
# TODO: remove when API v1 is removed
|
||||
if 'credential_type' not in data and self.version == 1:
|
||||
# If `credential_type` is not provided, assume the payload is a
|
||||
# v1 credential payload that specifies a `kind` and a flat list
|
||||
# of field values
|
||||
#
|
||||
# In this scenario, we should automatically detect the proper
|
||||
# CredentialType based on the provided values
|
||||
kind = data.get('kind', 'ssh')
|
||||
credential_type = CredentialType.from_v1_kind(kind, data)
|
||||
if credential_type is None:
|
||||
raise serializers.ValidationError({"kind": _('"%s" is not a valid choice' % kind)})
|
||||
data['credential_type'] = credential_type.pk
|
||||
value = OrderedDict(
|
||||
list({'credential_type': credential_type}.items()) +
|
||||
list(super(CredentialSerializer, self).to_internal_value(data).items())
|
||||
)
|
||||
|
||||
# Make a set of the keys in the POST/PUT payload
|
||||
# - Subtract real fields (name, organization, inputs)
|
||||
# - Subtract virtual v1 fields defined on the determined credential
|
||||
# type (username, password, etc...)
|
||||
# - Any leftovers are invalid for the determined credential type
|
||||
valid_fields = set(super(CredentialSerializer, self).get_fields().keys())
|
||||
valid_fields.update(V2CredentialFields().get_fields().keys())
|
||||
valid_fields.update(['kind', 'cloud'])
|
||||
|
||||
for field in set(data.keys()) - valid_fields - set(credential_type.defined_fields):
|
||||
if data.get(field):
|
||||
raise serializers.ValidationError(
|
||||
{"detail": _("'{field_name}' is not a valid field for {credential_type_name}").format(
|
||||
field_name=field, credential_type_name=credential_type.name
|
||||
)}
|
||||
)
|
||||
value.pop('kind', None)
|
||||
return value
|
||||
return super(CredentialSerializer, self).to_internal_value(data)
|
||||
|
||||
def validate_credential_type(self, credential_type):
|
||||
if self.instance and credential_type.pk != self.instance.credential_type.pk:
|
||||
for rel in (
|
||||
@@ -2788,35 +2584,12 @@ class CredentialSerializerCreate(CredentialSerializer):
|
||||
if attrs.get('team'):
|
||||
attrs['organization'] = attrs['team'].organization
|
||||
|
||||
try:
|
||||
return super(CredentialSerializerCreate, self).validate(attrs)
|
||||
except ValidationError as e:
|
||||
# TODO: remove when API v1 is removed
|
||||
# If we have an `inputs` error on `/api/v1/`:
|
||||
# {'inputs': {'username': [...]}}
|
||||
# ...instead, send back:
|
||||
# {'username': [...]}
|
||||
if self.version == 1 and isinstance(e.detail.get('inputs'), dict):
|
||||
e.detail = e.detail['inputs']
|
||||
raise e
|
||||
else:
|
||||
raise
|
||||
return super(CredentialSerializerCreate, self).validate(attrs)
|
||||
|
||||
def create(self, validated_data):
|
||||
user = validated_data.pop('user', None)
|
||||
team = validated_data.pop('team', None)
|
||||
|
||||
# If our payload contains v1 credential fields, translate to the new
|
||||
# model
|
||||
# TODO: remove when API v1 is removed
|
||||
if self.version == 1:
|
||||
for attr in (
|
||||
set(V1Credential.FIELDS) & set(validated_data.keys()) # set intersection
|
||||
):
|
||||
validated_data.setdefault('inputs', {})
|
||||
value = validated_data.pop(attr)
|
||||
if value:
|
||||
validated_data['inputs'][attr] = value
|
||||
credential = super(CredentialSerializerCreate, self).create(validated_data)
|
||||
|
||||
if user:
|
||||
@@ -2895,35 +2668,6 @@ class LabelsListMixin(object):
|
||||
return res
|
||||
|
||||
|
||||
# TODO: remove when API v1 is removed
|
||||
class V1JobOptionsSerializer(BaseSerializer, metaclass=BaseSerializerMetaclass):
|
||||
|
||||
class Meta:
|
||||
model = Credential
|
||||
fields = ('*', 'cloud_credential', 'network_credential')
|
||||
|
||||
V1_FIELDS = ('cloud_credential', 'network_credential',)
|
||||
|
||||
def build_field(self, field_name, info, model_class, nested_depth):
|
||||
if field_name in self.V1_FIELDS:
|
||||
return (DeprecatedCredentialField, {})
|
||||
return super(V1JobOptionsSerializer, self).build_field(field_name, info, model_class, nested_depth)
|
||||
|
||||
|
||||
class LegacyCredentialFields(BaseSerializer, metaclass=BaseSerializerMetaclass):
|
||||
|
||||
class Meta:
|
||||
model = Credential
|
||||
fields = ('*', 'credential', 'vault_credential')
|
||||
|
||||
LEGACY_FIELDS = ('credential', 'vault_credential',)
|
||||
|
||||
def build_field(self, field_name, info, model_class, nested_depth):
|
||||
if field_name in self.LEGACY_FIELDS:
|
||||
return (DeprecatedCredentialField, {})
|
||||
return super(LegacyCredentialFields, self).build_field(field_name, info, model_class, nested_depth)
|
||||
|
||||
|
||||
class JobOptionsSerializer(LabelsListMixin, BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
@@ -2932,16 +2676,6 @@ class JobOptionsSerializer(LabelsListMixin, BaseSerializer):
|
||||
'force_handlers', 'skip_tags', 'start_at_task', 'timeout',
|
||||
'use_fact_cache',)
|
||||
|
||||
def get_fields(self):
|
||||
fields = super(JobOptionsSerializer, self).get_fields()
|
||||
|
||||
# TODO: remove when API v1 is removed
|
||||
if self.version == 1:
|
||||
fields.update(V1JobOptionsSerializer().get_fields())
|
||||
|
||||
fields.update(LegacyCredentialFields().get_fields())
|
||||
return fields
|
||||
|
||||
def get_related(self, obj):
|
||||
res = super(JobOptionsSerializer, self).get_related(obj)
|
||||
res['labels'] = self.reverse('api:job_template_label_list', kwargs={'pk': obj.pk})
|
||||
@@ -2955,40 +2689,18 @@ class JobOptionsSerializer(LabelsListMixin, BaseSerializer):
|
||||
res['project'] = self.reverse('api:project_detail', kwargs={'pk': obj.project.pk})
|
||||
except ObjectDoesNotExist:
|
||||
setattr(obj, 'project', None)
|
||||
try:
|
||||
if obj.credential:
|
||||
res['credential'] = self.reverse(
|
||||
'api:credential_detail', kwargs={'pk': obj.credential}
|
||||
)
|
||||
except ObjectDoesNotExist:
|
||||
setattr(obj, 'credential', None)
|
||||
try:
|
||||
if obj.vault_credential:
|
||||
res['vault_credential'] = self.reverse(
|
||||
'api:credential_detail', kwargs={'pk': obj.vault_credential}
|
||||
)
|
||||
except ObjectDoesNotExist:
|
||||
setattr(obj, 'vault_credential', None)
|
||||
if self.version > 1:
|
||||
if isinstance(obj, UnifiedJobTemplate):
|
||||
res['extra_credentials'] = self.reverse(
|
||||
'api:job_template_extra_credentials_list',
|
||||
kwargs={'pk': obj.pk}
|
||||
)
|
||||
res['credentials'] = self.reverse(
|
||||
'api:job_template_credentials_list',
|
||||
kwargs={'pk': obj.pk}
|
||||
)
|
||||
elif isinstance(obj, UnifiedJob):
|
||||
res['extra_credentials'] = self.reverse('api:job_extra_credentials_list', kwargs={'pk': obj.pk})
|
||||
res['credentials'] = self.reverse('api:job_credentials_list', kwargs={'pk': obj.pk})
|
||||
else:
|
||||
cloud_cred = obj.cloud_credential
|
||||
if cloud_cred:
|
||||
res['cloud_credential'] = self.reverse('api:credential_detail', kwargs={'pk': cloud_cred})
|
||||
net_cred = obj.network_credential
|
||||
if net_cred:
|
||||
res['network_credential'] = self.reverse('api:credential_detail', kwargs={'pk': net_cred})
|
||||
if isinstance(obj, UnifiedJobTemplate):
|
||||
res['extra_credentials'] = self.reverse(
|
||||
'api:job_template_extra_credentials_list',
|
||||
kwargs={'pk': obj.pk}
|
||||
)
|
||||
res['credentials'] = self.reverse(
|
||||
'api:job_template_credentials_list',
|
||||
kwargs={'pk': obj.pk}
|
||||
)
|
||||
elif isinstance(obj, UnifiedJob):
|
||||
res['extra_credentials'] = self.reverse('api:job_extra_credentials_list', kwargs={'pk': obj.pk})
|
||||
res['credentials'] = self.reverse('api:job_credentials_list', kwargs={'pk': obj.pk})
|
||||
|
||||
return res
|
||||
|
||||
@@ -3002,70 +2714,9 @@ class JobOptionsSerializer(LabelsListMixin, BaseSerializer):
|
||||
ret['project'] = None
|
||||
if 'playbook' in ret:
|
||||
ret['playbook'] = ''
|
||||
ret['credential'] = obj.credential
|
||||
ret['vault_credential'] = obj.vault_credential
|
||||
if self.version == 1:
|
||||
ret['cloud_credential'] = obj.cloud_credential
|
||||
ret['network_credential'] = obj.network_credential
|
||||
return ret
|
||||
|
||||
def create(self, validated_data):
|
||||
deprecated_fields = {}
|
||||
for key in ('credential', 'vault_credential', 'cloud_credential', 'network_credential'):
|
||||
if key in validated_data:
|
||||
deprecated_fields[key] = validated_data.pop(key)
|
||||
obj = super(JobOptionsSerializer, self).create(validated_data)
|
||||
if deprecated_fields: # TODO: remove in 3.3
|
||||
self._update_deprecated_fields(deprecated_fields, obj)
|
||||
return obj
|
||||
|
||||
def update(self, obj, validated_data):
|
||||
deprecated_fields = {}
|
||||
for key in ('credential', 'vault_credential', 'cloud_credential', 'network_credential'):
|
||||
if key in validated_data:
|
||||
deprecated_fields[key] = validated_data.pop(key)
|
||||
obj = super(JobOptionsSerializer, self).update(obj, validated_data)
|
||||
if deprecated_fields: # TODO: remove in 3.3
|
||||
self._update_deprecated_fields(deprecated_fields, obj)
|
||||
return obj
|
||||
|
||||
def _update_deprecated_fields(self, fields, obj):
|
||||
for key, existing in (
|
||||
('credential', obj.credentials.filter(credential_type__kind='ssh')),
|
||||
('vault_credential', obj.credentials.filter(credential_type__kind='vault')),
|
||||
('cloud_credential', obj.cloud_credentials),
|
||||
('network_credential', obj.network_credentials),
|
||||
):
|
||||
if key in fields:
|
||||
new_cred = fields[key]
|
||||
if new_cred not in existing:
|
||||
for cred in existing:
|
||||
obj.credentials.remove(cred)
|
||||
if new_cred:
|
||||
obj.credentials.add(new_cred)
|
||||
|
||||
def validate(self, attrs):
|
||||
v1_credentials = {}
|
||||
view = self.context.get('view', None)
|
||||
for attr, kind, error in (
|
||||
('cloud_credential', 'cloud', _('You must provide a cloud credential.')),
|
||||
('network_credential', 'net', _('You must provide a network credential.')),
|
||||
('credential', 'ssh', _('You must provide an SSH credential.')),
|
||||
('vault_credential', 'vault', _('You must provide a vault credential.')),
|
||||
):
|
||||
if kind in ('cloud', 'net') and self.version > 1:
|
||||
continue # cloud and net deprecated creds are v1 only
|
||||
if attr in attrs:
|
||||
v1_credentials[attr] = None
|
||||
pk = attrs.pop(attr)
|
||||
if pk:
|
||||
cred = v1_credentials[attr] = Credential.objects.get(pk=pk)
|
||||
if cred.credential_type.kind != kind:
|
||||
raise serializers.ValidationError({attr: error})
|
||||
if ((not self.instance or cred.pk != getattr(self.instance, attr)) and
|
||||
view and view.request and view.request.user not in cred.use_role):
|
||||
raise PermissionDenied()
|
||||
|
||||
if 'project' in self.fields and 'playbook' in self.fields:
|
||||
project = attrs.get('project', self.instance and self.instance.project or None)
|
||||
playbook = attrs.get('playbook', self.instance and self.instance.playbook or '')
|
||||
@@ -3079,7 +2730,6 @@ class JobOptionsSerializer(LabelsListMixin, BaseSerializer):
|
||||
raise serializers.ValidationError({'playbook': _('Must select playbook for project.')})
|
||||
|
||||
ret = super(JobOptionsSerializer, self).validate(attrs)
|
||||
ret.update(v1_credentials)
|
||||
return ret
|
||||
|
||||
|
||||
@@ -3105,12 +2755,6 @@ class JobTemplateMixin(object):
|
||||
if obj.survey_spec is not None and ('name' in obj.survey_spec and 'description' in obj.survey_spec):
|
||||
d['survey'] = dict(title=obj.survey_spec['name'], description=obj.survey_spec['description'])
|
||||
d['recent_jobs'] = self._recent_jobs(obj)
|
||||
|
||||
# TODO: remove in 3.3
|
||||
if self.version == 1 and 'vault_credential' in d:
|
||||
if d['vault_credential'].get('kind','') == 'vault':
|
||||
d['vault_credential']['kind'] = 'ssh'
|
||||
|
||||
return d
|
||||
|
||||
|
||||
@@ -3146,9 +2790,8 @@ class JobTemplateSerializer(JobTemplateMixin, UnifiedJobTemplateSerializer, JobO
|
||||
object_roles = self.reverse('api:job_template_object_roles_list', kwargs={'pk': obj.pk}),
|
||||
instance_groups = self.reverse('api:job_template_instance_groups_list', kwargs={'pk': obj.pk}),
|
||||
slice_workflow_jobs = self.reverse('api:job_template_slice_workflow_jobs_list', kwargs={'pk': obj.pk}),
|
||||
copy = self.reverse('api:job_template_copy', kwargs={'pk': obj.pk}),
|
||||
))
|
||||
if self.version > 1:
|
||||
res['copy'] = self.reverse('api:job_template_copy', kwargs={'pk': obj.pk})
|
||||
if obj.host_config_key:
|
||||
res['callback'] = self.reverse('api:job_template_callback', kwargs={'pk': obj.pk})
|
||||
return res
|
||||
@@ -3181,9 +2824,6 @@ class JobTemplateSerializer(JobTemplateMixin, UnifiedJobTemplateSerializer, JobO
|
||||
summary_fields = super(JobTemplateSerializer, self).get_summary_fields(obj)
|
||||
all_creds = []
|
||||
# Organize credential data into multitude of deprecated fields
|
||||
# TODO: remove most of this as v1 is removed
|
||||
vault_credential = None
|
||||
credential = None
|
||||
extra_creds = []
|
||||
if obj.pk:
|
||||
for cred in obj.credentials.all():
|
||||
@@ -3194,30 +2834,12 @@ class JobTemplateSerializer(JobTemplateMixin, UnifiedJobTemplateSerializer, JobO
|
||||
'kind': cred.kind,
|
||||
'cloud': cred.credential_type.kind == 'cloud'
|
||||
}
|
||||
if self.version > 1:
|
||||
summarized_cred['credential_type_id'] = cred.credential_type_id
|
||||
all_creds.append(summarized_cred)
|
||||
if cred.credential_type.kind in ('cloud', 'net'):
|
||||
extra_creds.append(summarized_cred)
|
||||
elif summarized_cred['kind'] == 'ssh':
|
||||
credential = summarized_cred
|
||||
elif summarized_cred['kind'] == 'vault':
|
||||
vault_credential = summarized_cred
|
||||
# Selectively apply those fields, depending on view deetails
|
||||
if (self.is_detail_view or self.version == 1) and credential:
|
||||
summary_fields['credential'] = credential
|
||||
else:
|
||||
# Credential could be an empty dictionary in this case
|
||||
summary_fields.pop('credential', None)
|
||||
if (self.is_detail_view or self.version == 1) and vault_credential:
|
||||
summary_fields['vault_credential'] = vault_credential
|
||||
else:
|
||||
# vault credential could be empty dictionary
|
||||
summary_fields.pop('vault_credential', None)
|
||||
if self.version > 1:
|
||||
if self.is_detail_view:
|
||||
summary_fields['extra_credentials'] = extra_creds
|
||||
summary_fields['credentials'] = all_creds
|
||||
if self.is_detail_view:
|
||||
summary_fields['extra_credentials'] = extra_creds
|
||||
summary_fields['credentials'] = all_creds
|
||||
return summary_fields
|
||||
|
||||
|
||||
@@ -3250,6 +2872,7 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
|
||||
activity_stream = self.reverse('api:job_activity_stream_list', kwargs={'pk': obj.pk}),
|
||||
notifications = self.reverse('api:job_notifications_list', kwargs={'pk': obj.pk}),
|
||||
labels = self.reverse('api:job_label_list', kwargs={'pk': obj.pk}),
|
||||
create_schedule = self.reverse('api:job_create_schedule', kwargs={'pk': obj.pk}),
|
||||
))
|
||||
try:
|
||||
if obj.job_template:
|
||||
@@ -3257,8 +2880,6 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
|
||||
kwargs={'pk': obj.job_template.pk})
|
||||
except ObjectDoesNotExist:
|
||||
setattr(obj, 'job_template', None)
|
||||
if (obj.can_start or True) and self.version == 1: # TODO: remove in 3.3
|
||||
res['start'] = self.reverse('api:job_start', kwargs={'pk': obj.pk})
|
||||
if obj.can_cancel or True:
|
||||
res['cancel'] = self.reverse('api:job_cancel', kwargs={'pk': obj.pk})
|
||||
try:
|
||||
@@ -3268,8 +2889,6 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
|
||||
)
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
if self.version > 1:
|
||||
res['create_schedule'] = self.reverse('api:job_create_schedule', kwargs={'pk': obj.pk})
|
||||
res['relaunch'] = self.reverse('api:job_relaunch', kwargs={'pk': obj.pk})
|
||||
return res
|
||||
|
||||
@@ -3320,9 +2939,6 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
|
||||
summary_fields = super(JobSerializer, self).get_summary_fields(obj)
|
||||
all_creds = []
|
||||
# Organize credential data into multitude of deprecated fields
|
||||
# TODO: remove most of this as v1 is removed
|
||||
vault_credential = None
|
||||
credential = None
|
||||
extra_creds = []
|
||||
if obj.pk:
|
||||
for cred in obj.credentials.all():
|
||||
@@ -3333,30 +2949,12 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
|
||||
'kind': cred.kind,
|
||||
'cloud': cred.credential_type.kind == 'cloud'
|
||||
}
|
||||
if self.version > 1:
|
||||
summarized_cred['credential_type_id'] = cred.credential_type_id
|
||||
all_creds.append(summarized_cred)
|
||||
if cred.credential_type.kind in ('cloud', 'net'):
|
||||
extra_creds.append(summarized_cred)
|
||||
elif summarized_cred['kind'] == 'ssh':
|
||||
credential = summarized_cred
|
||||
elif summarized_cred['kind'] == 'vault':
|
||||
vault_credential = summarized_cred
|
||||
# Selectively apply those fields, depending on view deetails
|
||||
if (self.is_detail_view or self.version == 1) and credential:
|
||||
summary_fields['credential'] = credential
|
||||
else:
|
||||
# Credential could be an empty dictionary in this case
|
||||
summary_fields.pop('credential', None)
|
||||
if (self.is_detail_view or self.version == 1) and vault_credential:
|
||||
summary_fields['vault_credential'] = vault_credential
|
||||
else:
|
||||
# vault credential could be empty dictionary
|
||||
summary_fields.pop('vault_credential', None)
|
||||
if self.version > 1:
|
||||
if self.is_detail_view:
|
||||
summary_fields['extra_credentials'] = extra_creds
|
||||
summary_fields['credentials'] = all_creds
|
||||
if self.is_detail_view:
|
||||
summary_fields['extra_credentials'] = extra_creds
|
||||
summary_fields['credentials'] = all_creds
|
||||
return summary_fields
|
||||
|
||||
|
||||
@@ -3696,9 +3294,8 @@ class WorkflowJobTemplateSerializer(JobTemplateMixin, LabelsListMixin, UnifiedJo
|
||||
access_list = self.reverse('api:workflow_job_template_access_list', kwargs={'pk': obj.pk}),
|
||||
object_roles = self.reverse('api:workflow_job_template_object_roles_list', kwargs={'pk': obj.pk}),
|
||||
survey_spec = self.reverse('api:workflow_job_template_survey_spec', kwargs={'pk': obj.pk}),
|
||||
copy = self.reverse('api:workflow_job_template_copy', kwargs={'pk': obj.pk}),
|
||||
))
|
||||
if self.version > 1:
|
||||
res['copy'] = self.reverse('api:workflow_job_template_copy', kwargs={'pk': obj.pk})
|
||||
if obj.organization:
|
||||
res['organization'] = self.reverse('api:organization_detail', kwargs={'pk': obj.organization.pk})
|
||||
return res
|
||||
@@ -4401,17 +3998,16 @@ class JobLaunchSerializer(BaseSerializer):
|
||||
name=getattrd(obj, '%s.name' % field_name, None),
|
||||
id=getattrd(obj, '%s.pk' % field_name, None))
|
||||
elif field_name == 'credentials':
|
||||
if self.version > 1:
|
||||
for cred in obj.credentials.all():
|
||||
cred_dict = dict(
|
||||
id=cred.id,
|
||||
name=cred.name,
|
||||
credential_type=cred.credential_type.pk,
|
||||
passwords_needed=cred.passwords_needed
|
||||
)
|
||||
if cred.credential_type.managed_by_tower and 'vault_id' in cred.credential_type.defined_fields:
|
||||
cred_dict['vault_id'] = cred.get_input('vault_id', default=None)
|
||||
defaults_dict.setdefault(field_name, []).append(cred_dict)
|
||||
for cred in obj.credentials.all():
|
||||
cred_dict = dict(
|
||||
id=cred.id,
|
||||
name=cred.name,
|
||||
credential_type=cred.credential_type.pk,
|
||||
passwords_needed=cred.passwords_needed
|
||||
)
|
||||
if cred.credential_type.managed_by_tower and 'vault_id' in cred.credential_type.defined_fields:
|
||||
cred_dict['vault_id'] = cred.get_input('vault_id', default=None)
|
||||
defaults_dict.setdefault(field_name, []).append(cred_dict)
|
||||
else:
|
||||
defaults_dict[field_name] = getattr(obj, field_name)
|
||||
return defaults_dict
|
||||
@@ -4584,9 +4180,8 @@ class NotificationTemplateSerializer(BaseSerializer):
|
||||
res.update(dict(
|
||||
test = self.reverse('api:notification_template_test', kwargs={'pk': obj.pk}),
|
||||
notifications = self.reverse('api:notification_template_notification_list', kwargs={'pk': obj.pk}),
|
||||
copy = self.reverse('api:notification_template_copy', kwargs={'pk': obj.pk}),
|
||||
))
|
||||
if self.version > 1:
|
||||
res['copy'] = self.reverse('api:notification_template_copy', kwargs={'pk': obj.pk})
|
||||
if obj.organization:
|
||||
res['organization'] = self.reverse('api:organization_detail', kwargs={'pk': obj.organization.pk})
|
||||
return res
|
||||
|
||||
@@ -8,15 +8,15 @@ job template.
|
||||
|
||||
For example, using curl:
|
||||
|
||||
curl -H "Content-Type: application/json" -d '{"host_config_key": "HOST_CONFIG_KEY"}' http://server/api/v1/job_templates/N/callback/
|
||||
curl -H "Content-Type: application/json" -d '{"host_config_key": "HOST_CONFIG_KEY"}' http://server/api/v2/job_templates/N/callback/
|
||||
|
||||
Or using wget:
|
||||
|
||||
wget -O /dev/null --post-data='{"host_config_key": "HOST_CONFIG_KEY"}' --header=Content-Type:application/json http://server/api/v1/job_templates/N/callback/
|
||||
wget -O /dev/null --post-data='{"host_config_key": "HOST_CONFIG_KEY"}' --header=Content-Type:application/json http://server/api/v2/job_templates/N/callback/
|
||||
|
||||
You may also pass `extra_vars` to the callback:
|
||||
|
||||
curl -H "Content-Type: application/json" -d '{"host_config_key": "HOST_CONFIG_KEY", "extra_vars": {"key": "value"}}' http://server/api/v1/job_templates/N/callback/
|
||||
curl -H "Content-Type: application/json" -d '{"host_config_key": "HOST_CONFIG_KEY", "extra_vars": {"key": "value"}}' http://server/api/v2/job_templates/N/callback/
|
||||
|
||||
The response will return status 202 if the request is valid, 403 for an
|
||||
invalid host config key, or 400 if the host cannot be determined from the
|
||||
@@ -30,7 +30,7 @@ A GET request may be used to verify that the correct host will be selected.
|
||||
This request must authenticate as a valid user with permission to edit the
|
||||
job template. For example:
|
||||
|
||||
curl http://user:password@server/api/v1/job_templates/N/callback/
|
||||
curl http://user:password@server/api/v2/job_templates/N/callback/
|
||||
|
||||
The response will include the host config key as well as the host name(s)
|
||||
that would match the request:
|
||||
|
||||
@@ -6,4 +6,4 @@ One result should be returned containing the following fields:
|
||||
|
||||
{% include "api/_result_fields_common.md" %}
|
||||
|
||||
Use the primary URL for the user (/api/v1/users/N/) to modify the user.
|
||||
Use the primary URL for the user (/api/v2/users/N/) to modify the user.
|
||||
|
||||
@@ -6,7 +6,6 @@ from django.conf.urls import url
|
||||
from awx.api.views import (
|
||||
JobList,
|
||||
JobDetail,
|
||||
JobStart,
|
||||
JobCancel,
|
||||
JobRelaunch,
|
||||
JobCreateSchedule,
|
||||
@@ -23,7 +22,6 @@ from awx.api.views import (
|
||||
urls = [
|
||||
url(r'^$', JobList.as_view(), name='job_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/$', JobDetail.as_view(), name='job_detail'),
|
||||
url(r'^(?P<pk>[0-9]+)/start/$', JobStart.as_view(), name='job_start'), # Todo: Remove In 3.3
|
||||
url(r'^(?P<pk>[0-9]+)/cancel/$', JobCancel.as_view(), name='job_cancel'),
|
||||
url(r'^(?P<pk>[0-9]+)/relaunch/$', JobRelaunch.as_view(), name='job_relaunch'),
|
||||
url(r'^(?P<pk>[0-9]+)/create_schedule/$', JobCreateSchedule.as_view(), name='job_create_schedule'),
|
||||
|
||||
@@ -11,10 +11,9 @@ from awx.api.generics import (
|
||||
)
|
||||
from awx.api.views import (
|
||||
ApiRootView,
|
||||
ApiV1RootView,
|
||||
ApiV2RootView,
|
||||
ApiV1PingView,
|
||||
ApiV1ConfigView,
|
||||
ApiV2PingView,
|
||||
ApiV2ConfigView,
|
||||
AuthView,
|
||||
UserMeList,
|
||||
DashboardView,
|
||||
@@ -74,10 +73,25 @@ from .oauth2 import urls as oauth2_urls
|
||||
from .oauth2_root import urls as oauth2_root_urls
|
||||
|
||||
|
||||
v1_urls = [
|
||||
url(r'^$', ApiV1RootView.as_view(), name='api_v1_root_view'),
|
||||
url(r'^ping/$', ApiV1PingView.as_view(), name='api_v1_ping_view'),
|
||||
url(r'^config/$', ApiV1ConfigView.as_view(), name='api_v1_config_view'),
|
||||
v2_urls = [
|
||||
url(r'^$', ApiV2RootView.as_view(), name='api_v2_root_view'),
|
||||
url(r'^credential_types/', include(credential_type_urls)),
|
||||
url(r'^credential_input_sources/', include(credential_input_source_urls)),
|
||||
url(r'^hosts/(?P<pk>[0-9]+)/ansible_facts/$', HostAnsibleFactsDetail.as_view(), name='host_ansible_facts_detail'),
|
||||
url(r'^jobs/(?P<pk>[0-9]+)/extra_credentials/$', JobExtraCredentialsList.as_view(), name='job_extra_credentials_list'),
|
||||
url(r'^jobs/(?P<pk>[0-9]+)/credentials/$', JobCredentialsList.as_view(), name='job_credentials_list'),
|
||||
url(r'^job_templates/(?P<pk>[0-9]+)/extra_credentials/$', JobTemplateExtraCredentialsList.as_view(), name='job_template_extra_credentials_list'),
|
||||
url(r'^job_templates/(?P<pk>[0-9]+)/credentials/$', JobTemplateCredentialsList.as_view(), name='job_template_credentials_list'),
|
||||
url(r'^schedules/preview/$', SchedulePreview.as_view(), name='schedule_rrule'),
|
||||
url(r'^schedules/zoneinfo/$', ScheduleZoneInfo.as_view(), name='schedule_zoneinfo'),
|
||||
url(r'^applications/$', OAuth2ApplicationList.as_view(), name='o_auth2_application_list'),
|
||||
url(r'^applications/(?P<pk>[0-9]+)/$', OAuth2ApplicationDetail.as_view(), name='o_auth2_application_detail'),
|
||||
url(r'^applications/(?P<pk>[0-9]+)/tokens/$', ApplicationOAuth2TokenList.as_view(), name='application_o_auth2_token_list'),
|
||||
url(r'^tokens/$', OAuth2TokenList.as_view(), name='o_auth2_token_list'),
|
||||
url(r'^', include(oauth2_urls)),
|
||||
url(r'^metrics/$', MetricsView.as_view(), name='metrics_view'),
|
||||
url(r'^ping/$', ApiV2PingView.as_view(), name='api_v2_ping_view'),
|
||||
url(r'^config/$', ApiV2ConfigView.as_view(), name='api_v2_config_view'),
|
||||
url(r'^auth/$', AuthView.as_view()),
|
||||
url(r'^me/$', UserMeList.as_view(), name='user_me_list'),
|
||||
url(r'^dashboard/$', DashboardView.as_view(), name='dashboard_view'),
|
||||
@@ -119,30 +133,10 @@ v1_urls = [
|
||||
url(r'^activity_stream/', include(activity_stream_urls)),
|
||||
]
|
||||
|
||||
v2_urls = [
|
||||
url(r'^$', ApiV2RootView.as_view(), name='api_v2_root_view'),
|
||||
url(r'^credential_types/', include(credential_type_urls)),
|
||||
url(r'^credential_input_sources/', include(credential_input_source_urls)),
|
||||
url(r'^hosts/(?P<pk>[0-9]+)/ansible_facts/$', HostAnsibleFactsDetail.as_view(), name='host_ansible_facts_detail'),
|
||||
url(r'^jobs/(?P<pk>[0-9]+)/extra_credentials/$', JobExtraCredentialsList.as_view(), name='job_extra_credentials_list'),
|
||||
url(r'^jobs/(?P<pk>[0-9]+)/credentials/$', JobCredentialsList.as_view(), name='job_credentials_list'),
|
||||
url(r'^job_templates/(?P<pk>[0-9]+)/extra_credentials/$', JobTemplateExtraCredentialsList.as_view(), name='job_template_extra_credentials_list'),
|
||||
url(r'^job_templates/(?P<pk>[0-9]+)/credentials/$', JobTemplateCredentialsList.as_view(), name='job_template_credentials_list'),
|
||||
url(r'^schedules/preview/$', SchedulePreview.as_view(), name='schedule_rrule'),
|
||||
url(r'^schedules/zoneinfo/$', ScheduleZoneInfo.as_view(), name='schedule_zoneinfo'),
|
||||
url(r'^applications/$', OAuth2ApplicationList.as_view(), name='o_auth2_application_list'),
|
||||
url(r'^applications/(?P<pk>[0-9]+)/$', OAuth2ApplicationDetail.as_view(), name='o_auth2_application_detail'),
|
||||
url(r'^applications/(?P<pk>[0-9]+)/tokens/$', ApplicationOAuth2TokenList.as_view(), name='application_o_auth2_token_list'),
|
||||
url(r'^tokens/$', OAuth2TokenList.as_view(), name='o_auth2_token_list'),
|
||||
url(r'^', include(oauth2_urls)),
|
||||
url(r'^metrics/$', MetricsView.as_view(), name='metrics_view'),
|
||||
]
|
||||
|
||||
app_name = 'api'
|
||||
urlpatterns = [
|
||||
url(r'^$', ApiRootView.as_view(), name='api_root_view'),
|
||||
url(r'^(?P<version>(v2))/', include(v2_urls)),
|
||||
url(r'^(?P<version>(v1|v2))/', include(v1_urls)),
|
||||
url(r'^login/$', LoggedLoginView.as_view(
|
||||
template_name='rest_framework/login.html',
|
||||
extra_context={'inside_login_context': True}
|
||||
|
||||
@@ -27,19 +27,6 @@ def drf_reverse(viewname, args=None, kwargs=None, request=None, format=None, **e
|
||||
return url
|
||||
|
||||
|
||||
def get_request_version(request):
|
||||
"""
|
||||
The API version of a request as an integer i.e., 1 or 2
|
||||
"""
|
||||
version = settings.REST_FRAMEWORK['DEFAULT_VERSION']
|
||||
if request and hasattr(request, 'version'):
|
||||
version = request.version
|
||||
if version is None:
|
||||
# For requests to /api/
|
||||
return None
|
||||
return int(version.lstrip('v'))
|
||||
|
||||
|
||||
def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra):
|
||||
if request is None or getattr(request, 'version', None) is None:
|
||||
# We need the "current request" to determine the correct version to
|
||||
|
||||
@@ -62,7 +62,6 @@ from wsgiref.util import FileWrapper
|
||||
# AWX
|
||||
from awx.main.tasks import send_notifications, update_inventory_computed_fields
|
||||
from awx.main.access import get_user_queryset, HostAccess
|
||||
from awx.api.filters import V1CredentialFilterBackend
|
||||
from awx.api.generics import (
|
||||
APIView, BaseUsersList, CopyAPIView, DeleteLastUnattachLabelMixin,
|
||||
GenericAPIView, ListAPIView, ListCreateAPIView,
|
||||
@@ -72,7 +71,7 @@ from awx.api.generics import (
|
||||
SubListCreateAPIView, SubListCreateAttachDetachAPIView,
|
||||
SubListDestroyAPIView, get_view_name
|
||||
)
|
||||
from awx.api.versioning import reverse, get_request_version
|
||||
from awx.api.versioning import reverse
|
||||
from awx.conf.license import get_license
|
||||
from awx.main import models
|
||||
from awx.main.utils import (
|
||||
@@ -96,7 +95,7 @@ from awx.api.permissions import (
|
||||
)
|
||||
from awx.api import renderers
|
||||
from awx.api import serializers
|
||||
from awx.api.metadata import RoleMetadata, JobTypeMetadata
|
||||
from awx.api.metadata import RoleMetadata
|
||||
from awx.main.constants import ACTIVE_STATES
|
||||
from awx.main.scheduler.dag_workflow import WorkflowDAG
|
||||
from awx.api.views.mixin import (
|
||||
@@ -143,10 +142,9 @@ from awx.api.views.root import ( # noqa
|
||||
ApiRootView,
|
||||
ApiOAuthAuthorizationRootView,
|
||||
ApiVersionRootView,
|
||||
ApiV1RootView,
|
||||
ApiV2RootView,
|
||||
ApiV1PingView,
|
||||
ApiV1ConfigView,
|
||||
ApiV2PingView,
|
||||
ApiV2ConfigView,
|
||||
)
|
||||
|
||||
|
||||
@@ -1246,22 +1244,10 @@ class CredentialTypeActivityStreamList(SubListAPIView):
|
||||
search_fields = ('changes',)
|
||||
|
||||
|
||||
# remove in 3.3
|
||||
class CredentialViewMixin(object):
|
||||
|
||||
@property
|
||||
def related_search_fields(self):
|
||||
ret = super(CredentialViewMixin, self).related_search_fields
|
||||
if get_request_version(self.request) == 1 and 'credential_type__search' in ret:
|
||||
ret.remove('credential_type__search')
|
||||
return ret
|
||||
|
||||
|
||||
class CredentialList(CredentialViewMixin, ListCreateAPIView):
|
||||
class CredentialList(ListCreateAPIView):
|
||||
|
||||
model = models.Credential
|
||||
serializer_class = serializers.CredentialSerializerCreate
|
||||
filter_backends = ListCreateAPIView.filter_backends + [V1CredentialFilterBackend]
|
||||
|
||||
|
||||
class CredentialOwnerUsersList(SubListAPIView):
|
||||
@@ -1289,13 +1275,12 @@ class CredentialOwnerTeamsList(SubListAPIView):
|
||||
return self.model.objects.filter(pk__in=teams)
|
||||
|
||||
|
||||
class UserCredentialsList(CredentialViewMixin, SubListCreateAPIView):
|
||||
class UserCredentialsList(SubListCreateAPIView):
|
||||
|
||||
model = models.Credential
|
||||
serializer_class = serializers.UserCredentialSerializerCreate
|
||||
parent_model = models.User
|
||||
parent_key = 'user'
|
||||
filter_backends = SubListCreateAPIView.filter_backends + [V1CredentialFilterBackend]
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.get_parent_object()
|
||||
@@ -1306,13 +1291,12 @@ class UserCredentialsList(CredentialViewMixin, SubListCreateAPIView):
|
||||
return user_creds & visible_creds
|
||||
|
||||
|
||||
class TeamCredentialsList(CredentialViewMixin, SubListCreateAPIView):
|
||||
class TeamCredentialsList(SubListCreateAPIView):
|
||||
|
||||
model = models.Credential
|
||||
serializer_class = serializers.TeamCredentialSerializerCreate
|
||||
parent_model = models.Team
|
||||
parent_key = 'team'
|
||||
filter_backends = SubListCreateAPIView.filter_backends + [V1CredentialFilterBackend]
|
||||
|
||||
def get_queryset(self):
|
||||
team = self.get_parent_object()
|
||||
@@ -1323,13 +1307,12 @@ class TeamCredentialsList(CredentialViewMixin, SubListCreateAPIView):
|
||||
return (team_creds & visible_creds).distinct()
|
||||
|
||||
|
||||
class OrganizationCredentialList(CredentialViewMixin, SubListCreateAPIView):
|
||||
class OrganizationCredentialList(SubListCreateAPIView):
|
||||
|
||||
model = models.Credential
|
||||
serializer_class = serializers.OrganizationCredentialSerializerCreate
|
||||
parent_model = models.Organization
|
||||
parent_key = 'organization'
|
||||
filter_backends = SubListCreateAPIView.filter_backends + [V1CredentialFilterBackend]
|
||||
|
||||
def get_queryset(self):
|
||||
organization = self.get_parent_object()
|
||||
@@ -1348,7 +1331,6 @@ class CredentialDetail(RetrieveUpdateDestroyAPIView):
|
||||
|
||||
model = models.Credential
|
||||
serializer_class = serializers.CredentialSerializer
|
||||
filter_backends = RetrieveUpdateDestroyAPIView.filter_backends + [V1CredentialFilterBackend]
|
||||
|
||||
|
||||
class CredentialActivityStreamList(SubListAPIView):
|
||||
@@ -1754,10 +1736,10 @@ class EnforceParentRelationshipMixin(object):
|
||||
* Tower uses a shallow (2-deep only) url pattern. For example:
|
||||
|
||||
When an object hangs off of a parent object you would have the url of the
|
||||
form /api/v1/parent_model/34/child_model. If you then wanted a child of the
|
||||
child model you would NOT do /api/v1/parent_model/34/child_model/87/child_child_model
|
||||
Instead, you would access the child_child_model via /api/v1/child_child_model/87/
|
||||
and you would create child_child_model's off of /api/v1/child_model/87/child_child_model_set
|
||||
form /api/v2/parent_model/34/child_model. If you then wanted a child of the
|
||||
child model you would NOT do /api/v2/parent_model/34/child_model/87/child_child_model
|
||||
Instead, you would access the child_child_model via /api/v2/child_child_model/87/
|
||||
and you would create child_child_model's off of /api/v2/child_model/87/child_child_model_set
|
||||
Now, when creating child_child_model related to child_model you still want to
|
||||
link child_child_model to parent_model. That's what this class is for
|
||||
'''
|
||||
@@ -1899,11 +1881,6 @@ class GroupDetail(RelatedJobsPreventDeleteMixin, ControlledByScmMixin, RetrieveU
|
||||
obj = self.get_object()
|
||||
if not request.user.can_access(self.model, 'delete', obj):
|
||||
raise PermissionDenied()
|
||||
if get_request_version(request) == 1: # TODO: deletion of automatic inventory_source, remove in 3.3
|
||||
try:
|
||||
obj.deprecated_inventory_source.delete()
|
||||
except models.Group.deprecated_inventory_source.RelatedObjectDoesNotExist:
|
||||
pass
|
||||
obj.delete_recursive()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -2093,13 +2070,6 @@ class InventorySourceList(ListCreateAPIView):
|
||||
serializer_class = serializers.InventorySourceSerializer
|
||||
always_allow_superuser = False
|
||||
|
||||
@property
|
||||
def allowed_methods(self):
|
||||
methods = super(InventorySourceList, self).allowed_methods
|
||||
if get_request_version(getattr(self, 'request', None)) == 1:
|
||||
methods.remove('POST')
|
||||
return methods
|
||||
|
||||
|
||||
class InventorySourceDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView):
|
||||
|
||||
@@ -2290,7 +2260,6 @@ class InventoryUpdateNotificationsList(SubListAPIView):
|
||||
class JobTemplateList(ListCreateAPIView):
|
||||
|
||||
model = models.JobTemplate
|
||||
metadata_class = JobTypeMetadata
|
||||
serializer_class = serializers.JobTemplateSerializer
|
||||
always_allow_superuser = False
|
||||
|
||||
@@ -2305,7 +2274,6 @@ class JobTemplateList(ListCreateAPIView):
|
||||
class JobTemplateDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView):
|
||||
|
||||
model = models.JobTemplate
|
||||
metadata_class = JobTypeMetadata
|
||||
serializer_class = serializers.JobTemplateSerializer
|
||||
always_allow_superuser = False
|
||||
|
||||
@@ -2314,7 +2282,6 @@ class JobTemplateLaunch(RetrieveAPIView):
|
||||
|
||||
model = models.JobTemplate
|
||||
obj_permission_type = 'start'
|
||||
metadata_class = JobTypeMetadata
|
||||
serializer_class = serializers.JobLaunchSerializer
|
||||
always_allow_superuser = False
|
||||
|
||||
@@ -2358,65 +2325,44 @@ class JobTemplateLaunch(RetrieveAPIView):
|
||||
ignored_fields = {}
|
||||
modern_data = data.copy()
|
||||
|
||||
for fd in ('credential', 'vault_credential', 'inventory'):
|
||||
id_fd = '{}_id'.format(fd)
|
||||
if fd not in modern_data and id_fd in modern_data:
|
||||
modern_data[fd] = modern_data[id_fd]
|
||||
|
||||
# This block causes `extra_credentials` to _always_ raise error if
|
||||
# the launch endpoint if we're accessing `/api/v1/`
|
||||
if get_request_version(self.request) == 1 and 'extra_credentials' in modern_data:
|
||||
raise ParseError({"extra_credentials": _(
|
||||
"Field is not allowed for use with v1 API."
|
||||
)})
|
||||
id_fd = '{}_id'.format('inventory')
|
||||
if 'inventory' not in modern_data and id_fd in modern_data:
|
||||
modern_data['inventory'] = modern_data[id_fd]
|
||||
|
||||
# Automatically convert legacy launch credential arguments into a list of `.credentials`
|
||||
if 'credentials' in modern_data and (
|
||||
'credential' in modern_data or
|
||||
'vault_credential' in modern_data or
|
||||
'extra_credentials' in modern_data
|
||||
):
|
||||
if 'credentials' in modern_data and 'extra_credentials' in modern_data:
|
||||
raise ParseError({"error": _(
|
||||
"'credentials' cannot be used in combination with 'credential', 'vault_credential', or 'extra_credentials'."
|
||||
"'credentials' cannot be used in combination with 'extra_credentials'."
|
||||
)})
|
||||
|
||||
if (
|
||||
'credential' in modern_data or
|
||||
'vault_credential' in modern_data or
|
||||
'extra_credentials' in modern_data
|
||||
):
|
||||
if 'extra_credentials' in modern_data:
|
||||
# make a list of the current credentials
|
||||
existing_credentials = obj.credentials.all()
|
||||
template_credentials = list(existing_credentials) # save copy of existing
|
||||
new_credentials = []
|
||||
for key, conditional, _type, type_repr in (
|
||||
('credential', lambda cred: cred.credential_type.kind != 'ssh', int, 'pk value'),
|
||||
('vault_credential', lambda cred: cred.credential_type.kind != 'vault', int, 'pk value'),
|
||||
('extra_credentials', lambda cred: cred.credential_type.kind not in ('cloud', 'net'), Iterable, 'a list')
|
||||
):
|
||||
if key in modern_data:
|
||||
# if a specific deprecated key is specified, remove all
|
||||
# credentials of _that_ type from the list of current
|
||||
# credentials
|
||||
existing_credentials = filter(conditional, existing_credentials)
|
||||
prompted_value = modern_data.pop(key)
|
||||
if 'extra_credentials' in modern_data:
|
||||
existing_credentials = [
|
||||
cred for cred in existing_credentials
|
||||
if cred.credential_type.kind not in ('cloud', 'net')
|
||||
]
|
||||
prompted_value = modern_data.pop('extra_credentials')
|
||||
|
||||
# validate type, since these are not covered by a serializer
|
||||
if not isinstance(prompted_value, _type):
|
||||
msg = _(
|
||||
"Incorrect type. Expected {}, received {}."
|
||||
).format(type_repr, prompted_value.__class__.__name__)
|
||||
raise ParseError({key: [msg], 'credentials': [msg]})
|
||||
# validate type, since these are not covered by a serializer
|
||||
if not isinstance(prompted_value, Iterable):
|
||||
msg = _(
|
||||
"Incorrect type. Expected a list received {}."
|
||||
).format(prompted_value.__class__.__name__)
|
||||
raise ParseError({'extra_credentials': [msg], 'credentials': [msg]})
|
||||
|
||||
# add the deprecated credential specified in the request
|
||||
if not isinstance(prompted_value, Iterable) or isinstance(prompted_value, str):
|
||||
prompted_value = [prompted_value]
|
||||
# add the deprecated credential specified in the request
|
||||
if not isinstance(prompted_value, Iterable) or isinstance(prompted_value, str):
|
||||
prompted_value = [prompted_value]
|
||||
|
||||
# If user gave extra_credentials, special case to use exactly
|
||||
# the given list without merging with JT credentials
|
||||
if key == 'extra_credentials' and prompted_value:
|
||||
obj._deprecated_credential_launch = True # signal to not merge credentials
|
||||
new_credentials.extend(prompted_value)
|
||||
# If user gave extra_credentials, special case to use exactly
|
||||
# the given list without merging with JT credentials
|
||||
if prompted_value:
|
||||
obj._deprecated_credential_launch = True # signal to not merge credentials
|
||||
new_credentials.extend(prompted_value)
|
||||
|
||||
# combine the list of "new" and the filtered list of "old"
|
||||
new_credentials.extend([cred.pk for cred in existing_credentials])
|
||||
@@ -2926,7 +2872,7 @@ class JobTemplateCallback(GenericAPIView):
|
||||
return Response(status=status.HTTP_201_CREATED, headers=headers)
|
||||
|
||||
|
||||
class JobTemplateJobsList(SubListCreateAPIView):
|
||||
class JobTemplateJobsList(SubListAPIView):
|
||||
|
||||
model = models.Job
|
||||
serializer_class = serializers.JobListSerializer
|
||||
@@ -2934,13 +2880,6 @@ class JobTemplateJobsList(SubListCreateAPIView):
|
||||
relationship = 'jobs'
|
||||
parent_key = 'job_template'
|
||||
|
||||
@property
|
||||
def allowed_methods(self):
|
||||
methods = super(JobTemplateJobsList, self).allowed_methods
|
||||
if get_request_version(getattr(self, 'request', None)) > 1:
|
||||
methods.remove('POST')
|
||||
return methods
|
||||
|
||||
|
||||
class JobTemplateSliceWorkflowJobsList(SubListCreateAPIView):
|
||||
|
||||
@@ -3135,8 +3074,6 @@ class WorkflowJobTemplateCopy(CopyAPIView):
|
||||
copy_return_serializer_class = serializers.WorkflowJobTemplateSerializer
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if get_request_version(request) < 2:
|
||||
return self.v1_not_allowed()
|
||||
obj = self.get_object()
|
||||
if not request.user.can_access(obj.__class__, 'read', obj):
|
||||
raise PermissionDenied()
|
||||
@@ -3493,56 +3430,17 @@ class SystemJobTemplateNotificationTemplatesSuccessList(SubListCreateAttachDetac
|
||||
relationship = 'notification_templates_success'
|
||||
|
||||
|
||||
class JobList(ListCreateAPIView):
|
||||
class JobList(ListAPIView):
|
||||
|
||||
model = models.Job
|
||||
metadata_class = JobTypeMetadata
|
||||
serializer_class = serializers.JobListSerializer
|
||||
|
||||
@property
|
||||
def allowed_methods(self):
|
||||
methods = super(JobList, self).allowed_methods
|
||||
if get_request_version(getattr(self, 'request', None)) > 1:
|
||||
methods.remove('POST')
|
||||
return methods
|
||||
|
||||
# NOTE: Remove in 3.3, switch ListCreateAPIView to ListAPIView
|
||||
def post(self, request, *args, **kwargs):
|
||||
if get_request_version(self.request) > 1:
|
||||
return Response({"error": _("POST not allowed for Job launching in version 2 of the api")},
|
||||
status=status.HTTP_405_METHOD_NOT_ALLOWED)
|
||||
return super(JobList, self).post(request, *args, **kwargs)
|
||||
|
||||
|
||||
class JobDetail(UnifiedJobDeletionMixin, RetrieveUpdateDestroyAPIView):
|
||||
class JobDetail(UnifiedJobDeletionMixin, RetrieveDestroyAPIView):
|
||||
|
||||
model = models.Job
|
||||
metadata_class = JobTypeMetadata
|
||||
serializer_class = serializers.JobDetailSerializer
|
||||
|
||||
# NOTE: When removing the V1 API in 3.4, delete the following four methods,
|
||||
# and let this class inherit from RetrieveDestroyAPIView instead of
|
||||
# RetrieveUpdateDestroyAPIView.
|
||||
@property
|
||||
def allowed_methods(self):
|
||||
methods = super(JobDetail, self).allowed_methods
|
||||
if get_request_version(getattr(self, 'request', None)) > 1:
|
||||
methods.remove('PUT')
|
||||
methods.remove('PATCH')
|
||||
return methods
|
||||
|
||||
def put(self, request, *args, **kwargs):
|
||||
if get_request_version(self.request) > 1:
|
||||
return Response({"error": _("PUT not allowed for Job Details in version 2 of the API")},
|
||||
status=status.HTTP_405_METHOD_NOT_ALLOWED)
|
||||
return super(JobDetail, self).put(request, *args, **kwargs)
|
||||
|
||||
def patch(self, request, *args, **kwargs):
|
||||
if get_request_version(self.request) > 1:
|
||||
return Response({"error": _("PUT not allowed for Job Details in version 2 of the API")},
|
||||
status=status.HTTP_405_METHOD_NOT_ALLOWED)
|
||||
return super(JobDetail, self).patch(request, *args, **kwargs)
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
obj = self.get_object()
|
||||
# Only allow changes (PUT/PATCH) when job status is "new".
|
||||
@@ -3591,44 +3489,6 @@ class JobActivityStreamList(SubListAPIView):
|
||||
search_fields = ('changes',)
|
||||
|
||||
|
||||
# TODO: remove endpoint in 3.3
|
||||
class JobStart(GenericAPIView):
|
||||
|
||||
model = models.Job
|
||||
obj_permission_type = 'start'
|
||||
serializer_class = serializers.EmptySerializer
|
||||
deprecated = True
|
||||
|
||||
def v2_not_allowed(self):
|
||||
return Response({'detail': 'Action only possible through v1 API.'},
|
||||
status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if get_request_version(request) > 1:
|
||||
return self.v2_not_allowed()
|
||||
obj = self.get_object()
|
||||
data = dict(
|
||||
can_start=obj.can_start,
|
||||
)
|
||||
if obj.can_start:
|
||||
data['passwords_needed_to_start'] = obj.passwords_needed_to_start
|
||||
return Response(data)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if get_request_version(request) > 1:
|
||||
return self.v2_not_allowed()
|
||||
obj = self.get_object()
|
||||
if obj.can_start:
|
||||
result = obj.signal_start(**request.data)
|
||||
if not result:
|
||||
data = dict(passwords_needed_to_start=obj.passwords_needed_to_start)
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
return Response(status=status.HTTP_202_ACCEPTED)
|
||||
else:
|
||||
return self.http_method_not_allowed(request, *args, **kwargs)
|
||||
|
||||
|
||||
class JobCancel(RetrieveAPIView):
|
||||
|
||||
model = models.Job
|
||||
|
||||
@@ -44,7 +44,6 @@ from awx.api.serializers import (
|
||||
InstanceGroupSerializer,
|
||||
InventoryUpdateEventSerializer,
|
||||
CustomInventoryScriptSerializer,
|
||||
InventoryDetailSerializer,
|
||||
JobTemplateSerializer,
|
||||
)
|
||||
from awx.api.views.mixin import (
|
||||
@@ -119,7 +118,7 @@ class InventoryList(ListCreateAPIView):
|
||||
class InventoryDetail(RelatedJobsPreventDeleteMixin, ControlledByScmMixin, RetrieveUpdateDestroyAPIView):
|
||||
|
||||
model = Inventory
|
||||
serializer_class = InventoryDetailSerializer
|
||||
serializer_class = InventorySerializer
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
obj = self.get_object()
|
||||
|
||||
@@ -25,7 +25,7 @@ from awx.main.utils import (
|
||||
get_custom_venv_choices,
|
||||
to_python_boolean,
|
||||
)
|
||||
from awx.api.versioning import reverse, get_request_version, drf_reverse
|
||||
from awx.api.versioning import reverse, drf_reverse
|
||||
from awx.conf.license import get_license
|
||||
from awx.main.constants import PRIVILEGE_ESCALATION_METHODS
|
||||
from awx.main.models import (
|
||||
@@ -50,12 +50,11 @@ class ApiRootView(APIView):
|
||||
def get(self, request, format=None):
|
||||
''' List supported API versions '''
|
||||
|
||||
v1 = reverse('api:api_v1_root_view', kwargs={'version': 'v1'})
|
||||
v2 = reverse('api:api_v2_root_view', kwargs={'version': 'v2'})
|
||||
data = OrderedDict()
|
||||
data['description'] = _('AWX REST API')
|
||||
data['current_version'] = v2
|
||||
data['available_versions'] = dict(v1 = v1, v2 = v2)
|
||||
data['available_versions'] = dict(v2 = v2)
|
||||
data['oauth2'] = drf_reverse('api:oauth_authorization_root_view')
|
||||
data['custom_logo'] = settings.CUSTOM_LOGO
|
||||
data['custom_login_info'] = settings.CUSTOM_LOGIN_INFO
|
||||
@@ -85,10 +84,10 @@ class ApiVersionRootView(APIView):
|
||||
def get(self, request, format=None):
|
||||
''' List top level resources '''
|
||||
data = OrderedDict()
|
||||
data['ping'] = reverse('api:api_v1_ping_view', request=request)
|
||||
data['ping'] = reverse('api:api_v2_ping_view', request=request)
|
||||
data['instances'] = reverse('api:instance_list', request=request)
|
||||
data['instance_groups'] = reverse('api:instance_group_list', request=request)
|
||||
data['config'] = reverse('api:api_v1_config_view', request=request)
|
||||
data['config'] = reverse('api:api_v2_config_view', request=request)
|
||||
data['settings'] = reverse('api:setting_category_list', request=request)
|
||||
data['me'] = reverse('api:user_me_list', request=request)
|
||||
data['dashboard'] = reverse('api:dashboard_view', request=request)
|
||||
@@ -98,12 +97,11 @@ class ApiVersionRootView(APIView):
|
||||
data['project_updates'] = reverse('api:project_update_list', request=request)
|
||||
data['teams'] = reverse('api:team_list', request=request)
|
||||
data['credentials'] = reverse('api:credential_list', request=request)
|
||||
if get_request_version(request) > 1:
|
||||
data['credential_types'] = reverse('api:credential_type_list', request=request)
|
||||
data['credential_input_sources'] = reverse('api:credential_input_source_list', request=request)
|
||||
data['applications'] = reverse('api:o_auth2_application_list', request=request)
|
||||
data['tokens'] = reverse('api:o_auth2_token_list', request=request)
|
||||
data['metrics'] = reverse('api:metrics_view', request=request)
|
||||
data['credential_types'] = reverse('api:credential_type_list', request=request)
|
||||
data['credential_input_sources'] = reverse('api:credential_input_source_list', request=request)
|
||||
data['applications'] = reverse('api:o_auth2_application_list', request=request)
|
||||
data['tokens'] = reverse('api:o_auth2_token_list', request=request)
|
||||
data['metrics'] = reverse('api:metrics_view', request=request)
|
||||
data['inventory'] = reverse('api:inventory_list', request=request)
|
||||
data['inventory_scripts'] = reverse('api:inventory_script_list', request=request)
|
||||
data['inventory_sources'] = reverse('api:inventory_source_list', request=request)
|
||||
@@ -131,15 +129,11 @@ class ApiVersionRootView(APIView):
|
||||
return Response(data)
|
||||
|
||||
|
||||
class ApiV1RootView(ApiVersionRootView):
|
||||
view_name = _('Version 1')
|
||||
|
||||
|
||||
class ApiV2RootView(ApiVersionRootView):
|
||||
view_name = _('Version 2')
|
||||
|
||||
|
||||
class ApiV1PingView(APIView):
|
||||
class ApiV2PingView(APIView):
|
||||
"""A simple view that reports very basic information about this
|
||||
instance, which is acceptable to be public information.
|
||||
"""
|
||||
@@ -174,14 +168,14 @@ class ApiV1PingView(APIView):
|
||||
return Response(response)
|
||||
|
||||
|
||||
class ApiV1ConfigView(APIView):
|
||||
class ApiV2ConfigView(APIView):
|
||||
|
||||
permission_classes = (IsAuthenticated,)
|
||||
view_name = _('Configuration')
|
||||
swagger_topic = 'System Configuration'
|
||||
|
||||
def check_permissions(self, request):
|
||||
super(ApiV1ConfigView, self).check_permissions(request)
|
||||
super(ApiV2ConfigView, self).check_permissions(request)
|
||||
if not request.user.is_superuser and request.method.lower() not in {'options', 'head', 'get'}:
|
||||
self.permission_denied(request) # Raises PermissionDenied exception.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user