mirror of
https://github.com/ansible/awx.git
synced 2026-01-12 02:19:58 -03:30
Implement support for ad hoc commands.
This commit is contained in:
parent
d9aa35566b
commit
f7b8d510dc
@ -7,7 +7,7 @@ from rest_framework import exceptions
|
||||
from rest_framework import HTTP_HEADER_ENCODING
|
||||
|
||||
# AWX
|
||||
from awx.main.models import Job, AuthToken
|
||||
from awx.main.models import UnifiedJob, AuthToken
|
||||
|
||||
|
||||
class TokenAuthentication(authentication.TokenAuthentication):
|
||||
@ -74,24 +74,26 @@ class TokenAuthentication(authentication.TokenAuthentication):
|
||||
# Return the user object and the token.
|
||||
return (token.user, token)
|
||||
|
||||
class JobTaskAuthentication(authentication.BaseAuthentication):
|
||||
class TaskAuthentication(authentication.BaseAuthentication):
|
||||
'''
|
||||
Custom authentication used for views accessed by the inventory and callback
|
||||
scripts when running a job.
|
||||
scripts when running a task.
|
||||
'''
|
||||
|
||||
|
||||
model = None
|
||||
|
||||
def authenticate(self, request):
|
||||
auth = authentication.get_authorization_header(request).split()
|
||||
if len(auth) != 2 or auth[0].lower() != 'token' or '-' not in auth[1]:
|
||||
return None
|
||||
job_id, job_key = auth[1].split('-', 1)
|
||||
pk, key = auth[1].split('-', 1)
|
||||
try:
|
||||
job = Job.objects.get(pk=job_id, status='running')
|
||||
except Job.DoesNotExist:
|
||||
unified_job = UnifiedJob.objects.get(pk=pk, status='running')
|
||||
except UnifiedJob.DoesNotExist:
|
||||
return None
|
||||
token = job.task_auth_token
|
||||
token = unified_job.task_auth_token
|
||||
if auth[1] != token:
|
||||
raise exceptions.AuthenticationFailed('Invalid job task token')
|
||||
raise exceptions.AuthenticationFailed('Invalid task token')
|
||||
return (None, token)
|
||||
|
||||
def authenticate_header(self, request):
|
||||
|
||||
@ -29,8 +29,9 @@ from awx.main.utils import * # noqa
|
||||
|
||||
__all__ = ['APIView', 'GenericAPIView', 'ListAPIView', 'SimpleListAPIView',
|
||||
'ListCreateAPIView', 'SubListAPIView', 'SubListCreateAPIView',
|
||||
'RetrieveAPIView', 'RetrieveUpdateAPIView',
|
||||
'RetrieveDestroyAPIView', 'RetrieveUpdateDestroyAPIView', 'DestroyAPIView']
|
||||
'SubListCreateAttachDetachAPIView', 'RetrieveAPIView',
|
||||
'RetrieveUpdateAPIView', 'RetrieveDestroyAPIView',
|
||||
'RetrieveUpdateDestroyAPIView', 'DestroyAPIView']
|
||||
|
||||
logger = logging.getLogger('awx.api.generics')
|
||||
|
||||
@ -131,12 +132,15 @@ class APIView(views.APIView):
|
||||
|
||||
def get_description_context(self):
|
||||
return {
|
||||
'view': self,
|
||||
'docstring': type(self).__doc__ or '',
|
||||
'new_in_13': getattr(self, 'new_in_13', False),
|
||||
'new_in_14': getattr(self, 'new_in_14', False),
|
||||
'new_in_145': getattr(self, 'new_in_145', False),
|
||||
'new_in_148': getattr(self, 'new_in_148', False),
|
||||
'new_in_200': getattr(self, 'new_in_200', False),
|
||||
'new_in_210': getattr(self, 'new_in_210', False),
|
||||
'new_in_220': getattr(self, 'new_in_220', False),
|
||||
}
|
||||
|
||||
def get_description(self, html=False):
|
||||
@ -153,7 +157,7 @@ class APIView(views.APIView):
|
||||
'''
|
||||
ret = super(APIView, self).metadata(request)
|
||||
added_in_version = '1.2'
|
||||
for version in ('2.1.0', '2.0.0', '1.4.8', '1.4.5', '1.4', '1.3'):
|
||||
for version in ('2.2.0', '2.1.0', '2.0.0', '1.4.8', '1.4.5', '1.4', '1.3'):
|
||||
if getattr(self, 'new_in_%s' % version.replace('.', ''), False):
|
||||
added_in_version = version
|
||||
break
|
||||
@ -328,8 +332,8 @@ class SubListAPIView(ListAPIView):
|
||||
return qs & sublist_qs
|
||||
|
||||
class SubListCreateAPIView(SubListAPIView, ListCreateAPIView):
|
||||
# Base class for a sublist view that allows for creating subobjects and
|
||||
# attaching/detaching them from the parent.
|
||||
# Base class for a sublist view that allows for creating subobjects
|
||||
# associated with the parent object.
|
||||
|
||||
# In addition to SubListAPIView properties, subclasses may define (if the
|
||||
# sub_obj requires a foreign key to the parent):
|
||||
@ -374,8 +378,13 @@ class SubListCreateAPIView(SubListAPIView, ListCreateAPIView):
|
||||
# object deserialized
|
||||
obj = serializer.save()
|
||||
serializer = self.serializer_class(obj)
|
||||
|
||||
headers = {'Location': obj.get_absolute_url()}
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
class SubListCreateAttachDetachAPIView(SubListCreateAPIView):
|
||||
# Base class for a sublist view that allows for creating subobjects and
|
||||
# attaching/detaching them from the parent.
|
||||
|
||||
def attach(self, request, *args, **kwargs):
|
||||
created = False
|
||||
|
||||
@ -19,7 +19,7 @@ from awx.main.utils import get_object_or_400
|
||||
logger = logging.getLogger('awx.api.permissions')
|
||||
|
||||
__all__ = ['ModelAccessPermission', 'JobTemplateCallbackPermission',
|
||||
'JobTaskPermission']
|
||||
'TaskPermission']
|
||||
|
||||
class ModelAccessPermission(permissions.BasePermission):
|
||||
'''
|
||||
@ -160,31 +160,31 @@ class JobTemplateCallbackPermission(ModelAccessPermission):
|
||||
else:
|
||||
return True
|
||||
|
||||
class JobTaskPermission(ModelAccessPermission):
|
||||
class TaskPermission(ModelAccessPermission):
|
||||
'''
|
||||
Permission checks used for API callbacks from running a task.
|
||||
'''
|
||||
|
||||
def has_permission(self, request, view, obj=None):
|
||||
# If another authentication method was used other than the one for job
|
||||
# If another authentication method was used other than the one for
|
||||
# callbacks, default to the superclass permissions checking.
|
||||
if request.user or not request.auth:
|
||||
return super(JobTaskPermission, self).has_permission(request, view, obj)
|
||||
return super(TaskPermission, self).has_permission(request, view, obj)
|
||||
|
||||
# Verify that the job ID present in the auth token is for a valid,
|
||||
# active job.
|
||||
# Verify that the ID present in the auth token is for a valid, active
|
||||
# unified job.
|
||||
try:
|
||||
job = Job.objects.get(active=True, status='running',
|
||||
pk=int(request.auth.split('-')[0]))
|
||||
except (Job.DoesNotExist, TypeError):
|
||||
unified_job = UnifiedJob.objects.get(active=True, status='running',
|
||||
pk=int(request.auth.split('-')[0]))
|
||||
except (UnifiedJob.DoesNotExist, TypeError):
|
||||
return False
|
||||
|
||||
# Verify that the request method is one of those allowed for the given
|
||||
# view, also that the job or inventory being accessed matches the auth
|
||||
# token.
|
||||
if view.model == Inventory and request.method.lower() in ('head', 'get'):
|
||||
return bool(not obj or obj.pk == job.inventory.pk)
|
||||
elif view.model == JobEvent and request.method.lower() == 'post':
|
||||
return bool(not obj or obj.pk == job.pk)
|
||||
return bool(not obj or obj.pk == unified_job.inventory_id)
|
||||
elif view.model in (JobEvent, AdHocCommandEvent) and request.method.lower() == 'post':
|
||||
return bool(not obj or obj.pk == unified_job.pk)
|
||||
else:
|
||||
return False
|
||||
|
||||
@ -132,16 +132,17 @@ class BaseSerializerMetaclass(serializers.SerializerMetaclass):
|
||||
for attr in dir(other):
|
||||
if attr.startswith('_'):
|
||||
continue
|
||||
val = getattr(other, attr)
|
||||
meta_val = getattr(meta, attr, [])
|
||||
val = getattr(other, attr, [])
|
||||
# Special handling for lists of strings (field names).
|
||||
if isinstance(val, (list, tuple)) and all([isinstance(x, basestring) for x in val]):
|
||||
new_vals = []
|
||||
except_vals = []
|
||||
if base: # Merge values from all bases.
|
||||
new_vals.extend([x for x in getattr(meta, attr, [])])
|
||||
new_vals.extend([x for x in meta_val])
|
||||
for v in val:
|
||||
if not base and v == '*': # Inherit all values from previous base(es).
|
||||
new_vals.extend([x for x in getattr(meta, attr, [])])
|
||||
new_vals.extend([x for x in meta_val])
|
||||
elif not base and v.startswith('-'): # Except these values.
|
||||
except_vals.append(v[1:])
|
||||
else:
|
||||
@ -226,6 +227,7 @@ class BaseSerializer(serializers.ModelSerializer):
|
||||
def get_type_choices(self):
|
||||
type_name_map = {
|
||||
'job': 'Playbook Run',
|
||||
'ad_hoc_command': 'Ad Hoc Command',
|
||||
'project_update': 'SCM Update',
|
||||
'inventory_update': 'Inventory Sync',
|
||||
'system_job': 'Management Job',
|
||||
@ -347,6 +349,23 @@ class BaseSerializer(serializers.ModelSerializer):
|
||||
exclusions.remove(field_name)
|
||||
return exclusions
|
||||
|
||||
def to_native(self, obj):
|
||||
# When rendering the raw data form, create an instance of the model so
|
||||
# that the model defaults will be filled in.
|
||||
view = self.context.get('view', None)
|
||||
parent_key = getattr(view, 'parent_key', None)
|
||||
if not obj and hasattr(view, '_raw_data_form_marker'):
|
||||
obj = self.opts.model()
|
||||
# FIXME: Would be nice to include any posted data for the raw data
|
||||
# form, so that a submission with errors can be modified in place
|
||||
# and resubmitted.
|
||||
ret = super(BaseSerializer, self).to_native(obj)
|
||||
# Remove parent key from raw form data, since it will be automatically
|
||||
# set by the sub list create view.
|
||||
if parent_key and hasattr(view, '_raw_data_form_marker'):
|
||||
ret.pop(parent_key, None)
|
||||
return ret
|
||||
|
||||
|
||||
class UnifiedJobTemplateSerializer(BaseSerializer):
|
||||
|
||||
@ -390,6 +409,7 @@ class UnifiedJobTemplateSerializer(BaseSerializer):
|
||||
class UnifiedJobSerializer(BaseSerializer):
|
||||
|
||||
result_stdout = serializers.Field(source='result_stdout')
|
||||
unified_job_template = serializers.Field(source='unified_job_template')
|
||||
|
||||
class Meta:
|
||||
model = UnifiedJob
|
||||
@ -400,7 +420,7 @@ class UnifiedJobSerializer(BaseSerializer):
|
||||
|
||||
def get_types(self):
|
||||
if type(self) is UnifiedJobSerializer:
|
||||
return ['project_update', 'inventory_update', 'job', 'system_job']
|
||||
return ['project_update', 'inventory_update', 'job', 'ad_hoc_command', 'system_job']
|
||||
else:
|
||||
return super(UnifiedJobSerializer, self).get_types()
|
||||
|
||||
@ -416,6 +436,8 @@ class UnifiedJobSerializer(BaseSerializer):
|
||||
res['stdout'] = reverse('api:inventory_update_stdout', args=(obj.pk,))
|
||||
elif isinstance(obj, Job):
|
||||
res['stdout'] = reverse('api:job_stdout', args=(obj.pk,))
|
||||
elif isinstance(obj, AdHocCommand):
|
||||
res['stdout'] = reverse('api:ad_hoc_command_stdout', args=(obj.pk,))
|
||||
return res
|
||||
|
||||
def to_native(self, obj):
|
||||
@ -427,6 +449,8 @@ class UnifiedJobSerializer(BaseSerializer):
|
||||
serializer_class = InventoryUpdateSerializer
|
||||
elif isinstance(obj, Job):
|
||||
serializer_class = JobSerializer
|
||||
elif isinstance(obj, AdHocCommand):
|
||||
serializer_class = AdHocCommandSerializer
|
||||
elif isinstance(obj, SystemJob):
|
||||
serializer_class = SystemJobSerializer
|
||||
if serializer_class:
|
||||
@ -447,7 +471,7 @@ class UnifiedJobListSerializer(UnifiedJobSerializer):
|
||||
|
||||
def get_types(self):
|
||||
if type(self) is UnifiedJobListSerializer:
|
||||
return ['project_update', 'inventory_update', 'job', 'system_job']
|
||||
return ['project_update', 'inventory_update', 'job', 'ad_hoc_command', 'system_job']
|
||||
else:
|
||||
return super(UnifiedJobListSerializer, self).get_types()
|
||||
|
||||
@ -460,6 +484,8 @@ class UnifiedJobListSerializer(UnifiedJobSerializer):
|
||||
serializer_class = InventoryUpdateListSerializer
|
||||
elif isinstance(obj, Job):
|
||||
serializer_class = JobListSerializer
|
||||
elif isinstance(obj, AdHocCommand):
|
||||
serializer_class = AdHocCommandListSerializer
|
||||
elif isinstance(obj, SystemJob):
|
||||
serializer_class = SystemJobListSerializer
|
||||
if serializer_class:
|
||||
@ -479,7 +505,7 @@ class UnifiedJobStdoutSerializer(UnifiedJobSerializer):
|
||||
|
||||
def get_types(self):
|
||||
if type(self) is UnifiedJobStdoutSerializer:
|
||||
return ['project_update', 'inventory_update', 'job', 'system_job']
|
||||
return ['project_update', 'inventory_update', 'job', 'ad_hoc_command', 'system_job']
|
||||
else:
|
||||
return super(UnifiedJobStdoutSerializer, self).get_types()
|
||||
|
||||
@ -746,6 +772,7 @@ class InventorySerializer(BaseSerializerWithVariables):
|
||||
inventory_sources = reverse('api:inventory_inventory_sources_list', args=(obj.pk,)),
|
||||
activity_stream = reverse('api:inventory_activity_stream_list', args=(obj.pk,)),
|
||||
scan_job_templates = reverse('api:inventory_scan_job_template_list', args=(obj.pk,)),
|
||||
ad_hoc_commands = reverse('api:inventory_ad_hoc_commands_list', args=(obj.pk,)),
|
||||
))
|
||||
if obj.organization and obj.organization.active:
|
||||
res['organization'] = reverse('api:organization_detail', args=(obj.organization.pk,))
|
||||
@ -784,6 +811,8 @@ class HostSerializer(BaseSerializerWithVariables):
|
||||
job_host_summaries = reverse('api:host_job_host_summaries_list', args=(obj.pk,)),
|
||||
activity_stream = reverse('api:host_activity_stream_list', args=(obj.pk,)),
|
||||
inventory_sources = reverse('api:host_inventory_sources_list', args=(obj.pk,)),
|
||||
ad_hoc_commands = reverse('api:host_ad_hoc_commands_list', args=(obj.pk,)),
|
||||
ad_hoc_command_events = reverse('api:host_ad_hoc_command_events_list', args=(obj.pk,)),
|
||||
))
|
||||
if obj.inventory and obj.inventory.active:
|
||||
res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,))
|
||||
@ -884,6 +913,7 @@ class GroupSerializer(BaseSerializerWithVariables):
|
||||
job_host_summaries = reverse('api:group_job_host_summaries_list', args=(obj.pk,)),
|
||||
activity_stream = reverse('api:group_activity_stream_list', args=(obj.pk,)),
|
||||
inventory_sources = reverse('api:group_inventory_sources_list', args=(obj.pk,)),
|
||||
ad_hoc_commands = reverse('api:group_ad_hoc_commands_list', args=(obj.pk,)),
|
||||
))
|
||||
if obj.inventory and obj.inventory.active:
|
||||
res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,))
|
||||
@ -1174,7 +1204,7 @@ class PermissionSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = Permission
|
||||
fields = ('*', 'user', 'team', 'project', 'inventory',
|
||||
'permission_type')
|
||||
'permission_type', 'run_ad_hoc_commands')
|
||||
|
||||
def get_related(self, obj):
|
||||
res = super(PermissionSerializer, self).get_related(obj)
|
||||
@ -1190,15 +1220,15 @@ class PermissionSerializer(BaseSerializer):
|
||||
|
||||
def validate(self, attrs):
|
||||
# Can only set either user or team.
|
||||
if attrs['user'] and attrs['team']:
|
||||
if attrs.get('user', None) and attrs.get('team', None):
|
||||
raise serializers.ValidationError('permission can only be assigned'
|
||||
' to a user OR a team, not both')
|
||||
# Cannot assign admit/read/write permissions for a project.
|
||||
if attrs['permission_type'] in ('admin', 'read', 'write') and attrs['project']:
|
||||
if attrs.get('permission_type', None) in ('admin', 'read', 'write') and attrs.get('project', None):
|
||||
raise serializers.ValidationError('project cannot be assigned for '
|
||||
'inventory-only permissions')
|
||||
# Project is required when setting deployment permissions.
|
||||
if attrs['permission_type'] in ('run', 'check') and not attrs['project']:
|
||||
if attrs.get('permission_type', None) in ('run', 'check') and not attrs.get('project', None):
|
||||
raise serializers.ValidationError('project is required when '
|
||||
'assigning deployment permissions')
|
||||
return attrs
|
||||
@ -1451,6 +1481,56 @@ class JobCancelSerializer(JobSerializer):
|
||||
fields = ('can_cancel',)
|
||||
|
||||
|
||||
class AdHocCommandSerializer(UnifiedJobSerializer):
|
||||
|
||||
class Meta:
|
||||
model = AdHocCommand
|
||||
fields = ('*', 'job_type', 'inventory', 'limit', 'credential',
|
||||
'module_name', 'module_args', 'forks', 'verbosity',
|
||||
'privilege_escalation')
|
||||
exclude = ('unified_job_template', 'name', 'description')
|
||||
|
||||
def get_related(self, obj):
|
||||
res = super(AdHocCommandSerializer, self).get_related(obj)
|
||||
if obj.inventory and obj.inventory.active:
|
||||
res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,))
|
||||
if obj.credential and obj.credential.active:
|
||||
res['credential'] = reverse('api:credential_detail', args=(obj.credential.pk,))
|
||||
res.update(dict(
|
||||
events = reverse('api:ad_hoc_command_ad_hoc_command_events_list', args=(obj.pk,)),
|
||||
activity_stream = reverse('api:ad_hoc_command_activity_stream_list', args=(obj.pk,)),
|
||||
))
|
||||
res['cancel'] = reverse('api:ad_hoc_command_cancel', args=(obj.pk,))
|
||||
res['relaunch'] = reverse('api:ad_hoc_command_relaunch', args=(obj.pk,))
|
||||
return res
|
||||
|
||||
def to_native(self, obj):
|
||||
# In raw data form, populate limit field from host/group name.
|
||||
view = self.context.get('view', None)
|
||||
parent_model = getattr(view, 'parent_model', None)
|
||||
if not (obj and obj.pk) and view and hasattr(view, '_raw_data_form_marker'):
|
||||
if not obj:
|
||||
obj = self.opts.model()
|
||||
if parent_model in (Host, Group):
|
||||
parent_obj = parent_model.objects.get(pk=view.kwargs['pk'])
|
||||
obj.limit = parent_obj.name
|
||||
ret = super(AdHocCommandSerializer, self).to_native(obj)
|
||||
# Hide inventory field from raw data, since it will be set automatically
|
||||
# by sub list create view.
|
||||
if not (obj and obj.pk) and view and hasattr(view, '_raw_data_form_marker'):
|
||||
if parent_model in (Host, Group):
|
||||
ret.pop('inventory', None)
|
||||
return ret
|
||||
|
||||
|
||||
class AdHocCommandCancelSerializer(AdHocCommandSerializer):
|
||||
|
||||
can_cancel = serializers.BooleanField(source='can_cancel', read_only=True)
|
||||
|
||||
class Meta:
|
||||
fields = ('can_cancel',)
|
||||
|
||||
|
||||
class SystemJobTemplateSerializer(UnifiedJobTemplateSerializer):
|
||||
|
||||
class Meta:
|
||||
@ -1482,6 +1562,9 @@ class SystemJobSerializer(UnifiedJobSerializer):
|
||||
class JobListSerializer(JobSerializer, UnifiedJobListSerializer):
|
||||
pass
|
||||
|
||||
class AdHocCommandListSerializer(AdHocCommandSerializer, UnifiedJobListSerializer):
|
||||
pass
|
||||
|
||||
class SystemJobListSerializer(SystemJobSerializer, UnifiedJobListSerializer):
|
||||
pass
|
||||
|
||||
@ -1548,6 +1631,27 @@ class JobEventSerializer(BaseSerializer):
|
||||
pass
|
||||
return d
|
||||
|
||||
|
||||
class AdHocCommandEventSerializer(BaseSerializer):
|
||||
|
||||
event_display = serializers.Field(source='get_event_display')
|
||||
|
||||
class Meta:
|
||||
model = AdHocCommandEvent
|
||||
fields = ('*', '-name', '-description', 'ad_hoc_command', 'event',
|
||||
'counter', 'event_display', 'event_data', 'failed',
|
||||
'changed', 'host', 'host_name')
|
||||
|
||||
def get_related(self, obj):
|
||||
res = super(AdHocCommandEventSerializer, self).get_related(obj)
|
||||
res.update(dict(
|
||||
ad_hoc_command = reverse('api:ad_hoc_command_detail', args=(obj.ad_hoc_command_id,)),
|
||||
))
|
||||
if obj.host:
|
||||
res['host'] = reverse('api:host_detail', args=(obj.host.pk,))
|
||||
return res
|
||||
|
||||
|
||||
class ScheduleSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
|
||||
@ -12,6 +12,7 @@ fields to create a new {{ model_verbose_name }} associated with this
|
||||
|
||||
{% block post_create %}{% endblock %}
|
||||
|
||||
{% if view.attach %}
|
||||
{% if parent_key %}
|
||||
# Remove {{ parent_model_verbose_name|title }} {{ model_verbose_name_plural|title }}:
|
||||
|
||||
@ -35,5 +36,6 @@ Make a POST request to this resource with `id` and `disassociate` fields to
|
||||
remove the {{ model_verbose_name }} from this {{ parent_model_verbose_name }}
|
||||
without deleting the {{ model_verbose_name }}.
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% include "api/_new_in_awx.md" %}
|
||||
|
||||
@ -74,6 +74,7 @@ inventory_urls = patterns('awx.api.views',
|
||||
url(r'^(?P<pk>[0-9]+)/inventory_sources/$', 'inventory_inventory_sources_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/activity_stream/$', 'inventory_activity_stream_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/scan_job_templates/$', 'inventory_scan_job_template_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/ad_hoc_commands/$', 'inventory_ad_hoc_commands_list'),
|
||||
)
|
||||
|
||||
host_urls = patterns('awx.api.views',
|
||||
@ -86,6 +87,8 @@ host_urls = patterns('awx.api.views',
|
||||
url(r'^(?P<pk>[0-9]+)/job_host_summaries/$', 'host_job_host_summaries_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/activity_stream/$', 'host_activity_stream_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/inventory_sources/$', 'host_inventory_sources_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/ad_hoc_commands/$', 'host_ad_hoc_commands_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/ad_hoc_command_events/$', 'host_ad_hoc_command_events_list'),
|
||||
)
|
||||
|
||||
group_urls = patterns('awx.api.views',
|
||||
@ -100,6 +103,7 @@ group_urls = patterns('awx.api.views',
|
||||
url(r'^(?P<pk>[0-9]+)/potential_children/$', 'group_potential_children_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/activity_stream/$', 'group_activity_stream_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/inventory_sources/$', 'group_inventory_sources_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/ad_hoc_commands/$', 'group_ad_hoc_commands_list'),
|
||||
)
|
||||
|
||||
inventory_source_urls = patterns('awx.api.views',
|
||||
@ -171,6 +175,21 @@ job_event_urls = patterns('awx.api.views',
|
||||
url(r'^(?P<pk>[0-9]+)/hosts/$', 'job_event_hosts_list'),
|
||||
)
|
||||
|
||||
ad_hoc_command_urls = patterns('awx.api.views',
|
||||
url(r'^$', 'ad_hoc_command_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/$', 'ad_hoc_command_detail'),
|
||||
url(r'^(?P<pk>[0-9]+)/cancel/$', 'ad_hoc_command_cancel'),
|
||||
url(r'^(?P<pk>[0-9]+)/relaunch/$', 'ad_hoc_command_relaunch'),
|
||||
url(r'^(?P<pk>[0-9]+)/events/$', 'ad_hoc_command_ad_hoc_command_events_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/activity_stream/$', 'ad_hoc_command_activity_stream_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/stdout/$', 'ad_hoc_command_stdout'),
|
||||
)
|
||||
|
||||
ad_hoc_command_event_urls = patterns('awx.api.views',
|
||||
url(r'^$', 'ad_hoc_command_event_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/$', 'ad_hoc_command_event_detail'),
|
||||
)
|
||||
|
||||
system_job_template_urls = patterns('awx.api.views',
|
||||
url(r'^$', 'system_job_template_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/$', 'system_job_template_detail'),
|
||||
@ -222,6 +241,8 @@ v1_urls = patterns('awx.api.views',
|
||||
url(r'^jobs/', include(job_urls)),
|
||||
url(r'^job_host_summaries/', include(job_host_summary_urls)),
|
||||
url(r'^job_events/', include(job_event_urls)),
|
||||
url(r'^ad_hoc_commands/', include(ad_hoc_command_urls)),
|
||||
url(r'^ad_hoc_command_events/', include(ad_hoc_command_event_urls)),
|
||||
url(r'^system_job_templates/', include(system_job_template_urls)),
|
||||
url(r'^system_jobs/', include(system_job_urls)),
|
||||
url(r'^unified_job_templates/$', 'unified_job_template_list'),
|
||||
|
||||
232
awx/api/views.py
232
awx/api/views.py
@ -44,7 +44,7 @@ import ansiconv
|
||||
from awx.main.task_engine import TaskSerializer, TASK_FILE
|
||||
from awx.main.access import get_user_queryset
|
||||
from awx.main.ha import is_ha_environment
|
||||
from awx.api.authentication import JobTaskAuthentication
|
||||
from awx.api.authentication import TaskAuthentication
|
||||
from awx.api.utils.decorators import paginated
|
||||
from awx.api.generics import get_view_name
|
||||
from awx.api.generics import * # noqa
|
||||
@ -111,6 +111,7 @@ class ApiV1RootView(APIView):
|
||||
data['hosts'] = reverse('api:host_list')
|
||||
data['job_templates'] = reverse('api:job_template_list')
|
||||
data['jobs'] = reverse('api:job_list')
|
||||
data['ad_hoc_commands'] = reverse('api:ad_hoc_command_list')
|
||||
data['system_job_templates'] = reverse('api:system_job_template_list')
|
||||
data['system_jobs'] = reverse('api:system_job_list')
|
||||
data['schedules'] = reverse('api:schedule_list')
|
||||
@ -492,28 +493,28 @@ class OrganizationInventoriesList(SubListAPIView):
|
||||
parent_model = Organization
|
||||
relationship = 'inventories'
|
||||
|
||||
class OrganizationUsersList(SubListCreateAPIView):
|
||||
class OrganizationUsersList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = User
|
||||
serializer_class = UserSerializer
|
||||
parent_model = Organization
|
||||
relationship = 'users'
|
||||
|
||||
class OrganizationAdminsList(SubListCreateAPIView):
|
||||
class OrganizationAdminsList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = User
|
||||
serializer_class = UserSerializer
|
||||
parent_model = Organization
|
||||
relationship = 'admins'
|
||||
|
||||
class OrganizationProjectsList(SubListCreateAPIView):
|
||||
class OrganizationProjectsList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Project
|
||||
serializer_class = ProjectSerializer
|
||||
parent_model = Organization
|
||||
relationship = 'projects'
|
||||
|
||||
class OrganizationTeamsList(SubListCreateAPIView):
|
||||
class OrganizationTeamsList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Team
|
||||
serializer_class = TeamSerializer
|
||||
@ -539,14 +540,14 @@ class TeamDetail(RetrieveUpdateDestroyAPIView):
|
||||
model = Team
|
||||
serializer_class = TeamSerializer
|
||||
|
||||
class TeamUsersList(SubListCreateAPIView):
|
||||
class TeamUsersList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = User
|
||||
serializer_class = UserSerializer
|
||||
parent_model = Team
|
||||
relationship = 'users'
|
||||
|
||||
class TeamPermissionsList(SubListCreateAPIView):
|
||||
class TeamPermissionsList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Permission
|
||||
serializer_class = PermissionSerializer
|
||||
@ -565,14 +566,14 @@ class TeamPermissionsList(SubListCreateAPIView):
|
||||
return base
|
||||
raise PermissionDenied()
|
||||
|
||||
class TeamProjectsList(SubListCreateAPIView):
|
||||
class TeamProjectsList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Project
|
||||
serializer_class = ProjectSerializer
|
||||
parent_model = Team
|
||||
relationship = 'projects'
|
||||
|
||||
class TeamCredentialsList(SubListCreateAPIView):
|
||||
class TeamCredentialsList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Credential
|
||||
serializer_class = CredentialSerializer
|
||||
@ -631,21 +632,21 @@ class ProjectPlaybooks(RetrieveAPIView):
|
||||
model = Project
|
||||
serializer_class = ProjectPlaybooksSerializer
|
||||
|
||||
class ProjectOrganizationsList(SubListCreateAPIView):
|
||||
class ProjectOrganizationsList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Organization
|
||||
serializer_class = OrganizationSerializer
|
||||
parent_model = Project
|
||||
relationship = 'organizations'
|
||||
|
||||
class ProjectTeamsList(SubListCreateAPIView):
|
||||
class ProjectTeamsList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Team
|
||||
serializer_class = TeamSerializer
|
||||
parent_model = Project
|
||||
relationship = 'teams'
|
||||
|
||||
class ProjectSchedulesList(SubListCreateAPIView):
|
||||
class ProjectSchedulesList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
view_name = "Project Schedules"
|
||||
|
||||
@ -746,7 +747,7 @@ class UserTeamsList(SubListAPIView):
|
||||
parent_model = User
|
||||
relationship = 'teams'
|
||||
|
||||
class UserPermissionsList(SubListCreateAPIView):
|
||||
class UserPermissionsList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Permission
|
||||
serializer_class = PermissionSerializer
|
||||
@ -767,7 +768,7 @@ class UserProjectsList(SubListAPIView):
|
||||
qs = self.request.user.get_queryset(self.model)
|
||||
return qs.filter(teams__in=parent.teams.distinct())
|
||||
|
||||
class UserCredentialsList(SubListCreateAPIView):
|
||||
class UserCredentialsList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Credential
|
||||
serializer_class = CredentialSerializer
|
||||
@ -932,7 +933,7 @@ class HostDetail(RetrieveUpdateDestroyAPIView):
|
||||
model = Host
|
||||
serializer_class = HostSerializer
|
||||
|
||||
class InventoryHostsList(SubListCreateAPIView):
|
||||
class InventoryHostsList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Host
|
||||
serializer_class = HostSerializer
|
||||
@ -940,7 +941,7 @@ class InventoryHostsList(SubListCreateAPIView):
|
||||
relationship = 'hosts'
|
||||
parent_key = 'inventory'
|
||||
|
||||
class HostGroupsList(SubListCreateAPIView):
|
||||
class HostGroupsList(SubListCreateAttachDetachAPIView):
|
||||
''' the list of groups a host is directly a member of '''
|
||||
|
||||
model = Group
|
||||
@ -991,7 +992,7 @@ class GroupList(ListCreateAPIView):
|
||||
model = Group
|
||||
serializer_class = GroupSerializer
|
||||
|
||||
class GroupChildrenList(SubListCreateAPIView):
|
||||
class GroupChildrenList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Group
|
||||
serializer_class = GroupSerializer
|
||||
@ -1050,7 +1051,7 @@ class GroupPotentialChildrenList(SubListAPIView):
|
||||
except_pks.update(parent.all_children.values_list('pk', flat=True))
|
||||
return qs.exclude(pk__in=except_pks)
|
||||
|
||||
class GroupHostsList(SubListCreateAPIView):
|
||||
class GroupHostsList(SubListCreateAttachDetachAPIView):
|
||||
''' the list of hosts directly below a group '''
|
||||
|
||||
model = Host
|
||||
@ -1124,7 +1125,7 @@ class GroupDetail(RetrieveUpdateDestroyAPIView):
|
||||
obj.mark_inactive_recursive()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
class InventoryGroupsList(SubListCreateAPIView):
|
||||
class InventoryGroupsList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Group
|
||||
serializer_class = GroupSerializer
|
||||
@ -1132,7 +1133,7 @@ class InventoryGroupsList(SubListCreateAPIView):
|
||||
relationship = 'groups'
|
||||
parent_key = 'inventory'
|
||||
|
||||
class InventoryRootGroupsList(SubListCreateAPIView):
|
||||
class InventoryRootGroupsList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Group
|
||||
serializer_class = GroupSerializer
|
||||
@ -1171,8 +1172,8 @@ class InventoryScriptView(RetrieveAPIView):
|
||||
|
||||
model = Inventory
|
||||
serializer_class = InventoryScriptSerializer
|
||||
authentication_classes = [JobTaskAuthentication] + api_settings.DEFAULT_AUTHENTICATION_CLASSES
|
||||
permission_classes = (JobTaskPermission,)
|
||||
authentication_classes = [TaskAuthentication] + api_settings.DEFAULT_AUTHENTICATION_CLASSES
|
||||
permission_classes = (TaskPermission,)
|
||||
filter_backends = ()
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
@ -1330,7 +1331,7 @@ class InventorySourceDetail(RetrieveUpdateAPIView):
|
||||
pu.cancel()
|
||||
return super(InventorySourceDetail, self).destroy(request, *args, **kwargs)
|
||||
|
||||
class InventorySourceSchedulesList(SubListCreateAPIView):
|
||||
class InventorySourceSchedulesList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
view_name = "Inventory Source Schedules"
|
||||
|
||||
@ -1479,7 +1480,7 @@ class JobTemplateLaunch(GenericAPIView):
|
||||
data = dict(job=new_job.id)
|
||||
return Response(data, status=status.HTTP_202_ACCEPTED)
|
||||
|
||||
class JobTemplateSchedulesList(SubListCreateAPIView):
|
||||
class JobTemplateSchedulesList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
view_name = "Job Template Schedules"
|
||||
|
||||
@ -1749,7 +1750,7 @@ class SystemJobTemplateLaunch(GenericAPIView):
|
||||
data = dict(system_job=new_job.id)
|
||||
return Response(data, status=status.HTTP_202_ACCEPTED)
|
||||
|
||||
class SystemJobTemplateSchedulesList(SubListCreateAPIView):
|
||||
class SystemJobTemplateSchedulesList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
view_name = "System Job Template Schedules"
|
||||
|
||||
@ -1944,8 +1945,8 @@ class GroupJobEventsList(BaseJobEventsList):
|
||||
class JobJobEventsList(BaseJobEventsList):
|
||||
|
||||
parent_model = Job
|
||||
authentication_classes = [JobTaskAuthentication] + api_settings.DEFAULT_AUTHENTICATION_CLASSES
|
||||
permission_classes = (JobTaskPermission,)
|
||||
authentication_classes = [TaskAuthentication] + api_settings.DEFAULT_AUTHENTICATION_CLASSES
|
||||
permission_classes = (TaskPermission,)
|
||||
|
||||
# Post allowed for job event callback only.
|
||||
def post(self, request, *args, **kwargs):
|
||||
@ -1966,8 +1967,6 @@ class JobJobPlaysList(BaseJobEventsList):
|
||||
|
||||
parent_model = Job
|
||||
view_name = 'Job Plays List'
|
||||
authentication_classes = [JobTaskAuthentication] + api_settings.DEFAULT_AUTHENTICATION_CLASSES
|
||||
permission_classes = (JobTaskPermission,)
|
||||
new_in_200 = True
|
||||
|
||||
@paginated
|
||||
@ -2042,8 +2041,6 @@ class JobJobTasksList(BaseJobEventsList):
|
||||
and their completion status.
|
||||
"""
|
||||
parent_model = Job
|
||||
authentication_classes = [JobTaskAuthentication] + api_settings.DEFAULT_AUTHENTICATION_CLASSES
|
||||
permission_classes = (JobTaskPermission,)
|
||||
view_name = 'Job Play Tasks List'
|
||||
new_in_200 = True
|
||||
|
||||
@ -2175,6 +2172,174 @@ class JobJobTasksList(BaseJobEventsList):
|
||||
# Done; return the results and count.
|
||||
return (results, count, None)
|
||||
|
||||
|
||||
class AdHocCommandList(ListCreateAPIView):
|
||||
|
||||
model = AdHocCommand
|
||||
serializer_class = AdHocCommandListSerializer
|
||||
new_in_220 = True
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
# Inject inventory ID if parent objects is a host/group.
|
||||
if hasattr(self, 'get_parent_object') and not getattr(self, 'parent_key', None):
|
||||
data = request.DATA
|
||||
# HACK: Make request data mutable.
|
||||
if getattr(data, '_mutable', None) is False:
|
||||
data._mutable = True
|
||||
parent_obj = self.get_parent_object()
|
||||
if isinstance(parent_obj, (Host, Group)):
|
||||
data['inventory'] = parent_obj.inventory_id
|
||||
response = super(AdHocCommandList, self).create(request, *args, **kwargs)
|
||||
if response.status_code != status.HTTP_201_CREATED:
|
||||
return response
|
||||
# Start ad hoc command running when created.
|
||||
ad_hoc_command = get_object_or_400(self.model, pk=response.data['id'])
|
||||
result = ad_hoc_command.signal_start(**request.DATA)
|
||||
if not result:
|
||||
data = dict(passwords_needed_to_start=ad_hoc_command.passwords_needed_to_start)
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
return response
|
||||
|
||||
|
||||
class InventoryAdHocCommandsList(AdHocCommandList, SubListCreateAPIView):
|
||||
|
||||
parent_model = Inventory
|
||||
relationship = 'ad_hoc_commands'
|
||||
parent_key = 'inventory'
|
||||
|
||||
|
||||
class GroupAdHocCommandsList(AdHocCommandList, SubListCreateAPIView):
|
||||
|
||||
parent_model = Group
|
||||
relationship = 'ad_hoc_commands'
|
||||
|
||||
|
||||
class HostAdHocCommandsList(AdHocCommandList, SubListCreateAPIView):
|
||||
|
||||
parent_model = Host
|
||||
relationship = 'ad_hoc_commands'
|
||||
|
||||
|
||||
class AdHocCommandDetail(RetrieveAPIView):
|
||||
|
||||
model = AdHocCommand
|
||||
serializer_class = AdHocCommandSerializer
|
||||
new_in_220 = True
|
||||
|
||||
|
||||
class AdHocCommandCancel(RetrieveAPIView):
|
||||
|
||||
model = AdHocCommand
|
||||
serializer_class = AdHocCommandCancelSerializer
|
||||
is_job_cancel = True
|
||||
new_in_220 = True
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
obj = self.get_object()
|
||||
if obj.can_cancel:
|
||||
obj.cancel()
|
||||
return Response(status=status.HTTP_202_ACCEPTED)
|
||||
else:
|
||||
return self.http_method_not_allowed(request, *args, **kwargs)
|
||||
|
||||
|
||||
class AdHocCommandRelaunch(GenericAPIView):
|
||||
|
||||
model = AdHocCommand
|
||||
is_job_start = True
|
||||
new_in_220 = True
|
||||
# FIXME: Add serializer class to define fields in OPTIONS request!
|
||||
|
||||
@csrf_exempt
|
||||
@transaction.non_atomic_requests
|
||||
def dispatch(self, *args, **kwargs):
|
||||
return super(AdHocCommandRelaunch, self).dispatch(*args, **kwargs)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
obj = self.get_object()
|
||||
data = {}
|
||||
data['passwords_needed_to_start'] = obj.passwords_needed_to_start
|
||||
return Response(data)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
obj = self.get_object()
|
||||
if not request.user.can_access(self.model, 'start', obj):
|
||||
raise PermissionDenied()
|
||||
new_ad_hoc_command = obj.copy()
|
||||
result = new_ad_hoc_command.signal_start(**request.DATA)
|
||||
if not result:
|
||||
data = dict(passwords_needed_to_start=obj.passwords_needed_to_start)
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
data = dict(ad_hoc_command=new_ad_hoc_command.id)
|
||||
return Response(data, status=status.HTTP_202_ACCEPTED)
|
||||
|
||||
|
||||
class AdHocCommandEventList(ListAPIView):
|
||||
|
||||
model = AdHocCommandEvent
|
||||
serializer_class = AdHocCommandEventSerializer
|
||||
new_in_220 = True
|
||||
|
||||
|
||||
class AdHocCommandEventDetail(RetrieveAPIView):
|
||||
|
||||
model = AdHocCommandEvent
|
||||
serializer_class = AdHocCommandEventSerializer
|
||||
new_in_220 = True
|
||||
|
||||
|
||||
class BaseAdHocCommandEventsList(SubListAPIView):
|
||||
|
||||
model = AdHocCommandEvent
|
||||
serializer_class = AdHocCommandEventSerializer
|
||||
parent_model = None # Subclasses must define this attribute.
|
||||
relationship = 'ad_hoc_command_events'
|
||||
view_name = 'Ad Hoc Command Events List'
|
||||
new_in_220 = True
|
||||
|
||||
|
||||
class HostAdHocCommandEventsList(BaseAdHocCommandEventsList):
|
||||
|
||||
parent_model = Host
|
||||
new_in_220 = True
|
||||
|
||||
#class GroupJobEventsList(BaseJobEventsList):
|
||||
# parent_model = Group
|
||||
|
||||
|
||||
class AdHocCommandAdHocCommandEventsList(BaseAdHocCommandEventsList):
|
||||
|
||||
parent_model = AdHocCommand
|
||||
authentication_classes = [TaskAuthentication] + api_settings.DEFAULT_AUTHENTICATION_CLASSES
|
||||
permission_classes = (TaskPermission,)
|
||||
new_in_220 = True
|
||||
|
||||
# Post allowed for ad hoc event callback only.
|
||||
def post(self, request, *args, **kwargs):
|
||||
parent_obj = get_object_or_404(self.parent_model, pk=self.kwargs['pk'])
|
||||
data = request.DATA.copy()
|
||||
data['ad_hoc_command'] = parent_obj.pk
|
||||
serializer = self.get_serializer(data=data)
|
||||
if serializer.is_valid():
|
||||
self.pre_save(serializer.object)
|
||||
self.object = serializer.save(force_insert=True)
|
||||
self.post_save(self.object, created=True)
|
||||
headers = {'Location': serializer.data['url']}
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED,
|
||||
headers=headers)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class AdHocCommandActivityStreamList(SubListAPIView):
|
||||
|
||||
model = ActivityStream
|
||||
serializer_class = ActivityStreamSerializer
|
||||
parent_model = AdHocCommand
|
||||
relationship = 'activitystream_set'
|
||||
new_in_220 = True
|
||||
|
||||
|
||||
class SystemJobList(ListCreateAPIView):
|
||||
|
||||
model = SystemJob
|
||||
@ -2254,6 +2419,11 @@ class JobStdout(UnifiedJobStdout):
|
||||
|
||||
model = Job
|
||||
|
||||
class AdHocCommandStdout(UnifiedJobStdout):
|
||||
|
||||
model = AdHocCommand
|
||||
new_in_220 = True
|
||||
|
||||
class ActivityStreamList(SimpleListAPIView):
|
||||
|
||||
model = ActivityStream
|
||||
|
||||
@ -152,6 +152,22 @@ class BaseAccess(object):
|
||||
def can_unattach(self, obj, sub_obj, relationship):
|
||||
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):
|
||||
'''
|
||||
I can see user records when:
|
||||
@ -255,6 +271,10 @@ class InventoryAccess(BaseAccess):
|
||||
- I'm a superuser.
|
||||
- I'm an org admin of the inventory's org.
|
||||
- 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
|
||||
@ -327,6 +347,18 @@ class InventoryAccess(BaseAccess):
|
||||
def can_delete(self, obj):
|
||||
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):
|
||||
'''
|
||||
I can see hosts whenever I can see their inventory.
|
||||
@ -358,25 +390,8 @@ class HostAccess(BaseAccess):
|
||||
return False
|
||||
|
||||
# Check to see if we have enough licenses
|
||||
reader = TaskSerializer()
|
||||
validation_info = reader.from_file()
|
||||
|
||||
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)
|
||||
self.check_license()
|
||||
return True
|
||||
|
||||
def can_change(self, obj, data):
|
||||
# Prevent moving a host to a different inventory.
|
||||
@ -972,21 +987,9 @@ class JobTemplateAccess(BaseAccess):
|
||||
# return False
|
||||
|
||||
def can_start(self, obj, validate_license=True):
|
||||
reader = TaskSerializer()
|
||||
validation_info = reader.from_file()
|
||||
|
||||
# Check license.
|
||||
if validate_license:
|
||||
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")
|
||||
self.check_license()
|
||||
|
||||
# Super users can start any job
|
||||
if self.user.is_superuser:
|
||||
@ -1105,19 +1108,6 @@ class JobAccess(BaseAccess):
|
||||
if not self.user.is_superuser:
|
||||
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())
|
||||
|
||||
@ -1142,20 +1132,7 @@ class JobAccess(BaseAccess):
|
||||
return self.can_read(obj)
|
||||
|
||||
def can_start(self, obj):
|
||||
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")
|
||||
self.check_license()
|
||||
|
||||
# A super user can relaunch a job
|
||||
if self.user.is_superuser:
|
||||
@ -1188,6 +1165,102 @@ class SystemJobAccess(BaseAccess):
|
||||
'''
|
||||
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):
|
||||
'''
|
||||
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)
|
||||
inventory_update_qs = self.user.get_queryset(InventoryUpdate).filter(source__in=CLOUD_INVENTORY_SOURCES)
|
||||
job_qs = self.user.get_queryset(Job)
|
||||
ad_hoc_command_qs = self.user.get_queryset(AdHocCommand)
|
||||
system_job_qs = self.user.get_queryset(SystemJob)
|
||||
qs = qs.filter(Q(ProjectUpdate___in=project_update_qs) |
|
||||
Q(InventoryUpdate___in=inventory_update_qs) |
|
||||
Q(Job___in=job_qs) |
|
||||
Q(AdHocCommand___in=ad_hoc_command_qs) |
|
||||
Q(SystemJob___in=system_job_qs))
|
||||
qs = qs.select_related(
|
||||
'created_by',
|
||||
@ -1537,6 +1612,8 @@ register_access(JobHostSummary, JobHostSummaryAccess)
|
||||
register_access(JobEvent, JobEventAccess)
|
||||
register_access(SystemJobTemplate, SystemJobTemplateAccess)
|
||||
register_access(SystemJob, SystemJobAccess)
|
||||
register_access(AdHocCommand, AdHocCommandAccess)
|
||||
register_access(AdHocCommandEvent, AdHocCommandEventAccess)
|
||||
register_access(Schedule, ScheduleAccess)
|
||||
register_access(UnifiedJobTemplate, UnifiedJobTemplateAccess)
|
||||
register_access(UnifiedJob, UnifiedJobAccess)
|
||||
|
||||
@ -117,7 +117,9 @@ class CallbackReceiver(object):
|
||||
with Socket('callbacks', 'r') as callbacks:
|
||||
for message in callbacks.listen():
|
||||
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)
|
||||
else:
|
||||
job_parent_events = last_parent_events.get(message['job_id'], {})
|
||||
@ -216,10 +218,68 @@ class CallbackReceiver(object):
|
||||
# Retrun the job event object.
|
||||
return job_event
|
||||
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)
|
||||
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):
|
||||
messages_processed = 0
|
||||
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']))
|
||||
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):
|
||||
|
||||
def get_allowed_methods(self):
|
||||
@ -107,6 +113,7 @@ class TowerSocket(object):
|
||||
socketio_manage(environ, {'/socket.io/test': TestNamespace,
|
||||
'/socket.io/jobs': JobNamespace,
|
||||
'/socket.io/job_events': JobEventNamespace,
|
||||
'/socket.io/ad_hoc_command_events': AdHocCommandEventNamespace,
|
||||
'/socket.io/schedules': ScheduleNamespace})
|
||||
else:
|
||||
logger.warn("Invalid connect path received: " + path)
|
||||
|
||||
@ -48,6 +48,8 @@ class SimpleDAG(object):
|
||||
def short_string_obj(obj):
|
||||
if type(obj) == Job:
|
||||
type_str = "Job"
|
||||
if type(obj) == AdHocCommand:
|
||||
type_str = "AdHocCommand"
|
||||
elif type(obj) == InventoryUpdate:
|
||||
type_str = "Inventory"
|
||||
elif type(obj) == ProjectUpdate:
|
||||
@ -100,6 +102,8 @@ class SimpleDAG(object):
|
||||
def get_node_type(self, obj):
|
||||
if type(obj) == Job:
|
||||
return "job"
|
||||
elif type(obj) == AdHocCommand:
|
||||
return "ad_hoc_command"
|
||||
elif type(obj) == InventoryUpdate:
|
||||
return "inventory_update"
|
||||
elif type(obj) == ProjectUpdate:
|
||||
@ -136,13 +140,14 @@ def get_tasks():
|
||||
RELEVANT_JOBS = ('pending', 'waiting', 'running')
|
||||
# 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_ad_hoc_commands = [ahc for ahc in AdHocCommand.objects.filter(status__in=RELEVANT_JOBS)]
|
||||
graph_inventory_updates = [iu for iu in
|
||||
InventoryUpdate.objects.filter(status__in=RELEVANT_JOBS)]
|
||||
graph_project_updates = [pu for pu in
|
||||
ProjectUpdate.objects.filter(status__in=RELEVANT_JOBS)]
|
||||
graph_system_jobs = [sj for sj in
|
||||
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,
|
||||
key=lambda task: task.created)
|
||||
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.inventory 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.activity_stream 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(JobTemplate)
|
||||
activity_stream_registrar.connect(Job)
|
||||
activity_stream_registrar.connect(AdHocCommand)
|
||||
# activity_stream_registrar.connect(JobHostSummary)
|
||||
# activity_stream_registrar.connect(JobEvent)
|
||||
#activity_stream_registrar.connect(Profile)
|
||||
|
||||
@ -50,6 +50,7 @@ class ActivityStream(models.Model):
|
||||
job = models.ManyToManyField("Job", blank=True)
|
||||
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+')
|
||||
ad_hoc_command = models.ManyToManyField("AdHocCommand", blank=True)
|
||||
schedule = models.ManyToManyField("Schedule", 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
|
||||
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):
|
||||
'''
|
||||
|
||||
@ -935,6 +935,7 @@ class JobEvent(CreatedModifiedModel):
|
||||
job.inventory.update_computed_fields()
|
||||
emit_websocket_notification('/socket.io/jobs', 'summary_complete', dict(unified_job_id=job.id))
|
||||
|
||||
|
||||
class SystemJobOptions(BaseModel):
|
||||
'''
|
||||
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.
|
||||
|
||||
permission_type = models.CharField(max_length=64, choices=PERMISSION_TYPE_CHOICES)
|
||||
run_ad_hoc_commands = models.BooleanField(default=False)
|
||||
|
||||
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.user,
|
||||
self.team,
|
||||
self.project,
|
||||
self.inventory,
|
||||
self.permission_type
|
||||
self.permission_type,
|
||||
'+adhoc' if self.run_ad_hoc_commands else '',
|
||||
))
|
||||
|
||||
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)
|
||||
|
||||
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 parent_instance_set(key, val):
|
||||
|
||||
@ -42,6 +42,17 @@ def emit_job_event_detail(sender, **kwargs):
|
||||
event_serialized["event_name"] = instance.event
|
||||
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):
|
||||
logger.debug("In update inventory computed fields")
|
||||
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_delete.connect(emit_update_inventory_on_created_or_deleted, sender=Job)
|
||||
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
|
||||
# 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)
|
||||
|
||||
__all__ = ['RunJob', 'RunSystemJob', 'RunProjectUpdate', 'RunInventoryUpdate',
|
||||
'handle_work_error', 'update_inventory_computed_fields']
|
||||
'RunAdHocCommand', 'handle_work_error', 'update_inventory_computed_fields']
|
||||
|
||||
HIDDEN_PASSWORD = '**********'
|
||||
|
||||
@ -137,6 +137,9 @@ def handle_work_error(self, task_id, subtasks=None):
|
||||
elif each_task['type'] == 'job':
|
||||
instance = Job.objects.get(id=each_task['id'])
|
||||
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:
|
||||
# Unknown task type
|
||||
break
|
||||
@ -1130,6 +1133,164 @@ class RunInventoryUpdate(BaseTask):
|
||||
def get_idle_timeout(self):
|
||||
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):
|
||||
|
||||
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.scripts import * # 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.jobs 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
|
||||
# packages. It does not import any code from either package, nor does its
|
||||
# license apply to Ansible or AWX.
|
||||
@ -65,28 +65,12 @@ class TokenAuth(requests.auth.AuthBase):
|
||||
return request
|
||||
|
||||
|
||||
class CallbackModule(object):
|
||||
class BaseCallbackModule(object):
|
||||
'''
|
||||
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'))
|
||||
self.base_url = os.getenv('REST_API_URL', '')
|
||||
self.auth_token = os.getenv('REST_API_TOKEN', '')
|
||||
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):
|
||||
self.counter += 1
|
||||
msg = {
|
||||
'job_id': self.job_id,
|
||||
'event': event,
|
||||
'event_data': event_data,
|
||||
'counter': self.counter,
|
||||
'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()
|
||||
if self.job_callback_debug:
|
||||
@ -148,6 +135,7 @@ class CallbackModule(object):
|
||||
self._init_connection()
|
||||
if self.context is None:
|
||||
self._start_connection()
|
||||
|
||||
self.socket.send_json(msg)
|
||||
self.socket.recv()
|
||||
return
|
||||
@ -174,25 +162,12 @@ class CallbackModule(object):
|
||||
url = urlparse.urlunsplit([parts.scheme,
|
||||
'%s:%d' % (parts.hostname, port),
|
||||
parts.path, parts.query, parts.fragment])
|
||||
url_path = '/api/v1/jobs/%d/job_events/' % self.job_id
|
||||
url = urlparse.urljoin(url, url_path)
|
||||
url = urlparse.urljoin(url, self.rest_api_path)
|
||||
headers = {'content-type': 'application/json'}
|
||||
response = requests.post(url, data=data, headers=headers, auth=auth)
|
||||
response.raise_for_status()
|
||||
|
||||
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:
|
||||
self._post_job_event_queue_msg(event, event_data)
|
||||
else:
|
||||
@ -233,58 +208,8 @@ class CallbackModule(object):
|
||||
def runner_on_file_diff(self, host, diff):
|
||||
self._log_event('runner_on_file_diff', host=host, diff=diff)
|
||||
|
||||
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()
|
||||
|
||||
def _terminate_ssh_control_masters(self):
|
||||
@staticmethod
|
||||
def terminate_ssh_control_masters():
|
||||
# Determine if control persist is being used and if any open sockets
|
||||
# exist after running the playbook.
|
||||
cp_path = os.environ.get('ANSIBLE_SSH_CONTROL_PATH', '')
|
||||
@ -341,3 +266,116 @@ class CallbackModule(object):
|
||||
time.sleep(1)
|
||||
for proc in procs_alive:
|
||||
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
|
||||
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
|
||||
# instead (based on docs from:
|
||||
# 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 %}
|
||||
<div id="footer">
|
||||
<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>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user