Files
awx/awx/main/tests/functional/conftest.py
AlanCoding fefab89815 Integrate content caching with existing task logic
Revert the --force flags

use the update id as metric for role caching

Shift the movement of cache to job folder from rsync task to python

Only install roles and collections if needed

Deal with roles and collections for jobs without sync
Skip local copy if roles or collections turned off

update docs for content caching

Design pivot - use empty cache dir to indicate lack of content

Do not cache content if we did not install content

Test changes to allay concerns about reliability of local_path

Do not blow away cache for SCM inventory updates
2020-07-20 19:45:21 -04:00

853 lines
26 KiB
Python

# 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