# Python import pytest from unittest import mock import tempfile import shutil import urllib.parse from unittest.mock import PropertyMock # Django from django.urls import resolve from django.http import Http404 from django.core.handlers.exception import response_for_exception from django.contrib.auth.models import User from django.core.serializers.json import DjangoJSONEncoder from django.db.backends.sqlite3.base import SQLiteCursorWrapper # AWX from awx.main.fields import JSONBField from awx.main.models.projects import Project from awx.main.models.ha import Instance from rest_framework.test import ( APIRequestFactory, force_authenticate, ) from awx.main.models.credential import CredentialType, Credential from awx.main.models.jobs import JobTemplate, SystemJobTemplate from awx.main.models.inventory import ( Group, Inventory, InventoryUpdate, InventorySource, CustomInventoryScript ) from awx.main.models.organization import ( Organization, Team, ) from awx.main.models.rbac import Role from awx.main.models.notifications import ( NotificationTemplate, Notification ) from awx.main.models.events import ( JobEvent, AdHocCommandEvent, ProjectUpdateEvent, InventoryUpdateEvent, SystemJobEvent, ) from awx.main.models.workflow import WorkflowJobTemplate from awx.main.models.ad_hoc_commands import AdHocCommand from awx.main.models.oauth import OAuth2Application as Application __SWAGGER_REQUESTS__ = {} @pytest.fixture(scope="session") def swagger_autogen(requests=__SWAGGER_REQUESTS__): return requests @pytest.fixture def user(): def u(name, is_superuser=False): try: user = User.objects.get(username=name) except User.DoesNotExist: user = User(username=name, is_superuser=is_superuser) user.set_password(name) user.save() return user return u @pytest.fixture def check_jobtemplate(project, inventory, credential): jt = JobTemplate.objects.create( job_type='check', project=project, inventory=inventory, name='check-job-template' ) jt.credentials.add(credential) return jt @pytest.fixture def deploy_jobtemplate(project, inventory, credential): jt = JobTemplate.objects.create( job_type='run', project=project, inventory=inventory, name='deploy-job-template' ) jt.credentials.add(credential) return jt @pytest.fixture def team(organization): return organization.teams.create(name='test-team') @pytest.fixture def team_member(user, team): ret = user('team-member', False) team.member_role.members.add(ret) return ret @pytest.fixture(scope="session", autouse=True) def project_playbooks(): ''' Return playbook_files as playbooks for manual projects when testing. ''' class PlaybooksMock(mock.PropertyMock): def __get__(self, obj, obj_type): return obj.playbook_files mocked = mock.patch.object(Project, 'playbooks', new_callable=PlaybooksMock) mocked.start() @pytest.fixture def run_computed_fields_right_away(request): def run_me(inventory_id): i = Inventory.objects.get(id=inventory_id) i.update_computed_fields() mocked = mock.patch( 'awx.main.signals.update_inventory_computed_fields.delay', new=run_me ) mocked.start() request.addfinalizer(mocked.stop) @pytest.fixture @mock.patch.object(Project, "update", lambda self, **kwargs: None) def project(instance, organization): prj = Project.objects.create(name="test-proj", description="test-proj-desc", organization=organization, playbook_files=['helloworld.yml', 'alt-helloworld.yml'], scm_revision='1234567890123456789012345678901234567890', scm_url='localhost', scm_type='git' ) return prj @pytest.fixture @mock.patch.object(Project, "update", lambda self, **kwargs: None) def manual_project(instance, organization): prj = Project.objects.create(name="test-manual-proj", description="manual-proj-desc", organization=organization, playbook_files=['helloworld.yml', 'alt-helloworld.yml'], local_path='_92__test_proj' ) return prj @pytest.fixture def project_factory(organization): def factory(name): try: prj = Project.objects.get(name=name) except Project.DoesNotExist: prj = Project.objects.create(name=name, description="description for " + name, organization=organization ) return prj return factory @pytest.fixture def job_factory(jt_linked, admin): def factory(job_template=jt_linked, initial_state='new', created_by=admin): return job_template.create_unified_job(_eager_fields={ 'status': initial_state, 'created_by': created_by}) return factory @pytest.fixture def team_factory(organization): def factory(name): try: t = Team.objects.get(name=name) except Team.DoesNotExist: t = Team.objects.create(name=name, description="description for " + name, organization=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 insights_project(): return Project.objects.create(name="test-insights-project", scm_type="insights") @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 credentialtype_kube(): kube = CredentialType.defaults['kubernetes_bearer_token']() kube.save() return kube @pytest.fixture def credentialtype_ssh(): ssh = CredentialType.defaults['ssh']() ssh.save() return ssh @pytest.fixture def credentialtype_aws(): aws = CredentialType.defaults['aws']() aws.save() return aws @pytest.fixture def credentialtype_net(): net = CredentialType.defaults['net']() net.save() return net @pytest.fixture def credentialtype_vault(): vault_type = CredentialType.defaults['vault']() vault_type.save() return vault_type @pytest.fixture def credentialtype_scm(): scm_type = CredentialType.defaults['scm']() scm_type.save() return scm_type @pytest.fixture def credentialtype_insights(): insights_type = CredentialType.defaults['insights']() insights_type.save() return insights_type @pytest.fixture def credentialtype_external(): external_type_inputs = { 'fields': [{ 'id': 'url', 'label': 'Server URL', 'type': 'string', 'help_text': 'The server url.' }, { 'id': 'token', 'label': 'Token', 'type': 'string', 'secret': True, 'help_text': 'An access token for the server.' }], 'metadata': [{ 'id': 'key', 'label': 'Key', 'type': 'string' }, { 'id': 'version', 'label': 'Version', 'type': 'string' }], 'required': ['url', 'token', 'key'], } class MockPlugin(object): def backend(self, **kwargs): return 'secret' with mock.patch('awx.main.models.credential.CredentialType.plugin', new_callable=PropertyMock) as mock_plugin: mock_plugin.return_value = MockPlugin() external_type = CredentialType( kind='external', managed_by_tower=True, name='External Service', inputs=external_type_inputs ) external_type.save() yield external_type @pytest.fixture def credential(credentialtype_aws): return Credential.objects.create(credential_type=credentialtype_aws, name='test-cred', inputs={'username': 'something', 'password': 'secret'}) @pytest.fixture def net_credential(credentialtype_net): return Credential.objects.create(credential_type=credentialtype_net, name='test-cred', inputs={'username': 'something', 'password': 'secret'}) @pytest.fixture def vault_credential(credentialtype_vault): return Credential.objects.create(credential_type=credentialtype_vault, name='test-cred', inputs={'vault_password': 'secret'}) @pytest.fixture def machine_credential(credentialtype_ssh): return Credential.objects.create(credential_type=credentialtype_ssh, name='machine-cred', inputs={'username': 'test_user', 'password': 'pas4word'}) @pytest.fixture def scm_credential(credentialtype_scm): return Credential.objects.create(credential_type=credentialtype_scm, name='scm-cred', inputs={'username': 'optimus', 'password': 'prime'}) @pytest.fixture def insights_credential(credentialtype_insights): return Credential.objects.create(credential_type=credentialtype_insights, name='insights-cred', inputs={'username': 'morocco_mole', 'password': 'secret_squirrel'}) @pytest.fixture def org_credential(organization, credentialtype_aws): return Credential.objects.create(credential_type=credentialtype_aws, name='test-cred', inputs={'username': 'something', 'password': 'secret'}, organization=organization) @pytest.fixture def external_credential(credentialtype_external): return Credential.objects.create(credential_type=credentialtype_external, name='external-cred', inputs={'url': 'http://testhost.com', 'token': 'secret1'}) @pytest.fixture def other_external_credential(credentialtype_external): return Credential.objects.create(credential_type=credentialtype_external, name='other-external-cred', inputs={'url': 'http://testhost.com', 'token': 'secret2'}) @pytest.fixture def kube_credential(credentialtype_kube): return Credential.objects.create(credential_type=credentialtype_kube, name='kube-cred', inputs={'host': 'my.cluster', 'bearer_token': 'my-token', 'verify_ssl': False}) @pytest.fixture def inventory(organization): return organization.inventories.create(name="test-inv") @pytest.fixture def insights_inventory(inventory): inventory.scm_type = 'insights' inventory.save() return inventory @pytest.fixture def scm_inventory_source(inventory, project): inv_src = InventorySource( name="test-scm-inv", source_project=project, source='scm', source_path='inventory_file', update_on_project_update=True, inventory=inventory, scm_last_revision=project.scm_revision) with mock.patch('awx.main.models.unified_jobs.UnifiedJobTemplate.update'): inv_src.save() return inv_src @pytest.fixture def inventory_factory(organization): def factory(name, org=organization): try: inv = Inventory.objects.get(name=name, organization=org) except Inventory.DoesNotExist: inv = Inventory.objects.create(name=name, organization=org) 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', organization=organization, notification_type="webhook", notification_configuration=dict(url="http://localhost", username="", password="", headers={"Test": "Header",})) @pytest.fixture def notification_template_with_encrypt(organization): return NotificationTemplate.objects.create(name='test-notification_template_with_encrypt', organization=organization, notification_type="slack", notification_configuration=dict(channels=["Foo", "Bar"], token="token")) @pytest.fixture def notification(notification_template): return Notification.objects.create(notification_template=notification_template, status='successful', notifications_sent=1, notification_type='email', 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 system_auditor(user): u = user('an-auditor', False) Role.singleton('system_auditor').members.add(u) return u @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) organization.admin_role.members.add(ret) organization.member_role.members.add(ret) return ret @pytest.fixture def org_auditor(user, organization): ret = user('org-auditor', False) organization.auditor_role.members.add(ret) 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): orgs = [] for i in range(0, organization_count): o = Organization.objects.create(name="test-org-%d" % i, description="test-org-desc") orgs.append(o) return orgs return rf @pytest.fixture def group_factory(inventory): def g(name): try: return Group.objects.get(name=name, inventory=inventory) except Exception: return Group.objects.create(inventory=inventory, name=name) return g @pytest.fixture def hosts(group_factory): group1 = group_factory('group-1') def rf(host_count=1): hosts = [] for i in range(0, host_count): name = '%s-host-%s' % (group1.name, i) (host, created) = group1.inventory.hosts.get_or_create(name=name) if created: group1.hosts.add(host) hosts.append(host) return hosts return rf @pytest.fixture def group(inventory): return inventory.groups.create(name='single-group') @pytest.fixture def inventory_source(inventory): # by making it ec2, the credential is not required return InventorySource.objects.create(name='single-inv-src', inventory=inventory, source='ec2') @pytest.fixture def inventory_source_factory(inventory_factory): def invsrc(name, source=None, inventory=None): if inventory is None: inventory = inventory_factory("inv-is-%s" % name) if source is None: source = 'file' try: return inventory.inventory_sources.get(name=name) except Exception: return inventory.inventory_sources.create(name=name, source=source) return invsrc @pytest.fixture def inventory_update(inventory_source): return InventoryUpdate.objects.create( inventory_source=inventory_source, source=inventory_source.source ) @pytest.fixture def inventory_script(organization): return CustomInventoryScript.objects.create(name='test inv script', organization=organization, script='#!/usr/bin/python') @pytest.fixture def host(group, inventory): return group.hosts.create(name='single-host', inventory=inventory) @pytest.fixture def permissions(): return { 'admin':{'create':True, 'read':True, 'write':True, 'update':True, 'delete':True, 'scm_update':True, 'execute':True, 'use':True,}, 'auditor':{'read':True, 'create':False, 'write':False, 'update':False, 'delete':False, 'scm_update':False, 'execute':False, 'use':False,}, 'usage':{'read':False, 'create':False, 'write':False, 'update':False, 'delete':False, 'scm_update':False, 'execute':False, 'use':True,}, } def _request(verb): def rf(url, data_or_user=None, user=None, middleware=None, expect=None, **kwargs): if type(data_or_user) is User and user is None: user = data_or_user elif 'data' not in kwargs: kwargs['data'] = data_or_user if 'format' not in kwargs and 'content_type' not in kwargs: kwargs['format'] = 'json' request = getattr(APIRequestFactory(), verb)(url, **kwargs) request_error = None try: view, view_args, view_kwargs = resolve(urllib.parse.urlparse(url)[2]) except Http404 as e: request_error = e if isinstance(kwargs.get('cookies', None), dict): for key, value in kwargs['cookies'].items(): request.COOKIES[key] = value if middleware: middleware.process_request(request) if user: force_authenticate(request, user=user) if not request_error: response = view(request, *view_args, **view_kwargs) else: response = response_for_exception(request, request_error) if middleware: middleware.process_response(request, response) if expect: if response.status_code != expect: if getattr(response, 'data', None): try: data_copy = response.data.copy() # Make translated strings printable for key, value in response.data.items(): if isinstance(value, list): response.data[key] = [] for item in value: response.data[key].append(str(item)) else: response.data[key] = str(value) except Exception: response.data = data_copy assert response.status_code == expect, 'Response data: {}'.format( getattr(response, 'data', None) ) if hasattr(response, 'render'): response.render() __SWAGGER_REQUESTS__.setdefault(request.path, {})[ (request.method.lower(), response.status_code) ] = (response.get('Content-Type', None), response.content, kwargs.get('data')) 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 ad_hoc_command_factory(inventory, machine_credential, admin): def factory(inventory=inventory, credential=machine_credential, initial_state='new', created_by=admin): adhoc = AdHocCommand( name='test-adhoc', inventory=inventory, credential=credential, status=initial_state, created_by=created_by ) adhoc.save() return adhoc return factory @pytest.fixture def job_template(): return JobTemplate.objects.create(name='test-job_template') @pytest.fixture def job_template_labels(organization, job_template): job_template.labels.create(name="label-1", organization=organization) job_template.labels.create(name="label-2", organization=organization) return job_template @pytest.fixture def jt_linked(organization, project, inventory, machine_credential, credential, net_credential, vault_credential): ''' A job template with a reasonably complete set of related objects to test RBAC and other functionality affected by related objects ''' jt = JobTemplate.objects.create( project=project, inventory=inventory, playbook='helloworld.yml', organization=organization ) jt.credentials.add(machine_credential, vault_credential, credential, net_credential) return jt @pytest.fixture def workflow_job_template(organization): wjt = WorkflowJobTemplate(name='test-workflow_job_template', organization=organization) wjt.save() return wjt @pytest.fixture def workflow_job_factory(workflow_job_template, admin): def factory(workflow_job_template=workflow_job_template, initial_state='new', created_by=admin): return workflow_job_template.create_unified_job(_eager_fields={ 'status': initial_state, 'created_by': created_by}) return factory @pytest.fixture def system_job_template(): sys_jt = SystemJobTemplate(name='test-system_job_template', job_type='cleanup_jobs') sys_jt.save() return sys_jt @pytest.fixture def system_job_factory(system_job_template, admin): def factory(system_job_template=system_job_template, initial_state='new', created_by=admin): return system_job_template.create_unified_job(_eager_fields={ 'status': initial_state, 'created_by': created_by}) return factory def dumps(value): return DjangoJSONEncoder().encode(value) # Taken from https://github.com/django-extensions/django-extensions/blob/54fe88df801d289882a79824be92d823ab7be33e/django_extensions/db/fields/json.py def get_db_prep_save(self, value, connection, **kwargs): """Convert our JSON object to a string before we save""" if value is None and self.null: return None # default values come in as strings; only non-strings should be # run through `dumps` if not isinstance(value, str): value = dumps(value) return value @pytest.fixture def monkeypatch_jsonbfield_get_db_prep_save(mocker): JSONBField.get_db_prep_save = get_db_prep_save @pytest.fixture def oauth_application(admin): return Application.objects.create( name='test app', user=admin, client_type='confidential', authorization_grant_type='password' ) @pytest.fixture def sqlite_copy_expert(request): # copy_expert is postgres-specific, and SQLite doesn't support it; mock its # behavior to test that it writes a file that contains stdout from events path = tempfile.mkdtemp(prefix='job-event-stdout') def write_stdout(self, sql, fd): # simulate postgres copy_expert support with ORM code parts = sql.split(' ') tablename = parts[parts.index('from') + 1] for cls in (JobEvent, AdHocCommandEvent, ProjectUpdateEvent, InventoryUpdateEvent, SystemJobEvent): if cls._meta.db_table == tablename: for event in cls.objects.order_by('start_line').all(): fd.write(event.stdout) setattr(SQLiteCursorWrapper, 'copy_expert', write_stdout) request.addfinalizer(lambda: shutil.rmtree(path)) request.addfinalizer(lambda: delattr(SQLiteCursorWrapper, 'copy_expert')) return path @pytest.fixture def disable_database_settings(mocker): m = mocker.patch('awx.conf.settings.SettingsWrapper.all_supported_settings', new_callable=PropertyMock) m.return_value = [] @pytest.fixture def slice_jt_factory(inventory): def r(N, jt_kwargs=None): for i in range(N): inventory.hosts.create(name='foo{}'.format(i)) if not jt_kwargs: jt_kwargs = {} return JobTemplate.objects.create( name='slice-jt-from-factory', job_slice_count=N, inventory=inventory, **jt_kwargs ) return r @pytest.fixture def slice_job_factory(slice_jt_factory): def r(N, jt_kwargs=None, prompts=None, spawn=False): slice_jt = slice_jt_factory(N, jt_kwargs=jt_kwargs) if not prompts: prompts = {} slice_job = slice_jt.create_unified_job(**prompts) if spawn: for node in slice_job.workflow_nodes.all(): # does what the task manager does for spawning workflow jobs kv = node.get_job_kwargs() job = node.unified_job_template.create_unified_job(**kv) node.job = job node.save() return slice_job return r