From 9e4655419ed2622d6473f51358bb7732806f6d4b Mon Sep 17 00:00:00 2001 From: Aaron Tan Date: Tue, 15 Nov 2016 20:59:39 -0500 Subject: [PATCH] Fix flake8 E302 errors. --- awx/__init__.py | 2 + awx/api/authentication.py | 1 + awx/api/filters.py | 4 + awx/api/generics.py | 16 ++ awx/api/metadata.py | 1 + awx/api/permissions.py | 4 + awx/api/serializers.py | 29 ++- awx/api/views.py | 192 ++++++++++++++++++ awx/conf/conf.py | 1 + awx/lib/sitecustomize.py | 1 + awx/main/access.py | 37 ++++ awx/main/conf.py | 1 + awx/main/consumers.py | 1 + awx/main/fields.py | 2 + awx/main/ha.py | 1 + .../commands/cleanup_activitystream.py | 1 + .../management/commands/cleanup_authtokens.py | 1 + awx/main/management/commands/cleanup_facts.py | 5 +- awx/main/management/commands/cleanup_jobs.py | 1 + .../management/commands/list_instances.py | 1 + .../management/commands/register_instance.py | 1 + .../commands/run_callback_receiver.py | 3 +- .../commands/run_fact_cache_receiver.py | 2 + .../commands/run_socketio_service.py | 23 ++- awx/main/management/commands/stats.py | 1 + .../management/commands/update_password.py | 4 +- .../management/commands/workload_generator.py | 5 +- awx/main/managers.py | 1 + awx/main/middleware.py | 1 + awx/main/models/__init__.py | 7 + awx/main/models/ad_hoc_commands.py | 1 + awx/main/models/base.py | 1 + awx/main/models/channels.py | 1 + awx/main/models/fact.py | 5 +- awx/main/models/ha.py | 2 + awx/main/models/jobs.py | 3 + awx/main/models/label.py | 2 +- awx/main/models/mixins.py | 2 +- awx/main/models/notifications.py | 4 +- awx/main/models/organization.py | 3 +- awx/main/models/projects.py | 1 + awx/main/models/rbac.py | 2 + awx/main/models/unified_jobs.py | 2 + awx/main/models/workflow.py | 12 +- awx/main/redact.py | 2 + awx/main/scheduler/__init__.py | 1 + awx/main/scheduler/dag_simple.py | 1 + awx/main/scheduler/dag_workflow.py | 1 + awx/main/scheduler/dependency_graph.py | 2 + awx/main/scheduler/partial.py | 10 + awx/main/scheduler/tasks.py | 4 + awx/main/signals.py | 20 ++ awx/main/tasks.py | 11 + awx/main/tests/URI.py | 2 + awx/main/tests/base.py | 8 + awx/main/tests/conftest.py | 10 + awx/main/tests/factories/fixtures.py | 5 + awx/main/tests/factories/objects.py | 1 + awx/main/tests/factories/tower.py | 8 + .../functional/api/test_activity_streams.py | 10 + awx/main/tests/functional/api/test_adhoc.py | 11 +- .../api/test_create_attach_views.py | 2 + .../tests/functional/api/test_credential.py | 34 +++- .../functional/api/test_fact_versions.py | 23 ++- .../tests/functional/api/test_fact_view.py | 20 +- .../tests/functional/api/test_host_detail.py | 1 + .../tests/functional/api/test_inventory.py | 4 + .../functional/api/test_job_runtime_params.py | 20 ++ .../tests/functional/api/test_job_template.py | 15 +- .../api/test_organization_counts.py | 10 + .../functional/api/test_rbac_displays.py | 11 +- .../api/test_resource_access_lists.py | 1 + awx/main/tests/functional/api/test_role.py | 1 + .../tests/functional/api/test_settings.py | 3 +- .../tests/functional/api/test_survey_spec.py | 17 ++ .../functional/api/test_unified_jobs_view.py | 6 +- awx/main/tests/functional/api/test_user.py | 3 + .../tests/functional/commands/conftest.py | 8 +- .../functional/commands/test_cleanup_facts.py | 10 +- .../functional/commands/test_commands.py | 1 + .../commands/test_run_fact_cache_receiver.py | 5 + awx/main/tests/functional/conftest.py | 51 ++++- .../tests/functional/core/test_licenses.py | 2 + .../models/fact/test_get_host_fact.py | 2 +- .../models/fact/test_get_timeline.py | 12 +- .../tests/functional/models/test_workflow.py | 5 +- .../tests/functional/test_db_credential.py | 1 + .../functional/test_fixture_factories.py | 3 + awx/main/tests/functional/test_jobs.py | 2 + .../tests/functional/test_notifications.py | 8 + awx/main/tests/functional/test_partial.py | 3 + awx/main/tests/functional/test_projects.py | 6 + awx/main/tests/functional/test_rbac_api.py | 30 ++- awx/main/tests/functional/test_rbac_core.py | 4 +- .../tests/functional/test_rbac_credential.py | 19 ++ .../tests/functional/test_rbac_inventory.py | 8 +- awx/main/tests/functional/test_rbac_job.py | 14 ++ .../tests/functional/test_rbac_job_start.py | 9 +- .../functional/test_rbac_job_templates.py | 9 + awx/main/tests/functional/test_rbac_label.py | 6 + .../functional/test_rbac_notifications.py | 14 ++ .../functional/test_rbac_organization.py | 1 + .../tests/functional/test_rbac_project.py | 9 + awx/main/tests/functional/test_rbac_role.py | 2 + awx/main/tests/functional/test_rbac_team.py | 6 + awx/main/tests/functional/test_rbac_user.py | 7 + .../tests/functional/test_rbac_workflow.py | 6 + awx/main/tests/job_base.py | 1 + awx/main/tests/manual/workflows/linear.py | 2 + awx/main/tests/manual/workflows/parallel.py | 2 + awx/main/tests/old/ad_hoc.py | 2 + .../tests/old/commands/commands_monolithic.py | 3 + awx/main/tests/old/inventory.py | 1 + awx/main/tests/old/jobs/job_launch.py | 2 + awx/main/tests/old/jobs/job_relaunch.py | 1 + awx/main/tests/old/jobs/jobs_monolithic.py | 4 + awx/main/tests/old/jobs/start_cancel.py | 1 + awx/main/tests/old/jobs/survey_password.py | 2 + awx/main/tests/old/projects.py | 1 + awx/main/tests/old/schedules.py | 2 + awx/main/tests/old/scripts.py | 2 + awx/main/tests/old/settings.py | 1 + awx/main/tests/old/tasks.py | 1 + awx/main/tests/old/users.py | 2 + .../tests/unit/api/serializers/conftest.py | 8 +- .../serializers/test_inventory_serializers.py | 2 +- .../api/serializers/test_job_serializers.py | 9 +- .../test_job_template_serializers.py | 9 +- .../serializers/test_workflow_serializers.py | 8 +- awx/main/tests/unit/api/test_filters.py | 4 + awx/main/tests/unit/api/test_generics.py | 10 +- awx/main/tests/unit/api/test_roles.py | 3 + awx/main/tests/unit/api/test_views.py | 3 + .../unit/models/test_job_template_unit.py | 5 + awx/main/tests/unit/models/test_label.py | 3 +- .../tests/unit/models/test_survey_models.py | 5 + .../tests/unit/models/test_workflow_unit.py | 17 +- awx/main/tests/unit/scheduler/conftest.py | 21 +- awx/main/tests/unit/scheduler/test_dag.py | 12 +- .../unit/scheduler/test_dependency_graph.py | 50 ++--- .../test_scheduler_inventory_update.py | 8 +- .../unit/scheduler/test_scheduler_job.py | 4 +- .../test_scheduler_project_update.py | 4 +- awx/main/tests/unit/settings/test_defaults.py | 1 + awx/main/tests/unit/test_access.py | 16 +- awx/main/tests/unit/test_ha.py | 2 + .../tests/unit/test_python_requirements.py | 4 +- awx/main/tests/unit/test_redact.py | 5 +- awx/main/tests/unit/test_settings.py | 1 + awx/main/tests/unit/test_signals.py | 3 +- awx/main/tests/unit/test_tasks.py | 5 + awx/main/tests/unit/test_unified_jobs.py | 2 + awx/main/tests/unit/test_validators.py | 9 +- awx/main/utils.py | 24 ++- awx/main/validators.py | 1 + awx/plugins/fact_caching/tower.py | 1 + awx/plugins/library/scan_files.py | 1 + awx/plugins/library/scan_packages.py | 3 + awx/plugins/library/scan_services.py | 4 + awx/settings/defaults.py | 2 + awx/sso/__init__.py | 1 + awx/sso/conf.py | 1 + awx/sso/views.py | 1 + setup.py | 1 + .../rbac_dummy_data_generator.py | 1 + 165 files changed, 1117 insertions(+), 119 deletions(-) diff --git a/awx/__init__.py b/awx/__init__.py index 45abf0e7fa..f5457b5caa 100644 --- a/awx/__init__.py +++ b/awx/__init__.py @@ -17,6 +17,7 @@ try: except ImportError: # pragma: no cover MODE = 'production' + def find_commands(management_dir): # Modified version of function from django/core/management/__init__.py. command_dir = os.path.join(management_dir, 'commands') @@ -33,6 +34,7 @@ def find_commands(management_dir): pass return commands + def prepare_env(): # Update the default settings environment variable based on current mode. os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'awx.settings.%s' % MODE) diff --git a/awx/api/authentication.py b/awx/api/authentication.py index f00950ed85..5f410c8b11 100644 --- a/awx/api/authentication.py +++ b/awx/api/authentication.py @@ -21,6 +21,7 @@ from awx.main.models import UnifiedJob, AuthToken logger = logging.getLogger('awx.api.authentication') + class TokenAuthentication(authentication.TokenAuthentication): ''' Custom token authentication using tokens that expire and are associated diff --git a/awx/api/filters.py b/awx/api/filters.py index 08a26735d2..d861303f1e 100644 --- a/awx/api/filters.py +++ b/awx/api/filters.py @@ -20,12 +20,14 @@ from rest_framework.filters import BaseFilterBackend # Ansible Tower from awx.main.utils import get_type_for_model, to_python_boolean + class MongoFilterBackend(BaseFilterBackend): # FIX: Note that MongoEngine can't use the filter backends from DRF def filter_queryset(self, request, queryset, view): return queryset + class TypeFilterBackend(BaseFilterBackend): ''' Filter on type field now returned with all objects. @@ -62,6 +64,7 @@ class TypeFilterBackend(BaseFilterBackend): # Return a 400 for invalid field names. raise ParseError(*e.args) + class FieldLookupBackend(BaseFilterBackend): ''' Filter using field lookups provided via query string parameters. @@ -229,6 +232,7 @@ class FieldLookupBackend(BaseFilterBackend): except ValidationError as e: raise ParseError(e.messages) + class OrderByBackend(BaseFilterBackend): ''' Filter to apply ordering based on query string parameters. diff --git a/awx/api/generics.py b/awx/api/generics.py index 6329b3fd62..652e5e0588 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -42,6 +42,7 @@ __all__ = ['APIView', 'GenericAPIView', 'ListAPIView', 'SimpleListAPIView', logger = logging.getLogger('awx.api.generics') + def get_view_name(cls, suffix=None): ''' Wrapper around REST framework get_view_name() to support get_name() method @@ -59,6 +60,7 @@ def get_view_name(cls, suffix=None): return ('%s %s' % (name, suffix)) if suffix else name return views.get_view_name(cls, suffix=None) + def get_view_description(cls, html=False): ''' Wrapper around REST framework get_view_description() to support @@ -78,6 +80,7 @@ def get_view_description(cls, html=False): desc = '
%s
' % desc return mark_safe(desc) + class APIView(views.APIView): def initialize_request(self, request, *args, **kwargs): @@ -227,11 +230,13 @@ class GenericAPIView(generics.GenericAPIView, APIView): d['settings'] = settings return d + class SimpleListAPIView(generics.ListAPIView, GenericAPIView): def get_queryset(self): return self.request.user.get_queryset(self.model) + class ListAPIView(generics.ListAPIView, GenericAPIView): # Base class for a read-only list view. @@ -266,10 +271,12 @@ class ListAPIView(generics.ListAPIView, GenericAPIView): fields.append(field.name) return fields + class ListCreateAPIView(ListAPIView, generics.ListCreateAPIView): # Base class for a list view that allows creating new objects. pass + class ParentMixin(object): def get_parent_object(self): @@ -288,6 +295,7 @@ class ParentMixin(object): if not self.request.user.can_access(*args): raise PermissionDenied() + class SubListAPIView(ListAPIView, ParentMixin): # Base class for a read-only sublist view. @@ -315,6 +323,7 @@ class SubListAPIView(ListAPIView, ParentMixin): sublist_qs = getattrd(parent, self.relationship).distinct() return qs & sublist_qs + class SubListCreateAPIView(SubListAPIView, ListCreateAPIView): # Base class for a sublist view that allows for creating subobjects # associated with the parent object. @@ -367,6 +376,7 @@ class SubListCreateAPIView(SubListAPIView, ListCreateAPIView): headers = {'Location': obj.get_absolute_url()} return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + class SubListCreateAttachDetachAPIView(SubListCreateAPIView): # Base class for a sublist view that allows for creating subobjects and # attaching/detaching them from the parent. @@ -490,12 +500,15 @@ class DeleteLastUnattachLabelMixin(object): return res + class SubDetailAPIView(generics.RetrieveAPIView, GenericAPIView, ParentMixin): pass + class RetrieveAPIView(generics.RetrieveAPIView, GenericAPIView): pass + class RetrieveUpdateAPIView(RetrieveAPIView, generics.RetrieveUpdateAPIView): def update(self, request, *args, **kwargs): @@ -510,6 +523,7 @@ class RetrieveUpdateAPIView(RetrieveAPIView, generics.RetrieveUpdateAPIView): ''' scrub any fields the user cannot/should not put/patch, based on user context. This runs after read-only serialization filtering ''' pass + class RetrieveDestroyAPIView(RetrieveAPIView, generics.RetrieveDestroyAPIView): def destroy(self, request, *args, **kwargs): @@ -520,9 +534,11 @@ class RetrieveDestroyAPIView(RetrieveAPIView, generics.RetrieveDestroyAPIView): obj.delete() return Response(status=status.HTTP_204_NO_CONTENT) + class RetrieveUpdateDestroyAPIView(RetrieveUpdateAPIView, RetrieveDestroyAPIView): pass + class DestroyAPIView(GenericAPIView, generics.DestroyAPIView): pass diff --git a/awx/api/metadata.py b/awx/api/metadata.py index 54850f285e..6dd186c9ef 100644 --- a/awx/api/metadata.py +++ b/awx/api/metadata.py @@ -184,6 +184,7 @@ class Metadata(metadata.SimpleMetadata): return metadata + class RoleMetadata(Metadata): def determine_metadata(self, request, view): metadata = super(RoleMetadata, self).determine_metadata(request, view) diff --git a/awx/api/permissions.py b/awx/api/permissions.py index cda66ff2ec..a655360dc8 100644 --- a/awx/api/permissions.py +++ b/awx/api/permissions.py @@ -21,6 +21,7 @@ logger = logging.getLogger('awx.api.permissions') __all__ = ['ModelAccessPermission', 'JobTemplateCallbackPermission', 'TaskPermission', 'ProjectUpdatePermission', 'UserPermission'] + class ModelAccessPermission(permissions.BasePermission): ''' Default permissions class to check user access based on the model and @@ -140,6 +141,7 @@ class ModelAccessPermission(permissions.BasePermission): def has_object_permission(self, request, view, obj): return self.has_permission(request, view, obj) + class JobTemplateCallbackPermission(ModelAccessPermission): ''' Permission check used by job template callback view for requests from @@ -165,6 +167,7 @@ class JobTemplateCallbackPermission(ModelAccessPermission): else: return True + class TaskPermission(ModelAccessPermission): ''' Permission checks used for API callbacks from running a task. @@ -192,6 +195,7 @@ class TaskPermission(ModelAccessPermission): else: return False + class ProjectUpdatePermission(ModelAccessPermission): ''' Permission check used by ProjectUpdateView to determine who can update projects diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 0f4fb338b1..434feb6d0c 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -496,6 +496,7 @@ class BaseSerializer(serializers.ModelSerializer): class EmptySerializer(serializers.Serializer): pass + class BaseFactSerializer(BaseSerializer): __metaclass__ = BaseSerializerMetaclass @@ -509,6 +510,7 @@ class BaseFactSerializer(BaseSerializer): ret['module'] = serializers.ChoiceField(choices=choices, read_only=True, required=False) return ret + class UnifiedJobTemplateSerializer(BaseSerializer): class Meta: @@ -1496,7 +1498,6 @@ class TeamSerializer(BaseSerializer): return ret - class RoleSerializer(BaseSerializer): class Meta: @@ -1804,6 +1805,7 @@ class LabelsListMixin(object): res['labels'] = self._summary_field_labels(obj) return res + class JobOptionsSerializer(LabelsListMixin, BaseSerializer): class Meta: @@ -2068,6 +2070,7 @@ class JobRelaunchSerializer(JobSerializer): attrs = super(JobRelaunchSerializer, self).validate(attrs) return attrs + class AdHocCommandSerializer(UnifiedJobSerializer): class Meta: @@ -2168,6 +2171,7 @@ class SystemJobTemplateSerializer(UnifiedJobTemplateSerializer): )) return res + class SystemJobSerializer(UnifiedJobSerializer): class Meta: @@ -2184,6 +2188,7 @@ class SystemJobSerializer(UnifiedJobSerializer): res['cancel'] = reverse('api:system_job_cancel', args=(obj.pk,)) return res + class SystemJobCancelSerializer(SystemJobSerializer): can_cancel = serializers.BooleanField(read_only=True) @@ -2191,6 +2196,7 @@ class SystemJobCancelSerializer(SystemJobSerializer): class Meta: fields = ('can_cancel',) + class WorkflowJobTemplateSerializer(LabelsListMixin, UnifiedJobTemplateSerializer): show_capabilities = ['start', 'edit', 'delete'] @@ -2216,10 +2222,12 @@ class WorkflowJobTemplateSerializer(LabelsListMixin, UnifiedJobTemplateSerialize def validate_extra_vars(self, value): return vars_validate_or_raise(value) + # TODO: class WorkflowJobTemplateListSerializer(WorkflowJobTemplateSerializer): pass + # TODO: class WorkflowJobSerializer(LabelsListMixin, UnifiedJobSerializer): @@ -2247,10 +2255,12 @@ class WorkflowJobSerializer(LabelsListMixin, UnifiedJobSerializer): ret['extra_vars'] = obj.display_extra_vars() return ret + # TODO: class WorkflowJobListSerializer(WorkflowJobSerializer, UnifiedJobListSerializer): pass + class WorkflowJobCancelSerializer(WorkflowJobSerializer): can_cancel = serializers.BooleanField(read_only=True) @@ -2346,6 +2356,7 @@ class WorkflowJobTemplateNodeSerializer(WorkflowNodeBaseSerializer): "unified_job_template": _("Can not nest a %s inside a WorkflowJobTemplate") % ujt_obj.__class__.__name__}) return super(WorkflowJobTemplateNodeSerializer, self).validate(attrs) + class WorkflowJobNodeSerializer(WorkflowNodeBaseSerializer): class Meta: model = WorkflowJobNode @@ -2362,14 +2373,16 @@ class WorkflowJobNodeSerializer(WorkflowNodeBaseSerializer): res['workflow_job'] = reverse('api:workflow_job_detail', args=(obj.workflow_job.pk,)) return res + class WorkflowJobNodeListSerializer(WorkflowJobNodeSerializer): pass + class WorkflowJobNodeDetailSerializer(WorkflowJobNodeSerializer): pass -class WorkflowJobTemplateNodeDetailSerializer(WorkflowJobTemplateNodeSerializer): +class WorkflowJobTemplateNodeDetailSerializer(WorkflowJobTemplateNodeSerializer): ''' Influence the api browser sample data to not include workflow_job_template when editing a WorkflowNode. @@ -2384,18 +2397,23 @@ class WorkflowJobTemplateNodeDetailSerializer(WorkflowJobTemplateNodeSerializer) field_kwargs.pop('queryset', None) return field_class, field_kwargs + class WorkflowJobTemplateNodeListSerializer(WorkflowJobTemplateNodeSerializer): pass + class JobListSerializer(JobSerializer, UnifiedJobListSerializer): pass + class AdHocCommandListSerializer(AdHocCommandSerializer, UnifiedJobListSerializer): pass + class SystemJobListSerializer(SystemJobSerializer, UnifiedJobListSerializer): pass + class JobHostSummarySerializer(BaseSerializer): class Meta: @@ -2610,6 +2628,7 @@ class JobLaunchSerializer(BaseSerializer): obj.credential = JT_credential return attrs + class WorkflowJobLaunchSerializer(BaseSerializer): can_start_without_user_input = serializers.BooleanField(read_only=True) @@ -2668,6 +2687,7 @@ class WorkflowJobLaunchSerializer(BaseSerializer): obj.extra_vars = WFJT_extra_vars return attrs + class NotificationTemplateSerializer(BaseSerializer): show_capabilities = ['edit', 'delete'] @@ -2757,6 +2777,7 @@ class NotificationTemplateSerializer(BaseSerializer): raise serializers.ValidationError(error_list) return attrs + class NotificationSerializer(BaseSerializer): class Meta: @@ -2771,6 +2792,7 @@ class NotificationSerializer(BaseSerializer): )) return res + class LabelSerializer(BaseSerializer): class Meta: @@ -2783,6 +2805,7 @@ class LabelSerializer(BaseSerializer): res['organization'] = reverse('api:organization_detail', args=(obj.organization.pk,)) return res + class ScheduleSerializer(BaseSerializer): show_capabilities = ['edit', 'delete'] @@ -2856,6 +2879,7 @@ class ScheduleSerializer(BaseSerializer): raise serializers.ValidationError(_("rrule parsing failed validation.")) return value + class ActivityStreamSerializer(BaseSerializer): changes = serializers.SerializerMethodField() @@ -2999,6 +3023,7 @@ class FactVersionSerializer(BaseFactSerializer): res['fact_view'] = build_url('api:host_fact_compare_view', args=(obj.host.pk,), get=params) return res + class FactSerializer(BaseFactSerializer): class Meta: diff --git a/awx/api/views.py b/awx/api/views.py index ee219894f8..44d8f8b419 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -76,6 +76,7 @@ from awx.main.scheduler.tasks import run_job_complete logger = logging.getLogger('awx.api.views') + def api_exception_handler(exc, context): ''' Override default API exception handler to catch IntegrityError exceptions. @@ -105,6 +106,7 @@ class ApiRootView(APIView): ) return Response(data) + class ApiV1RootView(APIView): authentication_classes = [] @@ -386,6 +388,7 @@ class DashboardView(APIView): 'total': job_template_list.count()} return Response(data) + class DashboardJobsGraphView(APIView): view_name = _("Dashboard Jobs Graphs") @@ -443,12 +446,14 @@ class ScheduleList(ListAPIView): serializer_class = ScheduleSerializer new_in_148 = True + class ScheduleDetail(RetrieveUpdateDestroyAPIView): model = Schedule serializer_class = ScheduleSerializer new_in_148 = True + class ScheduleUnifiedJobsList(SubListAPIView): model = UnifiedJob @@ -458,6 +463,7 @@ class ScheduleUnifiedJobsList(SubListAPIView): view_name = _('Schedule Jobs List') new_in_148 = True + class AuthView(APIView): authentication_classes = [] @@ -497,6 +503,7 @@ class AuthView(APIView): data[name] = backend_data return Response(data) + class AuthTokenView(APIView): authentication_classes = [] @@ -554,6 +561,7 @@ class AuthTokenView(APIView): logger.warning(smart_text(u"Login failed for user {}".format(request.data['username']))) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + class OrganizationCountsMixin(object): def get_serializer_context(self, *args, **kwargs): @@ -635,6 +643,7 @@ class OrganizationCountsMixin(object): return full_context + class OrganizationList(OrganizationCountsMixin, ListCreateAPIView): model = Organization @@ -663,6 +672,7 @@ class OrganizationList(OrganizationCountsMixin, ListCreateAPIView): # Okay, create the organization as usual. return super(OrganizationList, self).create(request, *args, **kwargs) + class OrganizationDetail(RetrieveUpdateDestroyAPIView): model = Organization @@ -702,6 +712,7 @@ class OrganizationDetail(RetrieveUpdateDestroyAPIView): return full_context + class OrganizationInventoriesList(SubListAPIView): model = Inventory @@ -726,6 +737,7 @@ class BaseUsersList(SubListCreateAttachDetachAPIView): pass return ret + class OrganizationUsersList(BaseUsersList): model = User @@ -733,6 +745,7 @@ class OrganizationUsersList(BaseUsersList): parent_model = Organization relationship = 'member_role.members' + class OrganizationAdminsList(BaseUsersList): model = User @@ -740,6 +753,7 @@ class OrganizationAdminsList(BaseUsersList): parent_model = Organization relationship = 'admin_role.members' + class OrganizationProjectsList(SubListCreateAttachDetachAPIView): model = Project @@ -748,6 +762,7 @@ class OrganizationProjectsList(SubListCreateAttachDetachAPIView): relationship = 'projects' parent_key = 'organization' + class OrganizationTeamsList(SubListCreateAttachDetachAPIView): model = Team @@ -756,6 +771,7 @@ class OrganizationTeamsList(SubListCreateAttachDetachAPIView): relationship = 'teams' parent_key = 'organization' + class OrganizationActivityStreamList(SubListAPIView): model = ActivityStream @@ -774,6 +790,7 @@ class OrganizationActivityStreamList(SubListAPIView): # Okay, let it through. return super(OrganizationActivityStreamList, self).get(request, *args, **kwargs) + class OrganizationNotificationTemplatesList(SubListCreateAttachDetachAPIView): model = NotificationTemplate @@ -782,6 +799,7 @@ class OrganizationNotificationTemplatesList(SubListCreateAttachDetachAPIView): relationship = 'notification_templates' parent_key = 'organization' + class OrganizationNotificationTemplatesAnyList(SubListCreateAttachDetachAPIView): model = NotificationTemplate @@ -789,6 +807,7 @@ class OrganizationNotificationTemplatesAnyList(SubListCreateAttachDetachAPIView) parent_model = Organization relationship = 'notification_templates_any' + class OrganizationNotificationTemplatesErrorList(SubListCreateAttachDetachAPIView): model = NotificationTemplate @@ -796,6 +815,7 @@ class OrganizationNotificationTemplatesErrorList(SubListCreateAttachDetachAPIVie parent_model = Organization relationship = 'notification_templates_error' + class OrganizationNotificationTemplatesSuccessList(SubListCreateAttachDetachAPIView): model = NotificationTemplate @@ -803,12 +823,14 @@ class OrganizationNotificationTemplatesSuccessList(SubListCreateAttachDetachAPIV parent_model = Organization relationship = 'notification_templates_success' + class OrganizationAccessList(ResourceAccessList): model = User # needs to be User for AccessLists's resource_model = Organization new_in_300 = True + class OrganizationObjectRolesList(SubListAPIView): model = Role @@ -821,6 +843,7 @@ class OrganizationObjectRolesList(SubListAPIView): content_type = ContentType.objects.get_for_model(self.parent_model) return Role.objects.filter(content_type=content_type, object_id=po.pk) + class TeamList(ListCreateAPIView): model = Team @@ -831,11 +854,13 @@ class TeamList(ListCreateAPIView): qs = qs.select_related('admin_role', 'read_role', 'member_role', 'organization') return qs + class TeamDetail(RetrieveUpdateDestroyAPIView): model = Team serializer_class = TeamSerializer + class TeamUsersList(BaseUsersList): model = User @@ -880,6 +905,7 @@ class TeamRolesList(SubListCreateAttachDetachAPIView): return super(TeamRolesList, self).post(request, *args, **kwargs) + class TeamObjectRolesList(SubListAPIView): model = Role @@ -892,6 +918,7 @@ class TeamObjectRolesList(SubListAPIView): content_type = ContentType.objects.get_for_model(self.parent_model) return Role.objects.filter(content_type=content_type, object_id=po.pk) + class TeamProjectsList(SubListAPIView): model = Project @@ -909,6 +936,7 @@ class TeamProjectsList(SubListAPIView): ) return self.model.accessible_objects(self.request.user, 'read_role').filter(pk__in=[t.content_object.pk for t in proj_roles]) + class TeamActivityStreamList(SubListAPIView): model = ActivityStream @@ -936,12 +964,14 @@ class TeamActivityStreamList(SubListAPIView): Q(project__in=Project.accessible_objects(parent, 'read_role')) | Q(credential__in=Credential.accessible_objects(parent, 'read_role'))) + class TeamAccessList(ResourceAccessList): model = User # needs to be User for AccessLists's resource_model = Team new_in_300 = True + class ProjectList(ListCreateAPIView): model = Project @@ -959,6 +989,7 @@ class ProjectList(ListCreateAPIView): ) return projects_qs + class ProjectDetail(RetrieveUpdateDestroyAPIView): model = Project @@ -973,11 +1004,13 @@ class ProjectDetail(RetrieveUpdateDestroyAPIView): pu.cancel() return super(ProjectDetail, self).destroy(request, *args, **kwargs) + class ProjectPlaybooks(RetrieveAPIView): model = Project serializer_class = ProjectPlaybooksSerializer + class ProjectTeamsList(ListAPIView): model = Team @@ -992,6 +1025,7 @@ class ProjectTeamsList(ListAPIView): all_roles = Role.objects.filter(Q(descendents__content_type=project_ct) & Q(descendents__object_id=p.pk), content_type=team_ct) return self.model.accessible_objects(self.request.user, 'read_role').filter(pk__in=[t.content_object.pk for t in all_roles]) + class ProjectSchedulesList(SubListCreateAttachDetachAPIView): view_name = _("Project Schedules") @@ -1003,6 +1037,7 @@ class ProjectSchedulesList(SubListCreateAttachDetachAPIView): parent_key = 'unified_job_template' new_in_148 = True + class ProjectActivityStreamList(SubListAPIView): model = ActivityStream @@ -1031,6 +1066,7 @@ class ProjectActivityStreamList(SubListAPIView): return qs.filter(project=parent) return qs.filter(Q(project=parent) | Q(credential=parent.credential)) + class ProjectNotificationTemplatesAnyList(SubListCreateAttachDetachAPIView): model = NotificationTemplate @@ -1038,6 +1074,7 @@ class ProjectNotificationTemplatesAnyList(SubListCreateAttachDetachAPIView): parent_model = Project relationship = 'notification_templates_any' + class ProjectNotificationTemplatesErrorList(SubListCreateAttachDetachAPIView): model = NotificationTemplate @@ -1045,6 +1082,7 @@ class ProjectNotificationTemplatesErrorList(SubListCreateAttachDetachAPIView): parent_model = Project relationship = 'notification_templates_error' + class ProjectNotificationTemplatesSuccessList(SubListCreateAttachDetachAPIView): model = NotificationTemplate @@ -1052,6 +1090,7 @@ class ProjectNotificationTemplatesSuccessList(SubListCreateAttachDetachAPIView): parent_model = Project relationship = 'notification_templates_success' + class ProjectUpdatesList(SubListAPIView): model = ProjectUpdate @@ -1060,6 +1099,7 @@ class ProjectUpdatesList(SubListAPIView): relationship = 'project_updates' new_in_13 = True + class ProjectUpdateView(RetrieveAPIView): model = Project @@ -1081,11 +1121,13 @@ class ProjectUpdateView(RetrieveAPIView): else: return self.http_method_not_allowed(request, *args, **kwargs) + class ProjectUpdateList(ListAPIView): model = ProjectUpdate serializer_class = ProjectUpdateListSerializer + class ProjectUpdateDetail(RetrieveDestroyAPIView): model = ProjectUpdate @@ -1098,6 +1140,7 @@ class ProjectUpdateDetail(RetrieveDestroyAPIView): raise PermissionDenied(detail=_('Can not delete job resource when associated workflow job is running.')) return super(ProjectUpdateDetail, self).destroy(request, *args, **kwargs) + class ProjectUpdateCancel(RetrieveAPIView): model = ProjectUpdate @@ -1113,6 +1156,7 @@ class ProjectUpdateCancel(RetrieveAPIView): else: return self.http_method_not_allowed(request, *args, **kwargs) + class ProjectUpdateNotificationsList(SubListAPIView): model = Notification @@ -1120,12 +1164,14 @@ class ProjectUpdateNotificationsList(SubListAPIView): parent_model = ProjectUpdate relationship = 'notifications' + class ProjectAccessList(ResourceAccessList): model = User # needs to be User for AccessLists's resource_model = Project new_in_300 = True + class ProjectObjectRolesList(SubListAPIView): model = Role @@ -1138,6 +1184,7 @@ class ProjectObjectRolesList(SubListAPIView): content_type = ContentType.objects.get_for_model(self.parent_model) return Role.objects.filter(content_type=content_type, object_id=po.pk) + class UserList(ListCreateAPIView): model = User @@ -1159,6 +1206,7 @@ class UserList(ListCreateAPIView): pass return ret + class UserMeList(ListAPIView): model = User @@ -1168,6 +1216,7 @@ class UserMeList(ListAPIView): def get_queryset(self): return self.model.objects.filter(pk=self.request.user.pk) + class UserTeamsList(ListAPIView): model = User @@ -1179,6 +1228,7 @@ class UserTeamsList(ListAPIView): raise PermissionDenied() return Team.accessible_objects(self.request.user, 'read_role').filter(member_role__members=u) + class UserRolesList(SubListCreateAttachDetachAPIView): model = Role @@ -1230,6 +1280,7 @@ class UserRolesList(SubListCreateAttachDetachAPIView): # We hide roles that shouldn't be seen in our queryset return True + class UserProjectsList(SubListAPIView): model = Project @@ -1243,6 +1294,7 @@ class UserProjectsList(SubListAPIView): user_qs = Project.accessible_objects(parent, 'read_role') return my_qs & user_qs + class UserOrganizationsList(OrganizationCountsMixin, SubListAPIView): model = Organization @@ -1257,6 +1309,7 @@ class UserOrganizationsList(OrganizationCountsMixin, SubListAPIView): user_qs = Organization.objects.filter(member_role__members=parent) return my_qs & user_qs + class UserAdminOfOrganizationsList(OrganizationCountsMixin, SubListAPIView): model = Organization @@ -1271,6 +1324,7 @@ class UserAdminOfOrganizationsList(OrganizationCountsMixin, SubListAPIView): user_qs = Organization.objects.filter(admin_role__members=parent) return my_qs & user_qs + class UserActivityStreamList(SubListAPIView): model = ActivityStream @@ -1333,6 +1387,7 @@ class UserDetail(RetrieveUpdateDestroyAPIView): raise PermissionDenied(_('Cannot delete user.')) return super(UserDetail, self).destroy(request, *args, **kwargs) + class UserAccessList(ResourceAccessList): model = User # needs to be User for AccessLists's @@ -1430,6 +1485,7 @@ class CredentialDetail(RetrieveUpdateDestroyAPIView): model = Credential serializer_class = CredentialSerializer + class CredentialActivityStreamList(SubListAPIView): model = ActivityStream @@ -1448,12 +1504,14 @@ class CredentialActivityStreamList(SubListAPIView): # Okay, let it through. return super(CredentialActivityStreamList, self).get(request, *args, **kwargs) + class CredentialAccessList(ResourceAccessList): model = User # needs to be User for AccessLists's resource_model = Credential new_in_300 = True + class CredentialObjectRolesList(SubListAPIView): model = Role @@ -1466,11 +1524,13 @@ class CredentialObjectRolesList(SubListAPIView): content_type = ContentType.objects.get_for_model(self.parent_model) return Role.objects.filter(content_type=content_type, object_id=po.pk) + class InventoryScriptList(ListCreateAPIView): model = CustomInventoryScript serializer_class = CustomInventoryScriptSerializer + class InventoryScriptDetail(RetrieveUpdateDestroyAPIView): model = CustomInventoryScript @@ -1486,6 +1546,7 @@ class InventoryScriptDetail(RetrieveUpdateDestroyAPIView): inv_src.save() return super(InventoryScriptDetail, self).destroy(request, *args, **kwargs) + class InventoryScriptObjectRolesList(SubListAPIView): model = Role @@ -1498,6 +1559,7 @@ class InventoryScriptObjectRolesList(SubListAPIView): content_type = ContentType.objects.get_for_model(self.parent_model) return Role.objects.filter(content_type=content_type, object_id=po.pk) + class InventoryList(ListCreateAPIView): model = Inventory @@ -1509,6 +1571,7 @@ class InventoryList(ListCreateAPIView): qs = qs.select_related('admin_role', 'read_role', 'update_role', 'use_role', 'adhoc_role') return qs + class InventoryDetail(RetrieveUpdateDestroyAPIView): model = Inventory @@ -1519,6 +1582,7 @@ class InventoryDetail(RetrieveUpdateDestroyAPIView): with ignore_inventory_group_removal(): return super(InventoryDetail, self).destroy(request, *args, **kwargs) + class InventoryActivityStreamList(SubListAPIView): model = ActivityStream @@ -1543,12 +1607,14 @@ class InventoryActivityStreamList(SubListAPIView): qs = self.request.user.get_queryset(self.model) return qs.filter(Q(inventory=parent) | Q(host__in=parent.hosts.all()) | Q(group__in=parent.groups.all())) + class InventoryAccessList(ResourceAccessList): model = User # needs to be User for AccessLists's resource_model = Inventory new_in_300 = True + class InventoryObjectRolesList(SubListAPIView): model = Role @@ -1561,6 +1627,7 @@ class InventoryObjectRolesList(SubListAPIView): content_type = ContentType.objects.get_for_model(self.parent_model) return Role.objects.filter(content_type=content_type, object_id=po.pk) + class InventoryJobTemplateList(SubListAPIView): model = JobTemplate @@ -1575,6 +1642,7 @@ class InventoryJobTemplateList(SubListAPIView): qs = self.request.user.get_queryset(self.model) return qs.filter(inventory=parent) + class InventoryScanJobTemplateList(SubListAPIView): model = JobTemplate @@ -1589,17 +1657,20 @@ class InventoryScanJobTemplateList(SubListAPIView): qs = self.request.user.get_queryset(self.model) return qs.filter(job_type=PERM_INVENTORY_SCAN, inventory=parent) + class HostList(ListCreateAPIView): always_allow_superuser = False model = Host serializer_class = HostSerializer + class HostDetail(RetrieveUpdateDestroyAPIView): model = Host serializer_class = HostSerializer + class InventoryHostsList(SubListCreateAttachDetachAPIView): model = Host @@ -1608,6 +1679,7 @@ class InventoryHostsList(SubListCreateAttachDetachAPIView): relationship = 'hosts' parent_key = 'inventory' + class HostGroupsList(SubListCreateAttachDetachAPIView): ''' the list of groups a host is directly a member of ''' @@ -1629,6 +1701,7 @@ class HostGroupsList(SubListCreateAttachDetachAPIView): data['inventory'] = self.get_parent_object().inventory_id return super(HostGroupsList, self).create(request, *args, **kwargs) + class HostAllGroupsList(SubListAPIView): ''' the list of all groups of which the host is directly or indirectly a member ''' @@ -1644,6 +1717,7 @@ class HostAllGroupsList(SubListAPIView): sublist_qs = parent.all_groups.distinct() return qs & sublist_qs + class HostInventorySourcesList(SubListAPIView): model = InventorySource @@ -1652,6 +1726,7 @@ class HostInventorySourcesList(SubListAPIView): relationship = 'inventory_sources' new_in_148 = True + class HostActivityStreamList(SubListAPIView): model = ActivityStream @@ -1676,6 +1751,7 @@ class HostActivityStreamList(SubListAPIView): qs = self.request.user.get_queryset(self.model) return qs.filter(Q(host=parent) | Q(inventory=parent.inventory)) + class SystemTrackingEnforcementMixin(APIView): ''' Use check_permissions instead of initial() because it's in the OPTION's path as well @@ -1686,6 +1762,7 @@ class SystemTrackingEnforcementMixin(APIView): "of system tracking.")) return super(SystemTrackingEnforcementMixin, self).check_permissions(request) + class HostFactVersionsList(ListAPIView, ParentMixin, SystemTrackingEnforcementMixin): model = Fact @@ -1711,6 +1788,7 @@ class HostFactVersionsList(ListAPIView, ParentMixin, SystemTrackingEnforcementMi queryset = self.get_queryset() or [] return Response(dict(results=self.serializer_class(queryset, many=True).data)) + class HostFactCompareView(SubDetailAPIView, SystemTrackingEnforcementMixin): model = Fact @@ -1730,6 +1808,7 @@ class HostFactCompareView(SubDetailAPIView, SystemTrackingEnforcementMixin): return Response({'detail': _('Fact not found.')}, status=status.HTTP_404_NOT_FOUND) return Response(self.serializer_class(instance=fact_entry).data) + class GroupList(ListCreateAPIView): model = Group @@ -1765,6 +1844,7 @@ class EnforceParentRelationshipMixin(object): data[self.enforce_parent_relationship] = getattr(self.get_parent_object(), '%s_id' % self.enforce_parent_relationship) return super(EnforceParentRelationshipMixin, self).create(request, *args, **kwargs) + class GroupChildrenList(EnforceParentRelationshipMixin, SubListCreateAttachDetachAPIView): model = Group @@ -1773,6 +1853,7 @@ class GroupChildrenList(EnforceParentRelationshipMixin, SubListCreateAttachDetac relationship = 'children' enforce_parent_relationship = 'inventory' + class GroupPotentialChildrenList(SubListAPIView): model = Group @@ -1790,6 +1871,7 @@ class GroupPotentialChildrenList(SubListAPIView): except_pks.update(parent.all_children.values_list('pk', flat=True)) return qs.exclude(pk__in=except_pks) + class GroupHostsList(SubListCreateAttachDetachAPIView): ''' the list of hosts directly below a group ''' @@ -1815,6 +1897,7 @@ class GroupHostsList(SubListCreateAttachDetachAPIView): return self.attach(request, *args, **kwargs) return super(GroupHostsList, self).create(request, *args, **kwargs) + class GroupAllHostsList(SubListAPIView): ''' the list of all hosts below a group, even including subgroups ''' @@ -1830,6 +1913,7 @@ class GroupAllHostsList(SubListAPIView): sublist_qs = parent.all_hosts.distinct() return qs & sublist_qs + class GroupInventorySourcesList(SubListAPIView): model = InventorySource @@ -1838,6 +1922,7 @@ class GroupInventorySourcesList(SubListAPIView): relationship = 'inventory_sources' new_in_148 = True + class GroupActivityStreamList(SubListAPIView): model = ActivityStream @@ -1862,6 +1947,7 @@ class GroupActivityStreamList(SubListAPIView): qs = self.request.user.get_queryset(self.model) return qs.filter(Q(group=parent) | Q(host__in=parent.hosts.all())) + class GroupDetail(RetrieveUpdateDestroyAPIView): model = Group @@ -1874,6 +1960,7 @@ class GroupDetail(RetrieveUpdateDestroyAPIView): obj.delete_recursive() return Response(status=status.HTTP_204_NO_CONTENT) + class InventoryGroupsList(SubListCreateAttachDetachAPIView): model = Group @@ -1882,6 +1969,7 @@ class InventoryGroupsList(SubListCreateAttachDetachAPIView): relationship = 'groups' parent_key = 'inventory' + class InventoryRootGroupsList(SubListCreateAttachDetachAPIView): model = Group @@ -1896,27 +1984,32 @@ class InventoryRootGroupsList(SubListCreateAttachDetachAPIView): qs = self.request.user.get_queryset(self.model).distinct() # need distinct for '&' operator return qs & parent.root_groups + class BaseVariableData(RetrieveUpdateAPIView): parser_classes = api_settings.DEFAULT_PARSER_CLASSES + [YAMLParser] renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [YAMLRenderer] is_variable_data = True # Special flag for permissions check. + class InventoryVariableData(BaseVariableData): model = Inventory serializer_class = InventoryVariableDataSerializer + class HostVariableData(BaseVariableData): model = Host serializer_class = HostVariableDataSerializer + class GroupVariableData(BaseVariableData): model = Group serializer_class = GroupVariableDataSerializer + class InventoryScriptView(RetrieveAPIView): model = Inventory @@ -2002,6 +2095,7 @@ class InventoryScriptView(RetrieveAPIView): return Response(data) + class InventoryTreeView(RetrieveAPIView): model = Inventory @@ -2034,6 +2128,7 @@ class InventoryTreeView(RetrieveAPIView): group_children_map) return Response(tree_data) + class InventoryInventorySourcesList(SubListAPIView): model = InventorySource @@ -2050,12 +2145,14 @@ class InventoryInventorySourcesList(SubListAPIView): return qs.filter(Q(inventory__pk=parent.pk) | Q(group__inventory__pk=parent.pk)) + class InventorySourceList(ListAPIView): model = InventorySource serializer_class = InventorySourceSerializer new_in_14 = True + class InventorySourceDetail(RetrieveUpdateAPIView): model = InventorySource @@ -2071,6 +2168,7 @@ class InventorySourceDetail(RetrieveUpdateAPIView): pu.cancel() return super(InventorySourceDetail, self).destroy(request, *args, **kwargs) + class InventorySourceSchedulesList(SubListCreateAttachDetachAPIView): view_name = _("Inventory Source Schedules") @@ -2082,6 +2180,7 @@ class InventorySourceSchedulesList(SubListCreateAttachDetachAPIView): parent_key = 'unified_job_template' new_in_148 = True + class InventorySourceActivityStreamList(SubListAPIView): model = ActivityStream @@ -2100,6 +2199,7 @@ class InventorySourceActivityStreamList(SubListAPIView): # Okay, let it through. return super(InventorySourceActivityStreamList, self).get(request, *args, **kwargs) + class InventorySourceNotificationTemplatesAnyList(SubListCreateAttachDetachAPIView): model = NotificationTemplate @@ -2115,14 +2215,17 @@ class InventorySourceNotificationTemplatesAnyList(SubListCreateAttachDetachAPIVi status=status.HTTP_400_BAD_REQUEST) return super(InventorySourceNotificationTemplatesAnyList, self).post(request, *args, **kwargs) + class InventorySourceNotificationTemplatesErrorList(InventorySourceNotificationTemplatesAnyList): relationship = 'notification_templates_error' + class InventorySourceNotificationTemplatesSuccessList(InventorySourceNotificationTemplatesAnyList): relationship = 'notification_templates_success' + class InventorySourceHostsList(SubListAPIView): model = Host @@ -2131,6 +2234,7 @@ class InventorySourceHostsList(SubListAPIView): relationship = 'hosts' new_in_148 = True + class InventorySourceGroupsList(SubListAPIView): model = Group @@ -2139,6 +2243,7 @@ class InventorySourceGroupsList(SubListAPIView): relationship = 'groups' new_in_148 = True + class InventorySourceUpdatesList(SubListAPIView): model = InventoryUpdate @@ -2147,6 +2252,7 @@ class InventorySourceUpdatesList(SubListAPIView): relationship = 'inventory_updates' new_in_14 = True + class InventorySourceUpdateView(RetrieveAPIView): model = InventorySource @@ -2166,11 +2272,13 @@ class InventorySourceUpdateView(RetrieveAPIView): else: return self.http_method_not_allowed(request, *args, **kwargs) + class InventoryUpdateList(ListAPIView): model = InventoryUpdate serializer_class = InventoryUpdateListSerializer + class InventoryUpdateDetail(RetrieveDestroyAPIView): model = InventoryUpdate @@ -2183,6 +2291,7 @@ class InventoryUpdateDetail(RetrieveDestroyAPIView): raise PermissionDenied(detail=_('Can not delete job resource when associated workflow job is running.')) return super(InventoryUpdateDetail, self).destroy(request, *args, **kwargs) + class InventoryUpdateCancel(RetrieveAPIView): model = InventoryUpdate @@ -2198,6 +2307,7 @@ class InventoryUpdateCancel(RetrieveAPIView): else: return self.http_method_not_allowed(request, *args, **kwargs) + class InventoryUpdateNotificationsList(SubListAPIView): model = Notification @@ -2205,6 +2315,7 @@ class InventoryUpdateNotificationsList(SubListAPIView): parent_model = InventoryUpdate relationship = 'notifications' + class JobTemplateList(ListCreateAPIView): model = JobTemplate @@ -2222,12 +2333,14 @@ class JobTemplateList(ListCreateAPIView): job_template.admin_role.members.add(request.user) return ret + class JobTemplateDetail(RetrieveUpdateDestroyAPIView): model = JobTemplate serializer_class = JobTemplateSerializer always_allow_superuser = False + class JobTemplateLaunch(RetrieveAPIView, GenericAPIView): model = JobTemplate @@ -2298,6 +2411,7 @@ class JobTemplateLaunch(RetrieveAPIView, GenericAPIView): data['job'] = new_job.id return Response(data, status=status.HTTP_201_CREATED) + class JobTemplateSchedulesList(SubListCreateAttachDetachAPIView): view_name = _("Job Template Schedules") @@ -2309,6 +2423,7 @@ class JobTemplateSchedulesList(SubListCreateAttachDetachAPIView): parent_key = 'unified_job_template' new_in_148 = True + class JobTemplateSurveySpec(GenericAPIView): model = JobTemplate @@ -2375,11 +2490,13 @@ class JobTemplateSurveySpec(GenericAPIView): obj.save() return Response() + class WorkflowJobTemplateSurveySpec(JobTemplateSurveySpec): model = WorkflowJobTemplate parent_model = WorkflowJobTemplate + class JobTemplateActivityStreamList(SubListAPIView): model = ActivityStream @@ -2398,6 +2515,7 @@ class JobTemplateActivityStreamList(SubListAPIView): # Okay, let it through. return super(JobTemplateActivityStreamList, self).get(request, *args, **kwargs) + class JobTemplateNotificationTemplatesAnyList(SubListCreateAttachDetachAPIView): model = NotificationTemplate @@ -2405,6 +2523,7 @@ class JobTemplateNotificationTemplatesAnyList(SubListCreateAttachDetachAPIView): parent_model = JobTemplate relationship = 'notification_templates_any' + class JobTemplateNotificationTemplatesErrorList(SubListCreateAttachDetachAPIView): model = NotificationTemplate @@ -2412,6 +2531,7 @@ class JobTemplateNotificationTemplatesErrorList(SubListCreateAttachDetachAPIView parent_model = JobTemplate relationship = 'notification_templates_error' + class JobTemplateNotificationTemplatesSuccessList(SubListCreateAttachDetachAPIView): model = NotificationTemplate @@ -2419,6 +2539,7 @@ class JobTemplateNotificationTemplatesSuccessList(SubListCreateAttachDetachAPIVi parent_model = JobTemplate relationship = 'notification_templates_success' + class JobTemplateLabelList(DeleteLastUnattachLabelMixin, SubListCreateAttachDetachAPIView): model = Label @@ -2439,6 +2560,7 @@ class JobTemplateLabelList(DeleteLastUnattachLabelMixin, SubListCreateAttachDeta del request.data['organization'] return super(JobTemplateLabelList, self).post(request, *args, **kwargs) + class JobTemplateCallback(GenericAPIView): model = JobTemplate @@ -2605,12 +2727,14 @@ class JobTemplateJobsList(SubListCreateAPIView): relationship = 'jobs' parent_key = 'job_template' + class JobTemplateAccessList(ResourceAccessList): model = User # needs to be User for AccessLists's resource_model = JobTemplate new_in_300 = True + class JobTemplateObjectRolesList(SubListAPIView): model = Role @@ -2623,24 +2747,28 @@ class JobTemplateObjectRolesList(SubListAPIView): content_type = ContentType.objects.get_for_model(self.parent_model) return Role.objects.filter(content_type=content_type, object_id=po.pk) + class WorkflowJobNodeList(ListAPIView): model = WorkflowJobNode serializer_class = WorkflowJobNodeListSerializer new_in_310 = True + class WorkflowJobNodeDetail(RetrieveAPIView): model = WorkflowJobNode serializer_class = WorkflowJobNodeDetailSerializer new_in_310 = True + class WorkflowJobTemplateNodeList(ListCreateAPIView): model = WorkflowJobTemplateNode serializer_class = WorkflowJobTemplateNodeListSerializer new_in_310 = True + class WorkflowJobTemplateNodeDetail(RetrieveUpdateDestroyAPIView): model = WorkflowJobTemplateNode @@ -2717,6 +2845,7 @@ class WorkflowJobTemplateNodeChildrenBaseList(EnforceParentRelationshipMixin, Su return None + class WorkflowJobTemplateNodeSuccessNodesList(WorkflowJobTemplateNodeChildrenBaseList): relationship = 'success_nodes' @@ -2743,6 +2872,7 @@ class WorkflowJobNodeChildrenBaseList(SubListAPIView): self.check_parent_access(parent) return getattr(parent, self.relationship).all() + class WorkflowJobNodeSuccessNodesList(WorkflowJobNodeChildrenBaseList): relationship = 'success_nodes' @@ -2771,6 +2901,7 @@ class WorkflowJobTemplateList(ListCreateAPIView): return ret ''' + # TODO: class WorkflowJobTemplateDetail(RetrieveUpdateDestroyAPIView): @@ -2787,6 +2918,7 @@ class WorkflowJobTemplateLabelList(JobTemplateLabelList): class WorkflowJobTemplateLaunch(RetrieveAPIView): + model = WorkflowJobTemplate serializer_class = WorkflowJobLaunchSerializer new_in_310 = True @@ -2823,6 +2955,7 @@ class WorkflowJobTemplateLaunch(RetrieveAPIView): data['workflow_job'] = new_job.id return Response(data, status=status.HTTP_201_CREATED) + # TODO: class WorkflowJobTemplateWorkflowNodesList(SubListCreateAPIView): @@ -2838,6 +2971,7 @@ class WorkflowJobTemplateWorkflowNodesList(SubListCreateAPIView): data[fd] = None return super(WorkflowJobTemplateWorkflowNodesList, self).update_raw_data(data) + # TODO: class WorkflowJobTemplateJobsList(SubListAPIView): @@ -2847,6 +2981,7 @@ class WorkflowJobTemplateJobsList(SubListAPIView): relationship = 'jobs' parent_key = 'workflow_job_template' + class WorkflowJobTemplateSchedulesList(SubListCreateAttachDetachAPIView): view_name = _("Workflow Job Template Schedules") @@ -2857,6 +2992,7 @@ class WorkflowJobTemplateSchedulesList(SubListCreateAttachDetachAPIView): relationship = 'schedules' parent_key = 'unified_job_template' + class WorkflowJobTemplateNotificationTemplatesAnyList(SubListCreateAttachDetachAPIView): model = NotificationTemplate @@ -2865,6 +3001,7 @@ class WorkflowJobTemplateNotificationTemplatesAnyList(SubListCreateAttachDetachA relationship = 'notification_templates_any' new_in_310 = True + class WorkflowJobTemplateNotificationTemplatesErrorList(SubListCreateAttachDetachAPIView): model = NotificationTemplate @@ -2873,6 +3010,7 @@ class WorkflowJobTemplateNotificationTemplatesErrorList(SubListCreateAttachDetac relationship = 'notification_templates_error' new_in_310 = True + class WorkflowJobTemplateNotificationTemplatesSuccessList(SubListCreateAttachDetachAPIView): model = NotificationTemplate @@ -2881,6 +3019,7 @@ class WorkflowJobTemplateNotificationTemplatesSuccessList(SubListCreateAttachDet relationship = 'notification_templates_success' new_in_310 = True + # TODO: class WorkflowJobList(ListCreateAPIView): @@ -2888,6 +3027,7 @@ class WorkflowJobList(ListCreateAPIView): serializer_class = WorkflowJobListSerializer new_in_310 = True + # TODO: class WorkflowJobDetail(RetrieveDestroyAPIView): @@ -2895,6 +3035,7 @@ class WorkflowJobDetail(RetrieveDestroyAPIView): serializer_class = WorkflowJobSerializer new_in_310 = True + class WorkflowJobWorkflowNodesList(SubListAPIView): model = WorkflowJobNode @@ -2905,6 +3046,7 @@ class WorkflowJobWorkflowNodesList(SubListAPIView): parent_key = 'workflow_job' new_in_310 = True + class WorkflowJobCancel(RetrieveAPIView): model = WorkflowJob @@ -2922,6 +3064,7 @@ class WorkflowJobCancel(RetrieveAPIView): else: return self.http_method_not_allowed(request, *args, **kwargs) + class WorkflowJobNotificationsList(SubListAPIView): model = Notification @@ -2930,6 +3073,7 @@ class WorkflowJobNotificationsList(SubListAPIView): relationship = 'notifications' new_in_310 = True + class SystemJobTemplateList(ListAPIView): model = SystemJobTemplate @@ -2940,11 +3084,13 @@ class SystemJobTemplateList(ListAPIView): raise PermissionDenied(_("Superuser privileges needed.")) return super(SystemJobTemplateList, self).get(request, *args, **kwargs) + class SystemJobTemplateDetail(RetrieveAPIView): model = SystemJobTemplate serializer_class = SystemJobTemplateSerializer + class SystemJobTemplateLaunch(GenericAPIView): model = SystemJobTemplate @@ -2962,6 +3108,7 @@ class SystemJobTemplateLaunch(GenericAPIView): data = dict(system_job=new_job.id) return Response(data, status=status.HTTP_201_CREATED) + class SystemJobTemplateSchedulesList(SubListCreateAttachDetachAPIView): view_name = _("System Job Template Schedules") @@ -2972,6 +3119,7 @@ class SystemJobTemplateSchedulesList(SubListCreateAttachDetachAPIView): relationship = 'schedules' parent_key = 'unified_job_template' + class SystemJobTemplateJobsList(SubListAPIView): model = SystemJob @@ -2980,6 +3128,7 @@ class SystemJobTemplateJobsList(SubListAPIView): relationship = 'jobs' parent_key = 'system_job_template' + class SystemJobTemplateNotificationTemplatesAnyList(SubListCreateAttachDetachAPIView): model = NotificationTemplate @@ -2987,6 +3136,7 @@ class SystemJobTemplateNotificationTemplatesAnyList(SubListCreateAttachDetachAPI parent_model = SystemJobTemplate relationship = 'notification_templates_any' + class SystemJobTemplateNotificationTemplatesErrorList(SubListCreateAttachDetachAPIView): model = NotificationTemplate @@ -2994,6 +3144,7 @@ class SystemJobTemplateNotificationTemplatesErrorList(SubListCreateAttachDetachA parent_model = SystemJobTemplate relationship = 'notification_templates_error' + class SystemJobTemplateNotificationTemplatesSuccessList(SubListCreateAttachDetachAPIView): model = NotificationTemplate @@ -3001,11 +3152,13 @@ class SystemJobTemplateNotificationTemplatesSuccessList(SubListCreateAttachDetac parent_model = SystemJobTemplate relationship = 'notification_templates_success' + class JobList(ListCreateAPIView): model = Job serializer_class = JobListSerializer + class JobDetail(RetrieveUpdateDestroyAPIView): model = Job @@ -3024,6 +3177,7 @@ class JobDetail(RetrieveUpdateDestroyAPIView): raise PermissionDenied(detail=_('Can not delete job resource when associated workflow job is running.')) return super(JobDetail, self).destroy(request, *args, **kwargs) + class JobLabelList(SubListAPIView): model = Label @@ -3032,6 +3186,7 @@ class JobLabelList(SubListAPIView): relationship = 'labels' parent_key = 'job' + class WorkflowJobLabelList(JobLabelList): parent_model = WorkflowJob @@ -3053,6 +3208,7 @@ class JobActivityStreamList(SubListAPIView): # Okay, let it through. return super(JobActivityStreamList, self).get(request, *args, **kwargs) + class JobStart(GenericAPIView): model = Job @@ -3081,6 +3237,7 @@ class JobStart(GenericAPIView): else: return self.http_method_not_allowed(request, *args, **kwargs) + class JobCancel(RetrieveAPIView): model = Job @@ -3095,6 +3252,7 @@ class JobCancel(RetrieveAPIView): else: return self.http_method_not_allowed(request, *args, **kwargs) + class JobRelaunch(RetrieveAPIView, GenericAPIView): model = Job @@ -3128,6 +3286,7 @@ class JobRelaunch(RetrieveAPIView, GenericAPIView): headers = {'Location': new_job.get_absolute_url()} return Response(data, status=status.HTTP_201_CREATED, headers=headers) + class JobNotificationsList(SubListAPIView): model = Notification @@ -3135,6 +3294,7 @@ class JobNotificationsList(SubListAPIView): parent_model = Job relationship = 'notifications' + class BaseJobHostSummariesList(SubListAPIView): model = JobHostSummary @@ -3143,33 +3303,40 @@ class BaseJobHostSummariesList(SubListAPIView): relationship = 'job_host_summaries' view_name = _('Job Host Summaries List') + class HostJobHostSummariesList(BaseJobHostSummariesList): parent_model = Host + class GroupJobHostSummariesList(BaseJobHostSummariesList): parent_model = Group + class JobJobHostSummariesList(BaseJobHostSummariesList): parent_model = Job + class JobHostSummaryDetail(RetrieveAPIView): model = JobHostSummary serializer_class = JobHostSummarySerializer + class JobEventList(ListAPIView): model = JobEvent serializer_class = JobEventSerializer + class JobEventDetail(RetrieveAPIView): model = JobEvent serializer_class = JobEventSerializer + class JobEventChildrenList(SubListAPIView): model = JobEvent @@ -3178,6 +3345,7 @@ class JobEventChildrenList(SubListAPIView): relationship = 'children' view_name = _('Job Event Children List') + class JobEventHostsList(SubListAPIView): model = Host @@ -3186,6 +3354,7 @@ class JobEventHostsList(SubListAPIView): relationship = 'hosts' view_name = _('Job Event Hosts List') + class BaseJobEventsList(SubListAPIView): model = JobEvent @@ -3194,18 +3363,22 @@ class BaseJobEventsList(SubListAPIView): relationship = 'job_events' view_name = _('Job Events List') + class HostJobEventsList(BaseJobEventsList): parent_model = Host + class GroupJobEventsList(BaseJobEventsList): parent_model = Group + class JobJobEventsList(BaseJobEventsList): parent_model = Job + class JobJobPlaysList(BaseJobEventsList): parent_model = Job @@ -3588,6 +3761,7 @@ class HostAdHocCommandEventsList(BaseAdHocCommandEventsList): parent_model = Host new_in_220 = True + #class GroupJobEventsList(BaseJobEventsList): # parent_model = Group @@ -3641,6 +3815,7 @@ class SystemJobDetail(RetrieveDestroyAPIView): model = SystemJob serializer_class = SystemJobSerializer + class SystemJobCancel(RetrieveAPIView): model = SystemJob @@ -3655,6 +3830,7 @@ class SystemJobCancel(RetrieveAPIView): else: return self.http_method_not_allowed(request, *args, **kwargs) + class SystemJobNotificationsList(SubListAPIView): model = Notification @@ -3662,18 +3838,21 @@ class SystemJobNotificationsList(SubListAPIView): parent_model = SystemJob relationship = 'notifications' + class UnifiedJobTemplateList(ListAPIView): model = UnifiedJobTemplate serializer_class = UnifiedJobTemplateSerializer new_in_148 = True + class UnifiedJobList(ListAPIView): model = UnifiedJob serializer_class = UnifiedJobListSerializer new_in_148 = True + class UnifiedJobStdout(RetrieveAPIView): authentication_classes = [TokenGetAuthentication] + api_settings.DEFAULT_AUTHENTICATION_CLASSES @@ -3743,29 +3922,35 @@ class UnifiedJobStdout(RetrieveAPIView): else: return super(UnifiedJobStdout, self).retrieve(request, *args, **kwargs) + class ProjectUpdateStdout(UnifiedJobStdout): model = ProjectUpdate + class InventoryUpdateStdout(UnifiedJobStdout): model = InventoryUpdate + class JobStdout(UnifiedJobStdout): model = Job + class AdHocCommandStdout(UnifiedJobStdout): model = AdHocCommand new_in_220 = True + class NotificationTemplateList(ListCreateAPIView): model = NotificationTemplate serializer_class = NotificationTemplateSerializer new_in_300 = True + class NotificationTemplateDetail(RetrieveUpdateDestroyAPIView): model = NotificationTemplate @@ -3781,6 +3966,7 @@ class NotificationTemplateDetail(RetrieveUpdateDestroyAPIView): status=status.HTTP_405_METHOD_NOT_ALLOWED) return super(NotificationTemplateDetail, self).delete(request, *args, **kwargs) + class NotificationTemplateTest(GenericAPIView): view_name = _('NotificationTemplate Test') @@ -3802,6 +3988,7 @@ class NotificationTemplateTest(GenericAPIView): headers=headers, status=status.HTTP_202_ACCEPTED) + class NotificationTemplateNotificationList(SubListAPIView): model = Notification @@ -3810,30 +3997,35 @@ class NotificationTemplateNotificationList(SubListAPIView): relationship = 'notifications' parent_key = 'notification_template' + class NotificationList(ListAPIView): model = Notification serializer_class = NotificationSerializer new_in_300 = True + class NotificationDetail(RetrieveAPIView): model = Notification serializer_class = NotificationSerializer new_in_300 = True + class LabelList(ListCreateAPIView): model = Label serializer_class = LabelSerializer new_in_300 = True + class LabelDetail(RetrieveUpdateAPIView): model = Label serializer_class = LabelSerializer new_in_300 = True + class ActivityStreamList(SimpleListAPIView): model = ActivityStream diff --git a/awx/conf/conf.py b/awx/conf/conf.py index 8562bb0929..cd494f1dee 100644 --- a/awx/conf/conf.py +++ b/awx/conf/conf.py @@ -57,6 +57,7 @@ register( category_slug='cows', ) + def _get_read_only_ansible_cow_selection_default(): return getattr(settings, 'ANSIBLE_COW_SELECTION', 'No default cow!') diff --git a/awx/lib/sitecustomize.py b/awx/lib/sitecustomize.py index 02ac2eba55..be7c06102d 100644 --- a/awx/lib/sitecustomize.py +++ b/awx/lib/sitecustomize.py @@ -5,6 +5,7 @@ import sys # Based on http://stackoverflow.com/a/6879344/131141 -- Initialize tower display # callback as early as possible to wrap ansible.display.Display methods. + def argv_ready(argv): if argv and os.path.basename(argv[0]) in {'ansible', 'ansible-playbook'}: import tower_display_callback # noqa diff --git a/awx/main/access.py b/awx/main/access.py index 20d5ae508f..7f23edafa2 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -58,13 +58,16 @@ access_registry = { # ... } + class StateConflict(ValidationError): status_code = 409 + def register_access(model_class, access_class): access_classes = access_registry.setdefault(model_class, []) access_classes.append(access_class) + @property def user_admin_role(self): role = Role.objects.get( @@ -76,9 +79,11 @@ def user_admin_role(self): role.parents = [org.admin_role.pk for org in self.organizations] return role + def user_accessible_objects(user, role_name): return ResourceMixin._accessible_objects(User, user, role_name) + def get_user_queryset(user, model_class): ''' Return a queryset for the given model_class containing only the instances @@ -98,6 +103,7 @@ def get_user_queryset(user, model_class): queryset = queryset.filter(pk__in=qs.values_list('pk', flat=True)) return queryset + def check_user_access(user, model_class, action, *args, **kwargs): ''' Return True if user can perform action against model_class with the @@ -117,6 +123,7 @@ def check_user_access(user, model_class, action, *args, **kwargs): return result return False + def get_user_capabilities(user, instance, **kwargs): ''' Returns a dictionary of capabilities the user has on the particular @@ -129,6 +136,7 @@ def get_user_capabilities(user, instance, **kwargs): return access_class(user).get_user_capabilities(instance, **kwargs) return None + def check_superuser(func): ''' check_superuser is a decorator that provides a simple short circuit @@ -141,6 +149,7 @@ def check_superuser(func): return func(self, *args, **kwargs) return wrapper + class BaseAccess(object): ''' Base class for checking user access to a given model. Subclasses should @@ -488,6 +497,7 @@ class OrganizationAccess(BaseAccess): "active_jobs": active_jobs}) return True + class InventoryAccess(BaseAccess): ''' I can see inventory when: @@ -557,6 +567,7 @@ class InventoryAccess(BaseAccess): def can_run_ad_hoc_commands(self, obj): return self.user in obj.adhoc_role + class HostAccess(BaseAccess): ''' I can see hosts whenever I can see their inventory. @@ -611,6 +622,7 @@ class HostAccess(BaseAccess): def can_delete(self, obj): return obj and self.user in obj.inventory.admin_role + class GroupAccess(BaseAccess): ''' I can see groups whenever I can see their inventory. @@ -678,6 +690,7 @@ class GroupAccess(BaseAccess): return self.user.can_access(InventorySource, 'start', obj.inventory_source, validate_license=validate_license) return False + class InventorySourceAccess(BaseAccess): ''' I can see inventory sources whenever I can see their group or inventory. @@ -757,6 +770,7 @@ class InventoryUpdateAccess(BaseAccess): def can_delete(self, obj): return self.user in obj.inventory_source.inventory.admin_role + class CredentialAccess(BaseAccess): ''' I can see credentials when: @@ -829,6 +843,7 @@ class CredentialAccess(BaseAccess): # return True return self.can_change(obj, None) + class TeamAccess(BaseAccess): ''' I can see a team when: @@ -889,6 +904,7 @@ class TeamAccess(BaseAccess): return super(TeamAccess, self).can_unattach(obj, sub_obj, relationship, *args, **kwargs) + class ProjectAccess(BaseAccess): ''' I can see projects when: @@ -943,6 +959,7 @@ class ProjectAccess(BaseAccess): def can_start(self, obj, validate_license=True): return obj and self.user in obj.update_role + class ProjectUpdateAccess(BaseAccess): ''' I can see project updates when I can see the project. @@ -979,6 +996,7 @@ class ProjectUpdateAccess(BaseAccess): def can_delete(self, obj): return obj and self.user in obj.project.admin_role + class JobTemplateAccess(BaseAccess): ''' I can see job templates when: @@ -1175,6 +1193,7 @@ class JobTemplateAccess(BaseAccess): "active_jobs": active_jobs}) return True + class JobAccess(BaseAccess): ''' I can see jobs when: @@ -1313,6 +1332,7 @@ class JobAccess(BaseAccess): return True return obj.job_template is not None and self.user in obj.job_template.admin_role + class SystemJobTemplateAccess(BaseAccess): ''' I can only see/manage System Job Templates if I'm a super user @@ -1325,6 +1345,7 @@ class SystemJobTemplateAccess(BaseAccess): '''Only a superuser can start a job from a SystemJobTemplate''' return False + class SystemJobAccess(BaseAccess): ''' I can only see manage System Jobs if I'm a super user @@ -1334,6 +1355,7 @@ class SystemJobAccess(BaseAccess): def can_start(self, obj, validate_license=True): return False # no relaunching of system jobs + # TODO: class WorkflowJobTemplateNodeAccess(BaseAccess): ''' @@ -1430,6 +1452,7 @@ class WorkflowJobTemplateNodeAccess(BaseAccess): def can_unattach(self, obj, sub_obj, relationship, data, skip_sub_obj_read_check=False): return self.wfjt_admin(obj) and self.check_same_WFJT(obj, sub_obj) + class WorkflowJobNodeAccess(BaseAccess): ''' I can see a WorkflowJobNode if I have permission to... @@ -1462,6 +1485,7 @@ class WorkflowJobNodeAccess(BaseAccess): def can_delete(self, obj): return False + # TODO: revisit for survey logic, notification attachments? class WorkflowJobTemplateAccess(BaseAccess): ''' @@ -1604,6 +1628,7 @@ class WorkflowJobAccess(BaseAccess): return False return self.can_delete(obj) or self.user == obj.created_by + class AdHocCommandAccess(BaseAccess): ''' I can only see/run ad hoc commands when: @@ -1660,6 +1685,7 @@ class AdHocCommandAccess(BaseAccess): return True return obj.inventory is not None and self.user in obj.inventory.admin_role + class AdHocCommandEventAccess(BaseAccess): ''' I can see ad hoc command event records whenever I can read both ad hoc @@ -1688,6 +1714,7 @@ class AdHocCommandEventAccess(BaseAccess): 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. @@ -1713,6 +1740,7 @@ class JobHostSummaryAccess(BaseAccess): def can_delete(self, obj): return False + class JobEventAccess(BaseAccess): ''' I can see job event records whenever I can read both job and host. @@ -1746,6 +1774,7 @@ class JobEventAccess(BaseAccess): def can_delete(self, obj): return False + class UnifiedJobTemplateAccess(BaseAccess): ''' I can see a unified job template whenever I can see the same project, @@ -1788,6 +1817,7 @@ class UnifiedJobTemplateAccess(BaseAccess): return qs.all() + class UnifiedJobAccess(BaseAccess): ''' I can see a unified job whenever I can see the same project update, @@ -1838,6 +1868,7 @@ class UnifiedJobAccess(BaseAccess): #) return qs.all() + class ScheduleAccess(BaseAccess): ''' I can see a schedule if I can see it's related unified job, I can create them or update them if I have write access @@ -1878,6 +1909,7 @@ class ScheduleAccess(BaseAccess): def can_delete(self, obj): return self.can_change(obj, {}) + class NotificationTemplateAccess(BaseAccess): ''' I can see/use a notification_template if I have permission to @@ -1926,6 +1958,7 @@ class NotificationTemplateAccess(BaseAccess): return False return self.user in obj.organization.admin_role + class NotificationAccess(BaseAccess): ''' I can see/use a notification if I have permission to @@ -1947,6 +1980,7 @@ class NotificationAccess(BaseAccess): def can_delete(self, obj): return self.user.can_access(NotificationTemplate, 'delete', obj.notification_template) + class LabelAccess(BaseAccess): ''' I can see/use a Label if I have permission to associated organization @@ -1980,6 +2014,7 @@ class LabelAccess(BaseAccess): def can_delete(self, obj): return self.can_change(obj, None) + class ActivityStreamAccess(BaseAccess): ''' I can see activity stream events only when I have permission on all objects included in the event @@ -2058,6 +2093,7 @@ class ActivityStreamAccess(BaseAccess): def can_delete(self, obj): return False + class CustomInventoryScriptAccess(BaseAccess): model = CustomInventoryScript @@ -2085,6 +2121,7 @@ class CustomInventoryScriptAccess(BaseAccess): def can_delete(self, obj): return self.can_admin(obj) + class RoleAccess(BaseAccess): ''' - I can see roles when diff --git a/awx/main/conf.py b/awx/main/conf.py index 70af78240a..b2ed431e87 100644 --- a/awx/main/conf.py +++ b/awx/main/conf.py @@ -77,6 +77,7 @@ register( category_slug='system', ) + def _load_default_license_from_file(): try: license_file = os.environ.get('AWX_LICENSE_FILE', '/etc/tower/license') diff --git a/awx/main/consumers.py b/awx/main/consumers.py index bbd155329a..5e0ec6a376 100644 --- a/awx/main/consumers.py +++ b/awx/main/consumers.py @@ -30,6 +30,7 @@ def user_from_token(auth_token): except User.DoesNotExist: return None + @channel_session def ws_connect(message): token = None diff --git a/awx/main/fields.py b/awx/main/fields.py index 91f59bab8a..1eb8090fc1 100644 --- a/awx/main/fields.py +++ b/awx/main/fields.py @@ -40,6 +40,7 @@ class JSONField(upstream_JSONField): # Based on AutoOneToOneField from django-annoying: # https://bitbucket.org/offline/django-annoying/src/a0de8b294db3/annoying/fields.py + class AutoSingleRelatedObjectDescriptor(SingleRelatedObjectDescriptor): """Descriptor for access to the object from its related class.""" @@ -56,6 +57,7 @@ class AutoSingleRelatedObjectDescriptor(SingleRelatedObjectDescriptor): obj.save() return obj + class AutoOneToOneField(models.OneToOneField): """OneToOneField that creates related object if it doesn't exist.""" diff --git a/awx/main/ha.py b/awx/main/ha.py index 5341ea32bb..35ed6f64f0 100644 --- a/awx/main/ha.py +++ b/awx/main/ha.py @@ -4,6 +4,7 @@ # AWX from awx.main.models import Instance + def is_ha_environment(): """Return True if this is an HA environment, and False otherwise. diff --git a/awx/main/management/commands/cleanup_activitystream.py b/awx/main/management/commands/cleanup_activitystream.py index f4803d2d84..cd3711790a 100644 --- a/awx/main/management/commands/cleanup_activitystream.py +++ b/awx/main/management/commands/cleanup_activitystream.py @@ -13,6 +13,7 @@ from django.utils.timezone import now # AWX from awx.main.models import ActivityStream + class Command(NoArgsCommand): ''' Management command to purge old activity stream events. diff --git a/awx/main/management/commands/cleanup_authtokens.py b/awx/main/management/commands/cleanup_authtokens.py index 65a8d67e6b..113fa52b2f 100644 --- a/awx/main/management/commands/cleanup_authtokens.py +++ b/awx/main/management/commands/cleanup_authtokens.py @@ -12,6 +12,7 @@ from django.utils.timezone import now # AWX from awx.main.models import * # noqa + class Command(BaseCommand): ''' Management command to cleanup expired auth tokens diff --git a/awx/main/management/commands/cleanup_facts.py b/awx/main/management/commands/cleanup_facts.py index e4c2d9f6f4..a709f81c1a 100644 --- a/awx/main/management/commands/cleanup_facts.py +++ b/awx/main/management/commands/cleanup_facts.py @@ -18,6 +18,7 @@ from awx.conf.license import feature_enabled OLDER_THAN = 'older_than' GRANULARITY = 'granularity' + class CleanupFacts(object): def __init__(self): self.timestamp = None @@ -27,7 +28,7 @@ class CleanupFacts(object): # Find all factVersion < pivot && > (pivot - granularity) grouped by host sorted by time descending (because it's indexed this way) # foreach group # Delete all except LAST entry (or Delete all except the FIRST entry, it's an arbitrary decision) - # + # # pivot -= granularity # group by host def cleanup(self, older_than_abs, granularity, module=None): @@ -89,6 +90,7 @@ class CleanupFacts(object): deleted_count = self.cleanup(t - older_than, granularity, module=module) print("Deleted %d facts." % deleted_count) + class Command(BaseCommand): help = 'Cleanup facts. For each host older than the value specified, keep one fact scan for each time window (granularity).' option_list = BaseCommand.option_list + ( @@ -142,4 +144,3 @@ class Command(BaseCommand): raise CommandError('--granularity invalid value "%s"' % options[GRANULARITY]) cleanup_facts.run(older_than, granularity, module=options['module']) - diff --git a/awx/main/management/commands/cleanup_jobs.py b/awx/main/management/commands/cleanup_jobs.py index 777d21204f..68f428447f 100644 --- a/awx/main/management/commands/cleanup_jobs.py +++ b/awx/main/management/commands/cleanup_jobs.py @@ -14,6 +14,7 @@ from django.utils.timezone import now # AWX from awx.main.models import Job, AdHocCommand, ProjectUpdate, InventoryUpdate, SystemJob + class Command(NoArgsCommand): ''' Management command to cleanup old jobs and project updates. diff --git a/awx/main/management/commands/list_instances.py b/awx/main/management/commands/list_instances.py index cbe767984a..e193a45dd0 100644 --- a/awx/main/management/commands/list_instances.py +++ b/awx/main/management/commands/list_instances.py @@ -4,6 +4,7 @@ from awx.main.models import Instance from django.core.management.base import NoArgsCommand + class Command(NoArgsCommand): """List instances from the Tower database """ diff --git a/awx/main/management/commands/register_instance.py b/awx/main/management/commands/register_instance.py index 01e2011aa0..7ce6be787b 100644 --- a/awx/main/management/commands/register_instance.py +++ b/awx/main/management/commands/register_instance.py @@ -7,6 +7,7 @@ from django.conf import settings from optparse import make_option from django.core.management.base import BaseCommand + class Command(BaseCommand): """ Internal tower command. diff --git a/awx/main/management/commands/run_callback_receiver.py b/awx/main/management/commands/run_callback_receiver.py index 5013e38aa4..c0105b2587 100644 --- a/awx/main/management/commands/run_callback_receiver.py +++ b/awx/main/management/commands/run_callback_receiver.py @@ -17,8 +17,8 @@ from awx.main.models import * # noqa logger = logging.getLogger('awx.main.commands.run_callback_receiver') -class CallbackBrokerWorker(ConsumerMixin): +class CallbackBrokerWorker(ConsumerMixin): def __init__(self, connection): self.connection = connection @@ -51,6 +51,7 @@ class CallbackBrokerWorker(ConsumerMixin): logger.error('Callback Task Processor Raised Exception: %r', exc) message.ack() + class Command(NoArgsCommand): ''' Save Job Callback receiver (see awx.plugins.callbacks.job_event_callback) diff --git a/awx/main/management/commands/run_fact_cache_receiver.py b/awx/main/management/commands/run_fact_cache_receiver.py index 90252e2a7d..afc6095309 100644 --- a/awx/main/management/commands/run_fact_cache_receiver.py +++ b/awx/main/management/commands/run_fact_cache_receiver.py @@ -18,6 +18,7 @@ from awx.main.socket_queue import Socket logger = logging.getLogger('awx.main.commands.run_fact_cache_receiver') + class FactCacheReceiver(object): def __init__(self): self.timestamp = None @@ -91,6 +92,7 @@ class FactCacheReceiver(object): else: self.process_fact_message(message) + class Command(NoArgsCommand): ''' blah blah diff --git a/awx/main/management/commands/run_socketio_service.py b/awx/main/management/commands/run_socketio_service.py index c1c49b7945..9b7e5a61d2 100644 --- a/awx/main/management/commands/run_socketio_service.py +++ b/awx/main/management/commands/run_socketio_service.py @@ -25,6 +25,7 @@ from socketio.namespace import BaseNamespace logger = logging.getLogger('awx.main.commands.run_socketio_service') + class SocketSession(object): def __init__(self, session_id, token_key, socket): self.socket = weakref.ref(socket) @@ -45,8 +46,8 @@ class SocketSession(object): auth_token = auth_token[0] return bool(not auth_token.is_expired()) -class SocketSessionManager(object): +class SocketSessionManager(object): def __init__(self): self.SESSIONS_MAX = 1000 self.socket_sessions = [] @@ -79,8 +80,8 @@ class SocketSessionManager(object): self._prune() return session -class SocketController(object): +class SocketController(object): def __init__(self, SocketSessionManager): self.server = None self.SocketSessionManager = SocketSessionManager @@ -127,12 +128,12 @@ class SocketController(object): socketController = SocketController(SocketSessionManager()) + # # Socket session is attached to self.session['socket_session'] # self.session and self.socket.session point to the same dict # class TowerBaseNamespace(BaseNamespace): - def get_allowed_methods(self): return ['recv_disconnect'] @@ -179,33 +180,33 @@ class TowerBaseNamespace(BaseNamespace): if socket_session and not socket_session.is_valid(): self.disconnect(silent=False) -class TestNamespace(TowerBaseNamespace): +class TestNamespace(TowerBaseNamespace): def recv_connect(self): logger.info("Received client connect for test namespace from %s" % str(self.environ['REMOTE_ADDR'])) self.emit('test', "If you see this then you attempted to connect to the test socket endpoint") super(TestNamespace, self).recv_connect() -class JobNamespace(TowerBaseNamespace): +class JobNamespace(TowerBaseNamespace): def recv_connect(self): logger.info("Received client connect for job namespace from %s" % str(self.environ['REMOTE_ADDR'])) super(JobNamespace, self).recv_connect() -class JobEventNamespace(TowerBaseNamespace): +class JobEventNamespace(TowerBaseNamespace): def recv_connect(self): logger.info("Received client connect for job event namespace from %s" % str(self.environ['REMOTE_ADDR'])) super(JobEventNamespace, self).recv_connect() -class AdHocCommandEventNamespace(TowerBaseNamespace): +class AdHocCommandEventNamespace(TowerBaseNamespace): def recv_connect(self): logger.info("Received client connect for ad hoc command event namespace from %s" % str(self.environ['REMOTE_ADDR'])) super(AdHocCommandEventNamespace, self).recv_connect() -class ScheduleNamespace(TowerBaseNamespace): +class ScheduleNamespace(TowerBaseNamespace): def get_allowed_methods(self): parent_allowed = super(ScheduleNamespace, self).get_allowed_methods() return parent_allowed + ["schedule_changed"] @@ -214,16 +215,16 @@ class ScheduleNamespace(TowerBaseNamespace): logger.info("Received client connect for schedule namespace from %s" % str(self.environ['REMOTE_ADDR'])) super(ScheduleNamespace, self).recv_connect() + # Catch-all namespace. # Deliver 'global' events over this namespace class ControlNamespace(TowerBaseNamespace): - def recv_connect(self): logger.warn("Received client connect for control namespace from %s" % str(self.environ['REMOTE_ADDR'])) super(ControlNamespace, self).recv_connect() -class TowerSocket(object): +class TowerSocket(object): def __call__(self, environ, start_response): path = environ['PATH_INFO'].strip('/') or 'index.html' if path.startswith('socket.io'): @@ -238,6 +239,7 @@ class TowerSocket(object): start_response('404 Not Found', []) return ['Tower version %s' % awx.__version__] + def notification_handler(server): with Socket('websocket', 'r') as websocket: for message in websocket.listen(): @@ -254,6 +256,7 @@ def notification_handler(server): else: socketController.broadcast_packet(packet) + class Command(NoArgsCommand): ''' SocketIO event emitter Tower service diff --git a/awx/main/management/commands/stats.py b/awx/main/management/commands/stats.py index 68b5ceef00..f55068d076 100644 --- a/awx/main/management/commands/stats.py +++ b/awx/main/management/commands/stats.py @@ -9,6 +9,7 @@ from django.core.management.base import BaseCommand # AWX from awx.main.models import * # noqa + class Command(BaseCommand): ''' Emits some simple statistics suitable for external monitoring diff --git a/awx/main/management/commands/update_password.py b/awx/main/management/commands/update_password.py index fe45799776..18a9fb053d 100644 --- a/awx/main/management/commands/update_password.py +++ b/awx/main/management/commands/update_password.py @@ -9,10 +9,11 @@ from django.core.management.base import BaseCommand from django.core.management.base import CommandError from django.contrib.auth.models import User + class UpdatePassword(object): def update_password(self, username, password): changed = False - u = User.objects.get(username=username) + u = User.objects.get(username=username) if not u: raise RuntimeError("User not found") check = u.check_password(password) @@ -22,6 +23,7 @@ class UpdatePassword(object): changed = True return changed + class Command(BaseCommand): option_list = BaseCommand.option_list + ( make_option('--username', dest='username', action='store', type='string', default=None, diff --git a/awx/main/management/commands/workload_generator.py b/awx/main/management/commands/workload_generator.py index 658d5c7228..5d1bd07019 100644 --- a/awx/main/management/commands/workload_generator.py +++ b/awx/main/management/commands/workload_generator.py @@ -23,7 +23,7 @@ from awx.main.utils import timedelta_total_seconds TEST_FACT_ANSIBLE = { "ansible_swapfree_mb" : 4092, "ansible_default_ipv6" : { - + }, "ansible_distribution_release" : "trusty", "ansible_system_vendor" : "innotek GmbH", @@ -199,6 +199,7 @@ EXPERIMENT_DEFAULT = { ] } + class Experiment(object): def __init__(self, exp, fact_fixtures, raw_db, mongoengine_db): self.db = raw_db @@ -291,6 +292,7 @@ class Experiment(object): print("Finished at: %s" % time_end) print("Total runtime: %s seconds" % timedelta_total_seconds(time_end - time_start)) + class Command(BaseCommand): option_list = BaseCommand.option_list + ( make_option('--drop', dest='drop', action='store_true', default=False, @@ -334,4 +336,3 @@ class Command(BaseCommand): self.experiment = Experiment(exp, FACT_FIXTURES, db, enginedb) self.experiment.generate_workload() - diff --git a/awx/main/managers.py b/awx/main/managers.py index 5fad1e11c8..522157a70f 100644 --- a/awx/main/managers.py +++ b/awx/main/managers.py @@ -20,6 +20,7 @@ class HostManager(models.Manager): except NotImplementedError: # For unit tests only, SQLite doesn't support distinct('name') return len(set(self.values_list('name', flat=True))) + class InstanceManager(models.Manager): """A custom manager class for the Instance model. diff --git a/awx/main/middleware.py b/awx/main/middleware.py index 75bdf01daa..187017ddba 100644 --- a/awx/main/middleware.py +++ b/awx/main/middleware.py @@ -68,6 +68,7 @@ class ActivityStreamMiddleware(threading.local): if instance.id not in self.instance_ids: self.instance_ids.append(instance.id) + class AuthTokenTimeoutMiddleware(object): """Presume that when the user includes the auth header, they go through the authentication mechanism. Further, that mechanism is presumed to extend diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index 331ea0b6e0..321c340a92 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -28,6 +28,8 @@ from awx.main.models.channels import * # noqa # the dumpdata command; see https://github.com/alex/django-taggit/issues/155). from django.core.serializers.python import Serializer as _PythonSerializer _original_handle_m2m_field = _PythonSerializer.handle_m2m_field + + def _new_handle_m2m_field(self, obj, field): try: field.rel.through._meta @@ -49,14 +51,17 @@ User.add_to_class('can_access', check_user_access) User.add_to_class('accessible_objects', user_accessible_objects) User.add_to_class('admin_role', user_admin_role) + @property def user_get_organizations(user): return Organization.objects.filter(member_role__members=user) + @property def user_get_admin_of_organizations(user): return Organization.objects.filter(admin_role__members=user) + @property def user_get_auditor_of_organizations(user): return Organization.objects.filter(auditor_role__members=user) @@ -66,10 +71,12 @@ User.add_to_class('organizations', user_get_organizations) User.add_to_class('admin_of_organizations', user_get_admin_of_organizations) User.add_to_class('auditor_of_organizations', user_get_auditor_of_organizations) + @property def user_is_system_auditor(user): return Role.singleton('system_auditor').members.filter(id=user.id).exists() + @user_is_system_auditor.setter def user_is_system_auditor(user, tf): if user.id: diff --git a/awx/main/models/ad_hoc_commands.py b/awx/main/models/ad_hoc_commands.py index bcb835763e..27d8754aa6 100644 --- a/awx/main/models/ad_hoc_commands.py +++ b/awx/main/models/ad_hoc_commands.py @@ -209,6 +209,7 @@ class AdHocCommand(UnifiedJob, JobNotificationMixin): def get_notification_friendly_name(self): return "AdHoc Command" + class AdHocCommandEvent(CreatedModifiedModel): ''' An event/message logged from the ad hoc event callback for each host. diff --git a/awx/main/models/base.py b/awx/main/models/base.py index 691b4532fe..bdee496bad 100644 --- a/awx/main/models/base.py +++ b/awx/main/models/base.py @@ -320,6 +320,7 @@ class CommonModelNameNotUnique(PrimordialModel): unique=False, ) + class NotificationFieldsModel(BaseModel): class Meta: diff --git a/awx/main/models/channels.py b/awx/main/models/channels.py index cd34678638..bd4f9514ba 100644 --- a/awx/main/models/channels.py +++ b/awx/main/models/channels.py @@ -1,5 +1,6 @@ from django.db import models + class ChannelGroup(models.Model): group = models.CharField(max_length=200, unique=True) channels = models.TextField() diff --git a/awx/main/models/fact.py b/awx/main/models/fact.py index 16a67eb45e..480834c2c1 100644 --- a/awx/main/models/fact.py +++ b/awx/main/models/fact.py @@ -8,6 +8,7 @@ from jsonbfield.fields import JSONField __all__ = ('Fact', ) + class Fact(models.Model): """A model representing a fact returned from Ansible. Facts are stored as JSON dictionaries. @@ -20,8 +21,8 @@ class Fact(models.Model): help_text=_('Host for the facts that the fact scan captured.'), ) timestamp = models.DateTimeField( - default=None, - editable=False, + default=None, + editable=False, help_text=_('Date and time of the corresponding fact scan gathering time.') ) module = models.CharField(max_length=128) diff --git a/awx/main/models/ha.py b/awx/main/models/ha.py index 3fff1d77cb..cb01d03722 100644 --- a/awx/main/models/ha.py +++ b/awx/main/models/ha.py @@ -39,9 +39,11 @@ class Instance(models.Model): # NOTE: TODO: Likely to repurpose this once standalone ramparts are a thing return "tower" + class TowerScheduleState(SingletonModel): schedule_last_run = models.DateTimeField(auto_now_add=True) + class JobOrigin(models.Model): """A model representing the relationship between a unified job and the instance that was responsible for starting that job. diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index cefc97e8eb..323a8f047f 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -188,6 +188,7 @@ class JobOptions(BaseModel): else: return [] + class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, ResourceMixin): ''' A job template is a reusable job definition for applying a project (with @@ -392,6 +393,7 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour any_notification_templates = set(any_notification_templates + list(base_notification_templates.filter(organization_notification_templates_for_any=self.project.organization))) return dict(error=list(error_notification_templates), success=list(success_notification_templates), any=list(any_notification_templates)) + class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin): ''' A job applies a project (with playbook) to an inventory source with a given @@ -646,6 +648,7 @@ class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin): def get_notification_friendly_name(self): return "Job" + class JobHostSummary(CreatedModifiedModel): ''' Per-host statistics for each job. diff --git a/awx/main/models/label.py b/awx/main/models/label.py index 9323ab4973..665d0dd98e 100644 --- a/awx/main/models/label.py +++ b/awx/main/models/label.py @@ -12,6 +12,7 @@ from awx.main.models.unified_jobs import UnifiedJobTemplate, UnifiedJob __all__ = ('Label', ) + class Label(CommonModelNameNotUnique): ''' Generic Tag. Designed for tagging Job Templates, but expandable to other models. @@ -55,4 +56,3 @@ class Label(CommonModelNameNotUnique): return True else: return False - diff --git a/awx/main/models/mixins.py b/awx/main/models/mixins.py index 8ce7025e8c..887d139573 100644 --- a/awx/main/models/mixins.py +++ b/awx/main/models/mixins.py @@ -16,6 +16,7 @@ from awx.main.fields import JSONField __all__ = ['ResourceMixin', 'SurveyJobTemplateMixin', 'SurveyJobMixin'] + class ResourceMixin(models.Model): class Meta: @@ -216,4 +217,3 @@ class SurveyJobMixin(models.Model): return json.dumps(extra_vars) else: return self.extra_vars - diff --git a/awx/main/models/notifications.py b/awx/main/models/notifications.py index 4f304fccda..8ba92b3782 100644 --- a/awx/main/models/notifications.py +++ b/awx/main/models/notifications.py @@ -25,6 +25,7 @@ logger = logging.getLogger('awx.main.models.notifications') __all__ = ['NotificationTemplate', 'Notification'] + class NotificationTemplate(CommonModel): NOTIFICATION_TYPES = [('email', _('Email'), CustomEmailBackend), @@ -116,6 +117,7 @@ class NotificationTemplate(CommonModel): notification_obj = EmailMessage(subject, backend_obj.format_body(body), sender, recipients) return backend_obj.send_messages([notification_obj]) + class Notification(CreatedModifiedModel): ''' A notification event emitted when a NotificationTemplate is run @@ -171,6 +173,7 @@ class Notification(CreatedModifiedModel): def get_absolute_url(self): return reverse('api:notification_detail', args=(self.pk,)) + class JobNotificationMixin(object): def get_notification_templates(self): raise RuntimeError("Define me") @@ -193,4 +196,3 @@ class JobNotificationMixin(object): def build_notification_failed_message(self): return self._build_notification_message('failed') - diff --git a/awx/main/models/organization.py b/awx/main/models/organization.py index d39c5180f8..8f96d1656c 100644 --- a/awx/main/models/organization.py +++ b/awx/main/models/organization.py @@ -72,7 +72,6 @@ class Organization(CommonModel, NotificationFieldsModel, ResourceMixin): return self.name - class Team(CommonModelNameNotUnique, ResourceMixin): ''' A team is a group of users that work on common projects. @@ -200,6 +199,8 @@ session token being invalid. For this case, mark the token as expired. Note: Again, because the value of reason is event based. The reason may not be set (i.e. may equal '') even though a session is expired or a limit is reached. """ + + class AuthToken(BaseModel): ''' Custom authentication tokens per user with expiration and request-specific diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index 1ed4e418bf..ee65791e6a 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -391,6 +391,7 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin): def get_absolute_url(self): return reverse('api:project_detail', args=(self.pk,)) + class ProjectUpdate(UnifiedJob, ProjectOptions, JobNotificationMixin): ''' Internal job for tracking project updates from SCM. diff --git a/awx/main/models/rbac.py b/awx/main/models/rbac.py index 5d6bef1bfc..7f8e4813df 100644 --- a/awx/main/models/rbac.py +++ b/awx/main/models/rbac.py @@ -79,6 +79,7 @@ def check_singleton(func): return func(*args, **kwargs) return wrapper + @contextlib.contextmanager def batch_role_ancestor_rebuilding(allow_nesting=False): ''' @@ -426,6 +427,7 @@ class Role(models.Model): def is_ancestor_of(self, role): return role.ancestors.filter(id=self.id).exists() + class RoleAncestorEntry(models.Model): class Meta: diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index 0e48491d04..ef536fea6f 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -356,6 +356,7 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, Notificatio dest_field.add(*list(src_field_value.all().values_list('id', flat=True))) return unified_job + class UnifiedJobTypeStringMixin(object): @classmethod def _underscore_to_camel(cls, word): @@ -379,6 +380,7 @@ class UnifiedJobTypeStringMixin(object): return None return model.objects.get(id=job_id) + class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique, UnifiedJobTypeStringMixin): ''' Concrete base class for unified job run by the task engine. diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py index 4e2b6e3b90..bed69330a5 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -32,6 +32,7 @@ __all__ = ['WorkflowJobTemplate', 'WorkflowJob', 'WorkflowJobOptions', 'Workflow CHAR_PROMPTS_LIST = ['job_type', 'job_tags', 'skip_tags', 'limit'] + class WorkflowNodeBase(CreatedModifiedModel): class Meta: abstract = True @@ -158,6 +159,7 @@ class WorkflowNodeBase(CreatedModifiedModel): return ['workflow_job', 'unified_job_template', 'inventory', 'credential', 'char_prompts'] + class WorkflowJobTemplateNode(WorkflowNodeBase): workflow_job_template = models.ForeignKey( 'WorkflowJobTemplate', @@ -183,6 +185,7 @@ class WorkflowJobTemplateNode(WorkflowNodeBase): create_kwargs[field_name] = getattr(self, field_name) return WorkflowJobNode.objects.create(**create_kwargs) + class WorkflowJobNode(WorkflowNodeBase): job = models.ForeignKey( 'UnifiedJob', @@ -205,7 +208,7 @@ class WorkflowJobNode(WorkflowNodeBase): default={}, editable=False, ) - + def get_absolute_url(self): return reverse('api:workflow_job_node_detail', args=(self.pk,)) @@ -257,6 +260,7 @@ class WorkflowJobNode(WorkflowNodeBase): data['extra_vars'] = extra_vars return data + class WorkflowJobOptions(BaseModel): class Meta: abstract = True @@ -268,8 +272,8 @@ class WorkflowJobOptions(BaseModel): extra_vars_dict = VarsDictProperty('extra_vars', True) -class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTemplateMixin, ResourceMixin): +class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTemplateMixin, ResourceMixin): class Meta: app_label = 'main' @@ -371,6 +375,7 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl warning_data[node.pk] = node_prompts_warnings return warning_data + class WorkflowJobInheritNodesMixin(object): def _inherit_relationship(self, old_node, new_node, node_ids_map, node_type): old_related_nodes = self._get_all_by_type(old_node, node_type) @@ -412,10 +417,9 @@ class WorkflowJobInheritNodesMixin(object): new_node = new_nodes[index] for node_type in ['success_nodes', 'failure_nodes', 'always_nodes']: self._inherit_relationship(old_node, new_node, node_ids_map, node_type) - + class WorkflowJob(UnifiedJob, WorkflowJobOptions, SurveyJobMixin, JobNotificationMixin, WorkflowJobInheritNodesMixin): - class Meta: app_label = 'main' ordering = ('id',) diff --git a/awx/main/redact.py b/awx/main/redact.py index 0e9ad0b5d1..a4776601ac 100644 --- a/awx/main/redact.py +++ b/awx/main/redact.py @@ -3,6 +3,7 @@ import urlparse REPLACE_STR = '$encrypted$' + class UriCleaner(object): REPLACE_STR = REPLACE_STR # https://regex101.com/r/sV2dO2/2 @@ -51,6 +52,7 @@ class UriCleaner(object): return redactedtext + class PlainTextCleaner(object): REPLACE_STR = REPLACE_STR diff --git a/awx/main/scheduler/__init__.py b/awx/main/scheduler/__init__.py index 16ca577d6c..0ac737c375 100644 --- a/awx/main/scheduler/__init__.py +++ b/awx/main/scheduler/__init__.py @@ -35,6 +35,7 @@ from celery.task.control import inspect logger = logging.getLogger('awx.main.scheduler') + class TaskManager(): def __init__(self): self.graph = DependencyGraph() diff --git a/awx/main/scheduler/dag_simple.py b/awx/main/scheduler/dag_simple.py index c6aa6247c0..1bfb387569 100644 --- a/awx/main/scheduler/dag_simple.py +++ b/awx/main/scheduler/dag_simple.py @@ -8,6 +8,7 @@ from awx.main.models import ( SystemJob, ) + class SimpleDAG(object): ''' A simple implementation of a directed acyclic graph ''' diff --git a/awx/main/scheduler/dag_workflow.py b/awx/main/scheduler/dag_workflow.py index 1995434f48..c765b48678 100644 --- a/awx/main/scheduler/dag_workflow.py +++ b/awx/main/scheduler/dag_workflow.py @@ -2,6 +2,7 @@ # AWX from awx.main.scheduler.dag_simple import SimpleDAG + class WorkflowDAG(SimpleDAG): def __init__(self, workflow_job=None): super(WorkflowDAG, self).__init__() diff --git a/awx/main/scheduler/dependency_graph.py b/awx/main/scheduler/dependency_graph.py index 08c00977c1..7b66a8fe1b 100644 --- a/awx/main/scheduler/dependency_graph.py +++ b/awx/main/scheduler/dependency_graph.py @@ -9,6 +9,8 @@ from awx.main.scheduler.partial import ( AdHocCommandDict, WorkflowJobDict, ) + + class DependencyGraph(object): PROJECT_UPDATES = 'project_updates' INVENTORY_UPDATES = 'inventory_updates' diff --git a/awx/main/scheduler/partial.py b/awx/main/scheduler/partial.py index e8355cbcfc..d16634f369 100644 --- a/awx/main/scheduler/partial.py +++ b/awx/main/scheduler/partial.py @@ -14,6 +14,7 @@ from awx.main.models import ( WorkflowJob, ) + class PartialModelDict(object): FIELDS = () model = None @@ -60,6 +61,7 @@ class PartialModelDict(object): def task_impact(self): raise RuntimeError("Inherit and implement me") + class JobDict(PartialModelDict): FIELDS = ( 'id', 'status', 'job_template_id', 'inventory_id', 'project_id', @@ -83,6 +85,7 @@ class JobDict(PartialModelDict): start_args = start_args or {} return start_args.get('inventory_sources_already_updated', []) + class ProjectUpdateDict(PartialModelDict): FIELDS = ( 'id', 'status', 'project_id', 'created', 'celery_task_id', @@ -105,6 +108,7 @@ class ProjectUpdateDict(PartialModelDict): } return [cls(o) for o in cls.model.objects.filter(**kv).values(*cls.get_db_values())] + class ProjectUpdateLatestDict(ProjectUpdateDict): FIELDS = ( 'id', 'status', 'project_id', 'created', 'finished', @@ -125,6 +129,7 @@ class ProjectUpdateLatestDict(ProjectUpdateDict): results.append(cls(cls.model.objects.filter(id=qs[0].id).values(*cls.get_db_values())[0])) return results + class InventoryUpdateDict(PartialModelDict): #'inventory_source__update_on_launch', #'inventory_source__update_cache_timeout', @@ -139,6 +144,7 @@ class InventoryUpdateDict(PartialModelDict): def task_impact(self): return 20 + class InventoryUpdateLatestDict(InventoryUpdateDict): #'inventory_source__update_on_launch', #'inventory_source__update_cache_timeout', @@ -166,6 +172,7 @@ class InventoryUpdateLatestDict(InventoryUpdateDict): results.append(cls(cls.model.objects.filter(id=qs[0].id).values(*cls.get_db_values())[0])) return results + class InventorySourceDict(PartialModelDict): FIELDS = ( 'id', @@ -187,6 +194,7 @@ class InventorySourceDict(PartialModelDict): } return [cls(o) for o in cls.model.objects.filter(**kv).values(*cls.get_db_values())] + class SystemJobDict(PartialModelDict): FIELDS = ( 'id', 'created', 'status', @@ -206,6 +214,7 @@ class SystemJobDict(PartialModelDict): } return [cls(o) for o in cls.model.objects.filter(**kv).values(*cls.get_db_values())] + class AdHocCommandDict(PartialModelDict): FIELDS = ( 'id', 'created', 'status', 'inventory_id', @@ -218,6 +227,7 @@ class AdHocCommandDict(PartialModelDict): def task_impact(self): return 20 + class WorkflowJobDict(PartialModelDict): FIELDS = ( 'id', 'created', 'status', 'workflow_job_template_id', diff --git a/awx/main/scheduler/tasks.py b/awx/main/scheduler/tasks.py index 61e5eecc46..5c4c821606 100644 --- a/awx/main/scheduler/tasks.py +++ b/awx/main/scheduler/tasks.py @@ -21,18 +21,22 @@ logger = logging.getLogger('awx.main.scheduler') # Would we need the request loop then? I think so. Even if we get the in-memory # updated model, the call to schedule() may get stale data. + @task def run_job_launch(job_id): TaskManager().schedule() + @task def run_job_complete(job_id): TaskManager().schedule() + @task def run_task_manager(): TaskManager().schedule() + @task def run_fail_inconsistent_running_jobs(): with transaction.atomic(): diff --git a/awx/main/signals.py b/awx/main/signals.py index cb2987beb7..f7df3d3935 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -32,6 +32,7 @@ logger = logging.getLogger('awx.main.signals') # Update has_active_failures for inventory/groups when a Host/Group is deleted, # when a Host-Group or Group-Group relationship is updated, or when a Job is deleted + def emit_job_event_detail(sender, **kwargs): instance = kwargs['instance'] created = kwargs['created'] @@ -45,6 +46,7 @@ def emit_job_event_detail(sender, **kwargs): event_serialized["group_name"] = "job_events" emit_channel_notification('job_events-' + str(instance.job.id), event_serialized) + def emit_ad_hoc_command_event_detail(sender, **kwargs): instance = kwargs['instance'] created = kwargs['created'] @@ -57,6 +59,7 @@ def emit_ad_hoc_command_event_detail(sender, **kwargs): event_serialized["group_name"] = "ad_hoc_command_events" emit_channel_notification('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): @@ -91,6 +94,7 @@ def emit_update_inventory_computed_fields(sender, **kwargs): else: update_inventory_computed_fields.delay(inventory.id, True) + def emit_update_inventory_on_created_or_deleted(sender, **kwargs): if getattr(_inventory_updates, 'is_updating', False): return @@ -111,6 +115,7 @@ def emit_update_inventory_on_created_or_deleted(sender, **kwargs): if inventory is not None: update_inventory_computed_fields.delay(inventory.id, True) + def rebuild_role_ancestor_list(reverse, model, instance, pk_set, action, **kwargs): 'When a role parent is added or removed, update our role hierarchy list' if action == 'post_add': @@ -125,6 +130,7 @@ def rebuild_role_ancestor_list(reverse, model, instance, pk_set, action, **kwarg else: model.rebuild_role_ancestor_list([], [instance.id]) + def sync_superuser_status_to_rbac(instance, **kwargs): 'When the is_superuser flag is changed on a user, reflect that in the membership of the System Admnistrator role' if instance.is_superuser: @@ -132,6 +138,7 @@ def sync_superuser_status_to_rbac(instance, **kwargs): else: Role.singleton(ROLE_SINGLETON_SYSTEM_ADMINISTRATOR).members.remove(instance) + def create_user_role(instance, **kwargs): try: Role.objects.get( @@ -146,6 +153,7 @@ def create_user_role(instance, **kwargs): ) role.members.add(instance) + def org_admin_edit_members(instance, action, model, reverse, pk_set, **kwargs): content_type = ContentType.objects.get_for_model(Organization) @@ -161,6 +169,7 @@ def org_admin_edit_members(instance, action, model, reverse, pk_set, **kwargs): if action == 'pre_remove': instance.content_object.admin_role.children.remove(user.admin_role) + def rbac_activity_stream(instance, sender, **kwargs): user_type = ContentType.objects.get_for_model(User) # Only if we are associating/disassociating @@ -195,6 +204,7 @@ def rbac_activity_stream(instance, sender, **kwargs): activity_stream_associate(sender, instance, role=role, **kwargs) + def cleanup_detached_labels_on_deleted_parent(sender, instance, **kwargs): for l in instance.labels.all(): if l.is_candidate_for_detach(): @@ -226,6 +236,7 @@ pre_delete.connect(cleanup_detached_labels_on_deleted_parent, sender=UnifiedJobT # Migrate hosts, groups to parent group(s) whenever a group is deleted + @receiver(pre_delete, sender=Group) def save_related_pks_before_group_delete(sender, **kwargs): if getattr(_inventory_updates, 'is_removing', False): @@ -236,6 +247,7 @@ def save_related_pks_before_group_delete(sender, **kwargs): instance._saved_hosts_pks = set(instance.hosts.values_list('pk', flat=True)) instance._saved_children_pks = set(instance.children.values_list('pk', flat=True)) + @receiver(post_delete, sender=Group) def migrate_children_from_deleted_group_to_parent_groups(sender, **kwargs): if getattr(_inventory_updates, 'is_removing', False): @@ -269,6 +281,7 @@ def migrate_children_from_deleted_group_to_parent_groups(sender, **kwargs): # Update host pointers to last_job and last_job_host_summary when a job is deleted + def _update_host_last_jhs(host): jhs_qs = JobHostSummary.objects.filter(host__pk=host.pk) try: @@ -286,12 +299,14 @@ def _update_host_last_jhs(host): if update_fields: host.save(update_fields=update_fields) + @receiver(pre_delete, sender=Job) def save_host_pks_before_job_delete(sender, **kwargs): instance = kwargs['instance'] hosts_qs = Host.objects.filter( last_job__pk=instance.pk) instance._saved_hosts_pks = set(hosts_qs.values_list('pk', flat=True)) + @receiver(post_delete, sender=Job) def update_host_last_job_after_job_deleted(sender, **kwargs): instance = kwargs['instance'] @@ -301,6 +316,7 @@ def update_host_last_job_after_job_deleted(sender, **kwargs): # Set via ActivityStreamRegistrar to record activity stream events + class ActivityStreamEnabled(threading.local): def __init__(self): self.enabled = True @@ -341,6 +357,7 @@ model_serializer_mapping = { Notification: NotificationSerializer, } + def activity_stream_create(sender, instance, created, **kwargs): if created and activity_stream_enabled: # Skip recording any inventory source directly associated with a group. @@ -363,6 +380,7 @@ def activity_stream_create(sender, instance, created, **kwargs): if instance._meta.model_name != 'setting': # Is not conf.Setting instance getattr(activity_entry, object1).add(instance) + def activity_stream_update(sender, instance, **kwargs): if instance.id is None: return @@ -386,6 +404,7 @@ def activity_stream_update(sender, instance, **kwargs): if instance._meta.model_name != 'setting': # Is not conf.Setting instance getattr(activity_entry, object1).add(instance) + def activity_stream_delete(sender, instance, **kwargs): if not activity_stream_enabled: return @@ -400,6 +419,7 @@ def activity_stream_delete(sender, instance, **kwargs): object1=object1) activity_entry.save() + def activity_stream_associate(sender, instance, **kwargs): if not activity_stream_enabled: return diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 6817bda76b..47a6583e9a 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -69,6 +69,7 @@ Try upgrading OpenSSH or providing your private key in an different format. \ logger = logging.getLogger('awx.main.tasks') + @celeryd_init.connect def celery_startup(conf=None, **kwargs): # Re-init all schedules @@ -81,6 +82,7 @@ def celery_startup(conf=None, **kwargs): except Exception as e: logger.error("Failed to rebuild schedule {}: {}".format(sch, e)) + @task(queue='default') def send_notifications(notification_list, job_id=None): if not isinstance(notification_list, list): @@ -102,6 +104,7 @@ def send_notifications(notification_list, job_id=None): if job_id is not None: job_actual.notifications.add(notification) + @task(bind=True, queue='default') def run_administrative_checks(self): if not settings.TOWER_ADMIN_ALERTS: @@ -122,10 +125,12 @@ def run_administrative_checks(self): tower_admin_emails, fail_silently=True) + @task(bind=True, queue='default') def cleanup_authtokens(self): AuthToken.objects.filter(expires__lt=now()).delete() + @task(bind=True) def cluster_node_heartbeat(self): inst = Instance.objects.filter(hostname=settings.CLUSTER_HOST_ID) @@ -136,6 +141,7 @@ def cluster_node_heartbeat(self): return raise RuntimeError("Cluster Host Not Found: {}".format(settings.CLUSTER_HOST_ID)) + @task(bind=True, queue='default') def tower_periodic_scheduler(self): run_now = now() @@ -165,6 +171,7 @@ def tower_periodic_scheduler(self): emit_channel_notification('schedules-changed', dict(id=schedule.id, group_name="schedules")) state.save() + def _send_notification_templates(instance, status_str): if status_str not in ['succeeded', 'failed']: raise ValueError(_("status_str must be either succeeded or failed")) @@ -192,6 +199,7 @@ def handle_work_success(self, result, task_actual): from awx.main.scheduler.tasks import run_job_complete run_job_complete.delay(instance.id) + @task(bind=True, queue='default') def handle_work_error(self, task_id, subtasks=None): print('Executing error task id %s, subtasks: %s' % @@ -231,6 +239,7 @@ def handle_work_error(self, task_id, subtasks=None): run_job_complete.delay(first_instance.id) pass + @task(queue='default') def update_inventory_computed_fields(inventory_id, should_update_hosts=True): ''' @@ -1222,6 +1231,7 @@ class RunProjectUpdate(BaseTask): else: logger.error("Could not find scm revision in check") + class RunInventoryUpdate(BaseTask): name = 'awx.main.tasks.run_inventory_update' @@ -1556,6 +1566,7 @@ 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. diff --git a/awx/main/tests/URI.py b/awx/main/tests/URI.py index d04da03436..54fd1d8b06 100644 --- a/awx/main/tests/URI.py +++ b/awx/main/tests/URI.py @@ -1,6 +1,8 @@ # Helps with test cases. # Save all components of a uri (i.e. scheme, username, password, etc.) so that # when we construct a uri string and decompose it, we can verify the decomposition + + class URI(object): DEFAULTS = { 'scheme' : 'http', diff --git a/awx/main/tests/base.py b/awx/main/tests/base.py index 907caa7e75..ca5f8237d2 100644 --- a/awx/main/tests/base.py +++ b/awx/main/tests/base.py @@ -41,6 +41,7 @@ TEST_PLAYBOOK = '''- hosts: mygroup command: test 1 = 1 ''' + class QueueTestMixin(object): def start_queue(self): self.start_rabbit() @@ -83,6 +84,7 @@ class QueueStartStopTestMixin(QueueTestMixin): super(QueueStartStopTestMixin, self).tearDown() self.terminate_queue() + class MockCommonlySlowTestMixin(object): def __init__(self, *args, **kwargs): from awx.api import generics @@ -91,6 +93,8 @@ class MockCommonlySlowTestMixin(object): ansible_version = get_ansible_version() + + class BaseTestMixin(MockCommonlySlowTestMixin): ''' Mixin with shared code for use by all test cases. @@ -676,17 +680,20 @@ class BaseTestMixin(MockCommonlySlowTestMixin): u'expected no traceback, got:\n%s' % job.result_traceback) + class BaseTest(BaseTestMixin, django.test.TestCase): ''' Base class for unit tests. ''' + class BaseTransactionTest(BaseTestMixin, django.test.TransactionTestCase): ''' Base class for tests requiring transactions (or where the test database needs to be accessed by subprocesses). ''' + @override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True, ANSIBLE_TRANSPORT='local') @@ -698,6 +705,7 @@ class BaseLiveServerTest(BaseTestMixin, django.test.LiveServerTestCase): super(BaseLiveServerTest, self).setUp() settings.INTERNAL_API_URL = self.live_server_url + @override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True, ANSIBLE_TRANSPORT='local', diff --git a/awx/main/tests/conftest.py b/awx/main/tests/conftest.py index 3412ca1ed8..1fdb3d1ca2 100644 --- a/awx/main/tests/conftest.py +++ b/awx/main/tests/conftest.py @@ -11,22 +11,27 @@ from awx.main.tests.factories import ( create_workflow_job_template, ) + @pytest.fixture def job_template_factory(): return create_job_template + @pytest.fixture def organization_factory(): return create_organization + @pytest.fixture def notification_template_factory(): return create_notification_template + @pytest.fixture def survey_spec_factory(): return create_survey_spec + @pytest.fixture def job_template_with_survey_passwords_factory(job_template_factory): def rf(persisted): @@ -38,22 +43,27 @@ def job_template_with_survey_passwords_factory(job_template_factory): return objects.job_template return rf + @pytest.fixture def job_with_secret_key_unit(job_with_secret_key_factory): return job_with_secret_key_factory(persisted=False) + @pytest.fixture def workflow_job_template_factory(): return create_workflow_job_template + @pytest.fixture def get_ssh_version(mocker): return mocker.patch('awx.main.tasks.get_ssh_version', return_value='OpenSSH_6.9p1, LibreSSL 2.1.8') + @pytest.fixture def job_template_with_survey_passwords_unit(job_template_with_survey_passwords_factory): return job_template_with_survey_passwords_factory(persisted=False) + @pytest.fixture def enterprise_license(): from awx.main.task_engine import TaskEnhancer diff --git a/awx/main/tests/factories/fixtures.py b/awx/main/tests/factories/fixtures.py index 8f6e5df414..41d95d8089 100644 --- a/awx/main/tests/factories/fixtures.py +++ b/awx/main/tests/factories/fixtures.py @@ -25,6 +25,7 @@ from awx.main.models import ( # persisted=False # + def mk_instance(persisted=True): if not persisted: raise RuntimeError('creating an Instance requires persisted=True') @@ -158,6 +159,7 @@ def mk_job_template(name, job_type='run', jt.save() return jt + def mk_workflow_job(status='new', workflow_job_template=None, extra_vars={}, persisted=True): job = WorkflowJob(status=status, extra_vars=json.dumps(extra_vars)) @@ -168,6 +170,7 @@ def mk_workflow_job(status='new', workflow_job_template=None, extra_vars={}, job.save() return job + def mk_workflow_job_template(name, extra_vars='', spec=None, organization=None, persisted=True): if extra_vars: extra_vars = json.dumps(extra_vars) @@ -182,6 +185,7 @@ def mk_workflow_job_template(name, extra_vars='', spec=None, organization=None, wfjt.save() return wfjt + def mk_workflow_job_template_node(workflow_job_template=None, unified_job_template=None, success_nodes=None, @@ -197,6 +201,7 @@ def mk_workflow_job_template_node(workflow_job_template=None, workflow_node.save() return workflow_node + def mk_workflow_job_node(unified_job_template=None, success_nodes=None, failure_nodes=None, diff --git a/awx/main/tests/factories/objects.py b/awx/main/tests/factories/objects.py index 9f739cc9cf..7de49d998c 100644 --- a/awx/main/tests/factories/objects.py +++ b/awx/main/tests/factories/objects.py @@ -2,6 +2,7 @@ from collections import namedtuple from .exc import NotUnique + def generate_objects(artifacts, kwargs): '''generate_objects takes a list of artifacts that are supported by a create function and compares it to the kwargs passed in to the create diff --git a/awx/main/tests/factories/tower.py b/awx/main/tests/factories/tower.py index 5a23a01577..9eb26538e8 100644 --- a/awx/main/tests/factories/tower.py +++ b/awx/main/tests/factories/tower.py @@ -87,6 +87,7 @@ def apply_roles(roles, objects, persisted): else: raise RuntimeError('unable to add non-user {} for members list of {}'.format(member_str, obj_str)) + def generate_users(organization, teams, superuser, persisted, **kwargs): '''generate_users evaluates a mixed list of User objects and strings. If a string is encountered a user with that username is created and added to the lookup dict. @@ -112,6 +113,7 @@ def generate_users(organization, teams, superuser, persisted, **kwargs): users[p1] = mk_user(p1, organization=organization, team=None, is_superuser=superuser, persisted=persisted) return users + def generate_teams(organization, persisted, **kwargs): '''generate_teams evalutes a mixed list of Team objects and strings. If a string is encountered a team with that string name is created and added to the lookup dict. @@ -126,6 +128,7 @@ def generate_teams(organization, persisted, **kwargs): teams[t] = mk_team(t, organization=organization, persisted=persisted) return teams + def create_survey_spec(variables=None, default_type='integer', required=True): ''' Returns a valid survey spec for a job template, based on the input @@ -176,6 +179,7 @@ def create_survey_spec(variables=None, default_type='integer', required=True): # or encapsulated by specific factory fixtures in a conftest # + def create_job_template(name, roles=None, persisted=True, **kwargs): Objects = generate_objects(["job_template", "jobs", "organization", @@ -260,6 +264,7 @@ def create_job_template(name, roles=None, persisted=True, **kwargs): organization=org, survey=spec,) + def create_organization(name, roles=None, persisted=True, **kwargs): Objects = generate_objects(["organization", "teams", "users", @@ -319,6 +324,7 @@ def create_organization(name, roles=None, persisted=True, **kwargs): notification_templates=_Mapped(notification_templates), inventories=_Mapped(inventories)) + def create_notification_template(name, roles=None, persisted=True, **kwargs): Objects = generate_objects(["notification_template", "organization", @@ -346,6 +352,7 @@ def create_notification_template(name, roles=None, persisted=True, **kwargs): superusers=_Mapped(superusers), teams=teams) + def generate_workflow_job_template_nodes(workflow_job_template, persisted, **kwargs): @@ -372,6 +379,7 @@ def generate_workflow_job_template_nodes(workflow_job_template, for related_index in workflow_job_template_nodes[i][node_type]: getattr(new_node, node_type).add(new_nodes[related_index]) + # TODO: Implement survey and jobs def create_workflow_job_template(name, organization=None, persisted=True, **kwargs): Objects = generate_objects(["workflow_job_template", diff --git a/awx/main/tests/functional/api/test_activity_streams.py b/awx/main/tests/functional/api/test_activity_streams.py index 85bb70f65d..8315504996 100644 --- a/awx/main/tests/functional/api/test_activity_streams.py +++ b/awx/main/tests/functional/api/test_activity_streams.py @@ -7,13 +7,16 @@ from awx.main.access import ActivityStreamAccess from django.core.urlresolvers import reverse + def mock_feature_enabled(feature): return True + @pytest.fixture def activity_stream_entry(organization, org_admin): return ActivityStream.objects.filter(organization__pk=organization.pk, user=org_admin, operation='associate').first() + @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.django_db def test_get_activity_stream_list(monkeypatch, organization, get, user, settings): @@ -23,6 +26,7 @@ def test_get_activity_stream_list(monkeypatch, organization, get, user, settings assert response.status_code == 200 + @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.django_db def test_basic_fields(monkeypatch, organization, get, user, settings): @@ -43,6 +47,7 @@ def test_basic_fields(monkeypatch, organization, get, user, settings): assert 'organization' in response.data['summary_fields'] assert response.data['summary_fields']['organization'][0]['name'] == 'test-org' + @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.django_db def test_middleware_actor_added(monkeypatch, post, get, user, settings): @@ -65,6 +70,7 @@ def test_middleware_actor_added(monkeypatch, post, get, user, settings): assert response.status_code == 200 assert response.data['summary_fields']['actor']['username'] == 'admin-poster' + @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.django_db def test_rbac_stream_resource_roles(activity_stream_entry, organization, org_admin, settings): @@ -74,6 +80,7 @@ def test_rbac_stream_resource_roles(activity_stream_entry, organization, org_adm assert activity_stream_entry.role.first() == organization.admin_role assert activity_stream_entry.object_relationship_type == 'awx.main.models.organization.Organization.admin_role' + @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.django_db def test_rbac_stream_user_roles(activity_stream_entry, organization, org_admin, settings): @@ -83,6 +90,7 @@ def test_rbac_stream_user_roles(activity_stream_entry, organization, org_admin, assert activity_stream_entry.role.first() == organization.admin_role assert activity_stream_entry.object_relationship_type == 'awx.main.models.organization.Organization.admin_role' + @pytest.mark.django_db @pytest.mark.activity_stream_access @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @@ -94,6 +102,7 @@ def test_stream_access_cant_change(activity_stream_entry, organization, org_admi assert not access.can_change(activity_stream_entry, {'organization': None}) assert not access.can_delete(activity_stream_entry) + @pytest.mark.django_db @pytest.mark.activity_stream_access @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @@ -129,6 +138,7 @@ def test_stream_queryset_hides_shows_items( assert queryset.filter(team__pk=team.pk, operation='create').count() == 1 assert queryset.filter(notification_template__pk=notification_template.pk, operation='create').count() == 1 + @pytest.mark.django_db @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) def test_stream_user_direct_role_updates(get, post, organization_factory): diff --git a/awx/main/tests/functional/api/test_adhoc.py b/awx/main/tests/functional/api/test_adhoc.py index e7029b0c79..d2ab578a22 100644 --- a/awx/main/tests/functional/api/test_adhoc.py +++ b/awx/main/tests/functional/api/test_adhoc.py @@ -4,7 +4,6 @@ import pytest from django.core.urlresolvers import reverse - """ def run_test_ad_hoc_command(self, **kwargs): # Post to list to start a new ad hoc command. @@ -23,6 +22,7 @@ from django.core.urlresolvers import reverse return self.post(url, data, expect=expect) """ + @pytest.fixture def post_adhoc(post, inventory, machine_credential): def f(url, data, user, expect=201): @@ -46,7 +46,6 @@ def post_adhoc(post, inventory, machine_credential): return f - @pytest.mark.django_db def test_admin_post_ad_hoc_command_list(admin, post_adhoc, inventory, machine_credential): res = post_adhoc(reverse('api:ad_hoc_command_list'), {}, admin, expect=201) @@ -65,35 +64,42 @@ def test_admin_post_ad_hoc_command_list(admin, post_adhoc, inventory, machine_cr def test_empty_post_403(admin, post): post(reverse('api:ad_hoc_command_list'), {}, admin, expect=400) + @pytest.mark.django_db def test_empty_put_405(admin, put): put(reverse('api:ad_hoc_command_list'), {}, admin, expect=405) + @pytest.mark.django_db def test_empty_patch_405(admin, patch): patch(reverse('api:ad_hoc_command_list'), {}, admin, expect=405) + @pytest.mark.django_db def test_empty_delete_405(admin, delete): delete(reverse('api:ad_hoc_command_list'), admin, expect=405) + @pytest.mark.django_db def test_user_post_ad_hoc_command_list(alice, post_adhoc, inventory, machine_credential): inventory.adhoc_role.members.add(alice) machine_credential.use_role.members.add(alice) post_adhoc(reverse('api:ad_hoc_command_list'), {}, alice, expect=201) + @pytest.mark.django_db def test_user_post_ad_hoc_command_list_xfail(alice, post_adhoc, inventory, machine_credential): inventory.read_role.members.add(alice) # just read access? no dice. machine_credential.use_role.members.add(alice) post_adhoc(reverse('api:ad_hoc_command_list'), {}, alice, expect=403) + @pytest.mark.django_db def test_user_post_ad_hoc_command_list_without_creds(alice, post_adhoc, inventory, machine_credential): inventory.adhoc_role.members.add(alice) post_adhoc(reverse('api:ad_hoc_command_list'), {}, alice, expect=403) + @pytest.mark.django_db def test_user_post_ad_hoc_command_list_without_inventory(alice, post_adhoc, inventory, machine_credential): machine_credential.use_role.members.add(alice) @@ -145,4 +151,3 @@ def test_bad_data3(admin, post_adhoc): @pytest.mark.django_db def test_bad_data4(admin, post_adhoc): post_adhoc(reverse('api:ad_hoc_command_list'), {'forks': -1}, admin, expect=400) - diff --git a/awx/main/tests/functional/api/test_create_attach_views.py b/awx/main/tests/functional/api/test_create_attach_views.py index 5399356a21..48f3aadc7b 100644 --- a/awx/main/tests/functional/api/test_create_attach_views.py +++ b/awx/main/tests/functional/api/test_create_attach_views.py @@ -16,6 +16,7 @@ def test_user_role_view_access(rando, inventory, mocker, post): inventory.admin_role, rando, 'members', data, skip_sub_obj_read_check=False) + @pytest.mark.django_db def test_team_role_view_access(rando, team, inventory, mocker, post): "Assure correct access method is called when assigning teams new roles" @@ -30,6 +31,7 @@ def test_team_role_view_access(rando, team, inventory, mocker, post): inventory.admin_role, team, 'member_role.parents', data, skip_sub_obj_read_check=False) + @pytest.mark.django_db def test_role_team_view_access(rando, team, inventory, mocker, post): """Assure that /role/N/teams/ enforces the same permission restrictions diff --git a/awx/main/tests/functional/api/test_credential.py b/awx/main/tests/functional/api/test_credential.py index 0e22f3d845..bd6cd25841 100644 --- a/awx/main/tests/functional/api/test_credential.py +++ b/awx/main/tests/functional/api/test_credential.py @@ -8,6 +8,7 @@ from django.core.urlresolvers import reverse # user credential creation # + @pytest.mark.django_db def test_create_user_credential_via_credentials_list(post, get, alice): response = post(reverse('api:credential_list'), { @@ -21,6 +22,7 @@ def test_create_user_credential_via_credentials_list(post, get, alice): assert response.status_code == 200 assert response.data['count'] == 1 + @pytest.mark.django_db def test_credential_validation_error_with_bad_user(post, admin): response = post(reverse('api:credential_list'), { @@ -31,6 +33,7 @@ def test_credential_validation_error_with_bad_user(post, admin): assert response.status_code == 400 assert response.data['user'][0] == 'Incorrect type. Expected pk value, received unicode.' + @pytest.mark.django_db def test_create_user_credential_via_user_credentials_list(post, get, alice): response = post(reverse('api:user_credentials_list', args=(alice.pk,)), { @@ -44,6 +47,7 @@ def test_create_user_credential_via_user_credentials_list(post, get, alice): assert response.status_code == 200 assert response.data['count'] == 1 + @pytest.mark.django_db def test_create_user_credential_via_credentials_list_xfail(post, alice, bob): response = post(reverse('api:credential_list'), { @@ -53,6 +57,7 @@ def test_create_user_credential_via_credentials_list_xfail(post, alice, bob): }, alice) assert response.status_code == 403 + @pytest.mark.django_db def test_create_user_credential_via_user_credentials_list_xfail(post, alice, bob): response = post(reverse('api:user_credentials_list', args=(bob.pk,)), { @@ -67,6 +72,7 @@ def test_create_user_credential_via_user_credentials_list_xfail(post, alice, bob # team credential creation # + @pytest.mark.django_db def test_create_team_credential(post, get, team, organization, org_admin, team_member): response = post(reverse('api:credential_list'), { @@ -83,6 +89,7 @@ def test_create_team_credential(post, get, team, organization, org_admin, team_m # Assure that credential's organization is implictly set to team's org assert response.data['results'][0]['summary_fields']['organization']['id'] == team.organization.id + @pytest.mark.django_db def test_create_team_credential_via_team_credentials_list(post, get, team, org_admin, team_member): response = post(reverse('api:team_credentials_list', args=(team.pk,)), { @@ -96,6 +103,7 @@ def test_create_team_credential_via_team_credentials_list(post, get, team, org_a assert response.status_code == 200 assert response.data['count'] == 1 + @pytest.mark.django_db def test_create_team_credential_by_urelated_user_xfail(post, team, organization, alice, team_member): response = post(reverse('api:credential_list'), { @@ -106,6 +114,7 @@ def test_create_team_credential_by_urelated_user_xfail(post, team, organization, }, alice) assert response.status_code == 403 + @pytest.mark.django_db def test_create_team_credential_by_team_member_xfail(post, team, organization, alice, team_member): # Members can't add credentials, only org admins.. for now? @@ -122,6 +131,7 @@ def test_create_team_credential_by_team_member_xfail(post, team, organization, a # Permission granting # + @pytest.mark.django_db def test_grant_org_credential_to_org_user_through_role_users(post, credential, organization, org_admin, org_member): credential.organization = organization @@ -131,6 +141,7 @@ def test_grant_org_credential_to_org_user_through_role_users(post, credential, o }, org_admin) assert response.status_code == 204 + @pytest.mark.django_db def test_grant_org_credential_to_org_user_through_user_roles(post, credential, organization, org_admin, org_member): credential.organization = organization @@ -140,6 +151,7 @@ def test_grant_org_credential_to_org_user_through_user_roles(post, credential, o }, org_admin) assert response.status_code == 204 + @pytest.mark.django_db def test_grant_org_credential_to_non_org_user_through_role_users(post, credential, organization, org_admin, alice): credential.organization = organization @@ -149,6 +161,7 @@ def test_grant_org_credential_to_non_org_user_through_role_users(post, credentia }, org_admin) assert response.status_code == 400 + @pytest.mark.django_db def test_grant_org_credential_to_non_org_user_through_user_roles(post, credential, organization, org_admin, alice): credential.organization = organization @@ -158,6 +171,7 @@ def test_grant_org_credential_to_non_org_user_through_user_roles(post, credentia }, org_admin) assert response.status_code == 400 + @pytest.mark.django_db def test_grant_private_credential_to_user_through_role_users(post, credential, alice, bob): # normal users can't do this @@ -167,6 +181,7 @@ def test_grant_private_credential_to_user_through_role_users(post, credential, a }, alice) assert response.status_code == 400 + @pytest.mark.django_db def test_grant_private_credential_to_org_user_through_role_users(post, credential, org_admin, org_member): # org admins can't either @@ -176,6 +191,7 @@ def test_grant_private_credential_to_org_user_through_role_users(post, credentia }, org_admin) assert response.status_code == 400 + @pytest.mark.django_db def test_sa_grant_private_credential_to_user_through_role_users(post, credential, admin, bob): # but system admins can @@ -184,6 +200,7 @@ def test_sa_grant_private_credential_to_user_through_role_users(post, credential }, admin) assert response.status_code == 204 + @pytest.mark.django_db def test_grant_private_credential_to_user_through_user_roles(post, credential, alice, bob): # normal users can't do this @@ -193,6 +210,7 @@ def test_grant_private_credential_to_user_through_user_roles(post, credential, a }, alice) assert response.status_code == 400 + @pytest.mark.django_db def test_grant_private_credential_to_org_user_through_user_roles(post, credential, org_admin, org_member): # org admins can't either @@ -202,6 +220,7 @@ def test_grant_private_credential_to_org_user_through_user_roles(post, credentia }, org_admin) assert response.status_code == 400 + @pytest.mark.django_db def test_sa_grant_private_credential_to_user_through_user_roles(post, credential, admin, bob): # but system admins can @@ -210,6 +229,7 @@ def test_sa_grant_private_credential_to_user_through_user_roles(post, credential }, admin) assert response.status_code == 204 + @pytest.mark.django_db def test_grant_org_credential_to_team_through_role_teams(post, credential, organization, org_admin, org_auditor, team): assert org_auditor not in credential.read_role @@ -221,6 +241,7 @@ def test_grant_org_credential_to_team_through_role_teams(post, credential, organ assert response.status_code == 204 assert org_auditor in credential.read_role + @pytest.mark.django_db def test_grant_org_credential_to_team_through_team_roles(post, credential, organization, org_admin, org_auditor, team): assert org_auditor not in credential.read_role @@ -232,6 +253,7 @@ def test_grant_org_credential_to_team_through_team_roles(post, credential, organ assert response.status_code == 204 assert org_auditor in credential.read_role + @pytest.mark.django_db def test_sa_grant_private_credential_to_team_through_role_teams(post, credential, admin, team): # not even a system admin can grant a private cred to a team though @@ -240,6 +262,7 @@ def test_sa_grant_private_credential_to_team_through_role_teams(post, credential }, admin) assert response.status_code == 400 + @pytest.mark.django_db def test_sa_grant_private_credential_to_team_through_team_roles(post, credential, admin, team): # not even a system admin can grant a private cred to a team though @@ -249,12 +272,11 @@ def test_sa_grant_private_credential_to_team_through_team_roles(post, credential assert response.status_code == 400 - - # # organization credentials # + @pytest.mark.django_db def test_create_org_credential_as_not_admin(post, organization, org_member): response = post(reverse('api:credential_list'), { @@ -264,6 +286,7 @@ def test_create_org_credential_as_not_admin(post, organization, org_member): }, org_member) assert response.status_code == 403 + @pytest.mark.django_db def test_create_org_credential_as_admin(post, organization, org_admin): response = post(reverse('api:credential_list'), { @@ -273,6 +296,7 @@ def test_create_org_credential_as_admin(post, organization, org_admin): }, org_admin) assert response.status_code == 201 + @pytest.mark.django_db def test_credential_detail(post, get, organization, org_admin): response = post(reverse('api:credential_list'), { @@ -288,6 +312,7 @@ def test_credential_detail(post, get, organization, org_admin): related_fields = response.data['related'] assert 'organization' in related_fields + @pytest.mark.django_db def test_list_created_org_credentials(post, get, organization, org_admin, org_member): response = post(reverse('api:credential_list'), { @@ -336,6 +361,7 @@ def test_cant_change_organization(patch, credential, organization, org_admin): }, org_admin) assert response.status_code == 403 + @pytest.mark.django_db def test_cant_add_organization(patch, credential, organization, org_admin): assert credential.organization is None @@ -350,6 +376,7 @@ def test_cant_add_organization(patch, credential, organization, org_admin): # Openstack Credentials # + @pytest.mark.django_db def test_openstack_create_ok(post, organization, admin): data = { @@ -364,6 +391,7 @@ def test_openstack_create_ok(post, organization, admin): response = post(reverse('api:credential_list'), data, admin) assert response.status_code == 201 + @pytest.mark.django_db def test_openstack_create_fail_required_fields(post, organization, admin): data = { @@ -383,6 +411,7 @@ def test_openstack_create_fail_required_fields(post, organization, admin): # misc xfail conditions # + @pytest.mark.django_db def test_create_credential_missing_user_team_org_xfail(post, admin): # Must specify one of user, team, or organization @@ -391,4 +420,3 @@ def test_create_credential_missing_user_team_org_xfail(post, admin): 'username': 'someusername', }, admin) assert response.status_code == 400 - diff --git a/awx/main/tests/functional/api/test_fact_versions.py b/awx/main/tests/functional/api/test_fact_versions.py index fc521eb615..8fe06ff7af 100644 --- a/awx/main/tests/functional/api/test_fact_versions.py +++ b/awx/main/tests/functional/api/test_fact_versions.py @@ -13,12 +13,15 @@ from awx.main.utils import timestamp_apiformat from django.core.urlresolvers import reverse from django.utils import timezone + def mock_feature_enabled(feature): return True + def mock_feature_disabled(feature): return False + def setup_common(hosts, fact_scans, get, user, epoch=timezone.now(), get_params={}, host_count=1): hosts = hosts(host_count=host_count) fact_scans(fact_scans=3, timestamp_epoch=epoch) @@ -28,6 +31,7 @@ def setup_common(hosts, fact_scans, get, user, epoch=timezone.now(), get_params= return (hosts[0], response) + def check_url(url1_full, fact_known, module): url1_split = urlparse.urlsplit(url1_full) url1 = url1_split.path @@ -42,16 +46,19 @@ def check_url(url1_full, fact_known, module): url2_params_sorted = sorted(url2_params, key=lambda val: val[0]) assert urllib.urlencode(url1_params_sorted) == urllib.urlencode(url2_params_sorted) + def check_response_facts(facts_known, response): for i, fact_known in enumerate(facts_known): assert fact_known.module == response.data['results'][i]['module'] assert timestamp_apiformat(fact_known.timestamp) == response.data['results'][i]['timestamp'] check_url(response.data['results'][i]['related']['fact_view'], fact_known, fact_known.module) + def check_system_tracking_feature_forbidden(response): assert 402 == response.status_code assert 'Your license does not permit use of system tracking.' == response.data['detail'] + @mock.patch('awx.api.views.feature_enabled', new=mock_feature_disabled) @pytest.mark.django_db @pytest.mark.license_feature @@ -62,6 +69,7 @@ def test_system_tracking_license_get(hosts, get, user): check_system_tracking_feature_forbidden(response) + @mock.patch('awx.api.views.feature_enabled', new=mock_feature_disabled) @pytest.mark.django_db @pytest.mark.license_feature @@ -72,6 +80,7 @@ def test_system_tracking_license_options(hosts, options, user): check_system_tracking_feature_forbidden(response) + @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.django_db @pytest.mark.license_feature @@ -85,6 +94,7 @@ def test_no_facts_db(hosts, get, user): } assert response_expected == response.data + @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.django_db def test_basic_fields(hosts, fact_scans, get, user): @@ -101,6 +111,7 @@ def test_basic_fields(hosts, fact_scans, get, user): assert 'timestamp' in results[0] assert 'module' in results[0] + @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.django_db @pytest.mark.license_feature @@ -117,6 +128,7 @@ def test_basic_options_fields(hosts, fact_scans, options, user): assert ("services", "Services") in response.data['actions']['GET']['module']['choices'] assert ("packages", "Packages") in response.data['actions']['GET']['module']['choices'] + @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.django_db def test_related_fact_view(hosts, fact_scans, get, user): @@ -130,6 +142,7 @@ def test_related_fact_view(hosts, fact_scans, get, user): for i, fact_known in enumerate(facts_known): check_url(response.data['results'][i]['related']['fact_view'], fact_known, fact_known.module) + @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.django_db def test_multiple_hosts(hosts, fact_scans, get, user): @@ -143,6 +156,7 @@ def test_multiple_hosts(hosts, fact_scans, get, user): for i, fact_known in enumerate(facts_known): check_url(response.data['results'][i]['related']['fact_view'], fact_known, fact_known.module) + @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.django_db def test_param_to_from(hosts, fact_scans, get, user): @@ -159,6 +173,7 @@ def test_param_to_from(hosts, fact_scans, get, user): check_response_facts(facts_known, response) + @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.django_db def test_param_module(hosts, fact_scans, get, user): @@ -174,6 +189,7 @@ def test_param_module(hosts, fact_scans, get, user): check_response_facts(facts_known, response) + @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.django_db def test_param_from(hosts, fact_scans, get, user): @@ -189,6 +205,7 @@ def test_param_from(hosts, fact_scans, get, user): check_response_facts(facts_known, response) + @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.django_db def test_param_to(hosts, fact_scans, get, user): @@ -204,6 +221,7 @@ def test_param_to(hosts, fact_scans, get, user): check_response_facts(facts_known, response) + def _test_user_access_control(hosts, fact_scans, get, user_obj, team_obj): hosts = hosts(host_count=1) fact_scans(fact_scans=1) @@ -214,6 +232,7 @@ def _test_user_access_control(hosts, fact_scans, get, user_obj, team_obj): response = get(url, user_obj) return response + @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.ac @pytest.mark.django_db @@ -224,6 +243,7 @@ def test_normal_user_403(hosts, fact_scans, get, user, team): assert 403 == response.status_code assert "You do not have permission to perform this action." == response.data['detail'] + @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.ac @pytest.mark.django_db @@ -233,6 +253,7 @@ def test_super_user_ok(hosts, fact_scans, get, user, team): assert 200 == response.status_code + @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.ac @pytest.mark.django_db @@ -244,6 +265,7 @@ def test_user_admin_ok(organization, hosts, fact_scans, get, user, team): assert 200 == response.status_code + @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.ac @pytest.mark.django_db @@ -255,4 +277,3 @@ def test_user_admin_403(organization, organizations, hosts, fact_scans, get, use response = _test_user_access_control(hosts, fact_scans, get, user_admin, team) assert 403 == response.status_code - diff --git a/awx/main/tests/functional/api/test_fact_view.py b/awx/main/tests/functional/api/test_fact_view.py index fd646d9456..c97bf4ab2d 100644 --- a/awx/main/tests/functional/api/test_fact_view.py +++ b/awx/main/tests/functional/api/test_fact_view.py @@ -6,12 +6,15 @@ from awx.main.utils import timestamp_apiformat from django.core.urlresolvers import reverse from django.utils import timezone + def mock_feature_enabled(feature): return True + def mock_feature_disabled(feature): return False + # TODO: Consider making the fact_scan() fixture a Class, instead of a function, and move this method into it def find_fact(facts, host_id, module_name, timestamp): for f in facts: @@ -19,6 +22,7 @@ def find_fact(facts, host_id, module_name, timestamp): return f raise RuntimeError('fact <%s, %s, %s> not found in %s', (host_id, module_name, timestamp, facts)) + def setup_common(hosts, fact_scans, get, user, epoch=timezone.now(), module_name='ansible', get_params={}): hosts = hosts(host_count=1) facts = fact_scans(fact_scans=1, timestamp_epoch=epoch) @@ -29,10 +33,12 @@ def setup_common(hosts, fact_scans, get, user, epoch=timezone.now(), module_name fact_known = find_fact(facts, hosts[0].id, module_name, epoch) return (fact_known, response) + def check_system_tracking_feature_forbidden(response): assert 402 == response.status_code assert 'Your license does not permit use of system tracking.' == response.data['detail'] + @mock.patch('awx.api.views.feature_enabled', new=mock_feature_disabled) @pytest.mark.django_db @pytest.mark.license_feature @@ -43,6 +49,7 @@ def test_system_tracking_license_get(hosts, get, user): check_system_tracking_feature_forbidden(response) + @mock.patch('awx.api.views.feature_enabled', new=mock_feature_disabled) @pytest.mark.django_db @pytest.mark.license_feature @@ -53,6 +60,7 @@ def test_system_tracking_license_options(hosts, options, user): check_system_tracking_feature_forbidden(response) + @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.django_db def test_no_fact_found(hosts, get, user): @@ -66,6 +74,7 @@ def test_no_fact_found(hosts, get, user): assert 404 == response.status_code assert expected_response == response.data + @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.django_db def test_basic_fields(hosts, fact_scans, get, user): @@ -88,6 +97,7 @@ def test_basic_fields(hosts, fact_scans, get, user): assert 'host' in response.data['related'] assert reverse('api:host_detail', args=(hosts[0].pk,)) == response.data['related']['host'] + @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.django_db def test_content(hosts, fact_scans, get, user, fact_ansible_json): @@ -98,6 +108,7 @@ def test_content(hosts, fact_scans, get, user, fact_ansible_json): assert timestamp_apiformat(fact_known.timestamp) == response.data['timestamp'] assert fact_known.module == response.data['module'] + def _test_search_by_module(hosts, fact_scans, get, user, fact_json, module_name): params = { 'module': module_name @@ -108,16 +119,19 @@ def _test_search_by_module(hosts, fact_scans, get, user, fact_json, module_name) assert timestamp_apiformat(fact_known.timestamp) == response.data['timestamp'] assert module_name == response.data['module'] + @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.django_db def test_search_by_module_packages(hosts, fact_scans, get, user, fact_packages_json): _test_search_by_module(hosts, fact_scans, get, user, fact_packages_json, 'packages') + @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.django_db def test_search_by_module_services(hosts, fact_scans, get, user, fact_services_json): _test_search_by_module(hosts, fact_scans, get, user, fact_services_json, 'services') + @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.django_db def test_search_by_timestamp_and_module(hosts, fact_scans, get, user, fact_packages_json): @@ -128,6 +142,7 @@ def test_search_by_timestamp_and_module(hosts, fact_scans, get, user, fact_packa assert fact_known.id == response.data['id'] + def _test_user_access_control(hosts, fact_scans, get, user_obj, team_obj): hosts = hosts(host_count=1) fact_scans(fact_scans=1) @@ -138,6 +153,7 @@ def _test_user_access_control(hosts, fact_scans, get, user_obj, team_obj): response = get(url, user_obj) return response + @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.ac @pytest.mark.django_db @@ -148,6 +164,7 @@ def test_normal_user_403(hosts, fact_scans, get, user, team): assert 403 == response.status_code assert "You do not have permission to perform this action." == response.data['detail'] + @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.ac @pytest.mark.django_db @@ -157,6 +174,7 @@ def test_super_user_ok(hosts, fact_scans, get, user, team): assert 200 == response.status_code + @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.ac @pytest.mark.django_db @@ -168,6 +186,7 @@ def test_user_admin_ok(organization, hosts, fact_scans, get, user, team): assert 200 == response.status_code + @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.ac @pytest.mark.django_db @@ -179,4 +198,3 @@ def test_user_admin_403(organization, organizations, hosts, fact_scans, get, use response = _test_user_access_control(hosts, fact_scans, get, user_admin, team) assert 403 == response.status_code - diff --git a/awx/main/tests/functional/api/test_host_detail.py b/awx/main/tests/functional/api/test_host_detail.py index 79213490b0..acfe73f4b5 100644 --- a/awx/main/tests/functional/api/test_host_detail.py +++ b/awx/main/tests/functional/api/test_host_detail.py @@ -4,6 +4,7 @@ import pytest from django.core.urlresolvers import reverse + @pytest.mark.django_db def test_basic_fields(hosts, fact_scans, get, user): hosts = hosts(host_count=1) diff --git a/awx/main/tests/functional/api/test_inventory.py b/awx/main/tests/functional/api/test_inventory.py index 38e06dad07..925d7352fd 100644 --- a/awx/main/tests/functional/api/test_inventory.py +++ b/awx/main/tests/functional/api/test_inventory.py @@ -2,6 +2,7 @@ import pytest from django.core.urlresolvers import reverse + @pytest.mark.django_db def test_inventory_source_notification_on_cloud_only(get, post, group_factory, user, notification_template): u = user('admin', True) @@ -48,6 +49,7 @@ def test_create_inventory_group(post, inventory, alice, role_field, expected_sta getattr(inventory, role_field).members.add(alice) post(reverse('api:inventory_groups_list', args=(inventory.id,)), data, alice, expect=expected_status_code) + @pytest.mark.parametrize("role_field,expected_status_code", [ (None, 403), ('admin_role', 201), @@ -106,6 +108,7 @@ def test_create_inventory_host(post, inventory, alice, role_field, expected_stat getattr(inventory, role_field).members.add(alice) post(reverse('api:inventory_hosts_list', args=(inventory.id,)), data, alice, expect=expected_status_code) + @pytest.mark.parametrize("role_field,expected_status_code", [ (None, 403), ('admin_role', 201), @@ -149,6 +152,7 @@ def test_delete_inventory_host(delete, host, alice, role_field, expected_status_ getattr(host.inventory, role_field).members.add(alice) delete(reverse('api:host_detail', args=(host.id,)), alice, expect=expected_status_code) + @pytest.mark.parametrize("role_field,expected_status_code", [ (None, 403), ('admin_role', 202), diff --git a/awx/main/tests/functional/api/test_job_runtime_params.py b/awx/main/tests/functional/api/test_job_runtime_params.py index af7d133c4c..ed48f7ec02 100644 --- a/awx/main/tests/functional/api/test_job_runtime_params.py +++ b/awx/main/tests/functional/api/test_job_runtime_params.py @@ -8,6 +8,7 @@ from awx.main.models.jobs import Job, JobTemplate from django.core.urlresolvers import reverse + @pytest.fixture def runtime_data(organization): cred_obj = Credential.objects.create(name='runtime-cred', kind='ssh', username='test_user2', password='pas4word2') @@ -22,10 +23,12 @@ def runtime_data(organization): credential=cred_obj.pk, ) + @pytest.fixture def job_with_links(machine_credential, inventory): return Job.objects.create(name='existing-job', credential=machine_credential, inventory=inventory) + @pytest.fixture def job_template_prompts(project, inventory, machine_credential): def rf(on_off): @@ -45,6 +48,7 @@ def job_template_prompts(project, inventory, machine_credential): ) return rf + @pytest.fixture def job_template_prompts_null(project): return JobTemplate.objects.create( @@ -62,6 +66,7 @@ def job_template_prompts_null(project): ask_credential_on_launch=True, ) + @pytest.fixture def bad_scan_JT(job_template_prompts): job_template = job_template_prompts(True) @@ -69,6 +74,7 @@ def bad_scan_JT(job_template_prompts): job_template.save() return job_template + # End of setup, tests start here @pytest.mark.django_db @pytest.mark.job_runtime_vars @@ -98,6 +104,7 @@ def test_job_ignore_unprompted_vars(runtime_data, job_template_prompts, post, ad assert 'job_tags' in response.data['ignored_fields'] assert 'skip_tags' in response.data['ignored_fields'] + @pytest.mark.django_db @pytest.mark.job_runtime_vars def test_job_accept_prompted_vars(runtime_data, job_template_prompts, post, admin_user, mocker): @@ -115,6 +122,7 @@ def test_job_accept_prompted_vars(runtime_data, job_template_prompts, post, admi mock_job.signal_start.assert_called_once_with(**runtime_data) + @pytest.mark.django_db @pytest.mark.job_runtime_vars def test_job_accept_null_tags(job_template_prompts, post, admin_user, mocker): @@ -129,6 +137,7 @@ def test_job_accept_null_tags(job_template_prompts, post, admin_user, mocker): mock_job.signal_start.assert_called_once_with(job_tags='', skip_tags='') + @pytest.mark.django_db @pytest.mark.job_runtime_vars def test_job_accept_prompted_vars_null(runtime_data, job_template_prompts_null, post, rando, mocker): @@ -154,6 +163,7 @@ def test_job_accept_prompted_vars_null(runtime_data, job_template_prompts_null, assert job_id == 968 mock_job.signal_start.assert_called_once_with(**runtime_data) + @pytest.mark.django_db @pytest.mark.job_runtime_vars def test_job_reject_invalid_prompted_vars(runtime_data, job_template_prompts, post, admin_user): @@ -168,6 +178,7 @@ def test_job_reject_invalid_prompted_vars(runtime_data, job_template_prompts, po assert response.data['inventory'] == [u'Invalid pk "87865" - object does not exist.'] assert response.data['credential'] == [u'Invalid pk "48474" - object does not exist.'] + @pytest.mark.django_db @pytest.mark.job_runtime_vars def test_job_reject_invalid_prompted_extra_vars(runtime_data, job_template_prompts, post, admin_user): @@ -179,6 +190,7 @@ def test_job_reject_invalid_prompted_extra_vars(runtime_data, job_template_promp assert response.data['extra_vars'] == ['Must be a valid JSON or YAML dictionary.'] + @pytest.mark.django_db @pytest.mark.job_runtime_vars def test_job_launch_fails_without_inventory(deploy_jobtemplate, post, admin_user): @@ -190,6 +202,7 @@ def test_job_launch_fails_without_inventory(deploy_jobtemplate, post, admin_user assert response.data['inventory'] == ["Job Template 'inventory' is missing or undefined."] + @pytest.mark.django_db @pytest.mark.job_runtime_vars def test_job_launch_fails_without_inventory_access(job_template_prompts, runtime_data, post, rando): @@ -202,6 +215,7 @@ def test_job_launch_fails_without_inventory_access(job_template_prompts, runtime assert response.data['detail'] == u'You do not have permission to perform this action.' + @pytest.mark.django_db @pytest.mark.job_runtime_vars def test_job_launch_fails_without_credential_access(job_template_prompts, runtime_data, post, rando): @@ -214,6 +228,7 @@ def test_job_launch_fails_without_credential_access(job_template_prompts, runtim assert response.data['detail'] == u'You do not have permission to perform this action.' + @pytest.mark.django_db @pytest.mark.job_runtime_vars def test_job_block_scan_job_type_change(job_template_prompts, post, admin_user): @@ -225,6 +240,7 @@ def test_job_block_scan_job_type_change(job_template_prompts, post, admin_user): assert 'job_type' in response.data + @pytest.mark.django_db @pytest.mark.job_runtime_vars def test_job_block_scan_job_inv_change(mocker, bad_scan_JT, runtime_data, post, admin_user): @@ -236,6 +252,7 @@ def test_job_block_scan_job_inv_change(mocker, bad_scan_JT, runtime_data, post, assert 'inventory' in response.data + @pytest.mark.django_db @pytest.mark.job_runtime_vars def test_job_relaunch_copy_vars(job_with_links, machine_credential, inventory, @@ -251,6 +268,7 @@ def test_job_relaunch_copy_vars(job_with_links, machine_credential, inventory, assert second_job.inventory == job_with_links.inventory assert second_job.limit == 'my_server' + @pytest.mark.django_db @pytest.mark.job_runtime_vars def test_job_relaunch_resource_access(job_with_links, user): @@ -271,6 +289,7 @@ def test_job_relaunch_resource_access(job_with_links, user): job_with_links.inventory.use_role.members.add(inventory_user) assert not inventory_user.can_access(Job, 'start', job_with_links) + @pytest.mark.django_db def test_job_launch_JT_with_validation(machine_credential, deploy_jobtemplate): deploy_jobtemplate.extra_vars = '{"job_template_var": 3}' @@ -291,6 +310,7 @@ def test_job_launch_JT_with_validation(machine_credential, deploy_jobtemplate): assert 'job_launch_var' in final_job_extra_vars assert job_obj.credential.id == machine_credential.id + @pytest.mark.django_db @pytest.mark.job_runtime_vars def test_job_launch_unprompted_vars_with_survey(mocker, survey_spec_factory, job_template_prompts, post, admin_user): diff --git a/awx/main/tests/functional/api/test_job_template.py b/awx/main/tests/functional/api/test_job_template.py index ca6bdf3d31..4983aaac69 100644 --- a/awx/main/tests/functional/api/test_job_template.py +++ b/awx/main/tests/functional/api/test_job_template.py @@ -9,6 +9,7 @@ from awx.main.migrations import _save_password_keys as save_password_keys from django.core.urlresolvers import reverse from django.apps import apps + @pytest.mark.django_db @pytest.mark.parametrize( "grant_project, grant_credential, grant_inventory, expect", [ @@ -34,6 +35,7 @@ def test_create(post, project, machine_credential, inventory, alice, grant_proje 'playbook': 'helloworld.yml', }, alice, expect=expect) + @pytest.mark.django_db @pytest.mark.parametrize( "grant_project, grant_credential, grant_inventory, expect", [ @@ -62,6 +64,7 @@ def test_edit_sensitive_fields(patch, job_template_factory, alice, grant_project 'playbook': 'alt-helloworld.yml', }, alice, expect=expect) + @pytest.mark.django_db def test_edit_playbook(patch, job_template_factory, alice): objs = job_template_factory('jt', organization='org1', project='prj', inventory='inv', credential='cred') @@ -79,6 +82,7 @@ def test_edit_playbook(patch, job_template_factory, alice): 'playbook': 'helloworld.yml', }, alice, expect=403) + @pytest.mark.django_db def test_edit_nonsenstive(patch, job_template_factory, alice): objs = job_template_factory('jt', organization='org1', project='prj', inventory='inv', credential='cred') @@ -104,6 +108,8 @@ def test_edit_nonsenstive(patch, job_template_factory, alice): }, alice, expect=200) print(res.data) assert res.data['name'] == 'updated' + + @pytest.fixture def jt_copy_edit(job_template_factory, project): objects = job_template_factory( @@ -111,6 +117,7 @@ def jt_copy_edit(job_template_factory, project): project=project) return objects.job_template + @pytest.mark.django_db def test_job_template_role_user(post, organization_factory, job_template_factory): objects = organization_factory("org", @@ -127,10 +134,8 @@ def test_job_template_role_user(post, organization_factory, job_template_factory assert response.status_code == 204 - @pytest.mark.django_db def test_jt_admin_copy_edit_functional(jt_copy_edit, rando, get, post): - # Grant random user JT admin access only jt_copy_edit.admin_role.members.add(rando) jt_copy_edit.save() @@ -143,6 +148,7 @@ def test_jt_admin_copy_edit_functional(jt_copy_edit, rando, get, post): post_response = post(reverse('api:job_template_list', args=[]), user=rando, data=post_data) assert post_response.status_code == 403 + @pytest.mark.django_db def test_scan_jt_no_inventory(job_template_factory): # A user should be able to create a scan job without a project, but an inventory is required @@ -175,6 +181,7 @@ def test_scan_jt_no_inventory(job_template_factory): assert not serializer.is_valid() assert 'inventory' in serializer.errors + @pytest.mark.django_db def test_scan_jt_surveys(inventory): serializer = JobTemplateSerializer(data={"name": "Test", "job_type": "scan", @@ -183,6 +190,7 @@ def test_scan_jt_surveys(inventory): assert not serializer.is_valid() assert "survey_enabled" in serializer.errors + @pytest.mark.django_db def test_jt_without_project(inventory): data = dict(name="Test", job_type="run", @@ -198,6 +206,7 @@ def test_jt_without_project(inventory): serializer = JobTemplateSerializer(data=data) assert serializer.is_valid() + @pytest.mark.django_db def test_disallow_template_delete_on_running_job(job_template_factory, delete, admin_user): objects = job_template_factory('jt', @@ -210,6 +219,7 @@ def test_disallow_template_delete_on_running_job(job_template_factory, delete, a delete_response = delete(reverse('api:job_template_detail', args=[objects.job_template.pk]), user=admin_user) assert delete_response.status_code == 409 + @pytest.mark.django_db def test_save_survey_passwords_to_job(job_template_with_survey_passwords): """Test that when a new job is created, the survey_passwords field is @@ -217,6 +227,7 @@ def test_save_survey_passwords_to_job(job_template_with_survey_passwords): job = job_template_with_survey_passwords.create_unified_job() assert job.survey_passwords == {'SSN': '$encrypted$', 'secret_key': '$encrypted$'} + @pytest.mark.django_db def test_save_survey_passwords_on_migration(job_template_with_survey_passwords): """Test that when upgrading to 3.0.2, the jobs connected to a JT that has diff --git a/awx/main/tests/functional/api/test_organization_counts.py b/awx/main/tests/functional/api/test_organization_counts.py index f20396e46b..500667e107 100644 --- a/awx/main/tests/functional/api/test_organization_counts.py +++ b/awx/main/tests/functional/api/test_organization_counts.py @@ -2,6 +2,7 @@ import pytest from django.core.urlresolvers import reverse + @pytest.fixture def organization_resource_creator(organization, user): def rf(users, admins, job_templates, projects, inventories, teams): @@ -58,10 +59,12 @@ COUNTS_ZEROS = { 'teams': 0 } + @pytest.fixture def resourced_organization(organization_resource_creator): return organization_resource_creator(**COUNTS_PRIMES) + @pytest.mark.django_db def test_org_counts_detail_admin(resourced_organization, user, get): # Check that all types of resources are counted by a superuser @@ -73,6 +76,7 @@ def test_org_counts_detail_admin(resourced_organization, user, get): counts = response.data['summary_fields']['related_field_counts'] assert counts == COUNTS_PRIMES + @pytest.mark.django_db def test_org_counts_detail_member(resourced_organization, user, get): # Check that a non-admin org member can only see users / admin in detail view @@ -91,6 +95,7 @@ def test_org_counts_detail_member(resourced_organization, user, get): 'teams': 0 } + @pytest.mark.django_db def test_org_counts_list_admin(resourced_organization, user, get): # Check that all types of resources are counted by a superuser @@ -101,6 +106,7 @@ def test_org_counts_list_admin(resourced_organization, user, get): counts = response.data['results'][0]['summary_fields']['related_field_counts'] assert counts == COUNTS_PRIMES + @pytest.mark.django_db def test_org_counts_list_member(resourced_organization, user, get): # Check that a non-admin user can only see the full project and @@ -120,6 +126,7 @@ def test_org_counts_list_member(resourced_organization, user, get): 'teams': 0 } + @pytest.mark.django_db def test_new_org_zero_counts(user, post): # Check that a POST to the organization list endpoint returns @@ -133,6 +140,7 @@ def test_new_org_zero_counts(user, post): counts_dict = new_org_list['summary_fields']['related_field_counts'] assert counts_dict == COUNTS_ZEROS + @pytest.mark.django_db def test_two_organizations(resourced_organization, organizations, user, get): # Check correct results for two organizations are returned @@ -151,6 +159,7 @@ def test_two_organizations(resourced_organization, organizations, user, get): assert counts[org_id_full] == COUNTS_PRIMES assert counts[org_id_zero] == COUNTS_ZEROS + @pytest.mark.django_db def test_scan_JT_counted(resourced_organization, user, get): admin_user = user('admin', True) @@ -171,6 +180,7 @@ def test_scan_JT_counted(resourced_organization, user, get): assert detail_response.status_code == 200 assert detail_response.data['summary_fields']['related_field_counts'] == counts_dict + @pytest.mark.django_db def test_JT_associated_with_project(organizations, project, user, get): # Check that adding a project to an organization gets the project's JT diff --git a/awx/main/tests/functional/api/test_rbac_displays.py b/awx/main/tests/functional/api/test_rbac_displays.py index f3705c354b..66c828020a 100644 --- a/awx/main/tests/functional/api/test_rbac_displays.py +++ b/awx/main/tests/functional/api/test_rbac_displays.py @@ -172,6 +172,7 @@ def mock_access_method(mocker): mock_method.__name__ = 'bars' # Required for a logging statement return mock_method + @pytest.mark.django_db class TestAccessListCapabilities: """ @@ -240,6 +241,7 @@ def test_team_roles_unattach(mocker, team, team_member, inventory, mock_access_m inventory.admin_role, team.member_role, 'parents', skip_sub_obj_read_check=True, data={}) assert response.data['results'][0]['summary_fields']['user_capabilities']['unattach'] == 'foobar' + @pytest.mark.django_db def test_user_roles_unattach(mocker, organization, alice, bob, mock_access_method, get): # Add to same organization so that alice and bob can see each other @@ -254,6 +256,7 @@ def test_user_roles_unattach(mocker, organization, alice, bob, mock_access_metho organization.member_role, alice, 'members', skip_sub_obj_read_check=True, data={}) assert response.data['results'][0]['summary_fields']['user_capabilities']['unattach'] == 'foobar' + @pytest.mark.django_db def test_team_roles_unattach_functional(team, team_member, inventory, get): team.member_role.children.add(inventory.admin_role) @@ -262,6 +265,7 @@ def test_team_roles_unattach_functional(team, team_member, inventory, get): # the inventory admin_role grants that ability assert response.data['results'][0]['summary_fields']['user_capabilities']['unattach'] + @pytest.mark.django_db def test_user_roles_unattach_functional(organization, alice, bob, get): organization.member_role.members.add(alice) @@ -278,6 +282,7 @@ def test_prefetch_jt_capabilities(job_template, rando): cache_list_capabilities(qs, ['admin', 'execute'], JobTemplate, rando) assert qs[0].capabilities_cache == {'edit': False, 'start': True} + @pytest.mark.django_db def test_prefetch_group_capabilities(group, rando): group.inventory.adhoc_role.members.add(rando) @@ -285,6 +290,7 @@ def test_prefetch_group_capabilities(group, rando): cache_list_capabilities(qs, ['inventory.admin', 'inventory.adhoc'], Group, rando) assert qs[0].capabilities_cache == {'edit': False, 'adhoc': True} + @pytest.mark.django_db def test_prefetch_jt_copy_capability(job_template, project, inventory, machine_credential, rando): job_template.project = project @@ -309,11 +315,13 @@ def test_prefetch_jt_copy_capability(job_template, project, inventory, machine_c ]}], JobTemplate, rando) assert qs[0].capabilities_cache == {'copy': True} + @pytest.mark.django_db def test_manual_projects_no_update(project, get, admin_user): response = get(reverse('api:project_detail', args=[project.pk]), admin_user, expect=200) assert not response.data['summary_fields']['user_capabilities']['start'] + @pytest.mark.django_db def test_group_update_capabilities_possible(group, inventory_source, admin_user): group.inventory_source = inventory_source @@ -322,6 +330,7 @@ def test_group_update_capabilities_possible(group, inventory_source, admin_user) capabilities = get_user_capabilities(admin_user, group, method_list=['start']) assert capabilities['start'] + @pytest.mark.django_db def test_group_update_capabilities_impossible(group, inventory_source, admin_user): inventory_source.source = "" @@ -332,6 +341,7 @@ def test_group_update_capabilities_impossible(group, inventory_source, admin_use capabilities = get_user_capabilities(admin_user, group, method_list=['start']) assert not capabilities['start'] + @pytest.mark.django_db def test_license_check_not_called(mocker, job_template, project, org_admin, get): job_template.project = project @@ -340,4 +350,3 @@ def test_license_check_not_called(mocker, job_template, project, org_admin, get) with mocker.patch('awx.main.access.BaseAccess.check_license', mock_license_check): get(reverse('api:job_template_detail', args=[job_template.pk]), org_admin, expect=200) assert not mock_license_check.called - diff --git a/awx/main/tests/functional/api/test_resource_access_lists.py b/awx/main/tests/functional/api/test_resource_access_lists.py index 9d8d95c98a..433c14a9c4 100644 --- a/awx/main/tests/functional/api/test_resource_access_lists.py +++ b/awx/main/tests/functional/api/test_resource_access_lists.py @@ -3,6 +3,7 @@ import pytest from django.core.urlresolvers import reverse from awx.main.models import Role + @pytest.mark.django_db def test_indirect_access_list(get, organization, project, team_factory, user, admin): project_admin = user('project_admin') diff --git a/awx/main/tests/functional/api/test_role.py b/awx/main/tests/functional/api/test_role.py index 94215521a5..98610bf8e4 100644 --- a/awx/main/tests/functional/api/test_role.py +++ b/awx/main/tests/functional/api/test_role.py @@ -2,6 +2,7 @@ import pytest from django.core.urlresolvers import reverse + @pytest.mark.django_db def test_admin_visible_to_orphaned_users(get, alice): names = set() diff --git a/awx/main/tests/functional/api/test_settings.py b/awx/main/tests/functional/api/test_settings.py index 1e64222522..7f57eb4fa0 100644 --- a/awx/main/tests/functional/api/test_settings.py +++ b/awx/main/tests/functional/api/test_settings.py @@ -11,6 +11,7 @@ from django.core.urlresolvers import reverse # AWX from awx.conf.models import Setting + ''' Ensures that tests don't pick up dev container license file ''' @@ -19,9 +20,9 @@ def mock_no_license_file(mocker): os.environ['AWX_LICENSE_FILE'] = '/does_not_exist' return None + @pytest.mark.django_db def test_license_cannot_be_removed_via_system_settings(mock_no_license_file, get, put, patch, delete, admin, enterprise_license): - url = reverse('api:setting_singleton_detail', args=('system',)) response = get(url, user=admin, expect=200) assert not response.data['LICENSE'] diff --git a/awx/main/tests/functional/api/test_survey_spec.py b/awx/main/tests/functional/api/test_survey_spec.py index 814a83f1ae..f954538973 100644 --- a/awx/main/tests/functional/api/test_survey_spec.py +++ b/awx/main/tests/functional/api/test_survey_spec.py @@ -16,11 +16,13 @@ def mock_no_surveys(self, add_host=False, feature=None, check_expiration=True): else: pass + @pytest.fixture def job_template_with_survey(job_template_factory): objects = job_template_factory('jt', project='prj', survey='submitted_email') return objects.job_template + # Survey license-based denial tests @mock.patch('awx.api.views.feature_enabled', lambda feature: False) @pytest.mark.django_db @@ -31,6 +33,7 @@ def test_survey_spec_view_denied(job_template_with_survey, get, admin_user): args=(job_template_with_survey.id,)), admin_user, expect=402) assert response.data['detail'] == 'Your license does not allow adding surveys.' + @mock.patch('awx.main.access.BaseAccess.check_license', mock_no_surveys) @pytest.mark.django_db @pytest.mark.survey @@ -39,6 +42,7 @@ def test_deny_enabling_survey(deploy_jobtemplate, patch, admin_user): data=dict(survey_enabled=True), user=admin_user, expect=402) assert response.data['detail'] == 'Feature surveys is not enabled in the active license.' + @mock.patch('awx.main.access.BaseAccess.check_license', new=mock_no_surveys) @pytest.mark.django_db @pytest.mark.survey @@ -48,6 +52,7 @@ def test_job_start_blocked_without_survey_license(job_template_with_survey, admi with pytest.raises(LicenseForbids): access.can_start(job_template_with_survey) + @mock.patch('awx.main.access.BaseAccess.check_license', mock_no_surveys) @pytest.mark.django_db @pytest.mark.survey @@ -65,6 +70,7 @@ def test_deny_creating_with_survey(project, post, admin_user): user=admin_user, expect=402) assert response.data['detail'] == 'Feature surveys is not enabled in the active license.' + # Test normal operations with survey license work @mock.patch('awx.api.views.feature_enabled', lambda feature: True) @pytest.mark.django_db @@ -73,6 +79,7 @@ def test_survey_spec_view_allowed(deploy_jobtemplate, get, admin_user): get(reverse('api:job_template_survey_spec', args=(deploy_jobtemplate.id,)), admin_user, expect=200) + @mock.patch('awx.api.views.feature_enabled', lambda feature: True) @pytest.mark.django_db @pytest.mark.survey @@ -83,6 +90,7 @@ def test_survey_spec_sucessful_creation(survey_spec_factory, job_template, post, updated_jt = JobTemplate.objects.get(pk=job_template.pk) assert updated_jt.survey_spec == survey_input_data + # Tests related to survey content validation @mock.patch('awx.api.views.feature_enabled', lambda feature: True) @pytest.mark.django_db @@ -96,6 +104,7 @@ def test_survey_spec_non_dict_error(deploy_jobtemplate, post, admin_user): user=admin_user, expect=400) assert response.data['error'] == "Survey question 0 is not a json object." + @mock.patch('awx.api.views.feature_enabled', lambda feature: True) @pytest.mark.django_db @pytest.mark.survey @@ -106,6 +115,7 @@ def test_survey_spec_dual_names_error(survey_spec_factory, deploy_jobtemplate, p user=user('admin', True), expect=400) assert response.data['error'] == "'variable' 'submitter_email' duplicated in survey question 1." + # Test actions that should be allowed with non-survey license @mock.patch('awx.main.access.BaseAccess.check_license', new=mock_no_surveys) @pytest.mark.django_db @@ -115,6 +125,7 @@ def test_disable_survey_access_without_license(job_template_with_survey, admin_u access = JobTemplateAccess(admin_user) assert access.can_change(job_template_with_survey, dict(survey_enabled=False)) + @mock.patch('awx.main.access.BaseAccess.check_license', new=mock_no_surveys) @pytest.mark.django_db @pytest.mark.survey @@ -124,6 +135,7 @@ def test_delete_survey_access_without_license(job_template_with_survey, admin_us assert access.can_change(job_template_with_survey, dict(survey_spec=None)) assert access.can_change(job_template_with_survey, dict(survey_spec={})) + @mock.patch('awx.main.access.BaseAccess.check_license', new=mock_no_surveys) @pytest.mark.django_db @pytest.mark.survey @@ -137,6 +149,7 @@ def test_job_start_allowed_with_survey_spec(job_template_factory, admin_user): access = JobTemplateAccess(admin_user) assert access.can_start(job_template_with_survey, {}) + @mock.patch('awx.main.access.BaseAccess.check_license', new=mock_no_surveys) @pytest.mark.django_db @pytest.mark.survey @@ -146,6 +159,7 @@ def test_job_template_delete_access_with_survey(job_template_with_survey, admin_ access = JobTemplateAccess(admin_user) assert access.can_delete(job_template_with_survey) + @mock.patch('awx.api.views.feature_enabled', lambda feature: False) @mock.patch('awx.main.access.BaseAccess.check_license', new=mock_no_surveys) @pytest.mark.django_db @@ -157,6 +171,7 @@ def test_delete_survey_spec_without_license(job_template_with_survey, delete, ad new_jt = JobTemplate.objects.get(pk=job_template_with_survey.pk) assert new_jt.survey_spec == {} + @mock.patch('awx.main.access.BaseAccess.check_license', lambda self, **kwargs: True) @mock.patch('awx.main.models.unified_jobs.UnifiedJobTemplate.create_unified_job', lambda self, extra_vars: mock.MagicMock(spec=Job, id=968)) @@ -174,6 +189,7 @@ def test_launch_survey_enabled_but_no_survey_spec(job_template_factory, post, ad dict(extra_vars=dict(survey_var=7)), admin_user, expect=201) assert 'survey_var' in response.data['ignored_fields']['extra_vars'] + @mock.patch('awx.main.access.BaseAccess.check_license', new=mock_no_surveys) @mock.patch('awx.main.models.unified_jobs.UnifiedJobTemplate.create_unified_job', lambda self: mock.MagicMock(spec=Job, id=968)) @@ -191,6 +207,7 @@ def test_launch_with_non_empty_survey_spec_no_license(job_template_factory, post obj.save() post(reverse('api:job_template_launch', args=[obj.pk]), {}, admin_user, expect=201) + @pytest.mark.django_db @pytest.mark.survey def test_redact_survey_passwords_in_activity_stream(job_template_with_survey_passwords): diff --git a/awx/main/tests/functional/api/test_unified_jobs_view.py b/awx/main/tests/functional/api/test_unified_jobs_view.py index ed7034a28e..0f0a2ca956 100644 --- a/awx/main/tests/functional/api/test_unified_jobs_view.py +++ b/awx/main/tests/functional/api/test_unified_jobs_view.py @@ -35,6 +35,7 @@ def test_cases(project): ret.append(e) return ret + @pytest.fixture def negative_test_cases(job_factory): ret = [] @@ -53,6 +54,7 @@ formats = [ ('html', 'text/html'), ] + @pytest.mark.parametrize("format,content_type", formats) @pytest.mark.django_db def test_project_update_redaction_enabled(get, format, content_type, test_cases, admin): @@ -66,6 +68,7 @@ def test_project_update_redaction_enabled(get, format, content_type, test_cases, assert test_data['uri'].password not in content assert content.count(test_data['uri'].host) == test_data['occurrences'] + @pytest.mark.parametrize("format,content_type", formats) @pytest.mark.django_db def test_job_redaction_disabled(get, format, content_type, negative_test_cases, admin): @@ -80,7 +83,6 @@ def test_job_redaction_disabled(get, format, content_type, negative_test_cases, @pytest.mark.django_db def test_options_fields_choices(instance, options, user): - url = reverse('api:unified_job_list') response = options(url, None, user('admin', True)) @@ -89,5 +91,3 @@ def test_options_fields_choices(instance, options, user): assert UnifiedJob.LAUNCH_TYPE_CHOICES == response.data['actions']['GET']['launch_type']['choices'] assert 'choice' == response.data['actions']['GET']['status']['type'] assert UnifiedJob.STATUS_CHOICES == response.data['actions']['GET']['status']['choices'] - - diff --git a/awx/main/tests/functional/api/test_user.py b/awx/main/tests/functional/api/test_user.py index d739d417c0..fb4c7a33e0 100644 --- a/awx/main/tests/functional/api/test_user.py +++ b/awx/main/tests/functional/api/test_user.py @@ -7,6 +7,7 @@ from django.core.urlresolvers import reverse # user creation # + @pytest.mark.django_db def test_user_create(post, admin): response = post(reverse('api:user_list'), { @@ -19,6 +20,7 @@ def test_user_create(post, admin): }, admin) assert response.status_code == 201 + @pytest.mark.django_db def test_fail_double_create_user(post, admin): response = post(reverse('api:user_list'), { @@ -41,6 +43,7 @@ def test_fail_double_create_user(post, admin): }, admin) assert response.status_code == 400 + @pytest.mark.django_db def test_create_delete_create_user(post, delete, admin): response = post(reverse('api:user_list'), { diff --git a/awx/main/tests/functional/commands/conftest.py b/awx/main/tests/functional/commands/conftest.py index 2de8846b0a..2917c10fcc 100644 --- a/awx/main/tests/functional/commands/conftest.py +++ b/awx/main/tests/functional/commands/conftest.py @@ -3,6 +3,7 @@ import time from datetime import datetime + @pytest.fixture def fact_msg_base(inventory, hosts): host_objs = hosts(1) @@ -13,6 +14,7 @@ def fact_msg_base(inventory, hosts): 'inventory_id': inventory.id } + @pytest.fixture def fact_msg_small(fact_msg_base): fact_msg_base['facts'] = { @@ -77,7 +79,7 @@ def fact_msg_small(fact_msg_base): } } return fact_msg_base - + ''' Facts sent from ansible to our fact cache reciever. @@ -92,18 +94,20 @@ key of 'ansible' } ''' + @pytest.fixture def fact_msg_ansible(fact_msg_base, fact_ansible_json): fact_msg_base['facts'] = fact_ansible_json return fact_msg_base + @pytest.fixture def fact_msg_packages(fact_msg_base, fact_packages_json): fact_msg_base['facts']['packages'] = fact_packages_json return fact_msg_base + @pytest.fixture def fact_msg_services(fact_msg_base, fact_services_json): fact_msg_base['facts']['services'] = fact_services_json return fact_msg_base - diff --git a/awx/main/tests/functional/commands/test_cleanup_facts.py b/awx/main/tests/functional/commands/test_cleanup_facts.py index 206fe04d2e..99849084bd 100644 --- a/awx/main/tests/functional/commands/test_cleanup_facts.py +++ b/awx/main/tests/functional/commands/test_cleanup_facts.py @@ -16,12 +16,15 @@ from awx.main.management.commands.cleanup_facts import CleanupFacts, Command from awx.main.models.fact import Fact from awx.main.models.inventory import Host + def mock_feature_enabled(feature): return True + def mock_feature_disabled(feature): return False + @pytest.mark.django_db def test_cleanup_granularity(fact_scans, hosts): epoch = timezone.now() @@ -52,6 +55,7 @@ def test_cleanup_older_than(fact_scans, hosts): deleted_count = cleanup_facts.cleanup(fact_middle.timestamp, granularity) assert 210 == deleted_count + @pytest.mark.django_db def test_cleanup_older_than_granularity_module(fact_scans, hosts): epoch = timezone.now() @@ -96,6 +100,7 @@ def test_cleanup_logic(fact_scans, hosts): timestamp_pivot -= granularity assert fact.timestamp == timestamp_pivot + @mock.patch('awx.main.management.commands.cleanup_facts.feature_enabled', new=mock_feature_disabled) @pytest.mark.django_db @pytest.mark.license_feature @@ -105,6 +110,7 @@ def test_system_tracking_feature_disabled(mocker): cmd.handle(None) assert 'The System Tracking feature is not enabled for your Tower instance' in err.value + @mock.patch('awx.main.management.commands.cleanup_facts.feature_enabled', new=mock_feature_enabled) @pytest.mark.django_db def test_parameters_ok(mocker): @@ -118,6 +124,7 @@ def test_parameters_ok(mocker): cmd.handle(None, **kv) run.assert_called_once_with(relativedelta(days=1), relativedelta(days=1), module=None) + @pytest.mark.django_db def test_string_time_to_timestamp_ok(): kvs = [ @@ -147,6 +154,7 @@ def test_string_time_to_timestamp_ok(): res = cmd.string_time_to_timestamp(kv['time']) assert kv['timestamp'] == res + @pytest.mark.django_db def test_string_time_to_timestamp_invalid(): kvs = [ @@ -176,6 +184,7 @@ def test_string_time_to_timestamp_invalid(): res = cmd.string_time_to_timestamp(kv['time']) assert res is None + @mock.patch('awx.main.management.commands.cleanup_facts.feature_enabled', new=mock_feature_enabled) @pytest.mark.django_db def test_parameters_fail(mocker): @@ -198,4 +207,3 @@ def test_parameters_fail(mocker): with pytest.raises(CommandError) as err: cmd.handle(None, older_than=kv['older_than'], granularity=kv['granularity']) assert kv['msg'] in err.value - diff --git a/awx/main/tests/functional/commands/test_commands.py b/awx/main/tests/functional/commands/test_commands.py index d3f4e6b949..95cd291cee 100644 --- a/awx/main/tests/functional/commands/test_commands.py +++ b/awx/main/tests/functional/commands/test_commands.py @@ -10,6 +10,7 @@ from django.core.management import call_command from awx.main.management.commands.update_password import UpdatePassword + def run_command(name, *args, **options): command_runner = options.pop('command_runner', call_command) stdin_fileobj = options.pop('stdin_fileobj', None) diff --git a/awx/main/tests/functional/commands/test_run_fact_cache_receiver.py b/awx/main/tests/functional/commands/test_run_fact_cache_receiver.py index c6b00ef048..2e9ceb3b9e 100644 --- a/awx/main/tests/functional/commands/test_run_fact_cache_receiver.py +++ b/awx/main/tests/functional/commands/test_run_fact_cache_receiver.py @@ -14,6 +14,7 @@ from awx.main.management.commands.run_fact_cache_receiver import FactCacheReceiv from awx.main.models.fact import Fact from awx.main.models.inventory import Host + # TODO: Check that timestamp and other attributes are as expected def check_process_fact_message_module(fact_returned, data, module_name): date_key = data['date_key'] @@ -36,6 +37,7 @@ def check_process_fact_message_module(fact_returned, data, module_name): assert timestamp == fact_returned.timestamp assert module_name == fact_returned.module + @pytest.mark.django_db def test_process_fact_message_ansible(fact_msg_ansible): receiver = FactCacheReceiver() @@ -43,6 +45,7 @@ def test_process_fact_message_ansible(fact_msg_ansible): check_process_fact_message_module(fact_returned, fact_msg_ansible, 'ansible') + @pytest.mark.django_db def test_process_fact_message_packages(fact_msg_packages): receiver = FactCacheReceiver() @@ -50,6 +53,7 @@ def test_process_fact_message_packages(fact_msg_packages): check_process_fact_message_module(fact_returned, fact_msg_packages, 'packages') + @pytest.mark.django_db def test_process_fact_message_services(fact_msg_services): receiver = FactCacheReceiver() @@ -83,6 +87,7 @@ def test_process_facts_message_ansible_overwrite(fact_scans, fact_msg_ansible): assert key in fact_obj.facts assert fact_msg_ansible['facts'] == (json.loads(fact_obj.facts) if isinstance(fact_obj.facts, unicode) else fact_obj.facts) # TODO: Just make response.data['facts'] when we're only dealing with postgres, or if jsonfields ever fixes this bug + # Ensure that the message flows from the socket through to process_fact_message() @pytest.mark.django_db def test_run_receiver(mocker, fact_msg_ansible): diff --git a/awx/main/tests/functional/conftest.py b/awx/main/tests/functional/conftest.py index 9d7b913069..f272f3f88e 100644 --- a/awx/main/tests/functional/conftest.py +++ b/awx/main/tests/functional/conftest.py @@ -43,6 +43,7 @@ from awx.main.models.notifications import ( Notification ) + ''' Disable all django model signals. ''' @@ -62,6 +63,7 @@ Allows django signal code to execute without the need for redis def celery_memory_broker(): settings.BROKER_URL='memory://localhost/' + @pytest.fixture def user(): def u(name, is_superuser=False): @@ -73,6 +75,7 @@ def user(): return user return u + @pytest.fixture def check_jobtemplate(project, inventory, credential): return \ @@ -84,6 +87,7 @@ def check_jobtemplate(project, inventory, credential): name='check-job-template' ) + @pytest.fixture def deploy_jobtemplate(project, inventory, credential): return \ @@ -95,10 +99,12 @@ def deploy_jobtemplate(project, inventory, credential): name='deploy-job-template' ) + @pytest.fixture def team(organization): return organization.teams.create(name='test-team') + @pytest.fixture def team_member(user, team): ret = user('team-member', False) @@ -116,6 +122,7 @@ def project(instance, organization): ) return prj + @pytest.fixture def project_factory(organization): def factory(name): @@ -129,12 +136,14 @@ def project_factory(organization): return prj return factory + @pytest.fixture def job_factory(job_template, admin): def factory(job_template=job_template, initial_state='new', created_by=admin): return job_template.create_job(created_by=created_by, status=initial_state) return factory + @pytest.fixture def team_factory(organization): def factory(name): @@ -147,35 +156,43 @@ def team_factory(organization): return t return factory + @pytest.fixture def user_project(user): owner = user('owner') return Project.objects.create(name="test-user-project", created_by=owner, description="test-user-project-desc") + @pytest.fixture def instance(settings): return Instance.objects.create(uuid=settings.SYSTEM_UUID, hostname="instance.example.org", capacity=100) + @pytest.fixture def organization(instance): return Organization.objects.create(name="test-org", description="test-org-desc") + @pytest.fixture def credential(): return Credential.objects.create(kind='aws', name='test-cred', username='something', password='secret') + @pytest.fixture def machine_credential(): return Credential.objects.create(name='machine-cred', kind='ssh', username='test_user', password='pas4word') + @pytest.fixture def org_credential(organization): return Credential.objects.create(kind='aws', name='test-cred', username='something', password='secret', organization=organization) + @pytest.fixture def inventory(organization): return organization.inventories.create(name="test-inv") + @pytest.fixture def inventory_factory(organization): def factory(name, org=organization): @@ -186,10 +203,12 @@ def inventory_factory(organization): return inv return factory + @pytest.fixture def label(organization): return organization.labels.create(name="test-label", description="test-label-desc") + @pytest.fixture def notification_template(organization): return NotificationTemplate.objects.create(name='test-notification_template', @@ -198,6 +217,7 @@ def notification_template(organization): notification_configuration=dict(url="http://localhost", headers={"Test": "Header"})) + @pytest.fixture def notification(notification_template): return Notification.objects.create(notification_template=notification_template, @@ -207,27 +227,33 @@ def notification(notification_template): recipients='admin@redhat.com', subject='email subject') + @pytest.fixture def job_template_with_survey_passwords(job_template_with_survey_passwords_factory): return job_template_with_survey_passwords_factory(persisted=True) + @pytest.fixture def admin(user): return user('admin', True) + @pytest.fixture def alice(user): return user('alice', False) + @pytest.fixture def bob(user): return user('bob', False) + @pytest.fixture def rando(user): "Rando, the random user that doesn't have access to anything" return user('rando', False) + @pytest.fixture def org_admin(user, organization): ret = user('org-admin', False) @@ -235,6 +261,7 @@ def org_admin(user, organization): organization.member_role.members.add(ret) return ret + @pytest.fixture def org_auditor(user, organization): ret = user('org-auditor', False) @@ -242,12 +269,14 @@ def org_auditor(user, organization): organization.member_role.members.add(ret) return ret + @pytest.fixture def org_member(user, organization): ret = user('org-member', False) organization.member_role.members.add(ret) return ret + @pytest.fixture def organizations(instance): def rf(organization_count=1): @@ -258,6 +287,7 @@ def organizations(instance): return orgs return rf + @pytest.fixture def group_factory(inventory): def g(name): @@ -267,6 +297,7 @@ def group_factory(inventory): return Group.objects.create(inventory=inventory, name=name) return g + @pytest.fixture def hosts(group_factory): group1 = group_factory('group-1') @@ -282,23 +313,28 @@ def hosts(group_factory): return hosts return rf + @pytest.fixture def group(inventory): return inventory.groups.create(name='single-group') + @pytest.fixture def inventory_source(group, inventory): return InventorySource.objects.create(name=group.name, group=group, inventory=inventory, source='gce') + @pytest.fixture def inventory_update(inventory_source): return InventoryUpdate.objects.create(inventory_source=inventory_source) + @pytest.fixture def host(group, inventory): return group.hosts.create(name='single-host', inventory=inventory) + @pytest.fixture def permissions(): return { @@ -340,36 +376,42 @@ def _request(verb): return response return rf + @pytest.fixture def post(): return _request('post') + @pytest.fixture def get(): return _request('get') + @pytest.fixture def put(): return _request('put') + @pytest.fixture def patch(): return _request('patch') + @pytest.fixture def delete(): return _request('delete') + @pytest.fixture def head(): return _request('head') + @pytest.fixture def options(): return _request('options') - @pytest.fixture def fact_scans(group_factory, fact_ansible_json, fact_packages_json, fact_services_json): group1 = group_factory('group-1') @@ -392,27 +434,33 @@ def fact_scans(group_factory, fact_ansible_json, fact_packages_json, fact_servic return facts return rf + def _fact_json(module_name): current_dir = os.path.dirname(os.path.realpath(__file__)) with open('%s/%s.json' % (current_dir, module_name)) as f: return json.load(f) + @pytest.fixture def fact_ansible_json(): return _fact_json('ansible') + @pytest.fixture def fact_packages_json(): return _fact_json('packages') + @pytest.fixture def fact_services_json(): return _fact_json('services') + @pytest.fixture def permission_inv_read(organization, inventory, team): return Permission.objects.create(inventory=inventory, team=team, permission_type=PERM_INVENTORY_READ) + @pytest.fixture def job_template(organization): jt = JobTemplate(name='test-job_template') @@ -420,6 +468,7 @@ def job_template(organization): return jt + @pytest.fixture def job_template_labels(organization, job_template): job_template.labels.create(name="label-1", organization=organization) diff --git a/awx/main/tests/functional/core/test_licenses.py b/awx/main/tests/functional/core/test_licenses.py index c9b2af6dc0..f2c3d9348e 100644 --- a/awx/main/tests/functional/core/test_licenses.py +++ b/awx/main/tests/functional/core/test_licenses.py @@ -8,6 +8,7 @@ from datetime import datetime from awx.main.models import Host from awx.main.task_engine import TaskEnhancer + @pytest.mark.django_db def test_license_writer(inventory, admin): task_enhancer = TaskEnhancer( @@ -50,6 +51,7 @@ def test_license_writer(inventory, admin): assert vdata['compliant'] is False assert vdata['subscription_name'] + @pytest.mark.django_db def test_expired_licenses(): task_enhancer = TaskEnhancer( diff --git a/awx/main/tests/functional/models/fact/test_get_host_fact.py b/awx/main/tests/functional/models/fact/test_get_host_fact.py index 4e74615b45..89234e3337 100644 --- a/awx/main/tests/functional/models/fact/test_get_host_fact.py +++ b/awx/main/tests/functional/models/fact/test_get_host_fact.py @@ -5,6 +5,7 @@ from django.utils import timezone from awx.main.models import Fact + @pytest.mark.django_db def test_newest_scan_exact(hosts, fact_scans): epoch = timezone.now() @@ -112,4 +113,3 @@ def test_by_module(hosts, fact_scans): assert fact_found_services == fact_known_services assert fact_found_packages == fact_known_packages - diff --git a/awx/main/tests/functional/models/fact/test_get_timeline.py b/awx/main/tests/functional/models/fact/test_get_timeline.py index da3360340a..192bd40e86 100644 --- a/awx/main/tests/functional/models/fact/test_get_timeline.py +++ b/awx/main/tests/functional/models/fact/test_get_timeline.py @@ -5,6 +5,7 @@ from django.utils import timezone from awx.main.models import Fact + def setup_common(hosts, fact_scans, ts_from=None, ts_to=None, epoch=timezone.now(), module_name='ansible', ts_known=None): hosts = hosts(host_count=2) facts = fact_scans(fact_scans=3, timestamp_epoch=epoch) @@ -20,6 +21,7 @@ def setup_common(hosts, fact_scans, ts_from=None, ts_to=None, epoch=timezone.now fact_objs = Fact.get_timeline(hosts[0].id, module=module_name, ts_from=ts_from, ts_to=ts_to) return (facts_known, fact_objs) + @pytest.mark.django_db def test_all(hosts, fact_scans): epoch = timezone.now() @@ -30,6 +32,7 @@ def test_all(hosts, fact_scans): assert 9 == len(facts_known) assert 9 == len(fact_objs) + @pytest.mark.django_db def test_all_ansible(hosts, fact_scans): epoch = timezone.now() @@ -43,6 +46,7 @@ def test_all_ansible(hosts, fact_scans): for i in xrange(len(facts_known) - 1, 0): assert facts_known[i].id == fact_objs[i].id + @pytest.mark.django_db def test_empty_db(hosts, fact_scans): hosts = hosts(host_count=2) @@ -54,6 +58,7 @@ def test_empty_db(hosts, fact_scans): assert 0 == len(fact_objs) + @pytest.mark.django_db def test_no_results(hosts, fact_scans): epoch = timezone.now() @@ -63,6 +68,7 @@ def test_no_results(hosts, fact_scans): (facts_known, fact_objs) = setup_common(hosts, fact_scans, ts_from, ts_to, epoch=epoch) assert 0 == len(fact_objs) + @pytest.mark.django_db def test_exact_same_equal(hosts, fact_scans): epoch = timezone.now() @@ -74,6 +80,7 @@ def test_exact_same_equal(hosts, fact_scans): assert facts_known[0].id == fact_objs[0].id + @pytest.mark.django_db def test_exact_from_exclusive_to_inclusive(hosts, fact_scans): epoch = timezone.now() @@ -87,6 +94,7 @@ def test_exact_from_exclusive_to_inclusive(hosts, fact_scans): assert facts_known[0].id == fact_objs[0].id + @pytest.mark.django_db def test_to_lte(hosts, fact_scans): epoch = timezone.now() @@ -101,6 +109,7 @@ def test_to_lte(hosts, fact_scans): for i in xrange(0, len(fact_objs)): assert facts_known_subset[len(facts_known_subset) - i - 1].id == fact_objs[i].id + @pytest.mark.django_db def test_from_gt(hosts, fact_scans): epoch = timezone.now() @@ -115,6 +124,7 @@ def test_from_gt(hosts, fact_scans): for i in xrange(0, len(fact_objs)): assert facts_known_subset[len(facts_known_subset) - i - 1].id == fact_objs[i].id + @pytest.mark.django_db def test_no_ts(hosts, fact_scans): epoch = timezone.now() @@ -125,5 +135,3 @@ def test_no_ts(hosts, fact_scans): for i in xrange(len(facts_known) - 1, 0): assert facts_known[i].id == fact_objs[i].id - - diff --git a/awx/main/tests/functional/models/test_workflow.py b/awx/main/tests/functional/models/test_workflow.py index 309e29e4f0..1a9cbb4b0b 100644 --- a/awx/main/tests/functional/models/test_workflow.py +++ b/awx/main/tests/functional/models/test_workflow.py @@ -14,7 +14,6 @@ from django.test import TransactionTestCase @pytest.mark.django_db class TestWorkflowDAGFunctional(TransactionTestCase): - def workflow_job(self): wfj = WorkflowJob.objects.create() nodes = [WorkflowJobNode.objects.create(workflow_job=wfj) for i in range(0, 5)] @@ -35,6 +34,7 @@ class TestWorkflowDAGFunctional(TransactionTestCase): with self.assertNumQueries(4): dag._init_graph(wfj) + @pytest.mark.django_db class TestWorkflowJob: @pytest.fixture @@ -95,9 +95,9 @@ class TestWorkflowJob: assert queued_node.get_job_kwargs()['extra_vars'] == {'a': 42, 'b': 43} assert queued_node.ancestor_artifacts == {'a': 42, 'b': 43} + @pytest.mark.django_db class TestWorkflowJobTemplate: - @pytest.fixture def wfjt(self, workflow_job_template_factory): wfjt = workflow_job_template_factory('test').workflow_job_template @@ -134,6 +134,7 @@ class TestWorkflowJobTemplate: assert (test_view.is_valid_relation(nodes[2], node_assoc_1) == {'Error': 'Cannot associate failure_nodes when always_nodes have been associated.'}) + @pytest.mark.django_db class TestWorkflowJobFailure: """ diff --git a/awx/main/tests/functional/test_db_credential.py b/awx/main/tests/functional/test_db_credential.py index 7ed76fdd62..90ec21d4cd 100644 --- a/awx/main/tests/functional/test_db_credential.py +++ b/awx/main/tests/functional/test_db_credential.py @@ -3,6 +3,7 @@ import pytest from django.db import IntegrityError from awx.main.models import Credential + @pytest.mark.django_db def test_cred_unique_org_name_kind(organization_factory): objects = organization_factory("test") diff --git a/awx/main/tests/functional/test_fixture_factories.py b/awx/main/tests/functional/test_fixture_factories.py index 1c25f9d38d..83d96fdbd3 100644 --- a/awx/main/tests/functional/test_fixture_factories.py +++ b/awx/main/tests/functional/test_fixture_factories.py @@ -2,6 +2,7 @@ import pytest from awx.main.tests.factories import NotUnique + def test_roles_exc_not_persisted(organization_factory): with pytest.raises(RuntimeError) as exc: organization_factory('test-org', roles=['test-org.admin_role:user1'], persisted=False) @@ -92,6 +93,7 @@ def test_job_template_factory(job_template_factory): assert jt_objects.job_template.survey_spec is not None assert 'test-survey' in jt_objects.jobs[1].extra_vars + def test_survey_spec_generator_simple(survey_spec_factory): survey_spec = survey_spec_factory('survey_variable') assert 'name' in survey_spec @@ -100,6 +102,7 @@ def test_survey_spec_generator_simple(survey_spec_factory): assert type(survey_spec['spec'][0]) is dict assert survey_spec['spec'][0]['type'] == 'integer' + def test_survey_spec_generator_mixed(survey_spec_factory): survey_spec = survey_spec_factory( [{'variable': 'question1', 'type': 'integer', 'max': 87}, diff --git a/awx/main/tests/functional/test_jobs.py b/awx/main/tests/functional/test_jobs.py index 8a44b622a8..ae6ee0e850 100644 --- a/awx/main/tests/functional/test_jobs.py +++ b/awx/main/tests/functional/test_jobs.py @@ -2,6 +2,7 @@ from awx.main.models import Job, Instance from django.test.utils import override_settings import pytest + @pytest.mark.django_db def test_orphan_unified_job_creation(instance, inventory): job = Job.objects.create(job_template=None, inventory=inventory, name='hi world') @@ -10,6 +11,7 @@ def test_orphan_unified_job_creation(instance, inventory): assert job2.inventory == inventory assert job2.name == 'hi world' + @pytest.mark.django_db def test_job_capacity_and_with_inactive_node(): Instance.objects.create(hostname='test-1', capacity=50) diff --git a/awx/main/tests/functional/test_notifications.py b/awx/main/tests/functional/test_notifications.py index e5494edbea..cfa8ce76ee 100644 --- a/awx/main/tests/functional/test_notifications.py +++ b/awx/main/tests/functional/test_notifications.py @@ -7,6 +7,7 @@ from awx.main.models.jobs import JobTemplate from django.core.urlresolvers import reverse + @pytest.mark.django_db def test_get_notification_template_list(get, user, notification_template): url = reverse('api:notification_template_list') @@ -14,6 +15,7 @@ def test_get_notification_template_list(get, user, notification_template): assert response.status_code == 200 assert len(response.data['results']) == 1 + @pytest.mark.django_db def test_basic_parameterization(get, post, user, organization): u = user('admin-poster', True) @@ -38,6 +40,7 @@ def test_basic_parameterization(get, post, user, organization): assert 'url' in response.data['notification_configuration'] assert 'headers' in response.data['notification_configuration'] + @pytest.mark.django_db def test_encrypted_subfields(get, post, user, organization): def assert_send(self, messages): @@ -63,6 +66,7 @@ def test_encrypted_subfields(get, post, user, organization): with mock.patch.object(notification_template_actual.notification_class, "send_messages", assert_send): notification_template_actual.send("Test", {'body': "Test"}) + @pytest.mark.django_db def test_inherited_notification_templates(get, post, user, organization, project): u = user('admin-poster', True) @@ -98,6 +102,7 @@ def test_inherited_notification_templates(get, post, user, organization, project assert len(project.notification_templates['any']) == 2 assert len(g.inventory_source.notification_templates['any']) == 1 + @pytest.mark.django_db def test_notification_template_merging(get, post, user, organization, project, notification_template): user('admin-poster', True) @@ -105,14 +110,17 @@ def test_notification_template_merging(get, post, user, organization, project, n project.notification_templates_any.add(notification_template) assert len(project.notification_templates['any']) == 1 + @pytest.mark.django_db def test_notification_template_simple_patch(patch, notification_template, admin): patch(reverse('api:notification_template_detail', args=(notification_template.id,)), { 'name': 'foo'}, admin, expect=200) + @pytest.mark.django_db def test_notification_template_invalid_notification_type(patch, notification_template, admin): patch(reverse('api:notification_template_detail', args=(notification_template.id,)), { 'notification_type': 'invalid'}, admin, expect=400) + @pytest.mark.django_db def test_disallow_delete_when_notifications_pending(delete, user, notification_template): u = user('superuser', True) diff --git a/awx/main/tests/functional/test_partial.py b/awx/main/tests/functional/test_partial.py index d0b3ec6fa2..fd6d294cf2 100644 --- a/awx/main/tests/functional/test_partial.py +++ b/awx/main/tests/functional/test_partial.py @@ -20,10 +20,12 @@ from awx.main.scheduler.partial import ( InventoryUpdateLatestDict, ) + @pytest.fixture def org(): return Organization.objects.create(name="org1") + class TestProjectUpdateLatestDictDict(): @pytest.fixture def successful_project_update(self): @@ -85,6 +87,7 @@ class TestInventoryUpdateDict(): assert 1 == len(tasks) assert waiting_inventory_update.id == tasks[0]['id'] + class TestInventoryUpdateLatestDict(): @pytest.fixture def inventory(self, org): diff --git a/awx/main/tests/functional/test_projects.py b/awx/main/tests/functional/test_projects.py index 0a8fec4e72..cd3f6e57ff 100644 --- a/awx/main/tests/functional/test_projects.py +++ b/awx/main/tests/functional/test_projects.py @@ -83,12 +83,14 @@ def test_team_project_list(get, team_project_list): # alice should see all projects they can see when viewing an admin assert get(reverse('api:user_projects_list', args=(admin.pk,)), alice).data['count'] == 2 + @pytest.mark.django_db def test_team_project_list_fail1(get, team_project_list): objects = team_project_list res = get(reverse('api:team_projects_list', args=(objects.teams.team2.pk,)), objects.users.alice) assert res.status_code == 403 + @pytest.mark.parametrize("u,expected_status_code", [ ('rando', 403), ('org_member', 403), @@ -115,18 +117,22 @@ def test_create_project(post, organization, org_admin, org_member, admin, rando, if expected_status_code == 201: assert Project.objects.filter(name='Project', organization=organization).exists() + @pytest.mark.django_db() def test_create_project_null_organization(post, organization, admin): post(reverse('api:project_list'), { 'name': 't', 'organization': None}, admin, expect=201) + @pytest.mark.django_db() def test_create_project_null_organization_xfail(post, organization, org_admin): post(reverse('api:project_list'), { 'name': 't', 'organization': None}, org_admin, expect=403) + @pytest.mark.django_db() def test_patch_project_null_organization(patch, organization, project, admin): patch(reverse('api:project_detail', args=(project.id,)), { 'name': 't', 'organization': organization.id}, admin, expect=200) + @pytest.mark.django_db() def test_patch_project_null_organization_xfail(patch, project, org_admin): patch(reverse('api:project_detail', args=(project.id,)), { 'name': 't', 'organization': None}, org_admin, expect=400) diff --git a/awx/main/tests/functional/test_rbac_api.py b/awx/main/tests/functional/test_rbac_api.py index 5d5591cc67..8e8a24fc3c 100644 --- a/awx/main/tests/functional/test_rbac_api.py +++ b/awx/main/tests/functional/test_rbac_api.py @@ -5,11 +5,14 @@ from django.db import transaction from django.core.urlresolvers import reverse from awx.main.models.rbac import Role, ROLE_SINGLETON_SYSTEM_ADMINISTRATOR + def mock_feature_enabled(feature): return True + #@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) + @pytest.fixture def role(): return Role.objects.create(role_field='admin_role') @@ -19,6 +22,7 @@ def role(): # /roles # + @pytest.mark.django_db def test_get_roles_list_admin(organization, get, admin): 'Admin can see list of all roles' @@ -28,6 +32,7 @@ def test_get_roles_list_admin(organization, get, admin): roles = response.data assert roles['count'] > 0 + @pytest.mark.django_db def test_get_roles_list_user(organization, inventory, team, get, user): 'Users can see all roles they have access to, but not all roles' @@ -57,6 +62,7 @@ def test_get_roles_list_user(organization, inventory, team, get, user): assert inventory.admin_role.id not in role_hash assert team.member_role.id not in role_hash + @pytest.mark.django_db def test_roles_visibility(get, organization, project, admin, alice, bob): Role.singleton('system_auditor').members.add(alice) @@ -66,6 +72,7 @@ def test_roles_visibility(get, organization, project, admin, alice, bob): organization.auditor_role.members.add(bob) assert get(reverse('api:role_list') + '?id=%d' % project.update_role.id, user=bob).data['count'] == 1 + @pytest.mark.django_db def test_roles_filter_visibility(get, organization, project, admin, alice, bob): Role.singleton('system_auditor').members.add(alice) @@ -80,6 +87,7 @@ def test_roles_filter_visibility(get, organization, project, admin, alice, bob): project.use_role.members.add(bob) # sibling role should still grant visibility assert get(reverse('api:user_roles_list', args=(admin.id,)) + '?id=%d' % project.update_role.id, user=bob).data['count'] == 1 + @pytest.mark.django_db def test_cant_create_role(post, admin): "Ensure we can't create new roles through the api" @@ -105,6 +113,7 @@ def test_cant_delete_role(delete, admin): # /user//roles # + @pytest.mark.django_db def test_get_user_roles_list(get, admin): url = reverse('api:user_roles_list', args=(admin.id,)) @@ -113,6 +122,7 @@ def test_get_user_roles_list(get, admin): roles = response.data assert roles['count'] > 0 # 'system_administrator' role if nothing else + @pytest.mark.django_db def test_user_view_other_user_roles(organization, inventory, team, get, alice, bob): 'Users can see roles for other users, but only the roles that that user has access to see as well' @@ -159,8 +169,6 @@ def test_user_view_other_user_roles(organization, inventory, team, get, alice, b assert team.member_role.id in role_hash # Alice can now see this - - @pytest.mark.django_db def test_add_role_to_user(role, post, admin): assert admin.roles.filter(id=role.id).count() == 0 @@ -178,6 +186,7 @@ def test_add_role_to_user(role, post, admin): assert response.status_code == 400 assert admin.roles.filter(id=role.id).count() == 1 + @pytest.mark.django_db def test_remove_role_from_user(role, post, admin): assert admin.roles.filter(id=role.id).count() == 0 @@ -191,12 +200,11 @@ def test_remove_role_from_user(role, post, admin): assert admin.roles.filter(id=role.id).count() == 0 - - # # /team//roles # + @pytest.mark.django_db def test_get_teams_roles_list(get, team, organization, admin): team.member_role.children.add(organization.admin_role) @@ -226,6 +234,7 @@ def test_add_role_to_teams(team, post, admin): assert response.status_code == 400 assert team.member_role.children.filter(id=team.member_role.id).count() == 1 + @pytest.mark.django_db def test_remove_role_from_teams(team, post, admin): assert team.member_role.children.filter(id=team.member_role.id).count() == 0 @@ -239,11 +248,11 @@ def test_remove_role_from_teams(team, post, admin): assert team.member_role.children.filter(id=team.member_role.id).count() == 0 - # # /roles// # + @pytest.mark.django_db def test_get_role(get, admin, role): url = reverse('api:role_detail', args=(role.id,)) @@ -259,6 +268,7 @@ def test_put_role_405(put, admin, role): #r = Role.objects.get(id=role.id) #assert r.name == 'Some new name' + @pytest.mark.django_db def test_put_role_access_denied(put, alice, role): url = reverse('api:role_detail', args=(role.id,)) @@ -270,6 +280,7 @@ def test_put_role_access_denied(put, alice, role): # /roles//users/ # + @pytest.mark.django_db def test_get_role_users(get, admin, role): role.members.add(admin) @@ -279,6 +290,7 @@ def test_get_role_users(get, admin, role): assert response.data['count'] == 1 assert response.data['results'][0]['id'] == admin.id + @pytest.mark.django_db def test_add_user_to_role(post, admin, role): url = reverse('api:role_users_list', args=(role.id,)) @@ -286,6 +298,7 @@ def test_add_user_to_role(post, admin, role): post(url, {'id': admin.id}, admin) assert role.members.filter(id=admin.id).count() == 1 + @pytest.mark.django_db def test_remove_user_to_role(post, admin, role): role.members.add(admin) @@ -294,6 +307,7 @@ def test_remove_user_to_role(post, admin, role): post(url, {'disassociate': True, 'id': admin.id}, admin) assert role.members.filter(id=admin.id).count() == 0 + @pytest.mark.django_db def test_org_admin_add_user_to_job_template(post, organization, check_jobtemplate, user): 'Tests that a user with permissions to assign/revoke membership to a particular role can do so' @@ -355,10 +369,12 @@ def test_user_fail_to_remove_user_to_job_template(post, organization, check_jobt assert joe in check_jobtemplate.execute_role + # # /roles//teams/ # + @pytest.mark.django_db def test_get_role_teams(get, team, admin, role): role.parents.add(team.member_role) @@ -377,6 +393,7 @@ def test_add_team_to_role(post, team, admin, role): assert res.status_code == 204 assert role.parents.filter(id=team.member_role.id).count() == 1 + @pytest.mark.django_db def test_remove_team_from_role(post, team, admin, role): role.members.add(admin) @@ -391,6 +408,7 @@ def test_remove_team_from_role(post, team, admin, role): # /roles//parents/ # + @pytest.mark.django_db def test_role_parents(get, team, admin, role): role.parents.add(team.member_role) @@ -405,6 +423,7 @@ def test_role_parents(get, team, admin, role): # /roles//children/ # + @pytest.mark.django_db def test_role_children(get, team, admin, role): role.parents.add(team.member_role) @@ -420,6 +439,7 @@ def test_role_children(get, team, admin, role): # Generics # + @pytest.mark.django_db def test_ensure_rbac_fields_are_present(organization, get, admin): url = reverse('api:organization_detail', args=(organization.id,)) diff --git a/awx/main/tests/functional/test_rbac_core.py b/awx/main/tests/functional/test_rbac_core.py index 75c89643ad..bae3b61dc0 100644 --- a/awx/main/tests/functional/test_rbac_core.py +++ b/awx/main/tests/functional/test_rbac_core.py @@ -53,7 +53,6 @@ def test_auto_inheritance_by_parents(organization, alice): assert alice not in organization.admin_role - @pytest.mark.django_db def test_accessible_objects(organization, alice, bob): A = Role.objects.create() @@ -68,6 +67,7 @@ def test_accessible_objects(organization, alice, bob): assert Organization.accessible_objects(alice, 'admin_role').count() == 1 assert Organization.accessible_objects(bob, 'admin_role').count() == 0 + @pytest.mark.django_db def test_team_symantics(organization, team, alice): assert alice not in organization.auditor_role @@ -93,6 +93,7 @@ def test_auto_field_adjustments(organization, inventory, team, alice): assert alice not in inventory.admin_role #assert False + @pytest.mark.django_db def test_implicit_deletes(alice): 'Ensures implicit resources and roles delete themselves' @@ -127,6 +128,7 @@ def test_content_object(user): org = Organization.objects.create(name='test-org') assert org.admin_role.content_object.id == org.id + @pytest.mark.django_db def test_hierarchy_rebuilding_multi_path(): 'Tests a subdtle cases around role hierarchy rebuilding when you have multiple paths to the same role of different length' diff --git a/awx/main/tests/functional/test_rbac_credential.py b/awx/main/tests/functional/test_rbac_credential.py index ae68f036d8..6c87a53d27 100644 --- a/awx/main/tests/functional/test_rbac_credential.py +++ b/awx/main/tests/functional/test_rbac_credential.py @@ -8,6 +8,7 @@ from awx.main.migrations import _rbac as rbac from django.apps import apps from django.contrib.auth.models import User + @pytest.mark.django_db def test_credential_migration_user(credential, user, permissions): u = user('user', False) @@ -18,6 +19,7 @@ def test_credential_migration_user(credential, user, permissions): assert u in credential.admin_role + @pytest.mark.django_db def test_two_teams_same_cred_name(organization_factory): objects = organization_factory("test", @@ -33,12 +35,14 @@ def test_two_teams_same_cred_name(organization_factory): assert objects.teams.team1.member_role in cred1.use_role.parents.all() assert objects.teams.team2.member_role in cred2.use_role.parents.all() + @pytest.mark.django_db def test_credential_use_role(credential, user, permissions): u = user('user', False) credential.use_role.members.add(u) assert u in credential.use_role + @pytest.mark.django_db def test_credential_migration_team_member(credential, team, user, permissions): u = user('user', False) @@ -58,6 +62,7 @@ def test_credential_migration_team_member(credential, team, user, permissions): assert u in credential.use_role assert u not in credential.admin_role + @pytest.mark.django_db def test_credential_migration_team_admin(credential, team, user, permissions): u = user('user', False) @@ -71,6 +76,7 @@ def test_credential_migration_team_admin(credential, team, user, permissions): rbac.migrate_credential(apps, None) assert u in credential.admin_role + @pytest.mark.django_db def test_credential_migration_org_auditor(credential, team, org_auditor): # Team's organization is the org_auditor's org @@ -89,6 +95,7 @@ def test_credential_migration_org_auditor(credential, team, org_auditor): assert org_auditor not in credential.use_role assert org_auditor in credential.read_role + def test_credential_access_superuser(): u = User(username='admin', is_superuser=True) access = CredentialAccess(u) @@ -98,6 +105,7 @@ def test_credential_access_superuser(): assert access.can_change(credential, None) assert access.can_delete(credential) + @pytest.mark.django_db def test_credential_access_auditor(credential, organization_factory): objects = organization_factory("org_cred_auditor", @@ -109,6 +117,7 @@ def test_credential_access_auditor(credential, organization_factory): access = CredentialAccess(objects.users.user1) assert access.can_read(credential) + @pytest.mark.django_db def test_credential_access_admin(user, team, credential): u = user('org-admin', False) @@ -135,6 +144,7 @@ def test_credential_access_admin(user, team, credential): # should have can_change access as org-admin assert access.can_change(credential, {'description': 'New description.'}) + @pytest.mark.django_db def test_org_credential_access_member(alice, org_credential, credential): org_credential.admin_role.members.add(alice) @@ -152,6 +162,7 @@ def test_org_credential_access_member(alice, org_credential, credential): 'description': 'New description.', 'organization': None}) + @pytest.mark.django_db def test_cred_job_template_xfail(user, deploy_jobtemplate): ' Personal credential migration ' @@ -167,6 +178,7 @@ def test_cred_job_template_xfail(user, deploy_jobtemplate): rbac.migrate_credential(apps, None) assert not access.can_change(cred, {'organization': org.pk}) + @pytest.mark.django_db def test_cred_job_template(user, team, deploy_jobtemplate): ' Team credential migration => org credential ' @@ -188,6 +200,7 @@ def test_cred_job_template(user, team, deploy_jobtemplate): org.admin_role.members.remove(a) assert not access.can_change(cred, {'organization': org.pk}) + @pytest.mark.django_db def test_cred_multi_job_template_single_org_xfail(user, deploy_jobtemplate): a = user('admin', False) @@ -204,6 +217,7 @@ def test_cred_multi_job_template_single_org_xfail(user, deploy_jobtemplate): assert not access.can_change(cred, {'organization': org.pk}) + @pytest.mark.django_db def test_cred_multi_job_template_single_org(user, team, deploy_jobtemplate): a = user('admin', False) @@ -223,6 +237,7 @@ def test_cred_multi_job_template_single_org(user, team, deploy_jobtemplate): org.admin_role.members.remove(a) assert not access.can_change(cred, {'organization': org.pk}) + @pytest.mark.django_db def test_single_cred_multi_job_template_multi_org(user, organizations, credential, team): orgs = organizations(2) @@ -252,6 +267,7 @@ def test_single_cred_multi_job_template_multi_org(user, organizations, credentia assert jts[0].credential != jts[1].credential + @pytest.mark.django_db def test_cred_inventory_source(user, inventory, credential): u = user('member', False) @@ -268,6 +284,7 @@ def test_cred_inventory_source(user, inventory, credential): rbac.migrate_credential(apps, None) assert u not in credential.use_role + @pytest.mark.django_db def test_cred_project(user, credential, project): u = user('member', False) @@ -280,12 +297,14 @@ def test_cred_project(user, credential, project): rbac.migrate_credential(apps, None) assert u not in credential.use_role + @pytest.mark.django_db def test_cred_no_org(user, credential): su = user('su', True) access = CredentialAccess(su) assert access.can_change(credential, {'user': su.pk}) + @pytest.mark.django_db def test_cred_team(user, team, credential): u = user('a', False) diff --git a/awx/main/tests/functional/test_rbac_inventory.py b/awx/main/tests/functional/test_rbac_inventory.py index 4cfee93653..f2d3a087ab 100644 --- a/awx/main/tests/functional/test_rbac_inventory.py +++ b/awx/main/tests/functional/test_rbac_inventory.py @@ -30,6 +30,7 @@ def test_custom_inv_script_access(organization, user): organization.admin_role.members.add(ou) assert ou in custom_inv.admin_role + @pytest.mark.django_db def test_modify_inv_script_foreign_org_admin(org_admin, organization, organization_factory, project): custom_inv = CustomInventoryScript.objects.create(name='test', script='test', description='test', @@ -39,6 +40,7 @@ def test_modify_inv_script_foreign_org_admin(org_admin, organization, organizati access = CustomInventoryScriptAccess(org_admin) assert not access.can_change(custom_inv, {'organization': other_org.pk, 'name': 'new-project'}) + @pytest.mark.django_db def test_org_member_inventory_script_permissions(org_member, organization): custom_inv = CustomInventoryScript.objects.create(name='test', script='test', organization=organization) @@ -47,6 +49,7 @@ def test_org_member_inventory_script_permissions(org_member, organization): assert not access.can_delete(custom_inv) assert not access.can_change(custom_inv, {'name': 'ed-test'}) + @pytest.mark.django_db def test_inventory_admin_user(inventory, permissions, user): u = user('admin', False) @@ -61,6 +64,7 @@ def test_inventory_admin_user(inventory, permissions, user): assert inventory.use_role.members.filter(id=u.id).exists() is False assert inventory.update_role.members.filter(id=u.id).exists() is False + @pytest.mark.django_db def test_inventory_auditor_user(inventory, permissions, user): u = user('auditor', False) @@ -77,6 +81,7 @@ def test_inventory_auditor_user(inventory, permissions, user): assert inventory.use_role.members.filter(id=u.id).exists() is False assert inventory.update_role.members.filter(id=u.id).exists() is False + @pytest.mark.django_db def test_inventory_updater_user(inventory, permissions, user): u = user('updater', False) @@ -92,6 +97,7 @@ def test_inventory_updater_user(inventory, permissions, user): assert inventory.use_role.members.filter(id=u.id).exists() is False assert inventory.update_role.members.filter(id=u.id).exists() + @pytest.mark.django_db def test_inventory_executor_user(inventory, permissions, user): u = user('executor', False) @@ -109,7 +115,6 @@ def test_inventory_executor_user(inventory, permissions, user): assert inventory.update_role.members.filter(id=u.id).exists() is False - @pytest.mark.django_db def test_inventory_admin_team(inventory, permissions, user, team): u = user('admin', False) @@ -232,6 +237,7 @@ def test_access_auditor(organization, inventory, user): assert not access.can_delete(inventory) assert not access.can_run_ad_hoc_commands(inventory) + @pytest.mark.django_db def test_inventory_update_org_admin(inventory_update, org_admin): access = InventoryUpdateAccess(org_admin) diff --git a/awx/main/tests/functional/test_rbac_job.py b/awx/main/tests/functional/test_rbac_job.py index febade67eb..de1ccf6817 100644 --- a/awx/main/tests/functional/test_rbac_job.py +++ b/awx/main/tests/functional/test_rbac_job.py @@ -23,22 +23,26 @@ def normal_job(deploy_jobtemplate): inventory=deploy_jobtemplate.inventory ) + @pytest.fixture def jt_user(deploy_jobtemplate, rando): deploy_jobtemplate.execute_role.members.add(rando) return rando + @pytest.fixture def inv_updater(inventory, rando): inventory.update_role.members.add(rando) return rando + @pytest.fixture def host_adhoc(host, machine_credential, rando): host.inventory.adhoc_role.members.add(rando) machine_credential.use_role.members.add(rando) return rando + @pytest.fixture def proj_updater(project, rando): project.update_role.members.add(rando) @@ -52,6 +56,7 @@ def test_superuser_sees_orphans(normal_job, admin_user): access = JobAccess(admin_user) assert access.can_read(normal_job) + @pytest.mark.django_db def test_org_member_does_not_see_orphans(normal_job, org_member, project): normal_job.job_template = None @@ -60,18 +65,21 @@ def test_org_member_does_not_see_orphans(normal_job, org_member, project): access = JobAccess(org_member) assert not access.can_read(normal_job) + @pytest.mark.django_db def test_org_admin_sees_orphans(normal_job, org_admin): normal_job.job_template = None access = JobAccess(org_admin) assert access.can_read(normal_job) + @pytest.mark.django_db def test_org_auditor_sees_orphans(normal_job, org_auditor): normal_job.job_template = None access = JobAccess(org_auditor) assert access.can_read(normal_job) + # Delete permissions testing @pytest.mark.django_db def test_JT_admin_delete_denied(normal_job, rando): @@ -79,12 +87,14 @@ def test_JT_admin_delete_denied(normal_job, rando): access = JobAccess(rando) assert not access.can_delete(normal_job) + @pytest.mark.django_db def test_inventory_admin_delete_denied(normal_job, rando): normal_job.job_template.inventory.admin_role.members.add(rando) access = JobAccess(rando) assert not access.can_delete(normal_job) + @pytest.mark.django_db def test_null_related_delete_denied(normal_job, rando): normal_job.project = None @@ -92,24 +102,28 @@ def test_null_related_delete_denied(normal_job, rando): access = JobAccess(rando) assert not access.can_delete(normal_job) + @pytest.mark.django_db def test_delete_job_with_orphan_proj(normal_job, rando): normal_job.project.organization = None access = JobAccess(rando) assert not access.can_delete(normal_job) + @pytest.mark.django_db def test_inventory_org_admin_delete_allowed(normal_job, org_admin): normal_job.project = None # do this so we test job->inventory->org->admin connection access = JobAccess(org_admin) assert access.can_delete(normal_job) + @pytest.mark.django_db def test_project_org_admin_delete_allowed(normal_job, org_admin): normal_job.inventory = None # do this so we test job->project->org->admin connection access = JobAccess(org_admin) assert access.can_delete(normal_job) + @pytest.mark.django_db class TestJobAndUpdateCancels: diff --git a/awx/main/tests/functional/test_rbac_job_start.py b/awx/main/tests/functional/test_rbac_job_start.py index c934973cf4..4b6c092680 100644 --- a/awx/main/tests/functional/test_rbac_job_start.py +++ b/awx/main/tests/functional/test_rbac_job_start.py @@ -4,10 +4,10 @@ from awx.main.models.inventory import Inventory from awx.main.models.credential import Credential from awx.main.models.jobs import JobTemplate, Job + @pytest.mark.django_db @pytest.mark.job_permissions def test_admin_executing_permissions(deploy_jobtemplate, inventory, machine_credential, user): - admin_user = user('admin-user', True) assert admin_user.can_access(Inventory, 'use', inventory) @@ -15,33 +15,34 @@ def test_admin_executing_permissions(deploy_jobtemplate, inventory, machine_cred assert admin_user.can_access(JobTemplate, 'start', deploy_jobtemplate) assert admin_user.can_access(Credential, 'use', machine_credential) + @pytest.mark.django_db @pytest.mark.job_permissions def test_job_template_start_access(deploy_jobtemplate, user): - common_user = user('test-user', False) deploy_jobtemplate.execute_role.members.add(common_user) assert common_user.can_access(JobTemplate, 'start', deploy_jobtemplate) + @pytest.mark.django_db @pytest.mark.job_permissions def test_credential_use_access(machine_credential, user): - common_user = user('test-user', False) machine_credential.use_role.members.add(common_user) assert common_user.can_access(Credential, 'use', machine_credential) + @pytest.mark.django_db @pytest.mark.job_permissions def test_inventory_use_access(inventory, user): - common_user = user('test-user', False) inventory.use_role.members.add(common_user) assert common_user.can_access(Inventory, 'use', inventory) + @pytest.mark.django_db class TestJobRelaunchAccess: @pytest.fixture diff --git a/awx/main/tests/functional/test_rbac_job_templates.py b/awx/main/tests/functional/test_rbac_job_templates.py index 71412959c9..ac545dafee 100644 --- a/awx/main/tests/functional/test_rbac_job_templates.py +++ b/awx/main/tests/functional/test_rbac_job_templates.py @@ -22,6 +22,7 @@ def jt_objects(job_template_factory): credential='cred1', cloud_credential='aws1', network_credential='juniper1') return objects + @pytest.mark.django_db def test_job_template_migration_check(credential, deploy_jobtemplate, check_jobtemplate, user): admin = user('admin', is_superuser=True) @@ -53,6 +54,7 @@ def test_job_template_migration_check(credential, deploy_jobtemplate, check_jobt assert admin in deploy_jobtemplate.execute_role assert joe not in deploy_jobtemplate.execute_role + @pytest.mark.django_db def test_job_template_migration_deploy(credential, deploy_jobtemplate, check_jobtemplate, user): admin = user('admin', is_superuser=True) @@ -168,6 +170,7 @@ def test_job_template_access_superuser(check_license, user, deploy_jobtemplate): assert access.can_read(deploy_jobtemplate) assert access.can_add({}) + @pytest.mark.django_db def test_job_template_access_read_level(jt_objects, rando): @@ -184,6 +187,7 @@ def test_job_template_access_read_level(jt_objects, rando): assert not access.can_add(dict(cloud_credential=jt_objects.cloud_credential.pk, project=proj_pk)) assert not access.can_add(dict(network_credential=jt_objects.network_credential.pk, project=proj_pk)) + @pytest.mark.django_db def test_job_template_access_use_level(jt_objects, rando): @@ -200,6 +204,7 @@ def test_job_template_access_use_level(jt_objects, rando): assert access.can_add(dict(cloud_credential=jt_objects.cloud_credential.pk, project=proj_pk)) assert access.can_add(dict(network_credential=jt_objects.network_credential.pk, project=proj_pk)) + @pytest.mark.django_db def test_job_template_access_org_admin(jt_objects, rando): access = JobTemplateAccess(rando) @@ -220,6 +225,7 @@ def test_job_template_access_org_admin(jt_objects, rando): assert access.can_read(jt_objects.job_template) assert access.can_delete(jt_objects.job_template) + @pytest.mark.django_db @pytest.mark.job_permissions def test_job_template_creator_access(project, rando, post): @@ -243,6 +249,7 @@ def test_job_template_creator_access(project, rando, post): # Creating a JT should place the creator in the admin role assert rando in jt_obj.admin_role + @pytest.mark.django_db def test_associate_label(label, user, job_template): access = JobTemplateAccess(user('joe', False)) @@ -250,6 +257,7 @@ def test_associate_label(label, user, job_template): label.organization.read_role.members.add(user('joe', False)) assert access.can_attach(job_template, label, 'labels', None) + @pytest.mark.django_db def test_move_schedule_to_JT_no_access(job_template, rando): schedule = Schedule.objects.create( @@ -260,6 +268,7 @@ def test_move_schedule_to_JT_no_access(job_template, rando): access = ScheduleAccess(rando) assert not access.can_change(schedule, data=dict(unified_job_template=jt2.pk)) + @pytest.mark.django_db def test_move_schedule_from_JT_no_access(job_template, rando): schedule = Schedule.objects.create( diff --git a/awx/main/tests/functional/test_rbac_label.py b/awx/main/tests/functional/test_rbac_label.py index d10e48ed0a..a34a4bf27f 100644 --- a/awx/main/tests/functional/test_rbac_label.py +++ b/awx/main/tests/functional/test_rbac_label.py @@ -4,6 +4,7 @@ from awx.main.access import ( LabelAccess, ) + @pytest.mark.django_db def test_label_get_queryset_user(label, user): u = user('user', False) @@ -11,16 +12,19 @@ def test_label_get_queryset_user(label, user): label.organization.member_role.members.add(u) assert access.get_queryset().count() == 1 + @pytest.mark.django_db def test_label_get_queryset_su(label, user): access = LabelAccess(user('user', True)) assert access.get_queryset().count() == 1 + @pytest.mark.django_db def test_label_access(label, user): access = LabelAccess(user('user', False)) assert not access.can_read(label) + @pytest.mark.django_db def test_label_access_superuser(label, user): access = LabelAccess(user('admin', True)) @@ -29,6 +33,7 @@ def test_label_access_superuser(label, user): assert access.can_change(label, None) assert access.can_delete(label) + @pytest.mark.django_db def test_label_access_admin(organization_factory): '''can_change because I am an admin of that org''' @@ -48,6 +53,7 @@ def test_label_access_admin(organization_factory): assert access.can_change(label, {'organization': members.organization.id}) assert access.can_delete(label) + @pytest.mark.django_db def test_label_access_user(label, user): access = LabelAccess(user('user', False)) diff --git a/awx/main/tests/functional/test_rbac_notifications.py b/awx/main/tests/functional/test_rbac_notifications.py index a9a5e7c5f9..05f19740fe 100644 --- a/awx/main/tests/functional/test_rbac_notifications.py +++ b/awx/main/tests/functional/test_rbac_notifications.py @@ -5,33 +5,39 @@ from awx.main.access import ( NotificationAccess ) + @pytest.mark.django_db def test_notification_template_get_queryset_orgmember(notification_template, user): access = NotificationTemplateAccess(user('user', False)) notification_template.organization.member_role.members.add(user('user', False)) assert access.get_queryset().count() == 0 + @pytest.mark.django_db def test_notification_template_get_queryset_nonorgmember(notification_template, user): access = NotificationTemplateAccess(user('user', False)) assert access.get_queryset().count() == 0 + @pytest.mark.django_db def test_notification_template_get_queryset_su(notification_template, user): access = NotificationTemplateAccess(user('user', True)) assert access.get_queryset().count() == 1 + @pytest.mark.django_db def test_notification_template_get_queryset_orgadmin(notification_template, user): access = NotificationTemplateAccess(user('admin', False)) notification_template.organization.admin_role.members.add(user('admin', False)) assert access.get_queryset().count() == 1 + @pytest.mark.django_db def test_notification_template_get_queryset_org_auditor(notification_template, org_auditor): access = NotificationTemplateAccess(org_auditor) assert access.get_queryset().count() == 1 + @pytest.mark.django_db def test_notification_template_access_superuser(notification_template_factory): nf_objects = notification_template_factory('test-orphaned', organization='test', superusers=['admin']) @@ -50,6 +56,7 @@ def test_notification_template_access_superuser(notification_template_factory): assert access.can_change(nf, None) assert access.can_delete(nf) + @pytest.mark.django_db def test_notification_template_access_admin(organization_factory, notification_template_factory): other_objects = organization_factory('other') @@ -75,6 +82,7 @@ def test_notification_template_access_admin(organization_factory, notification_t assert not access.can_change(nf, None) assert not access.can_delete(nf) + @pytest.mark.django_db def test_notification_template_access_org_user(notification_template, user): u = user('normal', False) @@ -84,34 +92,40 @@ def test_notification_template_access_org_user(notification_template, user): assert not access.can_change(notification_template, None) assert not access.can_delete(notification_template) + @pytest.mark.django_db def test_notificaiton_template_orphan_access_org_admin(notification_template, organization, org_admin): notification_template.organization = None access = NotificationTemplateAccess(org_admin) assert not access.can_change(notification_template, {'organization': organization.id}) + @pytest.mark.django_db def test_notification_access_get_queryset_org_admin(notification, org_admin): access = NotificationAccess(org_admin) assert access.get_queryset().count() == 1 + @pytest.mark.django_db def test_notification_access_get_queryset_org_auditor(notification, org_auditor): access = NotificationAccess(org_auditor) assert access.get_queryset().count() == 1 + @pytest.mark.django_db def test_notification_access_system_admin(notification, admin): access = NotificationAccess(admin) assert access.can_read(notification) assert access.can_delete(notification) + @pytest.mark.django_db def test_notification_access_org_admin(notification, org_admin): access = NotificationAccess(org_admin) assert access.can_read(notification) assert access.can_delete(notification) + @pytest.mark.django_db def test_notification_access_org_auditor(notification, org_auditor): access = NotificationAccess(org_auditor) diff --git a/awx/main/tests/functional/test_rbac_organization.py b/awx/main/tests/functional/test_rbac_organization.py index 77558c0e7c..1ecf6c7f85 100644 --- a/awx/main/tests/functional/test_rbac_organization.py +++ b/awx/main/tests/functional/test_rbac_organization.py @@ -22,6 +22,7 @@ def test_organization_migration_admin(organization, permissions, user): assert u in organization.admin_role + @pytest.mark.django_db def test_organization_migration_user(organization, permissions, user): u = user('user', False) diff --git a/awx/main/tests/functional/test_rbac_project.py b/awx/main/tests/functional/test_rbac_project.py index 2b342df198..9f7cfbe705 100644 --- a/awx/main/tests/functional/test_rbac_project.py +++ b/awx/main/tests/functional/test_rbac_project.py @@ -92,6 +92,7 @@ def test_project_migration(): assert o2.projects.all()[0].jobtemplates.count() == 1 assert o3.projects.all()[0].jobtemplates.count() == 0 + @pytest.mark.django_db def test_single_org_project_migration(organization): project = Project.objects.create(name='my project', @@ -103,6 +104,7 @@ def test_single_org_project_migration(organization): project = Project.objects.get(id=project.id) assert project.organization.id == organization.id + @pytest.mark.django_db def test_no_org_project_migration(organization): project = Project.objects.create(name='my project', @@ -112,6 +114,7 @@ def test_no_org_project_migration(organization): rbac.migrate_projects(apps, None) assert project.organization is None + @pytest.mark.django_db def test_multi_org_project_migration(): org1 = Organization.objects.create(name="org1", description="org1 desc") @@ -145,6 +148,7 @@ def test_project_user_project(user_project, project, user): assert u in user_project.read_role assert u not in project.read_role + @pytest.mark.django_db def test_project_accessible_by_sa(user, project): u = user('systemadmin', is_superuser=True) @@ -159,6 +163,7 @@ def test_project_accessible_by_sa(user, project): print(project.admin_role.ancestors.all()) assert u in project.admin_role + @pytest.mark.django_db def test_project_org_members(user, organization, project): admin = user('orgadmin') @@ -176,6 +181,7 @@ def test_project_org_members(user, organization, project): assert admin in project.admin_role assert member in project.read_role + @pytest.mark.django_db def test_project_team(user, team, project): nonmember = user('nonmember') @@ -194,6 +200,7 @@ def test_project_team(user, team, project): assert member in project.read_role assert nonmember not in project.read_role + @pytest.mark.django_db def test_project_explicit_permission(user, team, project, organization): u = user('prjuser') @@ -211,6 +218,7 @@ def test_project_explicit_permission(user, team, project, organization): assert u in project.read_role + @pytest.mark.django_db def test_create_project_foreign_org_admin(org_admin, organization, organization_factory): """Org admins can only create projects in their own org.""" @@ -218,6 +226,7 @@ def test_create_project_foreign_org_admin(org_admin, organization, organization_ access = ProjectAccess(org_admin) assert not access.can_add({'organization': other_org.pk, 'name': 'new-project'}) + @pytest.mark.django_db def test_modify_project_foreign_org_admin(org_admin, organization, organization_factory, project): """Org admins can only modify projects in their own org.""" diff --git a/awx/main/tests/functional/test_rbac_role.py b/awx/main/tests/functional/test_rbac_role.py index 613051e395..16ad46f8db 100644 --- a/awx/main/tests/functional/test_rbac_role.py +++ b/awx/main/tests/functional/test_rbac_role.py @@ -18,6 +18,7 @@ def test_team_access_attach(rando, team, inventory): data = {'id': inventory.admin_role.pk} assert not access.can_attach(team, inventory.admin_role, 'member_role.children', data, False) + @pytest.mark.django_db def test_user_access_attach(rando, inventory): inventory.read_role.members.add(rando) @@ -25,6 +26,7 @@ def test_user_access_attach(rando, inventory): data = {'id': inventory.admin_role.pk} assert not access.can_attach(rando, inventory.admin_role, 'roles', data, False) + @pytest.mark.django_db def test_role_access_attach(rando, inventory): inventory.read_role.members.add(rando) diff --git a/awx/main/tests/functional/test_rbac_team.py b/awx/main/tests/functional/test_rbac_team.py index 6907589462..5e7cf4ad85 100644 --- a/awx/main/tests/functional/test_rbac_team.py +++ b/awx/main/tests/functional/test_rbac_team.py @@ -22,6 +22,7 @@ def test_team_attach_unattach(team, user): assert not access.can_attach(team, team.member_role, 'member_role.children', None) assert not access.can_unattach(team, team.member_role, 'member_role.chidlren') + @pytest.mark.django_db def test_team_access_superuser(team, user): team.member_role.members.add(user('member', False)) @@ -36,6 +37,7 @@ def test_team_access_superuser(team, user): assert len(t.member_role.members.all()) == 1 assert len(t.organization.admin_role.members.all()) == 0 + @pytest.mark.django_db def test_team_access_org_admin(organization, team, user): a = user('admin', False) @@ -52,6 +54,7 @@ def test_team_access_org_admin(organization, team, user): assert len(t.member_role.members.all()) == 0 assert len(t.organization.admin_role.members.all()) == 1 + @pytest.mark.django_db def test_team_access_member(organization, team, user): u = user('member', False) @@ -68,6 +71,7 @@ def test_team_access_member(organization, team, user): assert len(t.member_role.members.all()) == 1 assert len(t.organization.admin_role.members.all()) == 0 + @pytest.mark.django_db def test_team_accessible_by(team, user, project): u = user('team_member', False) @@ -79,6 +83,7 @@ def test_team_accessible_by(team, user, project): team.member_role.members.add(u) assert u in project.read_role + @pytest.mark.django_db def test_team_accessible_objects(team, user, project): u = user('team_member', False) @@ -90,6 +95,7 @@ def test_team_accessible_objects(team, user, project): team.member_role.members.add(u) assert len(Project.accessible_objects(u, 'read_role')) == 1 + @pytest.mark.django_db def test_team_admin_member_access(team, user, project): u = user('team_admin', False) diff --git a/awx/main/tests/functional/test_rbac_user.py b/awx/main/tests/functional/test_rbac_user.py index de2b9aa8b1..19efd0859b 100644 --- a/awx/main/tests/functional/test_rbac_user.py +++ b/awx/main/tests/functional/test_rbac_user.py @@ -7,6 +7,7 @@ from awx.main.migrations import _rbac as rbac from awx.main.access import UserAccess from awx.main.models import Role + @pytest.mark.django_db def test_user_admin(user_project, project, user): username = unicode("\xc3\xb4", "utf-8") @@ -28,6 +29,7 @@ def test_user_admin(user_project, project, user): assert sa.members.filter(id=joe.id).exists() is False assert sa.members.filter(id=admin.id).exists() is True + @pytest.mark.django_db def test_user_queryset(user): u = user('pete', False) @@ -36,6 +38,7 @@ def test_user_queryset(user): qs = access.get_queryset() assert qs.count() == 1 + @pytest.mark.django_db def test_user_accessible_objects(user, organization): admin = user('admin', False) @@ -49,6 +52,7 @@ def test_user_accessible_objects(user, organization): organization.member_role.members.remove(u) assert User.accessible_objects(admin, 'admin_role').count() == 1 + @pytest.mark.django_db def test_org_user_admin(user, organization): admin = user('orgadmin') @@ -63,6 +67,7 @@ def test_org_user_admin(user, organization): organization.admin_role.members.remove(admin) assert admin not in member.admin_role + @pytest.mark.django_db def test_org_user_removed(user, organization): admin = user('orgadmin') @@ -76,6 +81,7 @@ def test_org_user_removed(user, organization): organization.member_role.members.remove(member) assert admin not in member.admin_role + @pytest.mark.django_db def test_org_admin_create_sys_auditor(org_admin): access = UserAccess(org_admin) @@ -83,6 +89,7 @@ def test_org_admin_create_sys_auditor(org_admin): username='new_user', password="pa$$sowrd", email="asdf@redhat.com", is_system_auditor='true')) + @pytest.mark.django_db def test_org_admin_edit_sys_auditor(org_admin, alice, organization): organization.member_role.members.add(alice) diff --git a/awx/main/tests/functional/test_rbac_workflow.py b/awx/main/tests/functional/test_rbac_workflow.py index 663af2639a..ca22dfd1a0 100644 --- a/awx/main/tests/functional/test_rbac_workflow.py +++ b/awx/main/tests/functional/test_rbac_workflow.py @@ -7,21 +7,25 @@ from awx.main.access import ( # WorkflowJobNodeAccess ) + @pytest.fixture def wfjt(workflow_job_template_factory, organization): objects = workflow_job_template_factory('test_workflow', organization=organization, persisted=True) return objects.workflow_job_template + @pytest.fixture def wfjt_with_nodes(workflow_job_template_factory, organization, job_template): objects = workflow_job_template_factory( 'test_workflow', organization=organization, workflow_job_template_nodes=[{'unified_job_template': job_template}], persisted=True) return objects.workflow_job_template + @pytest.fixture def wfjt_node(wfjt_with_nodes): return wfjt_with_nodes.workflow_job_template_nodes.all()[0] + @pytest.fixture def workflow_job(wfjt): return wfjt.jobs.create(name='test_workflow') @@ -50,6 +54,7 @@ class TestWorkflowJobTemplateAccess: access = WorkflowJobTemplateAccess(org_admin) assert not access.can_add({'reference_obj': wfjt_with_nodes}) + @pytest.mark.django_db class TestWorkflowJobTemplateNodeAccess: @@ -57,6 +62,7 @@ class TestWorkflowJobTemplateNodeAccess: access = WorkflowJobTemplateNodeAccess(org_admin) assert not access.can_change(wfjt_node, {'job_type': 'scan'}) + @pytest.mark.django_db class TestWorkflowJobAccess: diff --git a/awx/main/tests/job_base.py b/awx/main/tests/job_base.py index 76460e5b68..d24d19611a 100644 --- a/awx/main/tests/job_base.py +++ b/awx/main/tests/job_base.py @@ -12,6 +12,7 @@ TEST_PLAYBOOK = '''- hosts: all command: test 1 = 1 ''' + class BaseJobTestMixin(BaseTestMixin): diff --git a/awx/main/tests/manual/workflows/linear.py b/awx/main/tests/manual/workflows/linear.py index 4a8801de88..7375d8e9ab 100755 --- a/awx/main/tests/manual/workflows/linear.py +++ b/awx/main/tests/manual/workflows/linear.py @@ -5,6 +5,7 @@ from awx.main.models import ( ) from awx.main.models.jobs import JobTemplate + def do_init_workflow(job_template_success, job_template_fail, job_template_never): wfjt, created = WorkflowJobTemplate.objects.get_or_create(name="linear workflow") wfjt.delete() @@ -30,6 +31,7 @@ def do_init_workflow(job_template_success, job_template_fail, job_template_never nodes_success[1].failure_nodes.add(nodes_never[2]) + def do_init(): jt_success = JobTemplate.objects.get(id=5) jt_fail= JobTemplate.objects.get(id=6) diff --git a/awx/main/tests/manual/workflows/parallel.py b/awx/main/tests/manual/workflows/parallel.py index 766ddc6d0c..ff4973f8b4 100755 --- a/awx/main/tests/manual/workflows/parallel.py +++ b/awx/main/tests/manual/workflows/parallel.py @@ -5,6 +5,7 @@ from awx.main.models import ( ) from awx.main.models.jobs import JobTemplate + def do_init_workflow(job_template_success, job_template_fail, job_template_never, jts_parallel): wfjt, created = WorkflowJobTemplate.objects.get_or_create(name="parallel workflow") wfjt.delete() @@ -30,6 +31,7 @@ def do_init_workflow(job_template_success, job_template_fail, job_template_never for i, n in enumerate(nodes_parallel): n.failure_nodes.add(nodes_never[i]) + def do_init(): jt_success = JobTemplate.objects.get(id=5) jt_fail= JobTemplate.objects.get(id=6) diff --git a/awx/main/tests/old/ad_hoc.py b/awx/main/tests/old/ad_hoc.py index 2e05ae8017..8da0e33d24 100644 --- a/awx/main/tests/old/ad_hoc.py +++ b/awx/main/tests/old/ad_hoc.py @@ -29,6 +29,7 @@ from awx.main.tests.data.ssh import ( __all__ = ['RunAdHocCommandTest', 'AdHocCommandApiTest'] + class BaseAdHocCommandTest(BaseJobExecutionTest): ''' Common initialization for testing ad hoc commands. @@ -377,6 +378,7 @@ class RunAdHocCommandTest(BaseAdHocCommandTest): def run_pexpect_mock(self, *args, **kwargs): return 'successful', 0 + @unittest.skipIf(os.environ.get('SKIP_SLOW_TESTS', False), 'Skipping slow test') class AdHocCommandApiTest(BaseAdHocCommandTest): ''' diff --git a/awx/main/tests/old/commands/commands_monolithic.py b/awx/main/tests/old/commands/commands_monolithic.py index 28e0d79db1..5a176e76bb 100644 --- a/awx/main/tests/old/commands/commands_monolithic.py +++ b/awx/main/tests/old/commands/commands_monolithic.py @@ -185,6 +185,7 @@ class BaseCommandMixin(object): sys.stderr = original_stderr return result, captured_stdout, captured_stderr + class CreateDefaultOrgTest(BaseCommandMixin, BaseTest): ''' Test cases for create_default_org management command. @@ -209,6 +210,7 @@ class CreateDefaultOrgTest(BaseCommandMixin, BaseTest): self.assertFalse('Default organization added' in stdout) self.assertEqual(Organization.objects.count(), 1) + class DumpDataTest(BaseCommandMixin, BaseTest): ''' Test cases for dumpdata management command. @@ -223,6 +225,7 @@ class DumpDataTest(BaseCommandMixin, BaseTest): self.assertEqual(result, None) json.loads(stdout) + @override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True, ANSIBLE_TRANSPORT='local') diff --git a/awx/main/tests/old/inventory.py b/awx/main/tests/old/inventory.py index e7e751532c..a2baf49fe6 100644 --- a/awx/main/tests/old/inventory.py +++ b/awx/main/tests/old/inventory.py @@ -39,6 +39,7 @@ inventory['group-\u037c\u03b4\u0138\u0137\u03cd\u03a1\u0121\u0137\u0138\u01a1']. print json.dumps(inventory) """ + @unittest.skipIf(os.environ.get('SKIP_SLOW_TESTS', False), 'Skipping slow test') class InventoryTest(BaseTest): diff --git a/awx/main/tests/old/jobs/job_launch.py b/awx/main/tests/old/jobs/job_launch.py index dabf3568bc..f97b1638e0 100644 --- a/awx/main/tests/old/jobs/job_launch.py +++ b/awx/main/tests/old/jobs/job_launch.py @@ -17,6 +17,7 @@ import yaml __all__ = ['JobTemplateLaunchTest', 'JobTemplateLaunchPasswordsTest'] + @unittest.skipIf(os.environ.get('SKIP_SLOW_TESTS', False), 'Skipping slow test') class JobTemplateLaunchTest(BaseJobTestMixin, django.test.TransactionTestCase): def setUp(self): @@ -186,6 +187,7 @@ class JobTemplateLaunchTest(BaseJobTestMixin, django.test.TransactionTestCase): with self.current_user(self.user_sue): self.post(self.launch_url, {}, expect=400) + @unittest.skipIf(os.environ.get('SKIP_SLOW_TESTS', False), 'Skipping slow test') class JobTemplateLaunchPasswordsTest(BaseJobTestMixin, django.test.TransactionTestCase): def setUp(self): diff --git a/awx/main/tests/old/jobs/job_relaunch.py b/awx/main/tests/old/jobs/job_relaunch.py index 3a9e050288..5cb8f30cac 100644 --- a/awx/main/tests/old/jobs/job_relaunch.py +++ b/awx/main/tests/old/jobs/job_relaunch.py @@ -17,6 +17,7 @@ from awx.main.tests.job_base import BaseJobTestMixin __all__ = ['JobRelaunchTest',] + @unittest.skipIf(os.environ.get('SKIP_SLOW_TESTS', False), 'Skipping slow test') class JobRelaunchTest(BaseJobTestMixin, BaseLiveServerTest): diff --git a/awx/main/tests/old/jobs/jobs_monolithic.py b/awx/main/tests/old/jobs/jobs_monolithic.py index b7321bb611..eb4bbf85d7 100644 --- a/awx/main/tests/old/jobs/jobs_monolithic.py +++ b/awx/main/tests/old/jobs/jobs_monolithic.py @@ -186,6 +186,7 @@ TEST_SURVEY_REQUIREMENTS = ''' } ''' + @unittest.skipIf(os.environ.get('SKIP_SLOW_TESTS', False), 'Skipping slow test') class JobTemplateTest(BaseJobTestMixin, django.test.TransactionTestCase): @@ -505,6 +506,7 @@ class JobTemplateTest(BaseJobTestMixin, django.test.TransactionTestCase): with self.current_user(self.user_doug): self.get(detail_url, expect=403) + @unittest.skipIf(os.environ.get('SKIP_SLOW_TESTS', False), 'Skipping slow test') class JobTest(BaseJobTestMixin, django.test.TransactionTestCase): @@ -658,6 +660,7 @@ class JobTest(BaseJobTestMixin, django.test.TransactionTestCase): # and that jobs come back nicely serialized with related resources and so on ... # that we can drill all the way down and can get at host failure lists, etc ... + @unittest.skipIf(os.environ.get('SKIP_SLOW_TESTS', False), 'Skipping slow test') @override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True, @@ -1099,6 +1102,7 @@ class JobTransactionTest(BaseJobTestMixin, django.test.LiveServerTestCase): self.assertEqual(job.status, 'successful', job.result_stdout) self.assertFalse(errors) + @unittest.skipIf(os.environ.get('SKIP_SLOW_TESTS', False), 'Skipping slow test') class JobTemplateSurveyTest(BaseJobTestMixin, django.test.TransactionTestCase): def setUp(self): diff --git a/awx/main/tests/old/jobs/start_cancel.py b/awx/main/tests/old/jobs/start_cancel.py index 3a6957af69..1067a618f3 100644 --- a/awx/main/tests/old/jobs/start_cancel.py +++ b/awx/main/tests/old/jobs/start_cancel.py @@ -17,6 +17,7 @@ from awx.main.tests.job_base import BaseJobTestMixin __all__ = ['JobStartCancelTest',] + @unittest.skipIf(os.environ.get('SKIP_SLOW_TESTS', False), 'Skipping slow test') class JobStartCancelTest(BaseJobTestMixin, BaseLiveServerTest): diff --git a/awx/main/tests/old/jobs/survey_password.py b/awx/main/tests/old/jobs/survey_password.py index f7588301d8..64b3bfee06 100644 --- a/awx/main/tests/old/jobs/survey_password.py +++ b/awx/main/tests/old/jobs/survey_password.py @@ -141,6 +141,7 @@ TESTS = { } } + class SurveyPasswordBaseTest(BaseTest, QueueStartStopTestMixin): def setUp(self): super(SurveyPasswordBaseTest, self).setUp() @@ -176,6 +177,7 @@ class SurveyPasswordBaseTest(BaseTest, QueueStartStopTestMixin): url = reverse('api:job_detail', args=(job.pk,)) return self.get(url, expect=200, auth=self.get_super_credentials(), accept='application/json') + class SurveyPasswordRedactedTest(SurveyPasswordBaseTest): ''' Transpose TEST[]['tests'] to the below format. A more flat format." diff --git a/awx/main/tests/old/projects.py b/awx/main/tests/old/projects.py index 01c459b794..3b894e18cc 100644 --- a/awx/main/tests/old/projects.py +++ b/awx/main/tests/old/projects.py @@ -37,6 +37,7 @@ TEST_PLAYBOOK = '''- hosts: mygroup command: test 1 = 1 ''' + class ProjectsTest(BaseTransactionTest): # tests for users, projects, and teams diff --git a/awx/main/tests/old/schedules.py b/awx/main/tests/old/schedules.py index 916df4e89d..8e114d7950 100644 --- a/awx/main/tests/old/schedules.py +++ b/awx/main/tests/old/schedules.py @@ -46,6 +46,8 @@ BAD_SCHEDULES = ["", "DTSTART:20140331T055000 RRULE:FREQ=MINUTELY;INTERVAL=10;CO "DTSTART:20140331T055000Z RRULE:FREQ=YEARLY;BYWEEKNO=10;INTERVAL=1", "DTSTART:20140331T055000Z RRULE:FREQ=HOURLY;INTERVAL=1 DTSTART:20140331T055000Z RRULE:FREQ=HOURLY;INTERVAL=1", "DTSTART:20140331T055000Z RRULE:FREQ=HOURLY;INTERVAL=1 RRULE:FREQ=HOURLY;INTERVAL=1"] + + class ScheduleTest(BaseTest): def setUp(self): diff --git a/awx/main/tests/old/scripts.py b/awx/main/tests/old/scripts.py index 09df77ae45..7af019fd7c 100644 --- a/awx/main/tests/old/scripts.py +++ b/awx/main/tests/old/scripts.py @@ -14,6 +14,7 @@ from awx.main.tests.base import BaseLiveServerTest __all__ = ['InventoryScriptTest'] + class BaseScriptTest(BaseLiveServerTest): ''' Base class for tests that run external scripts to access the API. @@ -55,6 +56,7 @@ class BaseScriptTest(BaseLiveServerTest): stdout, stderr = proc.communicate() return proc.returncode, stdout, stderr + class InventoryScriptTest(BaseScriptTest): ''' Test helper to run management command as standalone script. diff --git a/awx/main/tests/old/settings.py b/awx/main/tests/old/settings.py index 3c507ac992..a1614dac38 100644 --- a/awx/main/tests/old/settings.py +++ b/awx/main/tests/old/settings.py @@ -47,6 +47,7 @@ TEST_TOWER_SETTINGS_MANIFEST = { } } + @override_settings(TOWER_SETTINGS_MANIFEST=TEST_TOWER_SETTINGS_MANIFEST) @pytest.mark.skip(reason="Settings deferred to 3.1") class SettingsPlaceholder(BaseTest): diff --git a/awx/main/tests/old/tasks.py b/awx/main/tests/old/tasks.py index a80ea07b87..eba9bf552b 100644 --- a/awx/main/tests/old/tasks.py +++ b/awx/main/tests/old/tasks.py @@ -237,6 +237,7 @@ TEST_VAULT_PLAYBOOK = '''$ANSIBLE_VAULT;1.1;AES256 TEST_VAULT_PASSWORD = '1234' + @unittest.skipIf(os.environ.get('SKIP_SLOW_TESTS', False), 'Skipping slow test') class RunJobTest(BaseJobExecutionTest): ''' diff --git a/awx/main/tests/old/users.py b/awx/main/tests/old/users.py index 73ceafd840..26ec3c3676 100644 --- a/awx/main/tests/old/users.py +++ b/awx/main/tests/old/users.py @@ -39,6 +39,7 @@ class AuthTokenTimeoutTest(BaseTest): self.assertIn('Auth-Token-Timeout', response) self.assertEqual(response['Auth-Token-Timeout'], str(settings.AUTH_TOKEN_EXPIRATION)) + class AuthTokenLimitTest(BaseTest): def setUp(self): super(AuthTokenLimitTest, self).setUp() @@ -163,6 +164,7 @@ class AuthTokenProxyTest(BaseTest): response = self._get_me(expect=200, auth=auth_token, remote_addr=remote_addr, client_kwargs=client_kwargs) self.check_me_is_admin(response) + class UsersTest(BaseTest): def collection(self): diff --git a/awx/main/tests/unit/api/serializers/conftest.py b/awx/main/tests/unit/api/serializers/conftest.py index 3b056a149f..af35a4a002 100644 --- a/awx/main/tests/unit/api/serializers/conftest.py +++ b/awx/main/tests/unit/api/serializers/conftest.py @@ -1,6 +1,7 @@ import pytest + @pytest.fixture def get_related_assert(): def fn(model_obj, related, resource_name, related_resource_name): @@ -8,6 +9,7 @@ def get_related_assert(): assert related[related_resource_name] == '/api/v1/%s/%d/%s/' % (resource_name, model_obj.pk, related_resource_name) return fn + @pytest.fixture def get_related_mock_and_run(): def fn(serializer_class, model_obj): @@ -16,6 +18,7 @@ def get_related_mock_and_run(): return related return fn + @pytest.fixture def test_get_related(get_related_assert, get_related_mock_and_run): def fn(serializer_class, model_obj, resource_name, related_resource_name): @@ -24,12 +27,14 @@ def test_get_related(get_related_assert, get_related_mock_and_run): return related return fn + @pytest.fixture def get_summary_fields_assert(): def fn(summary, summary_field_name): assert summary_field_name in summary return fn + @pytest.fixture def get_summary_fields_mock_and_run(): def fn(serializer_class, model_obj): @@ -37,10 +42,11 @@ def get_summary_fields_mock_and_run(): return serializer.get_summary_fields(model_obj) return fn + @pytest.fixture def test_get_summary_fields(get_summary_fields_mock_and_run, get_summary_fields_assert): def fn(serializer_class, model_obj, summary_field_name): summary = get_summary_fields_mock_and_run(serializer_class, model_obj) get_summary_fields_assert(summary, summary_field_name) - return summary + return summary return fn diff --git a/awx/main/tests/unit/api/serializers/test_inventory_serializers.py b/awx/main/tests/unit/api/serializers/test_inventory_serializers.py index 35f36a1100..e79ea7c10b 100644 --- a/awx/main/tests/unit/api/serializers/test_inventory_serializers.py +++ b/awx/main/tests/unit/api/serializers/test_inventory_serializers.py @@ -19,8 +19,8 @@ from rest_framework.test import ( force_authenticate, ) -class TestCustomInventoryScriptSerializer(object): +class TestCustomInventoryScriptSerializer(object): @pytest.mark.parametrize("superuser,sysaudit,admin_role,value", ((True, False, False, '#!/python'), (False, True, False, '#!/python'), diff --git a/awx/main/tests/unit/api/serializers/test_job_serializers.py b/awx/main/tests/unit/api/serializers/test_job_serializers.py index a9eaecd2e9..4997e3674b 100644 --- a/awx/main/tests/unit/api/serializers/test_job_serializers.py +++ b/awx/main/tests/unit/api/serializers/test_job_serializers.py @@ -13,6 +13,7 @@ from awx.main.models import ( Job, ) + def mock_JT_resource_data(): return ({}, []) @@ -22,18 +23,22 @@ def job_template(mocker): mock_jt.resource_validation_data = mock_JT_resource_data return mock_jt + @pytest.fixture def job(mocker, job_template): return mocker.MagicMock(pk=5, job_template=job_template) + @pytest.fixture def labels(mocker): return [Label(id=x, name='label-%d' % x) for x in xrange(0, 25)] + @pytest.fixture def jobs(mocker): return [Job(id=x, name='job-%d' % x) for x in xrange(0, 25)] + @mock.patch('awx.api.serializers.UnifiedJobTemplateSerializer.get_related', lambda x,y: {}) @mock.patch('awx.api.serializers.JobOptionsSerializer.get_related', lambda x,y: {}) class TestJobSerializerGetRelated(): @@ -58,10 +63,10 @@ class TestJobSerializerGetRelated(): assert 'job_template' in related assert related['job_template'] == '/api/v1/%s/%d/' % ('job_templates', job.job_template.pk) + @mock.patch('awx.api.serializers.BaseSerializer.to_representation', lambda self,obj: { 'extra_vars': obj.extra_vars}) class TestJobSerializerSubstitution(): - def test_survey_password_hide(self, mocker): job = mocker.MagicMock(**{ 'display_extra_vars.return_value': '{\"secret_key\": \"$encrypted$\"}', @@ -73,6 +78,7 @@ class TestJobSerializerSubstitution(): job.display_extra_vars.assert_called_once_with() assert 'my_password' not in extra_vars + @mock.patch('awx.api.serializers.BaseSerializer.get_summary_fields', lambda x,y: {}) class TestJobOptionsSerializerGetSummaryFields(): def test__summary_field_labels_10_max(self, mocker, job_template, labels): @@ -88,4 +94,3 @@ class TestJobOptionsSerializerGetSummaryFields(): def test_labels_exists(self, test_get_summary_fields, job_template): test_get_summary_fields(JobOptionsSerializer, job_template, 'labels') - diff --git a/awx/main/tests/unit/api/serializers/test_job_template_serializers.py b/awx/main/tests/unit/api/serializers/test_job_template_serializers.py index ba47b09e7f..77c7e69dc4 100644 --- a/awx/main/tests/unit/api/serializers/test_job_template_serializers.py +++ b/awx/main/tests/unit/api/serializers/test_job_template_serializers.py @@ -17,23 +17,28 @@ from rest_framework.test import APIRequestFactory #DRF from rest_framework import serializers + def mock_JT_resource_data(): return ({}, []) + @pytest.fixture def job_template(mocker): mock_jt = mocker.MagicMock(pk=5) mock_jt.resource_validation_data = mock_JT_resource_data return mock_jt + @pytest.fixture def job(mocker, job_template): return mocker.MagicMock(pk=5, job_template=job_template) + @pytest.fixture def jobs(mocker): return [Job(id=x, name='job-%d' % x) for x in xrange(0, 25)] + @mock.patch('awx.api.serializers.UnifiedJobTemplateSerializer.get_related', lambda x,y: {}) @mock.patch('awx.api.serializers.JobOptionsSerializer.get_related', lambda x,y: {}) class TestJobTemplateSerializerGetRelated(): @@ -57,6 +62,7 @@ class TestJobTemplateSerializerGetRelated(): related = get_related_mock_and_run(JobTemplateSerializer, job_template) assert 'callback' not in related + class TestJobTemplateSerializerGetSummaryFields(): def test__recent_jobs(self, mocker, job_template, jobs): @@ -109,8 +115,8 @@ class TestJobTemplateSerializerGetSummaryFields(): assert response['user_capabilities']['copy'] == 'foo' assert response['user_capabilities']['edit'] == 'foobar' -class TestJobTemplateSerializerValidation(object): +class TestJobTemplateSerializerValidation(object): good_extra_vars = ["{\"test\": \"keys\"}", "---\ntest: key"] bad_extra_vars = ["{\"test\": \"keys\"", "---\ntest: [2"] @@ -121,4 +127,3 @@ class TestJobTemplateSerializerValidation(object): for ev in self.bad_extra_vars: with pytest.raises(serializers.ValidationError): serializer.validate_extra_vars(ev) - diff --git a/awx/main/tests/unit/api/serializers/test_workflow_serializers.py b/awx/main/tests/unit/api/serializers/test_workflow_serializers.py index 3d86952f8d..4f11ce64be 100644 --- a/awx/main/tests/unit/api/serializers/test_workflow_serializers.py +++ b/awx/main/tests/unit/api/serializers/test_workflow_serializers.py @@ -16,6 +16,7 @@ from awx.main.models import ( WorkflowJobNode, ) + @mock.patch('awx.api.serializers.UnifiedJobTemplateSerializer.get_related', lambda x,y: {}) class TestWorkflowJobTemplateSerializerGetRelated(): @pytest.fixture @@ -35,6 +36,7 @@ class TestWorkflowJobTemplateSerializerGetRelated(): 'workflow_job_templates', related_resource_name) + @mock.patch('awx.api.serializers.BaseSerializer.get_related', lambda x,y: {}) class TestWorkflowNodeBaseSerializerGetRelated(): @pytest.fixture @@ -42,7 +44,7 @@ class TestWorkflowNodeBaseSerializerGetRelated(): jt = job_template_factory(name="blah", persisted=False).job_template jt.pk = 1 return jt - + @pytest.fixture def workflow_job_template_node_related(self, job_template): return WorkflowJobTemplateNode(pk=1, unified_job_template=job_template) @@ -60,6 +62,7 @@ class TestWorkflowNodeBaseSerializerGetRelated(): related = WorkflowJobTemplateNodeSerializer().get_related(workflow_job_template_node) assert 'unified_job_template' not in related + @mock.patch('awx.api.serializers.WorkflowNodeBaseSerializer.get_related', lambda x,y: {}) class TestWorkflowJobTemplateNodeSerializerGetRelated(): @pytest.fixture @@ -111,11 +114,12 @@ class FakeView: def get_object(self): return self.obj + class FakeRequest: pass -class TestWorkflowJobTemplateNodeSerializerCharPrompts(): +class TestWorkflowJobTemplateNodeSerializerCharPrompts(): @pytest.fixture def WFJT_serializer(self): serializer = WorkflowJobTemplateNodeSerializer() diff --git a/awx/main/tests/unit/api/test_filters.py b/awx/main/tests/unit/api/test_filters.py index 55ef257567..65356958bb 100644 --- a/awx/main/tests/unit/api/test_filters.py +++ b/awx/main/tests/unit/api/test_filters.py @@ -4,6 +4,7 @@ from rest_framework.exceptions import PermissionDenied from awx.api.filters import FieldLookupBackend from awx.main.models import Credential, JobTemplate + @pytest.mark.parametrize(u"empty_value", [u'', '']) def test_empty_in(empty_value): field_lookup = FieldLookupBackend() @@ -11,12 +12,14 @@ def test_empty_in(empty_value): field_lookup.value_to_python(JobTemplate, 'project__in', empty_value) assert 'empty value for __in' in str(excinfo.value) + @pytest.mark.parametrize(u"valid_value", [u'foo', u'foo,']) def test_valid_in(valid_value): field_lookup = FieldLookupBackend() value, new_lookup = field_lookup.value_to_python(JobTemplate, 'project__in', valid_value) assert 'foo' in value + @pytest.mark.parametrize('lookup_suffix', ['', 'contains', 'startswith', 'in']) @pytest.mark.parametrize('password_field', Credential.PASSWORD_FIELDS) def test_filter_on_password_field(password_field, lookup_suffix): @@ -26,6 +29,7 @@ def test_filter_on_password_field(password_field, lookup_suffix): field, new_lookup = field_lookup.get_field_from_lookup(Credential, lookup) assert 'not allowed' in str(excinfo.value) + @pytest.mark.parametrize('lookup_suffix', ['', 'contains', 'startswith', 'in']) @pytest.mark.parametrize('password_field', Credential.PASSWORD_FIELDS) def test_filter_on_related_password_field(password_field, lookup_suffix): diff --git a/awx/main/tests/unit/api/test_generics.py b/awx/main/tests/unit/api/test_generics.py index 289b4547a8..b10b1c6c54 100644 --- a/awx/main/tests/unit/api/test_generics.py +++ b/awx/main/tests/unit/api/test_generics.py @@ -10,21 +10,25 @@ from rest_framework.response import Response # AWX from awx.api.generics import ParentMixin, SubListCreateAttachDetachAPIView, DeleteLastUnattachLabelMixin + @pytest.fixture def get_object_or_404(mocker): # pytest patch without return_value generates a random value, we are counting on this return mocker.patch('awx.api.generics.get_object_or_404') + @pytest.fixture def get_object_or_400(mocker): return mocker.patch('awx.api.generics.get_object_or_400') + @pytest.fixture def mock_response_new(mocker): m = mocker.patch('awx.api.generics.Response.__new__') m.return_value = m return m + @pytest.fixture def parent_relationship_factory(mocker): def rf(serializer_class, relationship_name, relationship_value=mocker.Mock()): @@ -38,6 +42,7 @@ def parent_relationship_factory(mocker): return (serializer, mock_parent_relationship) return rf + # TODO: Test create and associate failure (i.e. id doesn't exist, record already exists, permission denied) # TODO: Mock and check return (Response) class TestSubListCreateAttachDetachAPIView: @@ -122,6 +127,7 @@ class TestSubListCreateAttachDetachAPIView: view.unattach_validate.assert_called_with(mock_request) view.unattach_by_id.assert_not_called() + class TestDeleteLastUnattachLabelMixin: @mock.patch('__builtin__.super') def test_unattach_ok(self, super, mocker): @@ -159,6 +165,7 @@ class TestDeleteLastUnattachLabelMixin: super.unattach_validate.assert_called_with(mock_request) assert mock_response == res + class TestParentMixin: def test_get_parent_object(self, mocker, get_object_or_404): parent_mixin = ParentMixin() @@ -168,7 +175,6 @@ class TestParentMixin: mock_parent_mixin = mocker.MagicMock(wraps=parent_mixin) return_value = mock_parent_mixin.get_parent_object() - + get_object_or_404.assert_called_with(parent_mixin.parent_model, **parent_mixin.kwargs) assert get_object_or_404.return_value == return_value - diff --git a/awx/main/tests/unit/api/test_roles.py b/awx/main/tests/unit/api/test_roles.py index 2ee8ec2102..5cb49a92b0 100644 --- a/awx/main/tests/unit/api/test_roles.py +++ b/awx/main/tests/unit/api/test_roles.py @@ -19,6 +19,7 @@ from awx.main.models import ( Role, ) + @pytest.mark.parametrize("pk, err", [ (111, "not change the membership"), (1, "may not perform"), @@ -48,6 +49,7 @@ def test_user_roles_list_user_admin_role(pk, err): assert response.status_code == 403 assert err in response.content + @pytest.mark.parametrize("admin_role, err", [ (True, "may not perform"), (False, "not change the membership"), @@ -80,6 +82,7 @@ def test_role_users_list_other_user_admin_role(admin_role, err): assert response.status_code == 403 assert err in response.content + def test_team_roles_list_post_org_roles(): with mock.patch('awx.api.views.get_object_or_400') as role_get, \ mock.patch('awx.api.views.ContentType.objects.get_for_model') as ct_get: diff --git a/awx/main/tests/unit/api/test_views.py b/awx/main/tests/unit/api/test_views.py index 798c30bf6f..37b9177db7 100644 --- a/awx/main/tests/unit/api/test_views.py +++ b/awx/main/tests/unit/api/test_views.py @@ -6,12 +6,14 @@ from awx.api.views import ( JobTemplateLabelList, ) + @pytest.fixture def mock_response_new(mocker): m = mocker.patch('awx.api.views.Response.__new__') m.return_value = m return m + class TestApiV1RootView: def test_get_endpoints(self, mocker, mock_response_new): endpoints = [ @@ -54,6 +56,7 @@ class TestApiV1RootView: for endpoint in endpoints: assert endpoint in data_arg + class TestJobTemplateLabelList: def test_inherited_mixin_unattach(self): with mock.patch('awx.api.generics.DeleteLastUnattachLabelMixin.unattach') as mixin_unattach: diff --git a/awx/main/tests/unit/models/test_job_template_unit.py b/awx/main/tests/unit/models/test_job_template_unit.py index 088b4d3d7c..28f2dc2993 100644 --- a/awx/main/tests/unit/models/test_job_template_unit.py +++ b/awx/main/tests/unit/models/test_job_template_unit.py @@ -14,6 +14,7 @@ def test_missing_project_error(job_template_factory): validation_errors, resources_needed_to_start = obj.resource_validation_data() assert 'project' in validation_errors + def test_inventory_credential_need_to_start(job_template_factory): objects = job_template_factory( 'job-template-few-resources', @@ -23,6 +24,7 @@ def test_inventory_credential_need_to_start(job_template_factory): assert 'inventory' in obj.resources_needed_to_start assert 'credential' in obj.resources_needed_to_start + def test_inventory_credential_contradictions(job_template_factory): objects = job_template_factory( 'job-template-paradox', @@ -35,6 +37,7 @@ def test_inventory_credential_contradictions(job_template_factory): assert 'inventory' in validation_errors assert 'credential' in validation_errors + def test_survey_answers_as_string(job_template_factory): objects = job_template_factory( 'job-template-with-survey', @@ -45,12 +48,14 @@ def test_survey_answers_as_string(job_template_factory): accepted, ignored = jt._accept_or_ignore_job_kwargs(extra_vars=user_extra_vars) assert 'var1' in accepted['extra_vars'] + @pytest.mark.survey def test_job_template_survey_password_redaction(job_template_with_survey_passwords_unit): """Tests the JobTemplate model's funciton to redact passwords from extra_vars - used when creating a new job""" assert job_template_with_survey_passwords_unit.survey_password_variables() == ['secret_key', 'SSN'] + def test_job_template_survey_variable_validation(job_template_factory): objects = job_template_factory( 'survey_variable_validation', diff --git a/awx/main/tests/unit/models/test_label.py b/awx/main/tests/unit/models/test_label.py index 9aaa6a1171..5c35803403 100644 --- a/awx/main/tests/unit/models/test_label.py +++ b/awx/main/tests/unit/models/test_label.py @@ -9,9 +9,9 @@ mock_query_set = mock.MagicMock() mock_objects = mock.MagicMock(filter=mock.MagicMock(return_value=mock_query_set)) + @mock.patch('awx.main.models.label.Label.objects', mock_objects) class TestLabelFilterMocked: - def test_get_orphaned_labels(self, mocker): ret = Label.get_orphaned_labels() @@ -63,4 +63,3 @@ class TestLabelFilterMocked: mock_jt_qs.count.assert_called_with() assert ret is expected - diff --git a/awx/main/tests/unit/models/test_survey_models.py b/awx/main/tests/unit/models/test_survey_models.py index 4f9653755e..d58f7bd1c7 100644 --- a/awx/main/tests/unit/models/test_survey_models.py +++ b/awx/main/tests/unit/models/test_survey_models.py @@ -19,6 +19,7 @@ def job(mocker): ret.project = mocker.MagicMock(scm_revision='asdf1234') return ret + @pytest.mark.survey def test_job_survey_password_redaction(): """Tests the Job model's funciton to redact passwords from @@ -37,6 +38,7 @@ def test_job_survey_password_redaction(): 'secret_key': '$encrypted$', 'SSN': '$encrypted$'} + @pytest.mark.survey def test_survey_passwords_not_in_extra_vars(): """Tests that survey passwords not included in extra_vars are @@ -52,6 +54,7 @@ def test_survey_passwords_not_in_extra_vars(): 'submitter_email': 'foobar@redhat.com', } + def test_job_safe_args_redacted_passwords(job): """Verify that safe_args hides passwords in the job extra_vars""" kwargs = {'ansible_version': '2.1'} @@ -61,6 +64,7 @@ def test_job_safe_args_redacted_passwords(job): extra_vars = json.loads(safe_args[ev_index]) assert extra_vars['secret_key'] == '$encrypted$' + def test_job_args_unredacted_passwords(job): kwargs = {'ansible_version': '2.1'} run_job = RunJob() @@ -69,6 +73,7 @@ def test_job_args_unredacted_passwords(job): extra_vars = json.loads(args[ev_index]) assert extra_vars['secret_key'] == 'my_password' + class TestWorkflowSurveys: def test_update_kwargs_survey_defaults(self, survey_spec_factory): "Assure that the survey default over-rides a JT variable" diff --git a/awx/main/tests/unit/models/test_workflow_unit.py b/awx/main/tests/unit/models/test_workflow_unit.py index 709668ddcf..e48b90385d 100644 --- a/awx/main/tests/unit/models/test_workflow_unit.py +++ b/awx/main/tests/unit/models/test_workflow_unit.py @@ -8,6 +8,7 @@ from awx.main.models.workflow import ( ) import mock + class TestWorkflowJobInheritNodesMixin(): class TestCreateWorkflowJobNodes(): @pytest.fixture @@ -23,7 +24,7 @@ class TestWorkflowJobInheritNodesMixin(): mixin = WorkflowJobInheritNodesMixin() mixin._create_workflow_job_nodes(job_template_nodes) - + for job_template_node in job_template_nodes: workflow_job_node_create.assert_any_call(workflow_job=mixin) @@ -38,7 +39,7 @@ class TestWorkflowJobInheritNodesMixin(): def test__map_workflow_job_nodes(self, job_template_nodes, job_nodes): mixin = WorkflowJobInheritNodesMixin() - + node_ids_map = mixin._map_workflow_job_nodes(job_template_nodes, job_nodes) assert len(node_ids_map) == len(job_template_nodes) @@ -59,7 +60,7 @@ class TestWorkflowJobInheritNodesMixin(): def job_nodes(self, mocker): nodes = [mocker.MagicMock(id=i) for i in range(100, 110)] return nodes - + @pytest.fixture def job_nodes_dict(self, job_nodes): _map = {} @@ -70,7 +71,7 @@ class TestWorkflowJobInheritNodesMixin(): def test__inherit_relationship(self, mocker, job_template_nodes, job_nodes, job_nodes_dict): mixin = WorkflowJobInheritNodesMixin() - + mixin._get_workflow_job_node_by_id = lambda x: job_nodes_dict[x] mixin._get_all_by_type = lambda x,node_type: x.success_nodes @@ -87,6 +88,7 @@ class TestWorkflowJobInheritNodesMixin(): def workflow_job_unit(): return WorkflowJob(name='workflow', status='new') + @pytest.fixture def workflow_job_template_unit(): return WorkflowJobTemplate(name='workflow') @@ -101,6 +103,7 @@ def jt_ask(job_template_factory): jt.ask_tags_on_launch = True return jt + @pytest.fixture def project_unit(): return Project(name='example-proj') @@ -108,6 +111,7 @@ def project_unit(): example_prompts = dict(job_type='check', job_tags='quack', limit='duck', skip_tags='oink') + @pytest.fixture def job_node_no_prompts(workflow_job_unit, jt_ask): return WorkflowJobNode(workflow_job=workflow_job_unit, unified_job_template=jt_ask) @@ -119,10 +123,12 @@ def job_node_with_prompts(job_node_no_prompts): job_node_no_prompts.credential = Credential(name='example-inv', kind='ssh', username='asdf', password='asdf') return job_node_no_prompts + @pytest.fixture def wfjt_node_no_prompts(workflow_job_template_unit, jt_ask): return WorkflowJobTemplateNode(workflow_job_template=workflow_job_template_unit, unified_job_template=jt_ask) + @pytest.fixture def wfjt_node_with_prompts(wfjt_node_no_prompts): wfjt_node_no_prompts.char_prompts = example_prompts @@ -130,8 +136,8 @@ def wfjt_node_with_prompts(wfjt_node_no_prompts): wfjt_node_no_prompts.credential = Credential(name='example-inv', kind='ssh', username='asdf', password='asdf') return wfjt_node_no_prompts -class TestWorkflowJobCreate: +class TestWorkflowJobCreate: def test_create_no_prompts(self, wfjt_node_no_prompts, workflow_job_unit, mocker): mock_create = mocker.MagicMock() with mocker.patch('awx.main.models.WorkflowJobNode.objects.create', mock_create): @@ -153,6 +159,7 @@ class TestWorkflowJobCreate: unified_job_template=wfjt_node_with_prompts.unified_job_template, workflow_job=workflow_job_unit) + @mock.patch('awx.main.models.workflow.WorkflowNodeBase.get_parent_nodes', lambda self: []) class TestWorkflowJobNodeJobKWARGS: """ diff --git a/awx/main/tests/unit/scheduler/conftest.py b/awx/main/tests/unit/scheduler/conftest.py index 8bbba71c46..56473323aa 100644 --- a/awx/main/tests/unit/scheduler/conftest.py +++ b/awx/main/tests/unit/scheduler/conftest.py @@ -20,6 +20,7 @@ from awx.main.scheduler import TaskManager def epoch(): return tz_now() + @pytest.fixture def scheduler_factory(mocker, epoch): mocker.patch('awx.main.models.Instance.objects.total_capacity', return_value=10000) @@ -51,6 +52,7 @@ def scheduler_factory(mocker, epoch): return sched return fn + @pytest.fixture def project_update_factory(epoch): def fn(): @@ -65,24 +67,28 @@ def project_update_factory(epoch): }) return fn + @pytest.fixture def pending_project_update(project_update_factory): project_update = project_update_factory() project_update['status'] = 'pending' return project_update + @pytest.fixture def waiting_project_update(epoch, project_update_factory): project_update = project_update_factory() project_update['status'] = 'waiting' return project_update + @pytest.fixture def running_project_update(epoch, project_update_factory): project_update = project_update_factory() project_update['status'] = 'running' return project_update + @pytest.fixture def successful_project_update(epoch, project_update_factory): project_update = project_update_factory() @@ -90,6 +96,7 @@ def successful_project_update(epoch, project_update_factory): project_update['status'] = 'successful' return project_update + @pytest.fixture def successful_project_update_cache_expired(epoch, project_update_factory): project_update = project_update_factory() @@ -100,6 +107,7 @@ def successful_project_update_cache_expired(epoch, project_update_factory): project_update['project__scm_update_cache_timeout'] = 1 return project_update + @pytest.fixture def failed_project_update(epoch, project_update_factory): project_update = project_update_factory() @@ -107,6 +115,7 @@ def failed_project_update(epoch, project_update_factory): project_update['status'] = 'failed' return project_update + @pytest.fixture def inventory_update_factory(epoch): def fn(): @@ -122,6 +131,7 @@ def inventory_update_factory(epoch): }) return fn + @pytest.fixture def inventory_update_latest_factory(epoch): def fn(): @@ -137,6 +147,7 @@ def inventory_update_latest_factory(epoch): }) return fn + @pytest.fixture def inventory_update_latest(inventory_update_latest_factory): return inventory_update_latest_factory() @@ -148,36 +159,42 @@ def successful_inventory_update_latest(inventory_update_latest_factory): iu['finished'] = iu['created'] + timedelta(seconds=10) return iu + @pytest.fixture def failed_inventory_update_latest(inventory_update_latest_factory): iu = inventory_update_latest_factory() iu['status'] = 'failed' return iu + @pytest.fixture def pending_inventory_update(epoch, inventory_update_factory): inventory_update = inventory_update_factory() inventory_update['status'] = 'pending' return inventory_update + @pytest.fixture def waiting_inventory_update(epoch, inventory_update_factory): inventory_update = inventory_update_factory() inventory_update['status'] = 'waiting' return inventory_update + @pytest.fixture def failed_inventory_update(epoch, inventory_update_factory): inventory_update = inventory_update_factory() inventory_update['status'] = 'failed' return inventory_update + @pytest.fixture def running_inventory_update(epoch, inventory_update_factory): inventory_update = inventory_update_factory() inventory_update['status'] = 'running' return inventory_update + @pytest.fixture def successful_inventory_update(epoch, inventory_update_factory): inventory_update = inventory_update_factory() @@ -208,12 +225,14 @@ def job_factory(epoch): }) return fn + @pytest.fixture def pending_job(job_factory): job = job_factory() job['status'] = 'pending' return job + @pytest.fixture def running_job(job_factory): job = job_factory() @@ -232,6 +251,7 @@ def inventory_source_factory(): }) return fn + @pytest.fixture def inventory_id_sources(inventory_source_factory): return [ @@ -240,4 +260,3 @@ def inventory_id_sources(inventory_source_factory): inventory_source_factory(id=2), ]), ] - diff --git a/awx/main/tests/unit/scheduler/test_dag.py b/awx/main/tests/unit/scheduler/test_dag.py index 3aad7e9b27..e32dffe28f 100644 --- a/awx/main/tests/unit/scheduler/test_dag.py +++ b/awx/main/tests/unit/scheduler/test_dag.py @@ -8,6 +8,7 @@ from awx.main.scheduler.dag_workflow import WorkflowDAG from awx.main.models import Job from awx.main.models.workflow import WorkflowJobNode + @pytest.fixture def dag_root(): dag = SimpleDAG() @@ -25,9 +26,10 @@ def dag_root(): dag.add_edge(data[0], data[1]) dag.add_edge(data[2], data[3]) dag.add_edge(data[4], data[5]) - + return dag + @pytest.fixture def dag_simple_edge_labels(): dag = SimpleDAG() @@ -62,6 +64,7 @@ class TestSimpleDAG(object): nodes = dag.get_dependencies(dag.nodes[0]['node_object'], 'two') ''' + @pytest.fixture def factory_node(): def fn(id, status): @@ -72,6 +75,7 @@ def factory_node(): return wfn return fn + @pytest.fixture def workflow_dag_level_2(factory_node): dag = WorkflowDAG() @@ -91,6 +95,7 @@ def workflow_dag_level_2(factory_node): return (dag, data[3:6], False) + @pytest.fixture def workflow_dag_multiple_roots(factory_node): dag = WorkflowDAG() @@ -111,6 +116,7 @@ def workflow_dag_multiple_roots(factory_node): expected = data[0:3] return (dag, expected, False) + @pytest.fixture def workflow_dag_multiple_edges_labeled(factory_node): dag = WorkflowDAG() @@ -133,6 +139,7 @@ def workflow_dag_multiple_edges_labeled(factory_node): expected = data[5:6] return (dag, expected, False) + @pytest.fixture def workflow_dag_finished(factory_node): dag = WorkflowDAG() @@ -155,6 +162,7 @@ def workflow_dag_finished(factory_node): expected = [] return (dag, expected, True) + @pytest.fixture def workflow_dag_always(factory_node): dag = WorkflowDAG() @@ -171,6 +179,7 @@ def workflow_dag_always(factory_node): expected = data[2:3] return (dag, expected, False) + @pytest.fixture(params=['workflow_dag_multiple_roots', 'workflow_dag_level_2', 'workflow_dag_multiple_edges_labeled', 'workflow_dag_finished', 'workflow_dag_always']) @@ -185,4 +194,3 @@ class TestWorkflowDAG(): def test_is_workflow_done(self, workflow_dag): dag, expected, is_done = workflow_dag assert dag.is_workflow_done() == is_done - diff --git a/awx/main/tests/unit/scheduler/test_dependency_graph.py b/awx/main/tests/unit/scheduler/test_dependency_graph.py index 081f175027..c1c92411ea 100644 --- a/awx/main/tests/unit/scheduler/test_dependency_graph.py +++ b/awx/main/tests/unit/scheduler/test_dependency_graph.py @@ -10,27 +10,31 @@ from django.utils.timezone import now as tz_now from awx.main.scheduler.dependency_graph import DependencyGraph from awx.main.scheduler.partial import ProjectUpdateDict + @pytest.fixture def graph(): return DependencyGraph() + @pytest.fixture def job(): return dict(project_id=1) + @pytest.fixture def unsuccessful_last_project(graph, job): - pu = ProjectUpdateDict(dict(id=1, - project__scm_update_cache_timeout=999999, - project_id=1, - status='failed', - created='3', + pu = ProjectUpdateDict(dict(id=1, + project__scm_update_cache_timeout=999999, + project_id=1, + status='failed', + created='3', finished='3',)) graph.add_latest_project_update(pu) return graph + @pytest.fixture def last_dependent_project(graph): now = tz_now() @@ -39,15 +43,16 @@ def last_dependent_project(graph): 'project_id': 1, 'created': now, } - pu = ProjectUpdateDict(dict(id=1, project_id=1, status='waiting', - project__scm_update_cache_timeout=0, - launch_type='dependency', + pu = ProjectUpdateDict(dict(id=1, project_id=1, status='waiting', + project__scm_update_cache_timeout=0, + launch_type='dependency', created=now - timedelta(seconds=1),)) - + graph.add_latest_project_update(pu) return (graph, job) + @pytest.fixture def timedout_project_update(graph, job): now = tz_now() @@ -56,16 +61,17 @@ def timedout_project_update(graph, job): 'project_id': 1, 'created': now, } - pu = ProjectUpdateDict(dict(id=1, project_id=1, status='successful', - project__scm_update_cache_timeout=10, - launch_type='dependency', - created=now - timedelta(seconds=100), + pu = ProjectUpdateDict(dict(id=1, project_id=1, status='successful', + project__scm_update_cache_timeout=10, + launch_type='dependency', + created=now - timedelta(seconds=100), finished=now - timedelta(seconds=11),)) - + graph.add_latest_project_update(pu) return (graph, job) + @pytest.fixture def not_timedout_project_update(graph, job): now = tz_now() @@ -74,19 +80,18 @@ def not_timedout_project_update(graph, job): 'project_id': 1, 'created': now, } - pu = ProjectUpdateDict(dict(id=1, project_id=1, status='successful', - project__scm_update_cache_timeout=3600, - launch_type='dependency', - created=now - timedelta(seconds=100), + pu = ProjectUpdateDict(dict(id=1, project_id=1, status='successful', + project__scm_update_cache_timeout=3600, + launch_type='dependency', + created=now - timedelta(seconds=100), finished=now - timedelta(seconds=11),)) - + graph.add_latest_project_update(pu) return (graph, job) class TestShouldUpdateRelatedProject(): - def test_no_project_updates(self, graph, job): actual = graph.should_update_related_project(job) @@ -107,7 +112,7 @@ class TestShouldUpdateRelatedProject(): assert False is actual def test_unsuccessful_last_project(self, unsuccessful_last_project, job): - graph = unsuccessful_last_project + graph = unsuccessful_last_project actual = graph.should_update_related_project(job) @@ -115,7 +120,6 @@ class TestShouldUpdateRelatedProject(): def test_last_dependent_project(self, last_dependent_project): (graph, job) = last_dependent_project - + actual = graph.should_update_related_project(job) assert False is actual - diff --git a/awx/main/tests/unit/scheduler/test_scheduler_inventory_update.py b/awx/main/tests/unit/scheduler/test_scheduler_inventory_update.py index 09125df527..5e49eec729 100644 --- a/awx/main/tests/unit/scheduler/test_scheduler_inventory_update.py +++ b/awx/main/tests/unit/scheduler/test_scheduler_inventory_update.py @@ -3,10 +3,12 @@ import pytest from datetime import timedelta + @pytest.fixture def pending_job(job_factory): return job_factory(project__scm_update_on_launch=False, inventory__inventory_sources=['1']) + @pytest.fixture def successful_inventory_update_latest(inventory_update_latest_factory): iu = inventory_update_latest_factory() @@ -15,6 +17,7 @@ def successful_inventory_update_latest(inventory_update_latest_factory): iu['finished'] = iu['created'] + timedelta(seconds=10) return iu + @pytest.fixture def successful_inventory_update_latest_cache_expired(inventory_update_latest_factory): iu = inventory_update_latest_factory() @@ -22,6 +25,7 @@ def successful_inventory_update_latest_cache_expired(inventory_update_latest_fac iu['finished'] = iu['created'] + timedelta(seconds=2) return iu + class TestStartInventoryUpdate(): def test_pending(self, scheduler_factory, pending_inventory_update): scheduler = scheduler_factory(tasks=[pending_inventory_update]) @@ -30,6 +34,7 @@ class TestStartInventoryUpdate(): scheduler.start_task.assert_called_with(pending_inventory_update) + class TestInventoryUpdateBlocked(): def test_running_inventory_update(self, epoch, scheduler_factory, running_inventory_update, pending_inventory_update): running_inventory_update['created'] = epoch - timedelta(seconds=100) @@ -47,8 +52,8 @@ class TestInventoryUpdateBlocked(): scheduler._schedule() -class TestCreateDependentInventoryUpdate(): +class TestCreateDependentInventoryUpdate(): def test(self, scheduler_factory, pending_job, waiting_inventory_update, inventory_id_sources): scheduler = scheduler_factory(tasks=[pending_job], create_inventory_update=waiting_inventory_update, @@ -82,4 +87,3 @@ class TestCreateDependentInventoryUpdate(): scheduler._schedule() scheduler.start_task.assert_called_with(waiting_inventory_update, [pending_job]) - diff --git a/awx/main/tests/unit/scheduler/test_scheduler_job.py b/awx/main/tests/unit/scheduler/test_scheduler_job.py index 5d045efaec..cac315af4b 100644 --- a/awx/main/tests/unit/scheduler/test_scheduler_job.py +++ b/awx/main/tests/unit/scheduler/test_scheduler_job.py @@ -3,6 +3,7 @@ import pytest from datetime import timedelta + class TestJobBlocked(): def test_inventory_update_waiting(self, scheduler_factory, waiting_inventory_update, pending_job): scheduler = scheduler_factory(tasks=[waiting_inventory_update, pending_job]) @@ -36,6 +37,7 @@ class TestJobBlocked(): scheduler.start_task.assert_not_called() assert scheduler.create_project_update.call_count == 0 + class TestJob(): @pytest.fixture def successful_project_update(self, project_update_factory): @@ -54,6 +56,7 @@ class TestJob(): scheduler.start_task.assert_called_with(pending_job) + class TestCapacity(): @pytest.fixture def pending_job_high_impact(self, mocker, job_factory): @@ -81,4 +84,3 @@ class TestCapacity(): calls = [mocker.call(job) for job in pending_jobs_impactful] scheduler.start_task.assert_has_calls(calls) - diff --git a/awx/main/tests/unit/scheduler/test_scheduler_project_update.py b/awx/main/tests/unit/scheduler/test_scheduler_project_update.py index 8122d93c09..e8a5af17c8 100644 --- a/awx/main/tests/unit/scheduler/test_scheduler_project_update.py +++ b/awx/main/tests/unit/scheduler/test_scheduler_project_update.py @@ -3,6 +3,7 @@ # ProjectUpdateDict. We should instead return a ProjectUpdateLatestDict() # For now, this is ok since the fields on deviate that much. + class TestStartProjectUpdate(): def test(self, scheduler_factory, pending_project_update): scheduler = scheduler_factory(tasks=[pending_project_update]) @@ -26,7 +27,6 @@ class TestStartProjectUpdate(): class TestCreateDependentProjectUpdate(): - def test(self, scheduler_factory, pending_job, waiting_project_update): scheduler = scheduler_factory(tasks=[pending_job], create_project_update=waiting_project_update) @@ -58,6 +58,7 @@ class TestCreateDependentProjectUpdate(): scheduler.start_task.assert_called_with(waiting_project_update, [pending_job]) + class TestProjectUpdateBlocked(): def test_projct_update_running(self, scheduler_factory, running_project_update, pending_project_update): scheduler = scheduler_factory(tasks=[running_project_update, pending_project_update]) @@ -72,4 +73,3 @@ class TestProjectUpdateBlocked(): scheduler._schedule() scheduler.start_task.assert_not_called() - diff --git a/awx/main/tests/unit/settings/test_defaults.py b/awx/main/tests/unit/settings/test_defaults.py index d1c80fce3e..894289002d 100644 --- a/awx/main/tests/unit/settings/test_defaults.py +++ b/awx/main/tests/unit/settings/test_defaults.py @@ -3,6 +3,7 @@ import pytest from django.conf import settings from datetime import timedelta + @pytest.mark.parametrize("job_name,function_path", [ ('admin_checks', 'awx.main.tasks.run_administrative_checks'), ('tower_scheduler', 'awx.main.tasks.tower_periodic_scheduler'), diff --git a/awx/main/tests/unit/test_access.py b/awx/main/tests/unit/test_access.py index 2bda14eea2..5dc3a5e57b 100644 --- a/awx/main/tests/unit/test_access.py +++ b/awx/main/tests/unit/test_access.py @@ -21,8 +21,8 @@ from awx.main.models import Credential, Inventory, Project, Role, Organization, def user_unit(): return User(username='rando', password='raginrando', email='rando@redhat.com') -class TestRelatedFieldAccess: +class TestRelatedFieldAccess: @pytest.fixture def resource_good(self, mocker): good_role = mocker.MagicMock(__contains__=lambda self, user: True) @@ -107,6 +107,7 @@ class TestRelatedFieldAccess: assert access.check_related( 'related', mocker.MagicMock, data, obj=resource, mandatory=True) + @pytest.fixture def job_template_with_ids(job_template_factory): # Create non-persisted objects with IDs to send to job_template_factory @@ -122,6 +123,7 @@ def job_template_with_ids(job_template_factory): persisted=False) return jt_objects.job_template + def test_superuser(mocker): user = mocker.MagicMock(spec=User, id=1, is_superuser=True) access = BaseAccess(user) @@ -129,6 +131,7 @@ def test_superuser(mocker): can_add = check_superuser(BaseAccess.can_add) assert can_add(access, None) is True + def test_not_superuser(mocker): user = mocker.MagicMock(spec=User, id=1, is_superuser=False) access = BaseAccess(user) @@ -136,6 +139,7 @@ def test_not_superuser(mocker): can_add = check_superuser(BaseAccess.can_add) assert can_add(access, None) is False + def test_jt_existing_values_are_nonsensitive(job_template_with_ids, user_unit): """Assure that permission checks are not required if submitted data is identical to what the job template already has.""" @@ -145,6 +149,7 @@ def test_jt_existing_values_are_nonsensitive(job_template_with_ids, user_unit): assert access.changes_are_non_sensitive(job_template_with_ids, data) + def test_change_jt_sensitive_data(job_template_with_ids, mocker, user_unit): """Assure that can_add is called with all ForeignKeys.""" @@ -167,6 +172,7 @@ def test_change_jt_sensitive_data(job_template_with_ids, mocker, user_unit): 'network_credential': job_template_with_ids.network_credential.id }) + def test_jt_add_scan_job_check(job_template_with_ids, user_unit): "Assure that permissions to add scan jobs work correctly" @@ -196,23 +202,28 @@ def test_jt_add_scan_job_check(job_template_with_ids, user_unit): 'job_type': 'scan' }) + def mock_raise_license_forbids(self, add_host=False, feature=None, check_expiration=True): raise LicenseForbids("Feature not enabled") + def mock_raise_none(self, add_host=False, feature=None, check_expiration=True): return None - + + def test_jt_can_start_ha(job_template_with_ids): with mock.patch.object(Instance.objects, 'active_count', return_value=2): with mock.patch('awx.main.access.BaseAccess.check_license', new=mock_raise_license_forbids): with pytest.raises(LicenseForbids): JobTemplateAccess(user_unit).can_start(job_template_with_ids) + def test_jt_can_add_bad_data(user_unit): "Assure that no server errors are returned if we call JT can_add with bad data" access = JobTemplateAccess(user_unit) assert not access.can_add({'asdf': 'asdf'}) + class TestWorkflowAccessMethods: @pytest.fixture def workflow(self, workflow_job_template_factory): @@ -259,6 +270,7 @@ def test_user_capabilities_method(): 'copy': 'foobar' } + def test_system_job_template_can_start(mocker): user = mocker.MagicMock(spec=User, id=1, is_system_auditor=True, is_superuser=False) assert user.is_system_auditor diff --git a/awx/main/tests/unit/test_ha.py b/awx/main/tests/unit/test_ha.py index 07249a67fb..ebd86cd3a2 100644 --- a/awx/main/tests/unit/test_ha.py +++ b/awx/main/tests/unit/test_ha.py @@ -6,10 +6,12 @@ import mock # AWX from awx.main.ha import is_ha_environment + @mock.patch('awx.main.models.Instance.objects.count', lambda: 2) def test_multiple_instances(): assert is_ha_environment() + @mock.patch('awx.main.models.Instance.objects.count', lambda: 1) def test_db_localhost(): assert is_ha_environment() is False diff --git a/awx/main/tests/unit/test_python_requirements.py b/awx/main/tests/unit/test_python_requirements.py index 8b7382dd74..7c5f671e78 100644 --- a/awx/main/tests/unit/test_python_requirements.py +++ b/awx/main/tests/unit/test_python_requirements.py @@ -4,8 +4,8 @@ from pip.operations import freeze from django.conf import settings -def test_env_matches_requirements_txt(): +def test_env_matches_requirements_txt(): def check_is_in(src, dests): if src not in dests: print("%s not in" % src) @@ -33,7 +33,7 @@ def test_env_matches_requirements_txt(): continue line.rstrip() reqs_expected.append(line.rstrip().split('==')) - + not_found = [] for r in reqs_expected: res = check_is_in(r, reqs_actual) diff --git a/awx/main/tests/unit/test_redact.py b/awx/main/tests/unit/test_redact.py index 3535869ee1..931ef72ebc 100644 --- a/awx/main/tests/unit/test_redact.py +++ b/awx/main/tests/unit/test_redact.py @@ -78,7 +78,6 @@ TEST_CLEARTEXT.append({ }) - # should redact sensitive usernames and passwords def test_uri_scm_simple_redacted(): for uri in TEST_URIS: @@ -88,12 +87,14 @@ def test_uri_scm_simple_redacted(): if uri.password: assert uri.username not in redacted_str + # should replace secret data with safe string, UriCleaner.REPLACE_STR def test_uri_scm_simple_replaced(): for uri in TEST_URIS: redacted_str = UriCleaner.remove_sensitive(str(uri)) assert redacted_str.count(UriCleaner.REPLACE_STR) == uri.get_secret_count() + # should redact multiple uris in text def test_uri_scm_multiple(): cleartext = '' @@ -108,6 +109,7 @@ def test_uri_scm_multiple(): if uri.password: assert uri.username not in redacted_str + # should replace multiple secret data with safe string def test_uri_scm_multiple_replaced(): cleartext = '' @@ -123,6 +125,7 @@ def test_uri_scm_multiple_replaced(): redacted_str = UriCleaner.remove_sensitive(cleartext) assert redacted_str.count(UriCleaner.REPLACE_STR) == find_count + # should redact and replace multiple secret data within a complex cleartext blob def test_uri_scm_cleartext_redact_and_replace(): for test_data in TEST_CLEARTEXT: diff --git a/awx/main/tests/unit/test_settings.py b/awx/main/tests/unit/test_settings.py index 2419431c78..d339262808 100644 --- a/awx/main/tests/unit/test_settings.py +++ b/awx/main/tests/unit/test_settings.py @@ -1,5 +1,6 @@ from split_settings.tools import include + def test_postprocess_auth_basic_enabled(): locals().update({'__file__': __file__}) diff --git a/awx/main/tests/unit/test_signals.py b/awx/main/tests/unit/test_signals.py index c3830ee525..cc14824a67 100644 --- a/awx/main/tests/unit/test_signals.py +++ b/awx/main/tests/unit/test_signals.py @@ -1,5 +1,6 @@ from awx.main import signals + class TestCleanupDetachedLabels: def test_cleanup_detached_labels_on_deleted_parent(self, mocker): mock_labels = [mocker.MagicMock(), mocker.MagicMock()] @@ -10,7 +11,7 @@ class TestCleanupDetachedLabels: mock_labels[1].is_candidate_for_detach.return_value = False signals.cleanup_detached_labels_on_deleted_parent(None, mock_instance) - + mock_labels[0].is_candidate_for_detach.assert_called_with() mock_labels[1].is_candidate_for_detach.assert_called_with() mock_labels[0].delete.assert_called_with() diff --git a/awx/main/tests/unit/test_tasks.py b/awx/main/tests/unit/test_tasks.py index 3c0f766553..b83772bb59 100644 --- a/awx/main/tests/unit/test_tasks.py +++ b/awx/main/tests/unit/test_tasks.py @@ -12,22 +12,26 @@ from awx.main.tasks import ( ) from awx.main.task_engine import TaskEnhancer + @contextmanager def apply_patches(_patches): [p.start() for p in _patches] yield [p.stop() for p in _patches] + def test_send_notifications_not_list(): with pytest.raises(TypeError): send_notifications(None) + def test_send_notifications_job_id(mocker): with mocker.patch('awx.main.models.UnifiedJob.objects.get'): send_notifications([], job_id=1) assert UnifiedJob.objects.get.called assert UnifiedJob.objects.get.called_with(id=1) + def test_send_notifications_list(mocker): patches = list() @@ -46,6 +50,7 @@ def test_send_notifications_list(mocker): assert mock_job.notifications.add.called assert mock_job.notifications.add.called_with(mock_notification) + @pytest.mark.parametrize("current_instances,call_count", [(91, 2), (89,1)]) def test_run_admin_checks_usage(mocker, current_instances, call_count): patches = list() diff --git a/awx/main/tests/unit/test_unified_jobs.py b/awx/main/tests/unit/test_unified_jobs.py index edd6978b47..592c4783b1 100644 --- a/awx/main/tests/unit/test_unified_jobs.py +++ b/awx/main/tests/unit/test_unified_jobs.py @@ -23,6 +23,7 @@ def test_result_stdout_raw_handle_file__found(exists, open): assert result == 'my_file_handler' + # stdout file missing, job finished @mock.patch('os.path.exists', return_value=False) def test_result_stdout_raw_handle__missing(exists): @@ -35,6 +36,7 @@ def test_result_stdout_raw_handle__missing(exists): assert isinstance(result, StringIO) assert result.read() == 'stdout capture is missing' + # stdout file missing, job not finished @mock.patch('os.path.exists', return_value=False) def test_result_stdout_raw_handle__pending(exists): diff --git a/awx/main/tests/unit/test_validators.py b/awx/main/tests/unit/test_validators.py index b62395424e..ea47785ed0 100644 --- a/awx/main/tests/unit/test_validators.py +++ b/awx/main/tests/unit/test_validators.py @@ -15,6 +15,7 @@ from awx.main.tests.data.ssh import ( import pytest + def test_valid_rsa_key(): valid_key = TEST_SSH_KEY_DATA pem_objects = validate_private_key(valid_key) @@ -26,6 +27,7 @@ def test_valid_rsa_key(): assert pem_objects[0]['key_type'] == 'rsa' assert not pem_objects[0]['key_enc'] + def test_valid_locked_rsa_key(): valid_key = TEST_SSH_KEY_DATA_LOCKED pem_objects = validate_private_key(valid_key) @@ -37,6 +39,7 @@ def test_valid_locked_rsa_key(): assert pem_objects[0]['key_type'] == 'rsa' assert pem_objects[0]['key_enc'] + def test_invalid_rsa_key(): invalid_key = TEST_SSH_KEY_DATA.replace('-----END', '----END') with pytest.raises(ValidationError): @@ -46,6 +49,7 @@ def test_invalid_rsa_key(): with pytest.raises(ValidationError): validate_ssh_private_key(invalid_key) + def test_valid_openssh_key(): valid_key = TEST_OPENSSH_KEY_DATA pem_objects = validate_private_key(valid_key) @@ -57,6 +61,7 @@ def test_valid_openssh_key(): assert pem_objects[0]['key_type'] == 'ed25519' assert not pem_objects[0]['key_enc'] + def test_valid_locked_openssh_key(): valid_key = TEST_OPENSSH_KEY_DATA_LOCKED pem_objects = validate_private_key(valid_key) @@ -67,7 +72,8 @@ def test_valid_locked_openssh_key(): pem_objects = validate_ssh_private_key(valid_key) assert pem_objects[0]['key_type'] == 'ed25519' assert pem_objects[0]['key_enc'] - + + def test_valid_rsa1_key(): valid_key = TEST_SSH_RSA1_KEY_DATA pem_objects = validate_ssh_private_key(valid_key) @@ -79,6 +85,7 @@ def test_valid_rsa1_key(): assert pem_objects[0]['key_type'] == 'rsa1' assert not pem_objects[0]['key_enc'] + def test_cert_with_key(): cert_with_key = TEST_SSH_CERT_KEY with pytest.raises(ValidationError): diff --git a/awx/main/utils.py b/awx/main/utils.py index d03ee3b0d4..bacde40198 100644 --- a/awx/main/utils.py +++ b/awx/main/utils.py @@ -73,6 +73,7 @@ def get_object_or_403(klass, *args, **kwargs): except queryset.model.MultipleObjectsReturned as e: raise PermissionDenied(*e.args) + def to_python_boolean(value, allow_none=False): value = unicode(value) if value.lower() in ('true', '1', 't'): @@ -84,6 +85,7 @@ def to_python_boolean(value, allow_none=False): else: raise ValueError(_(u'Unable to convert "%s" to boolean') % unicode(value)) + def camelcase_to_underscore(s): ''' Convert CamelCase names to lowercase_with_underscore. @@ -131,6 +133,7 @@ def get_ansible_version(): except: return 'unknown' + @memoize() def get_ssh_version(): ''' @@ -144,6 +147,7 @@ def get_ssh_version(): except: return 'unknown' + def get_awx_version(): ''' Return Ansible Tower version as reported by setuptools. @@ -167,9 +171,11 @@ def get_encryption_key_for_pk(pk, field_name): h.update(field_name) return h.digest()[:16] + def get_encryption_key(instance, field_name): return get_encryption_key_for_pk(instance.pk, field_name) + def encrypt_field(instance, field_name, ask=False, subfield=None): ''' Return content of the given instance and field name encrypted. @@ -188,6 +194,7 @@ def encrypt_field(instance, field_name, ask=False, subfield=None): b64data = base64.b64encode(encrypted) return '$encrypted$%s$%s' % ('AES', b64data) + def decrypt_value(encryption_key, value): algo, b64data = value[len('$encrypted$'):].split('$', 1) if algo != 'AES': @@ -197,6 +204,7 @@ def decrypt_value(encryption_key, value): value = cipher.decrypt(encrypted) return value.rstrip('\x00') + def decrypt_field(instance, field_name, subfield=None): ''' Return content of the given instance and field name decrypted. @@ -210,10 +218,12 @@ def decrypt_field(instance, field_name, subfield=None): return decrypt_value(key, value) + def decrypt_field_value(pk, field_name, value): key = get_encryption_key_for_pk(pk, field_name) return decrypt_value(key, value) + def update_scm_url(scm_type, url, username=True, password=True, check_special_cases=True, scp_format=False): ''' @@ -321,7 +331,6 @@ def update_scm_url(scm_type, url, username=True, password=True, return new_url - def get_allowed_fields(obj, serializer_mapping): from django.contrib.auth.models import User @@ -337,6 +346,7 @@ def get_allowed_fields(obj, serializer_mapping): return allowed_fields + def model_instance_diff(old, new, serializer_mapping=None): """ Calculate the differences between two model instances. One of the instances may be None (i.e., a newly @@ -505,6 +515,7 @@ def parse_yaml_or_json(vars_str): vars_dict = {} return vars_dict + @memoize() def get_system_task_capacity(): ''' @@ -549,6 +560,7 @@ def ignore_inventory_group_removal(): finally: _inventory_updates.is_removing = previous_value + @memoize() def check_proot_installed(): ''' @@ -564,6 +576,7 @@ def check_proot_installed(): except (OSError, ValueError): return False + def build_proot_temp_dir(): ''' Create a temporary directory for proot to use. @@ -573,6 +586,7 @@ def build_proot_temp_dir(): os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) return path + def wrap_args_with_proot(args, cwd, **kwargs): ''' Wrap existing command line with proot to restrict access to: @@ -616,6 +630,7 @@ def wrap_args_with_proot(args, cwd, **kwargs): new_args.extend(args) return new_args + def get_pk_from_dict(_dict, key): ''' Helper for obtaining a pk from user data dict or None if not present. @@ -625,6 +640,7 @@ def get_pk_from_dict(_dict, key): except (TypeError, KeyError, ValueError): return None + def build_url(*args, **kwargs): get = kwargs.pop('get', {}) url = reverse(*args, **kwargs) @@ -632,12 +648,14 @@ def build_url(*args, **kwargs): url += '?' + urllib.urlencode(get) return url + def timestamp_apiformat(timestamp): timestamp = timestamp.isoformat() if timestamp.endswith('+00:00'): timestamp = timestamp[:-6] + 'Z' return timestamp + # damn you python 2.6 def timedelta_total_seconds(timedelta): return ( @@ -648,6 +666,7 @@ def timedelta_total_seconds(timedelta): class NoDefaultProvided(object): pass + def getattrd(obj, name, default=NoDefaultProvided): """ Same as getattr(), but allows dot notation lookup @@ -664,10 +683,13 @@ def getattrd(obj, name, default=NoDefaultProvided): current_apps = apps + + def set_current_apps(apps): global current_apps current_apps = apps + def get_current_apps(): global current_apps return current_apps diff --git a/awx/main/validators.py b/awx/main/validators.py index afe465c76e..1c92d9a645 100644 --- a/awx/main/validators.py +++ b/awx/main/validators.py @@ -172,6 +172,7 @@ def validate_ssh_private_key(data): """ return validate_pem(data, min_keys=1) + def vars_validate_or_raise(vars_str): """ Validate that fields like extra_vars or variables on resources like diff --git a/awx/plugins/fact_caching/tower.py b/awx/plugins/fact_caching/tower.py index 3e89ccef36..a956a5f50b 100755 --- a/awx/plugins/fact_caching/tower.py +++ b/awx/plugins/fact_caching/tower.py @@ -44,6 +44,7 @@ except ImportError: print("pyzmq is required") sys.exit(1) + class CacheModule(BaseCacheModule): def __init__(self, *args, **kwargs): diff --git a/awx/plugins/library/scan_files.py b/awx/plugins/library/scan_files.py index 64c52c65bd..3ab092947d 100644 --- a/awx/plugins/library/scan_files.py +++ b/awx/plugins/library/scan_files.py @@ -99,6 +99,7 @@ EXAMPLES = ''' # }, ''' + def main(): module = AnsibleModule( argument_spec = dict(paths=dict(required=True, type='list'), diff --git a/awx/plugins/library/scan_packages.py b/awx/plugins/library/scan_packages.py index 3b94fb75ab..13b28542f6 100755 --- a/awx/plugins/library/scan_packages.py +++ b/awx/plugins/library/scan_packages.py @@ -39,6 +39,7 @@ EXAMPLES = ''' # }, ... ] } } ''' + def rpm_package_list(): import rpm trans_set = rpm.TransactionSet() @@ -53,6 +54,7 @@ def rpm_package_list(): installed_packages.append(package_details) return installed_packages + def deb_package_list(): import apt apt_cache = apt.Cache() @@ -67,6 +69,7 @@ def deb_package_list(): installed_packages.append(package_details) return installed_packages + def main(): module = AnsibleModule( argument_spec = dict(os_family=dict(required=True)) diff --git a/awx/plugins/library/scan_services.py b/awx/plugins/library/scan_services.py index 778cdcc058..11a8edc745 100644 --- a/awx/plugins/library/scan_services.py +++ b/awx/plugins/library/scan_services.py @@ -43,12 +43,14 @@ EXAMPLES = ''' # }, .... ] } } ''' + class BaseService(object): def __init__(self, module): self.module = module self.incomplete_warning = False + class ServiceScanService(BaseService): def gather_services(self): @@ -135,6 +137,7 @@ class ServiceScanService(BaseService): services.append(service_data) return services + class SystemctlScanService(BaseService): def systemd_enabled(self): @@ -170,6 +173,7 @@ class SystemctlScanService(BaseService): "source": "systemd"}) return services + def main(): module = AnsibleModule(argument_spec = dict()) service_modules = (ServiceScanService, SystemctlScanService) diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 4dc7791324..69cd7f232b 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -24,6 +24,7 @@ for setting in dir(global_settings): # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(__file__)) + def is_testing(argv=None): import sys '''Return True if running django or py.test unit tests.''' @@ -34,6 +35,7 @@ def is_testing(argv=None): return True return False + def IS_TESTING(argv=None): return is_testing(argv) diff --git a/awx/sso/__init__.py b/awx/sso/__init__.py index c7ec0d9321..aa65d65a11 100644 --- a/awx/sso/__init__.py +++ b/awx/sso/__init__.py @@ -11,6 +11,7 @@ xmlsec_initialized = False import dm.xmlsec.binding # noqa original_xmlsec_initialize = dm.xmlsec.binding.initialize + def xmlsec_initialize(*args, **kwargs): global xmlsec_init_lock, xmlsec_initialized, original_xmlsec_initialize with xmlsec_init_lock: diff --git a/awx/sso/conf.py b/awx/sso/conf.py index 533067a91c..48b58575c0 100644 --- a/awx/sso/conf.py +++ b/awx/sso/conf.py @@ -797,6 +797,7 @@ register( # SAML AUTHENTICATION SETTINGS ############################################################################### + def get_saml_metadata_url(): return urlparse.urljoin(settings.TOWER_URL_BASE, reverse('sso:saml_metadata')) diff --git a/awx/sso/views.py b/awx/sso/views.py index 512caa0e75..a25aabf511 100644 --- a/awx/sso/views.py +++ b/awx/sso/views.py @@ -22,6 +22,7 @@ from awx.api.serializers import UserSerializer logger = logging.getLogger('awx.sso.views') + class BaseRedirectView(RedirectView): def get_redirect_url(self, *args, **kwargs): diff --git a/setup.py b/setup.py index 78dde7b8ec..d7b2e925b0 100755 --- a/setup.py +++ b/setup.py @@ -41,6 +41,7 @@ else: ##################################################################### # Helper Functions + def explode_glob_path(path): """Take a glob and hand back the full recursive expansion, ignoring links. diff --git a/tools/data_generators/rbac_dummy_data_generator.py b/tools/data_generators/rbac_dummy_data_generator.py index 06f4880750..24b1323aea 100755 --- a/tools/data_generators/rbac_dummy_data_generator.py +++ b/tools/data_generators/rbac_dummy_data_generator.py @@ -82,6 +82,7 @@ inventory_hosts = [] jobs = [] #job_events = [] + def spread(n, m): ret = [] # At least one in each slot, split up the rest exponentially so the first