Merge pull request #3413 from ryanpetrello/bye-bye-v1

remove /api/v1 and deprecated credential fields

Reviewed-by: https://github.com/softwarefactory-project-zuul[bot]
This commit is contained in:
softwarefactory-project-zuul[bot]
2019-06-10 17:31:28 +00:00
committed by GitHub
50 changed files with 349 additions and 2281 deletions

View File

@@ -24,20 +24,6 @@ from rest_framework.filters import BaseFilterBackend
# AWX # AWX
from awx.main.utils import get_type_for_model, to_python_boolean 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.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): class TypeFilterBackend(BaseFilterBackend):
@@ -292,39 +278,6 @@ class FieldLookupBackend(BaseFilterBackend):
key = key[5:] key = key[5:]
q_not = True 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. # Convert value(s) to python and add to the appropriate list.
for value in values: for value in values:
if q_int: if q_int:

View File

@@ -34,7 +34,7 @@ from rest_framework.negotiation import DefaultContentNegotiation
# AWX # AWX
from awx.api.filters import FieldLookupBackend from awx.api.filters import FieldLookupBackend
from awx.main.models import ( from awx.main.models import (
UnifiedJob, UnifiedJobTemplate, User, Role UnifiedJob, UnifiedJobTemplate, User, Role, Credential
) )
from awx.main.access import access_registry from awx.main.access import access_registry
from awx.main.utils import ( 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.main.utils.db import get_all_field_names
from awx.api.serializers import ResourceAccessListElementSerializer, CopySerializer, UserSerializer 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 from awx.api.metadata import SublistAttachDetatchMetadata, Metadata
__all__ = ['APIView', 'GenericAPIView', 'ListAPIView', 'SimpleListAPIView', __all__ = ['APIView', 'GenericAPIView', 'ListAPIView', 'SimpleListAPIView',
@@ -288,12 +288,6 @@ class APIView(views.APIView):
template_list.append('api/%s.md' % template_basename) template_list.append('api/%s.md' % template_basename)
context = self.get_description_context() 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) description = render_to_string(template_list, context)
if context.get('deprecated') and context.get('swagger_method') is None: if context.get('deprecated') and context.get('swagger_method') is None:
# render deprecation messages at the very top # render deprecation messages at the very top
@@ -842,10 +836,6 @@ class CopyAPIView(GenericAPIView):
new_in_330 = True new_in_330 = True
new_in_api_v2 = 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): def _get_copy_return_serializer(self, *args, **kwargs):
if not self.copy_return_serializer_class: if not self.copy_return_serializer_class:
return self.get_serializer(*args, **kwargs) return self.get_serializer(*args, **kwargs)
@@ -859,15 +849,15 @@ class CopyAPIView(GenericAPIView):
def _decrypt_model_field_if_needed(obj, field_name, field_val): def _decrypt_model_field_if_needed(obj, field_name, field_val):
if field_name in getattr(type(obj), 'REENCRYPTION_BLACKLIST_AT_COPY', []): if field_name in getattr(type(obj), 'REENCRYPTION_BLACKLIST_AT_COPY', []):
return field_val 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: for sub_field in field_val:
if isinstance(sub_field, str) \ if isinstance(sub_field, str) \
and isinstance(field_val[sub_field], str): and isinstance(field_val[sub_field], str):
try: field_val[sub_field] = decrypt_field(obj, field_name, sub_field)
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)
elif isinstance(field_val, str): elif isinstance(field_val, str):
try: try:
field_val = decrypt_field(obj, field_name) field_val = decrypt_field(obj, field_name)
@@ -952,8 +942,6 @@ class CopyAPIView(GenericAPIView):
return ret return ret
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
if get_request_version(request) < 2:
return self.v1_not_allowed()
obj = self.get_object() obj = self.get_object()
if not request.user.can_access(obj.__class__, 'read', obj): if not request.user.can_access(obj.__class__, 'read', obj):
raise PermissionDenied() raise PermissionDenied()
@@ -968,8 +956,6 @@ class CopyAPIView(GenericAPIView):
return Response({'can_copy': can_copy}) return Response({'can_copy': can_copy})
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
if get_request_version(request) < 2:
return self.v1_not_allowed()
obj = self.get_object() obj = self.get_object()
create_kwargs = self._build_create_dict(obj) create_kwargs = self._build_create_dict(obj)
create_kwargs_check = {} create_kwargs_check = {}

View File

@@ -232,19 +232,6 @@ class RoleMetadata(Metadata):
return 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): class SublistAttachDetatchMetadata(Metadata):
def determine_actions(self, request, view): def determine_actions(self, request, view):

View File

