diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 636833edab..f5b9817bd2 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1481,6 +1481,18 @@ class JobCancelSerializer(JobSerializer): fields = ('can_cancel',) +class JobRelaunchSerializer(JobSerializer): + + class Meta: + fields = () + + def to_native(self, obj): + if obj: + return dict([(p, u'') for p in obj.passwords_needed_to_start]) + else: + return {} + + class AdHocCommandSerializer(UnifiedJobSerializer): name = serializers.CharField(source='name', read_only=True) @@ -1533,6 +1545,18 @@ class AdHocCommandCancelSerializer(AdHocCommandSerializer): fields = ('can_cancel',) +class AdHocCommandRelaunchSerializer(AdHocCommandSerializer): + + class Meta: + fields = () + + def to_native(self, obj): + if obj: + return dict([(p, u'') for p in obj.passwords_needed_to_start]) + else: + return {} + + class SystemJobTemplateSerializer(UnifiedJobTemplateSerializer): class Meta: diff --git a/awx/api/views.py b/awx/api/views.py index f1e5c6b008..7bbb6d8885 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -1846,8 +1846,8 @@ class JobCancel(RetrieveAPIView): class JobRelaunch(GenericAPIView): model = Job + serializer_class = JobRelaunchSerializer is_job_start = True - # FIXME: Add serializer class to define fields in OPTIONS request! @csrf_exempt @transaction.non_atomic_requests @@ -1858,22 +1858,31 @@ class JobRelaunch(GenericAPIView): obj = self.get_object() data = {} data['passwords_needed_to_start'] = obj.passwords_needed_to_start - #data['ask_variables_on_launch'] = obj.ask_variables_on_launch return Response(data) def post(self, request, *args, **kwargs): obj = self.get_object() if not request.user.can_access(self.model, 'start', obj): raise PermissionDenied() + + # Check for passwords needed before copying job. + needed = obj.passwords_needed_to_start + provided = dict([(field, request.DATA.get(field, '')) for field in needed]) + if not all(provided.values()): + data = dict(passwords_needed_to_start=needed) + return Response(data, status=status.HTTP_400_BAD_REQUEST) + new_job = obj.copy() result = new_job.signal_start(**request.DATA) if not result: - data = dict(passwords_needed_to_start=obj.passwords_needed_to_start) + data = dict(passwords_needed_to_start=new_job.passwords_needed_to_start) return Response(data, status=status.HTTP_400_BAD_REQUEST) else: - data = dict(job=new_job.id) - return Response(data, status=status.HTTP_202_ACCEPTED) - + data = JobSerializer(new_job).data + # Add job key to match what old relaunch returned. + data['job'] = new_job.id + headers = {'Location': new_job.get_absolute_url()} + return Response(data, status=status.HTTP_201_CREATED, headers=headers) class BaseJobHostSummariesList(SubListAPIView): @@ -2189,9 +2198,21 @@ class AdHocCommandList(ListCreateAPIView): parent_obj = self.get_parent_object() if isinstance(parent_obj, (Host, Group)): data['inventory'] = parent_obj.inventory_id + + # Check for passwords needed before creating ad hoc command. + credential_pk = get_pk_from_dict(request.DATA, 'credential') + if credential_pk: + credential = get_object_or_400(Credential, pk=credential_pk) + needed = credential.passwords_needed + provided = dict([(field, request.DATA.get(field, '')) for field in needed]) + if not all(provided.values()): + data = dict(passwords_needed_to_start=needed) + return Response(data, status=status.HTTP_400_BAD_REQUEST) + response = super(AdHocCommandList, self).create(request, *args, **kwargs) if response.status_code != status.HTTP_201_CREATED: return response + # Start ad hoc command running when created. ad_hoc_command = get_object_or_400(self.model, pk=response.data['id']) result = ad_hoc_command.signal_start(**request.DATA) @@ -2246,9 +2267,11 @@ class AdHocCommandCancel(RetrieveAPIView): class AdHocCommandRelaunch(GenericAPIView): model = AdHocCommand + serializer_class = AdHocCommandRelaunchSerializer is_job_start = True new_in_220 = True - # FIXME: Add serializer class to define fields in OPTIONS request! + + # FIXME: Figure out why OPTIONS request still shows all fields. @csrf_exempt @transaction.non_atomic_requests @@ -2257,22 +2280,33 @@ class AdHocCommandRelaunch(GenericAPIView): def get(self, request, *args, **kwargs): obj = self.get_object() - data = {} - data['passwords_needed_to_start'] = obj.passwords_needed_to_start + data = dict(passwords_needed_to_start=obj.passwords_needed_to_start) return Response(data) def post(self, request, *args, **kwargs): obj = self.get_object() if not request.user.can_access(self.model, 'start', obj): raise PermissionDenied() + + # Check for passwords needed before copying ad hoc command. + needed = obj.passwords_needed_to_start + provided = dict([(field, request.DATA.get(field, '')) for field in needed]) + if not all(provided.values()): + data = dict(passwords_needed_to_start=needed) + return Response(data, status=status.HTTP_400_BAD_REQUEST) + + # Copy and start the new ad hoc command. new_ad_hoc_command = obj.copy() result = new_ad_hoc_command.signal_start(**request.DATA) if not result: - data = dict(passwords_needed_to_start=obj.passwords_needed_to_start) + data = dict(passwords_needed_to_start=new_ad_hoc_command.passwords_needed_to_start) return Response(data, status=status.HTTP_400_BAD_REQUEST) else: - data = dict(ad_hoc_command=new_ad_hoc_command.id) - return Response(data, status=status.HTTP_202_ACCEPTED) + data = AdHocCommandSerializer(new_ad_hoc_command).data + # Add ad_hoc_command key to match what was previously returned. + data['ad_hoc_command'] = new_ad_hoc_command.id + headers = {'Location': new_ad_hoc_command.get_absolute_url()} + return Response(data, status=status.HTTP_201_CREATED, headers=headers) class AdHocCommandEventList(ListAPIView): diff --git a/awx/main/access.py b/awx/main/access.py index 4f92d6a462..738506b442 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -92,14 +92,6 @@ def check_user_access(user, model_class, action, *args, **kwargs): return result return False -def get_pk_from_dict(_dict, key): - ''' - Helper for obtaining a pk from user data dict or None if not present. - ''' - try: - return int(_dict[key]) - except (TypeError, KeyError, ValueError): - return None class BaseAccess(object): ''' diff --git a/awx/main/utils.py b/awx/main/utils.py index 16c5bf2557..5af048b41c 100644 --- a/awx/main/utils.py +++ b/awx/main/utils.py @@ -29,7 +29,7 @@ __all__ = ['get_object_or_400', 'get_object_or_403', 'camelcase_to_underscore', 'get_ansible_version', 'get_awx_version', 'update_scm_url', 'get_type_for_model', 'get_model_for_type', 'to_python_boolean', 'ignore_inventory_computed_fields', 'ignore_inventory_group_removal', - '_inventory_updates'] + '_inventory_updates', 'get_pk_from_dict'] def get_object_or_400(klass, *args, **kwargs): @@ -474,3 +474,12 @@ def wrap_args_with_proot(args, cwd, **kwargs): new_args.extend(['-w', cwd]) new_args.extend(args) return new_args + +def get_pk_from_dict(_dict, key): + ''' + Helper for obtaining a pk from user data dict or None if not present. + ''' + try: + return int(_dict[key]) + except (TypeError, KeyError, ValueError): + return None