mirror of
https://github.com/ansible/awx.git
synced 2026-02-28 00:08:44 -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
|
from rest_framework import HTTP_HEADER_ENCODING
|
||||||
|
|
||||||
# AWX
|
# AWX
|
||||||
from awx.main.models import Job, AuthToken
|
from awx.main.models import UnifiedJob, AuthToken
|
||||||
|
|
||||||
|
|
||||||
class TokenAuthentication(authentication.TokenAuthentication):
|
class TokenAuthentication(authentication.TokenAuthentication):
|
||||||
@@ -74,24 +74,26 @@ class TokenAuthentication(authentication.TokenAuthentication):
|
|||||||
# Return the user object and the token.
|
# Return the user object and the token.
|
||||||
return (token.user, token)
|
return (token.user, token)
|
||||||
|
|
||||||
class JobTaskAuthentication(authentication.BaseAuthentication):
|
class TaskAuthentication(authentication.BaseAuthentication):
|
||||||
'''
|
'''
|
||||||
Custom authentication used for views accessed by the inventory and callback
|
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):
|
def authenticate(self, request):
|
||||||
auth = authentication.get_authorization_header(request).split()
|
auth = authentication.get_authorization_header(request).split()
|
||||||
if len(auth) != 2 or auth[0].lower() != 'token' or '-' not in auth[1]:
|
if len(auth) != 2 or auth[0].lower() != 'token' or '-' not in auth[1]:
|
||||||
return None
|
return None
|
||||||
job_id, job_key = auth[1].split('-', 1)
|
pk, key = auth[1].split('-', 1)
|
||||||
try:
|
try:
|
||||||
job = Job.objects.get(pk=job_id, status='running')
|
unified_job = UnifiedJob.objects.get(pk=pk, status='running')
|
||||||
except Job.DoesNotExist:
|
except UnifiedJob.DoesNotExist:
|
||||||
return None
|
return None
|
||||||
token = job.task_auth_token
|
token = unified_job.task_auth_token
|
||||||
if auth[1] != token:
|
if auth[1] != token:
|
||||||
raise exceptions.AuthenticationFailed('Invalid job task token')
|
raise exceptions.AuthenticationFailed('Invalid task token')
|
||||||
return (None, token)
|
return (None, token)
|
||||||
|
|
||||||
def authenticate_header(self, request):
|
def authenticate_header(self, request):
|
||||||
|
|||||||
@@ -29,8 +29,9 @@ from awx.main.utils import * # noqa
|
|||||||
|
|
||||||
__all__ = ['APIView', 'GenericAPIView', 'ListAPIView', 'SimpleListAPIView',
|
__all__ = ['APIView', 'GenericAPIView', 'ListAPIView', 'SimpleListAPIView',
|
||||||
'ListCreateAPIView', 'SubListAPIView', 'SubListCreateAPIView',
|
'ListCreateAPIView', 'SubListAPIView', 'SubListCreateAPIView',
|
||||||
'RetrieveAPIView', 'RetrieveUpdateAPIView',
|
'SubListCreateAttachDetachAPIView', 'RetrieveAPIView',
|
||||||
'RetrieveDestroyAPIView', 'RetrieveUpdateDestroyAPIView', 'DestroyAPIView']
|
'RetrieveUpdateAPIView', 'RetrieveDestroyAPIView',
|
||||||
|
'RetrieveUpdateDestroyAPIView', 'DestroyAPIView']
|
||||||
|
|
||||||
logger = logging.getLogger('awx.api.generics')
|
logger = logging.getLogger('awx.api.generics')
|
||||||
|
|
||||||
@@ -131,12 +132,15 @@ class APIView(views.APIView):
|
|||||||
|
|
||||||
def get_description_context(self):
|
def get_description_context(self):
|
||||||
return {
|
return {
|
||||||
|
'view': self,
|
||||||
'docstring': type(self).__doc__ or '',
|
'docstring': type(self).__doc__ or '',
|
||||||
'new_in_13': getattr(self, 'new_in_13', False),
|
'new_in_13': getattr(self, 'new_in_13', False),
|
||||||
'new_in_14': getattr(self, 'new_in_14', False),
|
'new_in_14': getattr(self, 'new_in_14', False),
|
||||||
'new_in_145': getattr(self, 'new_in_145', False),
|
'new_in_145': getattr(self, 'new_in_145', False),
|
||||||
'new_in_148': getattr(self, 'new_in_148', False),
|
'new_in_148': getattr(self, 'new_in_148', False),
|
||||||
'new_in_200': getattr(self, 'new_in_200', 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):
|
def get_description(self, html=False):
|
||||||
@@ -153,7 +157,7 @@ class APIView(views.APIView):
|
|||||||
'''
|
'''
|
||||||
ret = super(APIView, self).metadata(request)
|
ret = super(APIView, self).metadata(request)
|
||||||
added_in_version = '1.2'
|
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):
|
if getattr(self, 'new_in_%s' % version.replace('.', ''), False):
|
||||||
added_in_version = version
|
added_in_version = version
|
||||||
break
|
break
|
||||||
@@ -328,8 +332,8 @@ class SubListAPIView(ListAPIView):
|
|||||||
return qs & sublist_qs
|
return qs & sublist_qs
|
||||||
|
|
||||||
class SubListCreateAPIView(SubListAPIView, ListCreateAPIView):
|
class SubListCreateAPIView(SubListAPIView, ListCreateAPIView):
|
||||||
# Base class for a sublist view that allows for creating subobjects and
|
# Base class for a sublist view that allows for creating subobjects
|
||||||
# attaching/detaching them from the parent.
|
# associated with the parent object.
|
||||||
|
|
||||||
# In addition to SubListAPIView properties, subclasses may define (if the
|
# In addition to SubListAPIView properties, subclasses may define (if the
|
||||||
# sub_obj requires a foreign key to the parent):
|
# sub_obj requires a foreign key to the parent):
|
||||||
@@ -374,8 +378,13 @@ class SubListCreateAPIView(SubListAPIView, ListCreateAPIView):
|
|||||||
# object deserialized
|
# object deserialized
|
||||||
obj = serializer.save()
|
obj = serializer.save()
|
||||||
serializer = self.serializer_class(obj)
|
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):
|
def attach(self, request, *args, **kwargs):
|
||||||
created = False
|
created = False
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ from awx.main.utils import get_object_or_400
|
|||||||
logger = logging.getLogger('awx.api.permissions')
|
logger = logging.getLogger('awx.api.permissions')
|
||||||
|
|
||||||
__all__ = ['ModelAccessPermission', 'JobTemplateCallbackPermission',
|
__all__ = ['ModelAccessPermission', 'JobTemplateCallbackPermission',
|
||||||
'JobTaskPermission']
|
'TaskPermission']
|
||||||
|
|
||||||
class ModelAccessPermission(permissions.BasePermission):
|
class ModelAccessPermission(permissions.BasePermission):
|
||||||
'''
|
'''
|
||||||
@@ -160,31 +160,31 @@ class JobTemplateCallbackPermission(ModelAccessPermission):
|
|||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
class JobTaskPermission(ModelAccessPermission):
|
class TaskPermission(ModelAccessPermission):
|
||||||
'''
|
'''
|
||||||
Permission checks used for API callbacks from running a task.
|
Permission checks used for API callbacks from running a task.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def has_permission(self, request, view, obj=None):
|
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.
|
# callbacks, default to the superclass permissions checking.
|
||||||
if request.user or not request.auth:
|
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,
|
# Verify that the ID present in the auth token is for a valid, active
|
||||||
# active job.
|
# unified job.
|
||||||
try:
|
try:
|
||||||
job = Job.objects.get(active=True, status='running',
|
unified_job = UnifiedJob.objects.get(active=True, status='running',
|
||||||
pk=int(request.auth.split('-')[0]))
|
pk=int(request.auth.split('-')[0]))
|
||||||
except (Job.DoesNotExist, TypeError):
|
except (UnifiedJob.DoesNotExist, TypeError):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Verify that the request method is one of those allowed for the given
|
# 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
|
# view, also that the job or inventory being accessed matches the auth
|
||||||
# token.
|
# token.
|
||||||
if view.model == Inventory and request.method.lower() in ('head', 'get'):
|
if view.model == Inventory and request.method.lower() in ('head', 'get'):
|
||||||
return bool(not obj or obj.pk == job.inventory.pk)
|
return bool(not obj or obj.pk == unified_job.inventory_id)
|
||||||
elif view.model == JobEvent and request.method.lower() == 'post':
|
elif view.model in (JobEvent, AdHocCommandEvent) and request.method.lower() == 'post':
|
||||||
return bool(not obj or obj.pk == job.pk)
|
return bool(not obj or obj.pk == unified_job.pk)
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -132,16 +132,17 @@ class BaseSerializerMetaclass(serializers.SerializerMetaclass):
|
|||||||
for attr in dir(other):
|
for attr in dir(other):
|
||||||
if attr.startswith('_'):
|
if attr.startswith('_'):
|
||||||
continue
|
continue
|
||||||
val = getattr(other, attr)
|
meta_val = getattr(meta, attr, [])
|
||||||
|
val = getattr(other, attr, [])
|
||||||
# Special handling for lists of strings (field names).
|
# Special handling for lists of strings (field names).
|
||||||
if isinstance(val, (list, tuple)) and all([isinstance(x, basestring) for x in val]):
|
if isinstance(val, (list, tuple)) and all([isinstance(x, basestring) for x in val]):
|
||||||
new_vals = []
|
new_vals = []
|
||||||
except_vals = []
|
except_vals = []
|
||||||
if base: # Merge values from all bases.
|
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:
|
for v in val:
|
||||||
if not base and v == '*': # Inherit all values from previous base(es).
|
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.
|
elif not base and v.startswith('-'): # Except these values.
|
||||||
except_vals.append(v[1:])
|
except_vals.append(v[1:])
|
||||||
else:
|
else:
|
||||||
@@ -226,6 +227,7 @@ class BaseSerializer(serializers.ModelSerializer):
|
|||||||
def get_type_choices(self):
|
def get_type_choices(self):
|
||||||
type_name_map = {
|
type_name_map = {
|
||||||
'job': 'Playbook Run',
|
'job': 'Playbook Run',
|
||||||
|
'ad_hoc_command': 'Ad Hoc Command',
|
||||||
'project_update': 'SCM Update',
|
'project_update': 'SCM Update',
|
||||||
'inventory_update': 'Inventory Sync',
|
'inventory_update': 'Inventory Sync',
|
||||||
'system_job': 'Management Job',
|
'system_job': 'Management Job',
|
||||||
@@ -347,6 +349,23 @@ class BaseSerializer(serializers.ModelSerializer):
|
|||||||
exclusions.remove(field_name)
|
exclusions.remove(field_name)
|
||||||
return exclusions
|
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):
|
class UnifiedJobTemplateSerializer(BaseSerializer):
|
||||||
|
|
||||||
@@ -390,6 +409,7 @@ class UnifiedJobTemplateSerializer(BaseSerializer):
|
|||||||
class UnifiedJobSerializer(BaseSerializer):
|
class UnifiedJobSerializer(BaseSerializer):
|
||||||
|
|
||||||
result_stdout = serializers.Field(source='result_stdout')
|
result_stdout = serializers.Field(source='result_stdout')
|
||||||
|
unified_job_template = serializers.Field(source='unified_job_template')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = UnifiedJob
|
model = UnifiedJob
|
||||||
@@ -400,7 +420,7 @@ class UnifiedJobSerializer(BaseSerializer):
|
|||||||
|
|
||||||
def get_types(self):
|
def get_types(self):
|
||||||
if type(self) is UnifiedJobSerializer:
|
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:
|
else:
|
||||||
return super(UnifiedJobSerializer, self).get_types()
|
return super(UnifiedJobSerializer, self).get_types()
|
||||||
|
|
||||||
@@ -416,6 +436,8 @@ class UnifiedJobSerializer(BaseSerializer):
|
|||||||
res['stdout'] = reverse('api:inventory_update_stdout', args=(obj.pk,))
|
res['stdout'] = reverse('api:inventory_update_stdout', args=(obj.pk,))
|
||||||
elif isinstance(obj, Job):
|
elif isinstance(obj, Job):
|
||||||
res['stdout'] = reverse('api:job_stdout', args=(obj.pk,))
|
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
|
return res
|
||||||
|
|
||||||
def to_native(self, obj):
|
def to_native(self, obj):
|
||||||
@@ -427,6 +449,8 @@ class UnifiedJobSerializer(BaseSerializer):
|
|||||||
serializer_class = InventoryUpdateSerializer
|
serializer_class = InventoryUpdateSerializer
|
||||||
elif isinstance(obj, Job):
|
elif isinstance(obj, Job):
|
||||||
serializer_class = JobSerializer
|
serializer_class = JobSerializer
|
||||||
|
elif isinstance(obj, AdHocCommand):
|
||||||
|
serializer_class = AdHocCommandSerializer
|
||||||
elif isinstance(obj, SystemJob):
|
elif isinstance(obj, SystemJob):
|
||||||
serializer_class = SystemJobSerializer
|
serializer_class = SystemJobSerializer
|
||||||
if serializer_class:
|
if serializer_class:
|
||||||
@@ -447,7 +471,7 @@ class UnifiedJobListSerializer(UnifiedJobSerializer):
|
|||||||
|
|
||||||
def get_types(self):
|
def get_types(self):
|
||||||
if type(self) is UnifiedJobListSerializer:
|
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:
|
else:
|
||||||
return super(UnifiedJobListSerializer, self).get_types()
|
return super(UnifiedJobListSerializer, self).get_types()
|
||||||
|
|
||||||
@@ -460,6 +484,8 @@ class UnifiedJobListSerializer(UnifiedJobSerializer):
|
|||||||
serializer_class = InventoryUpdateListSerializer
|
serializer_class = InventoryUpdateListSerializer
|
||||||
elif isinstance(obj, Job):
|
elif isinstance(obj, Job):
|
||||||
serializer_class = JobListSerializer
|
serializer_class = JobListSerializer
|
||||||
|
elif isinstance(obj, AdHocCommand):
|
||||||
|
serializer_class = AdHocCommandListSerializer
|
||||||
elif isinstance(obj, SystemJob):
|
elif isinstance(obj, SystemJob):
|
||||||
serializer_class = SystemJobListSerializer
|
serializer_class = SystemJobListSerializer
|
||||||
if serializer_class:
|
if serializer_class:
|
||||||
@@ -479,7 +505,7 @@ class UnifiedJobStdoutSerializer(UnifiedJobSerializer):
|
|||||||
|
|
||||||
def get_types(self):
|
def get_types(self):
|
||||||
if type(self) is UnifiedJobStdoutSerializer:
|
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:
|
else:
|
||||||
return super(UnifiedJobStdoutSerializer, self).get_types()
|
return super(UnifiedJobStdoutSerializer, self).get_types()
|
||||||
|
|
||||||
@@ -746,6 +772,7 @@ class InventorySerializer(BaseSerializerWithVariables):
|
|||||||
inventory_sources = reverse('api:inventory_inventory_sources_list', args=(obj.pk,)),
|
inventory_sources = reverse('api:inventory_inventory_sources_list', args=(obj.pk,)),
|
||||||
activity_stream = reverse('api:inventory_activity_stream_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,)),
|
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:
|
if obj.organization and obj.organization.active:
|
||||||
res['organization'] = reverse('api:organization_detail', args=(obj.organization.pk,))
|
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,)),
|
job_host_summaries = reverse('api:host_job_host_summaries_list', args=(obj.pk,)),
|
||||||
activity_stream = reverse('api:host_activity_stream_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,)),
|
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:
|
if obj.inventory and obj.inventory.active:
|
||||||
res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,))
|
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,)),
|
job_host_summaries = reverse('api:group_job_host_summaries_list', args=(obj.pk,)),
|
||||||
activity_stream = reverse('api:group_activity_stream_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,)),
|
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:
|
if obj.inventory and obj.inventory.active:
|
||||||
res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,))
|
res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,))
|
||||||
@@ -1174,7 +1204,7 @@ class PermissionSerializer(BaseSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Permission
|
model = Permission
|
||||||
fields = ('*', 'user', 'team', 'project', 'inventory',
|
fields = ('*', 'user', 'team', 'project', 'inventory',
|
||||||
'permission_type')
|
'permission_type', 'run_ad_hoc_commands')
|
||||||
|
|
||||||
def get_related(self, obj):
|
def get_related(self, obj):
|
||||||
res = super(PermissionSerializer, self).get_related(obj)
|
res = super(PermissionSerializer, self).get_related(obj)
|
||||||
@@ -1190,15 +1220,15 @@ class PermissionSerializer(BaseSerializer):
|
|||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
# Can only set either user or team.
|
# 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'
|
raise serializers.ValidationError('permission can only be assigned'
|
||||||
' to a user OR a team, not both')
|
' to a user OR a team, not both')
|
||||||
# Cannot assign admit/read/write permissions for a project.
|
# 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 '
|
raise serializers.ValidationError('project cannot be assigned for '
|
||||||
'inventory-only permissions')
|
'inventory-only permissions')
|
||||||
# Project is required when setting deployment 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 '
|
raise serializers.ValidationError('project is required when '
|
||||||
'assigning deployment permissions')
|
'assigning deployment permissions')
|
||||||
return attrs
|
return attrs
|
||||||
@@ -1451,6 +1481,56 @@ class JobCancelSerializer(JobSerializer):
|
|||||||
fields = ('can_cancel',)
|
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 SystemJobTemplateSerializer(UnifiedJobTemplateSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -1482,6 +1562,9 @@ class SystemJobSerializer(UnifiedJobSerializer):
|
|||||||
class JobListSerializer(JobSerializer, UnifiedJobListSerializer):
|
class JobListSerializer(JobSerializer, UnifiedJobListSerializer):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class AdHocCommandListSerializer(AdHocCommandSerializer, UnifiedJobListSerializer):
|
||||||
|
pass
|
||||||
|
|
||||||
class SystemJobListSerializer(SystemJobSerializer, UnifiedJobListSerializer):
|
class SystemJobListSerializer(SystemJobSerializer, UnifiedJobListSerializer):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -1548,6 +1631,27 @@ class JobEventSerializer(BaseSerializer):
|
|||||||
pass
|
pass
|
||||||
return d
|
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 ScheduleSerializer(BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ fields to create a new {{ model_verbose_name }} associated with this
|
|||||||
|
|
||||||
{% block post_create %}{% endblock %}
|
{% block post_create %}{% endblock %}
|
||||||
|
|
||||||
|
{% if view.attach %}
|
||||||
{% if parent_key %}
|
{% if parent_key %}
|
||||||
# Remove {{ parent_model_verbose_name|title }} {{ model_verbose_name_plural|title }}:
|
# 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 }}
|
remove the {{ model_verbose_name }} from this {{ parent_model_verbose_name }}
|
||||||
without deleting the {{ model_verbose_name }}.
|
without deleting the {{ model_verbose_name }}.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% include "api/_new_in_awx.md" %}
|
{% 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]+)/inventory_sources/$', 'inventory_inventory_sources_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/activity_stream/$', 'inventory_activity_stream_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]+)/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',
|
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]+)/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]+)/activity_stream/$', 'host_activity_stream_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/inventory_sources/$', 'host_inventory_sources_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',
|
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]+)/potential_children/$', 'group_potential_children_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/activity_stream/$', 'group_activity_stream_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]+)/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',
|
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'),
|
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',
|
system_job_template_urls = patterns('awx.api.views',
|
||||||
url(r'^$', 'system_job_template_list'),
|
url(r'^$', 'system_job_template_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/$', 'system_job_template_detail'),
|
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'^jobs/', include(job_urls)),
|
||||||
url(r'^job_host_summaries/', include(job_host_summary_urls)),
|
url(r'^job_host_summaries/', include(job_host_summary_urls)),
|
||||||
url(r'^job_events/', include(job_event_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_job_templates/', include(system_job_template_urls)),
|
||||||
url(r'^system_jobs/', include(system_job_urls)),
|
url(r'^system_jobs/', include(system_job_urls)),
|
||||||
url(r'^unified_job_templates/$', 'unified_job_template_list'),
|
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.task_engine import TaskSerializer, TASK_FILE
|
||||||
from awx.main.access import get_user_queryset
|
from awx.main.access import get_user_queryset
|
||||||
from awx.main.ha import is_ha_environment
|
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.utils.decorators import paginated
|
||||||
from awx.api.generics import get_view_name
|
from awx.api.generics import get_view_name
|
||||||
from awx.api.generics import * # noqa
|
from awx.api.generics import * # noqa
|
||||||
@@ -111,6 +111,7 @@ class ApiV1RootView(APIView):
|
|||||||
data['hosts'] = reverse('api:host_list')
|
data['hosts'] = reverse('api:host_list')
|
||||||
data['job_templates'] = reverse('api:job_template_list')
|
data['job_templates'] = reverse('api:job_template_list')
|
||||||
data['jobs'] = reverse('api:job_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_job_templates'] = reverse('api:system_job_template_list')
|
||||||
data['system_jobs'] = reverse('api:system_job_list')
|
data['system_jobs'] = reverse('api:system_job_list')
|
||||||
data['schedules'] = reverse('api:schedule_list')
|
data['schedules'] = reverse('api:schedule_list')
|
||||||
@@ -492,28 +493,28 @@ class OrganizationInventoriesList(SubListAPIView):
|
|||||||
parent_model = Organization
|
parent_model = Organization
|
||||||
relationship = 'inventories'
|
relationship = 'inventories'
|
||||||
|
|
||||||
class OrganizationUsersList(SubListCreateAPIView):
|
class OrganizationUsersList(SubListCreateAttachDetachAPIView):
|
||||||
|
|
||||||
model = User
|
model = User
|
||||||
serializer_class = UserSerializer
|
serializer_class = UserSerializer
|
||||||
parent_model = Organization
|
parent_model = Organization
|
||||||
relationship = 'users'
|
relationship = 'users'
|
||||||
|
|
||||||
class OrganizationAdminsList(SubListCreateAPIView):
|
class OrganizationAdminsList(SubListCreateAttachDetachAPIView):
|
||||||
|
|
||||||
model = User
|
model = User
|
||||||
serializer_class = UserSerializer
|
serializer_class = UserSerializer
|
||||||
parent_model = Organization
|
parent_model = Organization
|
||||||
relationship = 'admins'
|
relationship = 'admins'
|
||||||
|
|
||||||
class OrganizationProjectsList(SubListCreateAPIView):
|
class OrganizationProjectsList(SubListCreateAttachDetachAPIView):
|
||||||
|
|
||||||
model = Project
|
model = Project
|
||||||
serializer_class = ProjectSerializer
|
serializer_class = ProjectSerializer
|
||||||
parent_model = Organization
|
parent_model = Organization
|
||||||
relationship = 'projects'
|
relationship = 'projects'
|
||||||
|
|
||||||
class OrganizationTeamsList(SubListCreateAPIView):
|
class OrganizationTeamsList(SubListCreateAttachDetachAPIView):
|
||||||
|
|
||||||
model = Team
|
model = Team
|
||||||
serializer_class = TeamSerializer
|
serializer_class = TeamSerializer
|
||||||
@@ -539,14 +540,14 @@ class TeamDetail(RetrieveUpdateDestroyAPIView):
|
|||||||
model = Team
|
model = Team
|
||||||
serializer_class = TeamSerializer
|
serializer_class = TeamSerializer
|
||||||
|
|
||||||
class TeamUsersList(SubListCreateAPIView):
|
class TeamUsersList(SubListCreateAttachDetachAPIView):
|
||||||
|
|
||||||
model = User
|
model = User
|
||||||
serializer_class = UserSerializer
|
serializer_class = UserSerializer
|
||||||
parent_model = Team
|
parent_model = Team
|
||||||
relationship = 'users'
|
relationship = 'users'
|
||||||
|
|
||||||
class TeamPermissionsList(SubListCreateAPIView):
|
class TeamPermissionsList(SubListCreateAttachDetachAPIView):
|
||||||
|
|
||||||
model = Permission
|
model = Permission
|
||||||
serializer_class = PermissionSerializer
|
serializer_class = PermissionSerializer
|
||||||
@@ -565,14 +566,14 @@ class TeamPermissionsList(SubListCreateAPIView):
|
|||||||
return base
|
return base
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
|
|
||||||
class TeamProjectsList(SubListCreateAPIView):
|
class TeamProjectsList(SubListCreateAttachDetachAPIView):
|
||||||
|
|
||||||
model = Project
|
model = Project
|
||||||
serializer_class = ProjectSerializer
|
serializer_class = ProjectSerializer
|
||||||
parent_model = Team
|
parent_model = Team
|
||||||
relationship = 'projects'
|
relationship = 'projects'
|
||||||
|
|
||||||
class TeamCredentialsList(SubListCreateAPIView):
|
class TeamCredentialsList(SubListCreateAttachDetachAPIView):
|
||||||
|
|
||||||
model = Credential
|
model = Credential
|
||||||
serializer_class = CredentialSerializer
|
serializer_class = CredentialSerializer
|
||||||
@@ -631,21 +632,21 @@ class ProjectPlaybooks(RetrieveAPIView):
|
|||||||
model = Project
|
model = Project
|
||||||
serializer_class = ProjectPlaybooksSerializer
|
serializer_class = ProjectPlaybooksSerializer
|
||||||
|
|
||||||
class ProjectOrganizationsList(SubListCreateAPIView):
|
class ProjectOrganizationsList(SubListCreateAttachDetachAPIView):
|
||||||
|
|
||||||
model = Organization
|
model = Organization
|
||||||
serializer_class = OrganizationSerializer
|
serializer_class = OrganizationSerializer
|
||||||
parent_model = Project
|
parent_model = Project
|
||||||
relationship = 'organizations'
|
relationship = 'organizations'
|
||||||
|
|
||||||
class ProjectTeamsList(SubListCreateAPIView):
|
class ProjectTeamsList(SubListCreateAttachDetachAPIView):
|
||||||
|
|
||||||
model = Team
|
model = Team
|
||||||
serializer_class = TeamSerializer
|
serializer_class = TeamSerializer
|
||||||
parent_model = Project
|
parent_model = Project
|
||||||
relationship = 'teams'
|
relationship = 'teams'
|
||||||
|
|
||||||
class ProjectSchedulesList(SubListCreateAPIView):
|
class ProjectSchedulesList(SubListCreateAttachDetachAPIView):
|
||||||
|
|
||||||
view_name = "Project Schedules"
|
view_name = "Project Schedules"
|
||||||
|
|
||||||
@@ -746,7 +747,7 @@ class UserTeamsList(SubListAPIView):
|
|||||||
parent_model = User
|
parent_model = User
|
||||||
relationship = 'teams'
|
relationship = 'teams'
|
||||||
|
|
||||||
class UserPermissionsList(SubListCreateAPIView):
|
class UserPermissionsList(SubListCreateAttachDetachAPIView):
|
||||||
|
|
||||||
model = Permission
|
model = Permission
|
||||||
serializer_class = PermissionSerializer
|
serializer_class = PermissionSerializer
|
||||||
@@ -767,7 +768,7 @@ class UserProjectsList(SubListAPIView):
|
|||||||
qs = self.request.user.get_queryset(self.model)
|
qs = self.request.user.get_queryset(self.model)
|
||||||
return qs.filter(teams__in=parent.teams.distinct())
|
return qs.filter(teams__in=parent.teams.distinct())
|
||||||
|
|
||||||
class UserCredentialsList(SubListCreateAPIView):
|
class UserCredentialsList(SubListCreateAttachDetachAPIView):
|
||||||
|
|
||||||
model = Credential
|
model = Credential
|
||||||
serializer_class = CredentialSerializer
|
serializer_class = CredentialSerializer
|
||||||
@@ -932,7 +933,7 @@ class HostDetail(RetrieveUpdateDestroyAPIView):
|
|||||||
model = Host
|
model = Host
|
||||||
serializer_class = HostSerializer
|
serializer_class = HostSerializer
|
||||||
|
|
||||||
class InventoryHostsList(SubListCreateAPIView):
|
class InventoryHostsList(SubListCreateAttachDetachAPIView):
|
||||||
|
|
||||||
model = Host
|
model = Host
|
||||||
serializer_class = HostSerializer
|
serializer_class = HostSerializer
|
||||||
@@ -940,7 +941,7 @@ class InventoryHostsList(SubListCreateAPIView):
|
|||||||
relationship = 'hosts'
|
relationship = 'hosts'
|
||||||
parent_key = 'inventory'
|
parent_key = 'inventory'
|
||||||
|
|
||||||
class HostGroupsList(SubListCreateAPIView):
|
class HostGroupsList(SubListCreateAttachDetachAPIView):
|
||||||
''' the list of groups a host is directly a member of '''
|
''' the list of groups a host is directly a member of '''
|
||||||
|
|
||||||
model = Group
|
model = Group
|
||||||
@@ -991,7 +992,7 @@ class GroupList(ListCreateAPIView):
|
|||||||
model = Group
|
model = Group
|
||||||
serializer_class = GroupSerializer
|
serializer_class = GroupSerializer
|
||||||
|
|
||||||
class GroupChildrenList(SubListCreateAPIView):
|
class GroupChildrenList(SubListCreateAttachDetachAPIView):
|
||||||
|
|
||||||
model = Group
|
model = Group
|
||||||
serializer_class = GroupSerializer
|
serializer_class = GroupSerializer
|
||||||
@@ -1050,7 +1051,7 @@ class GroupPotentialChildrenList(SubListAPIView):
|
|||||||
except_pks.update(parent.all_children.values_list('pk', flat=True))
|
except_pks.update(parent.all_children.values_list('pk', flat=True))
|
||||||
return qs.exclude(pk__in=except_pks)
|
return qs.exclude(pk__in=except_pks)
|
||||||
|
|
||||||
class GroupHostsList(SubListCreateAPIView):
|
class GroupHostsList(SubListCreateAttachDetachAPIView):
|
||||||
''' the list of hosts directly below a group '''
|
''' the list of hosts directly below a group '''
|
||||||
|
|
||||||
model = Host
|
model = Host
|
||||||
@@ -1124,7 +1125,7 @@ class GroupDetail(RetrieveUpdateDestroyAPIView):
|
|||||||
obj.mark_inactive_recursive()
|
obj.mark_inactive_recursive()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
class InventoryGroupsList(SubListCreateAPIView):
|
class InventoryGroupsList(SubListCreateAttachDetachAPIView):
|
||||||
|
|
||||||
model = Group
|
model = Group
|
||||||
serializer_class = GroupSerializer
|
serializer_class = GroupSerializer
|
||||||
@@ -1132,7 +1133,7 @@ class InventoryGroupsList(SubListCreateAPIView):
|
|||||||
relationship = 'groups'
|
relationship = 'groups'
|
||||||
parent_key = 'inventory'
|
parent_key = 'inventory'
|
||||||
|
|
||||||
class InventoryRootGroupsList(SubListCreateAPIView):
|
class InventoryRootGroupsList(SubListCreateAttachDetachAPIView):
|
||||||
|
|
||||||
model = Group
|
model = Group
|
||||||
serializer_class = GroupSerializer
|
serializer_class = GroupSerializer
|
||||||
@@ -1171,8 +1172,8 @@ class InventoryScriptView(RetrieveAPIView):
|
|||||||
|
|
||||||
model = Inventory
|
model = Inventory
|
||||||
serializer_class = InventoryScriptSerializer
|
serializer_class = InventoryScriptSerializer
|
||||||
authentication_classes = [JobTaskAuthentication] + api_settings.DEFAULT_AUTHENTICATION_CLASSES
|
authentication_classes = [TaskAuthentication] + api_settings.DEFAULT_AUTHENTICATION_CLASSES
|
||||||
permission_classes = (JobTaskPermission,)
|
permission_classes = (TaskPermission,)
|
||||||
filter_backends = ()
|
filter_backends = ()
|
||||||
|
|
||||||
def retrieve(self, request, *args, **kwargs):
|
def retrieve(self, request, *args, **kwargs):
|
||||||
@@ -1330,7 +1331,7 @@ class InventorySourceDetail(RetrieveUpdateAPIView):
|
|||||||
pu.cancel()
|
pu.cancel()
|
||||||
return super(InventorySourceDetail, self).destroy(request, *args, **kwargs)
|
return super(InventorySourceDetail, self).destroy(request, *args, **kwargs)
|
||||||
|
|
||||||
class InventorySourceSchedulesList(SubListCreateAPIView):
|
class InventorySourceSchedulesList(SubListCreateAttachDetachAPIView):
|
||||||
|
|
||||||
view_name = "Inventory Source Schedules"
|
view_name = "Inventory Source Schedules"
|
||||||
|
|
||||||
@@ -1479,7 +1480,7 @@ class JobTemplateLaunch(GenericAPIView):
|
|||||||
data = dict(job=new_job.id)
|
data = dict(job=new_job.id)
|
||||||
return Response(data, status=status.HTTP_202_ACCEPTED)
|
return Response(data, status=status.HTTP_202_ACCEPTED)
|
||||||
|
|
||||||
class JobTemplateSchedulesList(SubListCreateAPIView):
|
class JobTemplateSchedulesList(SubListCreateAttachDetachAPIView):
|
||||||
|
|
||||||
view_name = "Job Template Schedules"
|
view_name = "Job Template Schedules"
|
||||||
|
|
||||||
@@ -1749,7 +1750,7 @@ class SystemJobTemplateLaunch(GenericAPIView):
|
|||||||
data = dict(system_job=new_job.id)
|
data = dict(system_job=new_job.id)
|
||||||
return Response(data, status=status.HTTP_202_ACCEPTED)
|
return Response(data, status=status.HTTP_202_ACCEPTED)
|
||||||
|
|
||||||
class SystemJobTemplateSchedulesList(SubListCreateAPIView):
|
class SystemJobTemplateSchedulesList(SubListCreateAttachDetachAPIView):
|
||||||
|
|
||||||
view_name = "System Job Template Schedules"
|
view_name = "System Job Template Schedules"
|
||||||
|
|
||||||
@@ -1944,8 +1945,8 @@ class GroupJobEventsList(BaseJobEventsList):
|
|||||||
class JobJobEventsList(BaseJobEventsList):
|
class JobJobEventsList(BaseJobEventsList):
|
||||||
|
|
||||||
parent_model = Job
|
parent_model = Job
|
||||||
authentication_classes = [JobTaskAuthentication] + api_settings.DEFAULT_AUTHENTICATION_CLASSES
|
authentication_classes = [TaskAuthentication] + api_settings.DEFAULT_AUTHENTICATION_CLASSES
|
||||||
permission_classes = (JobTaskPermission,)
|
permission_classes = (TaskPermission,)
|
||||||
|
|
||||||
# Post allowed for job event callback only.
|
# Post allowed for job event callback only.
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
@@ -1966,8 +1967,6 @@ class JobJobPlaysList(BaseJobEventsList):
|
|||||||
|
|
||||||
parent_model = Job
|
parent_model = Job
|
||||||
view_name = 'Job Plays List'
|
view_name = 'Job Plays List'
|
||||||
authentication_classes = [JobTaskAuthentication] + api_settings.DEFAULT_AUTHENTICATION_CLASSES
|
|
||||||
permission_classes = (JobTaskPermission,)
|
|
||||||
new_in_200 = True
|
new_in_200 = True
|
||||||
|
|
||||||
@paginated
|
@paginated
|
||||||
@@ -2042,8 +2041,6 @@ class JobJobTasksList(BaseJobEventsList):
|
|||||||
and their completion status.
|
and their completion status.
|
||||||
"""
|
"""
|
||||||
parent_model = Job
|
parent_model = Job
|
||||||
authentication_classes = [JobTaskAuthentication] + api_settings.DEFAULT_AUTHENTICATION_CLASSES
|
|
||||||
permission_classes = (JobTaskPermission,)
|
|
||||||
view_name = 'Job Play Tasks List'
|
view_name = 'Job Play Tasks List'
|
||||||
new_in_200 = True
|
new_in_200 = True
|
||||||
|
|
||||||
@@ -2175,6 +2172,174 @@ class JobJobTasksList(BaseJobEventsList):
|
|||||||
# Done; return the results and count.
|
# Done; return the results and count.
|
||||||
return (results, count, None)
|
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):
|
class SystemJobList(ListCreateAPIView):
|
||||||
|
|
||||||
model = SystemJob
|
model = SystemJob
|
||||||
@@ -2254,6 +2419,11 @@ class JobStdout(UnifiedJobStdout):
|
|||||||
|
|
||||||
model = Job
|
model = Job
|
||||||
|
|
||||||
|
class AdHocCommandStdout(UnifiedJobStdout):
|
||||||
|
|
||||||
|
model = AdHocCommand
|
||||||
|
new_in_220 = True
|
||||||
|
|
||||||
class ActivityStreamList(SimpleListAPIView):
|
class ActivityStreamList(SimpleListAPIView):
|
||||||
|
|
||||||
model = ActivityStream
|
model = ActivityStream
|
||||||
|
|||||||
@@ -152,6 +152,22 @@ class BaseAccess(object):
|
|||||||
def can_unattach(self, obj, sub_obj, relationship):
|
def can_unattach(self, obj, sub_obj, relationship):
|
||||||
return self.can_change(obj, None)
|
return self.can_change(obj, None)
|
||||||
|
|
||||||
|
def check_license(self):
|
||||||
|
reader = TaskSerializer()
|
||||||
|
validation_info = reader.from_file()
|
||||||
|
if 'test' in sys.argv or 'jenkins' in sys.argv:
|
||||||
|
validation_info['free_instances'] = 99999999
|
||||||
|
validation_info['time_remaining'] = 99999999
|
||||||
|
validation_info['grace_period_remaining'] = 99999999
|
||||||
|
|
||||||
|
if validation_info.get('time_remaining', None) is None:
|
||||||
|
raise PermissionDenied("license is missing")
|
||||||
|
if validation_info.get("grace_period_remaining") <= 0:
|
||||||
|
raise PermissionDenied("license has expired")
|
||||||
|
if validation_info.get('free_instances', 0) < 0:
|
||||||
|
#raise PermissionDenied("Host Count exceeds available instances")
|
||||||
|
raise PermissionDenied("license range of %s instances has been exceeded" % validation_info.get('available_instances', 0))
|
||||||
|
|
||||||
class UserAccess(BaseAccess):
|
class UserAccess(BaseAccess):
|
||||||
'''
|
'''
|
||||||
I can see user records when:
|
I can see user records when:
|
||||||
@@ -255,6 +271,10 @@ class InventoryAccess(BaseAccess):
|
|||||||
- I'm a superuser.
|
- I'm a superuser.
|
||||||
- I'm an org admin of the inventory's org.
|
- I'm an org admin of the inventory's org.
|
||||||
- I have admin permissions on it.
|
- I have admin permissions on it.
|
||||||
|
I can run ad hoc commands when:
|
||||||
|
- I'm a superuser.
|
||||||
|
- I'm an org admin of the inventory's org.
|
||||||
|
- I have read/write/admin permission on an inventory with the run_ad_hoc_commands flag set.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
model = Inventory
|
model = Inventory
|
||||||
@@ -327,6 +347,18 @@ class InventoryAccess(BaseAccess):
|
|||||||
def can_delete(self, obj):
|
def can_delete(self, obj):
|
||||||
return self.can_admin(obj, None)
|
return self.can_admin(obj, None)
|
||||||
|
|
||||||
|
def can_run_ad_hoc_commands(self, obj):
|
||||||
|
qs = self.get_queryset(PERMISSION_TYPES_ALLOWING_INVENTORY_READ)
|
||||||
|
if not obj or not qs.filter(pk=obj.pk).exists():
|
||||||
|
return False
|
||||||
|
if self.user.is_superuser:
|
||||||
|
return True
|
||||||
|
if self.user in obj.organization.admins.all():
|
||||||
|
return True
|
||||||
|
if qs.filter(pk=obj.pk, permissions__permission_type__in=PERMISSION_TYPES_ALLOWING_INVENTORY_READ, permissions__run_ad_hoc_commands=True).exists():
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
class HostAccess(BaseAccess):
|
class HostAccess(BaseAccess):
|
||||||
'''
|
'''
|
||||||
I can see hosts whenever I can see their inventory.
|
I can see hosts whenever I can see their inventory.
|
||||||
@@ -358,25 +390,8 @@ class HostAccess(BaseAccess):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# Check to see if we have enough licenses
|
# Check to see if we have enough licenses
|
||||||
reader = TaskSerializer()
|
self.check_license()
|
||||||
validation_info = reader.from_file()
|
return True
|
||||||
|
|
||||||
if 'test' in sys.argv or 'jenkins' in sys.argv:
|
|
||||||
# this hack is in here so the test code can function
|
|
||||||
# but still go down *most* of the license code path.
|
|
||||||
validation_info['free_instances'] = 99999999
|
|
||||||
validation_info['time_remaining'] = 99999999
|
|
||||||
validation_info['grace_period_remaining'] = 99999999
|
|
||||||
|
|
||||||
if validation_info.get('time_remaining', None) is None:
|
|
||||||
raise PermissionDenied("license is missing")
|
|
||||||
if validation_info.get('grace_period_remaining') <= 0:
|
|
||||||
raise PermissionDenied("license has expired")
|
|
||||||
|
|
||||||
if validation_info.get('free_instances', 0) > 0:
|
|
||||||
return True
|
|
||||||
instances = validation_info.get('available_instances', 0)
|
|
||||||
raise PermissionDenied("license range of %s instances has been exceeded" % instances)
|
|
||||||
|
|
||||||
def can_change(self, obj, data):
|
def can_change(self, obj, data):
|
||||||
# Prevent moving a host to a different inventory.
|
# Prevent moving a host to a different inventory.
|
||||||
@@ -972,21 +987,9 @@ class JobTemplateAccess(BaseAccess):
|
|||||||
# return False
|
# return False
|
||||||
|
|
||||||
def can_start(self, obj, validate_license=True):
|
def can_start(self, obj, validate_license=True):
|
||||||
reader = TaskSerializer()
|
# Check license.
|
||||||
validation_info = reader.from_file()
|
|
||||||
|
|
||||||
if validate_license:
|
if validate_license:
|
||||||
if 'test' in sys.argv or 'jenkins' in sys.argv:
|
self.check_license()
|
||||||
validation_info['free_instances'] = 99999999
|
|
||||||
validation_info['time_remaining'] = 99999999
|
|
||||||
validation_info['grace_period_remaining'] = 99999999
|
|
||||||
|
|
||||||
if validation_info.get('time_remaining', None) is None:
|
|
||||||
raise PermissionDenied("license is missing")
|
|
||||||
if validation_info.get("grace_period_remaining") <= 0:
|
|
||||||
raise PermissionDenied("license has expired")
|
|
||||||
if validation_info.get('free_instances', 0) < 0:
|
|
||||||
raise PermissionDenied("Host Count exceeds available instances")
|
|
||||||
|
|
||||||
# Super users can start any job
|
# Super users can start any job
|
||||||
if self.user.is_superuser:
|
if self.user.is_superuser:
|
||||||
@@ -1105,19 +1108,6 @@ class JobAccess(BaseAccess):
|
|||||||
if not self.user.is_superuser:
|
if not self.user.is_superuser:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
reader = TaskSerializer()
|
|
||||||
validation_info = reader.from_file()
|
|
||||||
if 'test' in sys.argv or 'jenkins' in sys.argv:
|
|
||||||
validation_info['free_instances'] = 99999999
|
|
||||||
validation_info['time_remaining'] = 99999999
|
|
||||||
validation_info['grace_period_remaining'] = 99999999
|
|
||||||
|
|
||||||
if validation_info.get('time_remaining', None) is None:
|
|
||||||
raise PermissionDenied("license is missing")
|
|
||||||
if validation_info.get("grace_period_remaining") <= 0:
|
|
||||||
raise PermissionDenied("license has expired")
|
|
||||||
if validation_info.get('free_instances', 0) < 0:
|
|
||||||
raise PermissionDenied("Host Count exceeds available instances")
|
|
||||||
|
|
||||||
add_data = dict(data.items())
|
add_data = dict(data.items())
|
||||||
|
|
||||||
@@ -1142,20 +1132,7 @@ class JobAccess(BaseAccess):
|
|||||||
return self.can_read(obj)
|
return self.can_read(obj)
|
||||||
|
|
||||||
def can_start(self, obj):
|
def can_start(self, obj):
|
||||||
reader = TaskSerializer()
|
self.check_license()
|
||||||
validation_info = reader.from_file()
|
|
||||||
|
|
||||||
if 'test' in sys.argv or 'jenkins' in sys.argv:
|
|
||||||
validation_info['free_instances'] = 99999999
|
|
||||||
validation_info['time_remaining'] = 99999999
|
|
||||||
validation_info['grace_period_remaining'] = 99999999
|
|
||||||
|
|
||||||
if validation_info.get('time_remaining', None) is None:
|
|
||||||
raise PermissionDenied("license is missing")
|
|
||||||
if validation_info.get("grace_period_remaining") <= 0:
|
|
||||||
raise PermissionDenied("license has expired")
|
|
||||||
if validation_info.get('free_instances', 0) < 0:
|
|
||||||
raise PermissionDenied("Host Count exceeds available instances")
|
|
||||||
|
|
||||||
# A super user can relaunch a job
|
# A super user can relaunch a job
|
||||||
if self.user.is_superuser:
|
if self.user.is_superuser:
|
||||||
@@ -1188,6 +1165,102 @@ class SystemJobAccess(BaseAccess):
|
|||||||
'''
|
'''
|
||||||
model = SystemJob
|
model = SystemJob
|
||||||
|
|
||||||
|
class AdHocCommandAccess(BaseAccess):
|
||||||
|
'''
|
||||||
|
I can only see/run ad hoc commands when:
|
||||||
|
- I am a superuser.
|
||||||
|
- I am an org admin and have permission to read the credential.
|
||||||
|
- I am a normal user with a user/team permission that has at least read
|
||||||
|
permission on the inventory and the run_ad_hoc_commands flag set, and I
|
||||||
|
can read the credential.
|
||||||
|
'''
|
||||||
|
model = AdHocCommand
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
qs = self.model.objects.filter(active=True).distinct()
|
||||||
|
qs = qs.select_related('created_by', 'modified_by', 'inventory',
|
||||||
|
'credential')
|
||||||
|
if self.user.is_superuser:
|
||||||
|
return qs
|
||||||
|
|
||||||
|
credential_ids = set(self.user.get_queryset(Credential).values_list('id', flat=True))
|
||||||
|
team_ids = set(Team.objects.filter(active=True, users__in=[self.user]).values_list('id', flat=True))
|
||||||
|
|
||||||
|
permission_ids = set(Permission.objects.filter(
|
||||||
|
Q(user=self.user) | Q(team__in=team_ids),
|
||||||
|
active=True,
|
||||||
|
permission_type__in=PERMISSION_TYPES_ALLOWING_INVENTORY_READ,
|
||||||
|
run_ad_hoc_commands=True,
|
||||||
|
).values_list('id', flat=True))
|
||||||
|
|
||||||
|
inventory_qs = self.user.get_queryset(Inventory)
|
||||||
|
inventory_qs = inventory_qs.filter(Q(permissions__in=permission_ids) | Q(organization__admins__in=[self.user]))
|
||||||
|
inventory_ids = set(inventory_qs.values_list('id', flat=True))
|
||||||
|
|
||||||
|
qs = qs.filter(
|
||||||
|
credential_id__in=credential_ids,
|
||||||
|
inventory_id__in=inventory_ids,
|
||||||
|
)
|
||||||
|
return qs
|
||||||
|
|
||||||
|
def can_add(self, data):
|
||||||
|
if not data or '_method' in data: # So the browseable API will work?
|
||||||
|
return True
|
||||||
|
|
||||||
|
self.check_license()
|
||||||
|
|
||||||
|
# If a credential is provided, the user should have read access to it.
|
||||||
|
credential_pk = get_pk_from_dict(data, 'credential')
|
||||||
|
if credential_pk:
|
||||||
|
credential = get_object_or_400(Credential, pk=credential_pk)
|
||||||
|
if not self.user.can_access(Credential, 'read', credential):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check that the user has the run ad hoc command permission on the
|
||||||
|
# given inventory.
|
||||||
|
inventory_pk = get_pk_from_dict(data, 'inventory')
|
||||||
|
if inventory_pk:
|
||||||
|
inventory = get_object_or_400(Inventory, pk=inventory_pk)
|
||||||
|
if not self.user.can_access(Inventory, 'run_ad_hoc_commands', inventory):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def can_change(self, obj, data):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def can_delete(self, obj):
|
||||||
|
return False
|
||||||
|
|
||||||
|
class AdHocCommandEventAccess(BaseAccess):
|
||||||
|
'''
|
||||||
|
I can see ad hoc command event records whenever I can read both ad hoc
|
||||||
|
command and host.
|
||||||
|
'''
|
||||||
|
|
||||||
|
model = AdHocCommandEvent
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
qs = self.model.objects.distinct()
|
||||||
|
qs = qs.select_related('created_by', 'modified_by', 'ad_hoc_command', 'host')
|
||||||
|
|
||||||
|
if self.user.is_superuser:
|
||||||
|
return qs
|
||||||
|
ad_hoc_command_qs = self.user.get_queryset(AdHocCommand)
|
||||||
|
host_qs = self.user.get_queryset(Host)
|
||||||
|
qs = qs.filter(Q(host__isnull=True) | Q(host__in=host_qs),
|
||||||
|
ad_hoc_command__in=ad_hoc_command_qs)
|
||||||
|
return qs
|
||||||
|
|
||||||
|
def can_add(self, data):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def can_change(self, obj, data):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def can_delete(self, obj):
|
||||||
|
return False
|
||||||
|
|
||||||
class JobHostSummaryAccess(BaseAccess):
|
class JobHostSummaryAccess(BaseAccess):
|
||||||
'''
|
'''
|
||||||
I can see job/host summary records whenever I can read both job and host.
|
I can see job/host summary records whenever I can read both job and host.
|
||||||
@@ -1293,10 +1366,12 @@ class UnifiedJobAccess(BaseAccess):
|
|||||||
project_update_qs = self.user.get_queryset(ProjectUpdate)
|
project_update_qs = self.user.get_queryset(ProjectUpdate)
|
||||||
inventory_update_qs = self.user.get_queryset(InventoryUpdate).filter(source__in=CLOUD_INVENTORY_SOURCES)
|
inventory_update_qs = self.user.get_queryset(InventoryUpdate).filter(source__in=CLOUD_INVENTORY_SOURCES)
|
||||||
job_qs = self.user.get_queryset(Job)
|
job_qs = self.user.get_queryset(Job)
|
||||||
|
ad_hoc_command_qs = self.user.get_queryset(AdHocCommand)
|
||||||
system_job_qs = self.user.get_queryset(SystemJob)
|
system_job_qs = self.user.get_queryset(SystemJob)
|
||||||
qs = qs.filter(Q(ProjectUpdate___in=project_update_qs) |
|
qs = qs.filter(Q(ProjectUpdate___in=project_update_qs) |
|
||||||
Q(InventoryUpdate___in=inventory_update_qs) |
|
Q(InventoryUpdate___in=inventory_update_qs) |
|
||||||
Q(Job___in=job_qs) |
|
Q(Job___in=job_qs) |
|
||||||
|
Q(AdHocCommand___in=ad_hoc_command_qs) |
|
||||||
Q(SystemJob___in=system_job_qs))
|
Q(SystemJob___in=system_job_qs))
|
||||||
qs = qs.select_related(
|
qs = qs.select_related(
|
||||||
'created_by',
|
'created_by',
|
||||||
@@ -1537,6 +1612,8 @@ register_access(JobHostSummary, JobHostSummaryAccess)
|
|||||||
register_access(JobEvent, JobEventAccess)
|
register_access(JobEvent, JobEventAccess)
|
||||||
register_access(SystemJobTemplate, SystemJobTemplateAccess)
|
register_access(SystemJobTemplate, SystemJobTemplateAccess)
|
||||||
register_access(SystemJob, SystemJobAccess)
|
register_access(SystemJob, SystemJobAccess)
|
||||||
|
register_access(AdHocCommand, AdHocCommandAccess)
|
||||||
|
register_access(AdHocCommandEvent, AdHocCommandEventAccess)
|
||||||
register_access(Schedule, ScheduleAccess)
|
register_access(Schedule, ScheduleAccess)
|
||||||
register_access(UnifiedJobTemplate, UnifiedJobTemplateAccess)
|
register_access(UnifiedJobTemplate, UnifiedJobTemplateAccess)
|
||||||
register_access(UnifiedJob, UnifiedJobAccess)
|
register_access(UnifiedJob, UnifiedJobAccess)
|
||||||
|
|||||||
@@ -117,7 +117,9 @@ class CallbackReceiver(object):
|
|||||||
with Socket('callbacks', 'r') as callbacks:
|
with Socket('callbacks', 'r') as callbacks:
|
||||||
for message in callbacks.listen():
|
for message in callbacks.listen():
|
||||||
total_messages += 1
|
total_messages += 1
|
||||||
if not use_workers:
|
if 'ad_hoc_command_id' in message:
|
||||||
|
self.process_ad_hoc_event(message)
|
||||||
|
elif not use_workers:
|
||||||
self.process_job_event(message)
|
self.process_job_event(message)
|
||||||
else:
|
else:
|
||||||
job_parent_events = last_parent_events.get(message['job_id'], {})
|
job_parent_events = last_parent_events.get(message['job_id'], {})
|
||||||
@@ -216,10 +218,68 @@ class CallbackReceiver(object):
|
|||||||
# Retrun the job event object.
|
# Retrun the job event object.
|
||||||
return job_event
|
return job_event
|
||||||
except DatabaseError as e:
|
except DatabaseError as e:
|
||||||
# Log the error and try again.
|
# Log the error and bail out.
|
||||||
logger.error('Database error saving job event: %s', e)
|
logger.error('Database error saving job event: %s', e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def process_ad_hoc_event(self, data):
|
||||||
|
# Sanity check: Do we need to do anything at all?
|
||||||
|
event = data.get('event', '')
|
||||||
|
if not event or 'ad_hoc_command_id' not in data:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get the correct "verbose" value from the job.
|
||||||
|
# If for any reason there's a problem, just use 0.
|
||||||
|
try:
|
||||||
|
verbose = AdHocCommand.objects.get(id=data['ad_hoc_command_id']).verbosity
|
||||||
|
except Exception, e:
|
||||||
|
verbose = 0
|
||||||
|
|
||||||
|
# Convert the datetime for the job event's creation appropriately,
|
||||||
|
# and include a time zone for it.
|
||||||
|
#
|
||||||
|
# In the event of any issue, throw it out, and Django will just save
|
||||||
|
# the current time.
|
||||||
|
try:
|
||||||
|
if not isinstance(data['created'], datetime.datetime):
|
||||||
|
data['created'] = parse_datetime(data['created'])
|
||||||
|
if not data['created'].tzinfo:
|
||||||
|
data['created'] = data['created'].replace(tzinfo=FixedOffset(0))
|
||||||
|
except (KeyError, ValueError):
|
||||||
|
data.pop('created', None)
|
||||||
|
|
||||||
|
# Print the data to stdout if we're in DEBUG mode.
|
||||||
|
if settings.DEBUG:
|
||||||
|
print data
|
||||||
|
|
||||||
|
# Sanity check: Don't honor keys that we don't recognize.
|
||||||
|
for key in data.keys():
|
||||||
|
if key not in ('ad_hoc_command_id', 'event', 'event_data',
|
||||||
|
'created', 'counter'):
|
||||||
|
data.pop(key)
|
||||||
|
|
||||||
|
# Save any modifications to the ad hoc command event to the database.
|
||||||
|
# If we get a database error of some kind, bail out.
|
||||||
|
try:
|
||||||
|
# If we're not in verbose mode, wipe out any module
|
||||||
|
# arguments. FIXME: Needed for adhoc?
|
||||||
|
res = data['event_data'].get('res', {})
|
||||||
|
if isinstance(res, dict):
|
||||||
|
i = res.get('invocation', {})
|
||||||
|
if verbose == 0 and 'module_args' in i:
|
||||||
|
i['module_args'] = ''
|
||||||
|
|
||||||
|
# Create a new AdHocCommandEvent object.
|
||||||
|
ad_hoc_command_event = AdHocCommandEvent.objects.create(**data)
|
||||||
|
|
||||||
|
# Retrun the ad hoc comamnd event object.
|
||||||
|
return ad_hoc_command_event
|
||||||
|
except DatabaseError as e:
|
||||||
|
# Log the error and bail out.
|
||||||
|
logger.error('Database error saving ad hoc command event: %s', e)
|
||||||
|
return None
|
||||||
|
|
||||||
def callback_worker(self, queue_actual, idx):
|
def callback_worker(self, queue_actual, idx):
|
||||||
messages_processed = 0
|
messages_processed = 0
|
||||||
while True:
|
while True:
|
||||||
|
|||||||
@@ -90,6 +90,12 @@ class JobEventNamespace(TowerBaseNamespace):
|
|||||||
logger.info("Received client connect for job event namespace from %s" % str(self.environ['REMOTE_ADDR']))
|
logger.info("Received client connect for job event namespace from %s" % str(self.environ['REMOTE_ADDR']))
|
||||||
super(JobEventNamespace, self).recv_connect()
|
super(JobEventNamespace, self).recv_connect()
|
||||||
|
|
||||||
|
class AdHocCommandEventNamespace(TowerBaseNamespace):
|
||||||
|
|
||||||
|
def recv_connect(self):
|
||||||
|
logger.info("Received client connect for ad hoc command event namespace from %s" % str(self.environ['REMOTE_ADDR']))
|
||||||
|
super(AdHocCommandEventNamespace, self).recv_connect()
|
||||||
|
|
||||||
class ScheduleNamespace(TowerBaseNamespace):
|
class ScheduleNamespace(TowerBaseNamespace):
|
||||||
|
|
||||||
def get_allowed_methods(self):
|
def get_allowed_methods(self):
|
||||||
@@ -107,6 +113,7 @@ class TowerSocket(object):
|
|||||||
socketio_manage(environ, {'/socket.io/test': TestNamespace,
|
socketio_manage(environ, {'/socket.io/test': TestNamespace,
|
||||||
'/socket.io/jobs': JobNamespace,
|
'/socket.io/jobs': JobNamespace,
|
||||||
'/socket.io/job_events': JobEventNamespace,
|
'/socket.io/job_events': JobEventNamespace,
|
||||||
|
'/socket.io/ad_hoc_command_events': AdHocCommandEventNamespace,
|
||||||
'/socket.io/schedules': ScheduleNamespace})
|
'/socket.io/schedules': ScheduleNamespace})
|
||||||
else:
|
else:
|
||||||
logger.warn("Invalid connect path received: " + path)
|
logger.warn("Invalid connect path received: " + path)
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ class SimpleDAG(object):
|
|||||||
def short_string_obj(obj):
|
def short_string_obj(obj):
|
||||||
if type(obj) == Job:
|
if type(obj) == Job:
|
||||||
type_str = "Job"
|
type_str = "Job"
|
||||||
|
if type(obj) == AdHocCommand:
|
||||||
|
type_str = "AdHocCommand"
|
||||||
elif type(obj) == InventoryUpdate:
|
elif type(obj) == InventoryUpdate:
|
||||||
type_str = "Inventory"
|
type_str = "Inventory"
|
||||||
elif type(obj) == ProjectUpdate:
|
elif type(obj) == ProjectUpdate:
|
||||||
@@ -100,6 +102,8 @@ class SimpleDAG(object):
|
|||||||
def get_node_type(self, obj):
|
def get_node_type(self, obj):
|
||||||
if type(obj) == Job:
|
if type(obj) == Job:
|
||||||
return "job"
|
return "job"
|
||||||
|
elif type(obj) == AdHocCommand:
|
||||||
|
return "ad_hoc_command"
|
||||||
elif type(obj) == InventoryUpdate:
|
elif type(obj) == InventoryUpdate:
|
||||||
return "inventory_update"
|
return "inventory_update"
|
||||||
elif type(obj) == ProjectUpdate:
|
elif type(obj) == ProjectUpdate:
|
||||||
@@ -136,13 +140,14 @@ def get_tasks():
|
|||||||
RELEVANT_JOBS = ('pending', 'waiting', 'running')
|
RELEVANT_JOBS = ('pending', 'waiting', 'running')
|
||||||
# TODO: Replace this when we can grab all objects in a sane way.
|
# TODO: Replace this when we can grab all objects in a sane way.
|
||||||
graph_jobs = [j for j in Job.objects.filter(status__in=RELEVANT_JOBS)]
|
graph_jobs = [j for j in Job.objects.filter(status__in=RELEVANT_JOBS)]
|
||||||
|
graph_ad_hoc_commands = [ahc for ahc in AdHocCommand.objects.filter(status__in=RELEVANT_JOBS)]
|
||||||
graph_inventory_updates = [iu for iu in
|
graph_inventory_updates = [iu for iu in
|
||||||
InventoryUpdate.objects.filter(status__in=RELEVANT_JOBS)]
|
InventoryUpdate.objects.filter(status__in=RELEVANT_JOBS)]
|
||||||
graph_project_updates = [pu for pu in
|
graph_project_updates = [pu for pu in
|
||||||
ProjectUpdate.objects.filter(status__in=RELEVANT_JOBS)]
|
ProjectUpdate.objects.filter(status__in=RELEVANT_JOBS)]
|
||||||
graph_system_jobs = [sj for sj in
|
graph_system_jobs = [sj for sj in
|
||||||
SystemJob.objects.filter(status__in=RELEVANT_JOBS)]
|
SystemJob.objects.filter(status__in=RELEVANT_JOBS)]
|
||||||
all_actions = sorted(graph_jobs + graph_inventory_updates +
|
all_actions = sorted(graph_jobs + graph_ad_hoc_commands + graph_inventory_updates +
|
||||||
graph_project_updates + graph_system_jobs,
|
graph_project_updates + graph_system_jobs,
|
||||||
key=lambda task: task.created)
|
key=lambda task: task.created)
|
||||||
return all_actions
|
return all_actions
|
||||||
|
|||||||
575
awx/main/migrations/0064_v220_changes.py
Normal file
575
awx/main/migrations/0064_v220_changes.py
Normal file
@@ -0,0 +1,575 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from south.utils import datetime_utils as datetime
|
||||||
|
from south.db import db
|
||||||
|
from south.v2 import SchemaMigration
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(SchemaMigration):
|
||||||
|
|
||||||
|
def forwards(self, orm):
|
||||||
|
# Adding model 'AdHocCommand'
|
||||||
|
db.create_table(u'main_adhoccommand', (
|
||||||
|
(u'unifiedjob_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['main.UnifiedJob'], unique=True, primary_key=True)),
|
||||||
|
('job_type', self.gf('django.db.models.fields.CharField')(default='run', max_length=64)),
|
||||||
|
('inventory', self.gf('django.db.models.fields.related.ForeignKey')(related_name='ad_hoc_commands', null=True, on_delete=models.SET_NULL, to=orm['main.Inventory'])),
|
||||||
|
('limit', self.gf('django.db.models.fields.CharField')(default='', max_length=1024, blank=True)),
|
||||||
|
('credential', self.gf('django.db.models.fields.related.ForeignKey')(default=None, related_name='ad_hoc_commands', null=True, on_delete=models.SET_NULL, to=orm['main.Credential'])),
|
||||||
|
('module_name', self.gf('django.db.models.fields.CharField')(default='command', max_length=1024)),
|
||||||
|
('module_args', self.gf('django.db.models.fields.TextField')(default='', blank=True)),
|
||||||
|
('forks', self.gf('django.db.models.fields.PositiveIntegerField')(default=0, blank=True)),
|
||||||
|
('verbosity', self.gf('django.db.models.fields.PositiveIntegerField')(default=0, blank=True)),
|
||||||
|
('privilege_escalation', self.gf('django.db.models.fields.CharField')(default='', max_length=64, blank=True)),
|
||||||
|
))
|
||||||
|
db.send_create_signal('main', ['AdHocCommand'])
|
||||||
|
|
||||||
|
# Adding model 'AdHocCommandEvent'
|
||||||
|
db.create_table(u'main_adhoccommandevent', (
|
||||||
|
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||||
|
('created', self.gf('django.db.models.fields.DateTimeField')(default=None)),
|
||||||
|
('modified', self.gf('django.db.models.fields.DateTimeField')(default=None)),
|
||||||
|
('ad_hoc_command', self.gf('django.db.models.fields.related.ForeignKey')(related_name='ad_hoc_command_events', to=orm['main.AdHocCommand'])),
|
||||||
|
('host', self.gf('django.db.models.fields.related.ForeignKey')(default=None, related_name='ad_hoc_command_events', null=True, on_delete=models.SET_NULL, to=orm['main.Host'])),
|
||||||
|
('host_name', self.gf('django.db.models.fields.CharField')(default='', max_length=1024)),
|
||||||
|
('event', self.gf('django.db.models.fields.CharField')(max_length=100)),
|
||||||
|
('event_data', self.gf('jsonfield.fields.JSONField')(default={}, blank=True)),
|
||||||
|
('failed', self.gf('django.db.models.fields.BooleanField')(default=False)),
|
||||||
|
('changed', self.gf('django.db.models.fields.BooleanField')(default=False)),
|
||||||
|
('counter', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
|
||||||
|
))
|
||||||
|
db.send_create_signal('main', ['AdHocCommandEvent'])
|
||||||
|
|
||||||
|
# Adding unique constraint on 'AdHocCommandEvent', fields ['ad_hoc_command', 'host_name']
|
||||||
|
db.create_unique(u'main_adhoccommandevent', ['ad_hoc_command_id', 'host_name'])
|
||||||
|
|
||||||
|
# Adding field 'Permission.run_ad_hoc_commands'
|
||||||
|
db.add_column(u'main_permission', 'run_ad_hoc_commands',
|
||||||
|
self.gf('django.db.models.fields.BooleanField')(default=False),
|
||||||
|
keep_default=False)
|
||||||
|
|
||||||
|
# Adding M2M table for field ad_hoc_command on 'ActivityStream'
|
||||||
|
m2m_table_name = db.shorten_name(u'main_activitystream_ad_hoc_command')
|
||||||
|
db.create_table(m2m_table_name, (
|
||||||
|
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
|
||||||
|
('activitystream', models.ForeignKey(orm['main.activitystream'], null=False)),
|
||||||
|
('adhoccommand', models.ForeignKey(orm['main.adhoccommand'], null=False))
|
||||||
|
))
|
||||||
|
db.create_unique(m2m_table_name, ['activitystream_id', 'adhoccommand_id'])
|
||||||
|
|
||||||
|
|
||||||
|
def backwards(self, orm):
|
||||||
|
# Removing unique constraint on 'AdHocCommandEvent', fields ['ad_hoc_command', 'host_name']
|
||||||
|
db.delete_unique(u'main_adhoccommandevent', ['ad_hoc_command_id', 'host_name'])
|
||||||
|
|
||||||
|
# Deleting model 'AdHocCommand'
|
||||||
|
db.delete_table(u'main_adhoccommand')
|
||||||
|
|
||||||
|
# Deleting model 'AdHocCommandEvent'
|
||||||
|
db.delete_table(u'main_adhoccommandevent')
|
||||||
|
|
||||||
|
# Deleting field 'Permission.run_ad_hoc_commands'
|
||||||
|
db.delete_column(u'main_permission', 'run_ad_hoc_commands')
|
||||||
|
|
||||||
|
# Removing M2M table for field ad_hoc_command on 'ActivityStream'
|
||||||
|
db.delete_table(db.shorten_name(u'main_activitystream_ad_hoc_command'))
|
||||||
|
|
||||||
|
|
||||||
|
models = {
|
||||||
|
u'auth.group': {
|
||||||
|
'Meta': {'object_name': 'Group'},
|
||||||
|
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||||
|
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||||
|
},
|
||||||
|
u'auth.permission': {
|
||||||
|
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
|
||||||
|
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||||
|
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
|
||||||
|
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||||
|
},
|
||||||
|
u'auth.user': {
|
||||||
|
'Meta': {'object_name': 'User'},
|
||||||
|
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||||
|
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||||
|
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||||
|
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
|
||||||
|
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||||
|
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||||
|
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||||
|
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||||
|
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
|
||||||
|
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||||
|
},
|
||||||
|
u'contenttypes.contenttype': {
|
||||||
|
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||||
|
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||||
|
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||||
|
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||||
|
},
|
||||||
|
'main.activitystream': {
|
||||||
|
'Meta': {'object_name': 'ActivityStream'},
|
||||||
|
'actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'activity_stream'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
|
||||||
|
'ad_hoc_command': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.AdHocCommand']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||||
|
'changes': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||||
|
'credential': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Credential']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||||
|
'custom_inventory_script': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.CustomInventoryScript']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||||
|
'group': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||||
|
'host': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Host']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||||
|
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'inventory': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Inventory']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||||
|
'inventory_source': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.InventorySource']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||||
|
'inventory_update': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.InventoryUpdate']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||||
|
'job': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Job']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||||
|
'job_template': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.JobTemplate']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||||
|
'object1': ('django.db.models.fields.TextField', [], {}),
|
||||||
|
'object2': ('django.db.models.fields.TextField', [], {}),
|
||||||
|
'object_relationship_type': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||||
|
'operation': ('django.db.models.fields.CharField', [], {'max_length': '13'}),
|
||||||
|
'organization': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Organization']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||||
|
'permission': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||||
|
'project': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Project']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||||
|
'project_update': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.ProjectUpdate']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||||
|
'schedule': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Schedule']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||||
|
'team': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Team']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||||
|
'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||||
|
'unified_job': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'activity_stream_as_unified_job+'", 'blank': 'True', 'to': "orm['main.UnifiedJob']"}),
|
||||||
|
'unified_job_template': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'activity_stream_as_unified_job_template+'", 'blank': 'True', 'to': "orm['main.UnifiedJobTemplate']"}),
|
||||||
|
'user': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.User']", 'symmetrical': 'False', 'blank': 'True'})
|
||||||
|
},
|
||||||
|
'main.adhoccommand': {
|
||||||
|
'Meta': {'object_name': 'AdHocCommand', '_ormbases': ['main.UnifiedJob']},
|
||||||
|
'credential': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'ad_hoc_commands'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Credential']"}),
|
||||||
|
'forks': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}),
|
||||||
|
'hosts': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'ad_hoc_commands'", 'symmetrical': 'False', 'through': "orm['main.AdHocCommandEvent']", 'to': "orm['main.Host']"}),
|
||||||
|
'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ad_hoc_commands'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}),
|
||||||
|
'job_type': ('django.db.models.fields.CharField', [], {'default': "'run'", 'max_length': '64'}),
|
||||||
|
'limit': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
|
||||||
|
'module_args': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
|
||||||
|
'module_name': ('django.db.models.fields.CharField', [], {'default': "'command'", 'max_length': '1024'}),
|
||||||
|
'privilege_escalation': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64', 'blank': 'True'}),
|
||||||
|
u'unifiedjob_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJob']", 'unique': 'True', 'primary_key': 'True'}),
|
||||||
|
'verbosity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'})
|
||||||
|
},
|
||||||
|
'main.adhoccommandevent': {
|
||||||
|
'Meta': {'ordering': "('-pk',)", 'unique_together': "[('ad_hoc_command', 'host_name')]", 'object_name': 'AdHocCommandEvent'},
|
||||||
|
'ad_hoc_command': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ad_hoc_command_events'", 'to': "orm['main.AdHocCommand']"}),
|
||||||
|
'changed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'counter': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
|
||||||
|
'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
|
||||||
|
'event': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||||
|
'event_data': ('jsonfield.fields.JSONField', [], {'default': '{}', 'blank': 'True'}),
|
||||||
|
'failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'host': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'ad_hoc_command_events'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Host']"}),
|
||||||
|
'host_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}),
|
||||||
|
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'})
|
||||||
|
},
|
||||||
|
'main.authtoken': {
|
||||||
|
'Meta': {'object_name': 'AuthToken'},
|
||||||
|
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||||
|
'expires': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||||
|
'key': ('django.db.models.fields.CharField', [], {'max_length': '40', 'primary_key': 'True'}),
|
||||||
|
'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
|
||||||
|
'request_hash': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '40', 'blank': 'True'}),
|
||||||
|
'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'auth_tokens'", 'to': u"orm['auth.User']"})
|
||||||
|
},
|
||||||
|
'main.credential': {
|
||||||
|
'Meta': {'ordering': "('kind', 'name')", 'unique_together': "[('user', 'team', 'kind', 'name')]", 'object_name': 'Credential'},
|
||||||
|
'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||||
|
'cloud': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
|
||||||
|
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'credential\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
|
||||||
|
'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
|
||||||
|
'host': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
|
||||||
|
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'kind': ('django.db.models.fields.CharField', [], {'default': "'ssh'", 'max_length': '32'}),
|
||||||
|
'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
|
||||||
|
'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'credential\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
|
||||||
|
'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}),
|
||||||
|
'password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
|
||||||
|
'project': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100', 'blank': 'True'}),
|
||||||
|
'ssh_key_data': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
|
||||||
|
'ssh_key_unlock': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
|
||||||
|
'su_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
|
||||||
|
'su_username': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
|
||||||
|
'sudo_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
|
||||||
|
'sudo_username': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
|
||||||
|
'team': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'credentials'", 'null': 'True', 'blank': 'True', 'to': "orm['main.Team']"}),
|
||||||
|
'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'credentials'", 'null': 'True', 'blank': 'True', 'to': u"orm['auth.User']"}),
|
||||||
|
'username': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
|
||||||
|
'vault_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'})
|
||||||
|
},
|
||||||
|
'main.custominventoryscript': {
|
||||||
|
'Meta': {'ordering': "('name',)", 'unique_together': "[('name', 'organization')]", 'object_name': 'CustomInventoryScript'},
|
||||||
|
'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||||
|
'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
|
||||||
|
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'custominventoryscript\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
|
||||||
|
'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
|
||||||
|
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
|
||||||
|
'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'custominventoryscript\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
|
||||||
|
'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}),
|
||||||
|
'organization': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'custom_inventory_scripts'", 'to': "orm['main.Organization']"}),
|
||||||
|
'script': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'})
|
||||||
|
},
|
||||||
|
'main.group': {
|
||||||
|
'Meta': {'ordering': "('name',)", 'unique_together': "(('name', 'inventory'),)", 'object_name': 'Group'},
|
||||||
|
'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||||
|
'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
|
||||||
|
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'group\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
|
||||||
|
'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
|
||||||
|
'groups_with_active_failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
|
||||||
|
'has_active_failures': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'has_inventory_sources': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'hosts': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'groups'", 'blank': 'True', 'to': "orm['main.Host']"}),
|
||||||
|
'hosts_with_active_failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
|
||||||
|
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'groups'", 'to': "orm['main.Inventory']"}),
|
||||||
|
'inventory_sources': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'groups'", 'symmetrical': 'False', 'to': "orm['main.InventorySource']"}),
|
||||||
|
'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
|
||||||
|
'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'group\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
|
||||||
|
'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}),
|
||||||
|
'parents': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'children'", 'blank': 'True', 'to': "orm['main.Group']"}),
|
||||||
|
'total_groups': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
|
||||||
|
'total_hosts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
|
||||||
|
'variables': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'})
|
||||||
|
},
|
||||||
|
'main.host': {
|
||||||
|
'Meta': {'ordering': "('inventory', 'name')", 'unique_together': "(('name', 'inventory'),)", 'object_name': 'Host'},
|
||||||
|
'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||||
|
'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
|
||||||
|
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'host\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
|
||||||
|
'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
|
||||||
|
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||||
|
'has_active_failures': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'has_inventory_sources': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'instance_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100', 'blank': 'True'}),
|
||||||
|
'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'hosts'", 'to': "orm['main.Inventory']"}),
|
||||||
|
'inventory_sources': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'hosts'", 'symmetrical': 'False', 'to': "orm['main.InventorySource']"}),
|
||||||
|
'last_job': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'hosts_as_last_job+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Job']"}),
|
||||||
|
'last_job_host_summary': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'hosts_as_last_job_summary+'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.JobHostSummary']", 'blank': 'True', 'null': 'True'}),
|
||||||
|
'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
|
||||||
|
'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'host\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
|
||||||
|
'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}),
|
||||||
|
'variables': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'})
|
||||||
|
},
|
||||||
|
'main.instance': {
|
||||||
|
'Meta': {'object_name': 'Instance'},
|
||||||
|
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||||
|
'hostname': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '250'}),
|
||||||
|
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
|
||||||
|
'primary': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'})
|
||||||
|
},
|
||||||
|
'main.inventory': {
|
||||||
|
'Meta': {'ordering': "('name',)", 'unique_together': "[('name', 'organization')]", 'object_name': 'Inventory'},
|
||||||
|
'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||||
|
'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
|
||||||
|
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'inventory\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
|
||||||
|
'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
|
||||||
|
'groups_with_active_failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
|
||||||
|
'has_active_failures': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'has_inventory_sources': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'hosts_with_active_failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
|
||||||
|
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'inventory_sources_with_failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
|
||||||
|
'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
|
||||||
|
'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'inventory\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
|
||||||
|
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}),
|
||||||
|
'organization': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'inventories'", 'to': "orm['main.Organization']"}),
|
||||||
|
'total_groups': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
|
||||||
|
'total_hosts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
|
||||||
|
'total_inventory_sources': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
|
||||||
|
'variables': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'})
|
||||||
|
},
|
||||||
|
'main.inventorysource': {
|
||||||
|
'Meta': {'object_name': 'InventorySource', '_ormbases': ['main.UnifiedJobTemplate']},
|
||||||
|
'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'inventorysources'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}),
|
||||||
|
'group': ('awx.main.fields.AutoOneToOneField', [], {'default': 'None', 'related_name': "'inventory_source'", 'unique': 'True', 'null': 'True', 'to': "orm['main.Group']"}),
|
||||||
|
'group_by': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
|
||||||
|
'instance_filters': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
|
||||||
|
'inventory': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'inventory_sources'", 'null': 'True', 'to': "orm['main.Inventory']"}),
|
||||||
|
'overwrite': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'overwrite_vars': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'source': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}),
|
||||||
|
'source_path': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
|
||||||
|
'source_regions': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
|
||||||
|
'source_script': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['main.CustomInventoryScript']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
|
||||||
|
'source_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
|
||||||
|
u'unifiedjobtemplate_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJobTemplate']", 'unique': 'True', 'primary_key': 'True'}),
|
||||||
|
'update_cache_timeout': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
|
||||||
|
'update_on_launch': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
|
||||||
|
},
|
||||||
|
'main.inventoryupdate': {
|
||||||
|
'Meta': {'object_name': 'InventoryUpdate', '_ormbases': ['main.UnifiedJob']},
|
||||||
|
'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'inventoryupdates'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}),
|
||||||
|
'group_by': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
|
||||||
|
'instance_filters': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
|
||||||
|
'inventory_source': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'inventory_updates'", 'to': "orm['main.InventorySource']"}),
|
||||||
|
'license_error': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'overwrite': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'overwrite_vars': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'source': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}),
|
||||||
|
'source_path': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
|
||||||
|
'source_regions': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
|
||||||
|
'source_script': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['main.CustomInventoryScript']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
|
||||||
|
'source_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
|
||||||
|
u'unifiedjob_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJob']", 'unique': 'True', 'primary_key': 'True'})
|
||||||
|
},
|
||||||
|
'main.job': {
|
||||||
|
'Meta': {'ordering': "('id',)", 'object_name': 'Job', '_ormbases': ['main.UnifiedJob']},
|
||||||
|
'cloud_credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs_as_cloud_credential+'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}),
|
||||||
|
'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}),
|
||||||
|
'extra_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
|
||||||
|
'force_handlers': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'forks': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}),
|
||||||
|
'hosts': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'jobs'", 'symmetrical': 'False', 'through': "orm['main.JobHostSummary']", 'to': "orm['main.Host']"}),
|
||||||
|
'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}),
|
||||||
|
'job_tags': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
|
||||||
|
'job_template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.JobTemplate']", 'blank': 'True', 'null': 'True'}),
|
||||||
|
'job_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
|
||||||
|
'limit': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
|
||||||
|
'playbook': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
|
||||||
|
'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Project']", 'blank': 'True', 'null': 'True'}),
|
||||||
|
'skip_tags': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
|
||||||
|
'start_at_task': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
|
||||||
|
u'unifiedjob_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJob']", 'unique': 'True', 'primary_key': 'True'}),
|
||||||
|
'verbosity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'})
|
||||||
|
},
|
||||||
|
'main.jobevent': {
|
||||||
|
'Meta': {'ordering': "('pk',)", 'object_name': 'JobEvent'},
|
||||||
|
'changed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'counter': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
|
||||||
|
'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
|
||||||
|
'event': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||||
|
'event_data': ('jsonfield.fields.JSONField', [], {'default': '{}', 'blank': 'True'}),
|
||||||
|
'failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'host': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'job_events_as_primary_host'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Host']"}),
|
||||||
|
'host_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}),
|
||||||
|
'hosts': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'job_events'", 'symmetrical': 'False', 'to': "orm['main.Host']"}),
|
||||||
|
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_events'", 'to': "orm['main.Job']"}),
|
||||||
|
'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
|
||||||
|
'parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'children'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.JobEvent']"}),
|
||||||
|
'play': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}),
|
||||||
|
'role': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}),
|
||||||
|
'task': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'})
|
||||||
|
},
|
||||||
|
'main.jobhostsummary': {
|
||||||
|
'Meta': {'ordering': "('-pk',)", 'unique_together': "[('job', 'host_name')]", 'object_name': 'JobHostSummary'},
|
||||||
|
'changed': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
|
||||||
|
'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
|
||||||
|
'dark': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
|
||||||
|
'failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
|
||||||
|
'host': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'job_host_summaries'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Host']"}),
|
||||||
|
'host_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}),
|
||||||
|
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_host_summaries'", 'to': "orm['main.Job']"}),
|
||||||
|
'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
|
||||||
|
'ok': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
|
||||||
|
'processed': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
|
||||||
|
'skipped': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
|
||||||
|
},
|
||||||
|
'main.joborigin': {
|
||||||
|
'Meta': {'object_name': 'JobOrigin'},
|
||||||
|
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||||
|
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'instance': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Instance']"}),
|
||||||
|
'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
|
||||||
|
'unified_job': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'job_origin'", 'unique': 'True', 'to': "orm['main.UnifiedJob']"})
|
||||||
|
},
|
||||||
|
'main.jobtemplate': {
|
||||||
|
'Meta': {'ordering': "('name',)", 'object_name': 'JobTemplate', '_ormbases': ['main.UnifiedJobTemplate']},
|
||||||
|
'ask_variables_on_launch': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'cloud_credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobtemplates_as_cloud_credential+'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}),
|
||||||
|
'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobtemplates'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}),
|
||||||
|
'extra_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
|
||||||
|
'force_handlers': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'forks': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}),
|
||||||
|
'host_config_key': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
|
||||||
|
'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobtemplates'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}),
|
||||||
|
'job_tags': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
|
||||||
|
'job_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
|
||||||
|
'limit': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
|
||||||
|
'playbook': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
|
||||||
|
'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobtemplates'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Project']", 'blank': 'True', 'null': 'True'}),
|
||||||
|
'skip_tags': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
|
||||||
|
'start_at_task': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
|
||||||
|
'survey_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'survey_spec': ('jsonfield.fields.JSONField', [], {'default': '{}', 'blank': 'True'}),
|
||||||
|
u'unifiedjobtemplate_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJobTemplate']", 'unique': 'True', 'primary_key': 'True'}),
|
||||||
|
'verbosity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'})
|
||||||
|
},
|
||||||
|
'main.organization': {
|
||||||
|
'Meta': {'ordering': "('name',)", 'object_name': 'Organization'},
|
||||||
|
'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||||
|
'admins': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'admin_of_organizations'", 'blank': 'True', 'to': u"orm['auth.User']"}),
|
||||||
|
'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
|
||||||
|
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'organization\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
|
||||||
|
'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
|
||||||
|
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
|
||||||
|
'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'organization\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
|
||||||
|
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}),
|
||||||
|
'projects': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'organizations'", 'blank': 'True', 'to': "orm['main.Project']"}),
|
||||||
|
'users': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'organizations'", 'blank': 'True', 'to': u"orm['auth.User']"})
|
||||||
|
},
|
||||||
|
'main.permission': {
|
||||||
|
'Meta': {'object_name': 'Permission'},
|
||||||
|
'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||||
|
'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
|
||||||
|
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'permission\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
|
||||||
|
'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
|
||||||
|
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}),
|
||||||
|
'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
|
||||||
|
'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'permission\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
|
||||||
|
'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}),
|
||||||
|
'permission_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
|
||||||
|
'project': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Project']"}),
|
||||||
|
'run_ad_hoc_commands': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'team': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Team']"}),
|
||||||
|
'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"})
|
||||||
|
},
|
||||||
|
'main.profile': {
|
||||||
|
'Meta': {'object_name': 'Profile'},
|
||||||
|
'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
|
||||||
|
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'ldap_dn': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}),
|
||||||
|
'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
|
||||||
|
'user': ('awx.main.fields.AutoOneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': u"orm['auth.User']"})
|
||||||
|
},
|
||||||
|
'main.project': {
|
||||||
|
'Meta': {'ordering': "('id',)", 'object_name': 'Project', '_ormbases': ['main.UnifiedJobTemplate']},
|
||||||
|
'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}),
|
||||||
|
'local_path': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
|
||||||
|
'scm_branch': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256', 'blank': 'True'}),
|
||||||
|
'scm_clean': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'scm_delete_on_next_update': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'scm_delete_on_update': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'scm_type': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '8', 'blank': 'True'}),
|
||||||
|
'scm_update_cache_timeout': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
|
||||||
|
'scm_update_on_launch': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'scm_url': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
|
||||||
|
u'unifiedjobtemplate_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJobTemplate']", 'unique': 'True', 'primary_key': 'True'})
|
||||||
|
},
|
||||||
|
'main.projectupdate': {
|
||||||
|
'Meta': {'object_name': 'ProjectUpdate', '_ormbases': ['main.UnifiedJob']},
|
||||||
|
'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projectupdates'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}),
|
||||||
|
'local_path': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
|
||||||
|
'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'project_updates'", 'to': "orm['main.Project']"}),
|
||||||
|
'scm_branch': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256', 'blank': 'True'}),
|
||||||
|
'scm_clean': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'scm_delete_on_update': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'scm_type': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '8', 'blank': 'True'}),
|
||||||
|
'scm_url': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
|
||||||
|
u'unifiedjob_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJob']", 'unique': 'True', 'primary_key': 'True'})
|
||||||
|
},
|
||||||
|
'main.schedule': {
|
||||||
|
'Meta': {'ordering': "['-next_run']", 'object_name': 'Schedule'},
|
||||||
|
'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||||
|
'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
|
||||||
|
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'schedule\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
|
||||||
|
'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
|
||||||
|
'dtend': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}),
|
||||||
|
'dtstart': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}),
|
||||||
|
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||||
|
'extra_data': ('jsonfield.fields.JSONField', [], {'default': '{}', 'blank': 'True'}),
|
||||||
|
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
|
||||||
|
'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'schedule\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
|
||||||
|
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}),
|
||||||
|
'next_run': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}),
|
||||||
|
'rrule': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||||
|
'unified_job_template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'schedules'", 'to': "orm['main.UnifiedJobTemplate']"})
|
||||||
|
},
|
||||||
|
'main.systemjob': {
|
||||||
|
'Meta': {'ordering': "('id',)", 'object_name': 'SystemJob', '_ormbases': ['main.UnifiedJob']},
|
||||||
|
'extra_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
|
||||||
|
'job_type': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}),
|
||||||
|
'system_job_template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.SystemJobTemplate']", 'blank': 'True', 'null': 'True'}),
|
||||||
|
u'unifiedjob_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJob']", 'unique': 'True', 'primary_key': 'True'})
|
||||||
|
},
|
||||||
|
'main.systemjobtemplate': {
|
||||||
|
'Meta': {'object_name': 'SystemJobTemplate', '_ormbases': ['main.UnifiedJobTemplate']},
|
||||||
|
'job_type': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}),
|
||||||
|
u'unifiedjobtemplate_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJobTemplate']", 'unique': 'True', 'primary_key': 'True'})
|
||||||
|
},
|
||||||
|
'main.team': {
|
||||||
|
'Meta': {'ordering': "('organization__name', 'name')", 'unique_together': "[('organization', 'name')]", 'object_name': 'Team'},
|
||||||
|
'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||||
|
'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
|
||||||
|
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'team\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
|
||||||
|
'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
|
||||||
|
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
|
||||||
|
'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'team\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
|
||||||
|
'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}),
|
||||||
|
'organization': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'teams'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Organization']"}),
|
||||||
|
'projects': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'teams'", 'blank': 'True', 'to': "orm['main.Project']"}),
|
||||||
|
'users': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'teams'", 'blank': 'True', 'to': u"orm['auth.User']"})
|
||||||
|
},
|
||||||
|
'main.unifiedjob': {
|
||||||
|
'Meta': {'object_name': 'UnifiedJob'},
|
||||||
|
'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||||
|
'cancel_flag': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'celery_task_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100', 'blank': 'True'}),
|
||||||
|
'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
|
||||||
|
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'unifiedjob\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
|
||||||
|
'dependent_jobs': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'dependent_jobs_rel_+'", 'to': "orm['main.UnifiedJob']"}),
|
||||||
|
'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
|
||||||
|
'elapsed': ('django.db.models.fields.DecimalField', [], {'max_digits': '12', 'decimal_places': '3'}),
|
||||||
|
'failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'finished': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}),
|
||||||
|
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'job_args': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
|
||||||
|
'job_cwd': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
|
||||||
|
'job_env': ('jsonfield.fields.JSONField', [], {'default': '{}', 'blank': 'True'}),
|
||||||
|
'job_explanation': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
|
||||||
|
'launch_type': ('django.db.models.fields.CharField', [], {'default': "'manual'", 'max_length': '20'}),
|
||||||
|
'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
|
||||||
|
'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'unifiedjob\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
|
||||||
|
'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}),
|
||||||
|
'old_pk': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'null': 'True'}),
|
||||||
|
'polymorphic_ctype': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'polymorphic_main.unifiedjob_set'", 'null': 'True', 'to': u"orm['contenttypes.ContentType']"}),
|
||||||
|
'result_stdout_file': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
|
||||||
|
'result_stdout_text': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
|
||||||
|
'result_traceback': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
|
||||||
|
'schedule': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['main.Schedule']", 'null': 'True', 'on_delete': 'models.SET_NULL'}),
|
||||||
|
'start_args': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
|
||||||
|
'started': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}),
|
||||||
|
'status': ('django.db.models.fields.CharField', [], {'default': "'new'", 'max_length': '20'}),
|
||||||
|
'unified_job_template': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'unifiedjob_unified_jobs'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.UnifiedJobTemplate']"})
|
||||||
|
},
|
||||||
|
'main.unifiedjobtemplate': {
|
||||||
|
'Meta': {'unique_together': "[('polymorphic_ctype', 'name')]", 'object_name': 'UnifiedJobTemplate'},
|
||||||
|
'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||||
|
'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
|
||||||
|
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'unifiedjobtemplate\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
|
||||||
|
'current_job': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'unifiedjobtemplate_as_current_job+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.UnifiedJob']"}),
|
||||||
|
'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
|
||||||
|
'has_schedules': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'last_job': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'unifiedjobtemplate_as_last_job+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.UnifiedJob']"}),
|
||||||
|
'last_job_failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'last_job_run': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}),
|
||||||
|
'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
|
||||||
|
'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'unifiedjobtemplate\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
|
||||||
|
'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}),
|
||||||
|
'next_job_run': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}),
|
||||||
|
'next_schedule': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'unifiedjobtemplate_as_next_schedule+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Schedule']"}),
|
||||||
|
'old_pk': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'null': 'True'}),
|
||||||
|
'polymorphic_ctype': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'polymorphic_main.unifiedjobtemplate_set'", 'null': 'True', 'to': u"orm['contenttypes.ContentType']"}),
|
||||||
|
'status': ('django.db.models.fields.CharField', [], {'default': "'ok'", 'max_length': '32'})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
complete_apps = ['main']
|
||||||
@@ -12,6 +12,7 @@ from awx.main.models.credential import * # noqa
|
|||||||
from awx.main.models.projects import * # noqa
|
from awx.main.models.projects import * # noqa
|
||||||
from awx.main.models.inventory import * # noqa
|
from awx.main.models.inventory import * # noqa
|
||||||
from awx.main.models.jobs import * # noqa
|
from awx.main.models.jobs import * # noqa
|
||||||
|
from awx.main.models.ad_hoc_commands import * # noqa
|
||||||
from awx.main.models.schedules import * # noqa
|
from awx.main.models.schedules import * # noqa
|
||||||
from awx.main.models.activity_stream import * # noqa
|
from awx.main.models.activity_stream import * # noqa
|
||||||
from awx.main.models.ha import * # noqa
|
from awx.main.models.ha import * # noqa
|
||||||
@@ -51,6 +52,7 @@ activity_stream_registrar.connect(Project)
|
|||||||
activity_stream_registrar.connect(Permission)
|
activity_stream_registrar.connect(Permission)
|
||||||
activity_stream_registrar.connect(JobTemplate)
|
activity_stream_registrar.connect(JobTemplate)
|
||||||
activity_stream_registrar.connect(Job)
|
activity_stream_registrar.connect(Job)
|
||||||
|
activity_stream_registrar.connect(AdHocCommand)
|
||||||
# activity_stream_registrar.connect(JobHostSummary)
|
# activity_stream_registrar.connect(JobHostSummary)
|
||||||
# activity_stream_registrar.connect(JobEvent)
|
# activity_stream_registrar.connect(JobEvent)
|
||||||
#activity_stream_registrar.connect(Profile)
|
#activity_stream_registrar.connect(Profile)
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ class ActivityStream(models.Model):
|
|||||||
job = models.ManyToManyField("Job", blank=True)
|
job = models.ManyToManyField("Job", blank=True)
|
||||||
unified_job_template = models.ManyToManyField("UnifiedJobTemplate", blank=True, related_name='activity_stream_as_unified_job_template+')
|
unified_job_template = models.ManyToManyField("UnifiedJobTemplate", blank=True, related_name='activity_stream_as_unified_job_template+')
|
||||||
unified_job = models.ManyToManyField("UnifiedJob", blank=True, related_name='activity_stream_as_unified_job+')
|
unified_job = models.ManyToManyField("UnifiedJob", blank=True, related_name='activity_stream_as_unified_job+')
|
||||||
|
ad_hoc_command = models.ManyToManyField("AdHocCommand", blank=True)
|
||||||
schedule = models.ManyToManyField("Schedule", blank=True)
|
schedule = models.ManyToManyField("Schedule", blank=True)
|
||||||
custom_inventory_script = models.ManyToManyField("CustomInventoryScript", blank=True)
|
custom_inventory_script = models.ManyToManyField("CustomInventoryScript", blank=True)
|
||||||
|
|
||||||
|
|||||||
311
awx/main/models/ad_hoc_commands.py
Normal file
311
awx/main/models/ad_hoc_commands.py
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
# Copyright (c) 2015 Ansible, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
|
||||||
|
# Python
|
||||||
|
import hmac
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Django
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import models
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
|
# Django-JSONField
|
||||||
|
from jsonfield import JSONField
|
||||||
|
|
||||||
|
# AWX
|
||||||
|
from awx.main.constants import CLOUD_PROVIDERS
|
||||||
|
from awx.main.models.base import * # noqa
|
||||||
|
from awx.main.models.unified_jobs import * # noqa
|
||||||
|
from awx.main.utils import decrypt_field, ignore_inventory_computed_fields
|
||||||
|
from awx.main.utils import emit_websocket_notification
|
||||||
|
|
||||||
|
logger = logging.getLogger('awx.main.models.ad_hoc_commands')
|
||||||
|
|
||||||
|
__all__ = ['AdHocCommand', 'AdHocCommandEvent']
|
||||||
|
|
||||||
|
|
||||||
|
class AdHocCommand(UnifiedJob):
|
||||||
|
|
||||||
|
MODULE_NAME_CHOICES = [(x,x) for x in settings.AD_HOC_COMMANDS]
|
||||||
|
|
||||||
|
PRIVILEGE_ESCALATION_CHOICES = [
|
||||||
|
('', _('None')),
|
||||||
|
('su', _('su')),
|
||||||
|
('sudo', _('sudo')),
|
||||||
|
]
|
||||||
|
|
||||||
|
class Meta(object):
|
||||||
|
app_label = 'main'
|
||||||
|
|
||||||
|
job_type = models.CharField(
|
||||||
|
max_length=64,
|
||||||
|
choices=JOB_TYPE_CHOICES,
|
||||||
|
default='run',
|
||||||
|
)
|
||||||
|
inventory = models.ForeignKey(
|
||||||
|
'Inventory',
|
||||||
|
related_name='ad_hoc_commands',
|
||||||
|
null=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
)
|
||||||
|
limit = models.CharField(
|
||||||
|
max_length=1024,
|
||||||
|
blank=True,
|
||||||
|
default='',
|
||||||
|
)
|
||||||
|
credential = models.ForeignKey(
|
||||||
|
'Credential',
|
||||||
|
related_name='ad_hoc_commands',
|
||||||
|
null=True,
|
||||||
|
default=None,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
)
|
||||||
|
module_name = models.CharField(
|
||||||
|
max_length=1024,
|
||||||
|
default='command',
|
||||||
|
choices=MODULE_NAME_CHOICES,
|
||||||
|
blank=True, # If blank, defaults to 'command'.
|
||||||
|
)
|
||||||
|
module_args = models.TextField(
|
||||||
|
blank=True,
|
||||||
|
default='',
|
||||||
|
)
|
||||||
|
forks = models.PositiveIntegerField(
|
||||||
|
blank=True,
|
||||||
|
default=0,
|
||||||
|
)
|
||||||
|
verbosity = models.PositiveIntegerField(
|
||||||
|
blank=True,
|
||||||
|
default=0,
|
||||||
|
)
|
||||||
|
privilege_escalation = models.CharField(
|
||||||
|
max_length=64,
|
||||||
|
choices=PRIVILEGE_ESCALATION_CHOICES,
|
||||||
|
default='',
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
hosts = models.ManyToManyField(
|
||||||
|
'Host',
|
||||||
|
related_name='ad_hoc_commands',
|
||||||
|
editable=False,
|
||||||
|
through='AdHocCommandEvent',
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean_credential(self):
|
||||||
|
cred = self.credential
|
||||||
|
if cred and cred.kind != 'ssh':
|
||||||
|
raise ValidationError(
|
||||||
|
'You must provide a machine / SSH credential.',
|
||||||
|
)
|
||||||
|
return cred
|
||||||
|
|
||||||
|
def clean_limit(self):
|
||||||
|
# FIXME: Future feature - check if no hosts would match and reject the
|
||||||
|
# command, instead of having to run it to find out.
|
||||||
|
return self.limit
|
||||||
|
|
||||||
|
def clean_module_name(self):
|
||||||
|
module_name = self.module_name.strip() or 'command'
|
||||||
|
if module_name not in settings.AD_HOC_COMMANDS:
|
||||||
|
raise ValidationError('Unsupported module for ad hoc commands.')
|
||||||
|
return module_name
|
||||||
|
|
||||||
|
def clean_module_args(self):
|
||||||
|
module_args = self.module_args
|
||||||
|
if self.module_name in ('command', 'shell') and not module_args:
|
||||||
|
raise ValidationError('No argument passed to %s module.' % self.module_name)
|
||||||
|
return module_args
|
||||||
|
|
||||||
|
@property
|
||||||
|
def passwords_needed_to_start(self):
|
||||||
|
'''Return list of password field names needed to start the job.'''
|
||||||
|
needed = []
|
||||||
|
if self.credential:
|
||||||
|
for pw in self.credential.passwords_needed:
|
||||||
|
if pw == 'password':
|
||||||
|
needed.append('ssh_password')
|
||||||
|
else:
|
||||||
|
needed.append(pw)
|
||||||
|
return needed
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_parent_field_name(cls):
|
||||||
|
return ''
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_task_class(cls):
|
||||||
|
from awx.main.tasks import RunAdHocCommand
|
||||||
|
return RunAdHocCommand
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('api:ad_hoc_command_detail', args=(self.pk,))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def task_auth_token(self):
|
||||||
|
'''Return temporary auth token used for task requests via API.'''
|
||||||
|
if self.status == 'running':
|
||||||
|
h = hmac.new(settings.SECRET_KEY, self.created.isoformat())
|
||||||
|
return '%d-%s' % (self.pk, h.hexdigest())
|
||||||
|
|
||||||
|
def get_passwords_needed_to_start(self):
|
||||||
|
return self.passwords_needed_to_start
|
||||||
|
|
||||||
|
def is_blocked_by(self, obj):
|
||||||
|
from awx.main.models import InventoryUpdate
|
||||||
|
if type(obj) == InventoryUpdate:
|
||||||
|
if self.inventory == obj.inventory_source.inventory:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def task_impact(self):
|
||||||
|
# NOTE: We sorta have to assume the host count matches and that forks default to 5
|
||||||
|
from awx.main.models.inventory import Host
|
||||||
|
count_hosts = Host.objects.filter(active=True, enabled=True, inventory__ad_hoc_commands__pk=self.pk).count()
|
||||||
|
return min(count_hosts, 5 if self.forks == 0 else self.forks) * 10
|
||||||
|
|
||||||
|
def generate_dependencies(self, active_tasks):
|
||||||
|
from awx.main.models import InventoryUpdate
|
||||||
|
if not self.inventory:
|
||||||
|
return []
|
||||||
|
inventory_sources = self.inventory.inventory_sources.filter(active=True, update_on_launch=True)
|
||||||
|
inventory_sources_found = []
|
||||||
|
dependencies = []
|
||||||
|
for obj in active_tasks:
|
||||||
|
if type(obj) == InventoryUpdate:
|
||||||
|
if obj.inventory_source in inventory_sources:
|
||||||
|
inventory_sources_found.append(obj.inventory_source)
|
||||||
|
# Skip updating any inventory sources that were already updated before
|
||||||
|
# running this job (via callback inventory refresh).
|
||||||
|
try:
|
||||||
|
start_args = json.loads(decrypt_field(self, 'start_args'))
|
||||||
|
except Exception:
|
||||||
|
start_args = None
|
||||||
|
start_args = start_args or {}
|
||||||
|
inventory_sources_already_updated = start_args.get('inventory_sources_already_updated', [])
|
||||||
|
if inventory_sources_already_updated:
|
||||||
|
for source in inventory_sources.filter(pk__in=inventory_sources_already_updated):
|
||||||
|
if source not in inventory_sources_found:
|
||||||
|
inventory_sources_found.append(source)
|
||||||
|
if inventory_sources.count(): # and not has_setup_failures? Probably handled as an error scenario in the task runner
|
||||||
|
for source in inventory_sources:
|
||||||
|
if source not in inventory_sources_found and source.needs_update_on_launch:
|
||||||
|
dependencies.append(source.create_inventory_update(launch_type='dependency'))
|
||||||
|
return dependencies
|
||||||
|
|
||||||
|
def copy(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
presets = {}
|
||||||
|
for kw in self.job_template._get_unified_job_field_names():
|
||||||
|
presets[kw] = getattr(self, kw)
|
||||||
|
return self.job_template.create_unified_job(**presets)
|
||||||
|
|
||||||
|
|
||||||
|
class AdHocCommandEvent(CreatedModifiedModel):
|
||||||
|
'''
|
||||||
|
An event/message logged from the ad hoc event callback for each host.
|
||||||
|
'''
|
||||||
|
|
||||||
|
EVENT_TYPES = [
|
||||||
|
# (event, verbose name, failed)
|
||||||
|
('runner_on_failed', _('Host Failed'), True),
|
||||||
|
('runner_on_ok', _('Host OK'), False),
|
||||||
|
('runner_on_unreachable', _('Host Unreachable'), True),
|
||||||
|
# Tower won't see no_hosts (check is done earlier without callback).
|
||||||
|
#('runner_on_no_hosts', _('No Hosts Matched'), False),
|
||||||
|
# Tower should probably never see skipped (no conditionals).
|
||||||
|
#('runner_on_skipped', _('Host Skipped'), False),
|
||||||
|
# Tower does not support async for ad hoc commands.
|
||||||
|
#('runner_on_async_poll', _('Host Polling'), False),
|
||||||
|
#('runner_on_async_ok', _('Host Async OK'), False),
|
||||||
|
#('runner_on_async_failed', _('Host Async Failure'), True),
|
||||||
|
# Tower does not yet support --diff mode
|
||||||
|
#('runner_on_file_diff', _('File Difference'), False),
|
||||||
|
]
|
||||||
|
FAILED_EVENTS = [x[0] for x in EVENT_TYPES if x[2]]
|
||||||
|
EVENT_CHOICES = [(x[0], x[1]) for x in EVENT_TYPES]
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = 'main'
|
||||||
|
unique_together = [('ad_hoc_command', 'host_name')]
|
||||||
|
ordering = ('-pk',)
|
||||||
|
|
||||||
|
ad_hoc_command = models.ForeignKey(
|
||||||
|
'AdHocCommand',
|
||||||
|
related_name='ad_hoc_command_events',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
editable=False,
|
||||||
|
)
|
||||||
|
host = models.ForeignKey(
|
||||||
|
'Host',
|
||||||
|
related_name='ad_hoc_command_events',
|
||||||
|
null=True,
|
||||||
|
default=None,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
editable=False,
|
||||||
|
)
|
||||||
|
host_name = models.CharField(
|
||||||
|
max_length=1024,
|
||||||
|
default='',
|
||||||
|
editable=False,
|
||||||
|
)
|
||||||
|
event = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
choices=EVENT_CHOICES,
|
||||||
|
)
|
||||||
|
event_data = JSONField(
|
||||||
|
blank=True,
|
||||||
|
default={},
|
||||||
|
)
|
||||||
|
failed = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
editable=False,
|
||||||
|
)
|
||||||
|
changed = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
editable=False,
|
||||||
|
)
|
||||||
|
counter = models.PositiveIntegerField(
|
||||||
|
default=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('api:ad_hoc_command_event_detail', args=(self.pk,))
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return u'%s @ %s' % (self.get_event_display(), self.created.isoformat())
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
from awx.main.models.inventory import Host
|
||||||
|
# If update_fields has been specified, add our field names to it,
|
||||||
|
# if it hasn't been specified, then we're just doing a normal save.
|
||||||
|
update_fields = kwargs.get('update_fields', [])
|
||||||
|
res = self.event_data.get('res', None)
|
||||||
|
if self.event in self.FAILED_EVENTS:
|
||||||
|
if not self.event_data.get('ignore_errors', False):
|
||||||
|
self.failed = True
|
||||||
|
if 'failed' not in update_fields:
|
||||||
|
update_fields.append('failed')
|
||||||
|
if isinstance(res, dict) and res.get('changed', False):
|
||||||
|
self.changed = True
|
||||||
|
if 'changed' not in update_fields:
|
||||||
|
update_fields.append('changed')
|
||||||
|
self.host_name = self.event_data.get('host', '').strip()
|
||||||
|
if 'host_name' not in update_fields:
|
||||||
|
update_fields.append('host_name')
|
||||||
|
try:
|
||||||
|
if not self.host_id and self.host_name:
|
||||||
|
host_qs = Host.objects.filter(inventory__ad_hoc_commands__id=self.ad_hoc_command_id, name=self.host_name)
|
||||||
|
host_id = host_qs.only('id').values_list('id', flat=True)
|
||||||
|
if host_id.exists():
|
||||||
|
self.host_id = host_id[0]
|
||||||
|
if 'host_id' not in update_fields:
|
||||||
|
update_fields.append('host_id')
|
||||||
|
except (IndexError, AttributeError):
|
||||||
|
pass
|
||||||
|
super(AdHocCommandEvent, self).save(*args, **kwargs)
|
||||||
@@ -722,6 +722,11 @@ class Group(CommonModelNameNotUnique):
|
|||||||
from awx.main.models.jobs import JobEvent
|
from awx.main.models.jobs import JobEvent
|
||||||
return JobEvent.objects.filter(host__in=self.all_hosts)
|
return JobEvent.objects.filter(host__in=self.all_hosts)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ad_hoc_commands(self):
|
||||||
|
from awx.main.models.ad_hoc_commands import AdHocCommand
|
||||||
|
return AdHocCommand.objects.filter(hosts__in=self.all_hosts)
|
||||||
|
|
||||||
|
|
||||||
class InventorySourceOptions(BaseModel):
|
class InventorySourceOptions(BaseModel):
|
||||||
'''
|
'''
|
||||||
|
|||||||
@@ -935,6 +935,7 @@ class JobEvent(CreatedModifiedModel):
|
|||||||
job.inventory.update_computed_fields()
|
job.inventory.update_computed_fields()
|
||||||
emit_websocket_notification('/socket.io/jobs', 'summary_complete', dict(unified_job_id=job.id))
|
emit_websocket_notification('/socket.io/jobs', 'summary_complete', dict(unified_job_id=job.id))
|
||||||
|
|
||||||
|
|
||||||
class SystemJobOptions(BaseModel):
|
class SystemJobOptions(BaseModel):
|
||||||
'''
|
'''
|
||||||
Common fields for SystemJobTemplate and SystemJob.
|
Common fields for SystemJobTemplate and SystemJob.
|
||||||
|
|||||||
@@ -128,15 +128,17 @@ class Permission(CommonModelNameNotUnique):
|
|||||||
# the project parameter is not used when dealing with READ, WRITE, or ADMIN permissions.
|
# the project parameter is not used when dealing with READ, WRITE, or ADMIN permissions.
|
||||||
|
|
||||||
permission_type = models.CharField(max_length=64, choices=PERMISSION_TYPE_CHOICES)
|
permission_type = models.CharField(max_length=64, choices=PERMISSION_TYPE_CHOICES)
|
||||||
|
run_ad_hoc_commands = models.BooleanField(default=False)
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return unicode("Permission(name=%s,ON(user=%s,team=%s),FOR(project=%s,inventory=%s,type=%s))" % (
|
return unicode("Permission(name=%s,ON(user=%s,team=%s),FOR(project=%s,inventory=%s,type=%s%s))" % (
|
||||||
self.name,
|
self.name,
|
||||||
self.user,
|
self.user,
|
||||||
self.team,
|
self.team,
|
||||||
self.project,
|
self.project,
|
||||||
self.inventory,
|
self.inventory,
|
||||||
self.permission_type
|
self.permission_type,
|
||||||
|
'+adhoc' if self.run_ad_hoc_commands else '',
|
||||||
))
|
))
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
|
|||||||
@@ -481,7 +481,7 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
|||||||
return u'%s-%s-%s' % (self.created, self.id, self.status)
|
return u'%s-%s-%s' % (self.created, self.id, self.status)
|
||||||
|
|
||||||
def _get_parent_instance(self):
|
def _get_parent_instance(self):
|
||||||
return getattr(self, self._get_parent_field_name())
|
return getattr(self, self._get_parent_field_name(), None)
|
||||||
|
|
||||||
def _update_parent_instance_no_save(self, parent_instance, update_fields=[]):
|
def _update_parent_instance_no_save(self, parent_instance, update_fields=[]):
|
||||||
def parent_instance_set(key, val):
|
def parent_instance_set(key, val):
|
||||||
|
|||||||
@@ -42,6 +42,17 @@ def emit_job_event_detail(sender, **kwargs):
|
|||||||
event_serialized["event_name"] = instance.event
|
event_serialized["event_name"] = instance.event
|
||||||
emit_websocket_notification('/socket.io/job_events', 'job_events-' + str(instance.job.id), event_serialized)
|
emit_websocket_notification('/socket.io/job_events', 'job_events-' + str(instance.job.id), event_serialized)
|
||||||
|
|
||||||
|
def emit_ad_hoc_command_event_detail(sender, **kwargs):
|
||||||
|
instance = kwargs['instance']
|
||||||
|
created = kwargs['created']
|
||||||
|
if created:
|
||||||
|
event_serialized = AdHocCommandEventSerializer(instance).data
|
||||||
|
event_serialized['id'] = instance.id
|
||||||
|
event_serialized["created"] = event_serialized["created"].isoformat()
|
||||||
|
event_serialized["modified"] = event_serialized["modified"].isoformat()
|
||||||
|
event_serialized["event_name"] = instance.event
|
||||||
|
emit_websocket_notification('/socket.io/ad_hoc_command_events', 'ad_hoc_command_events-' + str(instance.ad_hoc_command_id), event_serialized)
|
||||||
|
|
||||||
def emit_update_inventory_computed_fields(sender, **kwargs):
|
def emit_update_inventory_computed_fields(sender, **kwargs):
|
||||||
logger.debug("In update inventory computed fields")
|
logger.debug("In update inventory computed fields")
|
||||||
if getattr(_inventory_updates, 'is_updating', False):
|
if getattr(_inventory_updates, 'is_updating', False):
|
||||||
@@ -121,6 +132,7 @@ pre_save.connect(store_initial_active_state, sender=Job)
|
|||||||
post_save.connect(emit_update_inventory_on_created_or_deleted, sender=Job)
|
post_save.connect(emit_update_inventory_on_created_or_deleted, sender=Job)
|
||||||
post_delete.connect(emit_update_inventory_on_created_or_deleted, sender=Job)
|
post_delete.connect(emit_update_inventory_on_created_or_deleted, sender=Job)
|
||||||
post_save.connect(emit_job_event_detail, sender=JobEvent)
|
post_save.connect(emit_job_event_detail, sender=JobEvent)
|
||||||
|
post_save.connect(emit_ad_hoc_command_event_detail, sender=AdHocCommandEvent)
|
||||||
|
|
||||||
# Migrate hosts, groups to parent group(s) whenever a group is deleted or
|
# Migrate hosts, groups to parent group(s) whenever a group is deleted or
|
||||||
# marked as inactive.
|
# marked as inactive.
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ from awx.main.utils import (get_ansible_version, decrypt_field, update_scm_url,
|
|||||||
check_proot_installed, build_proot_temp_dir, wrap_args_with_proot)
|
check_proot_installed, build_proot_temp_dir, wrap_args_with_proot)
|
||||||
|
|
||||||
__all__ = ['RunJob', 'RunSystemJob', 'RunProjectUpdate', 'RunInventoryUpdate',
|
__all__ = ['RunJob', 'RunSystemJob', 'RunProjectUpdate', 'RunInventoryUpdate',
|
||||||
'handle_work_error', 'update_inventory_computed_fields']
|
'RunAdHocCommand', 'handle_work_error', 'update_inventory_computed_fields']
|
||||||
|
|
||||||
HIDDEN_PASSWORD = '**********'
|
HIDDEN_PASSWORD = '**********'
|
||||||
|
|
||||||
@@ -137,6 +137,9 @@ def handle_work_error(self, task_id, subtasks=None):
|
|||||||
elif each_task['type'] == 'job':
|
elif each_task['type'] == 'job':
|
||||||
instance = Job.objects.get(id=each_task['id'])
|
instance = Job.objects.get(id=each_task['id'])
|
||||||
instance_name = instance.job_template.name
|
instance_name = instance.job_template.name
|
||||||
|
elif each_task['type'] == 'ad_hoc_command':
|
||||||
|
instance = AdHocCommand.objects.get(id=each_task['id'])
|
||||||
|
instance_name = instance.module_name
|
||||||
else:
|
else:
|
||||||
# Unknown task type
|
# Unknown task type
|
||||||
break
|
break
|
||||||
@@ -1130,6 +1133,164 @@ class RunInventoryUpdate(BaseTask):
|
|||||||
def get_idle_timeout(self):
|
def get_idle_timeout(self):
|
||||||
return getattr(settings, 'INVENTORY_UPDATE_IDLE_TIMEOUT', None)
|
return getattr(settings, 'INVENTORY_UPDATE_IDLE_TIMEOUT', None)
|
||||||
|
|
||||||
|
|
||||||
|
class RunAdHocCommand(BaseTask):
|
||||||
|
'''
|
||||||
|
Celery task to run an ad hoc command using ansible.
|
||||||
|
'''
|
||||||
|
|
||||||
|
name = 'awx.main.tasks.run_ad_hoc_command'
|
||||||
|
model = AdHocCommand
|
||||||
|
|
||||||
|
def build_private_data(self, ad_hoc_command, **kwargs):
|
||||||
|
'''
|
||||||
|
Return SSH private key data needed for this ad hoc command (only if
|
||||||
|
stored in DB as ssh_key_data).
|
||||||
|
'''
|
||||||
|
# If we were sent SSH credentials, decrypt them and send them
|
||||||
|
# back (they will be written to a temporary file).
|
||||||
|
creds = ad_hoc_command.credential
|
||||||
|
if creds:
|
||||||
|
return decrypt_field(creds, 'ssh_key_data') or None
|
||||||
|
|
||||||
|
def build_passwords(self, ad_hoc_command, **kwargs):
|
||||||
|
'''
|
||||||
|
Build a dictionary of passwords for SSH private key, SSH user and
|
||||||
|
sudo/su.
|
||||||
|
'''
|
||||||
|
passwords = super(RunAdHocCommand, self).build_passwords(ad_hoc_command, **kwargs)
|
||||||
|
creds = ad_hoc_command.credential
|
||||||
|
if creds:
|
||||||
|
for field in ('ssh_key_unlock', 'ssh_password', 'sudo_password', 'su_password'):
|
||||||
|
if field == 'ssh_password':
|
||||||
|
value = kwargs.get(field, decrypt_field(creds, 'password'))
|
||||||
|
else:
|
||||||
|
value = kwargs.get(field, decrypt_field(creds, field))
|
||||||
|
if value not in ('', 'ASK'):
|
||||||
|
passwords[field] = value
|
||||||
|
return passwords
|
||||||
|
|
||||||
|
def build_env(self, ad_hoc_command, **kwargs):
|
||||||
|
'''
|
||||||
|
Build environment dictionary for ansible.
|
||||||
|
'''
|
||||||
|
plugin_dir = self.get_path_to('..', 'plugins', 'callback')
|
||||||
|
env = super(RunAdHocCommand, self).build_env(ad_hoc_command, **kwargs)
|
||||||
|
# Set environment variables needed for inventory and ad hoc event
|
||||||
|
# callbacks to work.
|
||||||
|
env['AD_HOC_COMMAND_ID'] = str(ad_hoc_command.pk)
|
||||||
|
env['INVENTORY_ID'] = str(ad_hoc_command.inventory.pk)
|
||||||
|
env['INVENTORY_HOSTVARS'] = str(True)
|
||||||
|
env['ANSIBLE_CALLBACK_PLUGINS'] = plugin_dir
|
||||||
|
env['ANSIBLE_LOAD_CALLBACK_PLUGINS'] = '1'
|
||||||
|
env['REST_API_URL'] = settings.INTERNAL_API_URL
|
||||||
|
env['REST_API_TOKEN'] = ad_hoc_command.task_auth_token or ''
|
||||||
|
env['CALLBACK_CONSUMER_PORT'] = str(settings.CALLBACK_CONSUMER_PORT)
|
||||||
|
if getattr(settings, 'JOB_CALLBACK_DEBUG', False):
|
||||||
|
env['JOB_CALLBACK_DEBUG'] = '2'
|
||||||
|
elif settings.DEBUG:
|
||||||
|
env['JOB_CALLBACK_DEBUG'] = '1'
|
||||||
|
|
||||||
|
# Create a directory for ControlPath sockets that is unique to each
|
||||||
|
# ad hoc command and visible inside the proot environment (when enabled).
|
||||||
|
cp_dir = os.path.join(kwargs['private_data_dir'], 'cp')
|
||||||
|
if not os.path.exists(cp_dir):
|
||||||
|
os.mkdir(cp_dir, 0700)
|
||||||
|
env['ANSIBLE_SSH_CONTROL_PATH'] = os.path.join(cp_dir, 'ansible-ssh-%%h-%%p-%%r')
|
||||||
|
|
||||||
|
return env
|
||||||
|
|
||||||
|
def build_args(self, ad_hoc_command, **kwargs):
|
||||||
|
'''
|
||||||
|
Build command line argument list for running ansible, optionally using
|
||||||
|
ssh-agent for public/private key authentication.
|
||||||
|
'''
|
||||||
|
creds = ad_hoc_command.credential
|
||||||
|
ssh_username, sudo_username, su_username = '', '', ''
|
||||||
|
if creds:
|
||||||
|
ssh_username = kwargs.get('username', creds.username)
|
||||||
|
sudo_username = kwargs.get('sudo_username', creds.sudo_username)
|
||||||
|
su_username = kwargs.get('su_username', creds.su_username)
|
||||||
|
# Always specify the normal SSH user as root by default. Since this
|
||||||
|
# task is normally running in the background under a service account,
|
||||||
|
# it doesn't make sense to rely on ansible's default of using the
|
||||||
|
# current user.
|
||||||
|
ssh_username = ssh_username or 'root'
|
||||||
|
inventory_script = self.get_path_to('..', 'plugins', 'inventory',
|
||||||
|
'awxrest.py')
|
||||||
|
args = ['ansible', '-i', inventory_script]
|
||||||
|
if ad_hoc_command.job_type == 'check':
|
||||||
|
args.append('--check')
|
||||||
|
args.extend(['-u', ssh_username])
|
||||||
|
if 'ssh_password' in kwargs.get('passwords', {}):
|
||||||
|
args.append('--ask-pass')
|
||||||
|
# We only specify sudo/su user and password if explicitly given by the
|
||||||
|
# credential. Credential should never specify both sudo and su.
|
||||||
|
if su_username:
|
||||||
|
args.extend(['-R', su_username])
|
||||||
|
if 'su_password' in kwargs.get('passwords', {}):
|
||||||
|
args.append('--ask-su-pass')
|
||||||
|
if sudo_username:
|
||||||
|
args.extend(['-U', sudo_username])
|
||||||
|
if 'sudo_password' in kwargs.get('passwords', {}):
|
||||||
|
args.append('--ask-sudo-pass')
|
||||||
|
if ad_hoc_command.privilege_escalation == 'sudo':
|
||||||
|
args.append('--sudo')
|
||||||
|
elif ad_hoc_command.privilege_escalation == 'su':
|
||||||
|
args.append('--su')
|
||||||
|
|
||||||
|
if ad_hoc_command.forks: # FIXME: Max limit?
|
||||||
|
args.append('--forks=%d' % ad_hoc_command.forks)
|
||||||
|
if ad_hoc_command.verbosity:
|
||||||
|
args.append('-%s' % ('v' * min(3, ad_hoc_command.verbosity)))
|
||||||
|
|
||||||
|
args.extend(['-m', ad_hoc_command.module_name])
|
||||||
|
args.extend(['-a', ad_hoc_command.module_args])
|
||||||
|
|
||||||
|
if ad_hoc_command.limit:
|
||||||
|
args.append(ad_hoc_command.limit)
|
||||||
|
else:
|
||||||
|
args.append('all')
|
||||||
|
|
||||||
|
return args
|
||||||
|
|
||||||
|
def build_cwd(self, ad_hoc_command, **kwargs):
|
||||||
|
return kwargs['private_data_dir']
|
||||||
|
|
||||||
|
def get_idle_timeout(self):
|
||||||
|
return getattr(settings, 'JOB_RUN_IDLE_TIMEOUT', None)
|
||||||
|
|
||||||
|
def get_password_prompts(self):
|
||||||
|
d = super(RunAdHocCommand, self).get_password_prompts()
|
||||||
|
d[re.compile(r'^Enter passphrase for .*:\s*?$', re.M)] = 'ssh_key_unlock'
|
||||||
|
d[re.compile(r'^Bad passphrase, try again for .*:\s*?$', re.M)] = ''
|
||||||
|
d[re.compile(r'^sudo password.*:\s*?$', re.M)] = 'sudo_password'
|
||||||
|
d[re.compile(r'^SUDO password.*:\s*?$', re.M)] = 'sudo_password'
|
||||||
|
d[re.compile(r'^su password.*:\s*?$', re.M)] = 'su_password'
|
||||||
|
d[re.compile(r'^SU password.*:\s*?$', re.M)] = 'su_password'
|
||||||
|
d[re.compile(r'^SSH password:\s*?$', re.M)] = 'ssh_password'
|
||||||
|
d[re.compile(r'^Password:\s*?$', re.M)] = 'ssh_password'
|
||||||
|
return d
|
||||||
|
|
||||||
|
def get_ssh_key_path(self, instance, **kwargs):
|
||||||
|
'''
|
||||||
|
If using an SSH key, return the path for use by ssh-agent.
|
||||||
|
'''
|
||||||
|
return kwargs.get('private_data_file', '')
|
||||||
|
|
||||||
|
def should_use_proot(self, instance, **kwargs):
|
||||||
|
'''
|
||||||
|
Return whether this task should use proot.
|
||||||
|
'''
|
||||||
|
return getattr(settings, 'AWX_PROOT_ENABLED', False)
|
||||||
|
|
||||||
|
def post_run_hook(self, ad_hoc_command, **kwargs):
|
||||||
|
'''
|
||||||
|
Hook for actions to run after ad hoc command has completed.
|
||||||
|
'''
|
||||||
|
super(RunAdHocCommand, self).post_run_hook(ad_hoc_command, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class RunSystemJob(BaseTask):
|
class RunSystemJob(BaseTask):
|
||||||
|
|
||||||
name = 'awx.main.tasks.run_system_job'
|
name = 'awx.main.tasks.run_system_job'
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from awx.main.tests.projects import ProjectsTest, ProjectUpdatesTest # noqa
|
|||||||
from awx.main.tests.commands import * # noqa
|
from awx.main.tests.commands import * # noqa
|
||||||
from awx.main.tests.scripts import * # noqa
|
from awx.main.tests.scripts import * # noqa
|
||||||
from awx.main.tests.tasks import RunJobTest # noqa
|
from awx.main.tests.tasks import RunJobTest # noqa
|
||||||
|
from awx.main.tests.ad_hoc import * # noqa
|
||||||
from awx.main.tests.licenses import LicenseTests # noqa
|
from awx.main.tests.licenses import LicenseTests # noqa
|
||||||
from awx.main.tests.jobs import * # noqa
|
from awx.main.tests.jobs import * # noqa
|
||||||
from awx.main.tests.activity_stream import * # noqa
|
from awx.main.tests.activity_stream import * # noqa
|
||||||
|
|||||||
720
awx/main/tests/ad_hoc.py
Normal file
720
awx/main/tests/ad_hoc.py
Normal file
@@ -0,0 +1,720 @@
|
|||||||
|
# Copyright (c) 2015 Ansible, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
|
||||||
|
# Python
|
||||||
|
from distutils.version import StrictVersion as Version
|
||||||
|
import glob
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
# Django
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.utils.timezone import now
|
||||||
|
|
||||||
|
# Django-CRUM
|
||||||
|
from crum import impersonate
|
||||||
|
|
||||||
|
# AWX
|
||||||
|
from awx.main.models import * # noqa
|
||||||
|
from awx.main.tests.base import BaseJobExecutionTest
|
||||||
|
from awx.main.tests.tasks import TEST_SSH_KEY_DATA, TEST_SSH_KEY_DATA_LOCKED, TEST_SSH_CERT_KEY, TEST_SSH_KEY_DATA_UNLOCK
|
||||||
|
|
||||||
|
__all__ = ['RunAdHocCommandTest', 'AdHocCommandApiTest']
|
||||||
|
|
||||||
|
|
||||||
|
class BaseAdHocCommandTest(BaseJobExecutionTest):
|
||||||
|
'''
|
||||||
|
Common initialization for testing ad hoc commands.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(BaseAdHocCommandTest, self).setUp()
|
||||||
|
self.setup_instances()
|
||||||
|
self.setup_users()
|
||||||
|
self.organization = self.make_organizations(self.super_django_user, 1)[0]
|
||||||
|
self.organization.admins.add(self.normal_django_user)
|
||||||
|
self.inventory = self.organization.inventories.create(name='test-inventory', description='description for test-inventory')
|
||||||
|
self.host = self.inventory.hosts.create(name='host.example.com')
|
||||||
|
self.host2 = self.inventory.hosts.create(name='host2.example.com')
|
||||||
|
self.group = self.inventory.groups.create(name='test-group')
|
||||||
|
self.group2 = self.inventory.groups.create(name='test-group2')
|
||||||
|
self.group.hosts.add(self.host)
|
||||||
|
self.group2.hosts.add(self.host, self.host2)
|
||||||
|
self.credential = None
|
||||||
|
settings.INTERNAL_API_URL = self.live_server_url
|
||||||
|
settings.CALLBACK_CONSUMER_PORT = ''
|
||||||
|
|
||||||
|
def create_test_credential(self, **kwargs):
|
||||||
|
self.credential = self.make_credential(**kwargs)
|
||||||
|
return self.credential
|
||||||
|
|
||||||
|
|
||||||
|
class RunAdHocCommandTest(BaseAdHocCommandTest):
|
||||||
|
'''
|
||||||
|
Test cases for RunAdHocCommand celery task.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def create_test_ad_hoc_command(self, **kwargs):
|
||||||
|
with impersonate(self.super_django_user):
|
||||||
|
opts = {
|
||||||
|
'inventory': self.inventory,
|
||||||
|
'credential': self.credential,
|
||||||
|
'job_type': 'run',
|
||||||
|
'module_name': 'command',
|
||||||
|
'module_args': 'uptime',
|
||||||
|
}
|
||||||
|
opts.update(kwargs)
|
||||||
|
self.ad_hoc_command = AdHocCommand.objects.create(**opts)
|
||||||
|
return self.ad_hoc_command
|
||||||
|
|
||||||
|
def check_ad_hoc_command_events(self, ad_hoc_command, runner_status='ok',
|
||||||
|
hosts=None):
|
||||||
|
ad_hoc_command_events = ad_hoc_command.ad_hoc_command_events.all()
|
||||||
|
for ad_hoc_command_event in ad_hoc_command_events:
|
||||||
|
unicode(ad_hoc_command_event) # For test coverage.
|
||||||
|
should_be_failed = bool(runner_status not in ('ok', 'skipped'))
|
||||||
|
should_be_changed = bool(runner_status in ('ok', 'failed') and ad_hoc_command.job_type == 'run')
|
||||||
|
if hosts is not None:
|
||||||
|
host_pks = set([x.pk for x in hosts])
|
||||||
|
else:
|
||||||
|
host_pks = set(ad_hoc_command.inventory.hosts.values_list('pk', flat=True))
|
||||||
|
qs = ad_hoc_command_events.filter(event=('runner_on_%s' % runner_status))
|
||||||
|
self.assertEqual(qs.count(), len(host_pks))
|
||||||
|
for evt in qs:
|
||||||
|
self.assertTrue(evt.host_id in host_pks)
|
||||||
|
self.assertTrue(evt.host_name)
|
||||||
|
self.assertEqual(evt.failed, should_be_failed)
|
||||||
|
self.assertEqual(evt.changed, should_be_changed)
|
||||||
|
|
||||||
|
def test_run_ad_hoc_command(self):
|
||||||
|
ad_hoc_command = self.create_test_ad_hoc_command()
|
||||||
|
self.assertEqual(ad_hoc_command.status, 'new')
|
||||||
|
self.assertFalse(ad_hoc_command.passwords_needed_to_start)
|
||||||
|
self.assertTrue(ad_hoc_command.signal_start())
|
||||||
|
ad_hoc_command = AdHocCommand.objects.get(pk=ad_hoc_command.pk)
|
||||||
|
self.check_job_result(ad_hoc_command, 'successful')
|
||||||
|
self.check_ad_hoc_command_events(ad_hoc_command, 'ok')
|
||||||
|
|
||||||
|
def test_check_mode_ad_hoc_command(self):
|
||||||
|
ad_hoc_command = self.create_test_ad_hoc_command(module_name='ping', module_args='', job_type='check')
|
||||||
|
self.assertEqual(ad_hoc_command.status, 'new')
|
||||||
|
self.assertFalse(ad_hoc_command.passwords_needed_to_start)
|
||||||
|
self.assertTrue(ad_hoc_command.signal_start())
|
||||||
|
ad_hoc_command = AdHocCommand.objects.get(pk=ad_hoc_command.pk)
|
||||||
|
self.check_job_result(ad_hoc_command, 'successful')
|
||||||
|
self.check_ad_hoc_command_events(ad_hoc_command, 'ok')
|
||||||
|
|
||||||
|
def test_run_ad_hoc_command_that_fails(self):
|
||||||
|
ad_hoc_command = self.create_test_ad_hoc_command(module_args='false')
|
||||||
|
self.assertEqual(ad_hoc_command.status, 'new')
|
||||||
|
self.assertFalse(ad_hoc_command.passwords_needed_to_start)
|
||||||
|
self.assertTrue(ad_hoc_command.signal_start())
|
||||||
|
ad_hoc_command = AdHocCommand.objects.get(pk=ad_hoc_command.pk)
|
||||||
|
self.check_job_result(ad_hoc_command, 'failed')
|
||||||
|
self.check_ad_hoc_command_events(ad_hoc_command, 'failed')
|
||||||
|
|
||||||
|
def test_check_mode_where_command_would_fail(self):
|
||||||
|
ad_hoc_command = self.create_test_ad_hoc_command(job_type='check', module_args='false')
|
||||||
|
self.assertEqual(ad_hoc_command.status, 'new')
|
||||||
|
self.assertFalse(ad_hoc_command.passwords_needed_to_start)
|
||||||
|
self.assertTrue(ad_hoc_command.signal_start())
|
||||||
|
ad_hoc_command = AdHocCommand.objects.get(pk=ad_hoc_command.pk)
|
||||||
|
self.check_job_result(ad_hoc_command, 'failed')
|
||||||
|
self.check_ad_hoc_command_events(ad_hoc_command, 'unreachable')
|
||||||
|
|
||||||
|
def test_cancel_ad_hoc_command(self):
|
||||||
|
ad_hoc_command = self.create_test_ad_hoc_command()
|
||||||
|
self.assertEqual(ad_hoc_command.status, 'new')
|
||||||
|
self.assertFalse(ad_hoc_command.cancel_flag)
|
||||||
|
self.assertFalse(ad_hoc_command.passwords_needed_to_start)
|
||||||
|
ad_hoc_command.cancel_flag = True
|
||||||
|
ad_hoc_command.save(update_fields=['cancel_flag'])
|
||||||
|
self.assertTrue(ad_hoc_command.signal_start())
|
||||||
|
ad_hoc_command = AdHocCommand.objects.get(pk=ad_hoc_command.pk)
|
||||||
|
self.check_job_result(ad_hoc_command, 'canceled')
|
||||||
|
self.assertTrue(ad_hoc_command.cancel_flag)
|
||||||
|
# Calling cancel afterwards just returns the cancel flag.
|
||||||
|
self.assertTrue(ad_hoc_command.cancel())
|
||||||
|
# Read attribute for test coverage.
|
||||||
|
ad_hoc_command.celery_task
|
||||||
|
ad_hoc_command.celery_task_id = ''
|
||||||
|
ad_hoc_command.save(update_fields=['celery_task_id'])
|
||||||
|
self.assertEqual(ad_hoc_command.celery_task, None)
|
||||||
|
# Unable to start ad hoc command again.
|
||||||
|
self.assertFalse(ad_hoc_command.signal_start())
|
||||||
|
|
||||||
|
def test_ad_hoc_command_options(self):
|
||||||
|
ad_hoc_command = self.create_test_ad_hoc_command(forks=2, verbosity=2)
|
||||||
|
self.assertEqual(ad_hoc_command.status, 'new')
|
||||||
|
self.assertFalse(ad_hoc_command.passwords_needed_to_start)
|
||||||
|
self.assertTrue(ad_hoc_command.signal_start())
|
||||||
|
ad_hoc_command = AdHocCommand.objects.get(pk=ad_hoc_command.pk)
|
||||||
|
self.check_job_result(ad_hoc_command, 'successful')
|
||||||
|
self.assertTrue('"--forks=2"' in ad_hoc_command.job_args)
|
||||||
|
self.assertTrue('"-vv"' in ad_hoc_command.job_args)
|
||||||
|
# Test with sudo privilege escalation.
|
||||||
|
ad_hoc_command2 = self.create_test_ad_hoc_command(privilege_escalation='sudo')
|
||||||
|
self.assertEqual(ad_hoc_command2.status, 'new')
|
||||||
|
self.assertFalse(ad_hoc_command2.passwords_needed_to_start)
|
||||||
|
self.assertTrue(ad_hoc_command2.signal_start())
|
||||||
|
ad_hoc_command2 = AdHocCommand.objects.get(pk=ad_hoc_command2.pk)
|
||||||
|
self.check_job_result(ad_hoc_command2, ('successful', 'failed'))
|
||||||
|
self.assertTrue('"--sudo"' in ad_hoc_command2.job_args)
|
||||||
|
# Test with su privilege escalation.
|
||||||
|
ad_hoc_command3 = self.create_test_ad_hoc_command(privilege_escalation='su')
|
||||||
|
self.assertEqual(ad_hoc_command3.status, 'new')
|
||||||
|
self.assertFalse(ad_hoc_command3.passwords_needed_to_start)
|
||||||
|
self.assertTrue(ad_hoc_command3.signal_start())
|
||||||
|
ad_hoc_command3 = AdHocCommand.objects.get(pk=ad_hoc_command3.pk)
|
||||||
|
self.check_job_result(ad_hoc_command3, ('successful', 'failed'))
|
||||||
|
self.assertTrue('"--su"' in ad_hoc_command3.job_args)
|
||||||
|
|
||||||
|
def test_limit_option(self):
|
||||||
|
# Test limit by hostname.
|
||||||
|
ad_hoc_command = self.create_test_ad_hoc_command(limit='host.example.com')
|
||||||
|
self.assertEqual(ad_hoc_command.status, 'new')
|
||||||
|
self.assertFalse(ad_hoc_command.passwords_needed_to_start)
|
||||||
|
self.assertTrue(ad_hoc_command.signal_start())
|
||||||
|
ad_hoc_command = AdHocCommand.objects.get(pk=ad_hoc_command.pk)
|
||||||
|
self.check_job_result(ad_hoc_command, 'successful')
|
||||||
|
self.check_ad_hoc_command_events(ad_hoc_command, 'ok', hosts=[self.host])
|
||||||
|
self.assertTrue('"host.example.com"' in ad_hoc_command.job_args)
|
||||||
|
# Test limit by group name.
|
||||||
|
ad_hoc_command2 = self.create_test_ad_hoc_command(limit='test-group')
|
||||||
|
self.assertEqual(ad_hoc_command2.status, 'new')
|
||||||
|
self.assertFalse(ad_hoc_command2.passwords_needed_to_start)
|
||||||
|
self.assertTrue(ad_hoc_command2.signal_start())
|
||||||
|
ad_hoc_command2 = AdHocCommand.objects.get(pk=ad_hoc_command.pk)
|
||||||
|
self.check_job_result(ad_hoc_command2, 'successful')
|
||||||
|
self.check_ad_hoc_command_events(ad_hoc_command2, 'ok', hosts=[self.host])
|
||||||
|
# Test limit by host not in inventory.
|
||||||
|
ad_hoc_command3 = self.create_test_ad_hoc_command(limit='bad-host')
|
||||||
|
self.assertEqual(ad_hoc_command3.status, 'new')
|
||||||
|
self.assertFalse(ad_hoc_command3.passwords_needed_to_start)
|
||||||
|
self.assertTrue(ad_hoc_command3.signal_start())
|
||||||
|
ad_hoc_command3 = AdHocCommand.objects.get(pk=ad_hoc_command3.pk)
|
||||||
|
self.check_job_result(ad_hoc_command3, 'successful')
|
||||||
|
self.check_ad_hoc_command_events(ad_hoc_command3, 'ok', hosts=[])
|
||||||
|
self.assertEqual(ad_hoc_command3.ad_hoc_command_events.count(), 0)
|
||||||
|
|
||||||
|
def test_ssh_username_and_password(self):
|
||||||
|
self.create_test_credential(username='sshuser', password='sshpass')
|
||||||
|
ad_hoc_command = self.create_test_ad_hoc_command()
|
||||||
|
self.assertEqual(ad_hoc_command.status, 'new')
|
||||||
|
self.assertFalse(ad_hoc_command.passwords_needed_to_start)
|
||||||
|
self.assertTrue(ad_hoc_command.signal_start())
|
||||||
|
ad_hoc_command = AdHocCommand.objects.get(pk=ad_hoc_command.pk)
|
||||||
|
self.check_job_result(ad_hoc_command, 'successful')
|
||||||
|
self.assertTrue('"-u"' in ad_hoc_command.job_args)
|
||||||
|
self.assertTrue('"--ask-pass"' in ad_hoc_command.job_args)
|
||||||
|
|
||||||
|
def test_ssh_ask_password(self):
|
||||||
|
self.create_test_credential(password='ASK')
|
||||||
|
ad_hoc_command = self.create_test_ad_hoc_command()
|
||||||
|
self.assertEqual(ad_hoc_command.status, 'new')
|
||||||
|
self.assertTrue(ad_hoc_command.passwords_needed_to_start)
|
||||||
|
self.assertTrue('ssh_password' in ad_hoc_command.passwords_needed_to_start)
|
||||||
|
self.assertFalse(ad_hoc_command.signal_start())
|
||||||
|
self.assertTrue(ad_hoc_command.signal_start(ssh_password='sshpass'))
|
||||||
|
ad_hoc_command = AdHocCommand.objects.get(pk=ad_hoc_command.pk)
|
||||||
|
self.check_job_result(ad_hoc_command, 'successful')
|
||||||
|
self.assertTrue('"--ask-pass"' in ad_hoc_command.job_args)
|
||||||
|
|
||||||
|
def test_sudo_username_and_password(self):
|
||||||
|
self.create_test_credential(sudo_username='sudouser',
|
||||||
|
sudo_password='sudopass')
|
||||||
|
ad_hoc_command = self.create_test_ad_hoc_command()
|
||||||
|
self.assertEqual(ad_hoc_command.status, 'new')
|
||||||
|
self.assertFalse(ad_hoc_command.passwords_needed_to_start)
|
||||||
|
self.assertTrue(ad_hoc_command.signal_start())
|
||||||
|
ad_hoc_command = AdHocCommand.objects.get(pk=ad_hoc_command.pk)
|
||||||
|
# Job may fail if current user doesn't have password-less sudo
|
||||||
|
# privileges, but we're mainly checking the command line arguments.
|
||||||
|
self.check_job_result(ad_hoc_command, ('successful', 'failed'))
|
||||||
|
self.assertTrue('"-U"' in ad_hoc_command.job_args)
|
||||||
|
self.assertTrue('"--ask-sudo-pass"' in ad_hoc_command.job_args)
|
||||||
|
self.assertFalse('"--sudo"' in ad_hoc_command.job_args)
|
||||||
|
self.assertFalse('"-R"' in ad_hoc_command.job_args)
|
||||||
|
self.assertFalse('"--ask-su-pass"' in ad_hoc_command.job_args)
|
||||||
|
self.assertFalse('"--su"' in ad_hoc_command.job_args)
|
||||||
|
|
||||||
|
def test_sudo_ask_password(self):
|
||||||
|
self.create_test_credential(sudo_password='ASK')
|
||||||
|
ad_hoc_command = self.create_test_ad_hoc_command()
|
||||||
|
self.assertEqual(ad_hoc_command.status, 'new')
|
||||||
|
self.assertTrue(ad_hoc_command.passwords_needed_to_start)
|
||||||
|
self.assertTrue('sudo_password' in ad_hoc_command.passwords_needed_to_start)
|
||||||
|
self.assertFalse(ad_hoc_command.signal_start())
|
||||||
|
self.assertTrue(ad_hoc_command.signal_start(sudo_password='sudopass'))
|
||||||
|
ad_hoc_command = AdHocCommand.objects.get(pk=ad_hoc_command.pk)
|
||||||
|
# Job may fail, but we're mainly checking the command line arguments.
|
||||||
|
self.check_job_result(ad_hoc_command, ('successful', 'failed'))
|
||||||
|
self.assertTrue('"--ask-sudo-pass"' in ad_hoc_command.job_args)
|
||||||
|
self.assertFalse('"--sudo"' in ad_hoc_command.job_args)
|
||||||
|
self.assertFalse('"-R"' in ad_hoc_command.job_args)
|
||||||
|
self.assertFalse('"--ask-su-pass"' in ad_hoc_command.job_args)
|
||||||
|
self.assertFalse('"--su"' in ad_hoc_command.job_args)
|
||||||
|
|
||||||
|
def test_su_username_and_password(self):
|
||||||
|
self.create_test_credential(su_username='suuser', su_password='supass')
|
||||||
|
ad_hoc_command = self.create_test_ad_hoc_command()
|
||||||
|
self.assertEqual(ad_hoc_command.status, 'new')
|
||||||
|
self.assertFalse(ad_hoc_command.passwords_needed_to_start)
|
||||||
|
self.assertTrue(ad_hoc_command.signal_start())
|
||||||
|
ad_hoc_command = AdHocCommand.objects.get(pk=ad_hoc_command.pk)
|
||||||
|
# Job may fail, but we're mainly checking the command line arguments.
|
||||||
|
self.check_job_result(ad_hoc_command, ('successful', 'failed'))
|
||||||
|
self.assertTrue('"-R"' in ad_hoc_command.job_args)
|
||||||
|
self.assertTrue('"--ask-su-pass"' in ad_hoc_command.job_args)
|
||||||
|
self.assertFalse('"--su"' in ad_hoc_command.job_args)
|
||||||
|
self.assertFalse('"-U"' in ad_hoc_command.job_args)
|
||||||
|
self.assertFalse('"--ask-sudo-pass"' in ad_hoc_command.job_args)
|
||||||
|
self.assertFalse('"--sudo"' in ad_hoc_command.job_args)
|
||||||
|
|
||||||
|
def test_su_ask_password(self):
|
||||||
|
self.create_test_credential(su_password='ASK')
|
||||||
|
ad_hoc_command = self.create_test_ad_hoc_command()
|
||||||
|
self.assertEqual(ad_hoc_command.status, 'new')
|
||||||
|
self.assertTrue(ad_hoc_command.passwords_needed_to_start)
|
||||||
|
self.assertTrue('su_password' in ad_hoc_command.passwords_needed_to_start)
|
||||||
|
self.assertFalse(ad_hoc_command.signal_start())
|
||||||
|
self.assertTrue(ad_hoc_command.signal_start(su_password='sudopass'))
|
||||||
|
ad_hoc_command = AdHocCommand.objects.get(pk=ad_hoc_command.pk)
|
||||||
|
# Job may fail, but we're mainly checking the command line arguments.
|
||||||
|
self.check_job_result(ad_hoc_command, ('successful', 'failed'))
|
||||||
|
self.assertTrue('"--ask-su-pass"' in ad_hoc_command.job_args)
|
||||||
|
self.assertFalse('"--su"' in ad_hoc_command.job_args)
|
||||||
|
self.assertFalse('"--ask-sudo-pass"' in ad_hoc_command.job_args)
|
||||||
|
self.assertFalse('"--sudo"' in ad_hoc_command.job_args)
|
||||||
|
|
||||||
|
def test_unlocked_ssh_key(self):
|
||||||
|
self.create_test_credential(ssh_key_data=TEST_SSH_KEY_DATA)
|
||||||
|
ad_hoc_command = self.create_test_ad_hoc_command()
|
||||||
|
self.assertEqual(ad_hoc_command.status, 'new')
|
||||||
|
self.assertFalse(ad_hoc_command.passwords_needed_to_start)
|
||||||
|
self.assertTrue(ad_hoc_command.signal_start())
|
||||||
|
ad_hoc_command = AdHocCommand.objects.get(pk=ad_hoc_command.pk)
|
||||||
|
self.check_job_result(ad_hoc_command, 'successful')
|
||||||
|
self.assertFalse('"--private-key=' in ad_hoc_command.job_args)
|
||||||
|
self.assertTrue('ssh-agent' in ad_hoc_command.job_args)
|
||||||
|
|
||||||
|
def test_locked_ssh_key_with_password(self):
|
||||||
|
self.create_test_credential(ssh_key_data=TEST_SSH_KEY_DATA_LOCKED,
|
||||||
|
ssh_key_unlock=TEST_SSH_KEY_DATA_UNLOCK)
|
||||||
|
ad_hoc_command = self.create_test_ad_hoc_command()
|
||||||
|
self.assertEqual(ad_hoc_command.status, 'new')
|
||||||
|
self.assertFalse(ad_hoc_command.passwords_needed_to_start)
|
||||||
|
self.assertTrue(ad_hoc_command.signal_start())
|
||||||
|
ad_hoc_command = AdHocCommand.objects.get(pk=ad_hoc_command.pk)
|
||||||
|
self.check_job_result(ad_hoc_command, 'successful')
|
||||||
|
self.assertTrue('ssh-agent' in ad_hoc_command.job_args)
|
||||||
|
self.assertTrue('Bad passphrase' not in ad_hoc_command.result_stdout)
|
||||||
|
|
||||||
|
def test_locked_ssh_key_with_bad_password(self):
|
||||||
|
self.create_test_credential(ssh_key_data=TEST_SSH_KEY_DATA_LOCKED,
|
||||||
|
ssh_key_unlock='not the passphrase')
|
||||||
|
ad_hoc_command = self.create_test_ad_hoc_command()
|
||||||
|
self.assertEqual(ad_hoc_command.status, 'new')
|
||||||
|
self.assertFalse(ad_hoc_command.passwords_needed_to_start)
|
||||||
|
self.assertTrue(ad_hoc_command.signal_start())
|
||||||
|
ad_hoc_command = AdHocCommand.objects.get(pk=ad_hoc_command.pk)
|
||||||
|
self.check_job_result(ad_hoc_command, 'failed')
|
||||||
|
self.assertTrue('ssh-agent' in ad_hoc_command.job_args)
|
||||||
|
self.assertTrue('Bad passphrase' in ad_hoc_command.result_stdout)
|
||||||
|
|
||||||
|
def test_locked_ssh_key_ask_password(self):
|
||||||
|
self.create_test_credential(ssh_key_data=TEST_SSH_KEY_DATA_LOCKED,
|
||||||
|
ssh_key_unlock='ASK')
|
||||||
|
ad_hoc_command = self.create_test_ad_hoc_command()
|
||||||
|
self.assertEqual(ad_hoc_command.status, 'new')
|
||||||
|
self.assertTrue(ad_hoc_command.passwords_needed_to_start)
|
||||||
|
self.assertTrue('ssh_key_unlock' in ad_hoc_command.passwords_needed_to_start)
|
||||||
|
self.assertFalse(ad_hoc_command.signal_start())
|
||||||
|
self.assertTrue(ad_hoc_command.signal_start(ssh_key_unlock='not it'))
|
||||||
|
ad_hoc_command = AdHocCommand.objects.get(pk=ad_hoc_command.pk)
|
||||||
|
self.check_job_result(ad_hoc_command, 'failed')
|
||||||
|
self.assertTrue('ssh-agent' in ad_hoc_command.job_args)
|
||||||
|
self.assertTrue('Bad passphrase' in ad_hoc_command.result_stdout)
|
||||||
|
# Try again and pass correct password.
|
||||||
|
ad_hoc_command = self.create_test_ad_hoc_command()
|
||||||
|
self.assertEqual(ad_hoc_command.status, 'new')
|
||||||
|
self.assertTrue(ad_hoc_command.passwords_needed_to_start)
|
||||||
|
self.assertTrue('ssh_key_unlock' in ad_hoc_command.passwords_needed_to_start)
|
||||||
|
self.assertFalse(ad_hoc_command.signal_start())
|
||||||
|
self.assertTrue(ad_hoc_command.signal_start(ssh_key_unlock=TEST_SSH_KEY_DATA_UNLOCK))
|
||||||
|
ad_hoc_command = AdHocCommand.objects.get(pk=ad_hoc_command.pk)
|
||||||
|
self.check_job_result(ad_hoc_command, 'successful')
|
||||||
|
self.assertTrue('ssh-agent' in ad_hoc_command.job_args)
|
||||||
|
self.assertTrue('Bad passphrase' not in ad_hoc_command.result_stdout)
|
||||||
|
|
||||||
|
def test_run_with_proot(self):
|
||||||
|
# Only run test if proot is installed
|
||||||
|
cmd = [getattr(settings, 'AWX_PROOT_CMD', 'proot'), '--version']
|
||||||
|
try:
|
||||||
|
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE)
|
||||||
|
proc.communicate()
|
||||||
|
has_proot = bool(proc.returncode == 0)
|
||||||
|
except (OSError, ValueError):
|
||||||
|
has_proot = False
|
||||||
|
if not has_proot:
|
||||||
|
self.skipTest('proot is not installed')
|
||||||
|
# Enable proot for this test.
|
||||||
|
settings.AWX_PROOT_ENABLED = True
|
||||||
|
# Hide local settings path.
|
||||||
|
settings.AWX_PROOT_HIDE_PATHS = [os.path.join(settings.BASE_DIR, 'settings')]
|
||||||
|
# Create list of paths that should not be visible to the command.
|
||||||
|
hidden_paths = [
|
||||||
|
os.path.join(settings.PROJECTS_ROOT, '*'),
|
||||||
|
os.path.join(settings.JOBOUTPUT_ROOT, '*'),
|
||||||
|
]
|
||||||
|
# Create a temp directory that should not be visible to the command.
|
||||||
|
temp_path = tempfile.mkdtemp()
|
||||||
|
self._temp_paths.append(temp_path)
|
||||||
|
hidden_paths.append(temp_path)
|
||||||
|
# Find a file in supervisor logs that should not be visible.
|
||||||
|
try:
|
||||||
|
supervisor_log_path = glob.glob('/var/log/supervisor/*')[0]
|
||||||
|
except IndexError:
|
||||||
|
supervisor_log_path = None
|
||||||
|
if supervisor_log_path:
|
||||||
|
hidden_paths.append(supervisor_log_path)
|
||||||
|
# Create and run ad hoc command.
|
||||||
|
module_args = ' && '.join(['echo %s && test ! -e %s' % (x, x) for x in hidden_paths])
|
||||||
|
ad_hoc_command = self.create_test_ad_hoc_command(module_name='shell', module_args=module_args, verbosity=2)
|
||||||
|
self.assertEqual(ad_hoc_command.status, 'new')
|
||||||
|
self.assertFalse(ad_hoc_command.passwords_needed_to_start)
|
||||||
|
self.assertTrue(ad_hoc_command.signal_start())
|
||||||
|
ad_hoc_command = AdHocCommand.objects.get(pk=ad_hoc_command.pk)
|
||||||
|
self.check_job_result(ad_hoc_command, 'successful')
|
||||||
|
self.check_ad_hoc_command_events(ad_hoc_command, 'ok')
|
||||||
|
|
||||||
|
def test_run_with_proot_not_installed(self):
|
||||||
|
# Enable proot for this test, specify invalid proot cmd.
|
||||||
|
settings.AWX_PROOT_ENABLED = True
|
||||||
|
settings.AWX_PROOT_CMD = 'PR00T'
|
||||||
|
ad_hoc_command = self.create_test_ad_hoc_command()
|
||||||
|
self.assertEqual(ad_hoc_command.status, 'new')
|
||||||
|
self.assertFalse(ad_hoc_command.passwords_needed_to_start)
|
||||||
|
self.assertTrue(ad_hoc_command.signal_start())
|
||||||
|
ad_hoc_command = AdHocCommand.objects.get(pk=ad_hoc_command.pk)
|
||||||
|
self.check_job_result(ad_hoc_command, 'error', expect_traceback=True)
|
||||||
|
|
||||||
|
|
||||||
|
class AdHocCommandApiTest(BaseAdHocCommandTest):
|
||||||
|
'''
|
||||||
|
Test API list/detail views for ad hoc commands.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(AdHocCommandApiTest, self).setUp()
|
||||||
|
self.create_test_credential(user=self.normal_django_user)
|
||||||
|
|
||||||
|
def test_ad_hoc_command_list(self):
|
||||||
|
url = reverse('api:ad_hoc_command_list')
|
||||||
|
|
||||||
|
# Retrieve the empty list of ad hoc commands.
|
||||||
|
qs = AdHocCommand.objects.none()
|
||||||
|
self.check_get_list(url, 'admin', qs)
|
||||||
|
self.check_get_list(url, 'normal', qs)
|
||||||
|
self.check_get_list(url, 'other', qs)
|
||||||
|
self.check_get_list(url, 'nobody', qs)
|
||||||
|
self.check_get_list(url, None, qs, expect=401)
|
||||||
|
|
||||||
|
# Post to list to start a new ad hoc command. Only admin and normal
|
||||||
|
# user (org admin) can run commands by default.
|
||||||
|
data = {
|
||||||
|
'inventory': self.inventory.pk,
|
||||||
|
'credential': self.credential.pk,
|
||||||
|
'module_name': 'command',
|
||||||
|
'module_args': 'uptime',
|
||||||
|
}
|
||||||
|
with self.current_user('admin'):
|
||||||
|
response = self.post(url, data, expect=201)
|
||||||
|
self.assertEqual(response['job_type'], 'run')
|
||||||
|
self.assertEqual(response['inventory'], self.inventory.pk)
|
||||||
|
self.assertEqual(response['credential'], self.credential.pk)
|
||||||
|
self.assertEqual(response['module_name'], 'command')
|
||||||
|
self.assertEqual(response['module_args'], 'uptime')
|
||||||
|
self.assertEqual(response['limit'], '')
|
||||||
|
self.assertEqual(response['forks'], 0)
|
||||||
|
self.assertEqual(response['verbosity'], 0)
|
||||||
|
self.assertEqual(response['privilege_escalation'], '')
|
||||||
|
self.put(url, data, expect=405)
|
||||||
|
self.patch(url, data, expect=405)
|
||||||
|
self.delete(url, expect=405)
|
||||||
|
with self.current_user('normal'):
|
||||||
|
response = self.post(url, data, expect=201)
|
||||||
|
self.put(url, data, expect=405)
|
||||||
|
self.patch(url, data, expect=405)
|
||||||
|
self.delete(url, expect=405)
|
||||||
|
with self.current_user('other'):
|
||||||
|
response = self.post(url, data, expect=403)
|
||||||
|
self.put(url, data, expect=405)
|
||||||
|
self.patch(url, data, expect=405)
|
||||||
|
self.delete(url, expect=405)
|
||||||
|
with self.current_user('nobody'):
|
||||||
|
response = self.post(url, data, expect=403)
|
||||||
|
self.put(url, data, expect=405)
|
||||||
|
self.patch(url, data, expect=405)
|
||||||
|
self.delete(url, expect=405)
|
||||||
|
with self.current_user(None):
|
||||||
|
response = self.post(url, data, expect=401)
|
||||||
|
self.put(url, data, expect=401)
|
||||||
|
self.patch(url, data, expect=401)
|
||||||
|
self.delete(url, expect=401)
|
||||||
|
|
||||||
|
# Retrieve the list of ad hoc commands (only admin/normal can see by default).
|
||||||
|
qs = AdHocCommand.objects.all()
|
||||||
|
self.assertEqual(qs.count(), 2)
|
||||||
|
self.check_get_list(url, 'admin', qs)
|
||||||
|
self.check_get_list(url, 'normal', qs)
|
||||||
|
qs = AdHocCommand.objects.none()
|
||||||
|
self.check_get_list(url, 'other', qs)
|
||||||
|
self.check_get_list(url, 'nobody', qs)
|
||||||
|
self.check_get_list(url, None, qs, expect=401)
|
||||||
|
|
||||||
|
# Explicitly give other user admin permission on the inventory (still
|
||||||
|
# not allowed to run ad hoc commands).
|
||||||
|
user_perm_url = reverse('api:user_permissions_list', args=(self.other_django_user.pk,))
|
||||||
|
user_perm_data = {
|
||||||
|
'name': 'Allow Other to Admin Inventory',
|
||||||
|
'inventory': self.inventory.pk,
|
||||||
|
'permission_type': 'admin',
|
||||||
|
}
|
||||||
|
with self.current_user('admin'):
|
||||||
|
response = self.post(user_perm_url, user_perm_data, expect=201)
|
||||||
|
user_perm_id = response['id']
|
||||||
|
with self.current_user('other'):
|
||||||
|
response = self.post(url, data, expect=403)
|
||||||
|
self.check_get_list(url, 'other', qs)
|
||||||
|
|
||||||
|
# Update permission to allow other user to run ad hoc commands. Fails
|
||||||
|
# when other user can't read credential.
|
||||||
|
user_perm_url = reverse('api:permission_detail', args=(user_perm_id,))
|
||||||
|
user_perm_data.update({
|
||||||
|
'name': 'Allow Other to Admin Inventory and Run Ad Hoc Commands',
|
||||||
|
'run_ad_hoc_commands': True,
|
||||||
|
})
|
||||||
|
with self.current_user('admin'):
|
||||||
|
response = self.patch(user_perm_url, user_perm_data, expect=200)
|
||||||
|
with self.current_user('other'):
|
||||||
|
response = self.post(url, data, expect=403)
|
||||||
|
|
||||||
|
# Succeeds once other user has a readable credential. Other user can
|
||||||
|
# only see his own ad hoc command (because of credential permissions).
|
||||||
|
other_cred = self.create_test_credential(user=self.other_django_user)
|
||||||
|
credential_id = data.pop('credential')
|
||||||
|
data['credential'] = other_cred.pk
|
||||||
|
with self.current_user('other'):
|
||||||
|
response = self.post(url, data, expect=201)
|
||||||
|
qs = AdHocCommand.objects.filter(created_by=self.other_django_user)
|
||||||
|
self.assertEqual(qs.count(), 1)
|
||||||
|
self.check_get_list(url, 'other', qs)
|
||||||
|
|
||||||
|
# Explicitly give nobody user read permission on the inventory.
|
||||||
|
user_perm_url = reverse('api:user_permissions_list', args=(self.nobody_django_user.pk,))
|
||||||
|
user_perm_data = {
|
||||||
|
'name': 'Allow Nobody to Read Inventory',
|
||||||
|
'inventory': self.inventory.pk,
|
||||||
|
'permission_type': 'read',
|
||||||
|
}
|
||||||
|
with self.current_user('admin'):
|
||||||
|
response = self.post(user_perm_url, user_perm_data, expect=201)
|
||||||
|
user_perm_id = response['id']
|
||||||
|
with self.current_user('nobody'):
|
||||||
|
response = self.post(url, data, expect=403)
|
||||||
|
self.check_get_list(url, 'other', qs)
|
||||||
|
|
||||||
|
# Create a cred for the nobody user, run an ad hoc command as the admin
|
||||||
|
# user with that cred. Nobody user can still not see the ad hoc command
|
||||||
|
# without the run_ad_hoc_commands permission flag.
|
||||||
|
nobody_cred = self.create_test_credential(user=self.nobody_django_user)
|
||||||
|
credential_id = data.pop('credential')
|
||||||
|
data['credential'] = nobody_cred.pk
|
||||||
|
with self.current_user('admin'):
|
||||||
|
response = self.post(url, data, expect=201)
|
||||||
|
qs = AdHocCommand.objects.none()
|
||||||
|
self.check_get_list(url, 'nobody', qs)
|
||||||
|
|
||||||
|
# Give the nobody user the run_ad_hoc_commands flag, and can now see
|
||||||
|
# the one ad hoc command previously run.
|
||||||
|
user_perm_url = reverse('api:permission_detail', args=(user_perm_id,))
|
||||||
|
user_perm_data.update({
|
||||||
|
'name': 'Allow Nobody to Read Inventory and Run Ad Hoc Commands',
|
||||||
|
'run_ad_hoc_commands': True,
|
||||||
|
})
|
||||||
|
with self.current_user('admin'):
|
||||||
|
response = self.patch(user_perm_url, user_perm_data, expect=200)
|
||||||
|
qs = AdHocCommand.objects.filter(credential_id=data['credential'])
|
||||||
|
self.assertEqual(qs.count(), 1)
|
||||||
|
self.check_get_list(url, 'nobody', qs)
|
||||||
|
data['credential'] = credential_id
|
||||||
|
|
||||||
|
# Post without inventory (should fail).
|
||||||
|
inventory_id = data.pop('inventory')
|
||||||
|
with self.current_user('admin'):
|
||||||
|
response = self.post(url, data, expect=400)
|
||||||
|
data['inventory'] = inventory_id
|
||||||
|
|
||||||
|
# Post without credential (should fail).
|
||||||
|
credential_id = data.pop('credential')
|
||||||
|
with self.current_user('admin'):
|
||||||
|
response = self.post(url, data, expect=400)
|
||||||
|
data['credential'] = credential_id
|
||||||
|
|
||||||
|
# Post with empty or unsupported module name (empty defaults to command).
|
||||||
|
module_name = data.pop('module_name')
|
||||||
|
with self.current_user('admin'):
|
||||||
|
response = self.post(url, data, expect=201)
|
||||||
|
self.assertEqual(response['module_name'], 'command')
|
||||||
|
data['module_name'] = ''
|
||||||
|
with self.current_user('admin'):
|
||||||
|
response = self.post(url, data, expect=201)
|
||||||
|
self.assertEqual(response['module_name'], 'command')
|
||||||
|
data['module_name'] = 'transcombobulator'
|
||||||
|
with self.current_user('admin'):
|
||||||
|
response = self.post(url, data, expect=400)
|
||||||
|
data['module_name'] = module_name
|
||||||
|
|
||||||
|
# Post with empty module args for shell/command modules (should fail),
|
||||||
|
# empty args for other modules ok.
|
||||||
|
module_args = data.pop('module_args')
|
||||||
|
with self.current_user('admin'):
|
||||||
|
response = self.post(url, data, expect=400)
|
||||||
|
data['module_name'] = 'shell'
|
||||||
|
with self.current_user('admin'):
|
||||||
|
response = self.post(url, data, expect=400)
|
||||||
|
data['module_args'] = ''
|
||||||
|
with self.current_user('admin'):
|
||||||
|
response = self.post(url, data, expect=400)
|
||||||
|
data['module_name'] = 'ping'
|
||||||
|
with self.current_user('admin'):
|
||||||
|
response = self.post(url, data, expect=201)
|
||||||
|
data['module_name'] = module_name
|
||||||
|
data['module_args'] = module_args
|
||||||
|
|
||||||
|
# Post with invalid values for other parameters.
|
||||||
|
data['job_type'] = 'something'
|
||||||
|
with self.current_user('admin'):
|
||||||
|
response = self.post(url, data, expect=400)
|
||||||
|
data['job_type'] = 'check'
|
||||||
|
with self.current_user('admin'):
|
||||||
|
response = self.post(url, data, expect=201)
|
||||||
|
self.assertEqual(response['job_type'], 'check')
|
||||||
|
data.pop('job_type')
|
||||||
|
data['verbosity'] = -1
|
||||||
|
with self.current_user('admin'):
|
||||||
|
response = self.post(url, data, expect=400)
|
||||||
|
data.pop('verbosity')
|
||||||
|
data['forks'] = -1
|
||||||
|
with self.current_user('admin'):
|
||||||
|
response = self.post(url, data, expect=400)
|
||||||
|
data.pop('forks')
|
||||||
|
data['privilege_escalation'] = 'telekinesis'
|
||||||
|
with self.current_user('admin'):
|
||||||
|
response = self.post(url, data, expect=400)
|
||||||
|
data['privilege_escalation'] = 'su'
|
||||||
|
with self.current_user('admin'):
|
||||||
|
response = self.post(url, data, expect=201)
|
||||||
|
self.assertEqual(response['privilege_escalation'], 'su')
|
||||||
|
data['privilege_escalation'] = 'sudo'
|
||||||
|
with self.current_user('admin'):
|
||||||
|
response = self.post(url, data, expect=201)
|
||||||
|
self.assertEqual(response['privilege_escalation'], 'sudo')
|
||||||
|
|
||||||
|
def test_ad_hoc_command_detail(self):
|
||||||
|
# Post to list to start a new ad hoc command.
|
||||||
|
url = reverse('api:ad_hoc_command_list')
|
||||||
|
data = {
|
||||||
|
'inventory': self.inventory.pk,
|
||||||
|
'credential': self.credential.pk,
|
||||||
|
'module_name': 'command',
|
||||||
|
'module_args': 'uptime',
|
||||||
|
}
|
||||||
|
with self.current_user('admin'):
|
||||||
|
response = self.post(url, data, expect=201)
|
||||||
|
|
||||||
|
# Retrieve detail for ad hoc command. Only GET is supported.
|
||||||
|
url = reverse('api:ad_hoc_command_detail', args=(response['id'],))
|
||||||
|
self.assertEqual(url, response['url'])
|
||||||
|
with self.current_user('admin'):
|
||||||
|
response = self.get(url, expect=200)
|
||||||
|
self.assertEqual(response['related']['credential'],
|
||||||
|
reverse('api:credential_detail', args=(self.credential.pk,)))
|
||||||
|
self.assertEqual(response['related']['inventory'],
|
||||||
|
reverse('api:inventory_detail', args=(self.inventory.pk,)))
|
||||||
|
self.assertTrue(response['related']['stdout'])
|
||||||
|
self.assertTrue(response['related']['cancel'])
|
||||||
|
self.assertTrue(response['related']['relaunch'])
|
||||||
|
self.assertTrue(response['related']['events'])
|
||||||
|
self.assertTrue(response['related']['activity_stream'])
|
||||||
|
self.put(url, data, expect=405)
|
||||||
|
self.patch(url, data, expect=405)
|
||||||
|
self.delete(url, expect=405)
|
||||||
|
with self.current_user('normal'):
|
||||||
|
response = self.get(url, expect=200)
|
||||||
|
self.put(url, data, expect=405)
|
||||||
|
self.patch(url, data, expect=405)
|
||||||
|
self.delete(url, expect=405)
|
||||||
|
with self.current_user('other'):
|
||||||
|
response = self.get(url, expect=403)
|
||||||
|
self.put(url, data, expect=405)
|
||||||
|
self.patch(url, data, expect=405)
|
||||||
|
self.delete(url, expect=405)
|
||||||
|
with self.current_user('nobody'):
|
||||||
|
response = self.get(url, expect=403)
|
||||||
|
self.put(url, data, expect=405)
|
||||||
|
self.patch(url, data, expect=405)
|
||||||
|
self.delete(url, expect=405)
|
||||||
|
with self.current_user(None):
|
||||||
|
response = self.get(url, expect=401)
|
||||||
|
self.put(url, data, expect=401)
|
||||||
|
self.patch(url, data, expect=401)
|
||||||
|
self.delete(url, expect=401)
|
||||||
|
|
||||||
|
def test_ad_hoc_command_cancel(self):
|
||||||
|
# Post to list to start a new ad hoc command.
|
||||||
|
url = reverse('api:ad_hoc_command_list')
|
||||||
|
data = {
|
||||||
|
'inventory': self.inventory.pk,
|
||||||
|
'credential': self.credential.pk,
|
||||||
|
'module_name': 'command',
|
||||||
|
'module_args': 'uptime',
|
||||||
|
}
|
||||||
|
with self.current_user('admin'):
|
||||||
|
response = self.post(url, data, expect=201)
|
||||||
|
|
||||||
|
url = reverse('api:ad_hoc_command_cancel', args=(response['id'],))
|
||||||
|
self.assertEqual(url, response['related']['cancel'])
|
||||||
|
|
||||||
|
# FIXME: Finish test.
|
||||||
|
|
||||||
|
def test_ad_hoc_command_relaunch(self):
|
||||||
|
self.skipTest('Not yet implemented')
|
||||||
|
|
||||||
|
def test_ad_hoc_command_events_list(self):
|
||||||
|
self.skipTest('Not yet implemented')
|
||||||
|
|
||||||
|
def test_ad_hoc_command_event_detail(self):
|
||||||
|
self.skipTest('Not yet implemented')
|
||||||
|
|
||||||
|
def test_ad_hoc_command_activity_stream(self):
|
||||||
|
self.skipTest('Not yet implemented')
|
||||||
|
|
||||||
|
def test_inventory_ad_hoc_commands_list(self):
|
||||||
|
self.skipTest('Not yet implemented')
|
||||||
|
|
||||||
|
def test_host_ad_hoc_commands_list(self):
|
||||||
|
self.skipTest('Not yet implemented')
|
||||||
|
|
||||||
|
def test_group_ad_hoc_commands_list(self):
|
||||||
|
self.skipTest('Not yet implemented')
|
||||||
|
|
||||||
|
def test_host_ad_hoc_command_events_list(self):
|
||||||
|
self.skipTest('Not yet implemented')
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Copyright (c) 2014 AnsibleWorks, Inc.
|
# Copyright (c) 2015 Ansible, Inc.
|
||||||
# This file is a utility Ansible plugin that is not part of the AWX or Ansible
|
# This file is a utility Ansible plugin that is not part of the AWX or Ansible
|
||||||
# packages. It does not import any code from either package, nor does its
|
# packages. It does not import any code from either package, nor does its
|
||||||
# license apply to Ansible or AWX.
|
# license apply to Ansible or AWX.
|
||||||
@@ -65,28 +65,12 @@ class TokenAuth(requests.auth.AuthBase):
|
|||||||
return request
|
return request
|
||||||
|
|
||||||
|
|
||||||
class CallbackModule(object):
|
class BaseCallbackModule(object):
|
||||||
'''
|
'''
|
||||||
Callback module for logging ansible-playbook job events via the REST API.
|
Callback module for logging ansible-playbook job events via the REST API.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
# These events should never have an associated play.
|
|
||||||
EVENTS_WITHOUT_PLAY = [
|
|
||||||
'playbook_on_start',
|
|
||||||
'playbook_on_stats',
|
|
||||||
]
|
|
||||||
# These events should never have an associated task.
|
|
||||||
EVENTS_WITHOUT_TASK = EVENTS_WITHOUT_PLAY + [
|
|
||||||
'playbook_on_setup',
|
|
||||||
'playbook_on_notify',
|
|
||||||
'playbook_on_import_for_host',
|
|
||||||
'playbook_on_not_import_for_host',
|
|
||||||
'playbook_on_no_hosts_matched',
|
|
||||||
'playbook_on_no_hosts_remaining',
|
|
||||||
]
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.job_id = int(os.getenv('JOB_ID'))
|
|
||||||
self.base_url = os.getenv('REST_API_URL', '')
|
self.base_url = os.getenv('REST_API_URL', '')
|
||||||
self.auth_token = os.getenv('REST_API_TOKEN', '')
|
self.auth_token = os.getenv('REST_API_TOKEN', '')
|
||||||
self.callback_consumer_port = os.getenv('CALLBACK_CONSUMER_PORT', '')
|
self.callback_consumer_port = os.getenv('CALLBACK_CONSUMER_PORT', '')
|
||||||
@@ -128,12 +112,15 @@ class CallbackModule(object):
|
|||||||
def _post_job_event_queue_msg(self, event, event_data):
|
def _post_job_event_queue_msg(self, event, event_data):
|
||||||
self.counter += 1
|
self.counter += 1
|
||||||
msg = {
|
msg = {
|
||||||
'job_id': self.job_id,
|
|
||||||
'event': event,
|
'event': event,
|
||||||
'event_data': event_data,
|
'event_data': event_data,
|
||||||
'counter': self.counter,
|
'counter': self.counter,
|
||||||
'created': datetime.datetime.utcnow().isoformat(),
|
'created': datetime.datetime.utcnow().isoformat(),
|
||||||
}
|
}
|
||||||
|
if getattr(self, 'job_id', None):
|
||||||
|
msg['job_id'] = self.job_id
|
||||||
|
if getattr(self, 'ad_hoc_command_id', None):
|
||||||
|
msg['ad_hoc_command_id'] = self.ad_hoc_command_id
|
||||||
|
|
||||||
active_pid = os.getpid()
|
active_pid = os.getpid()
|
||||||
if self.job_callback_debug:
|
if self.job_callback_debug:
|
||||||
@@ -148,6 +135,7 @@ class CallbackModule(object):
|
|||||||
self._init_connection()
|
self._init_connection()
|
||||||
if self.context is None:
|
if self.context is None:
|
||||||
self._start_connection()
|
self._start_connection()
|
||||||
|
|
||||||
self.socket.send_json(msg)
|
self.socket.send_json(msg)
|
||||||
self.socket.recv()
|
self.socket.recv()
|
||||||
return
|
return
|
||||||
@@ -174,25 +162,12 @@ class CallbackModule(object):
|
|||||||
url = urlparse.urlunsplit([parts.scheme,
|
url = urlparse.urlunsplit([parts.scheme,
|
||||||
'%s:%d' % (parts.hostname, port),
|
'%s:%d' % (parts.hostname, port),
|
||||||
parts.path, parts.query, parts.fragment])
|
parts.path, parts.query, parts.fragment])
|
||||||
url_path = '/api/v1/jobs/%d/job_events/' % self.job_id
|
url = urlparse.urljoin(url, self.rest_api_path)
|
||||||
url = urlparse.urljoin(url, url_path)
|
|
||||||
headers = {'content-type': 'application/json'}
|
headers = {'content-type': 'application/json'}
|
||||||
response = requests.post(url, data=data, headers=headers, auth=auth)
|
response = requests.post(url, data=data, headers=headers, auth=auth)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
def _log_event(self, event, **event_data):
|
def _log_event(self, event, **event_data):
|
||||||
play = getattr(self, 'play', None)
|
|
||||||
play_name = getattr(play, 'name', '')
|
|
||||||
if play_name and event not in self.EVENTS_WITHOUT_PLAY:
|
|
||||||
event_data['play'] = play_name
|
|
||||||
task = getattr(self, 'task', None)
|
|
||||||
task_name = getattr(task, 'name', '')
|
|
||||||
role_name = getattr(task, 'role_name', '')
|
|
||||||
if task_name and event not in self.EVENTS_WITHOUT_TASK:
|
|
||||||
event_data['task'] = task_name
|
|
||||||
if role_name and event not in self.EVENTS_WITHOUT_TASK:
|
|
||||||
event_data['role'] = role_name
|
|
||||||
|
|
||||||
if self.callback_consumer_port:
|
if self.callback_consumer_port:
|
||||||
self._post_job_event_queue_msg(event, event_data)
|
self._post_job_event_queue_msg(event, event_data)
|
||||||
else:
|
else:
|
||||||
@@ -233,58 +208,8 @@ class CallbackModule(object):
|
|||||||
def runner_on_file_diff(self, host, diff):
|
def runner_on_file_diff(self, host, diff):
|
||||||
self._log_event('runner_on_file_diff', host=host, diff=diff)
|
self._log_event('runner_on_file_diff', host=host, diff=diff)
|
||||||
|
|
||||||
def playbook_on_start(self):
|
@staticmethod
|
||||||
self._log_event('playbook_on_start')
|
def terminate_ssh_control_masters():
|
||||||
|
|
||||||
def playbook_on_notify(self, host, handler):
|
|
||||||
self._log_event('playbook_on_notify', host=host, handler=handler)
|
|
||||||
|
|
||||||
def playbook_on_no_hosts_matched(self):
|
|
||||||
self._log_event('playbook_on_no_hosts_matched')
|
|
||||||
|
|
||||||
def playbook_on_no_hosts_remaining(self):
|
|
||||||
self._log_event('playbook_on_no_hosts_remaining')
|
|
||||||
|
|
||||||
def playbook_on_task_start(self, name, is_conditional):
|
|
||||||
self._log_event('playbook_on_task_start', name=name,
|
|
||||||
is_conditional=is_conditional)
|
|
||||||
|
|
||||||
def playbook_on_vars_prompt(self, varname, private=True, prompt=None,
|
|
||||||
encrypt=None, confirm=False, salt_size=None,
|
|
||||||
salt=None, default=None):
|
|
||||||
self._log_event('playbook_on_vars_prompt', varname=varname,
|
|
||||||
private=private, prompt=prompt, encrypt=encrypt,
|
|
||||||
confirm=confirm, salt_size=salt_size, salt=salt,
|
|
||||||
default=default)
|
|
||||||
|
|
||||||
def playbook_on_setup(self):
|
|
||||||
self._log_event('playbook_on_setup')
|
|
||||||
|
|
||||||
def playbook_on_import_for_host(self, host, imported_file):
|
|
||||||
# don't care about recording this one
|
|
||||||
# self._log_event('playbook_on_import_for_host', host=host,
|
|
||||||
# imported_file=imported_file)
|
|
||||||
pass
|
|
||||||
|
|
||||||
def playbook_on_not_import_for_host(self, host, missing_file):
|
|
||||||
# don't care about recording this one
|
|
||||||
#self._log_event('playbook_on_not_import_for_host', host=host,
|
|
||||||
# missing_file=missing_file)
|
|
||||||
pass
|
|
||||||
|
|
||||||
def playbook_on_play_start(self, name):
|
|
||||||
# Only play name is passed via callback, get host pattern from the play.
|
|
||||||
pattern = getattr(getattr(self, 'play', None), 'hosts', name)
|
|
||||||
self._log_event('playbook_on_play_start', name=name, pattern=pattern)
|
|
||||||
|
|
||||||
def playbook_on_stats(self, stats):
|
|
||||||
d = {}
|
|
||||||
for attr in ('changed', 'dark', 'failures', 'ok', 'processed', 'skipped'):
|
|
||||||
d[attr] = getattr(stats, attr)
|
|
||||||
self._log_event('playbook_on_stats', **d)
|
|
||||||
self._terminate_ssh_control_masters()
|
|
||||||
|
|
||||||
def _terminate_ssh_control_masters(self):
|
|
||||||
# Determine if control persist is being used and if any open sockets
|
# Determine if control persist is being used and if any open sockets
|
||||||
# exist after running the playbook.
|
# exist after running the playbook.
|
||||||
cp_path = os.environ.get('ANSIBLE_SSH_CONTROL_PATH', '')
|
cp_path = os.environ.get('ANSIBLE_SSH_CONTROL_PATH', '')
|
||||||
@@ -341,3 +266,116 @@ class CallbackModule(object):
|
|||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
for proc in procs_alive:
|
for proc in procs_alive:
|
||||||
proc.kill()
|
proc.kill()
|
||||||
|
|
||||||
|
|
||||||
|
class JobCallbackModule(BaseCallbackModule):
|
||||||
|
'''
|
||||||
|
Callback module for logging ansible-playbook job events via the REST API.
|
||||||
|
'''
|
||||||
|
|
||||||
|
# These events should never have an associated play.
|
||||||
|
EVENTS_WITHOUT_PLAY = [
|
||||||
|
'playbook_on_start',
|
||||||
|
'playbook_on_stats',
|
||||||
|
]
|
||||||
|
# These events should never have an associated task.
|
||||||
|
EVENTS_WITHOUT_TASK = EVENTS_WITHOUT_PLAY + [
|
||||||
|
'playbook_on_setup',
|
||||||
|
'playbook_on_notify',
|
||||||
|
'playbook_on_import_for_host',
|
||||||
|
'playbook_on_not_import_for_host',
|
||||||
|
'playbook_on_no_hosts_matched',
|
||||||
|
'playbook_on_no_hosts_remaining',
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.job_id = int(os.getenv('JOB_ID', '0'))
|
||||||
|
self.rest_api_path = '/api/v1/jobs/%d/job_events/' % self.job_id
|
||||||
|
super(JobCallbackModule, self).__init__()
|
||||||
|
|
||||||
|
def _log_event(self, event, **event_data):
|
||||||
|
play = getattr(self, 'play', None)
|
||||||
|
play_name = getattr(play, 'name', '')
|
||||||
|
if play_name and event not in self.EVENTS_WITHOUT_PLAY:
|
||||||
|
event_data['play'] = play_name
|
||||||
|
task = getattr(self, 'task', None)
|
||||||
|
task_name = getattr(task, 'name', '')
|
||||||
|
role_name = getattr(task, 'role_name', '')
|
||||||
|
if task_name and event not in self.EVENTS_WITHOUT_TASK:
|
||||||
|
event_data['task'] = task_name
|
||||||
|
if role_name and event not in self.EVENTS_WITHOUT_TASK:
|
||||||
|
event_data['role'] = role_name
|
||||||
|
super(JobCallbackModule, self)._log_event(event, **event_data)
|
||||||
|
|
||||||
|
def playbook_on_start(self):
|
||||||
|
self._log_event('playbook_on_start')
|
||||||
|
|
||||||
|
def playbook_on_notify(self, host, handler):
|
||||||
|
self._log_event('playbook_on_notify', host=host, handler=handler)
|
||||||
|
|
||||||
|
def playbook_on_no_hosts_matched(self):
|
||||||
|
self._log_event('playbook_on_no_hosts_matched')
|
||||||
|
|
||||||
|
def playbook_on_no_hosts_remaining(self):
|
||||||
|
self._log_event('playbook_on_no_hosts_remaining')
|
||||||
|
|
||||||
|
def playbook_on_task_start(self, name, is_conditional):
|
||||||
|
self._log_event('playbook_on_task_start', name=name,
|
||||||
|
is_conditional=is_conditional)
|
||||||
|
|
||||||
|
def playbook_on_vars_prompt(self, varname, private=True, prompt=None,
|
||||||
|
encrypt=None, confirm=False, salt_size=None,
|
||||||
|
salt=None, default=None):
|
||||||
|
self._log_event('playbook_on_vars_prompt', varname=varname,
|
||||||
|
private=private, prompt=prompt, encrypt=encrypt,
|
||||||
|
confirm=confirm, salt_size=salt_size, salt=salt,
|
||||||
|
default=default)
|
||||||
|
|
||||||
|
def playbook_on_setup(self):
|
||||||
|
self._log_event('playbook_on_setup')
|
||||||
|
|
||||||
|
def playbook_on_import_for_host(self, host, imported_file):
|
||||||
|
# don't care about recording this one
|
||||||
|
# self._log_event('playbook_on_import_for_host', host=host,
|
||||||
|
# imported_file=imported_file)
|
||||||
|
pass
|
||||||
|
|
||||||
|
def playbook_on_not_import_for_host(self, host, missing_file):
|
||||||
|
# don't care about recording this one
|
||||||
|
#self._log_event('playbook_on_not_import_for_host', host=host,
|
||||||
|
# missing_file=missing_file)
|
||||||
|
pass
|
||||||
|
|
||||||
|
def playbook_on_play_start(self, name):
|
||||||
|
# Only play name is passed via callback, get host pattern from the play.
|
||||||
|
pattern = getattr(getattr(self, 'play', None), 'hosts', name)
|
||||||
|
self._log_event('playbook_on_play_start', name=name, pattern=pattern)
|
||||||
|
|
||||||
|
def playbook_on_stats(self, stats):
|
||||||
|
d = {}
|
||||||
|
for attr in ('changed', 'dark', 'failures', 'ok', 'processed', 'skipped'):
|
||||||
|
d[attr] = getattr(stats, attr)
|
||||||
|
self._log_event('playbook_on_stats', **d)
|
||||||
|
self.terminate_ssh_control_masters()
|
||||||
|
|
||||||
|
|
||||||
|
class AdHocCommandCallbackModule(BaseCallbackModule):
|
||||||
|
'''
|
||||||
|
Callback module for logging ansible ad hoc events via ZMQ or the REST API.
|
||||||
|
'''
|
||||||
|
|
||||||
|
# FIXME: Clean up lingering control persist sockets.
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.ad_hoc_command_id = int(os.getenv('AD_HOC_COMMAND_ID', '0'))
|
||||||
|
self.rest_api_path = '/api/v1/ad_hoc_commands/%d/events/' % self.ad_hoc_command_id
|
||||||
|
super(AdHocCommandCallbackModule, self).__init__()
|
||||||
|
|
||||||
|
def runner_on_file_diff(self, host, diff):
|
||||||
|
pass # Ignore file diff for ad hoc commands.
|
||||||
|
|
||||||
|
|
||||||
|
if os.getenv('JOB_ID', ''):
|
||||||
|
CallbackModule = JobCallbackModule
|
||||||
|
elif os.getenv('AD_HOC_COMMAND_ID', ''):
|
||||||
|
CallbackModule = AdHocCommandCallbackModule
|
||||||
|
|||||||
@@ -343,6 +343,29 @@ AWX_PROOT_SHOW_PATHS = []
|
|||||||
# Number of jobs to show as part of the job template history
|
# Number of jobs to show as part of the job template history
|
||||||
AWX_JOB_TEMPLATE_HISTORY = 10
|
AWX_JOB_TEMPLATE_HISTORY = 10
|
||||||
|
|
||||||
|
# Default list of modules allowed for ad hoc commands.
|
||||||
|
AD_HOC_COMMANDS = [
|
||||||
|
'command',
|
||||||
|
'shell',
|
||||||
|
'yum',
|
||||||
|
'apt',
|
||||||
|
'apt_key',
|
||||||
|
'apt_repository',
|
||||||
|
'apt_rpm',
|
||||||
|
'service',
|
||||||
|
'group',
|
||||||
|
'user',
|
||||||
|
'mount',
|
||||||
|
'ping',
|
||||||
|
'selinux',
|
||||||
|
'setup',
|
||||||
|
'win_ping',
|
||||||
|
'win_service',
|
||||||
|
'win_updates',
|
||||||
|
'win_group',
|
||||||
|
'win_user',
|
||||||
|
]
|
||||||
|
|
||||||
# Not possible to get list of regions without authenticating, so use this list
|
# Not possible to get list of regions without authenticating, so use this list
|
||||||
# instead (based on docs from:
|
# instead (based on docs from:
|
||||||
# http://docs.rackspace.com/loadbalancers/api/v1.0/clb-devguide/content/Service_Access_Endpoints-d1e517.html)
|
# http://docs.rackspace.com/loadbalancers/api/v1.0/clb-devguide/content/Service_Access_Endpoints-d1e517.html)
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ html body .dropdown-submenu:hover>a {
|
|||||||
{% block footer %}
|
{% block footer %}
|
||||||
<div id="footer">
|
<div id="footer">
|
||||||
<a href="http://www.ansible.com" target="_blank"><img class="towerlogo" src="{{ STATIC_URL }}img/tower_console_bug.png" /></a><br/>
|
<a href="http://www.ansible.com" target="_blank"><img class="towerlogo" src="{{ STATIC_URL }}img/tower_console_bug.png" /></a><br/>
|
||||||
Copyright © 2014 <a href="http://www.ansible.com" target="_blank">Ansible, Inc.</a> All rights reserved.
|
Copyright © 2015 <a href="http://www.ansible.com" target="_blank">Ansible, Inc.</a> All rights reserved.
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user