@@ -54,7 +54,7 @@ from awx.main.models import (
OAuth2AccessToken, OAuth2Application, Organization, Project, OAuth2AccessToken, OAuth2Application, Organization, Project,
ProjectUpdate, ProjectUpdateEvent, RefreshToken, Role, Schedule, ProjectUpdate, ProjectUpdateEvent, RefreshToken, Role, Schedule,
SystemJob, SystemJobEvent, SystemJobTemplate, Team, UnifiedJob, SystemJob, SystemJobEvent, SystemJobTemplate, Team, UnifiedJob,
UnifiedJobTemplate, V1Credential, WorkflowJob, WorkflowJobNode, UnifiedJobTemplate, WorkflowJob, WorkflowJobNode,
WorkflowJobTemplate, WorkflowJobTemplateNode, StdoutMaxBytesExceeded WorkflowJobTemplate, WorkflowJobTemplateNode, StdoutMaxBytesExceeded
) )
from awx.main.models.base import VERBOSITY_CHOICES, NEW_JOB_TYPE_CHOICES 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.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, from awx.api.fields import (BooleanNullField, CharNullField, ChoiceNullField,
VerbatimField, DeprecatedCredentialField) VerbatimField, DeprecatedCredentialField)
@@ -113,7 +113,6 @@ SUMMARIZABLE_FK_FIELDS = {
'source_project': DEFAULT_SUMMARY_FIELDS + ('status', 'scm_type'), 'source_project': DEFAULT_SUMMARY_FIELDS + ('status', 'scm_type'),
'project_update': DEFAULT_SUMMARY_FIELDS + ('status', 'failed',), 'project_update': DEFAULT_SUMMARY_FIELDS + ('status', 'failed',),
'credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud', 'credential_type_id'), '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': DEFAULT_SUMMARY_FIELDS + ('status', 'failed', 'elapsed', 'type'),
'job_template': DEFAULT_SUMMARY_FIELDS, 'job_template': DEFAULT_SUMMARY_FIELDS,
'workflow_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 Returns a dictionary of the form
{ '<type>': reverse(<type detail>) } { '<type>': reverse(<type detail>) }
for example 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'): if content_object is None or not hasattr(content_object, 'get_absolute_url'):
return {} return {}
@@ -301,10 +300,7 @@ class BaseSerializer(serializers.ModelSerializer, metaclass=BaseSerializerMetacl
@property @property
def version(self): def version(self):
""" return 2
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
def get_type(self, obj): def get_type(self, obj):
return get_type_for_model(self.Meta.model) 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 \ if view and (hasattr(view, 'retrieve') or view.request.method == 'POST') and \
type(obj) in settings.NAMED_URL_GRAPH: type(obj) in settings.NAMED_URL_GRAPH:
original_url = self.get_url(obj) original_url = self.get_url(obj)
if not original_url.startswith('/api/v1'): res['named_url'] = self._generate_named_url(
res['named_url'] = self._generate_named_url( original_url, obj, settings.NAMED_URL_GRAPH[type(obj)]
original_url, obj, settings.NAMED_URL_GRAPH[type(obj)] )
)
if getattr(obj, 'created_by', None): if getattr(obj, 'created_by', None):
res['created_by'] = self.reverse('api:user_detail', kwargs={'pk': obj.created_by.pk}) res['created_by'] = self.reverse('api:user_detail', kwargs={'pk': obj.created_by.pk})
if getattr(obj, 'modified_by', None): if getattr(obj, 'modified_by', None):
@@ -396,8 +391,6 @@ class BaseSerializer(serializers.ModelSerializer, metaclass=BaseSerializerMetacl
continue continue
summary_fields[fk] = OrderedDict() summary_fields[fk] = OrderedDict()
for field in related_fields: 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) fval = getattr(fkval, field, None)
@@ -884,10 +877,10 @@ class UserSerializer(BaseSerializer):
'username', 'first_name', 'last_name', 'username', 'first_name', 'last_name',
'email', 'is_superuser', 'is_system_auditor', 'password', 'ldap_dn', 'last_login', 'external_account') '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 = super(UserSerializer, self).to_representation(obj)
ret.pop('password', None) 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') ret['auth'] = obj.social_auth.values('provider', 'uid')
return ret 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}), 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}), 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}), 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: if obj.organization:
res['organization'] = self.reverse('api:organization_detail', res['organization'] = self.reverse('api:organization_detail',
kwargs={'pk': obj.organization.pk}) kwargs={'pk': obj.organization.pk})
@@ -1561,9 +1554,8 @@ class InventorySerializer(BaseSerializerWithVariables):
access_list = self.reverse('api:inventory_access_list', kwargs={'pk': obj.pk}), 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}), 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}), 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: if obj.insights_credential:
res['insights_credential'] = self.reverse('api:credential_detail', kwargs={'pk': obj.insights_credential.pk}) res['insights_credential'] = self.reverse('api:credential_detail', kwargs={'pk': obj.insights_credential.pk})
if obj.organization: if obj.organization:
@@ -1615,20 +1607,6 @@ class InventorySerializer(BaseSerializerWithVariables):
return super(InventorySerializer, self).validate(attrs) 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 InventoryScriptSerializer(InventorySerializer):
class Meta: class Meta:
@@ -1668,19 +1646,15 @@ class HostSerializer(BaseSerializerWithVariables):
smart_inventories = self.reverse('api:host_smart_inventories_list', kwargs={'pk': obj.pk}), 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_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}), 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: if obj.inventory:
res['inventory'] = self.reverse('api:inventory_detail', kwargs={'pk': obj.inventory.pk}) res['inventory'] = self.reverse('api:inventory_detail', kwargs={'pk': obj.inventory.pk})
if obj.last_job: if obj.last_job:
res['last_job'] = self.reverse('api:job_detail', kwargs={'pk': obj.last_job.pk}) res['last_job'] = self.reverse('api:job_detail', kwargs={'pk': obj.last_job.pk})
if obj.last_job_host_summary: 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}) 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 return res
def get_summary_fields(self, obj): def get_summary_fields(self, obj):
@@ -1766,6 +1740,7 @@ class AnsibleFactsSerializer(BaseSerializer):
class GroupSerializer(BaseSerializerWithVariables): class GroupSerializer(BaseSerializerWithVariables):
show_capabilities = ['copy', 'edit', 'delete']
capabilities_prefetch = ['inventory.admin', 'inventory.adhoc'] capabilities_prefetch = ['inventory.admin', 'inventory.adhoc']
groups_with_active_failures = serializers.IntegerField( groups_with_active_failures = serializers.IntegerField(
read_only=True, read_only=True,
@@ -1779,13 +1754,6 @@ class GroupSerializer(BaseSerializerWithVariables):
'total_hosts', 'hosts_with_active_failures', 'total_groups', 'total_hosts', 'hosts_with_active_failures', 'total_groups',
'groups_with_active_failures', 'has_inventory_sources') '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): def build_relational_field(self, field_name, relation_info):
field_class, field_kwargs = super(GroupSerializer, self).build_relational_field(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. # Inventory is read-only unless creating a new group.
@@ -1794,20 +1762,6 @@ class GroupSerializer(BaseSerializerWithVariables):
field_kwargs.pop('queryset', None) field_kwargs.pop('queryset', None)
return field_class, field_kwargs 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): def get_related(self, obj):
res = super(GroupSerializer, self).get_related(obj) res = super(GroupSerializer, self).get_related(obj)
res.update(dict( res.update(dict(
@@ -1822,24 +1776,10 @@ class GroupSerializer(BaseSerializerWithVariables):
inventory_sources = self.reverse('api:group_inventory_sources_list', kwargs={'pk': obj.pk}), 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}), 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: if obj.inventory:
res['inventory'] = self.reverse('api:inventory_detail', kwargs={'pk': obj.inventory.pk}) res['inventory'] = self.reverse('api:inventory_detail', kwargs={'pk': obj.inventory.pk})
return res 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): def validate_name(self, value):
if value in ('all', '_meta'): if value in ('all', '_meta'):
raise serializers.ValidationError(_('Invalid group name.')) raise serializers.ValidationError(_('Invalid group name.'))
@@ -1941,9 +1881,8 @@ class CustomInventoryScriptSerializer(BaseSerializer):
res = super(CustomInventoryScriptSerializer, self).get_related(obj) res = super(CustomInventoryScriptSerializer, self).get_related(obj)
res.update(dict( res.update(dict(
object_roles = self.reverse('api:inventory_script_object_roles_list', kwargs={'pk': obj.pk}), 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: if obj.organization:
res['organization'] = self.reverse('api:organization_detail', kwargs={'pk': obj.organization.pk}) 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) 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): class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOptionsSerializer):
@@ -2036,14 +1954,12 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt
{'admin': 'inventory.admin'}, {'admin': 'inventory.admin'},
{'start': 'inventory.update'} {'start': 'inventory.update'}
] ]
group = serializers.SerializerMethodField(
help_text=_('Automatic group relationship, will be removed in 3.3'))
class Meta: class Meta:
model = InventorySource model = InventorySource
fields = ('*', 'name', 'inventory', 'update_on_launch', 'update_cache_timeout', fields = ('*', 'name', 'inventory', 'update_on_launch', 'update_cache_timeout',
'source_project', 'update_on_project_update') + \ '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): def get_related(self, obj):
res = super(InventorySourceSerializer, self).get_related(obj) res = super(InventorySourceSerializer, self).get_related(obj)
@@ -2069,30 +1985,10 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt
if obj.last_update: if obj.last_update:
res['last_update'] = self.reverse('api:inventory_update_detail', res['last_update'] = self.reverse('api:inventory_update_detail',
kwargs={'pk': obj.last_update.pk}) 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: else:
res['credentials'] = self.reverse('api:inventory_source_credentials_list', kwargs={'pk': obj.pk}) res['credentials'] = self.reverse('api:inventory_source_credentials_list', kwargs={'pk': obj.pk})
return res 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 def get_group(self, obj): # TODO: remove in 3.3
if obj.deprecated_group: if obj.deprecated_group:
return obj.deprecated_group.id return obj.deprecated_group.id
@@ -2127,12 +2023,6 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt
raise serializers.ValidationError(_("Cannot use manual project for SCM-based inventory.")) raise serializers.ValidationError(_("Cannot use manual project for SCM-based inventory."))
return value 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): def validate_update_on_project_update(self, value):
if value and self.instance and self.instance.schedules.exists(): if value and self.instance and self.instance.schedules.exists():
raise serializers.ValidationError(_("Setting not compatible with existing schedules.")) raise serializers.ValidationError(_("Setting not compatible with existing schedules."))
@@ -2253,8 +2143,7 @@ class InventoryUpdateSerializer(UnifiedJobSerializer, InventorySourceOptionsSeri
if obj.inventory: if obj.inventory:
res['inventory'] = self.reverse('api:inventory_detail', kwargs={'pk': obj.inventory.pk}) 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 return res
@@ -2286,14 +2175,25 @@ class InventoryUpdateDetailSerializer(InventoryUpdateSerializer):
def get_summary_fields(self, obj): def get_summary_fields(self, obj):
summary_fields = super(InventoryUpdateDetailSerializer, self).get_summary_fields(obj) summary_fields = super(InventoryUpdateDetailSerializer, self).get_summary_fields(obj)
summary_obj = self.get_source_project(obj)
if summary_obj: source_project = self.get_source_project(obj)
if source_project:
summary_fields['source_project'] = {} summary_fields['source_project'] = {}
for field in SUMMARIZABLE_FK_FIELDS['project']: for field in SUMMARIZABLE_FK_FIELDS['project']:
value = getattr(summary_obj, field, None) value = getattr(source_project, field, None)
if value is not None: if value is not None:
summary_fields['source_project'][field] = value summary_fields['source_project'][field] = value
cred = obj.credentials.first()
if cred:
summary_fields['credential'] = {
'id': cred.pk,
'name': cred.name,
'description': cred.description,
'kind': cred.kind,
'cloud': cred.credential_type.kind == 'cloud'
}
return summary_fields return summary_fields
@@ -2562,67 +2462,22 @@ class CredentialTypeSerializer(BaseSerializer):
return fields 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): class CredentialSerializer(BaseSerializer):
show_capabilities = ['edit', 'delete', 'copy', 'use'] show_capabilities = ['edit', 'delete', 'copy', 'use']
capabilities_prefetch = ['admin', 'use'] capabilities_prefetch = ['admin', 'use']
class Meta: class Meta:
model = Credential model = Credential
fields = ('*', 'organization') fields = ('*', 'organization', 'credential_type', 'inputs', 'kind', 'cloud')
extra_kwargs = {
def get_fields(self): 'credential_type': {
fields = super(CredentialSerializer, self).get_fields() 'label': _('Credential Type'),
},
# TODO: remove when API v1 is removed }
if self.version == 1:
fields.update(V1CredentialFields().get_fields())
else:
fields.update(V2CredentialFields().get_fields())
return fields
def to_representation(self, data): def to_representation(self, data):
value = super(CredentialSerializer, self).to_representation(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: if 'inputs' in value:
value['inputs'] = data.display_inputs() value['inputs'] = data.display_inputs()
return value return value
@@ -2639,16 +2494,10 @@ class CredentialSerializer(BaseSerializer):
object_roles = self.reverse('api:credential_object_roles_list', kwargs={'pk': obj.pk}), 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_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}), 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] parents = [role for role in obj.admin_role.parents.all() if role.object_id is not None]
if parents: if parents:
@@ -2684,54 +2533,12 @@ class CredentialSerializer(BaseSerializer):
return summary_dict return summary_dict
def get_validation_exclusions(self, obj=None): 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) ret = super(CredentialSerializer, self).get_validation_exclusions(obj)
for field in ('credential_type', 'inputs'): for field in ('credential_type', 'inputs'):
if field in ret: if field in ret:
ret.remove(field) ret.remove(field)
return ret 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): def validate_credential_type(self, credential_type):
if self.instance and credential_type.pk != self.instance.credential_type.pk: if self.instance and credential_type.pk != self.instance.credential_type.pk:
for rel in ( for rel in (
@@ -2788,35 +2595,12 @@ class CredentialSerializerCreate(CredentialSerializer):
if attrs.get('team'): if attrs.get('team'):
attrs['organization'] = attrs['team'].organization attrs['organization'] = attrs['team'].organization
try: return super(CredentialSerializerCreate, self).validate(attrs)
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
def create(self, validated_data): def create(self, validated_data):
user = validated_data.pop('user', None) user = validated_data.pop('user', None)
team = validated_data.pop('team', 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) credential = super(CredentialSerializerCreate, self).create(validated_data)
if user: if user:
@@ -2895,35 +2679,6 @@ class LabelsListMixin(object):
return res 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 JobOptionsSerializer(LabelsListMixin, BaseSerializer):
class Meta: class Meta:
@@ -2932,16 +2687,6 @@ class JobOptionsSerializer(LabelsListMixin, BaseSerializer):
'force_handlers', 'skip_tags', 'start_at_task', 'timeout', 'force_handlers', 'skip_tags', 'start_at_task', 'timeout',
'use_fact_cache',) '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): def get_related(self, obj):
res = super(JobOptionsSerializer, self).get_related(obj) res = super(JobOptionsSerializer, self).get_related(obj)
res['labels'] = self.reverse('api:job_template_label_list', kwargs={'pk': obj.pk}) res['labels'] = self.reverse('api:job_template_label_list', kwargs={'pk': obj.pk})
@@ -2955,40 +2700,18 @@ class JobOptionsSerializer(LabelsListMixin, BaseSerializer):
res['project'] = self.reverse('api:project_detail', kwargs={'pk': obj.project.pk}) res['project'] = self.reverse('api:project_detail', kwargs={'pk': obj.project.pk})
except ObjectDoesNotExist: except ObjectDoesNotExist:
setattr(obj, 'project', None) setattr(obj, 'project', None)
try: if isinstance(obj, UnifiedJobTemplate):
if obj.credential: res['extra_credentials'] = self.reverse(
res['credential'] = self.reverse( 'api:job_template_extra_credentials_list',
'api:credential_detail', kwargs={'pk': obj.credential} kwargs={'pk': obj.pk}
) )
except ObjectDoesNotExist: res['credentials'] = self.reverse(
setattr(obj, 'credential', None) 'api:job_template_credentials_list',
try: kwargs={'pk': obj.pk}
if obj.vault_credential: )
res['vault_credential'] = self.reverse( elif isinstance(obj, UnifiedJob):
'api:credential_detail', kwargs={'pk': obj.vault_credential} 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})
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})
return res return res
@@ -3002,70 +2725,9 @@ class JobOptionsSerializer(LabelsListMixin, BaseSerializer):
ret['project'] = None ret['project'] = None
if 'playbook' in ret: if 'playbook' in ret:
ret['playbook'] = '' 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 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): 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: if 'project' in self.fields and 'playbook' in self.fields:
project = attrs.get('project', self.instance and self.instance.project or None) project = attrs.get('project', self.instance and self.instance.project or None)
playbook = attrs.get('playbook', self.instance and self.instance.playbook or '') playbook = attrs.get('playbook', self.instance and self.instance.playbook or '')
@@ -3079,7 +2741,6 @@ class JobOptionsSerializer(LabelsListMixin, BaseSerializer):
raise serializers.ValidationError({'playbook': _('Must select playbook for project.')}) raise serializers.ValidationError({'playbook': _('Must select playbook for project.')})
ret = super(JobOptionsSerializer, self).validate(attrs) ret = super(JobOptionsSerializer, self).validate(attrs)
ret.update(v1_credentials)
return ret return ret
@@ -3105,12 +2766,6 @@ class JobTemplateMixin(object):
if obj.survey_spec is not None and ('name' in obj.survey_spec and 'description' in obj.survey_spec): 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['survey'] = dict(title=obj.survey_spec['name'], description=obj.survey_spec['description'])
d['recent_jobs'] = self._recent_jobs(obj) 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 return d
@@ -3146,9 +2801,8 @@ class JobTemplateSerializer(JobTemplateMixin, UnifiedJobTemplateSerializer, JobO
object_roles = self.reverse('api:job_template_object_roles_list', kwargs={'pk': obj.pk}), 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}), 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}), 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: if obj.host_config_key:
res['callback'] = self.reverse('api:job_template_callback', kwargs={'pk': obj.pk}) res['callback'] = self.reverse('api:job_template_callback', kwargs={'pk': obj.pk})
return res return res
@@ -3181,9 +2835,6 @@ class JobTemplateSerializer(JobTemplateMixin, UnifiedJobTemplateSerializer, JobO
summary_fields = super(JobTemplateSerializer, self).get_summary_fields(obj) summary_fields = super(JobTemplateSerializer, self).get_summary_fields(obj)
all_creds = [] all_creds = []
# Organize credential data into multitude of deprecated fields # Organize credential data into multitude of deprecated fields
# TODO: remove most of this as v1 is removed
vault_credential = None
credential = None
extra_creds = [] extra_creds = []
if obj.pk: if obj.pk:
for cred in obj.credentials.all(): for cred in obj.credentials.all():
@@ -3194,30 +2845,12 @@ class JobTemplateSerializer(JobTemplateMixin, UnifiedJobTemplateSerializer, JobO
'kind': cred.kind, 'kind': cred.kind,
'cloud': cred.credential_type.kind == 'cloud' 'cloud': cred.credential_type.kind == 'cloud'
} }
if self.version > 1:
summarized_cred['credential_type_id'] = cred.credential_type_id
all_creds.append(summarized_cred) all_creds.append(summarized_cred)
if cred.credential_type.kind in ('cloud', 'net'): if cred.credential_type.kind in ('cloud', 'net'):
extra_creds.append(summarized_cred) extra_creds.append(summarized_cred)
elif summarized_cred['kind'] == 'ssh': if self.is_detail_view:
credential = summarized_cred summary_fields['extra_credentials'] = extra_creds
elif summarized_cred['kind'] == 'vault': summary_fields['credentials'] = all_creds
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
return summary_fields return summary_fields
@@ -3250,6 +2883,7 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
activity_stream = self.reverse('api:job_activity_stream_list', kwargs={'pk': obj.pk}), activity_stream = self.reverse('api:job_activity_stream_list', kwargs={'pk': obj.pk}),
notifications = self.reverse('api:job_notifications_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}), labels = self.reverse('api:job_label_list', kwargs={'pk': obj.pk}),
create_schedule = self.reverse('api:job_create_schedule', kwargs={'pk': obj.pk}),
)) ))
try: try:
if obj.job_template: if obj.job_template:
@@ -3257,8 +2891,6 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
kwargs={'pk': obj.job_template.pk}) kwargs={'pk': obj.job_template.pk})
except ObjectDoesNotExist: except ObjectDoesNotExist:
setattr(obj, 'job_template', None) 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: if obj.can_cancel or True:
res['cancel'] = self.reverse('api:job_cancel', kwargs={'pk': obj.pk}) res['cancel'] = self.reverse('api:job_cancel', kwargs={'pk': obj.pk})
try: try:
@@ -3268,8 +2900,6 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
) )
except ObjectDoesNotExist: except ObjectDoesNotExist:
pass 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}) res['relaunch'] = self.reverse('api:job_relaunch', kwargs={'pk': obj.pk})
return res return res
@@ -3320,9 +2950,6 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
summary_fields = super(JobSerializer, self).get_summary_fields(obj) summary_fields = super(JobSerializer, self).get_summary_fields(obj)
all_creds = [] all_creds = []
# Organize credential data into multitude of deprecated fields # Organize credential data into multitude of deprecated fields
# TODO: remove most of this as v1 is removed
vault_credential = None
credential = None
extra_creds = [] extra_creds = []
if obj.pk: if obj.pk:
for cred in obj.credentials.all(): for cred in obj.credentials.all():
@@ -3333,30 +2960,12 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
'kind': cred.kind, 'kind': cred.kind,
'cloud': cred.credential_type.kind == 'cloud' 'cloud': cred.credential_type.kind == 'cloud'
} }
if self.version > 1:
summarized_cred['credential_type_id'] = cred.credential_type_id
all_creds.append(summarized_cred) all_creds.append(summarized_cred)
if cred.credential_type.kind in ('cloud', 'net'): if cred.credential_type.kind in ('cloud', 'net'):
extra_creds.append(summarized_cred) extra_creds.append(summarized_cred)
elif summarized_cred['kind'] == 'ssh': if self.is_detail_view:
credential = summarized_cred summary_fields['extra_credentials'] = extra_creds
elif summarized_cred['kind'] == 'vault': summary_fields['credentials'] = all_creds
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
return summary_fields return summary_fields
@@ -3696,9 +3305,8 @@ class WorkflowJobTemplateSerializer(JobTemplateMixin, LabelsListMixin, UnifiedJo
access_list = self.reverse('api:workflow_job_template_access_list', kwargs={'pk': obj.pk}), 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}), 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}), 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: if obj.organization:
res['organization'] = self.reverse('api:organization_detail', kwargs={'pk': obj.organization.pk}) res['organization'] = self.reverse('api:organization_detail', kwargs={'pk': obj.organization.pk})
return res return res
@@ -4401,17 +4009,16 @@ class JobLaunchSerializer(BaseSerializer):
name=getattrd(obj, '%s.name' % field_name, None), name=getattrd(obj, '%s.name' % field_name, None),
id=getattrd(obj, '%s.pk' % field_name, None)) id=getattrd(obj, '%s.pk' % field_name, None))
elif field_name == 'credentials': elif field_name == 'credentials':
if self.version > 1: for cred in obj.credentials.all():
for cred in obj.credentials.all(): cred_dict = dict(
cred_dict = dict( id=cred.id,
id=cred.id, name=cred.name,
name=cred.name, credential_type=cred.credential_type.pk,
credential_type=cred.credential_type.pk, passwords_needed=cred.passwords_needed
passwords_needed=cred.passwords_needed )
) if cred.credential_type.managed_by_tower and 'vault_id' in cred.credential_type.defined_fields:
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)
cred_dict['vault_id'] = cred.get_input('vault_id', default=None) defaults_dict.setdefault(field_name, []).append(cred_dict)
defaults_dict.setdefault(field_name, []).append(cred_dict)
else: else:
defaults_dict[field_name] = getattr(obj, field_name) defaults_dict[field_name] = getattr(obj, field_name)
return defaults_dict return defaults_dict
@@ -4584,9 +4191,8 @@ class NotificationTemplateSerializer(BaseSerializer):
res.update(dict( res.update(dict(
test = self.reverse('api:notification_template_test', kwargs={'pk': obj.pk}), test = self.reverse('api:notification_template_test', kwargs={'pk': obj.pk}),
notifications = self.reverse('api:notification_template_notification_list', 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: if obj.organization:
res['organization'] = self.reverse('api:organization_detail', kwargs={'pk': obj.organization.pk}) res['organization'] = self.reverse('api:organization_detail', kwargs={'pk': obj.organization.pk})
return res return res

View File

@@ -8,15 +8,15 @@ job template.
For example, using curl: 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: 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: 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 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 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 This request must authenticate as a valid user with permission to edit the
job template. For example: 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) The response will include the host config key as well as the host name(s)
that would match the request: that would match the request:

View File

@@ -6,4 +6,4 @@ One result should be returned containing the following fields:
{% include "api/_result_fields_common.md" %} {% 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.

View File

@@ -6,7 +6,6 @@ from django.conf.urls import url
from awx.api.views import ( from awx.api.views import (
JobList, JobList,
JobDetail, JobDetail,
JobStart,
JobCancel, JobCancel,
JobRelaunch, JobRelaunch,
JobCreateSchedule, JobCreateSchedule,
@@ -23,7 +22,6 @@ from awx.api.views import (
urls = [ urls = [
url(r'^$', JobList.as_view(), name='job_list'), 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]+)/$', 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]+)/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]+)/relaunch/$', JobRelaunch.as_view(), name='job_relaunch'),
url(r'^(?P<pk>[0-9]+)/create_schedule/$', JobCreateSchedule.as_view(), name='job_create_schedule'), url(r'^(?P<pk>[0-9]+)/create_schedule/$', JobCreateSchedule.as_view(), name='job_create_schedule'),

View File

@@ -11,10 +11,9 @@ from awx.api.generics import (
) )
from awx.api.views import ( from awx.api.views import (
ApiRootView, ApiRootView,
ApiV1RootView,
ApiV2RootView, ApiV2RootView,
ApiV1PingView, ApiV2PingView,
ApiV1ConfigView, ApiV2ConfigView,
AuthView, AuthView,
UserMeList, UserMeList,
DashboardView, DashboardView,
@@ -74,10 +73,25 @@ from .oauth2 import urls as oauth2_urls
from .oauth2_root import urls as oauth2_root_urls from .oauth2_root import urls as oauth2_root_urls
v1_urls = [ v2_urls = [
url(r'^$', ApiV1RootView.as_view(), name='api_v1_root_view'), url(r'^$', ApiV2RootView.as_view(), name='api_v2_root_view'),
url(r'^ping/$', ApiV1PingView.as_view(), name='api_v1_ping_view'), url(r'^credential_types/', include(credential_type_urls)),
url(r'^config/$', ApiV1ConfigView.as_view(), name='api_v1_config_view'), 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'^auth/$', AuthView.as_view()),
url(r'^me/$', UserMeList.as_view(), name='user_me_list'), url(r'^me/$', UserMeList.as_view(), name='user_me_list'),
url(r'^dashboard/$', DashboardView.as_view(), name='dashboard_view'), url(r'^dashboard/$', DashboardView.as_view(), name='dashboard_view'),
@@ -119,30 +133,10 @@ v1_urls = [
url(r'^activity_stream/', include(activity_stream_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' app_name = 'api'
urlpatterns = [ urlpatterns = [
url(r'^$', ApiRootView.as_view(), name='api_root_view'), url(r'^$', ApiRootView.as_view(), name='api_root_view'),
url(r'^(?P<version>(v2))/', include(v2_urls)), url(r'^(?P<version>(v2))/', include(v2_urls)),
url(r'^(?P<version>(v1|v2))/', include(v1_urls)),
url(r'^login/$', LoggedLoginView.as_view( url(r'^login/$', LoggedLoginView.as_view(
template_name='rest_framework/login.html', template_name='rest_framework/login.html',
extra_context={'inside_login_context': True} extra_context={'inside_login_context': True}

View File

@@ -27,19 +27,6 @@ def drf_reverse(viewname, args=None, kwargs=None, request=None, format=None, **e
return url 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): def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra):
if request is None or getattr(request, 'version', None) is None: if request is None or getattr(request, 'version', None) is None:
# We need the "current request" to determine the correct version to # We need the "current request" to determine the correct version to

View File

@@ -62,7 +62,6 @@ from wsgiref.util import FileWrapper
# AWX # AWX
from awx.main.tasks import send_notifications, update_inventory_computed_fields from awx.main.tasks import send_notifications, update_inventory_computed_fields
from awx.main.access import get_user_queryset, HostAccess from awx.main.access import get_user_queryset, HostAccess
from awx.api.filters import V1CredentialFilterBackend
from awx.api.generics import ( from awx.api.generics import (
APIView, BaseUsersList, CopyAPIView, DeleteLastUnattachLabelMixin, APIView, BaseUsersList, CopyAPIView, DeleteLastUnattachLabelMixin,
GenericAPIView, ListAPIView, ListCreateAPIView, GenericAPIView, ListAPIView, ListCreateAPIView,
@@ -72,7 +71,7 @@ from awx.api.generics import (
SubListCreateAPIView, SubListCreateAttachDetachAPIView, SubListCreateAPIView, SubListCreateAttachDetachAPIView,
SubListDestroyAPIView, get_view_name 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.conf.license import get_license
from awx.main import models from awx.main import models
from awx.main.utils import ( from awx.main.utils import (
@@ -96,7 +95,7 @@ from awx.api.permissions import (
) )
from awx.api import renderers from awx.api import renderers
from awx.api import serializers 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.constants import ACTIVE_STATES
from awx.main.scheduler.dag_workflow import WorkflowDAG from awx.main.scheduler.dag_workflow import WorkflowDAG
from awx.api.views.mixin import ( from awx.api.views.mixin import (
@@ -143,10 +142,9 @@ from awx.api.views.root import ( # noqa
ApiRootView, ApiRootView,
ApiOAuthAuthorizationRootView, ApiOAuthAuthorizationRootView,
ApiVersionRootView, ApiVersionRootView,
ApiV1RootView,
ApiV2RootView, ApiV2RootView,
ApiV1PingView, ApiV2PingView,
ApiV1ConfigView, ApiV2ConfigView,
) )
@@ -1246,22 +1244,10 @@ class CredentialTypeActivityStreamList(SubListAPIView):
search_fields = ('changes',) search_fields = ('changes',)
# remove in 3.3 class CredentialList(ListCreateAPIView):
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):
model = models.Credential model = models.Credential
serializer_class = serializers.CredentialSerializerCreate serializer_class = serializers.CredentialSerializerCreate
filter_backends = ListCreateAPIView.filter_backends + [V1CredentialFilterBackend]
class CredentialOwnerUsersList(SubListAPIView): class CredentialOwnerUsersList(SubListAPIView):
@@ -1289,13 +1275,12 @@ class CredentialOwnerTeamsList(SubListAPIView):
return self.model.objects.filter(pk__in=teams) return self.model.objects.filter(pk__in=teams)
class UserCredentialsList(CredentialViewMixin, SubListCreateAPIView): class UserCredentialsList(SubListCreateAPIView):
model = models.Credential model = models.Credential
serializer_class = serializers.UserCredentialSerializerCreate serializer_class = serializers.UserCredentialSerializerCreate
parent_model = models.User parent_model = models.User
parent_key = 'user' parent_key = 'user'
filter_backends = SubListCreateAPIView.filter_backends + [V1CredentialFilterBackend]
def get_queryset(self): def get_queryset(self):
user = self.get_parent_object() user = self.get_parent_object()
@@ -1306,13 +1291,12 @@ class UserCredentialsList(CredentialViewMixin, SubListCreateAPIView):
return user_creds & visible_creds return user_creds & visible_creds
class TeamCredentialsList(CredentialViewMixin, SubListCreateAPIView): class TeamCredentialsList(SubListCreateAPIView):
model = models.Credential model = models.Credential
serializer_class = serializers.TeamCredentialSerializerCreate serializer_class = serializers.TeamCredentialSerializerCreate
parent_model = models.Team parent_model = models.Team
parent_key = 'team' parent_key = 'team'
filter_backends = SubListCreateAPIView.filter_backends + [V1CredentialFilterBackend]
def get_queryset(self): def get_queryset(self):
team = self.get_parent_object() team = self.get_parent_object()
@@ -1323,13 +1307,12 @@ class TeamCredentialsList(CredentialViewMixin, SubListCreateAPIView):
return (team_creds & visible_creds).distinct() return (team_creds & visible_creds).distinct()
class OrganizationCredentialList(CredentialViewMixin, SubListCreateAPIView): class OrganizationCredentialList(SubListCreateAPIView):
model = models.Credential model = models.Credential
serializer_class = serializers.OrganizationCredentialSerializerCreate serializer_class = serializers.OrganizationCredentialSerializerCreate
parent_model = models.Organization parent_model = models.Organization
parent_key = 'organization' parent_key = 'organization'
filter_backends = SubListCreateAPIView.filter_backends + [V1CredentialFilterBackend]
def get_queryset(self): def get_queryset(self):
organization = self.get_parent_object() organization = self.get_parent_object()
@@ -1348,7 +1331,6 @@ class CredentialDetail(RetrieveUpdateDestroyAPIView):
model = models.Credential model = models.Credential
serializer_class = serializers.CredentialSerializer serializer_class = serializers.CredentialSerializer
filter_backends = RetrieveUpdateDestroyAPIView.filter_backends + [V1CredentialFilterBackend]
class CredentialActivityStreamList(SubListAPIView): class CredentialActivityStreamList(SubListAPIView):
@@ -1754,10 +1736,10 @@ class EnforceParentRelationshipMixin(object):
* Tower uses a shallow (2-deep only) url pattern. For example: * 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 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 form /api/v2/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 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/v1/child_child_model/87/ 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/v1/child_model/87/child_child_model_set 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 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 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() obj = self.get_object()
if not request.user.can_access(self.model, 'delete', obj): if not request.user.can_access(self.model, 'delete', obj):
raise PermissionDenied() 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() obj.delete_recursive()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
@@ -2093,13 +2070,6 @@ class InventorySourceList(ListCreateAPIView):
serializer_class = serializers.InventorySourceSerializer serializer_class = serializers.InventorySourceSerializer
always_allow_superuser = False 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): class InventorySourceDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView):
@@ -2290,7 +2260,6 @@ class InventoryUpdateNotificationsList(SubListAPIView):
class JobTemplateList(ListCreateAPIView): class JobTemplateList(ListCreateAPIView):
model = models.JobTemplate model = models.JobTemplate
metadata_class = JobTypeMetadata
serializer_class = serializers.JobTemplateSerializer serializer_class = serializers.JobTemplateSerializer
always_allow_superuser = False always_allow_superuser = False
@@ -2305,7 +2274,6 @@ class JobTemplateList(ListCreateAPIView):
class JobTemplateDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView): class JobTemplateDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView):
model = models.JobTemplate model = models.JobTemplate
metadata_class = JobTypeMetadata
serializer_class = serializers.JobTemplateSerializer serializer_class = serializers.JobTemplateSerializer
always_allow_superuser = False always_allow_superuser = False
@@ -2314,7 +2282,6 @@ class JobTemplateLaunch(RetrieveAPIView):
model = models.JobTemplate model = models.JobTemplate
obj_permission_type = 'start' obj_permission_type = 'start'
metadata_class = JobTypeMetadata
serializer_class = serializers.JobLaunchSerializer serializer_class = serializers.JobLaunchSerializer
always_allow_superuser = False always_allow_superuser = False
@@ -2358,65 +2325,44 @@ class JobTemplateLaunch(RetrieveAPIView):
ignored_fields = {} ignored_fields = {}
modern_data = data.copy() modern_data = data.copy()
for fd in ('credential', 'vault_credential', 'inventory'): id_fd = '{}_id'.format('inventory')
id_fd = '{}_id'.format(fd) if 'inventory' not in modern_data and id_fd in modern_data:
if fd not in modern_data and id_fd in modern_data: modern_data['inventory'] = modern_data[id_fd]
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."
)})
# Automatically convert legacy launch credential arguments into a list of `.credentials` # Automatically convert legacy launch credential arguments into a list of `.credentials`
if 'credentials' in modern_data and ( if 'credentials' in modern_data and 'extra_credentials' in modern_data:
'credential' in modern_data or
'vault_credential' in modern_data or
'extra_credentials' in modern_data
):
raise ParseError({"error": _( 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 ( if 'extra_credentials' in modern_data:
'credential' in modern_data or
'vault_credential' in modern_data or
'extra_credentials' in modern_data
):
# make a list of the current credentials # make a list of the current credentials
existing_credentials = obj.credentials.all() existing_credentials = obj.credentials.all()
template_credentials = list(existing_credentials) # save copy of existing template_credentials = list(existing_credentials) # save copy of existing
new_credentials = [] new_credentials = []
for key, conditional, _type, type_repr in ( if 'extra_credentials' in modern_data:
('credential', lambda cred: cred.credential_type.kind != 'ssh', int, 'pk value'), existing_credentials = [
('vault_credential', lambda cred: cred.credential_type.kind != 'vault', int, 'pk value'), cred for cred in existing_credentials
('extra_credentials', lambda cred: cred.credential_type.kind not in ('cloud', 'net'), Iterable, 'a list') if cred.credential_type.kind not in ('cloud', 'net')
): ]
if key in modern_data: prompted_value = modern_data.pop('extra_credentials')
# 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)
# validate type, since these are not covered by a serializer # validate type, since these are not covered by a serializer
if not isinstance(prompted_value, _type): if not isinstance(prompted_value, Iterable):
msg = _( msg = _(
"Incorrect type. Expected {}, received {}." "Incorrect type. Expected a list received {}."
).format(type_repr, prompted_value.__class__.__name__) ).format(prompted_value.__class__.__name__)
raise ParseError({key: [msg], 'credentials': [msg]}) raise ParseError({'extra_credentials': [msg], 'credentials': [msg]})
# add the deprecated credential specified in the request # add the deprecated credential specified in the request
if not isinstance(prompted_value, Iterable) or isinstance(prompted_value, str): if not isinstance(prompted_value, Iterable) or isinstance(prompted_value, str):
prompted_value = [prompted_value] prompted_value = [prompted_value]
# If user gave extra_credentials, special case to use exactly # If user gave extra_credentials, special case to use exactly
# the given list without merging with JT credentials # the given list without merging with JT credentials
if key == 'extra_credentials' and prompted_value: if prompted_value:
obj._deprecated_credential_launch = True # signal to not merge credentials obj._deprecated_credential_launch = True # signal to not merge credentials
new_credentials.extend(prompted_value) new_credentials.extend(prompted_value)
# combine the list of "new" and the filtered list of "old" # combine the list of "new" and the filtered list of "old"
new_credentials.extend([cred.pk for cred in existing_credentials]) 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) return Response(status=status.HTTP_201_CREATED, headers=headers)
class JobTemplateJobsList(SubListCreateAPIView): class JobTemplateJobsList(SubListAPIView):
model = models.Job model = models.Job
serializer_class = serializers.JobListSerializer serializer_class = serializers.JobListSerializer
@@ -2934,13 +2880,6 @@ class JobTemplateJobsList(SubListCreateAPIView):
relationship = 'jobs' relationship = 'jobs'
parent_key = 'job_template' 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): class JobTemplateSliceWorkflowJobsList(SubListCreateAPIView):
@@ -3135,8 +3074,6 @@ class WorkflowJobTemplateCopy(CopyAPIView):
copy_return_serializer_class = serializers.WorkflowJobTemplateSerializer copy_return_serializer_class = serializers.WorkflowJobTemplateSerializer
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
if get_request_version(request) < 2:
return self.v1_not_allowed()
obj = self.get_object() obj = self.get_object()
if not request.user.can_access(obj.__class__, 'read', obj): if not request.user.can_access(obj.__class__, 'read', obj):
raise PermissionDenied() raise PermissionDenied()
@@ -3493,56 +3430,17 @@ class SystemJobTemplateNotificationTemplatesSuccessList(SubListCreateAttachDetac
relationship = 'notification_templates_success' relationship = 'notification_templates_success'
class JobList(ListCreateAPIView): class JobList(ListAPIView):
model = models.Job model = models.Job
metadata_class = JobTypeMetadata
serializer_class = serializers.JobListSerializer 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 class JobDetail(UnifiedJobDeletionMixin, RetrieveDestroyAPIView):
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):
model = models.Job model = models.Job
metadata_class = JobTypeMetadata
serializer_class = serializers.JobDetailSerializer 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): def update(self, request, *args, **kwargs):
obj = self.get_object() obj = self.get_object()
# Only allow changes (PUT/PATCH) when job status is "new". # Only allow changes (PUT/PATCH) when job status is "new".
@@ -3591,44 +3489,6 @@ class JobActivityStreamList(SubListAPIView):
search_fields = ('changes',) 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): class JobCancel(RetrieveAPIView):
model = models.Job model = models.Job

View File

@@ -44,7 +44,6 @@ from awx.api.serializers import (
InstanceGroupSerializer, InstanceGroupSerializer,
InventoryUpdateEventSerializer, InventoryUpdateEventSerializer,
CustomInventoryScriptSerializer, CustomInventoryScriptSerializer,
InventoryDetailSerializer,
JobTemplateSerializer, JobTemplateSerializer,
) )
from awx.api.views.mixin import ( from awx.api.views.mixin import (
@@ -119,7 +118,7 @@ class InventoryList(ListCreateAPIView):
class InventoryDetail(RelatedJobsPreventDeleteMixin, ControlledByScmMixin, RetrieveUpdateDestroyAPIView): class InventoryDetail(RelatedJobsPreventDeleteMixin, ControlledByScmMixin, RetrieveUpdateDestroyAPIView):
model = Inventory model = Inventory
serializer_class = InventoryDetailSerializer serializer_class = InventorySerializer
def update(self, request, *args, **kwargs): def update(self, request, *args, **kwargs):
obj = self.get_object() obj = self.get_object()

View File

@@ -25,7 +25,7 @@ from awx.main.utils import (
get_custom_venv_choices, get_custom_venv_choices,
to_python_boolean, 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.conf.license import get_license
from awx.main.constants import PRIVILEGE_ESCALATION_METHODS from awx.main.constants import PRIVILEGE_ESCALATION_METHODS
from awx.main.models import ( from awx.main.models import (
@@ -50,12 +50,11 @@ class ApiRootView(APIView):
def get(self, request, format=None): def get(self, request, format=None):
''' List supported API versions ''' ''' List supported API versions '''
v1 = reverse('api:api_v1_root_view', kwargs={'version': 'v1'})
v2 = reverse('api:api_v2_root_view', kwargs={'version': 'v2'}) v2 = reverse('api:api_v2_root_view', kwargs={'version': 'v2'})
data = OrderedDict() data = OrderedDict()
data['description'] = _('AWX REST API') data['description'] = _('AWX REST API')
data['current_version'] = v2 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['oauth2'] = drf_reverse('api:oauth_authorization_root_view')
data['custom_logo'] = settings.CUSTOM_LOGO data['custom_logo'] = settings.CUSTOM_LOGO
data['custom_login_info'] = settings.CUSTOM_LOGIN_INFO data['custom_login_info'] = settings.CUSTOM_LOGIN_INFO
@@ -85,10 +84,10 @@ class ApiVersionRootView(APIView):
def get(self, request, format=None): def get(self, request, format=None):
''' List top level resources ''' ''' List top level resources '''
data = OrderedDict() 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['instances'] = reverse('api:instance_list', request=request)
data['instance_groups'] = reverse('api:instance_group_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['settings'] = reverse('api:setting_category_list', request=request)
data['me'] = reverse('api:user_me_list', request=request) data['me'] = reverse('api:user_me_list', request=request)
data['dashboard'] = reverse('api:dashboard_view', 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['project_updates'] = reverse('api:project_update_list', request=request)
data['teams'] = reverse('api:team_list', request=request) data['teams'] = reverse('api:team_list', request=request)
data['credentials'] = reverse('api:credential_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_types'] = reverse('api:credential_type_list', request=request) data['credential_input_sources'] = reverse('api:credential_input_source_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['applications'] = reverse('api:o_auth2_application_list', request=request) data['tokens'] = reverse('api:o_auth2_token_list', request=request)
data['tokens'] = reverse('api:o_auth2_token_list', request=request) data['metrics'] = reverse('api:metrics_view', request=request)
data['metrics'] = reverse('api:metrics_view', request=request)
data['inventory'] = reverse('api:inventory_list', request=request) data['inventory'] = reverse('api:inventory_list', request=request)
data['inventory_scripts'] = reverse('api:inventory_script_list', request=request) data['inventory_scripts'] = reverse('api:inventory_script_list', request=request)
data['inventory_sources'] = reverse('api:inventory_source_list', request=request) data['inventory_sources'] = reverse('api:inventory_source_list', request=request)
@@ -131,15 +129,11 @@ class ApiVersionRootView(APIView):
return Response(data) return Response(data)
class ApiV1RootView(ApiVersionRootView):
view_name = _('Version 1')
class ApiV2RootView(ApiVersionRootView): class ApiV2RootView(ApiVersionRootView):
view_name = _('Version 2') view_name = _('Version 2')
class ApiV1PingView(APIView): class ApiV2PingView(APIView):
"""A simple view that reports very basic information about this """A simple view that reports very basic information about this
instance, which is acceptable to be public information. instance, which is acceptable to be public information.
""" """
@@ -174,14 +168,14 @@ class ApiV1PingView(APIView):
return Response(response) return Response(response)
class ApiV1ConfigView(APIView): class ApiV2ConfigView(APIView):
permission_classes = (IsAuthenticated,) permission_classes = (IsAuthenticated,)
view_name = _('Configuration') view_name = _('Configuration')
swagger_topic = 'System Configuration' swagger_topic = 'System Configuration'
def check_permissions(self, request): 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'}: if not request.user.is_superuser and request.method.lower() not in {'options', 'head', 'get'}:
self.permission_denied(request) # Raises PermissionDenied exception. self.permission_denied(request) # Raises PermissionDenied exception.

View File

@@ -88,7 +88,7 @@ class SettingSingletonSerializer(serializers.Serializer):
continue continue
extra_kwargs = {} extra_kwargs = {}
# Make LICENSE and AWX_ISOLATED_KEY_GENERATION read-only here; # Make LICENSE and AWX_ISOLATED_KEY_GENERATION read-only here;
# LICENSE is only updated via /api/v1/config/ # LICENSE is only updated via /api/v2/config/
# AWX_ISOLATED_KEY_GENERATION is only set/unset via the setup playbook # AWX_ISOLATED_KEY_GENERATION is only set/unset via the setup playbook
if key in ('LICENSE', 'AWX_ISOLATED_KEY_GENERATION'): if key in ('LICENSE', 'AWX_ISOLATED_KEY_GENERATION'):
extra_kwargs['read_only'] = True extra_kwargs['read_only'] = True

View File

@@ -65,41 +65,6 @@ def test_non_admin_user_does_not_see_categories(api_request, dummy_setting, norm
assert not response.data['results'] assert not response.data['results']
@pytest.mark.django_db
@mock.patch(
'awx.conf.views.VERSION_SPECIFIC_CATEGORIES_TO_EXCLUDE',
{
1: set([]),
2: set(['foobar']),
}
)
def test_version_specific_category_slug_to_exclude_does_not_show_up(api_request, dummy_setting):
with dummy_setting(
'FOO_BAR',
field_class=fields.IntegerField,
category='FooBar',
category_slug='foobar'
):
response = api_request(
'get',
reverse('api:setting_category_list',
kwargs={'version': 'v2'})
)
for item in response.data['results']:
assert item['slug'] != 'foobar'
response = api_request(
'get',
reverse('api:setting_category_list',
kwargs={'version': 'v1'})
)
contains = False
for item in response.data['results']:
if item['slug'] != 'foobar':
contains = True
break
assert contains
@pytest.mark.django_db @pytest.mark.django_db
def test_setting_singleton_detail_retrieve(api_request, dummy_setting): def test_setting_singleton_detail_retrieve(api_request, dummy_setting):
with dummy_setting( with dummy_setting(

View File

@@ -24,7 +24,7 @@ from awx.api.generics import (
RetrieveUpdateDestroyAPIView, RetrieveUpdateDestroyAPIView,
) )
from awx.api.permissions import IsSuperUser from awx.api.permissions import IsSuperUser
from awx.api.versioning import reverse, get_request_version from awx.api.versioning import reverse
from awx.main.utils import camelcase_to_underscore from awx.main.utils import camelcase_to_underscore
from awx.main.utils.handlers import AWXProxyHandler, LoggingConnectivityException from awx.main.utils.handlers import AWXProxyHandler, LoggingConnectivityException
from awx.main.tasks import handle_setting_changes from awx.main.tasks import handle_setting_changes
@@ -35,13 +35,6 @@ from awx.conf import settings_registry
SettingCategory = collections.namedtuple('SettingCategory', ('url', 'slug', 'name')) SettingCategory = collections.namedtuple('SettingCategory', ('url', 'slug', 'name'))
VERSION_SPECIFIC_CATEGORIES_TO_EXCLUDE = {
1: set([
'named-url',
]),
2: set([]),
}
class SettingCategoryList(ListAPIView): class SettingCategoryList(ListAPIView):
@@ -60,8 +53,6 @@ class SettingCategoryList(ListAPIView):
else: else:
categories = {} categories = {}
for category_slug in sorted(categories.keys()): for category_slug in sorted(categories.keys()):
if category_slug in VERSION_SPECIFIC_CATEGORIES_TO_EXCLUDE[get_request_version(self.request)]:
continue
url = reverse('api:setting_singleton_detail', kwargs={'category_slug': category_slug}, request=self.request) url = reverse('api:setting_singleton_detail', kwargs={'category_slug': category_slug}, request=self.request)
setting_categories.append(SettingCategory(url, category_slug, categories[category_slug])) setting_categories.append(SettingCategory(url, category_slug, categories[category_slug]))
return setting_categories return setting_categories
@@ -77,8 +68,6 @@ class SettingSingletonDetail(RetrieveUpdateDestroyAPIView):
def get_queryset(self): def get_queryset(self):
self.category_slug = self.kwargs.get('category_slug', 'all') self.category_slug = self.kwargs.get('category_slug', 'all')
all_category_slugs = list(settings_registry.get_registered_categories().keys()) all_category_slugs = list(settings_registry.get_registered_categories().keys())
for slug_to_delete in VERSION_SPECIFIC_CATEGORIES_TO_EXCLUDE[get_request_version(self.request)]:
all_category_slugs.remove(slug_to_delete)
if self.request.user.is_superuser or getattr(self.request.user, 'is_system_auditor', False): if self.request.user.is_superuser or getattr(self.request.user, 'is_system_auditor', False):
category_slugs = all_category_slugs category_slugs = all_category_slugs
else: else:
@@ -90,7 +79,6 @@ class SettingSingletonDetail(RetrieveUpdateDestroyAPIView):
registered_settings = settings_registry.get_registered_settings( registered_settings = settings_registry.get_registered_settings(
category_slug=self.category_slug, read_only=False, category_slug=self.category_slug, read_only=False,
slugs_to_ignore=VERSION_SPECIFIC_CATEGORIES_TO_EXCLUDE[get_request_version(self.request)]
) )
if self.category_slug == 'user': if self.category_slug == 'user':
return Setting.objects.filter(key__in=registered_settings, user=self.request.user) return Setting.objects.filter(key__in=registered_settings, user=self.request.user)
@@ -101,7 +89,6 @@ class SettingSingletonDetail(RetrieveUpdateDestroyAPIView):
settings_qs = self.get_queryset() settings_qs = self.get_queryset()
registered_settings = settings_registry.get_registered_settings( registered_settings = settings_registry.get_registered_settings(
category_slug=self.category_slug, category_slug=self.category_slug,
slugs_to_ignore=VERSION_SPECIFIC_CATEGORIES_TO_EXCLUDE[get_request_version(self.request)]
) )
all_settings = {} all_settings = {}
for setting in settings_qs: for setting in settings_qs:

View File

@@ -1001,19 +1001,6 @@ class GroupAccess(BaseAccess):
def can_delete(self, obj): def can_delete(self, obj):
return bool(obj and self.user in obj.inventory.admin_role) return bool(obj and self.user in obj.inventory.admin_role)
def can_start(self, obj, validate_license=True):
# TODO: Delete for 3.3, only used by v1 serializer
# Used as another alias to inventory_source start access for user_capabilities
if obj:
try:
return self.user.can_access(
InventorySource, 'start', obj.deprecated_inventory_source,
validate_license=validate_license)
obj.deprecated_inventory_source
except Group.deprecated_inventory_source.RelatedObjectDoesNotExist:
return False
return False
class InventorySourceAccess(NotificationAttachMixin, BaseAccess): class InventorySourceAccess(NotificationAttachMixin, BaseAccess):
''' '''
@@ -2387,11 +2374,6 @@ class UnifiedJobTemplateAccess(BaseAccess):
Q(inventorysource__inventory__id__in=Inventory._accessible_pk_qs( Q(inventorysource__inventory__id__in=Inventory._accessible_pk_qs(
Inventory, self.user, 'read_role'))) Inventory, self.user, 'read_role')))
def get_queryset(self):
# TODO: remove after the depreciation of v1 API
qs = super(UnifiedJobTemplateAccess, self).get_queryset()
return qs.exclude(inventorysource__source="")
def can_start(self, obj, validate_license=True): def can_start(self, obj, validate_license=True):
access_class = access_registry[obj.__class__] access_class = access_registry[obj.__class__]
access_instance = access_class(self.user) access_instance = access_class(self.user)

View File

@@ -118,7 +118,7 @@ register(
default=_load_default_license_from_file, default=_load_default_license_from_file,
label=_('License'), label=_('License'),
help_text=_('The license controls which features and functionality are ' help_text=_('The license controls which features and functionality are '
'enabled. Use /api/v1/config/ to update or change ' 'enabled. Use /api/v2/config/ to update or change '
'the license.'), 'the license.'),
category=_('System'), category=_('System'),
category_slug='system', category_slug='system',

View File

@@ -638,7 +638,7 @@ class CredentialInputField(JSONSchemaField):
v != '$encrypted$', v != '$encrypted$',
model_instance.pk model_instance.pk
]): ]):
if not isinstance(getattr(model_instance, k), str): if not isinstance(model_instance.inputs.get(k), str):
raise django_exceptions.ValidationError( raise django_exceptions.ValidationError(
_('secret values must be of type string, not {}').format(type(v).__name__), _('secret values must be of type string, not {}').format(type(v).__name__),
code='invalid', code='invalid',
@@ -704,15 +704,15 @@ class CredentialInputField(JSONSchemaField):
# 'ssh_key_unlock': 'do-you-need-me?', # 'ssh_key_unlock': 'do-you-need-me?',
# } # }
# ...we have to fetch the actual key value from the database # ...we have to fetch the actual key value from the database
if model_instance.pk and model_instance.ssh_key_data == '$encrypted$': if model_instance.pk and model_instance.inputs.get('ssh_key_data') == '$encrypted$':
model_instance.ssh_key_data = model_instance.__class__.objects.get( model_instance.inputs['ssh_key_data'] = model_instance.__class__.objects.get(
pk=model_instance.pk pk=model_instance.pk
).ssh_key_data ).inputs.get('ssh_key_data')
if model_instance.has_encrypted_ssh_key_data and not value.get('ssh_key_unlock'): if model_instance.has_encrypted_ssh_key_data and not value.get('ssh_key_unlock'):
errors['ssh_key_unlock'] = [_('must be set when SSH key is encrypted.')] errors['ssh_key_unlock'] = [_('must be set when SSH key is encrypted.')]
if all([ if all([
model_instance.ssh_key_data, model_instance.inputs.get('ssh_key_data'),
value.get('ssh_key_unlock'), value.get('ssh_key_unlock'),
not model_instance.has_encrypted_ssh_key_data not model_instance.has_encrypted_ssh_key_data
]): ]):

View File

@@ -34,7 +34,7 @@ class Command(BaseCommand):
scm_update_cache_timeout=0, scm_update_cache_timeout=0,
organization=o) organization=o)
p.save(skip_update=True) p.save(skip_update=True)
ssh_type = CredentialType.from_v1_kind('ssh') ssh_type = CredentialType.objects.filter(namespace='ssh').first()
c = Credential.objects.create(credential_type=ssh_type, c = Credential.objects.create(credential_type=ssh_type,
name='Demo Credential', name='Demo Credential',
inputs={ inputs={

View File

@@ -16,7 +16,7 @@ from awx.main.models.organization import ( # noqa
Organization, Profile, Team, UserSessionMembership Organization, Profile, Team, UserSessionMembership
) )
from awx.main.models.credential import ( # noqa from awx.main.models.credential import ( # noqa
Credential, CredentialType, CredentialInputSource, ManagedCredentialType, V1Credential, build_safe_env Credential, CredentialType, CredentialInputSource, ManagedCredentialType, build_safe_env
) )
from awx.main.models.projects import Project, ProjectUpdate # noqa from awx.main.models.projects import Project, ProjectUpdate # noqa
from awx.main.models.inventory import ( # noqa from awx.main.models.inventory import ( # noqa
@@ -174,9 +174,6 @@ User.add_to_class('is_in_enterprise_category', user_is_in_enterprise_category)
def o_auth2_application_get_absolute_url(self, request=None): def o_auth2_application_get_absolute_url(self, request=None):
# this page does not exist in v1
if request.version == 'v1':
return reverse('api:o_auth2_application_detail', kwargs={'pk': self.pk}) # use default version
return reverse('api:o_auth2_application_detail', kwargs={'pk': self.pk}, request=request) return reverse('api:o_auth2_application_detail', kwargs={'pk': self.pk}, request=request)
@@ -184,9 +181,6 @@ OAuth2Application.add_to_class('get_absolute_url', o_auth2_application_get_absol
def o_auth2_token_get_absolute_url(self, request=None): def o_auth2_token_get_absolute_url(self, request=None):
# this page does not exist in v1
if request.version == 'v1':
return reverse('api:o_auth2_token_detail', kwargs={'pk': self.pk}) # use default version
return reverse('api:o_auth2_token_detail', kwargs={'pk': self.pk}, request=request) return reverse('api:o_auth2_token_detail', kwargs={'pk': self.pk}, request=request)

View File

@@ -42,7 +42,7 @@ from awx.main.models.rbac import (
from awx.main.utils import encrypt_field from awx.main.utils import encrypt_field
from . import injectors as builtin_injectors from . import injectors as builtin_injectors
__all__ = ['Credential', 'CredentialType', 'CredentialInputSource', 'V1Credential', 'build_safe_env'] __all__ = ['Credential', 'CredentialType', 'CredentialInputSource', 'build_safe_env']
logger = logging.getLogger('awx.main.models.credential') logger = logging.getLogger('awx.main.models.credential')
credential_plugins = dict( credential_plugins = dict(
@@ -73,164 +73,6 @@ def build_safe_env(env):
return safe_env return safe_env
class V1Credential(object):
#
# API v1 backwards compat; as long as we continue to support the
# /api/v1/credentials/ endpoint, we'll keep these definitions around.
# The credential serializers are smart enough to detect the request
# version and use *these* fields for constructing the serializer if the URL
# starts with /api/v1/
#
PASSWORD_FIELDS = ('password', 'security_token', 'ssh_key_data',
'ssh_key_unlock', 'become_password',
'vault_password', 'secret', 'authorize_password')
KIND_CHOICES = [
('ssh', 'Machine'),
('net', 'Network'),
('scm', 'Source Control'),
('aws', 'Amazon Web Services'),
('vmware', 'VMware vCenter'),
('satellite6', 'Red Hat Satellite 6'),
('cloudforms', 'Red Hat CloudForms'),
('gce', 'Google Compute Engine'),
('azure_rm', 'Microsoft Azure Resource Manager'),
('openstack', 'OpenStack'),
('rhv', 'Red Hat Virtualization'),
('insights', 'Insights'),
('tower', 'Ansible Tower'),
]
FIELDS = {
'kind': models.CharField(
max_length=32,
choices=[
(kind[0], _(kind[1]))
for kind in KIND_CHOICES
],
default='ssh',
),
'cloud': models.BooleanField(
default=False,
editable=False,
),
'host': models.CharField(
blank=True,
default='',
max_length=1024,
verbose_name=_('Host'),
help_text=_('The hostname or IP address to use.'),
),
'username': models.CharField(
blank=True,
default='',
max_length=1024,
verbose_name=_('Username'),
help_text=_('Username for this credential.'),
),
'password': models.CharField(
blank=True,
default='',
max_length=1024,
verbose_name=_('Password'),
help_text=_('Password for this credential (or "ASK" to prompt the '
'user for machine credentials).'),
),
'security_token': models.CharField(
blank=True,
default='',
max_length=1024,
verbose_name=_('Security Token'),
help_text=_('Security Token for this credential'),
),
'project': models.CharField(
blank=True,
default='',
max_length=100,
verbose_name=_('Project'),
help_text=_('The identifier for the project.'),
),
'domain': models.CharField(
blank=True,
default='',
max_length=100,
verbose_name=_('Domain'),
help_text=_('The identifier for the domain.'),
),
'ssh_key_data': models.TextField(
blank=True,
default='',
verbose_name=_('SSH private key'),
help_text=_('RSA or DSA private key to be used instead of password.'),
),
'ssh_key_unlock': models.CharField(
max_length=1024,
blank=True,
default='',
verbose_name=_('SSH key unlock'),
help_text=_('Passphrase to unlock SSH private key if encrypted (or '
'"ASK" to prompt the user for machine credentials).'),
),
'become_method': models.CharField(
max_length=32,
blank=True,
default='',
help_text=_('Privilege escalation method.')
),
'become_username': models.CharField(
max_length=1024,
blank=True,
default='',
help_text=_('Privilege escalation username.'),
),
'become_password': models.CharField(
max_length=1024,
blank=True,
default='',
help_text=_('Password for privilege escalation method.')
),
'vault_password': models.CharField(
max_length=1024,
blank=True,
default='',
help_text=_('Vault password (or "ASK" to prompt the user).'),
),
'authorize': models.BooleanField(
default=False,
help_text=_('Whether to use the authorize mechanism.'),
),
'authorize_password': models.CharField(
max_length=1024,
blank=True,
default='',
help_text=_('Password used by the authorize mechanism.'),
),
'client': models.CharField(
max_length=128,
blank=True,
default='',
help_text=_('Client Id or Application Id for the credential'),
),
'secret': models.CharField(
max_length=1024,
blank=True,
default='',
help_text=_('Secret Token for this credential'),
),
'subscription': models.CharField(
max_length=1024,
blank=True,
default='',
help_text=_('Subscription identifier for this credential'),
),
'tenant': models.CharField(
max_length=1024,
blank=True,
default='',
help_text=_('Tenant identifier for this credential'),
)
}
class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin): class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
''' '''
A credential contains information about how to talk to a remote resource A credential contains information about how to talk to a remote resource
@@ -286,34 +128,9 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
'admin_role', 'admin_role',
]) ])
def __getattr__(self, item):
if item != 'inputs':
if item in V1Credential.FIELDS:
return self.inputs.get(item, V1Credential.FIELDS[item].default)
elif item in self.inputs:
return self.inputs[item]
raise AttributeError(item)
def __setattr__(self, item, value):
if item in V1Credential.FIELDS and item in self.credential_type.defined_fields:
if value:
self.inputs[item] = value
elif item in self.inputs:
del self.inputs[item]
return
super(Credential, self).__setattr__(item, value)
@property @property
def kind(self): def kind(self):
# TODO 3.3: remove the need for this helper property by removing its return self.credential_type.namespace
# usage throughout the codebase
type_ = self.credential_type
if type_.kind != 'cloud':
return type_.kind
for field in V1Credential.KIND_CHOICES:
kind, name = field
if name == type_.name:
return kind
@property @property
def cloud(self): def cloud(self):
@@ -330,7 +147,7 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
# #
@property @property
def needs_ssh_password(self): def needs_ssh_password(self):
return self.credential_type.kind == 'ssh' and self.password == 'ASK' return self.credential_type.kind == 'ssh' and self.inputs.get('password') == 'ASK'
@property @property
def has_encrypted_ssh_key_data(self): def has_encrypted_ssh_key_data(self):
@@ -350,17 +167,17 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
@property @property
def needs_ssh_key_unlock(self): def needs_ssh_key_unlock(self):
if self.credential_type.kind == 'ssh' and self.ssh_key_unlock in ('ASK', ''): if self.credential_type.kind == 'ssh' and self.inputs.get('ssh_key_unlock') in ('ASK', ''):
return self.has_encrypted_ssh_key_data return self.has_encrypted_ssh_key_data
return False return False
@property @property
def needs_become_password(self): def needs_become_password(self):
return self.credential_type.kind == 'ssh' and self.become_password == 'ASK' return self.credential_type.kind == 'ssh' and self.inputs.get('become_password') == 'ASK'
@property @property
def needs_vault_password(self): def needs_vault_password(self):
return self.credential_type.kind == 'vault' and self.vault_password == 'ASK' return self.credential_type.kind == 'vault' and self.inputs.get('vault_password') == 'ASK'
@property @property
def passwords_needed(self): def passwords_needed(self):
@@ -396,6 +213,10 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
super(Credential, self).save(*args, **kwargs) super(Credential, self).save(*args, **kwargs)
def mark_field_for_save(self, update_fields, field):
if 'inputs' not in update_fields:
update_fields.append('inputs')
def encrypt_field(self, field, ask): def encrypt_field(self, field, ask):
if field not in self.inputs: if field not in self.inputs:
return None return None
@@ -405,13 +226,6 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
elif field in self.inputs: elif field in self.inputs:
del self.inputs[field] del self.inputs[field]
def mark_field_for_save(self, update_fields, field):
if field in self.credential_type.secret_fields:
# If we've encrypted a v1 field, we actually want to persist
# self.inputs
field = 'inputs'
super(Credential, self).mark_field_for_save(update_fields, field)
def display_inputs(self): def display_inputs(self):
field_val = self.inputs.copy() field_val = self.inputs.copy()
for k, v in field_val.items(): for k, v in field_val.items():
@@ -429,7 +243,7 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
type_alias = self.credential_type.name type_alias = self.credential_type.name
else: else:
type_alias = self.credential_type_id type_alias = self.credential_type_id
if self.kind == 'vault' and self.has_input('vault_id'): if self.credential_type.kind == 'vault' and self.has_input('vault_id'):
if display: if display:
fmt_str = '{} (id={})' fmt_str = '{} (id={})'
else: else:
@@ -456,7 +270,7 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
:param field_name(str): The name of the input field. :param field_name(str): The name of the input field.
:param default(optional[str]): A default return value to use. :param default(optional[str]): A default return value to use.
""" """
if self.kind != 'external' and field_name in self.dynamic_input_fields: if self.credential_type.kind != 'external' and field_name in self.dynamic_input_fields:
return self._get_dynamic_input(field_name) return self._get_dynamic_input(field_name)
if field_name in self.credential_type.secret_fields: if field_name in self.credential_type.secret_fields:
try: try:
@@ -552,15 +366,8 @@ class CredentialType(CommonModelNameNotUnique):
return instance return instance
def get_absolute_url(self, request=None): def get_absolute_url(self, request=None):
# Page does not exist in API v1
if request.version == 'v1':
return reverse('api:credential_type_detail', kwargs={'pk': self.pk})
return reverse('api:credential_type_detail', kwargs={'pk': self.pk}, request=request) return reverse('api:credential_type_detail', kwargs={'pk': self.pk}, request=request)
@property
def unique_by_kind(self):
return self.kind != 'cloud'
@property @property
def defined_fields(self): def defined_fields(self):
return [field.get('id') for field in self.inputs.get('fields', [])] return [field.get('id') for field in self.inputs.get('fields', [])]
@@ -629,29 +436,6 @@ class CredentialType(CommonModelNameNotUnique):
inputs=plugin.inputs inputs=plugin.inputs
) )
@classmethod
def from_v1_kind(cls, kind, data={}):
match = None
kind = kind or 'ssh'
kind_choices = dict(V1Credential.KIND_CHOICES)
requirements = {}
if kind == 'ssh':
if data.get('vault_password'):
requirements['kind'] = 'vault'
else:
requirements['kind'] = 'ssh'
elif kind in ('net', 'scm', 'insights'):
requirements['kind'] = kind
elif kind in kind_choices:
requirements.update(dict(
kind='cloud',
name=kind_choices[kind]
))
if requirements:
requirements['managed_by_tower'] = True
match = cls.objects.filter(**requirements)[:1].get()
return match
def inject_credential(self, credential, env, safe_env, args, private_data_dir): def inject_credential(self, credential, env, safe_env, args, private_data_dir):
""" """
Inject credential data into the environment variables and arguments Inject credential data into the environment variables and arguments
@@ -678,9 +462,11 @@ class CredentialType(CommonModelNameNotUnique):
files) files)
""" """
if not self.injectors: if not self.injectors:
if self.managed_by_tower and credential.kind in dir(builtin_injectors): if self.managed_by_tower and credential.credential_type.namespace in dir(builtin_injectors):
injected_env = {} injected_env = {}
getattr(builtin_injectors, credential.kind)(credential, injected_env, private_data_dir) getattr(builtin_injectors, credential.credential_type.namespace)(
credential, injected_env, private_data_dir
)
env.update(injected_env) env.update(injected_env)
safe_env.update(build_safe_env(injected_env)) safe_env.update(build_safe_env(injected_env))
return return
@@ -1335,12 +1121,12 @@ class CredentialInputSource(PrimordialModel):
) )
def clean_target_credential(self): def clean_target_credential(self):
if self.target_credential.kind == 'external': if self.target_credential.credential_type.kind == 'external':
raise ValidationError(_('Target must be a non-external credential')) raise ValidationError(_('Target must be a non-external credential'))
return self.target_credential return self.target_credential
def clean_source_credential(self): def clean_source_credential(self):
if self.source_credential.kind != 'external': if self.source_credential.credential_type.kind != 'external':
raise ValidationError(_('Source must be an external credential')) raise ValidationError(_('Source must be an external credential'))
return self.source_credential return self.source_credential

View File

@@ -18,7 +18,7 @@ from django.db import models
from django.utils.encoding import smart_str from django.utils.encoding import smart_str
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ValidationError, FieldDoesNotExist from django.core.exceptions import FieldDoesNotExist
# REST Framework # REST Framework
from rest_framework.exceptions import ParseError from rest_framework.exceptions import ParseError
@@ -152,21 +152,9 @@ class JobOptions(BaseModel):
extra_vars_dict = VarsDictProperty('extra_vars', True) extra_vars_dict = VarsDictProperty('extra_vars', True)
def clean_credential(self): @property
cred = self.credential def machine_credential(self):
if cred and cred.kind != 'ssh': return self.credentials.filter(credential_type__kind='ssh').first()
raise ValidationError(
_('You must provide an SSH credential.'),
)
return cred
def clean_vault_credential(self):
cred = self.vault_credential
if cred and cred.kind != 'vault':
raise ValidationError(
_('You must provide a Vault credential.'),
)
return cred
@property @property
def network_credentials(self): def network_credentials(self):
@@ -180,41 +168,6 @@ class JobOptions(BaseModel):
def vault_credentials(self): def vault_credentials(self):
return list(self.credentials.filter(credential_type__kind='vault')) return list(self.credentials.filter(credential_type__kind='vault'))
@property
def credential(self):
cred = self.get_deprecated_credential('ssh')
if cred is not None:
return cred.pk
@property
def vault_credential(self):
cred = self.get_deprecated_credential('vault')
if cred is not None:
return cred.pk
def get_deprecated_credential(self, kind):
for cred in self.credentials.all():
if cred.credential_type.kind == kind:
return cred
else:
return None
# TODO: remove when API v1 is removed
@property
def cloud_credential(self):
try:
return self.cloud_credentials[-1].pk
except IndexError:
return None
# TODO: remove when API v1 is removed
@property
def network_credential(self):
try:
return self.network_credentials[-1].pk
except IndexError:
return None
@property @property
def passwords_needed_to_start(self): def passwords_needed_to_start(self):
'''Return list of password field names needed to start the job.''' '''Return list of password field names needed to start the job.'''
@@ -707,7 +660,7 @@ class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin, TaskMana
data.update(dict(inventory=self.inventory.name if self.inventory else None, data.update(dict(inventory=self.inventory.name if self.inventory else None,
project=self.project.name if self.project else None, project=self.project.name if self.project else None,
playbook=self.playbook, playbook=self.playbook,
credential=getattr(self.get_deprecated_credential('ssh'), 'name', None), credential=getattr(self.machine_credential, 'name', None),
limit=self.limit, limit=self.limit,
extra_vars=self.display_extra_vars(), extra_vars=self.display_extra_vars(),
hosts=all_hosts)) hosts=all_hosts))

View File

@@ -794,7 +794,7 @@ class BaseTask(object):
data += '\n' data += '\n'
# For credentials used with ssh-add, write to a named pipe which # For credentials used with ssh-add, write to a named pipe which
# will be read then closed, instead of leaving the SSH key on disk. # will be read then closed, instead of leaving the SSH key on disk.
if credential and credential.kind in ('ssh', 'scm') and not ssh_too_old: if credential and credential.credential_type.namespace in ('ssh', 'scm') and not ssh_too_old:
try: try:
os.mkdir(os.path.join(private_data_dir, 'env')) os.mkdir(os.path.join(private_data_dir, 'env'))
except OSError as e: except OSError as e:
@@ -1324,7 +1324,7 @@ class RunJob(BaseTask):
and ansible-vault. and ansible-vault.
''' '''
passwords = super(RunJob, self).build_passwords(job, runtime_passwords) passwords = super(RunJob, self).build_passwords(job, runtime_passwords)
cred = job.get_deprecated_credential('ssh') cred = job.machine_credential
if cred: if cred:
for field in ('ssh_key_unlock', 'ssh_password', 'become_password', 'vault_password'): for field in ('ssh_key_unlock', 'ssh_password', 'become_password', 'vault_password'):
value = runtime_passwords.get(field, cred.get_input('password' if field == 'ssh_password' else field, default='')) value = runtime_passwords.get(field, cred.get_input('password' if field == 'ssh_password' else field, default=''))
@@ -1408,6 +1408,9 @@ class RunJob(BaseTask):
# Set environment variables for cloud credentials. # Set environment variables for cloud credentials.
cred_files = private_data_files.get('credentials', {}) cred_files = private_data_files.get('credentials', {})
for cloud_cred in job.cloud_credentials:
if cloud_cred and cloud_cred.credential_type.namespace == 'openstack':
env['OS_CLIENT_CONFIG_FILE'] = cred_files.get(cloud_cred, '')
for network_cred in job.network_credentials: for network_cred in job.network_credentials:
env['ANSIBLE_NET_USERNAME'] = network_cred.get_input('username', default='') env['ANSIBLE_NET_USERNAME'] = network_cred.get_input('username', default='')
@@ -1429,7 +1432,7 @@ class RunJob(BaseTask):
Build command line argument list for running ansible-playbook, Build command line argument list for running ansible-playbook,
optionally using ssh-agent for public/private key authentication. optionally using ssh-agent for public/private key authentication.
''' '''
creds = job.get_deprecated_credential('ssh') creds = job.machine_credential
ssh_username, become_username, become_method = '', '', '' ssh_username, become_username, become_method = '', '', ''
if creds: if creds:
@@ -2228,9 +2231,9 @@ class RunAdHocCommand(BaseTask):
creds = ad_hoc_command.credential creds = ad_hoc_command.credential
ssh_username, become_username, become_method = '', '', '' ssh_username, become_username, become_method = '', '', ''
if creds: if creds:
ssh_username = creds.username ssh_username = creds.get_input('username', default='')
become_method = creds.become_method become_method = creds.get_input('become_method', default='')
become_username = creds.become_username become_username = creds.get_input('become_username', default='')
else: else:
become_method = None become_method = None
become_username = "" become_username = ""

View File

@@ -168,7 +168,7 @@ def mk_job_template(name, job_type='run',
if persisted and credential: if persisted and credential:
jt.save() jt.save()
jt.credentials.add(credential) jt.credentials.add(credential)
if jt.credential is None: if jt.machine_credential is None:
jt.ask_credential_on_launch = True jt.ask_credential_on_launch = True
jt.project = project jt.project = project

View File

@@ -1,4 +1,3 @@
import itertools
import re import re
from unittest import mock # noqa from unittest import mock # noqa
@@ -27,197 +26,6 @@ def test_idempotent_credential_type_setup():
assert CredentialType.objects.count() == total assert CredentialType.objects.count() == total
@pytest.mark.django_db
@pytest.mark.parametrize('kind, total', [
('ssh', 1), ('net', 0)
])
def test_filter_by_v1_kind(get, admin, organization, kind, total):
CredentialType.setup_tower_managed_defaults()
cred = Credential(
credential_type=CredentialType.from_v1_kind('ssh'),
name='Best credential ever',
organization=organization,
inputs={
'username': u'jim',
'password': u'secret'
}
)
cred.save()
response = get(
reverse('api:credential_list', kwargs={'version': 'v1'}),
admin,
QUERY_STRING='kind=%s' % kind
)
assert response.status_code == 200
assert response.data['count'] == total
@pytest.mark.django_db
def test_filter_by_v1_kind_with_vault(get, admin, organization):
CredentialType.setup_tower_managed_defaults()
cred = Credential(
credential_type=CredentialType.objects.get(kind='ssh'),
name='Best credential ever',
organization=organization,
inputs={
'username': u'jim',
'password': u'secret'
}
)
cred.save()
cred = Credential(
credential_type=CredentialType.objects.get(kind='vault'),
name='Best credential ever',
organization=organization,
inputs={
'vault_password': u'vault!'
}
)
cred.save()
response = get(
reverse('api:credential_list', kwargs={'version': 'v1'}),
admin,
QUERY_STRING='kind=ssh'
)
assert response.status_code == 200
assert response.data['count'] == 2
@pytest.mark.django_db
def test_insights_credentials_in_v1_api_list(get, admin, organization):
credential_type = CredentialType.defaults['insights']()
credential_type.save()
cred = Credential(
credential_type=credential_type,
name='Best credential ever',
organization=organization,
inputs={
'username': u'joe',
'password': u'secret'
}
)
cred.save()
response = get(
reverse('api:credential_list', kwargs={'version': 'v1'}),
admin
)
assert response.status_code == 200
assert response.data['count'] == 1
cred = response.data['results'][0]
assert cred['kind'] == 'insights'
assert cred['username'] == 'joe'
assert cred['password'] == '$encrypted$'
@pytest.mark.django_db
def test_create_insights_credentials_in_v1(get, post, admin, organization):
credential_type = CredentialType.defaults['insights']()
credential_type.save()
response = post(
reverse('api:credential_list', kwargs={'version': 'v1'}),
{
'name': 'Best Credential Ever',
'organization': organization.id,
'kind': 'insights',
'username': 'joe',
'password': 'secret'
},
admin
)
assert response.status_code == 201
cred = Credential.objects.get(pk=response.data['id'])
assert cred.username == 'joe'
assert decrypt_field(cred, 'password') == 'secret'
assert cred.credential_type == credential_type
@pytest.mark.django_db
def test_custom_credentials_not_in_v1_api_list(get, admin, organization):
"""
'Custom' credentials (those not managed by Tower) shouldn't be visible from
the V1 credentials API list
"""
credential_type = CredentialType(
kind='cloud',
name='MyCloud',
inputs = {
'fields': [{
'id': 'password',
'label': 'Password',
'type': 'string',
'secret': True
}]
}
)
credential_type.save()
cred = Credential(
credential_type=credential_type,
name='Best credential ever',
organization=organization,
inputs={
'password': u'secret'
}
)
cred.save()
response = get(
reverse('api:credential_list', kwargs={'version': 'v1'}),
admin
)
assert response.status_code == 200
assert response.data['count'] == 0
@pytest.mark.django_db
def test_custom_credentials_not_in_v1_api_detail(get, admin, organization):
"""
'Custom' credentials (those not managed by Tower) shouldn't be visible from
the V1 credentials API detail
"""
credential_type = CredentialType(
kind='cloud',
name='MyCloud',
inputs = {
'fields': [{
'id': 'password',
'label': 'Password',
'type': 'string',
'secret': True
}]
}
)
credential_type.save()
cred = Credential(
credential_type=credential_type,
name='Best credential ever',
organization=organization,
inputs={
'password': u'secret'
}
)
cred.save()
response = get(
reverse('api:credential_detail', kwargs={'version': 'v1', 'pk': cred.pk}),
admin
)
assert response.status_code == 404
@pytest.mark.django_db
def test_filter_by_v1_invalid_kind(get, admin, organization):
response = get(
reverse('api:credential_list', kwargs={'version': 'v1'}),
admin,
QUERY_STRING='kind=bad_kind'
)
assert response.status_code == 400
# #
# user credential creation # user credential creation
# #
@@ -225,7 +33,6 @@ def test_filter_by_v1_invalid_kind(get, admin, organization):
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version, params', [ @pytest.mark.parametrize('version, params', [
['v1', {'username': 'someusername'}],
['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}] ['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}]
]) ])
def test_create_user_credential_via_credentials_list(post, get, alice, credentialtype_ssh, version, params): def test_create_user_credential_via_credentials_list(post, get, alice, credentialtype_ssh, version, params):
@@ -245,7 +52,6 @@ def test_create_user_credential_via_credentials_list(post, get, alice, credentia
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version, params', [ @pytest.mark.parametrize('version, params', [
['v1', {'username': 'someusername'}],
['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}] ['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}]
]) ])
def test_credential_validation_error_with_bad_user(post, admin, version, credentialtype_ssh, params): def test_credential_validation_error_with_bad_user(post, admin, version, credentialtype_ssh, params):
@@ -262,7 +68,6 @@ def test_credential_validation_error_with_bad_user(post, admin, version, credent
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version, params', [ @pytest.mark.parametrize('version, params', [
['v1', {'username': 'someusername'}],
['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}] ['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}]
]) ])
def test_create_user_credential_via_user_credentials_list(post, get, alice, credentialtype_ssh, version, params): def test_create_user_credential_via_user_credentials_list(post, get, alice, credentialtype_ssh, version, params):
@@ -282,7 +87,6 @@ def test_create_user_credential_via_user_credentials_list(post, get, alice, cred
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version, params', [ @pytest.mark.parametrize('version, params', [
['v1', {'username': 'someusername'}],
['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}] ['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}]
]) ])
def test_create_user_credential_via_credentials_list_xfail(post, alice, bob, version, params): def test_create_user_credential_via_credentials_list_xfail(post, alice, bob, version, params):
@@ -298,7 +102,6 @@ def test_create_user_credential_via_credentials_list_xfail(post, alice, bob, ver
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version, params', [ @pytest.mark.parametrize('version, params', [
['v1', {'username': 'someusername'}],
['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}] ['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}]
]) ])
def test_create_user_credential_via_user_credentials_list_xfail(post, alice, bob, version, params): def test_create_user_credential_via_user_credentials_list_xfail(post, alice, bob, version, params):
@@ -319,7 +122,6 @@ def test_create_user_credential_via_user_credentials_list_xfail(post, alice, bob
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version, params', [ @pytest.mark.parametrize('version, params', [
['v1', {'username': 'someusername'}],
['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}] ['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}]
]) ])
def test_create_team_credential(post, get, team, organization, org_admin, team_member, credentialtype_ssh, version, params): def test_create_team_credential(post, get, team, organization, org_admin, team_member, credentialtype_ssh, version, params):
@@ -345,7 +147,6 @@ def test_create_team_credential(post, get, team, organization, org_admin, team_m
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version, params', [ @pytest.mark.parametrize('version, params', [
['v1', {'username': 'someusername'}],
['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}] ['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}]
]) ])
def test_create_team_credential_via_team_credentials_list(post, get, team, org_admin, team_member, credentialtype_ssh, version, params): def test_create_team_credential_via_team_credentials_list(post, get, team, org_admin, team_member, credentialtype_ssh, version, params):
@@ -368,7 +169,6 @@ def test_create_team_credential_via_team_credentials_list(post, get, team, org_a
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version, params', [ @pytest.mark.parametrize('version, params', [
['v1', {'username': 'someusername'}],
['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}] ['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}]
]) ])
def test_create_team_credential_by_urelated_user_xfail(post, team, organization, alice, team_member, version, params): def test_create_team_credential_by_urelated_user_xfail(post, team, organization, alice, team_member, version, params):
@@ -385,7 +185,6 @@ def test_create_team_credential_by_urelated_user_xfail(post, team, organization,
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version, params', [ @pytest.mark.parametrize('version, params', [
['v1', {'username': 'someusername'}],
['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}] ['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}]
]) ])
def test_create_team_credential_by_team_member_xfail(post, team, organization, alice, team_member, version, params): def test_create_team_credential_by_team_member_xfail(post, team, organization, alice, team_member, version, params):
@@ -407,7 +206,7 @@ def test_create_team_credential_by_team_member_xfail(post, team, organization, a
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version', ['v1', 'v2']) @pytest.mark.parametrize('version', ['v2'])
def test_grant_org_credential_to_org_user_through_role_users(post, credential, organization, org_admin, org_member, version): def test_grant_org_credential_to_org_user_through_role_users(post, credential, organization, org_admin, org_member, version):
credential.organization = organization credential.organization = organization
credential.save() credential.save()
@@ -418,7 +217,7 @@ def test_grant_org_credential_to_org_user_through_role_users(post, credential, o
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version', ['v1', 'v2']) @pytest.mark.parametrize('version', ['v2'])
def test_grant_org_credential_to_org_user_through_user_roles(post, credential, organization, org_admin, org_member, version): def test_grant_org_credential_to_org_user_through_user_roles(post, credential, organization, org_admin, org_member, version):
credential.organization = organization credential.organization = organization
credential.save() credential.save()
@@ -429,7 +228,7 @@ def test_grant_org_credential_to_org_user_through_user_roles(post, credential, o
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version', ['v1', 'v2']) @pytest.mark.parametrize('version', ['v2'])
def test_grant_org_credential_to_non_org_user_through_role_users(post, credential, organization, org_admin, alice, version): def test_grant_org_credential_to_non_org_user_through_role_users(post, credential, organization, org_admin, alice, version):
credential.organization = organization credential.organization = organization
credential.save() credential.save()
@@ -440,7 +239,7 @@ def test_grant_org_credential_to_non_org_user_through_role_users(post, credentia
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version', ['v1', 'v2']) @pytest.mark.parametrize('version', ['v2'])
def test_grant_org_credential_to_non_org_user_through_user_roles(post, credential, organization, org_admin, alice, version): def test_grant_org_credential_to_non_org_user_through_user_roles(post, credential, organization, org_admin, alice, version):
credential.organization = organization credential.organization = organization
credential.save() credential.save()
@@ -451,7 +250,7 @@ def test_grant_org_credential_to_non_org_user_through_user_roles(post, credentia
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version', ['v1', 'v2']) @pytest.mark.parametrize('version', ['v2'])
def test_grant_private_credential_to_user_through_role_users(post, credential, alice, bob, version): def test_grant_private_credential_to_user_through_role_users(post, credential, alice, bob, version):
# normal users can't do this # normal users can't do this
credential.admin_role.members.add(alice) credential.admin_role.members.add(alice)
@@ -462,7 +261,7 @@ def test_grant_private_credential_to_user_through_role_users(post, credential, a
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version', ['v1', 'v2']) @pytest.mark.parametrize('version', ['v2'])
def test_grant_private_credential_to_org_user_through_role_users(post, credential, org_admin, org_member, version): def test_grant_private_credential_to_org_user_through_role_users(post, credential, org_admin, org_member, version):
# org admins can't either # org admins can't either
credential.admin_role.members.add(org_admin) credential.admin_role.members.add(org_admin)
@@ -473,7 +272,7 @@ def test_grant_private_credential_to_org_user_through_role_users(post, credentia
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version', ['v1', 'v2']) @pytest.mark.parametrize('version', ['v2'])
def test_sa_grant_private_credential_to_user_through_role_users(post, credential, admin, bob, version): def test_sa_grant_private_credential_to_user_through_role_users(post, credential, admin, bob, version):
# but system admins can # but system admins can
response = post(reverse('api:role_users_list', kwargs={'version': version, 'pk': credential.use_role.id}), { response = post(reverse('api:role_users_list', kwargs={'version': version, 'pk': credential.use_role.id}), {
@@ -483,7 +282,7 @@ def test_sa_grant_private_credential_to_user_through_role_users(post, credential
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version', ['v1', 'v2']) @pytest.mark.parametrize('version', ['v2'])
def test_grant_private_credential_to_user_through_user_roles(post, credential, alice, bob, version): def test_grant_private_credential_to_user_through_user_roles(post, credential, alice, bob, version):
# normal users can't do this # normal users can't do this
credential.admin_role.members.add(alice) credential.admin_role.members.add(alice)
@@ -494,7 +293,7 @@ def test_grant_private_credential_to_user_through_user_roles(post, credential, a
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version', ['v1', 'v2']) @pytest.mark.parametrize('version', ['v2'])
def test_grant_private_credential_to_org_user_through_user_roles(post, credential, org_admin, org_member, version): def test_grant_private_credential_to_org_user_through_user_roles(post, credential, org_admin, org_member, version):
# org admins can't either # org admins can't either
credential.admin_role.members.add(org_admin) credential.admin_role.members.add(org_admin)
@@ -505,7 +304,7 @@ def test_grant_private_credential_to_org_user_through_user_roles(post, credentia
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version', ['v1', 'v2']) @pytest.mark.parametrize('version', ['v2'])
def test_sa_grant_private_credential_to_user_through_user_roles(post, credential, admin, bob, version): def test_sa_grant_private_credential_to_user_through_user_roles(post, credential, admin, bob, version):
# but system admins can # but system admins can
response = post(reverse('api:user_roles_list', kwargs={'version': version, 'pk': bob.id}), { response = post(reverse('api:user_roles_list', kwargs={'version': version, 'pk': bob.id}), {
@@ -515,7 +314,7 @@ def test_sa_grant_private_credential_to_user_through_user_roles(post, credential
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version', ['v1', 'v2']) @pytest.mark.parametrize('version', ['v2'])
def test_grant_org_credential_to_team_through_role_teams(post, credential, organization, org_admin, org_auditor, team, version): def test_grant_org_credential_to_team_through_role_teams(post, credential, organization, org_admin, org_auditor, team, version):
assert org_auditor not in credential.read_role assert org_auditor not in credential.read_role
credential.organization = organization credential.organization = organization
@@ -528,7 +327,7 @@ def test_grant_org_credential_to_team_through_role_teams(post, credential, organ
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version', ['v1', 'v2']) @pytest.mark.parametrize('version', ['v2'])
def test_grant_org_credential_to_team_through_team_roles(post, credential, organization, org_admin, org_auditor, team, version): def test_grant_org_credential_to_team_through_team_roles(post, credential, organization, org_admin, org_auditor, team, version):
assert org_auditor not in credential.read_role assert org_auditor not in credential.read_role
credential.organization = organization credential.organization = organization
@@ -541,7 +340,7 @@ def test_grant_org_credential_to_team_through_team_roles(post, credential, organ
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version', ['v1', 'v2']) @pytest.mark.parametrize('version', ['v2'])
def test_sa_grant_private_credential_to_team_through_role_teams(post, credential, admin, team, version): def test_sa_grant_private_credential_to_team_through_role_teams(post, credential, admin, team, version):
# not even a system admin can grant a private cred to a team though # not even a system admin can grant a private cred to a team though
response = post(reverse('api:role_teams_list', kwargs={'version': version, 'pk': credential.use_role.id}), { response = post(reverse('api:role_teams_list', kwargs={'version': version, 'pk': credential.use_role.id}), {
@@ -551,7 +350,7 @@ def test_sa_grant_private_credential_to_team_through_role_teams(post, credential
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version', ['v1', 'v2']) @pytest.mark.parametrize('version', ['v2'])
def test_sa_grant_private_credential_to_team_through_team_roles(post, credential, admin, team, version): def test_sa_grant_private_credential_to_team_through_team_roles(post, credential, admin, team, version):
# not even a system admin can grant a private cred to a team though # not even a system admin can grant a private cred to a team though
response = post(reverse('api:role_teams_list', kwargs={'version': version, 'pk': team.id}), { response = post(reverse('api:role_teams_list', kwargs={'version': version, 'pk': team.id}), {
@@ -567,7 +366,6 @@ def test_sa_grant_private_credential_to_team_through_team_roles(post, credential
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version, params', [ @pytest.mark.parametrize('version, params', [
['v1', {'username': 'someusername'}],
['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}] ['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}]
]) ])
def test_create_org_credential_as_not_admin(post, organization, org_member, credentialtype_ssh, version, params): def test_create_org_credential_as_not_admin(post, organization, org_member, credentialtype_ssh, version, params):
@@ -583,7 +381,6 @@ def test_create_org_credential_as_not_admin(post, organization, org_member, cred
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version, params', [ @pytest.mark.parametrize('version, params', [
['v1', {'username': 'someusername'}],
['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}] ['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}]
]) ])
def test_create_org_credential_as_admin(post, organization, org_admin, credentialtype_ssh, version, params): def test_create_org_credential_as_admin(post, organization, org_admin, credentialtype_ssh, version, params):
@@ -599,7 +396,6 @@ def test_create_org_credential_as_admin(post, organization, org_admin, credentia
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version, params', [ @pytest.mark.parametrize('version, params', [
['v1', {'username': 'someusername'}],
['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}] ['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}]
]) ])
def test_credential_detail(post, get, organization, org_admin, credentialtype_ssh, version, params): def test_credential_detail(post, get, organization, org_admin, credentialtype_ssh, version, params):
@@ -624,7 +420,6 @@ def test_credential_detail(post, get, organization, org_admin, credentialtype_ss
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version, params', [ @pytest.mark.parametrize('version, params', [
['v1', {'username': 'someusername'}],
['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}] ['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}]
]) ])
def test_list_created_org_credentials(post, get, organization, org_admin, org_member, credentialtype_ssh, version, params): def test_list_created_org_credentials(post, get, organization, org_admin, org_member, credentialtype_ssh, version, params):
@@ -667,12 +462,11 @@ def test_list_created_org_credentials(post, get, organization, org_admin, org_me
@pytest.mark.parametrize('order_by', ('password', '-password', 'password,pk', '-password,pk')) @pytest.mark.parametrize('order_by', ('password', '-password', 'password,pk', '-password,pk'))
@pytest.mark.parametrize('version', ('v1', 'v2'))
@pytest.mark.django_db @pytest.mark.django_db
def test_list_cannot_order_by_encrypted_field(post, get, organization, org_admin, credentialtype_ssh, order_by, version): def test_list_cannot_order_by_encrypted_field(post, get, organization, org_admin, credentialtype_ssh, order_by):
for i, password in enumerate(('abc', 'def', 'xyz')): for i, password in enumerate(('abc', 'def', 'xyz')):
response = post( response = post(
reverse('api:credential_list', kwargs={'version': version}), reverse('api:credential_list', kwargs={'version': 'v2'}),
{ {
'organization': organization.id, 'organization': organization.id,
'name': 'C%d' % i, 'name': 'C%d' % i,
@@ -682,7 +476,7 @@ def test_list_cannot_order_by_encrypted_field(post, get, organization, org_admin
) )
response = get( response = get(
reverse('api:credential_list', kwargs={'version': version}), reverse('api:credential_list', kwargs={'version': 'v2'}),
org_admin, org_admin,
QUERY_STRING='order_by=%s' % order_by, QUERY_STRING='order_by=%s' % order_by,
status=400 status=400
@@ -690,22 +484,6 @@ def test_list_cannot_order_by_encrypted_field(post, get, organization, org_admin
assert response.status_code == 400 assert response.status_code == 400
@pytest.mark.django_db
def test_v1_credential_kind_validity(get, post, organization, admin, credentialtype_ssh):
params = {
'name': 'Best credential ever',
'organization': organization.id,
'kind': 'nonsense'
}
response = post(
reverse('api:credential_list', kwargs={'version': 'v1'}),
params,
admin
)
assert response.status_code == 400
assert response.data['kind'] == ['"nonsense" is not a valid choice']
@pytest.mark.django_db @pytest.mark.django_db
def test_inputs_cannot_contain_extra_fields(get, post, organization, admin, credentialtype_ssh): def test_inputs_cannot_contain_extra_fields(get, post, organization, admin, credentialtype_ssh):
params = { params = {
@@ -725,34 +503,6 @@ def test_inputs_cannot_contain_extra_fields(get, post, organization, admin, cred
assert "'invalid_field' was unexpected" in response.data['inputs'][0] assert "'invalid_field' was unexpected" in response.data['inputs'][0]
@pytest.mark.django_db
@pytest.mark.parametrize('field_name, field_value', itertools.product(
['username', 'password', 'ssh_key_data', 'become_method', 'become_username', 'become_password'], # noqa
['', None]
))
def test_nullish_field_data(get, post, organization, admin, field_name, field_value):
ssh = CredentialType.defaults['ssh']()
ssh.save()
params = {
'name': 'Best credential ever',
'credential_type': ssh.pk,
'organization': organization.id,
'inputs': {
field_name: field_value
}
}
response = post(
reverse('api:credential_list', kwargs={'version': 'v2'}),
params,
admin
)
assert response.status_code == 201
assert Credential.objects.count() == 1
cred = Credential.objects.all()[:1].get()
assert getattr(cred, field_name) == ''
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('field_value', ['', None, False]) @pytest.mark.parametrize('field_value', ['', None, False])
def test_falsey_field_data(get, post, organization, admin, field_value): def test_falsey_field_data(get, post, organization, admin, field_value):
@@ -776,7 +526,7 @@ def test_falsey_field_data(get, post, organization, admin, field_value):
assert Credential.objects.count() == 1 assert Credential.objects.count() == 1
cred = Credential.objects.all()[:1].get() cred = Credential.objects.all()[:1].get()
assert cred.authorize is False assert cred.inputs['authorize'] is False
@pytest.mark.django_db @pytest.mark.django_db
@@ -811,14 +561,6 @@ def test_field_dependencies(get, post, organization, admin, kind, extraneous):
# #
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version, params', [ @pytest.mark.parametrize('version, params', [
['v1', {
'kind': 'scm',
'name': 'Best credential ever',
'username': 'some_username',
'password': 'some_password',
'ssh_key_data': EXAMPLE_ENCRYPTED_PRIVATE_KEY,
'ssh_key_unlock': 'some_key_unlock',
}],
['v2', { ['v2', {
'credential_type': 1, 'credential_type': 1,
'name': 'Best credential ever', 'name': 'Best credential ever',
@@ -851,12 +593,6 @@ def test_scm_create_ok(post, organization, admin, version, params):
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version, params', [ @pytest.mark.parametrize('version, params', [
['v1', {
'kind': 'ssh',
'name': 'Best credential ever',
'password': 'secret',
'vault_password': '',
}],
['v2', { ['v2', {
'credential_type': 1, 'credential_type': 1,
'name': 'Best credential ever', 'name': 'Best credential ever',
@@ -882,38 +618,11 @@ def test_ssh_create_ok(post, organization, admin, version, params):
assert decrypt_field(cred, 'password') == 'secret' assert decrypt_field(cred, 'password') == 'secret'
@pytest.mark.django_db
def test_v1_ssh_vault_ambiguity(post, organization, admin):
vault = CredentialType.defaults['vault']()
vault.save()
params = {
'organization': organization.id,
'kind': 'ssh',
'name': 'Best credential ever',
'username': 'joe',
'password': 'secret',
'ssh_key_data': 'some_key_data',
'ssh_key_unlock': 'some_key_unlock',
'vault_password': 'vault_password',
}
response = post(
reverse('api:credential_list', kwargs={'version': 'v1'}),
params,
admin
)
assert response.status_code == 400
# #
# Vault Credentials # Vault Credentials
# #
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version, params', [ @pytest.mark.parametrize('version, params', [
['v1', {
'kind': 'ssh',
'name': 'Best credential ever',
'vault_password': 'some_password',
}],
['v2', { ['v2', {
'credential_type': 1, 'credential_type': 1,
'name': 'Best credential ever', 'name': 'Best credential ever',
@@ -968,16 +677,6 @@ def test_vault_password_required(post, organization, admin):
# #
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version, params', [ @pytest.mark.parametrize('version, params', [
['v1', {
'kind': 'net',
'name': 'Best credential ever',
'username': 'some_username',
'password': 'some_password',
'ssh_key_data': EXAMPLE_ENCRYPTED_PRIVATE_KEY,
'ssh_key_unlock': 'some_key_unlock',
'authorize': True,
'authorize_password': 'some_authorize_password',
}],
['v2', { ['v2', {
'credential_type': 1, 'credential_type': 1,
'name': 'Best credential ever', 'name': 'Best credential ever',
@@ -1017,13 +716,6 @@ def test_net_create_ok(post, organization, admin, version, params):
# #
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version, params', [ @pytest.mark.parametrize('version, params', [
['v1', {
'kind': 'cloudforms',
'name': 'Best credential ever',
'host': 'some_host',
'username': 'some_username',
'password': 'some_password',
}],
['v2', { ['v2', {
'credential_type': 1, 'credential_type': 1,
'name': 'Best credential ever', 'name': 'Best credential ever',
@@ -1057,13 +749,6 @@ def test_cloudforms_create_ok(post, organization, admin, version, params):
# #
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version, params', [ @pytest.mark.parametrize('version, params', [
['v1', {
'kind': 'gce',
'name': 'Best credential ever',
'username': 'some_username',
'project': 'some_project',
'ssh_key_data': EXAMPLE_PRIVATE_KEY,
}],
['v2', { ['v2', {
'credential_type': 1, 'credential_type': 1,
'name': 'Best credential ever', 'name': 'Best credential ever',
@@ -1097,16 +782,6 @@ def test_gce_create_ok(post, organization, admin, version, params):
# #
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version, params', [ @pytest.mark.parametrize('version, params', [
['v1', {
'kind': 'azure_rm',
'name': 'Best credential ever',
'subscription': 'some_subscription',
'username': 'some_username',
'password': 'some_password',
'client': 'some_client',
'secret': 'some_secret',
'tenant': 'some_tenant'
}],
['v2', { ['v2', {
'credential_type': 1, 'credential_type': 1,
'name': 'Best credential ever', 'name': 'Best credential ever',
@@ -1146,13 +821,6 @@ def test_azure_rm_create_ok(post, organization, admin, version, params):
# #
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version, params', [ @pytest.mark.parametrize('version, params', [
['v1', {
'kind': 'satellite6',
'name': 'Best credential ever',
'host': 'some_host',
'username': 'some_username',
'password': 'some_password',
}],
['v2', { ['v2', {
'credential_type': 1, 'credential_type': 1,
'name': 'Best credential ever', 'name': 'Best credential ever',
@@ -1186,13 +854,6 @@ def test_satellite6_create_ok(post, organization, admin, version, params):
# #
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version, params', [ @pytest.mark.parametrize('version, params', [
['v1', {
'kind': 'aws',
'name': 'Best credential ever',
'username': 'some_username',
'password': 'some_password',
'security_token': 'abc123'
}],
['v2', { ['v2', {
'credential_type': 1, 'credential_type': 1,
'name': 'Best credential ever', 'name': 'Best credential ever',
@@ -1223,10 +884,6 @@ def test_aws_create_ok(post, organization, admin, version, params):
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version, params', [ @pytest.mark.parametrize('version, params', [
['v1', {
'kind': 'aws',
'name': 'Best credential ever',
}],
['v2', { ['v2', {
'credential_type': 1, 'credential_type': 1,
'name': 'Best credential ever', 'name': 'Best credential ever',
@@ -1258,13 +915,6 @@ def test_aws_create_fail_required_fields(post, organization, admin, version, par
# #
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version, params', [ @pytest.mark.parametrize('version, params', [
['v1', {
'kind': 'vmware',
'host': 'some_host',
'name': 'Best credential ever',
'username': 'some_username',
'password': 'some_password'
}],
['v2', { ['v2', {
'credential_type': 1, 'credential_type': 1,
'name': 'Best credential ever', 'name': 'Best credential ever',
@@ -1295,10 +945,6 @@ def test_vmware_create_ok(post, organization, admin, version, params):
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version, params', [ @pytest.mark.parametrize('version, params', [
['v1', {
'kind': 'vmware',
'name': 'Best credential ever',
}],
['v2', { ['v2', {
'credential_type': 1, 'credential_type': 1,
'name': 'Best credential ever', 'name': 'Best credential ever',
@@ -1330,12 +976,6 @@ def test_vmware_create_fail_required_fields(post, organization, admin, version,
# #
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version, params', [ @pytest.mark.parametrize('version, params', [
['v1', {
'username': 'some_user',
'password': 'some_password',
'project': 'some_project',
'host': 'some_host',
}],
['v2', { ['v2', {
'credential_type': 1, 'credential_type': 1,
'inputs': { 'inputs': {
@@ -1396,7 +1036,6 @@ def test_openstack_verify_ssl(get, post, organization, admin, verify_ssl, expect
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version, params', [ @pytest.mark.parametrize('version, params', [
['v1', {}],
['v2', { ['v2', {
'credential_type': 1, 'credential_type': 1,
'inputs': {} 'inputs': {}
@@ -1425,12 +1064,6 @@ def test_openstack_create_fail_required_fields(post, organization, admin, versio
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version, params', [ @pytest.mark.parametrize('version, params', [
['v1', {
'name': 'Best credential ever',
'kind': 'ssh',
'username': 'joe',
'password': '',
}],
['v2', { ['v2', {
'name': 'Best credential ever', 'name': 'Best credential ever',
'credential_type': 1, 'credential_type': 1,
@@ -1624,12 +1257,6 @@ def test_cloud_credential_type_mutability(patch, organization, admin, credential
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version, params', [ @pytest.mark.parametrize('version, params', [
['v1', {
'name': 'Best credential ever',
'kind': 'ssh',
'username': 'joe',
'ssh_key_data': '$encrypted$',
}],
['v2', { ['v2', {
'name': 'Best credential ever', 'name': 'Best credential ever',
'credential_type': 1, 'credential_type': 1,
@@ -1664,13 +1291,6 @@ def test_ssh_unlock_needed(put, organization, admin, credentialtype_ssh, version
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version, params', [ @pytest.mark.parametrize('version, params', [
['v1', {
'name': 'Best credential ever',
'kind': 'ssh',
'username': 'joe',
'ssh_key_data': '$encrypted$',
'ssh_key_unlock': 'superfluous-key-unlock',
}],
['v2', { ['v2', {
'name': 'Best credential ever', 'name': 'Best credential ever',
'credential_type': 1, 'credential_type': 1,
@@ -1705,13 +1325,6 @@ def test_ssh_unlock_not_needed(put, organization, admin, credentialtype_ssh, ver
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version, params', [ @pytest.mark.parametrize('version, params', [
['v1', {
'name': 'Best credential ever',
'kind': 'ssh',
'username': 'joe',
'ssh_key_data': '$encrypted$',
'ssh_key_unlock': 'new-unlock',
}],
['v2', { ['v2', {
'name': 'Best credential ever', 'name': 'Best credential ever',
'credential_type': 1, 'credential_type': 1,
@@ -1753,11 +1366,6 @@ def test_ssh_unlock_with_prior_value(put, organization, admin, credentialtype_ss
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version, params', [ @pytest.mark.parametrize('version, params', [
['v1', {
'kind': 'ssh',
'username': 'joe',
'password': 'secret',
}],
['v2', { ['v2', {
'credential_type': 1, 'credential_type': 1,
'inputs': { 'inputs': {
@@ -1783,12 +1391,8 @@ def test_secret_encryption_on_create(get, post, organization, admin, credentialt
assert response.status_code == 200 assert response.status_code == 200
assert response.data['count'] == 1 assert response.data['count'] == 1
cred = response.data['results'][0] cred = response.data['results'][0]
if version == 'v1': assert cred['inputs']['username'] == 'joe'
assert cred['username'] == 'joe' assert cred['inputs']['password'] == '$encrypted$'
assert cred['password'] == '$encrypted$'
elif version == 'v2':
assert cred['inputs']['username'] == 'joe'
assert cred['inputs']['password'] == '$encrypted$'
cred = Credential.objects.all()[:1].get() cred = Credential.objects.all()[:1].get()
assert cred.inputs['password'].startswith('$encrypted$UTF8$AES') assert cred.inputs['password'].startswith('$encrypted$UTF8$AES')
@@ -1797,7 +1401,6 @@ def test_secret_encryption_on_create(get, post, organization, admin, credentialt
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version, params', [ @pytest.mark.parametrize('version, params', [
['v1', {'password': 'secret'}],
['v2', {'inputs': {'username': 'joe', 'password': 'secret'}}] ['v2', {'inputs': {'username': 'joe', 'password': 'secret'}}]
]) ])
def test_secret_encryption_on_update(get, post, patch, organization, admin, credentialtype_ssh, version, params): def test_secret_encryption_on_update(get, post, patch, organization, admin, credentialtype_ssh, version, params):
@@ -1829,12 +1432,8 @@ def test_secret_encryption_on_update(get, post, patch, organization, admin, cred
assert response.status_code == 200 assert response.status_code == 200
assert response.data['count'] == 1 assert response.data['count'] == 1
cred = response.data['results'][0] cred = response.data['results'][0]
if version == 'v1': assert cred['inputs']['username'] == 'joe'
assert cred['username'] == 'joe' assert cred['inputs']['password'] == '$encrypted$'
assert cred['password'] == '$encrypted$'
elif version == 'v2':
assert cred['inputs']['username'] == 'joe'
assert cred['inputs']['password'] == '$encrypted$'
cred = Credential.objects.all()[:1].get() cred = Credential.objects.all()[:1].get()
assert cred.inputs['password'].startswith('$encrypted$UTF8$AES') assert cred.inputs['password'].startswith('$encrypted$UTF8$AES')
@@ -1843,10 +1442,6 @@ def test_secret_encryption_on_update(get, post, patch, organization, admin, cred
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('version, params', [ @pytest.mark.parametrize('version, params', [
['v1', {
'username': 'joe',
'password': '$encrypted$',
}],
['v2', { ['v2', {
'inputs': { 'inputs': {
'username': 'joe', 'username': 'joe',
@@ -1930,7 +1525,6 @@ def test_custom_credential_type_create(get, post, organization, admin):
@pytest.mark.parametrize('version, params', [ @pytest.mark.parametrize('version, params', [
['v1', {'name': 'Some name', 'username': 'someusername'}],
['v2', {'name': 'Some name', 'credential_type': 1, 'inputs': {'username': 'someusername'}}] ['v2', {'name': 'Some name', 'credential_type': 1, 'inputs': {'username': 'someusername'}}]
]) ])
@pytest.mark.django_db @pytest.mark.django_db

View File

@@ -1,4 +1,3 @@
import json
from unittest import mock from unittest import mock
import pytest import pytest
@@ -25,74 +24,6 @@ def job_template(job_template, project, inventory):
return job_template return job_template
@pytest.mark.django_db
@pytest.mark.parametrize('key', ('credential', 'vault_credential'))
def test_credential_access_empty(get, job_template, admin, key):
url = reverse('api:job_template_detail', kwargs={'pk': job_template.pk})
resp = get(url, admin)
assert resp.data[key] is None
assert key not in resp.data['summary_fields']
@pytest.mark.django_db
def test_ssh_credential_access(get, job_template, admin, machine_credential):
job_template.credentials.add(machine_credential)
url = reverse('api:job_template_detail', kwargs={'pk': job_template.pk})
resp = get(url, admin)
assert resp.data['credential'] == machine_credential.pk
assert resp.data['summary_fields']['credential']['credential_type_id'] == machine_credential.pk
assert resp.data['summary_fields']['credential']['kind'] == 'ssh'
@pytest.mark.django_db
@pytest.mark.parametrize('key', ('credential', 'vault_credential', 'cloud_credential', 'network_credential'))
def test_invalid_credential_update(get, patch, job_template, admin, key):
url = reverse('api:job_template_detail', kwargs={'pk': job_template.pk, 'version': 'v1'})
resp = patch(url, {key: 999999}, admin, expect=400)
assert 'Credential 999999 does not exist' in json.loads(smart_str(smart_str(resp.content)))[key]
@pytest.mark.django_db
def test_ssh_credential_update(get, patch, job_template, admin, machine_credential):
url = reverse('api:job_template_detail', kwargs={'pk': job_template.pk})
patch(url, {'credential': machine_credential.pk}, admin, expect=200)
resp = get(url, admin)
assert resp.data['credential'] == machine_credential.pk
@pytest.mark.django_db
def test_ssh_credential_update_invalid_kind(get, patch, job_template, admin, vault_credential):
url = reverse('api:job_template_detail', kwargs={'pk': job_template.pk})
resp = patch(url, {'credential': vault_credential.pk}, admin, expect=400)
assert 'You must provide an SSH credential.' in smart_str(resp.content)
@pytest.mark.django_db
def test_vault_credential_access(get, job_template, admin, vault_credential):
job_template.credentials.add(vault_credential)
url = reverse('api:job_template_detail', kwargs={'pk': job_template.pk})
resp = get(url, admin)
assert resp.data['vault_credential'] == vault_credential.pk
assert resp.data['summary_fields']['vault_credential']['credential_type_id'] == vault_credential.pk # noqa
assert resp.data['summary_fields']['vault_credential']['kind'] == 'vault'
@pytest.mark.django_db
def test_vault_credential_update(get, patch, job_template, admin, vault_credential):
url = reverse('api:job_template_detail', kwargs={'pk': job_template.pk})
patch(url, {'vault_credential': vault_credential.pk}, admin, expect=200)
resp = get(url, admin)
assert resp.data['vault_credential'] == vault_credential.pk
@pytest.mark.django_db
def test_vault_credential_update_invalid_kind(get, patch, job_template, admin,
machine_credential):
url = reverse('api:job_template_detail', kwargs={'pk': job_template.pk})
resp = patch(url, {'vault_credential': machine_credential.pk}, admin, expect=400)
assert 'You must provide a vault credential.' in smart_str(resp.content)
@pytest.mark.django_db @pytest.mark.django_db
def test_extra_credentials_filtering(get, job_template, admin, def test_extra_credentials_filtering(get, job_template, admin,
machine_credential, vault_credential, credential): machine_credential, vault_credential, credential):
@@ -209,24 +140,6 @@ def test_extra_credentials_unique_by_kind(get, post, job_template, admin,
assert 'Cannot assign multiple Amazon Web Services credentials.' in smart_str(resp.content) assert 'Cannot assign multiple Amazon Web Services credentials.' in smart_str(resp.content)
@pytest.mark.django_db
def test_ssh_credential_at_launch(get, post, job_template, admin, machine_credential):
url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk})
pk = post(url, {'credential': machine_credential.pk}, admin, expect=201).data['job']
summary_fields = get(reverse('api:job_detail', kwargs={'pk': pk}), admin).data['summary_fields']
assert len(summary_fields['credentials']) == 1
@pytest.mark.django_db
def test_vault_credential_at_launch(get, post, job_template, admin, vault_credential):
url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk})
pk = post(url, {'vault_credential': vault_credential.pk}, admin, expect=201).data['job']
summary_fields = get(reverse('api:job_detail', kwargs={'pk': pk}), admin).data['summary_fields']
assert len(summary_fields['credentials']) == 1
@pytest.mark.django_db @pytest.mark.django_db
def test_extra_credentials_at_launch(get, post, job_template, admin, credential): def test_extra_credentials_at_launch(get, post, job_template, admin, credential):
url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk}) url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk})
@@ -236,30 +149,6 @@ def test_extra_credentials_at_launch(get, post, job_template, admin, credential)
assert len(summary_fields['credentials']) == 1 assert len(summary_fields['credentials']) == 1
@pytest.mark.django_db
def test_modify_ssh_credential_at_launch(get, post, job_template, admin,
machine_credential, vault_credential, credential):
job_template.credentials.add(vault_credential)
job_template.credentials.add(credential)
url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk})
pk = post(url, {'credential': machine_credential.pk}, admin, expect=201).data['job']
summary_fields = get(reverse('api:job_detail', kwargs={'pk': pk}), admin).data['summary_fields']
assert len(summary_fields['credentials']) == 3
@pytest.mark.django_db
def test_modify_vault_credential_at_launch(get, post, job_template, admin,
machine_credential, vault_credential, credential):
job_template.credentials.add(machine_credential)
job_template.credentials.add(credential)
url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk})
pk = post(url, {'vault_credential': vault_credential.pk}, admin, expect=201).data['job']
summary_fields = get(reverse('api:job_detail', kwargs={'pk': pk}), admin).data['summary_fields']
assert len(summary_fields['credentials']) == 3
@pytest.mark.django_db @pytest.mark.django_db
def test_modify_extra_credentials_at_launch(get, post, job_template, admin, def test_modify_extra_credentials_at_launch(get, post, job_template, admin,
machine_credential, vault_credential, credential): machine_credential, vault_credential, credential):
@@ -272,22 +161,6 @@ def test_modify_extra_credentials_at_launch(get, post, job_template, admin,
assert len(summary_fields['credentials']) == 3 assert len(summary_fields['credentials']) == 3
@pytest.mark.django_db
def test_overwrite_ssh_credential_at_launch(get, post, job_template, admin, machine_credential):
job_template.credentials.add(machine_credential)
new_cred = machine_credential
new_cred.pk = None
new_cred.save()
url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk})
pk = post(url, {'credential': new_cred.pk}, admin, expect=201).data['job']
summary_fields = get(reverse('api:job_detail', kwargs={'pk': pk}), admin).data['summary_fields']
assert len(summary_fields['credentials']) == 1
assert summary_fields['credentials'][0]['id'] == new_cred.pk
@pytest.mark.django_db @pytest.mark.django_db
def test_ssh_password_prompted_at_launch(get, post, job_template, admin, machine_credential): def test_ssh_password_prompted_at_launch(get, post, job_template, admin, machine_credential):
job_template.credentials.add(machine_credential) job_template.credentials.add(machine_credential)
@@ -375,49 +248,6 @@ def test_invalid_mixed_credentials_specification(get, post, job_template, admin,
user=admin, expect=400) user=admin, expect=400)
@pytest.mark.django_db
def test_rbac_default_credential_usage(get, post, job_template, alice, machine_credential):
job_template.credentials.add(machine_credential)
job_template.execute_role.members.add(alice)
# alice can launch; she's not adding any _new_ credentials, and she has
# execute access to the JT
url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk})
post(url, {'credential': machine_credential.pk}, alice, expect=201)
# make (copy) a _new_ SSH cred
new_cred = Credential.objects.create(
name=machine_credential.name,
credential_type=machine_credential.credential_type,
inputs=machine_credential.inputs
)
# alice is attempting to launch with a *different* SSH cred, but
# she does not have access to it, so she cannot launch
url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk})
post(url, {'credential': new_cred.pk}, alice, expect=403)
# if alice has gains access to the credential, she *can* launch
new_cred.use_role.members.add(alice)
url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk})
post(url, {'credential': new_cred.pk}, alice, expect=201)
@pytest.mark.django_db
def test_inventory_source_deprecated_credential(get, patch, admin, ec2_source, credential):
url = reverse('api:inventory_source_detail', kwargs={'pk': ec2_source.pk})
patch(url, {'credential': credential.pk}, admin, expect=200)
resp = get(url, admin, expect=200)
assert json.loads(smart_str(resp.content))['credential'] == credential.pk
@pytest.mark.django_db
def test_inventory_source_invalid_deprecated_credential(patch, admin, ec2_source, credential):
url = reverse('api:inventory_source_detail', kwargs={'pk': ec2_source.pk})
resp = patch(url, {'credential': 999999}, admin, expect=400)
assert 'Credential 999999 does not exist' in smart_str(resp.content)
@pytest.mark.django_db @pytest.mark.django_db
def test_deprecated_credential_activity_stream(patch, admin_user, machine_credential, job_template): def test_deprecated_credential_activity_stream(patch, admin_user, machine_credential, job_template):
job_template.credentials.add(machine_credential) job_template.credentials.add(machine_credential)

View File

@@ -309,8 +309,8 @@ def test_job_launch_with_default_creds(machine_credential, vault_credential, dep
prompted_fields, ignored_fields, errors = deploy_jobtemplate._accept_or_ignore_job_kwargs(**kv) prompted_fields, ignored_fields, errors = deploy_jobtemplate._accept_or_ignore_job_kwargs(**kv)
job_obj = deploy_jobtemplate.create_unified_job(**prompted_fields) job_obj = deploy_jobtemplate.create_unified_job(**prompted_fields)
assert job_obj.credential == machine_credential.pk assert job_obj.machine_credential.pk == machine_credential.pk
assert job_obj.vault_credential == vault_credential.pk assert job_obj.vault_credentials[0].pk == vault_credential.pk
@pytest.mark.django_db @pytest.mark.django_db
@@ -350,14 +350,14 @@ def test_job_launch_with_empty_creds(machine_credential, vault_credential, deplo
prompted_fields, ignored_fields, errors = deploy_jobtemplate._accept_or_ignore_job_kwargs(**serializer.validated_data) prompted_fields, ignored_fields, errors = deploy_jobtemplate._accept_or_ignore_job_kwargs(**serializer.validated_data)
job_obj = deploy_jobtemplate.create_unified_job(**prompted_fields) job_obj = deploy_jobtemplate.create_unified_job(**prompted_fields)
assert job_obj.credential is deploy_jobtemplate.credential assert job_obj.machine_credential.pk == deploy_jobtemplate.machine_credential.pk
assert job_obj.vault_credential is deploy_jobtemplate.vault_credential assert job_obj.vault_credentials[0].pk == deploy_jobtemplate.vault_credentials[0].pk
@pytest.mark.django_db @pytest.mark.django_db
def test_job_launch_fails_with_missing_vault_password(machine_credential, vault_credential, def test_job_launch_fails_with_missing_vault_password(machine_credential, vault_credential,
deploy_jobtemplate, post, rando): deploy_jobtemplate, post, rando):
vault_credential.vault_password = 'ASK' vault_credential.inputs['vault_password'] = 'ASK'
vault_credential.save() vault_credential.save()
deploy_jobtemplate.credentials.add(vault_credential) deploy_jobtemplate.credentials.add(vault_credential)
deploy_jobtemplate.execute_role.members.add(rando) deploy_jobtemplate.execute_role.members.add(rando)
@@ -440,7 +440,7 @@ def test_job_launch_fails_with_missing_multivault_password(machine_credential, v
@pytest.mark.django_db @pytest.mark.django_db
def test_job_launch_fails_with_missing_ssh_password(machine_credential, deploy_jobtemplate, post, def test_job_launch_fails_with_missing_ssh_password(machine_credential, deploy_jobtemplate, post,
rando): rando):
machine_credential.password = 'ASK' machine_credential.inputs['password'] = 'ASK'
machine_credential.save() machine_credential.save()
deploy_jobtemplate.credentials.add(machine_credential) deploy_jobtemplate.credentials.add(machine_credential)
deploy_jobtemplate.execute_role.members.add(rando) deploy_jobtemplate.execute_role.members.add(rando)
@@ -457,9 +457,9 @@ def test_job_launch_fails_with_missing_ssh_password(machine_credential, deploy_j
@pytest.mark.django_db @pytest.mark.django_db
def test_job_launch_fails_with_missing_vault_and_ssh_password(machine_credential, vault_credential, def test_job_launch_fails_with_missing_vault_and_ssh_password(machine_credential, vault_credential,
deploy_jobtemplate, post, rando): deploy_jobtemplate, post, rando):
vault_credential.vault_password = 'ASK' vault_credential.inputs['vault_password'] = 'ASK'
vault_credential.save() vault_credential.save()
machine_credential.password = 'ASK' machine_credential.inputs['password'] = 'ASK'
machine_credential.save() machine_credential.save()
deploy_jobtemplate.credentials.add(machine_credential) deploy_jobtemplate.credentials.add(machine_credential)
deploy_jobtemplate.credentials.add(vault_credential) deploy_jobtemplate.credentials.add(vault_credential)
@@ -477,7 +477,7 @@ def test_job_launch_fails_with_missing_vault_and_ssh_password(machine_credential
@pytest.mark.django_db @pytest.mark.django_db
def test_job_launch_pass_with_prompted_vault_password(machine_credential, vault_credential, def test_job_launch_pass_with_prompted_vault_password(machine_credential, vault_credential,
deploy_jobtemplate, post, rando): deploy_jobtemplate, post, rando):
vault_credential.vault_password = 'ASK' vault_credential.inputs['vault_password'] = 'ASK'
vault_credential.save() vault_credential.save()
deploy_jobtemplate.credentials.add(machine_credential) deploy_jobtemplate.credentials.add(machine_credential)
deploy_jobtemplate.credentials.add(vault_credential) deploy_jobtemplate.credentials.add(vault_credential)

View File

@@ -19,148 +19,27 @@ from rest_framework.exceptions import ValidationError
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize( @pytest.mark.parametrize(
"grant_project, grant_credential, grant_inventory, expect", [ "grant_project, grant_inventory, expect", [
(True, True, True, 201), (True, True, 201),
(True, True, False, 403), (True, False, 403),
(True, False, True, 403), (False, True, 403),
(False, True, True, 403),
] ]
) )
def test_create(post, project, machine_credential, inventory, alice, grant_project, grant_credential, grant_inventory, expect): def test_create(post, project, machine_credential, inventory, alice, grant_project, grant_inventory, expect):
if grant_project: if grant_project:
project.use_role.members.add(alice) project.use_role.members.add(alice)
if grant_credential:
machine_credential.use_role.members.add(alice)
if grant_inventory: if grant_inventory:
inventory.use_role.members.add(alice) inventory.use_role.members.add(alice)
r = post(reverse('api:job_template_list'), { r = post(reverse('api:job_template_list'), {
'name': 'Some name', 'name': 'Some name',
'project': project.id, 'project': project.id,
'credential': machine_credential.id, # TODO: remove in 3.3
'inventory': inventory.id, 'inventory': inventory.id,
'playbook': 'helloworld.yml', 'playbook': 'helloworld.yml',
}, alice) }, alice)
if expect == 201:
jt = JobTemplate.objects.get(id=r.data['id'])
assert set(jt.credentials.values_list('id', flat=True)) == set([machine_credential.id])
assert r.status_code == expect assert r.status_code == expect
# TODO: remove in 3.3
@pytest.mark.django_db
def test_create_with_v1_deprecated_credentials(get, post, project, machine_credential, credential, net_credential, inventory, alice):
project.use_role.members.add(alice)
machine_credential.use_role.members.add(alice)
credential.use_role.members.add(alice)
net_credential.use_role.members.add(alice)
inventory.use_role.members.add(alice)
pk = post(reverse('api:job_template_list', kwargs={'version': 'v1'}), {
'name': 'Some name',
'project': project.id,
'credential': machine_credential.id,
'cloud_credential': credential.id,
'network_credential': net_credential.id,
'inventory': inventory.id,
'playbook': 'helloworld.yml',
}, alice, expect=201).data['id']
url = reverse('api:job_template_detail', kwargs={'version': 'v1', 'pk': pk})
response = get(url, alice)
assert response.data.get('cloud_credential') == credential.pk
assert response.data.get('network_credential') == net_credential.pk
# TODO: remove in 3.3
@pytest.mark.django_db
def test_create_with_empty_v1_deprecated_credentials(get, post, project, machine_credential, inventory, alice):
project.use_role.members.add(alice)
machine_credential.use_role.members.add(alice)
inventory.use_role.members.add(alice)
pk = post(reverse('api:job_template_list', kwargs={'version': 'v1'}), {
'name': 'Some name',
'project': project.id,
'credential': machine_credential.id,
'cloud_credential': None,
'network_credential': None,
'inventory': inventory.id,
'playbook': 'helloworld.yml',
}, alice, expect=201).data['id']
url = reverse('api:job_template_detail', kwargs={'version': 'v1', 'pk': pk})
response = get(url, alice)
assert response.data.get('cloud_credential') is None
assert response.data.get('network_credential') is None
# TODO: remove in 3.3
@pytest.mark.django_db
def test_create_v1_rbac_check(get, post, project, credential, net_credential, rando):
project.use_role.members.add(rando)
base_kwargs = dict(
name = 'Made with cloud/net creds I have no access to',
project = project.id,
ask_inventory_on_launch = True,
ask_credential_on_launch = True,
playbook = 'helloworld.yml',
)
base_kwargs['cloud_credential'] = credential.pk
post(reverse('api:job_template_list', kwargs={'version': 'v1'}), base_kwargs, rando, expect=403)
base_kwargs.pop('cloud_credential')
base_kwargs['network_credential'] = net_credential.pk
post(reverse('api:job_template_list', kwargs={'version': 'v1'}), base_kwargs, rando, expect=403)
# TODO: remove as each field tested has support removed
@pytest.mark.django_db
def test_jt_deprecated_summary_fields(
project, inventory,
machine_credential, net_credential, vault_credential,
mocker):
jt = JobTemplate.objects.create(
project=project,
inventory=inventory,
playbook='helloworld.yml'
)
class MockView:
kwargs = {}
request = None
class MockRequest:
version = 'v1'
user = None
view = MockView()
request = MockRequest()
view.request = request
serializer = JobTemplateSerializer(instance=jt, context={'view': view, 'request': request})
for kwargs in [{}, {'pk': 1}]: # detail vs. list view
for version in ['v1', 'v2']:
view.kwargs = kwargs
request.version = version
sf = serializer.get_summary_fields(jt)
assert 'credential' not in sf
assert 'vault_credential' not in sf
jt.credentials.add(machine_credential, net_credential, vault_credential)
view.kwargs = {'pk': 1}
for version in ['v1', 'v2']:
request.version = version
sf = serializer.get_summary_fields(jt)
assert 'credential' in sf
assert sf['credential'] # not empty dict
assert 'vault_credential' in sf
assert sf['vault_credential']
@pytest.mark.django_db @pytest.mark.django_db
def test_extra_credential_creation(get, post, organization_factory, job_template_factory, credentialtype_aws): def test_extra_credential_creation(get, post, organization_factory, job_template_factory, credentialtype_aws):
objs = organization_factory("org", superusers=['admin']) objs = organization_factory("org", superusers=['admin'])
@@ -293,79 +172,6 @@ def test_attach_extra_credential_wrong_kind_xfail(get, post, organization_factor
assert response.data.get('count') == 0 assert response.data.get('count') == 0
# TODO: remove in 3.3
@pytest.mark.django_db
def test_v1_extra_credentials_detail(get, organization_factory, job_template_factory, credential, net_credential):
objs = organization_factory("org", superusers=['admin'])
jt = job_template_factory("jt", organization=objs.organization,
inventory='test_inv', project='test_proj').job_template
jt.credentials.add(credential)
jt.credentials.add(net_credential)
jt.save()
url = reverse('api:job_template_detail', kwargs={'version': 'v1', 'pk': jt.pk})
response = get(url, user=objs.superusers.admin)
assert response.data.get('cloud_credential') == credential.pk
assert response.data.get('network_credential') == net_credential.pk
# TODO: remove in 3.3
@pytest.mark.django_db
def test_v1_set_extra_credentials_assignment(get, patch, organization_factory, job_template_factory, credential, net_credential):
objs = organization_factory("org", superusers=['admin'])
jt = job_template_factory("jt", organization=objs.organization,
inventory='test_inv', project='test_proj').job_template
jt.save()
url = reverse('api:job_template_detail', kwargs={'version': 'v1', 'pk': jt.pk})
response = patch(url, {
'cloud_credential': credential.pk,
'network_credential': net_credential.pk
}, objs.superusers.admin)
assert response.status_code == 200
url = reverse('api:job_template_detail', kwargs={'version': 'v1', 'pk': jt.pk})
response = get(url, user=objs.superusers.admin)
assert response.status_code == 200
assert response.data.get('cloud_credential') == credential.pk
assert response.data.get('network_credential') == net_credential.pk
url = reverse('api:job_template_detail', kwargs={'version': 'v1', 'pk': jt.pk})
response = patch(url, {
'cloud_credential': None,
'network_credential': None,
}, objs.superusers.admin)
assert response.status_code == 200
url = reverse('api:job_template_detail', kwargs={'version': 'v1', 'pk': jt.pk})
response = get(url, user=objs.superusers.admin)
assert response.status_code == 200
assert response.data.get('cloud_credential') is None
assert response.data.get('network_credential') is None
@pytest.mark.django_db
def test_filter_by_v1(get, organization_factory, job_template_factory, credential, net_credential):
objs = organization_factory("org", superusers=['admin'])
jt = job_template_factory("jt", organization=objs.organization,
inventory='test_inv', project='test_proj').job_template
jt.credentials.add(credential)
jt.credentials.add(net_credential)
jt.save()
for query in (
('cloud_credential', str(credential.pk)),
('network_credential', str(net_credential.pk))
):
url = reverse('api:job_template_list', kwargs={'version': 'v1'})
response = get(
url,
user=objs.superusers.admin,
QUERY_STRING='='.join(query)
)
assert response.data.get('count') == 1
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize( @pytest.mark.parametrize(
"grant_project, grant_inventory, expect", [ "grant_project, grant_inventory, expect", [
@@ -588,29 +394,6 @@ def test_launch_with_extra_credentials_not_allowed(get, post, organization_facto
assert resp.data.get('count') == 0 assert resp.data.get('count') == 0
@pytest.mark.django_db
def test_v1_launch_with_extra_credentials(get, post, organization_factory,
job_template_factory, machine_credential,
credential, net_credential):
# launch requests to `/api/v1/job_templates/N/launch/` should ignore
# `extra_credentials`, as they're only supported in v2 of the API.
objs = organization_factory("org", superusers=['admin'])
jt = job_template_factory("jt", organization=objs.organization,
inventory='test_inv', project='test_proj').job_template
jt.ask_credential_on_launch = True
jt.save()
resp = post(
reverse('api:job_template_launch', kwargs={'pk': jt.pk, 'version': 'v1'}),
dict(
credential=machine_credential.pk,
extra_credentials=[credential.pk, net_credential.pk]
),
objs.superusers.admin, expect=400
)
assert 'Field is not allowed for use with v1 API' in resp.data.get('extra_credentials')
@pytest.mark.django_db @pytest.mark.django_db
def test_jt_without_project(inventory): def test_jt_without_project(inventory):
data = dict(name="Test", job_type="run", data = dict(name="Test", job_type="run",

View File

@@ -65,7 +65,7 @@ class TestJobTemplateCopyEdit:
return objects.job_template return objects.job_template
def fake_context(self, user): def fake_context(self, user):
request = RequestFactory().get('/api/v1/resource/42/') request = RequestFactory().get('/api/v2/resource/42/')
request.user = user request.user = user
class FakeView(object): class FakeView(object):
@@ -151,7 +151,7 @@ def mock_access_method(mocker):
class TestAccessListCapabilities: class TestAccessListCapabilities:
""" """
Test that the access_list serializer shows the exact output of the RoleAccess.can_attach Test that the access_list serializer shows the exact output of the RoleAccess.can_attach
- looks at /api/v1/inventories/N/access_list/ - looks at /api/v2/inventories/N/access_list/
- test for types: direct, indirect, and team access - test for types: direct, indirect, and team access
""" """

View File

@@ -55,7 +55,7 @@ def test_node_rejects_unprompted_fields(inventory, project, workflow_job_templat
ask_limit_on_launch = False ask_limit_on_launch = False
) )
url = reverse('api:workflow_job_template_workflow_nodes_list', url = reverse('api:workflow_job_template_workflow_nodes_list',
kwargs={'pk': workflow_job_template.pk, 'version': 'v1'}) kwargs={'pk': workflow_job_template.pk, 'version': 'v2'})
r = post(url, {'unified_job_template': job_template.pk, 'limit': 'webservers'}, r = post(url, {'unified_job_template': job_template.pk, 'limit': 'webservers'},
user=admin_user, expect=400) user=admin_user, expect=400)
assert 'limit' in r.data assert 'limit' in r.data
@@ -71,7 +71,7 @@ def test_node_accepts_prompted_fields(inventory, project, workflow_job_template,
ask_limit_on_launch = True ask_limit_on_launch = True
) )
url = reverse('api:workflow_job_template_workflow_nodes_list', url = reverse('api:workflow_job_template_workflow_nodes_list',
kwargs={'pk': workflow_job_template.pk, 'version': 'v1'}) kwargs={'pk': workflow_job_template.pk, 'version': 'v2'})
post(url, {'unified_job_template': job_template.pk, 'limit': 'webservers'}, post(url, {'unified_job_template': job_template.pk, 'limit': 'webservers'},
user=admin_user, expect=201) user=admin_user, expect=201)

View File

@@ -63,7 +63,10 @@ class TestCreateUnifiedJob:
second_job = job_with_links.copy_unified_job() second_job = job_with_links.copy_unified_job()
# Check that job data matches the original variables # Check that job data matches the original variables
assert second_job.credential == job_with_links.credential assert [c.pk for c in second_job.credentials.all()] == [
machine_credential.pk,
net_credential.pk
]
assert second_job.inventory == job_with_links.inventory assert second_job.inventory == job_with_links.inventory
assert second_job.limit == 'my_server' assert second_job.limit == 'my_server'
assert net_credential in second_job.credentials.all() assert net_credential in second_job.credentials.all()

View File

@@ -99,25 +99,6 @@ def test_default_cred_types():
assert type_().managed_by_tower is True assert type_().managed_by_tower is True
@pytest.mark.django_db
@pytest.mark.parametrize('kind', ['net', 'scm', 'ssh', 'vault'])
def test_cred_type_kind_uniqueness(kind):
"""
non-cloud credential types are exclusive_on_kind (you can only use *one* of
them at a time)
"""
assert CredentialType.defaults[kind]().unique_by_kind is True
@pytest.mark.django_db
def test_cloud_kind_uniqueness():
"""
you can specify more than one cloud credential type (as long as they have
different names so you don't e.g., use ec2 twice")
"""
assert CredentialType.defaults['aws']().unique_by_kind is False
@pytest.mark.django_db @pytest.mark.django_db
def test_credential_creation(organization_factory): def test_credential_creation(organization_factory):
org = organization_factory('test').organization org = organization_factory('test').organization
@@ -141,7 +122,7 @@ def test_credential_creation(organization_factory):
cred.full_clean() cred.full_clean()
assert isinstance(cred, Credential) assert isinstance(cred, Credential)
assert cred.name == "Bob's Credential" assert cred.name == "Bob's Credential"
assert cred.inputs['username'] == cred.username == 'bob' assert cred.inputs['username'] == 'bob'
@pytest.mark.django_db @pytest.mark.django_db

View File

@@ -1,10 +1,7 @@
from unittest import mock from unittest import mock
import pytest import pytest
from rest_framework.exceptions import PermissionDenied
from awx.api.versioning import reverse from awx.api.versioning import reverse
from awx.api.serializers import JobTemplateSerializer
from awx.main.access import ( from awx.main.access import (
BaseAccess, BaseAccess,
JobTemplateAccess, JobTemplateAccess,
@@ -29,16 +26,18 @@ def test_job_template_access_superuser(check_license, user, deploy_jobtemplate):
@pytest.mark.django_db @pytest.mark.django_db
def test_job_template_access_read_level(jt_linked, rando): def test_job_template_access_read_level(jt_linked, rando):
ssh_cred = jt_linked.machine_credential
vault_cred = jt_linked.vault_credentials[0]
access = JobTemplateAccess(rando) access = JobTemplateAccess(rando)
jt_linked.project.read_role.members.add(rando) jt_linked.project.read_role.members.add(rando)
jt_linked.inventory.read_role.members.add(rando) jt_linked.inventory.read_role.members.add(rando)
jt_linked.get_deprecated_credential('ssh').read_role.members.add(rando) ssh_cred.read_role.members.add(rando)
proj_pk = jt_linked.project.pk proj_pk = jt_linked.project.pk
assert not access.can_add(dict(inventory=jt_linked.inventory.pk, project=proj_pk)) assert not access.can_add(dict(inventory=jt_linked.inventory.pk, project=proj_pk))
assert not access.can_add(dict(credential=jt_linked.credential, project=proj_pk)) assert not access.can_add(dict(credential=ssh_cred.pk, project=proj_pk))
assert not access.can_add(dict(vault_credential=jt_linked.vault_credential, project=proj_pk)) assert not access.can_add(dict(vault_credential=vault_cred.pk, project=proj_pk))
for cred in jt_linked.credentials.all(): for cred in jt_linked.credentials.all():
assert not access.can_unattach(jt_linked, cred, 'credentials', {}) assert not access.can_unattach(jt_linked, cred, 'credentials', {})
@@ -46,17 +45,19 @@ def test_job_template_access_read_level(jt_linked, rando):
@pytest.mark.django_db @pytest.mark.django_db
def test_job_template_access_use_level(jt_linked, rando): def test_job_template_access_use_level(jt_linked, rando):
ssh_cred = jt_linked.machine_credential
vault_cred = jt_linked.vault_credentials[0]
access = JobTemplateAccess(rando) access = JobTemplateAccess(rando)
jt_linked.project.use_role.members.add(rando) jt_linked.project.use_role.members.add(rando)
jt_linked.inventory.use_role.members.add(rando) jt_linked.inventory.use_role.members.add(rando)
jt_linked.get_deprecated_credential('ssh').use_role.members.add(rando) ssh_cred.use_role.members.add(rando)
jt_linked.get_deprecated_credential('vault').use_role.members.add(rando) vault_cred.use_role.members.add(rando)
proj_pk = jt_linked.project.pk proj_pk = jt_linked.project.pk
assert access.can_add(dict(inventory=jt_linked.inventory.pk, project=proj_pk)) assert access.can_add(dict(inventory=jt_linked.inventory.pk, project=proj_pk))
assert access.can_add(dict(credential=jt_linked.credential, project=proj_pk)) assert access.can_add(dict(credential=ssh_cred.pk, project=proj_pk))
assert access.can_add(dict(vault_credential=jt_linked.vault_credential, project=proj_pk)) assert access.can_add(dict(vault_credential=vault_cred.pk, project=proj_pk))
for cred in jt_linked.credentials.all(): for cred in jt_linked.credentials.all():
assert not access.can_unattach(jt_linked, cred, 'credentials', {}) assert not access.can_unattach(jt_linked, cred, 'credentials', {})
@@ -65,6 +66,8 @@ def test_job_template_access_use_level(jt_linked, rando):
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize("role_names", [("admin_role",), ("job_template_admin_role", "inventory_admin_role", "project_admin_role")]) @pytest.mark.parametrize("role_names", [("admin_role",), ("job_template_admin_role", "inventory_admin_role", "project_admin_role")])
def test_job_template_access_admin(role_names, jt_linked, rando): def test_job_template_access_admin(role_names, jt_linked, rando):
ssh_cred = jt_linked.machine_credential
access = JobTemplateAccess(rando) access = JobTemplateAccess(rando)
# Appoint this user as admin of the organization # Appoint this user as admin of the organization
#jt_linked.inventory.organization.admin_role.members.add(rando) #jt_linked.inventory.organization.admin_role.members.add(rando)
@@ -77,11 +80,11 @@ def test_job_template_access_admin(role_names, jt_linked, rando):
# Assign organization permission in the same way the create view does # Assign organization permission in the same way the create view does
organization = jt_linked.inventory.organization organization = jt_linked.inventory.organization
jt_linked.get_deprecated_credential('ssh').admin_role.parents.add(organization.admin_role) ssh_cred.admin_role.parents.add(organization.admin_role)
proj_pk = jt_linked.project.pk proj_pk = jt_linked.project.pk
assert access.can_add(dict(inventory=jt_linked.inventory.pk, project=proj_pk)) assert access.can_add(dict(inventory=jt_linked.inventory.pk, project=proj_pk))
assert access.can_add(dict(credential=jt_linked.credential, project=proj_pk)) assert access.can_add(dict(credential=ssh_cred.pk, project=proj_pk))
for cred in jt_linked.credentials.all(): for cred in jt_linked.credentials.all():
assert access.can_unattach(jt_linked, cred, 'credentials', {}) assert access.can_unattach(jt_linked, cred, 'credentials', {})
@@ -104,7 +107,7 @@ def test_job_template_extra_credentials_prompts_access(
jt.execute_role.members.add(rando) jt.execute_role.members.add(rando)
r = post( r = post(
reverse('api:job_template_launch', kwargs={'version': 'v2', 'pk': jt.id}), reverse('api:job_template_launch', kwargs={'version': 'v2', 'pk': jt.id}),
{'vault_credential': vault_credential.pk}, rando {'credentials': [machine_credential.pk, vault_credential.pk]}, rando
) )
assert r.status_code == 403 assert r.status_code == 403
@@ -126,57 +129,6 @@ class TestJobTemplateCredentials:
assert JobTemplateAccess(rando).can_attach( assert JobTemplateAccess(rando).can_attach(
job_template, credential, 'credentials', {}) job_template, credential, 'credentials', {})
def test_job_template_vault_cred_check(self, mocker, job_template, vault_credential, rando, project):
# TODO: remove in 3.4
job_template.admin_role.members.add(rando)
# not allowed to use the vault cred
# this is checked in the serializer validate method, not access.py
view = mocker.MagicMock()
view.request = mocker.MagicMock()
view.request.user = rando
serializer = JobTemplateSerializer(job_template, context={'view': view})
with pytest.raises(PermissionDenied):
serializer.validate({
'vault_credential': vault_credential.pk,
'project': project, # necessary because job_template fixture fails validation
'ask_inventory_on_launch': True,
})
def test_job_template_vault_cred_check_noop(self, mocker, job_template, vault_credential, rando, project):
# TODO: remove in 3.4
job_template.credentials.add(vault_credential)
job_template.admin_role.members.add(rando)
# not allowed to use the vault cred
# this is checked in the serializer validate method, not access.py
view = mocker.MagicMock()
view.request = mocker.MagicMock()
view.request.user = rando
serializer = JobTemplateSerializer(job_template, context={'view': view})
# should not raise error:
serializer.validate({
'vault_credential': vault_credential.pk,
'project': project, # necessary because job_template fixture fails validation
'playbook': 'helloworld.yml',
'ask_inventory_on_launch': True,
})
def test_new_jt_with_vault(self, mocker, vault_credential, project, rando):
project.admin_role.members.add(rando)
# TODO: remove in 3.4
# this is checked in the serializer validate method, not access.py
view = mocker.MagicMock()
view.request = mocker.MagicMock()
view.request.user = rando
serializer = JobTemplateSerializer(context={'view': view})
with pytest.raises(PermissionDenied):
serializer.validate({
'vault_credential': vault_credential.pk,
'project': project,
'playbook': 'helloworld.yml',
'ask_inventory_on_launch': True,
'name': 'asdf'
})
@pytest.mark.django_db @pytest.mark.django_db
class TestOrphanJobTemplate: class TestOrphanJobTemplate:

View File

@@ -103,8 +103,7 @@ class TestJobTemplateSerializerGetSummaryFields():
with mocker.patch("awx.api.serializers.role_summary_fields_generator", return_value='Can eat pie'): with mocker.patch("awx.api.serializers.role_summary_fields_generator", return_value='Can eat pie'):
with mocker.patch("awx.main.access.JobTemplateAccess.can_change", return_value='foobar'): with mocker.patch("awx.main.access.JobTemplateAccess.can_change", return_value='foobar'):
with mocker.patch("awx.main.access.JobTemplateAccess.can_copy", return_value='foo'): with mocker.patch("awx.main.access.JobTemplateAccess.can_copy", return_value='foo'):
with mock.patch.object(jt_obj.__class__, 'get_deprecated_credential', return_value=None): response = serializer.get_summary_fields(jt_obj)
response = serializer.get_summary_fields(jt_obj)
assert response['user_capabilities']['copy'] == 'foo' assert response['user_capabilities']['copy'] == 'foo'
assert response['user_capabilities']['edit'] == 'foobar' assert response['user_capabilities']['edit'] == 'foobar'

View File

@@ -688,13 +688,19 @@ class TestJobCredentials(TestJobExecution):
job.websocket_emit_status = mock.Mock() job.websocket_emit_status = mock.Mock()
job._credentials = [] job._credentials = []
def _credentials_filter(credential_type__kind=None):
creds = job._credentials
if credential_type__kind:
creds = [c for c in creds if c.credential_type.kind == credential_type__kind]
return mock.Mock(
__iter__ = lambda *args: iter(creds),
first = lambda: creds[0] if len(creds) else None
)
credentials_mock = mock.Mock(**{ credentials_mock = mock.Mock(**{
'all': lambda: job._credentials, 'all': lambda: job._credentials,
'add': job._credentials.append, 'add': job._credentials.append,
'filter.return_value': mock.Mock( 'filter.side_effect': _credentials_filter,
__iter__ = lambda *args: iter(job._credentials),
first = lambda: job._credentials[0]
),
'prefetch_related': lambda _: credentials_mock, 'prefetch_related': lambda _: credentials_mock,
'spec_set': ['all', 'add', 'filter', 'prefetch_related'], 'spec_set': ['all', 'add', 'filter', 'prefetch_related'],
}) })

View File

@@ -7,7 +7,7 @@ from rest_framework.generics import ListAPIView
# AWX # AWX
from awx.main.views import ApiErrorView from awx.main.views import ApiErrorView
from awx.api.views import JobList, InventorySourceList from awx.api.views import JobList
from awx.api.generics import ListCreateAPIView, SubListAttachDetachAPIView from awx.api.generics import ListCreateAPIView, SubListAttachDetachAPIView
@@ -40,20 +40,10 @@ def test_exception_view_raises_exception(api_view_obj_fixture, method_name):
getattr(api_view_obj_fixture, method_name)(request_mock) getattr(api_view_obj_fixture, method_name)(request_mock)
@pytest.mark.parametrize('version, supports_post', [(1, True), (2, False)]) def test_disable_post_on_v2_jobs_list():
def test_disable_post_on_v2_jobs_list(version, supports_post):
job_list = JobList() job_list = JobList()
job_list.request = mock.MagicMock() job_list.request = mock.MagicMock()
with mock.patch('awx.api.views.get_request_version', return_value=version): assert ('POST' in job_list.allowed_methods) is False
assert ('POST' in job_list.allowed_methods) == supports_post
@pytest.mark.parametrize('version, supports_post', [(1, False), (2, True)])
def test_disable_post_on_v1_inventory_source_list(version, supports_post):
inv_source_list = InventorySourceList()
inv_source_list.request = mock.MagicMock()
with mock.patch('awx.api.views.get_request_version', return_value=version):
assert ('POST' in inv_source_list.allowed_methods) == supports_post
def test_views_have_search_fields(all_views): def test_views_have_search_fields(all_views):

View File

@@ -36,11 +36,6 @@ export default ['$scope', 'Rest', 'CredentialList', 'Prompt', 'ProcessErrors', '
$scope.$on(`${list.iterator}_options`, function(event, data){ $scope.$on(`${list.iterator}_options`, function(event, data){
$scope.options = data.data.actions.GET; $scope.options = data.data.actions.GET;
optionsRequestDataProcessing();
});
$scope.$watchCollection(`${$scope.list.name}`, function() {
optionsRequestDataProcessing();
}); });
function assignCredentialKinds () { function assignCredentialKinds () {
@@ -69,26 +64,6 @@ export default ['$scope', 'Rest', 'CredentialList', 'Prompt', 'ProcessErrors', '
}); });
} }
// iterate over the list and add fields like type label, after the
// OPTIONS request returns, or the list is sorted/paginated/searched
function optionsRequestDataProcessing(){
if ($scope[list.name] !== undefined) {
$scope[list.name].forEach(function(item, item_idx) {
var itm = $scope[list.name][item_idx];
// Set the item type label
if (list.fields.kind && $scope.options &&
$scope.options.hasOwnProperty('kind')) {
$scope.options.kind.choices.forEach(function(choice) {
if (choice[0] === item.kind) {
itm.kind_label = choice[1];
}
});
}
});
}
}
$scope.copyCredential = credential => { $scope.copyCredential = credential => {
Wait('start'); Wait('start');
new Credential('get', credential.id) new Credential('get', credential.id)

View File

@@ -28,7 +28,7 @@ function adhocController($q, $scope, $stateParams,
return { return {
adhocUrl: GetBasePath('inventory') + id + '/ad_hoc_commands/', adhocUrl: GetBasePath('inventory') + id + '/ad_hoc_commands/',
inventoryUrl: GetBasePath('inventory') + id + '/', inventoryUrl: GetBasePath('inventory') + id + '/',
machineCredentialUrl: GetBasePath('credentials') + '?kind=ssh' machineCredentialUrl: GetBasePath('credentials') + '?credential_type__namespace=ssh'
}; };
}; };

View File

@@ -133,25 +133,36 @@ export default ['$state', 'ConfigData', '$scope', 'SourcesFormDefinition', 'Pars
}); });
$scope.lookupCredential = function(){ $scope.lookupCredential = function(){
if($scope.source.value !== "scm" && $scope.source.value !== "custom") { // For most source type selections, we filter for 1-1 matches to credential_type namespace.
let kind = ($scope.source.value === "ec2") ? "aws" : $scope.source.value; let searchKey = 'credential_type__namespace';
$state.go('.credential', { let searchValue = $scope.source.value;
credential_search: {
kind: kind, // SCM and custom source types are more generic in terms of the credentials they
page_size: '5', // accept - any cloud or user-defined credential type can be used. We filter for
page: '1' // these using the credential_type kind field, which categorizes all cloud and
} // user-defined credentials as 'cloud'.
}); if ($scope.source.value === 'scm') {
searchKey = 'credential_type__kind';
searchValue = 'cloud';
} }
else {
$state.go('.credential', { if ($scope.source.value === 'custom') {
credential_search: { searchKey = 'credential_type__kind';
credential_type__kind: "cloud", searchValue = 'cloud';
page_size: '5',
page: '1'
}
});
} }
// When the selection is 'ec2' we actually want to filter for the 'aws' namespace.
if ($scope.source.value === 'ec2') {
searchValue = 'aws';
}
$state.go('.credential', {
credential_search: {
[searchKey]: searchValue,
page_size: '5',
page: '1'
}
});
}; };
$scope.lookupProject = function(){ $scope.lookupProject = function(){
@@ -169,7 +180,7 @@ export default ['$state', 'ConfigData', '$scope', 'SourcesFormDefinition', 'Pars
$scope.credentialBasePath = GetBasePath('credentials') + '?credential_type__kind__in=cloud,network'; $scope.credentialBasePath = GetBasePath('credentials') + '?credential_type__kind__in=cloud,network';
} }
else{ else{
$scope.credentialBasePath = (source === 'ec2') ? GetBasePath('credentials') + '?kind=aws' : GetBasePath('credentials') + (source === '' ? '' : '?kind=' + (source)); $scope.credentialBasePath = (source === 'ec2') ? GetBasePath('credentials') + '?credential_type__namespace=aws' : GetBasePath('credentials') + (source === '' ? '' : '?credential_type__namespace=' + (source));
} }
if (source === 'ec2' || source === 'custom' || source === 'vmware' || source === 'openstack' || source === 'scm' || source === 'cloudforms' || source === "satellite6" || source === "azure_rm") { if (source === 'ec2' || source === 'custom' || source === 'vmware' || source === 'openstack' || source === 'scm' || source === 'cloudforms' || source === "satellite6" || source === "azure_rm") {
$scope.envParseType = 'yaml'; $scope.envParseType = 'yaml';

View File

@@ -302,25 +302,36 @@ export default ['$state', '$scope', 'ParseVariableString', 'ParseTypeChange',
}; };
$scope.lookupCredential = function(){ $scope.lookupCredential = function(){
if($scope.source.value !== "scm" && $scope.source.value !== "custom") { // For most source type selections, we filter for 1-1 matches to credential_type namespace.
let kind = ($scope.source.value === "ec2") ? "aws" : $scope.source.value; let searchKey = 'credential_type__namespace';
$state.go('.credential', { let searchValue = $scope.source.value;
credential_search: {
kind: kind, // SCM and custom source types are more generic in terms of the credentials they
page_size: '5', // accept - any cloud or user-defined credential type can be used. We filter for
page: '1' // these using the credential_type kind field, which categorizes all cloud and
} // user-defined credentials as 'cloud'.
}); if ($scope.source.value === 'scm') {
searchKey = 'credential_type__kind';
searchValue = 'cloud';
} }
else {
$state.go('.credential', { if ($scope.source.value === 'custom') {
credential_search: { searchKey = 'credential_type__kind';
credential_type__kind: "cloud", searchValue = 'cloud';
page_size: '5',
page: '1'
}
});
} }
// When the selection is 'ec2' we actually want to filter for the 'aws' namespace.
if ($scope.source.value === 'ec2') {
searchValue = 'aws';
}
$state.go('.credential', {
credential_search: {
[searchKey]: searchValue,
page_size: '5',
page: '1'
}
});
}; };
$scope.formCancel = function() { $scope.formCancel = function() {
@@ -384,7 +395,7 @@ export default ['$state', '$scope', 'ParseVariableString', 'ParseTypeChange',
$scope.credentialBasePath = GetBasePath('credentials') + '?credential_type__kind__in=cloud,network'; $scope.credentialBasePath = GetBasePath('credentials') + '?credential_type__kind__in=cloud,network';
} }
else{ else{
$scope.credentialBasePath = (source === 'ec2') ? GetBasePath('credentials') + '?kind=aws' : GetBasePath('credentials') + (source === '' ? '' : '?kind=' + (source)); $scope.credentialBasePath = (source === 'ec2') ? GetBasePath('credentials') + '?credential_type__namespace=aws' : GetBasePath('credentials') + (source === '' ? '' : 'credential_type__namespace=' + (source));
} }
if (source === 'ec2' || source === 'custom' || source === 'vmware' || source === 'openstack' || source === 'scm' || source === 'cloudforms' || source === "satellite6") { if (source === 'ec2' || source === 'custom' || source === 'vmware' || source === 'openstack' || source === 'scm' || source === 'cloudforms' || source === "satellite6") {
$scope.envParseType = 'yaml'; $scope.envParseType = 'yaml';

View File

@@ -39,7 +39,7 @@ export default {
Dataset: ['ListDefinition', 'QuerySet', '$stateParams', 'GetBasePath', '$transition$', Dataset: ['ListDefinition', 'QuerySet', '$stateParams', 'GetBasePath', '$transition$',
(list, qs, $stateParams, GetBasePath, $transition$) => { (list, qs, $stateParams, GetBasePath, $transition$) => {
const toState = $transition$.to(); const toState = $transition$.to();
toState.params.credential_search.value.kind = _.get($stateParams, 'credential_search.kind', null); toState.params.credential_search.value.credential_type__namespace = _.get($stateParams, 'credential_search.credential_type__namespace', null);
toState.params.credential_search.value.credential_type__kind = _.get($stateParams, 'credential_search.credential_type__kind', null); toState.params.credential_search.value.credential_type__kind = _.get($stateParams, 'credential_search.credential_type__kind', null);
return qs.search(GetBasePath('credentials'), $stateParams[`${list.iterator}_search`]); return qs.search(GetBasePath('credentials'), $stateParams[`${list.iterator}_search`]);
} }

View File

@@ -20,7 +20,7 @@
* *
* ``` * ```
* /api/v2/inventories/9/ * /api/v2/inventories/9/
* /api/v2/credentials/?name=SSH Key&kind=ssh * /api/v2/credentials/?name=SSH Key&credential_type__namespace=ssh
* ``` * ```
* *
* When constructing the URL be sure to use the GetBasePath() method found in js/shared/Utilities.js. GetBasePath uses the response objects from /api and * When constructing the URL be sure to use the GetBasePath() method found in js/shared/Utilities.js. GetBasePath uses the response objects from /api and

View File

@@ -654,13 +654,13 @@ function(SettingsUtils, i18n, $rootScope) {
else { else {
switch(base) { switch(base) {
case 'credential': case 'credential':
query += '&kind=ssh&role_level=use_role'; query += '&credential_type__namespace=ssh&role_level=use_role';
break; break;
case 'scm_credential': case 'scm_credential':
query += '&kind=scm&role_level=use_role'; query += '&redential_type__namespace=scm&role_level=use_role';
break; break;
case 'network_credential': case 'network_credential':
query += '&kind=net&role_level=use_role'; query += '&redential_type__namespace=net&role_level=use_role';
break; break;
case 'cloud_credential': case 'cloud_credential':
query += '&cloud=true&role_level=use_role'; query += '&cloud=true&role_level=use_role';

View File

@@ -22,5 +22,5 @@ def version(request):
context.get('view'), context.get('view'),
'deprecated', 'deprecated',
False False
) or request.path.startswith('/api/v1/') )
} }

View File

@@ -262,52 +262,6 @@ endpoint:
} }
API Backwards Compatability
---------------------------
`/api/v1/credentials/` still exists in Tower 3.2, and it transparently works as
before with minimal surprises by attempting to translate `/api/v1/` requests to
the new ``Credential`` and ``Credential Type`` models.
* When creating or modifying a ``Job Template`` through `v1` of the API,
old-style credential assignment will transparently map to the new model. For
example, the following `POST`'ed payload:
{
credential: <pk>,
vault_credential: <pk>,
cloud_credential: <pk>,
network_credential: <pk>,
}
...would transparently update ``JobTemplate.extra_credentials`` to a list
containing both the cloud and network ``Credentials``.
Similarly, an `HTTP GET /api/v1/job_credentials/N/` will populate
`cloud_credential`, and `network_credential` with the *most recently applied*
matching credential in the list.
* Custom ``Credentials`` will not be returned in the ``v1`` API; if a user
defines their own ``Credential Type``, its credentials won't show up in the
``v1`` API.
* ``HTTP POST`` requests to ``/api/v1/credentials/`` will transparently map
old-style attributes (i.e., ``username``, ``password``, ``ssh_key_data``) to
the appropriate new-style model. Similarly, ``HTTP GET
/api/v1/credentials/N/`` requests will continue to contain old-style
key-value mappings in their payloads.
* Vault credentials are a new first-level type of credential in Tower 3.2.
As such, any ``Credentials`` pre-Tower 3.2 that contain *both* SSH and Vault
parameters will be migrated to separate distinct ``Credentials``
post-migration.
For example, if your Tower 3.1 installation has one ``Credential`` with
a defined ``username``, ``password``, and ``vault_password``, after migration
*two* ``Credentials`` will exist (one which contains the ``username`` and
``password``, and another which contains only the ``vault_password``).
Additional Criteria Additional Criteria
------------------- -------------------
* Rackspace is being removed from official support in Tower 3.2. Pre-existing * Rackspace is being removed from official support in Tower 3.2. Pre-existing

View File

@@ -30,7 +30,6 @@ Notifications can succeed or fail but that will not cause its associated job to
Once a Notification Template is created, its configuration can be tested by utilizing the endpoint at `/api/v2/notification_templates/<n>/test` This will emit a test notification given the configuration defined by the notification. These test notifications will also appear in the notifications list at `/api/v2/notifications` Once a Notification Template is created, its configuration can be tested by utilizing the endpoint at `/api/v2/notification_templates/<n>/test` This will emit a test notification given the configuration defined by the notification. These test notifications will also appear in the notifications list at `/api/v2/notifications`
# Notification Types # Notification Types
The currently defined Notification Types are: The currently defined Notification Types are:

View File

@@ -332,7 +332,7 @@ def make_the_data():
name='%s Credential %d User %d' % (prefix, credential_id, user_idx), name='%s Credential %d User %d' % (prefix, credential_id, user_idx),
defaults=dict(created_by=next(creator_gen), defaults=dict(created_by=next(creator_gen),
modified_by=next(modifier_gen)), modified_by=next(modifier_gen)),
credential_type=CredentialType.from_v1_kind('ssh') credential_type=CredentialType.objects.filter(namespace='ssh').first()
) )
credential.admin_role.members.add(user) credential.admin_role.members.add(user)
credentials.append(credential) credentials.append(credential)
@@ -355,7 +355,7 @@ def make_the_data():
name='%s Credential %d team %d' % (prefix, credential_id, team_idx), name='%s Credential %d team %d' % (prefix, credential_id, team_idx),
defaults=dict(created_by=next(creator_gen), defaults=dict(created_by=next(creator_gen),
modified_by=next(modifier_gen)), modified_by=next(modifier_gen)),
credential_type=CredentialType.from_v1_kind('ssh') credential_type=CredentialType.objects.filter(namespace='ssh').first()
) )
credential.admin_role.parents.add(team.member_role) credential.admin_role.parents.add(team.member_role)
credentials.append(credential) credentials.append(credential)

View File

@@ -3,7 +3,7 @@ docker run --name awx_test -it --memory="4g" --cpuset="0,1" -v /Users/meyers/ans
## How to use the logstash container ## How to use the logstash container
POST the following content to `/api/v1/settings/logging/` (this uses POST the following content to `/api/v2/settings/logging/` (this uses
authentication set up inside of the logstash configuration file). authentication set up inside of the logstash configuration file).
``` ```

View File

@@ -95,7 +95,7 @@ and
} }
``` ```
These can be entered via Configure-Tower-in-Tower by making a POST to These can be entered via Configure-Tower-in-Tower by making a POST to
`/api/v1/settings/logging/`. `/api/v2/settings/logging/`.
### Connecting Logstash to 3rd Party Receivers ### Connecting Logstash to 3rd Party Receivers

View File

@@ -1,54 +0,0 @@
#!/usr/bin/env python
import datetime
import getpass
import json
import urllib2
REST_API_URL = "http://awx.example.com/api/v1/"
REST_API_USER = "admin"
REST_API_PASS = "password"
JOB_TEMPLATE_ID = 1
# Setup urllib2 for basic password authentication.
password_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
password_mgr.add_password(None, REST_API_URL, REST_API_USER, REST_API_PASS)
handler = urllib2.HTTPBasicAuthHandler(password_mgr)
opener = urllib2.build_opener(handler)
urllib2.install_opener(opener)
# Read the job template.
JOB_TEMPLATE_URL="%sjob_templates/%d/" % (REST_API_URL, JOB_TEMPLATE_ID)
response = urllib2.urlopen(JOB_TEMPLATE_URL)
data = json.loads(response.read())
# Update data if needed for the new job.
data.pop('id')
data.update({
'name': 'my new job started at %s' % str(datetime.datetime.now()),
'verbosity': 3,
})
# Create a new job based on the template and data.
JOB_TEMPLATE_JOBS_URL="%sjobs/" % JOB_TEMPLATE_URL
request = urllib2.Request(JOB_TEMPLATE_JOBS_URL, json.dumps(data),
{'Content-type': 'application/json'})
response = urllib2.urlopen(request)
data = json.loads(response.read())
# Get the job ID and check for passwords needed to start the job.
JOB_ID = data['id']
JOB_START_URL = '%sjobs/%d/start/' % (REST_API_URL, JOB_ID)
response = urllib2.urlopen(JOB_START_URL)
data = json.loads(response.read())
# Prompt for any passwords needed.
start_data = {}
for password in data.get('passwords_needed_to_start', []):
value = getpass.getpass('%s: ' % password)
start_data[password] = value
# Make POST request to start the job.
request = urllib2.Request(JOB_START_URL, json.dumps(start_data),
{'Content-type': 'application/json'})
response = urllib2.urlopen(request)