mirror of
https://github.com/ansible/awx.git
synced 2026-02-02 01:58:09 -03:30
Implement support for ad hoc commands.
This commit is contained in:
@@ -7,7 +7,7 @@ from rest_framework import exceptions
|
||||
from rest_framework import HTTP_HEADER_ENCODING
|
||||
|
||||
# AWX
|
||||
from awx.main.models import Job, AuthToken
|
||||
from awx.main.models import UnifiedJob, AuthToken
|
||||
|
||||
|
||||
class TokenAuthentication(authentication.TokenAuthentication):
|
||||
@@ -74,24 +74,26 @@ class TokenAuthentication(authentication.TokenAuthentication):
|
||||
# Return the user object and the token.
|
||||
return (token.user, token)
|
||||
|
||||
class JobTaskAuthentication(authentication.BaseAuthentication):
|
||||
class TaskAuthentication(authentication.BaseAuthentication):
|
||||
'''
|
||||
Custom authentication used for views accessed by the inventory and callback
|
||||
scripts when running a job.
|
||||
scripts when running a task.
|
||||
'''
|
||||
|
||||
|
||||
model = None
|
||||
|
||||
def authenticate(self, request):
|
||||
auth = authentication.get_authorization_header(request).split()
|
||||
if len(auth) != 2 or auth[0].lower() != 'token' or '-' not in auth[1]:
|
||||
return None
|
||||
job_id, job_key = auth[1].split('-', 1)
|
||||
pk, key = auth[1].split('-', 1)
|
||||
try:
|
||||
job = Job.objects.get(pk=job_id, status='running')
|
||||
except Job.DoesNotExist:
|
||||
unified_job = UnifiedJob.objects.get(pk=pk, status='running')
|
||||
except UnifiedJob.DoesNotExist:
|
||||
return None
|
||||
token = job.task_auth_token
|
||||
token = unified_job.task_auth_token
|
||||
if auth[1] != token:
|
||||
raise exceptions.AuthenticationFailed('Invalid job task token')
|
||||
raise exceptions.AuthenticationFailed('Invalid task token')
|
||||
return (None, token)
|
||||
|
||||
def authenticate_header(self, request):
|
||||
|
||||
@@ -29,8 +29,9 @@ from awx.main.utils import * # noqa
|
||||
|
||||
__all__ = ['APIView', 'GenericAPIView', 'ListAPIView', 'SimpleListAPIView',
|
||||
'ListCreateAPIView', 'SubListAPIView', 'SubListCreateAPIView',
|
||||
'RetrieveAPIView', 'RetrieveUpdateAPIView',
|
||||
'RetrieveDestroyAPIView', 'RetrieveUpdateDestroyAPIView', 'DestroyAPIView']
|
||||
'SubListCreateAttachDetachAPIView', 'RetrieveAPIView',
|
||||
'RetrieveUpdateAPIView', 'RetrieveDestroyAPIView',
|
||||
'RetrieveUpdateDestroyAPIView', 'DestroyAPIView']
|
||||
|
||||
logger = logging.getLogger('awx.api.generics')
|
||||
|
||||
@@ -131,12 +132,15 @@ class APIView(views.APIView):
|
||||
|
||||
def get_description_context(self):
|
||||
return {
|
||||
'view': self,
|
||||
'docstring': type(self).__doc__ or '',
|
||||
'new_in_13': getattr(self, 'new_in_13', False),
|
||||
'new_in_14': getattr(self, 'new_in_14', False),
|
||||
'new_in_145': getattr(self, 'new_in_145', False),
|
||||
'new_in_148': getattr(self, 'new_in_148', False),
|
||||
'new_in_200': getattr(self, 'new_in_200', False),
|
||||
'new_in_210': getattr(self, 'new_in_210', False),
|
||||
'new_in_220': getattr(self, 'new_in_220', False),
|
||||
}
|
||||
|
||||
def get_description(self, html=False):
|
||||
@@ -153,7 +157,7 @@ class APIView(views.APIView):
|
||||
'''
|
||||
ret = super(APIView, self).metadata(request)
|
||||
added_in_version = '1.2'
|
||||
for version in ('2.1.0', '2.0.0', '1.4.8', '1.4.5', '1.4', '1.3'):
|
||||
for version in ('2.2.0', '2.1.0', '2.0.0', '1.4.8', '1.4.5', '1.4', '1.3'):
|
||||
if getattr(self, 'new_in_%s' % version.replace('.', ''), False):
|
||||
added_in_version = version
|
||||
break
|
||||
@@ -328,8 +332,8 @@ class SubListAPIView(ListAPIView):
|
||||
return qs & sublist_qs
|
||||
|
||||
class SubListCreateAPIView(SubListAPIView, ListCreateAPIView):
|
||||
# Base class for a sublist view that allows for creating subobjects and
|
||||
# attaching/detaching them from the parent.
|
||||
# Base class for a sublist view that allows for creating subobjects
|
||||
# associated with the parent object.
|
||||
|
||||
# In addition to SubListAPIView properties, subclasses may define (if the
|
||||
# sub_obj requires a foreign key to the parent):
|
||||
@@ -374,8 +378,13 @@ class SubListCreateAPIView(SubListAPIView, ListCreateAPIView):
|
||||
# object deserialized
|
||||
obj = serializer.save()
|
||||
serializer = self.serializer_class(obj)
|
||||
|
||||
headers = {'Location': obj.get_absolute_url()}
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
class SubListCreateAttachDetachAPIView(SubListCreateAPIView):
|
||||
# Base class for a sublist view that allows for creating subobjects and
|
||||
# attaching/detaching them from the parent.
|
||||
|
||||
def attach(self, request, *args, **kwargs):
|
||||
created = False
|
||||
|
||||
@@ -19,7 +19,7 @@ from awx.main.utils import get_object_or_400
|
||||
logger = logging.getLogger('awx.api.permissions')
|
||||
|
||||
__all__ = ['ModelAccessPermission', 'JobTemplateCallbackPermission',
|
||||
'JobTaskPermission']
|
||||
'TaskPermission']
|
||||
|
||||
class ModelAccessPermission(permissions.BasePermission):
|
||||
'''
|
||||
@@ -160,31 +160,31 @@ class JobTemplateCallbackPermission(ModelAccessPermission):
|
||||
else:
|
||||
return True
|
||||
|
||||
class JobTaskPermission(ModelAccessPermission):
|
||||
class TaskPermission(ModelAccessPermission):
|
||||
'''
|
||||
Permission checks used for API callbacks from running a task.
|
||||
'''
|
||||
|
||||
def has_permission(self, request, view, obj=None):
|
||||
# If another authentication method was used other than the one for job
|
||||
# If another authentication method was used other than the one for
|
||||
# callbacks, default to the superclass permissions checking.
|
||||
if request.user or not request.auth:
|
||||
return super(JobTaskPermission, self).has_permission(request, view, obj)
|
||||
return super(TaskPermission, self).has_permission(request, view, obj)
|
||||
|
||||
# Verify that the job ID present in the auth token is for a valid,
|
||||
# active job.
|
||||
# Verify that the ID present in the auth token is for a valid, active
|
||||
# unified job.
|
||||
try:
|
||||
job = Job.objects.get(active=True, status='running',
|
||||
pk=int(request.auth.split('-')[0]))
|
||||
except (Job.DoesNotExist, TypeError):
|
||||
unified_job = UnifiedJob.objects.get(active=True, status='running',
|
||||
pk=int(request.auth.split('-')[0]))
|
||||
except (UnifiedJob.DoesNotExist, TypeError):
|
||||
return False
|
||||
|
||||
# Verify that the request method is one of those allowed for the given
|
||||
# view, also that the job or inventory being accessed matches the auth
|
||||
# token.
|
||||
if view.model == Inventory and request.method.lower() in ('head', 'get'):
|
||||
return bool(not obj or obj.pk == job.inventory.pk)
|
||||
elif view.model == JobEvent and request.method.lower() == 'post':
|
||||
return bool(not obj or obj.pk == job.pk)
|
||||
return bool(not obj or obj.pk == unified_job.inventory_id)
|
||||
elif view.model in (JobEvent, AdHocCommandEvent) and request.method.lower() == 'post':
|
||||
return bool(not obj or obj.pk == unified_job.pk)
|
||||
else:
|
||||
return False
|
||||
|
||||
@@ -132,16 +132,17 @@ class BaseSerializerMetaclass(serializers.SerializerMetaclass):
|
||||
for attr in dir(other):
|
||||
if attr.startswith('_'):
|
||||
continue
|
||||
val = getattr(other, attr)
|
||||
meta_val = getattr(meta, attr, [])
|
||||
val = getattr(other, attr, [])
|
||||
# Special handling for lists of strings (field names).
|
||||
if isinstance(val, (list, tuple)) and all([isinstance(x, basestring) for x in val]):
|
||||
new_vals = []
|
||||
except_vals = []
|
||||
if base: # Merge values from all bases.
|
||||
new_vals.extend([x for x in getattr(meta, attr, [])])
|
||||
new_vals.extend([x for x in meta_val])
|
||||
for v in val:
|
||||
if not base and v == '*': # Inherit all values from previous base(es).
|
||||
new_vals.extend([x for x in getattr(meta, attr, [])])
|
||||
new_vals.extend([x for x in meta_val])
|
||||
elif not base and v.startswith('-'): # Except these values.
|
||||
except_vals.append(v[1:])
|
||||
else:
|
||||
@@ -226,6 +227,7 @@ class BaseSerializer(serializers.ModelSerializer):
|
||||
def get_type_choices(self):
|
||||
type_name_map = {
|
||||
'job': 'Playbook Run',
|
||||
'ad_hoc_command': 'Ad Hoc Command',
|
||||
'project_update': 'SCM Update',
|
||||
'inventory_update': 'Inventory Sync',
|
||||
'system_job': 'Management Job',
|
||||
@@ -347,6 +349,23 @@ class BaseSerializer(serializers.ModelSerializer):
|
||||
exclusions.remove(field_name)
|
||||
return exclusions
|
||||
|
||||
def to_native(self, obj):
|
||||
# When rendering the raw data form, create an instance of the model so
|
||||
# that the model defaults will be filled in.
|
||||
view = self.context.get('view', None)
|
||||
parent_key = getattr(view, 'parent_key', None)
|
||||
if not obj and hasattr(view, '_raw_data_form_marker'):
|
||||
obj = self.opts.model()
|
||||
# FIXME: Would be nice to include any posted data for the raw data
|
||||
# form, so that a submission with errors can be modified in place
|
||||
# and resubmitted.
|
||||
ret = super(BaseSerializer, self).to_native(obj)
|
||||
# Remove parent key from raw form data, since it will be automatically
|
||||
# set by the sub list create view.
|
||||
if parent_key and hasattr(view, '_raw_data_form_marker'):
|
||||
ret.pop(parent_key, None)
|
||||
return ret
|
||||
|
||||
|
||||
class UnifiedJobTemplateSerializer(BaseSerializer):
|
||||
|
||||
@@ -390,6 +409,7 @@ class UnifiedJobTemplateSerializer(BaseSerializer):
|
||||
class UnifiedJobSerializer(BaseSerializer):
|
||||
|
||||
result_stdout = serializers.Field(source='result_stdout')
|
||||
unified_job_template = serializers.Field(source='unified_job_template')
|
||||
|
||||
class Meta:
|
||||
model = UnifiedJob
|
||||
@@ -400,7 +420,7 @@ class UnifiedJobSerializer(BaseSerializer):
|
||||
|
||||
def get_types(self):
|
||||
if type(self) is UnifiedJobSerializer:
|
||||
return ['project_update', 'inventory_update', 'job', 'system_job']
|
||||
return ['project_update', 'inventory_update', 'job', 'ad_hoc_command', 'system_job']
|
||||
else:
|
||||
return super(UnifiedJobSerializer, self).get_types()
|
||||
|
||||
@@ -416,6 +436,8 @@ class UnifiedJobSerializer(BaseSerializer):
|
||||
res['stdout'] = reverse('api:inventory_update_stdout', args=(obj.pk,))
|
||||
elif isinstance(obj, Job):
|
||||
res['stdout'] = reverse('api:job_stdout', args=(obj.pk,))
|
||||
elif isinstance(obj, AdHocCommand):
|
||||
res['stdout'] = reverse('api:ad_hoc_command_stdout', args=(obj.pk,))
|
||||
return res
|
||||
|
||||
def to_native(self, obj):
|
||||
@@ -427,6 +449,8 @@ class UnifiedJobSerializer(BaseSerializer):
|
||||
serializer_class = InventoryUpdateSerializer
|
||||
elif isinstance(obj, Job):
|
||||
serializer_class = JobSerializer
|
||||
elif isinstance(obj, AdHocCommand):
|
||||
serializer_class = AdHocCommandSerializer
|
||||
elif isinstance(obj, SystemJob):
|
||||
serializer_class = SystemJobSerializer
|
||||
if serializer_class:
|
||||
@@ -447,7 +471,7 @@ class UnifiedJobListSerializer(UnifiedJobSerializer):
|
||||
|
||||
def get_types(self):
|
||||
if type(self) is UnifiedJobListSerializer:
|
||||
return ['project_update', 'inventory_update', 'job', 'system_job']
|
||||
return ['project_update', 'inventory_update', 'job', 'ad_hoc_command', 'system_job']
|
||||
else:
|
||||
return super(UnifiedJobListSerializer, self).get_types()
|
||||
|
||||
@@ -460,6 +484,8 @@ class UnifiedJobListSerializer(UnifiedJobSerializer):
|
||||
serializer_class = InventoryUpdateListSerializer
|
||||
elif isinstance(obj, Job):
|
||||
serializer_class = JobListSerializer
|
||||
elif isinstance(obj, AdHocCommand):
|
||||
serializer_class = AdHocCommandListSerializer
|
||||
elif isinstance(obj, SystemJob):
|
||||
serializer_class = SystemJobListSerializer
|
||||
if serializer_class:
|
||||
@@ -479,7 +505,7 @@ class UnifiedJobStdoutSerializer(UnifiedJobSerializer):
|
||||
|
||||
def get_types(self):
|
||||
if type(self) is UnifiedJobStdoutSerializer:
|
||||
return ['project_update', 'inventory_update', 'job', 'system_job']
|
||||
return ['project_update', 'inventory_update', 'job', 'ad_hoc_command', 'system_job']
|
||||
else:
|
||||
return super(UnifiedJobStdoutSerializer, self).get_types()
|
||||
|
||||
@@ -746,6 +772,7 @@ class InventorySerializer(BaseSerializerWithVariables):
|
||||
inventory_sources = reverse('api:inventory_inventory_sources_list', args=(obj.pk,)),
|
||||
activity_stream = reverse('api:inventory_activity_stream_list', args=(obj.pk,)),
|
||||
scan_job_templates = reverse('api:inventory_scan_job_template_list', args=(obj.pk,)),
|
||||
ad_hoc_commands = reverse('api:inventory_ad_hoc_commands_list', args=(obj.pk,)),
|
||||
))
|
||||
if obj.organization and obj.organization.active:
|
||||
res['organization'] = reverse('api:organization_detail', args=(obj.organization.pk,))
|
||||
@@ -784,6 +811,8 @@ class HostSerializer(BaseSerializerWithVariables):
|
||||
job_host_summaries = reverse('api:host_job_host_summaries_list', args=(obj.pk,)),
|
||||
activity_stream = reverse('api:host_activity_stream_list', args=(obj.pk,)),
|
||||
inventory_sources = reverse('api:host_inventory_sources_list', args=(obj.pk,)),
|
||||
ad_hoc_commands = reverse('api:host_ad_hoc_commands_list', args=(obj.pk,)),
|
||||
ad_hoc_command_events = reverse('api:host_ad_hoc_command_events_list', args=(obj.pk,)),
|
||||
))
|
||||
if obj.inventory and obj.inventory.active:
|
||||
res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,))
|
||||
@@ -884,6 +913,7 @@ class GroupSerializer(BaseSerializerWithVariables):
|
||||
job_host_summaries = reverse('api:group_job_host_summaries_list', args=(obj.pk,)),
|
||||
activity_stream = reverse('api:group_activity_stream_list', args=(obj.pk,)),
|
||||
inventory_sources = reverse('api:group_inventory_sources_list', args=(obj.pk,)),
|
||||
ad_hoc_commands = reverse('api:group_ad_hoc_commands_list', args=(obj.pk,)),
|
||||
))
|
||||
if obj.inventory and obj.inventory.active:
|
||||
res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,))
|
||||
@@ -1174,7 +1204,7 @@ class PermissionSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = Permission
|
||||
fields = ('*', 'user', 'team', 'project', 'inventory',
|
||||
'permission_type')
|
||||
'permission_type', 'run_ad_hoc_commands')
|
||||
|
||||
def get_related(self, obj):
|
||||
res = super(PermissionSerializer, self).get_related(obj)
|
||||
@@ -1190,15 +1220,15 @@ class PermissionSerializer(BaseSerializer):
|
||||
|
||||
def validate(self, attrs):
|
||||
# Can only set either user or team.
|
||||
if attrs['user'] and attrs['team']:
|
||||
if attrs.get('user', None) and attrs.get('team', None):
|
||||
raise serializers.ValidationError('permission can only be assigned'
|
||||
' to a user OR a team, not both')
|
||||
# Cannot assign admit/read/write permissions for a project.
|
||||
if attrs['permission_type'] in ('admin', 'read', 'write') and attrs['project']:
|
||||
if attrs.get('permission_type', None) in ('admin', 'read', 'write') and attrs.get('project', None):
|
||||
raise serializers.ValidationError('project cannot be assigned for '
|
||||
'inventory-only permissions')
|
||||
# Project is required when setting deployment permissions.
|
||||
if attrs['permission_type'] in ('run', 'check') and not attrs['project']:
|
||||
if attrs.get('permission_type', None) in ('run', 'check') and not attrs.get('project', None):
|
||||
raise serializers.ValidationError('project is required when '
|
||||
'assigning deployment permissions')
|
||||
return attrs
|
||||
@@ -1451,6 +1481,56 @@ class JobCancelSerializer(JobSerializer):
|
||||
fields = ('can_cancel',)
|
||||
|
||||
|
||||
class AdHocCommandSerializer(UnifiedJobSerializer):
|
||||
|
||||
class Meta:
|
||||
model = AdHocCommand
|
||||
fields = ('*', 'job_type', 'inventory', 'limit', 'credential',
|
||||
'module_name', 'module_args', 'forks', 'verbosity',
|
||||
'privilege_escalation')
|
||||
exclude = ('unified_job_template', 'name', 'description')
|
||||
|
||||
def get_related(self, obj):
|
||||
res = super(AdHocCommandSerializer, self).get_related(obj)
|
||||
if obj.inventory and obj.inventory.active:
|
||||
res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,))
|
||||
if obj.credential and obj.credential.active:
|
||||
res['credential'] = reverse('api:credential_detail', args=(obj.credential.pk,))
|
||||
res.update(dict(
|
||||
events = reverse('api:ad_hoc_command_ad_hoc_command_events_list', args=(obj.pk,)),
|
||||
activity_stream = reverse('api:ad_hoc_command_activity_stream_list', args=(obj.pk,)),
|
||||
))
|
||||
res['cancel'] = reverse('api:ad_hoc_command_cancel', args=(obj.pk,))
|
||||
res['relaunch'] = reverse('api:ad_hoc_command_relaunch', args=(obj.pk,))
|
||||
return res
|
||||
|
||||
def to_native(self, obj):
|
||||
# In raw data form, populate limit field from host/group name.
|
||||
view = self.context.get('view', None)
|
||||
parent_model = getattr(view, 'parent_model', None)
|
||||
if not (obj and obj.pk) and view and hasattr(view, '_raw_data_form_marker'):
|
||||
if not obj:
|
||||
obj = self.opts.model()
|
||||
if parent_model in (Host, Group):
|
||||
parent_obj = parent_model.objects.get(pk=view.kwargs['pk'])
|
||||
obj.limit = parent_obj.name
|
||||
ret = super(AdHocCommandSerializer, self).to_native(obj)
|
||||
# Hide inventory field from raw data, since it will be set automatically
|
||||
# by sub list create view.
|
||||
if not (obj and obj.pk) and view and hasattr(view, '_raw_data_form_marker'):
|
||||
if parent_model in (Host, Group):
|
||||
ret.pop('inventory', None)
|
||||
return ret
|
||||
|
||||
|
||||
class AdHocCommandCancelSerializer(AdHocCommandSerializer):
|
||||
|
||||
can_cancel = serializers.BooleanField(source='can_cancel', read_only=True)
|
||||
|
||||
class Meta:
|
||||
fields = ('can_cancel',)
|
||||
|
||||
|
||||
class SystemJobTemplateSerializer(UnifiedJobTemplateSerializer):
|
||||
|
||||
class Meta:
|
||||
@@ -1482,6 +1562,9 @@ class SystemJobSerializer(UnifiedJobSerializer):
|
||||
class JobListSerializer(JobSerializer, UnifiedJobListSerializer):
|
||||
pass
|
||||
|
||||
class AdHocCommandListSerializer(AdHocCommandSerializer, UnifiedJobListSerializer):
|
||||
pass
|
||||
|
||||
class SystemJobListSerializer(SystemJobSerializer, UnifiedJobListSerializer):
|
||||
pass
|
||||
|
||||
@@ -1548,6 +1631,27 @@ class JobEventSerializer(BaseSerializer):
|
||||
pass
|
||||
return d
|
||||
|
||||
|
||||
class AdHocCommandEventSerializer(BaseSerializer):
|
||||
|
||||
event_display = serializers.Field(source='get_event_display')
|
||||
|
||||
class Meta:
|
||||
model = AdHocCommandEvent
|
||||
fields = ('*', '-name', '-description', 'ad_hoc_command', 'event',
|
||||
'counter', 'event_display', 'event_data', 'failed',
|
||||
'changed', 'host', 'host_name')
|
||||
|
||||
def get_related(self, obj):
|
||||
res = super(AdHocCommandEventSerializer, self).get_related(obj)
|
||||
res.update(dict(
|
||||
ad_hoc_command = reverse('api:ad_hoc_command_detail', args=(obj.ad_hoc_command_id,)),
|
||||
))
|
||||
if obj.host:
|
||||
res['host'] = reverse('api:host_detail', args=(obj.host.pk,))
|
||||
return res
|
||||
|
||||
|
||||
class ScheduleSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -12,6 +12,7 @@ fields to create a new {{ model_verbose_name }} associated with this
|
||||
|
||||
{% block post_create %}{% endblock %}
|
||||
|
||||
{% if view.attach %}
|
||||
{% if parent_key %}
|
||||
# Remove {{ parent_model_verbose_name|title }} {{ model_verbose_name_plural|title }}:
|
||||
|
||||
@@ -35,5 +36,6 @@ Make a POST request to this resource with `id` and `disassociate` fields to
|
||||
remove the {{ model_verbose_name }} from this {{ parent_model_verbose_name }}
|
||||
without deleting the {{ model_verbose_name }}.
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% include "api/_new_in_awx.md" %}
|
||||
|
||||
@@ -74,6 +74,7 @@ inventory_urls = patterns('awx.api.views',
|
||||
url(r'^(?P<pk>[0-9]+)/inventory_sources/$', 'inventory_inventory_sources_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/activity_stream/$', 'inventory_activity_stream_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/scan_job_templates/$', 'inventory_scan_job_template_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/ad_hoc_commands/$', 'inventory_ad_hoc_commands_list'),
|
||||
)
|
||||
|
||||
host_urls = patterns('awx.api.views',
|
||||
@@ -86,6 +87,8 @@ host_urls = patterns('awx.api.views',
|
||||
url(r'^(?P<pk>[0-9]+)/job_host_summaries/$', 'host_job_host_summaries_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/activity_stream/$', 'host_activity_stream_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/inventory_sources/$', 'host_inventory_sources_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/ad_hoc_commands/$', 'host_ad_hoc_commands_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/ad_hoc_command_events/$', 'host_ad_hoc_command_events_list'),
|
||||
)
|
||||
|
||||
group_urls = patterns('awx.api.views',
|
||||
@@ -100,6 +103,7 @@ group_urls = patterns('awx.api.views',
|
||||
url(r'^(?P<pk>[0-9]+)/potential_children/$', 'group_potential_children_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/activity_stream/$', 'group_activity_stream_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/inventory_sources/$', 'group_inventory_sources_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/ad_hoc_commands/$', 'group_ad_hoc_commands_list'),
|
||||
)
|
||||
|
||||
inventory_source_urls = patterns('awx.api.views',
|
||||
@@ -171,6 +175,21 @@ job_event_urls = patterns('awx.api.views',
|
||||
url(r'^(?P<pk>[0-9]+)/hosts/$', 'job_event_hosts_list'),
|
||||
)
|
||||
|
||||
ad_hoc_command_urls = patterns('awx.api.views',
|
||||
url(r'^$', 'ad_hoc_command_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/$', 'ad_hoc_command_detail'),
|
||||
url(r'^(?P<pk>[0-9]+)/cancel/$', 'ad_hoc_command_cancel'),
|
||||
url(r'^(?P<pk>[0-9]+)/relaunch/$', 'ad_hoc_command_relaunch'),
|
||||
url(r'^(?P<pk>[0-9]+)/events/$', 'ad_hoc_command_ad_hoc_command_events_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/activity_stream/$', 'ad_hoc_command_activity_stream_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/stdout/$', 'ad_hoc_command_stdout'),
|
||||
)
|
||||
|
||||
ad_hoc_command_event_urls = patterns('awx.api.views',
|
||||
url(r'^$', 'ad_hoc_command_event_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/$', 'ad_hoc_command_event_detail'),
|
||||
)
|
||||
|
||||
system_job_template_urls = patterns('awx.api.views',
|
||||
url(r'^$', 'system_job_template_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/$', 'system_job_template_detail'),
|
||||
@@ -222,6 +241,8 @@ v1_urls = patterns('awx.api.views',
|
||||
url(r'^jobs/', include(job_urls)),
|
||||
url(r'^job_host_summaries/', include(job_host_summary_urls)),
|
||||
url(r'^job_events/', include(job_event_urls)),
|
||||
url(r'^ad_hoc_commands/', include(ad_hoc_command_urls)),
|
||||
url(r'^ad_hoc_command_events/', include(ad_hoc_command_event_urls)),
|
||||
url(r'^system_job_templates/', include(system_job_template_urls)),
|
||||
url(r'^system_jobs/', include(system_job_urls)),
|
||||
url(r'^unified_job_templates/$', 'unified_job_template_list'),
|
||||
|
||||
232
awx/api/views.py
232
awx/api/views.py
@@ -44,7 +44,7 @@ import ansiconv
|
||||
from awx.main.task_engine import TaskSerializer, TASK_FILE
|
||||
from awx.main.access import get_user_queryset
|
||||
from awx.main.ha import is_ha_environment
|
||||
from awx.api.authentication import JobTaskAuthentication
|
||||
from awx.api.authentication import TaskAuthentication
|
||||
from awx.api.utils.decorators import paginated
|
||||
from awx.api.generics import get_view_name
|
||||
from awx.api.generics import * # noqa
|
||||
@@ -111,6 +111,7 @@ class ApiV1RootView(APIView):
|
||||
data['hosts'] = reverse('api:host_list')
|
||||
data['job_templates'] = reverse('api:job_template_list')
|
||||
data['jobs'] = reverse('api:job_list')
|
||||
data['ad_hoc_commands'] = reverse('api:ad_hoc_command_list')
|
||||
data['system_job_templates'] = reverse('api:system_job_template_list')
|
||||
data['system_jobs'] = reverse('api:system_job_list')
|
||||
data['schedules'] = reverse('api:schedule_list')
|
||||
@@ -492,28 +493,28 @@ class OrganizationInventoriesList(SubListAPIView):
|
||||
parent_model = Organization
|
||||
relationship = 'inventories'
|
||||
|
||||
class OrganizationUsersList(SubListCreateAPIView):
|
||||
class OrganizationUsersList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = User
|
||||
serializer_class = UserSerializer
|
||||
parent_model = Organization
|
||||
relationship = 'users'
|
||||
|
||||
class OrganizationAdminsList(SubListCreateAPIView):
|
||||
class OrganizationAdminsList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = User
|
||||
serializer_class = UserSerializer
|
||||
parent_model = Organization
|
||||
relationship = 'admins'
|
||||
|
||||
class OrganizationProjectsList(SubListCreateAPIView):
|
||||
class OrganizationProjectsList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Project
|
||||
serializer_class = ProjectSerializer
|
||||
parent_model = Organization
|
||||
relationship = 'projects'
|
||||
|
||||
class OrganizationTeamsList(SubListCreateAPIView):
|
||||
class OrganizationTeamsList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Team
|
||||
serializer_class = TeamSerializer
|
||||
@@ -539,14 +540,14 @@ class TeamDetail(RetrieveUpdateDestroyAPIView):
|
||||
model = Team
|
||||
serializer_class = TeamSerializer
|
||||
|
||||
class TeamUsersList(SubListCreateAPIView):
|
||||
class TeamUsersList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = User
|
||||
serializer_class = UserSerializer
|
||||
parent_model = Team
|
||||
relationship = 'users'
|
||||
|
||||
class TeamPermissionsList(SubListCreateAPIView):
|
||||
class TeamPermissionsList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Permission
|
||||
serializer_class = PermissionSerializer
|
||||
@@ -565,14 +566,14 @@ class TeamPermissionsList(SubListCreateAPIView):
|
||||
return base
|
||||
raise PermissionDenied()
|
||||
|
||||
class TeamProjectsList(SubListCreateAPIView):
|
||||
class TeamProjectsList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Project
|
||||
serializer_class = ProjectSerializer
|
||||
parent_model = Team
|
||||
relationship = 'projects'
|
||||
|
||||
class TeamCredentialsList(SubListCreateAPIView):
|
||||
class TeamCredentialsList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Credential
|
||||
serializer_class = CredentialSerializer
|
||||
@@ -631,21 +632,21 @@ class ProjectPlaybooks(RetrieveAPIView):
|
||||
model = Project
|
||||
serializer_class = ProjectPlaybooksSerializer
|
||||
|
||||
class ProjectOrganizationsList(SubListCreateAPIView):
|
||||
class ProjectOrganizationsList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Organization
|
||||
serializer_class = OrganizationSerializer
|
||||
parent_model = Project
|
||||
relationship = 'organizations'
|
||||
|
||||
class ProjectTeamsList(SubListCreateAPIView):
|
||||
class ProjectTeamsList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Team
|
||||
serializer_class = TeamSerializer
|
||||
parent_model = Project
|
||||
relationship = 'teams'
|
||||
|
||||
class ProjectSchedulesList(SubListCreateAPIView):
|
||||
class ProjectSchedulesList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
view_name = "Project Schedules"
|
||||
|
||||
@@ -746,7 +747,7 @@ class UserTeamsList(SubListAPIView):
|
||||
parent_model = User
|
||||
relationship = 'teams'
|
||||
|
||||
class UserPermissionsList(SubListCreateAPIView):
|
||||
class UserPermissionsList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Permission
|
||||
serializer_class = PermissionSerializer
|
||||
@@ -767,7 +768,7 @@ class UserProjectsList(SubListAPIView):
|
||||
qs = self.request.user.get_queryset(self.model)
|
||||
return qs.filter(teams__in=parent.teams.distinct())
|
||||
|
||||
class UserCredentialsList(SubListCreateAPIView):
|
||||
class UserCredentialsList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Credential
|
||||
serializer_class = CredentialSerializer
|
||||
@@ -932,7 +933,7 @@ class HostDetail(RetrieveUpdateDestroyAPIView):
|
||||
model = Host
|
||||
serializer_class = HostSerializer
|
||||
|
||||
class InventoryHostsList(SubListCreateAPIView):
|
||||
class InventoryHostsList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Host
|
||||
serializer_class = HostSerializer
|
||||
@@ -940,7 +941,7 @@ class InventoryHostsList(SubListCreateAPIView):
|
||||
relationship = 'hosts'
|
||||
parent_key = 'inventory'
|
||||
|
||||
class HostGroupsList(SubListCreateAPIView):
|
||||
class HostGroupsList(SubListCreateAttachDetachAPIView):
|
||||
''' the list of groups a host is directly a member of '''
|
||||
|
||||
model = Group
|
||||
@@ -991,7 +992,7 @@ class GroupList(ListCreateAPIView):
|
||||
model = Group
|
||||
serializer_class = GroupSerializer
|
||||
|
||||
class GroupChildrenList(SubListCreateAPIView):
|
||||
class GroupChildrenList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Group
|
||||
serializer_class = GroupSerializer
|
||||
@@ -1050,7 +1051,7 @@ class GroupPotentialChildrenList(SubListAPIView):
|
||||
except_pks.update(parent.all_children.values_list('pk', flat=True))
|
||||
return qs.exclude(pk__in=except_pks)
|
||||
|
||||
class GroupHostsList(SubListCreateAPIView):
|
||||
class GroupHostsList(SubListCreateAttachDetachAPIView):
|
||||
''' the list of hosts directly below a group '''
|
||||
|
||||
model = Host
|
||||
@@ -1124,7 +1125,7 @@ class GroupDetail(RetrieveUpdateDestroyAPIView):
|
||||
obj.mark_inactive_recursive()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
class InventoryGroupsList(SubListCreateAPIView):
|
||||
class InventoryGroupsList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Group
|
||||
serializer_class = GroupSerializer
|
||||
@@ -1132,7 +1133,7 @@ class InventoryGroupsList(SubListCreateAPIView):
|
||||
relationship = 'groups'
|
||||
parent_key = 'inventory'
|
||||
|
||||
class InventoryRootGroupsList(SubListCreateAPIView):
|
||||
class InventoryRootGroupsList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Group
|
||||
serializer_class = GroupSerializer
|
||||
@@ -1171,8 +1172,8 @@ class InventoryScriptView(RetrieveAPIView):
|
||||
|
||||
model = Inventory
|
||||
serializer_class = InventoryScriptSerializer
|
||||
authentication_classes = [JobTaskAuthentication] + api_settings.DEFAULT_AUTHENTICATION_CLASSES
|
||||
permission_classes = (JobTaskPermission,)
|
||||
authentication_classes = [TaskAuthentication] + api_settings.DEFAULT_AUTHENTICATION_CLASSES
|
||||
permission_classes = (TaskPermission,)
|
||||
filter_backends = ()
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
@@ -1330,7 +1331,7 @@ class InventorySourceDetail(RetrieveUpdateAPIView):
|
||||
pu.cancel()
|
||||
return super(InventorySourceDetail, self).destroy(request, *args, **kwargs)
|
||||
|
||||
class InventorySourceSchedulesList(SubListCreateAPIView):
|
||||
class InventorySourceSchedulesList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
view_name = "Inventory Source Schedules"
|
||||
|
||||
@@ -1479,7 +1480,7 @@ class JobTemplateLaunch(GenericAPIView):
|
||||
data = dict(job=new_job.id)
|
||||
return Response(data, status=status.HTTP_202_ACCEPTED)
|
||||
|
||||
class JobTemplateSchedulesList(SubListCreateAPIView):
|
||||
class JobTemplateSchedulesList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
view_name = "Job Template Schedules"
|
||||
|
||||
@@ -1749,7 +1750,7 @@ class SystemJobTemplateLaunch(GenericAPIView):
|
||||
data = dict(system_job=new_job.id)
|
||||
return Response(data, status=status.HTTP_202_ACCEPTED)
|
||||
|
||||
class SystemJobTemplateSchedulesList(SubListCreateAPIView):
|
||||
class SystemJobTemplateSchedulesList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
view_name = "System Job Template Schedules"
|
||||
|
||||
@@ -1944,8 +1945,8 @@ class GroupJobEventsList(BaseJobEventsList):
|
||||
class JobJobEventsList(BaseJobEventsList):
|
||||
|
||||
parent_model = Job
|
||||
authentication_classes = [JobTaskAuthentication] + api_settings.DEFAULT_AUTHENTICATION_CLASSES
|
||||
permission_classes = (JobTaskPermission,)
|
||||
authentication_classes = [TaskAuthentication] + api_settings.DEFAULT_AUTHENTICATION_CLASSES
|
||||
permission_classes = (TaskPermission,)
|
||||
|
||||
# Post allowed for job event callback only.
|
||||
def post(self, request, *args, **kwargs):
|
||||
@@ -1966,8 +1967,6 @@ class JobJobPlaysList(BaseJobEventsList):
|
||||
|
||||
parent_model = Job
|
||||
view_name = 'Job Plays List'
|
||||
authentication_classes = [JobTaskAuthentication] + api_settings.DEFAULT_AUTHENTICATION_CLASSES
|
||||
permission_classes = (JobTaskPermission,)
|
||||
new_in_200 = True
|
||||
|
||||
@paginated
|
||||
@@ -2042,8 +2041,6 @@ class JobJobTasksList(BaseJobEventsList):
|
||||
and their completion status.
|
||||
"""
|
||||
parent_model = Job
|
||||
authentication_classes = [JobTaskAuthentication] + api_settings.DEFAULT_AUTHENTICATION_CLASSES
|
||||
permission_classes = (JobTaskPermission,)
|
||||
view_name = 'Job Play Tasks List'
|
||||
new_in_200 = True
|
||||
|
||||
@@ -2175,6 +2172,174 @@ class JobJobTasksList(BaseJobEventsList):
|
||||
# Done; return the results and count.
|
||||
return (results, count, None)
|
||||
|
||||
|
||||
class AdHocCommandList(ListCreateAPIView):
|
||||
|
||||
model = AdHocCommand
|
||||
serializer_class = AdHocCommandListSerializer
|
||||
new_in_220 = True
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
# Inject inventory ID if parent objects is a host/group.
|
||||
if hasattr(self, 'get_parent_object') and not getattr(self, 'parent_key', None):
|
||||
data = request.DATA
|
||||
# HACK: Make request data mutable.
|
||||
if getattr(data, '_mutable', None) is False:
|
||||
data._mutable = True
|
||||
parent_obj = self.get_parent_object()
|
||||
if isinstance(parent_obj, (Host, Group)):
|
||||
data['inventory'] = parent_obj.inventory_id
|
||||
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)
|
||||
if not result:
|
||||
data = dict(passwords_needed_to_start=ad_hoc_command.passwords_needed_to_start)
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
return response
|
||||
|
||||
|
||||
class InventoryAdHocCommandsList(AdHocCommandList, SubListCreateAPIView):
|
||||
|
||||
parent_model = Inventory
|
||||
relationship = 'ad_hoc_commands'
|
||||
parent_key = 'inventory'
|
||||
|
||||
|
||||
class GroupAdHocCommandsList(AdHocCommandList, SubListCreateAPIView):
|
||||
|
||||
parent_model = Group
|
||||
relationship = 'ad_hoc_commands'
|
||||
|
||||
|
||||
class HostAdHocCommandsList(AdHocCommandList, SubListCreateAPIView):
|
||||
|
||||
parent_model = Host
|
||||
relationship = 'ad_hoc_commands'
|
||||
|
||||
|
||||
class AdHocCommandDetail(RetrieveAPIView):
|
||||
|
||||
model = AdHocCommand
|
||||
serializer_class = AdHocCommandSerializer
|
||||
new_in_220 = True
|
||||
|
||||
|
||||
class AdHocCommandCancel(RetrieveAPIView):
|
||||
|
||||
model = AdHocCommand
|
||||
serializer_class = AdHocCommandCancelSerializer
|
||||
is_job_cancel = True
|
||||
new_in_220 = True
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
obj = self.get_object()
|
||||
if obj.can_cancel:
|
||||
obj.cancel()
|
||||
return Response(status=status.HTTP_202_ACCEPTED)
|
||||
else:
|
||||
return self.http_method_not_allowed(request, *args, **kwargs)
|
||||
|
||||
|
||||
class AdHocCommandRelaunch(GenericAPIView):
|
||||
|
||||
model = AdHocCommand
|
||||
is_job_start = True
|
||||
new_in_220 = True
|
||||
# FIXME: Add serializer class to define fields in OPTIONS request!
|
||||
|
||||
@csrf_exempt
|
||||
@transaction.non_atomic_requests
|
||||
def dispatch(self, *args, **kwargs):
|
||||
return super(AdHocCommandRelaunch, self).dispatch(*args, **kwargs)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
obj = self.get_object()
|
||||
data = {}
|
||||
data['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()
|
||||
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)
|
||||
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)
|
||||
|
||||
|
||||
class AdHocCommandEventList(ListAPIView):
|
||||
|
||||
model = AdHocCommandEvent
|
||||
serializer_class = AdHocCommandEventSerializer
|
||||
new_in_220 = True
|
||||
|
||||
|
||||
class AdHocCommandEventDetail(RetrieveAPIView):
|
||||
|
||||
model = AdHocCommandEvent
|
||||
serializer_class = AdHocCommandEventSerializer
|
||||
new_in_220 = True
|
||||
|
||||
|
||||
class BaseAdHocCommandEventsList(SubListAPIView):
|
||||
|
||||
model = AdHocCommandEvent
|
||||
serializer_class = AdHocCommandEventSerializer
|
||||
parent_model = None # Subclasses must define this attribute.
|
||||
relationship = 'ad_hoc_command_events'
|
||||
view_name = 'Ad Hoc Command Events List'
|
||||
new_in_220 = True
|
||||
|
||||
|
||||
class HostAdHocCommandEventsList(BaseAdHocCommandEventsList):
|
||||
|
||||
parent_model = Host
|
||||
new_in_220 = True
|
||||
|
||||
#class GroupJobEventsList(BaseJobEventsList):
|
||||
# parent_model = Group
|
||||
|
||||
|
||||
class AdHocCommandAdHocCommandEventsList(BaseAdHocCommandEventsList):
|
||||
|
||||
parent_model = AdHocCommand
|
||||
authentication_classes = [TaskAuthentication] + api_settings.DEFAULT_AUTHENTICATION_CLASSES
|
||||
permission_classes = (TaskPermission,)
|
||||
new_in_220 = True
|
||||
|
||||
# Post allowed for ad hoc event callback only.
|
||||
def post(self, request, *args, **kwargs):
|
||||
parent_obj = get_object_or_404(self.parent_model, pk=self.kwargs['pk'])
|
||||
data = request.DATA.copy()
|
||||
data['ad_hoc_command'] = parent_obj.pk
|
||||
serializer = self.get_serializer(data=data)
|
||||
if serializer.is_valid():
|
||||
self.pre_save(serializer.object)
|
||||
self.object = serializer.save(force_insert=True)
|
||||
self.post_save(self.object, created=True)
|
||||
headers = {'Location': serializer.data['url']}
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED,
|
||||
headers=headers)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class AdHocCommandActivityStreamList(SubListAPIView):
|
||||
|
||||
model = ActivityStream
|
||||
serializer_class = ActivityStreamSerializer
|
||||
parent_model = AdHocCommand
|
||||
relationship = 'activitystream_set'
|
||||
new_in_220 = True
|
||||
|
||||
|
||||
class SystemJobList(ListCreateAPIView):
|
||||
|
||||
model = SystemJob
|
||||
@@ -2254,6 +2419,11 @@ class JobStdout(UnifiedJobStdout):
|
||||
|
||||
model = Job
|
||||
|
||||
class AdHocCommandStdout(UnifiedJobStdout):
|
||||
|
||||
model = AdHocCommand
|
||||
new_in_220 = True
|
||||
|
||||
class ActivityStreamList(SimpleListAPIView):
|
||||
|
||||
model = ActivityStream
|
||||
|
||||
Reference in New Issue
Block a user