Implement support for ad hoc commands.

This commit is contained in:
Chris Church 2015-03-30 13:04:19 -04:00
parent d9aa35566b
commit f7b8d510dc
26 changed files with 2530 additions and 221 deletions

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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" %}

View File

@ -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'),

View File

@ -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

View File

@ -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)

View File

@ -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:

View File

@ -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)

View File

@ -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

View 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']

View File

@ -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)

View File

@ -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)

View 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)

View File

@ -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):
'''

View File

@ -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.

View File

@ -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):

View File

@ -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):

View File

@ -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.

View File

@ -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'

View File

@ -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
View 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')

View File

@ -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

View File

@ -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)

View File

@ -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 &copy; 2014 <a href="http://www.ansible.com" target="_blank">Ansible, Inc.</a> All rights reserved.
Copyright &copy; 2015 <a href="http://www.ansible.com" target="_blank">Ansible, Inc.</a> All rights reserved.
</div>
{% endblock %}