Merge branch 'devel' into 8898-fix-update-vault-credentials

This commit is contained in:
Jessica Steurer
2023-04-28 10:37:15 -03:00
committed by GitHub
67 changed files with 393 additions and 535 deletions

View File

@@ -1,6 +1,6 @@
-include awx/ui_next/Makefile -include awx/ui_next/Makefile
PYTHON ?= python3.9 PYTHON := $(notdir $(shell for i in python3.9 python3; do command -v $$i; done|sed 1q))
DOCKER_COMPOSE ?= docker-compose DOCKER_COMPOSE ?= docker-compose
OFFICIAL ?= no OFFICIAL ?= no
NODE ?= node NODE ?= node
@@ -296,13 +296,13 @@ swagger: reports
check: black check: black
api-lint: api-lint:
BLACK_ARGS="--check" make black BLACK_ARGS="--check" $(MAKE) black
flake8 awx flake8 awx
yamllint -s . yamllint -s .
## Run egg_info_dev to generate awx.egg-info for development.
awx-link: awx-link:
[ -d "/awx_devel/awx.egg-info" ] || $(PYTHON) /awx_devel/tools/scripts/egg_info_dev [ -d "/awx_devel/awx.egg-info" ] || $(PYTHON) /awx_devel/tools/scripts/egg_info_dev
cp -f /tmp/awx.egg-link /var/lib/awx/venv/awx/lib/$(PYTHON)/site-packages/awx.egg-link
TEST_DIRS ?= awx/main/tests/unit awx/main/tests/functional awx/conf/tests awx/sso/tests TEST_DIRS ?= awx/main/tests/unit awx/main/tests/functional awx/conf/tests awx/sso/tests
PYTEST_ARGS ?= -n auto PYTEST_ARGS ?= -n auto
@@ -321,7 +321,7 @@ github_ci_setup:
# CI_GITHUB_TOKEN is defined in .github files # CI_GITHUB_TOKEN is defined in .github files
echo $(CI_GITHUB_TOKEN) | docker login ghcr.io -u $(GITHUB_ACTOR) --password-stdin echo $(CI_GITHUB_TOKEN) | docker login ghcr.io -u $(GITHUB_ACTOR) --password-stdin
docker pull $(DEVEL_IMAGE_NAME) || : # Pre-pull image to warm build cache docker pull $(DEVEL_IMAGE_NAME) || : # Pre-pull image to warm build cache
make docker-compose-build $(MAKE) docker-compose-build
## Runs AWX_DOCKER_CMD inside a new docker container. ## Runs AWX_DOCKER_CMD inside a new docker container.
docker-runner: docker-runner:
@@ -371,7 +371,7 @@ test_collection_sanity:
rm -rf $(COLLECTION_INSTALL) rm -rf $(COLLECTION_INSTALL)
if ! [ -x "$(shell command -v ansible-test)" ]; then pip install ansible-core; fi if ! [ -x "$(shell command -v ansible-test)" ]; then pip install ansible-core; fi
ansible --version ansible --version
COLLECTION_VERSION=1.0.0 make install_collection COLLECTION_VERSION=1.0.0 $(MAKE) install_collection
cd $(COLLECTION_INSTALL) && ansible-test sanity $(COLLECTION_SANITY_ARGS) cd $(COLLECTION_INSTALL) && ansible-test sanity $(COLLECTION_SANITY_ARGS)
test_collection_integration: install_collection test_collection_integration: install_collection
@@ -589,7 +589,7 @@ docker-compose-cluster-elk: awx/projects docker-compose-sources
$(DOCKER_COMPOSE) -f tools/docker-compose/_sources/docker-compose.yml -f tools/elastic/docker-compose.logstash-link-cluster.yml -f tools/elastic/docker-compose.elastic-override.yml up --no-recreate $(DOCKER_COMPOSE) -f tools/docker-compose/_sources/docker-compose.yml -f tools/elastic/docker-compose.logstash-link-cluster.yml -f tools/elastic/docker-compose.elastic-override.yml up --no-recreate
docker-compose-container-group: docker-compose-container-group:
MINIKUBE_CONTAINER_GROUP=true make docker-compose MINIKUBE_CONTAINER_GROUP=true $(MAKE) docker-compose
clean-elk: clean-elk:
docker stop tools_kibana_1 docker stop tools_kibana_1
@@ -606,7 +606,18 @@ VERSION:
@echo "awx: $(VERSION)" @echo "awx: $(VERSION)"
PYTHON_VERSION: PYTHON_VERSION:
@echo "$(PYTHON)" | sed 's:python::' @echo "$(subst python,,$(PYTHON))"
.PHONY: version-for-buildyml
version-for-buildyml:
@echo $(firstword $(subst +, ,$(VERSION)))
# version-for-buildyml prints a special version string for build.yml,
# chopping off the sha after the '+' sign.
# tools/ansible/build.yml was doing this: make print-VERSION | cut -d + -f -1
# This does the same thing in native make without
# the pipe or the extra processes, and now the pb does `make version-for-buildyml`
# Example:
# 22.1.1.dev38+g523c0d9781 becomes 22.1.1.dev38
.PHONY: Dockerfile .PHONY: Dockerfile
## Generate Dockerfile for awx image ## Generate Dockerfile for awx image
@@ -658,6 +669,7 @@ messages:
fi; \ fi; \
$(PYTHON) manage.py makemessages -l en_us --keep-pot $(PYTHON) manage.py makemessages -l en_us --keep-pot
.PHONY: print-%
print-%: print-%:
@echo $($*) @echo $($*)
@@ -669,12 +681,12 @@ HELP_FILTER=.PHONY
## Display help targets ## Display help targets
help: help:
@printf "Available targets:\n" @printf "Available targets:\n"
@make -s help/generate | grep -vE "\w($(HELP_FILTER))" @$(MAKE) -s help/generate | grep -vE "\w($(HELP_FILTER))"
## Display help for all targets ## Display help for all targets
help/all: help/all:
@printf "Available targets:\n" @printf "Available targets:\n"
@make -s help/generate @$(MAKE) -s help/generate
## Generate help output from MAKEFILE_LIST ## Generate help output from MAKEFILE_LIST
help/generate: help/generate:
@@ -698,4 +710,4 @@ help/generate:
## Display help for ui-next targets ## Display help for ui-next targets
help/ui-next: help/ui-next:
@make -s help MAKEFILE_LIST="awx/ui_next/Makefile" @$(MAKE) -s help MAKEFILE_LIST="awx/ui_next/Makefile"

View File

@@ -33,7 +33,7 @@ from rest_framework.negotiation import DefaultContentNegotiation
# AWX # AWX
from awx.api.filters import FieldLookupBackend from awx.api.filters import FieldLookupBackend
from awx.main.models import UnifiedJob, UnifiedJobTemplate, User, Role, Credential, WorkflowJobTemplateNode, WorkflowApprovalTemplate from awx.main.models import UnifiedJob, UnifiedJobTemplate, User, Role, Credential, WorkflowJobTemplateNode, WorkflowApprovalTemplate
from awx.main.access import access_registry from awx.main.access import optimize_queryset
from awx.main.utils import camelcase_to_underscore, get_search_fields, getattrd, get_object_or_400, decrypt_field, get_awx_version from awx.main.utils import camelcase_to_underscore, get_search_fields, getattrd, get_object_or_400, decrypt_field, get_awx_version
from awx.main.utils.db import get_all_field_names from awx.main.utils.db import get_all_field_names
from awx.main.utils.licensing import server_product_name from awx.main.utils.licensing import server_product_name
@@ -362,12 +362,7 @@ class GenericAPIView(generics.GenericAPIView, APIView):
return self.queryset._clone() return self.queryset._clone()
elif self.model is not None: elif self.model is not None:
qs = self.model._default_manager qs = self.model._default_manager
if self.model in access_registry: qs = optimize_queryset(qs)
access_class = access_registry[self.model]
if access_class.select_related:
qs = qs.select_related(*access_class.select_related)
if access_class.prefetch_related:
qs = qs.prefetch_related(*access_class.prefetch_related)
return qs return qs
else: else:
return super(GenericAPIView, self).get_queryset() return super(GenericAPIView, self).get_queryset()
@@ -510,6 +505,9 @@ class SubListAPIView(ParentMixin, ListAPIView):
# And optionally (user must have given access permission on parent object # And optionally (user must have given access permission on parent object
# to view sublist): # to view sublist):
# parent_access = 'read' # parent_access = 'read'
# filter_read_permission sets whether or not to override the default intersection behavior
# implemented here
filter_read_permission = True
def get_description_context(self): def get_description_context(self):
d = super(SubListAPIView, self).get_description_context() d = super(SubListAPIView, self).get_description_context()
@@ -524,8 +522,10 @@ class SubListAPIView(ParentMixin, ListAPIView):
def get_queryset(self): def get_queryset(self):
parent = self.get_parent_object() parent = self.get_parent_object()
self.check_parent_access(parent) self.check_parent_access(parent)
qs = self.request.user.get_queryset(self.model).distinct()
sublist_qs = self.get_sublist_queryset(parent) sublist_qs = self.get_sublist_queryset(parent)
if not self.filter_read_permission:
return optimize_queryset(sublist_qs)
qs = self.request.user.get_queryset(self.model).distinct()
return qs & sublist_qs return qs & sublist_qs
def get_sublist_queryset(self, parent): def get_sublist_queryset(self, parent):

View File

@@ -954,7 +954,7 @@ class UnifiedJobStdoutSerializer(UnifiedJobSerializer):
class UserSerializer(BaseSerializer): class UserSerializer(BaseSerializer):
password = serializers.CharField(required=False, default='', write_only=True, help_text=_('Write-only field used to change the password.')) password = serializers.CharField(required=False, default='', help_text=_('Field used to change the password.'))
ldap_dn = serializers.CharField(source='profile.ldap_dn', read_only=True) ldap_dn = serializers.CharField(source='profile.ldap_dn', read_only=True)
external_account = serializers.SerializerMethodField(help_text=_('Set if the account is managed by an external service')) external_account = serializers.SerializerMethodField(help_text=_('Set if the account is managed by an external service'))
is_system_auditor = serializers.BooleanField(default=False) is_system_auditor = serializers.BooleanField(default=False)
@@ -981,7 +981,12 @@ class UserSerializer(BaseSerializer):
def to_representation(self, obj): def to_representation(self, obj):
ret = super(UserSerializer, self).to_representation(obj) ret = super(UserSerializer, self).to_representation(obj)
ret.pop('password', None) if self.get_external_account(obj):
# If this is an external account it shouldn't have a password field
ret.pop('password', None)
else:
# If its an internal account lets assume there is a password and return $encrypted$ to the user
ret['password'] = '$encrypted$'
if obj and type(self) is UserSerializer: if obj and type(self) is UserSerializer:
ret['auth'] = obj.social_auth.values('provider', 'uid') ret['auth'] = obj.social_auth.values('provider', 'uid')
return ret return ret
@@ -1019,7 +1024,7 @@ class UserSerializer(BaseSerializer):
# For now we're not raising an error, just not saving password for # For now we're not raising an error, just not saving password for
# users managed by LDAP who already have an unusable password set. # users managed by LDAP who already have an unusable password set.
# Get external password will return something like ldap or enterprise or None if the user isn't external. We only want to allow a password update for a None option # Get external password will return something like ldap or enterprise or None if the user isn't external. We only want to allow a password update for a None option
if new_password and not self.get_external_account(obj): if new_password and new_password != '$encrypted$' and not self.get_external_account(obj):
obj.set_password(new_password) obj.set_password(new_password)
obj.save(update_fields=['password']) obj.save(update_fields=['password'])
@@ -2185,7 +2190,7 @@ class BulkHostCreateSerializer(serializers.Serializer):
host_data = [] host_data = []
for r in result: for r in result:
item = {k: getattr(r, k) for k in return_keys} item = {k: getattr(r, k) for k in return_keys}
if not settings.IS_TESTING_MODE: if settings.DATABASES and ('sqlite3' not in settings.DATABASES.get('default', {}).get('ENGINE')):
# sqlite acts different with bulk_create -- it doesn't return the id of the objects # sqlite acts different with bulk_create -- it doesn't return the id of the objects
# to get it, you have to do an additional query, which is not useful for our tests # to get it, you have to do an additional query, which is not useful for our tests
item['url'] = reverse('api:host_detail', kwargs={'pk': r.id}) item['url'] = reverse('api:host_detail', kwargs={'pk': r.id})

View File

@@ -62,7 +62,7 @@ from wsgiref.util import FileWrapper
# AWX # AWX
from awx.main.tasks.system import send_notifications, update_inventory_computed_fields from awx.main.tasks.system import send_notifications, update_inventory_computed_fields
from awx.main.access import get_user_queryset, HostAccess from awx.main.access import get_user_queryset
from awx.api.generics import ( from awx.api.generics import (
APIView, APIView,
BaseUsersList, BaseUsersList,
@@ -794,13 +794,7 @@ class ExecutionEnvironmentActivityStreamList(SubListAPIView):
parent_model = models.ExecutionEnvironment parent_model = models.ExecutionEnvironment
relationship = 'activitystream_set' relationship = 'activitystream_set'
search_fields = ('changes',) search_fields = ('changes',)
filter_read_permission = False
def get_queryset(self):
parent = self.get_parent_object()
self.check_parent_access(parent)
qs = self.request.user.get_queryset(self.model)
return qs.filter(execution_environment=parent)
class ProjectList(ListCreateAPIView): class ProjectList(ListCreateAPIView):
@@ -1634,13 +1628,7 @@ class InventoryHostsList(HostRelatedSearchMixin, SubListCreateAttachDetachAPIVie
parent_model = models.Inventory parent_model = models.Inventory
relationship = 'hosts' relationship = 'hosts'
parent_key = 'inventory' parent_key = 'inventory'
filter_read_permission = False
def get_queryset(self):
inventory = self.get_parent_object()
qs = getattrd(inventory, self.relationship).all()
# Apply queryset optimizations
qs = qs.select_related(*HostAccess.select_related).prefetch_related(*HostAccess.prefetch_related)
return qs
class HostGroupsList(SubListCreateAttachDetachAPIView): class HostGroupsList(SubListCreateAttachDetachAPIView):
@@ -2581,16 +2569,7 @@ class JobTemplateCredentialsList(SubListCreateAttachDetachAPIView):
serializer_class = serializers.CredentialSerializer serializer_class = serializers.CredentialSerializer
parent_model = models.JobTemplate parent_model = models.JobTemplate
relationship = 'credentials' relationship = 'credentials'
filter_read_permission = False
def get_queryset(self):
# Return the full list of credentials
parent = self.get_parent_object()
self.check_parent_access(parent)
sublist_qs = getattrd(parent, self.relationship)
sublist_qs = sublist_qs.prefetch_related(
'created_by', 'modified_by', 'admin_role', 'use_role', 'read_role', 'admin_role__parents', 'admin_role__members'
)
return sublist_qs
def is_valid_relation(self, parent, sub, created=False): def is_valid_relation(self, parent, sub, created=False):
if sub.unique_hash() in [cred.unique_hash() for cred in parent.credentials.all()]: if sub.unique_hash() in [cred.unique_hash() for cred in parent.credentials.all()]:
@@ -2780,6 +2759,7 @@ class JobTemplateInstanceGroupsList(SubListAttachDetachAPIView):
serializer_class = serializers.InstanceGroupSerializer serializer_class = serializers.InstanceGroupSerializer
parent_model = models.JobTemplate parent_model = models.JobTemplate
relationship = 'instance_groups' relationship = 'instance_groups'
filter_read_permission = False
class JobTemplateAccessList(ResourceAccessList): class JobTemplateAccessList(ResourceAccessList):
@@ -2870,16 +2850,7 @@ class WorkflowJobTemplateNodeChildrenBaseList(EnforceParentRelationshipMixin, Su
relationship = '' relationship = ''
enforce_parent_relationship = 'workflow_job_template' enforce_parent_relationship = 'workflow_job_template'
search_fields = ('unified_job_template__name', 'unified_job_template__description') search_fields = ('unified_job_template__name', 'unified_job_template__description')
filter_read_permission = False
'''
Limit the set of WorkflowJobTemplateNodes to the related nodes of specified by
'relationship'
'''
def get_queryset(self):
parent = self.get_parent_object()
self.check_parent_access(parent)
return getattr(parent, self.relationship).all()
def is_valid_relation(self, parent, sub, created=False): def is_valid_relation(self, parent, sub, created=False):
if created: if created:
@@ -2954,14 +2925,7 @@ class WorkflowJobNodeChildrenBaseList(SubListAPIView):
parent_model = models.WorkflowJobNode parent_model = models.WorkflowJobNode
relationship = '' relationship = ''
search_fields = ('unified_job_template__name', 'unified_job_template__description') search_fields = ('unified_job_template__name', 'unified_job_template__description')
filter_read_permission = False
#
# Limit the set of WorkflowJobNodes to the related nodes of specified by self.relationship
#
def get_queryset(self):
parent = self.get_parent_object()
self.check_parent_access(parent)
return getattr(parent, self.relationship).all()
class WorkflowJobNodeSuccessNodesList(WorkflowJobNodeChildrenBaseList): class WorkflowJobNodeSuccessNodesList(WorkflowJobNodeChildrenBaseList):
@@ -3140,11 +3104,8 @@ class WorkflowJobTemplateWorkflowNodesList(SubListCreateAPIView):
relationship = 'workflow_job_template_nodes' relationship = 'workflow_job_template_nodes'
parent_key = 'workflow_job_template' parent_key = 'workflow_job_template'
search_fields = ('unified_job_template__name', 'unified_job_template__description') search_fields = ('unified_job_template__name', 'unified_job_template__description')
ordering = ('id',) # assure ordering by id for consistency
def get_queryset(self): filter_read_permission = False
parent = self.get_parent_object()
self.check_parent_access(parent)
return getattr(parent, self.relationship).order_by('id')
class WorkflowJobTemplateJobsList(SubListAPIView): class WorkflowJobTemplateJobsList(SubListAPIView):
@@ -3236,11 +3197,8 @@ class WorkflowJobWorkflowNodesList(SubListAPIView):
relationship = 'workflow_job_nodes' relationship = 'workflow_job_nodes'
parent_key = 'workflow_job' parent_key = 'workflow_job'
search_fields = ('unified_job_template__name', 'unified_job_template__description') search_fields = ('unified_job_template__name', 'unified_job_template__description')
ordering = ('id',) # assure ordering by id for consistency
def get_queryset(self): filter_read_permission = False
parent = self.get_parent_object()
self.check_parent_access(parent)
return getattr(parent, self.relationship).order_by('id')
class WorkflowJobCancel(GenericCancelView): class WorkflowJobCancel(GenericCancelView):
@@ -3554,11 +3512,7 @@ class BaseJobHostSummariesList(SubListAPIView):
relationship = 'job_host_summaries' relationship = 'job_host_summaries'
name = _('Job Host Summaries List') name = _('Job Host Summaries List')
search_fields = ('host_name',) search_fields = ('host_name',)
filter_read_permission = False
def get_queryset(self):
parent = self.get_parent_object()
self.check_parent_access(parent)
return getattr(parent, self.relationship).select_related('job', 'job__job_template', 'host')
class HostJobHostSummariesList(BaseJobHostSummariesList): class HostJobHostSummariesList(BaseJobHostSummariesList):

View File

@@ -61,12 +61,6 @@ class OrganizationList(OrganizationCountsMixin, ListCreateAPIView):
model = Organization model = Organization
serializer_class = OrganizationSerializer serializer_class = OrganizationSerializer
def get_queryset(self):
qs = Organization.accessible_objects(self.request.user, 'read_role')
qs = qs.select_related('admin_role', 'auditor_role', 'member_role', 'read_role')
qs = qs.prefetch_related('created_by', 'modified_by')
return qs
class OrganizationDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView): class OrganizationDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView):
model = Organization model = Organization
@@ -207,6 +201,7 @@ class OrganizationInstanceGroupsList(SubListAttachDetachAPIView):
serializer_class = InstanceGroupSerializer serializer_class = InstanceGroupSerializer
parent_model = Organization parent_model = Organization
relationship = 'instance_groups' relationship = 'instance_groups'
filter_read_permission = False
class OrganizationGalaxyCredentialsList(SubListAttachDetachAPIView): class OrganizationGalaxyCredentialsList(SubListAttachDetachAPIView):
@@ -214,6 +209,7 @@ class OrganizationGalaxyCredentialsList(SubListAttachDetachAPIView):
serializer_class = CredentialSerializer serializer_class = CredentialSerializer
parent_model = Organization parent_model = Organization
relationship = 'galaxy_credentials' relationship = 'galaxy_credentials'
filter_read_permission = False
def is_valid_relation(self, parent, sub, created=False): def is_valid_relation(self, parent, sub, created=False):
if sub.kind != 'galaxy_api_token': if sub.kind != 'galaxy_api_token':

View File

@@ -2952,3 +2952,19 @@ class WorkflowApprovalTemplateAccess(BaseAccess):
for cls in BaseAccess.__subclasses__(): for cls in BaseAccess.__subclasses__():
access_registry[cls.model] = cls access_registry[cls.model] = cls
access_registry[UnpartitionedJobEvent] = UnpartitionedJobEventAccess access_registry[UnpartitionedJobEvent] = UnpartitionedJobEventAccess
def optimize_queryset(queryset):
"""
A utility method in case you already have a queryset and just want to
apply the standard optimizations for that model.
In other words, use if you do not want to start from filtered_queryset for some reason.
"""
if not queryset.model or queryset.model not in access_registry:
return queryset
access_class = access_registry[queryset.model]
if access_class.select_related:
queryset = queryset.select_related(*access_class.select_related)
if access_class.prefetch_related:
queryset = queryset.prefetch_related(*access_class.prefetch_related)
return queryset

View File

@@ -2,6 +2,8 @@ import json
import logging import logging
import os import os
import time import time
import signal
import sys
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.conf import settings from django.conf import settings
@@ -50,6 +52,11 @@ class Command(BaseCommand):
} }
return json.dumps(payload) return json.dumps(payload)
def notify_listener_and_exit(self, *args):
with pg_bus_conn(new_connection=False) as conn:
conn.notify('web_heartbeet', self.construct_payload(action='offline'))
sys.exit(0)
def do_hearbeat_loop(self): def do_hearbeat_loop(self):
with pg_bus_conn(new_connection=True) as conn: with pg_bus_conn(new_connection=True) as conn:
while True: while True:
@@ -57,10 +64,10 @@ class Command(BaseCommand):
conn.notify('web_heartbeet', self.construct_payload()) conn.notify('web_heartbeet', self.construct_payload())
time.sleep(settings.BROADCAST_WEBSOCKET_BEACON_FROM_WEB_RATE_SECONDS) time.sleep(settings.BROADCAST_WEBSOCKET_BEACON_FROM_WEB_RATE_SECONDS)
# TODO: Send a message with action=offline if we notice a SIGTERM or SIGINT
# (wsrelay can use this to remove the node quicker)
def handle(self, *arg, **options): def handle(self, *arg, **options):
self.print_banner() self.print_banner()
signal.signal(signal.SIGTERM, self.notify_listener_and_exit)
signal.signal(signal.SIGINT, self.notify_listener_and_exit)
# Note: We don't really try any reconnect logic to pg_notify here, # Note: We don't really try any reconnect logic to pg_notify here,
# just let supervisor restart if we fail. # just let supervisor restart if we fail.

View File

@@ -1479,8 +1479,6 @@ class PluginFileInjector(object):
def build_env(self, inventory_update, env, private_data_dir, private_data_files): def build_env(self, inventory_update, env, private_data_dir, private_data_files):
injector_env = self.get_plugin_env(inventory_update, private_data_dir, private_data_files) injector_env = self.get_plugin_env(inventory_update, private_data_dir, private_data_files)
env.update(injector_env) env.update(injector_env)
# Preserves current behavior for Ansible change in default planned for 2.10
env['ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS'] = 'never'
# All CLOUD_PROVIDERS sources implement as inventory plugin from collection # All CLOUD_PROVIDERS sources implement as inventory plugin from collection
env['ANSIBLE_INVENTORY_ENABLED'] = 'auto' env['ANSIBLE_INVENTORY_ENABLED'] = 'auto'
return env return env

View File

@@ -1,6 +1,5 @@
{ {
"ANSIBLE_JINJA2_NATIVE": "True", "ANSIBLE_JINJA2_NATIVE": "True",
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never",
"AZURE_CLIENT_ID": "fooo", "AZURE_CLIENT_ID": "fooo",
"AZURE_CLOUD_ENVIRONMENT": "fooo", "AZURE_CLOUD_ENVIRONMENT": "fooo",
"AZURE_SECRET": "fooo", "AZURE_SECRET": "fooo",

View File

@@ -1,5 +1,4 @@
{ {
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never",
"TOWER_HOST": "https://foo.invalid", "TOWER_HOST": "https://foo.invalid",
"TOWER_PASSWORD": "fooo", "TOWER_PASSWORD": "fooo",
"TOWER_USERNAME": "fooo", "TOWER_USERNAME": "fooo",

View File

@@ -1,6 +1,5 @@
{ {
"ANSIBLE_JINJA2_NATIVE": "True", "ANSIBLE_JINJA2_NATIVE": "True",
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never",
"AWS_ACCESS_KEY_ID": "fooo", "AWS_ACCESS_KEY_ID": "fooo",
"AWS_SECRET_ACCESS_KEY": "fooo", "AWS_SECRET_ACCESS_KEY": "fooo",
"AWS_SECURITY_TOKEN": "fooo", "AWS_SECURITY_TOKEN": "fooo",

View File

@@ -1,6 +1,5 @@
{ {
"ANSIBLE_JINJA2_NATIVE": "True", "ANSIBLE_JINJA2_NATIVE": "True",
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never",
"GCE_CREDENTIALS_FILE_PATH": "{{ file_reference }}", "GCE_CREDENTIALS_FILE_PATH": "{{ file_reference }}",
"GOOGLE_APPLICATION_CREDENTIALS": "{{ file_reference }}", "GOOGLE_APPLICATION_CREDENTIALS": "{{ file_reference }}",
"GCP_AUTH_KIND": "serviceaccount", "GCP_AUTH_KIND": "serviceaccount",

View File

@@ -1,5 +1,4 @@
{ {
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never",
"INSIGHTS_USER": "fooo", "INSIGHTS_USER": "fooo",
"INSIGHTS_PASSWORD": "fooo" "INSIGHTS_PASSWORD": "fooo"
} }

View File

@@ -1,4 +1,3 @@
{ {
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never",
"OS_CLIENT_CONFIG_FILE": "{{ file_reference }}" "OS_CLIENT_CONFIG_FILE": "{{ file_reference }}"
} }

View File

@@ -1,5 +1,4 @@
{ {
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never",
"OVIRT_INI_PATH": "{{ file_reference }}", "OVIRT_INI_PATH": "{{ file_reference }}",
"OVIRT_PASSWORD": "fooo", "OVIRT_PASSWORD": "fooo",
"OVIRT_URL": "https://foo.invalid", "OVIRT_URL": "https://foo.invalid",

View File

@@ -1,5 +1,4 @@
{ {
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never",
"FOREMAN_PASSWORD": "fooo", "FOREMAN_PASSWORD": "fooo",
"FOREMAN_SERVER": "https://foo.invalid", "FOREMAN_SERVER": "https://foo.invalid",
"FOREMAN_USER": "fooo" "FOREMAN_USER": "fooo"

View File

@@ -1,5 +1,4 @@
{ {
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never",
"VMWARE_HOST": "https://foo.invalid", "VMWARE_HOST": "https://foo.invalid",
"VMWARE_PASSWORD": "fooo", "VMWARE_PASSWORD": "fooo",
"VMWARE_USER": "fooo", "VMWARE_USER": "fooo",

View File

@@ -329,3 +329,21 @@ def test_galaxy_credential_association(alice, admin, organization, post, get):
'Public Galaxy 4', 'Public Galaxy 4',
'Public Galaxy 5', 'Public Galaxy 5',
] ]
@pytest.mark.django_db
def test_org_admin_credential_count(org_admin, admin, organization, post, get):
galaxy = CredentialType.defaults['galaxy_api_token']()
galaxy.save()
for i in range(3):
cred = Credential.objects.create(credential_type=galaxy, name=f'test_{i}', inputs={'url': 'https://galaxy.ansible.com/'})
url = reverse('api:organization_galaxy_credentials_list', kwargs={'pk': organization.pk})
post(url, {'associate': True, 'id': cred.pk}, user=admin, expect=204)
# org admin should see all associated galaxy credentials
resp = get(url, user=org_admin)
assert resp.data['count'] == 3
# removing one to validate new count
post(url, {'disassociate': True, 'id': Credential.objects.get(name='test_1').pk}, user=admin, expect=204)
resp_new = get(url, user=org_admin)
assert resp_new.data['count'] == 2

View File

@@ -0,0 +1,28 @@
# Python
from unittest import mock
import uuid
# patch python-ldap
with mock.patch('__main__.__builtins__.dir', return_value=[]):
import ldap # NOQA
# Load development settings for base variables.
from awx.settings.development import * # NOQA
# Some things make decisions based on settings.SETTINGS_MODULE, so this is done for that
SETTINGS_MODULE = 'awx.settings.development'
# Use SQLite for unit tests instead of PostgreSQL. If the lines below are
# commented out, Django will create the test_awx-dev database in PostgreSQL to
# run unit tests.
CACHES = {'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 'LOCATION': 'unique-{}'.format(str(uuid.uuid4()))}}
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'awx.sqlite3'), # noqa
'TEST': {
# Test database cannot be :memory: for inventory tests.
'NAME': os.path.join(BASE_DIR, 'awx_test.sqlite3') # noqa
},
}
}

View File

@@ -1,8 +1,32 @@
from split_settings.tools import include from split_settings.tools import include
LOCAL_SETTINGS = (
'ALLOWED_HOSTS',
'BROADCAST_WEBSOCKET_PORT',
'BROADCAST_WEBSOCKET_VERIFY_CERT',
'BROADCAST_WEBSOCKET_PROTOCOL',
'BROADCAST_WEBSOCKET_SECRET',
'DATABASES',
'CACHES',
'DEBUG',
'NAMED_URL_GRAPH',
)
def test_postprocess_auth_basic_enabled(): def test_postprocess_auth_basic_enabled():
locals().update({'__file__': __file__}) locals().update({'__file__': __file__})
include('../../../settings/defaults.py', scope=locals()) include('../../../settings/defaults.py', scope=locals())
assert 'awx.api.authentication.LoggedBasicAuthentication' in locals()['REST_FRAMEWORK']['DEFAULT_AUTHENTICATION_CLASSES'] assert 'awx.api.authentication.LoggedBasicAuthentication' in locals()['REST_FRAMEWORK']['DEFAULT_AUTHENTICATION_CLASSES']
def test_default_settings():
from django.conf import settings
for k in dir(settings):
if k not in settings.DEFAULTS_SNAPSHOT or k in LOCAL_SETTINGS:
continue
default_val = getattr(settings.default_settings, k, None)
snapshot_val = settings.DEFAULTS_SNAPSHOT[k]
assert default_val == snapshot_val, f'Setting for {k} does not match shapshot:\nsnapshot: {snapshot_val}\ndefault: {default_val}'

View File

@@ -1,24 +1,16 @@
# Copyright (c) 2015 Ansible, Inc. # Copyright (c) 2015 Ansible, Inc.
# All Rights Reserved. # All Rights Reserved.
# Python
import base64 import base64
import os import os
import re # noqa import re # noqa
import sys
import tempfile import tempfile
import socket import socket
from datetime import timedelta from datetime import timedelta
# python-ldap
if "pytest" in sys.modules: import ldap
IS_TESTING_MODE = True
from unittest import mock
with mock.patch('__main__.__builtins__.dir', return_value=[]):
import ldap
else:
IS_TESTING_MODE = False
import ldap
DEBUG = True DEBUG = True

View File

@@ -9,7 +9,6 @@ import socket
import copy import copy
import sys import sys
import traceback import traceback
import uuid
# Centos-7 doesn't include the svg mime type # Centos-7 doesn't include the svg mime type
# /usr/lib64/python/mimetypes.py # /usr/lib64/python/mimetypes.py
@@ -62,38 +61,9 @@ DEBUG_TOOLBAR_CONFIG = {'ENABLE_STACKTRACES': True}
SYSTEM_UUID = '00000000-0000-0000-0000-000000000000' SYSTEM_UUID = '00000000-0000-0000-0000-000000000000'
INSTALL_UUID = '00000000-0000-0000-0000-000000000000' INSTALL_UUID = '00000000-0000-0000-0000-000000000000'
# Store a snapshot of default settings at this point before loading any
# customizable config files.
DEFAULTS_SNAPSHOT = {}
this_module = sys.modules[__name__]
for setting in dir(this_module):
if setting == setting.upper():
DEFAULTS_SNAPSHOT[setting] = copy.deepcopy(getattr(this_module, setting))
# If there is an `/etc/tower/settings.py`, include it.
# If there is a `/etc/tower/conf.d/*.py`, include them.
include(optional('/etc/tower/settings.py'), scope=locals())
include(optional('/etc/tower/conf.d/*.py'), scope=locals())
BASE_VENV_PATH = "/var/lib/awx/venv/" BASE_VENV_PATH = "/var/lib/awx/venv/"
AWX_VENV_PATH = os.path.join(BASE_VENV_PATH, "awx") AWX_VENV_PATH = os.path.join(BASE_VENV_PATH, "awx")
# Use SQLite for unit tests instead of PostgreSQL. If the lines below are
# commented out, Django will create the test_awx-dev database in PostgreSQL to
# run unit tests.
if "pytest" in sys.modules:
CACHES = {'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 'LOCATION': 'unique-{}'.format(str(uuid.uuid4()))}}
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'awx.sqlite3'), # noqa
'TEST': {
# Test database cannot be :memory: for inventory tests.
'NAME': os.path.join(BASE_DIR, 'awx_test.sqlite3') # noqa
},
}
}
CLUSTER_HOST_ID = socket.gethostname() CLUSTER_HOST_ID = socket.gethostname()
AWX_CALLBACK_PROFILE = True AWX_CALLBACK_PROFILE = True
@@ -105,11 +75,28 @@ AWX_CALLBACK_PROFILE = True
AWX_DISABLE_TASK_MANAGERS = False AWX_DISABLE_TASK_MANAGERS = False
# ======================!!!!!!! FOR DEVELOPMENT ONLY !!!!!!!================================= # ======================!!!!!!! FOR DEVELOPMENT ONLY !!!!!!!=================================
from .application_name import set_application_name # Store a snapshot of default settings at this point before loading any
# customizable config files.
this_module = sys.modules[__name__]
local_vars = dir(this_module)
DEFAULTS_SNAPSHOT = {} # define after we save local_vars so we do not snapshot the snapshot
for setting in local_vars:
if setting.isupper():
DEFAULTS_SNAPSHOT[setting] = copy.deepcopy(getattr(this_module, setting))
set_application_name(DATABASES, CLUSTER_HOST_ID) del local_vars # avoid temporary variables from showing up in dir(settings)
del this_module
#
###############################################################################################
#
# Any settings defined after this point will be marked as as a read_only database setting
#
################################################################################################
del set_application_name # If there is an `/etc/tower/settings.py`, include it.
# If there is a `/etc/tower/conf.d/*.py`, include them.
include(optional('/etc/tower/settings.py'), scope=locals())
include(optional('/etc/tower/conf.d/*.py'), scope=locals())
# If any local_*.py files are present in awx/settings/, use them to override # If any local_*.py files are present in awx/settings/, use them to override
# default settings for development. If not present, we can still run using # default settings for development. If not present, we can still run using
@@ -123,3 +110,11 @@ try:
except ImportError: except ImportError:
traceback.print_exc() traceback.print_exc()
sys.exit(1) sys.exit(1)
# The below runs AFTER all of the custom settings are imported
# because conf.d files will define DATABASES and this should modify that
from .application_name import set_application_name
set_application_name(DATABASES, CLUSTER_HOST_ID) # NOQA
del set_application_name

View File

@@ -47,17 +47,21 @@ AWX_ISOLATION_SHOW_PATHS = [
# Store a snapshot of default settings at this point before loading any # Store a snapshot of default settings at this point before loading any
# customizable config files. # customizable config files.
this_module = sys.modules[__name__]
local_vars = dir(this_module)
DEFAULTS_SNAPSHOT = {} # define after we save local_vars so we do not snapshot the snapshot
for setting in local_vars:
if setting.isupper():
DEFAULTS_SNAPSHOT[setting] = copy.deepcopy(getattr(this_module, setting))
del local_vars # avoid temporary variables from showing up in dir(settings)
del this_module
# #
############################################################################################### ###############################################################################################
# #
# Any settings defined after this point will be marked as as a read_only database setting # Any settings defined after this point will be marked as as a read_only database setting
# #
################################################################################################ ################################################################################################
DEFAULTS_SNAPSHOT = {}
this_module = sys.modules[__name__]
for setting in dir(this_module):
if setting == setting.upper():
DEFAULTS_SNAPSHOT[setting] = copy.deepcopy(getattr(this_module, setting))
# Load settings from any .py files in the global conf.d directory specified in # Load settings from any .py files in the global conf.d directory specified in
# the environment, defaulting to /etc/tower/conf.d/. # the environment, defaulting to /etc/tower/conf.d/.
@@ -98,8 +102,8 @@ except IOError:
else: else:
raise raise
# The below runs AFTER all of the custom settings are imported. # The below runs AFTER all of the custom settings are imported
# because conf.d files will define DATABASES and this should modify that
from .application_name import set_application_name from .application_name import set_application_name
set_application_name(DATABASES, CLUSTER_HOST_ID) # NOQA set_application_name(DATABASES, CLUSTER_HOST_ID) # NOQA

View File

@@ -115,16 +115,16 @@ function AdHocCredentialStep({ credentialTypeId }) {
searchColumns={[ searchColumns={[
{ {
name: t`Name`, name: t`Name`,
key: 'name', key: 'name__icontains',
isDefault: true, isDefault: true,
}, },
{ {
name: t`Created By (Username)`, name: t`Created By (Username)`,
key: 'created_by__username', key: 'created_by__username__icontains',
}, },
{ {
name: t`Modified By (Username)`, name: t`Modified By (Username)`,
key: 'modified_by__username', key: 'modified_by__username__icontains',
}, },
]} ]}
sortColumns={[ sortColumns={[

View File

@@ -195,9 +195,9 @@ function getRouteConfig(userProfile = {}) {
deleteRoute('host_metrics'); deleteRoute('host_metrics');
deleteRouteGroup('settings'); deleteRouteGroup('settings');
deleteRoute('management_jobs'); deleteRoute('management_jobs');
if (userProfile?.isOrgAdmin) return routeConfig;
deleteRoute('topology_view'); deleteRoute('topology_view');
deleteRoute('instances'); deleteRoute('instances');
if (userProfile?.isOrgAdmin) return routeConfig;
if (!userProfile?.isNotificationAdmin) deleteRoute('notification_templates'); if (!userProfile?.isNotificationAdmin) deleteRoute('notification_templates');
return routeConfig; return routeConfig;

View File

@@ -101,10 +101,8 @@ describe('getRouteConfig', () => {
'/credential_types', '/credential_types',
'/notification_templates', '/notification_templates',
'/instance_groups', '/instance_groups',
'/instances',
'/applications', '/applications',
'/execution_environments', '/execution_environments',
'/topology_view',
]); ]);
}); });
@@ -237,10 +235,8 @@ describe('getRouteConfig', () => {
'/credential_types', '/credential_types',
'/notification_templates', '/notification_templates',
'/instance_groups', '/instance_groups',
'/instances',
'/applications', '/applications',
'/execution_environments', '/execution_environments',
'/topology_view',
]); ]);
}); });
@@ -268,10 +264,8 @@ describe('getRouteConfig', () => {
'/credential_types', '/credential_types',
'/notification_templates', '/notification_templates',
'/instance_groups', '/instance_groups',
'/instances',
'/applications', '/applications',
'/execution_environments', '/execution_environments',
'/topology_view',
]); ]);
}); });
}); });

View File

@@ -131,12 +131,6 @@ function HostMetrics() {
> >
{t`Automation`} {t`Automation`}
</HeaderCell> </HeaderCell>
<HeaderCell
sortKey="used_in_inventories"
tooltip={t`How many inventories is the host in, recomputed on a weekly schedule`}
>
{t`Inventories`}
</HeaderCell>
<HeaderCell <HeaderCell
sortKey="deleted_counter" sortKey="deleted_counter"
tooltip={t`How many times was the host deleted`} tooltip={t`How many times was the host deleted`}

View File

@@ -21,7 +21,6 @@ function HostMetricsListItem({ item, isSelected, onSelect, rowIndex }) {
{formatDateString(item.last_automation)} {formatDateString(item.last_automation)}
</Td> </Td>
<Td dataLabel={t`Automation`}>{item.automated_counter}</Td> <Td dataLabel={t`Automation`}>{item.automated_counter}</Td>
<Td dataLabel={t`Inventories`}>{item.used_in_inventories || 0}</Td>
<Td dataLabel={t`Deleted`}>{item.deleted_counter}</Td> <Td dataLabel={t`Deleted`}>{item.deleted_counter}</Td>
</Tr> </Tr>
); );

View File

@@ -17,11 +17,7 @@ import { CardBody, CardActionsRow } from 'components/Card';
import { Detail, DetailList, UserDateDetail } from 'components/DetailList'; import { Detail, DetailList, UserDateDetail } from 'components/DetailList';
import { VariablesDetail } from 'components/CodeEditor'; import { VariablesDetail } from 'components/CodeEditor';
import { formatDateString, secondsToHHMMSS } from 'util/dates'; import { formatDateString, secondsToHHMMSS } from 'util/dates';
import { import { WorkflowApprovalsAPI, WorkflowJobsAPI } from 'api';
WorkflowApprovalsAPI,
WorkflowJobTemplatesAPI,
WorkflowJobsAPI,
} from 'api';
import useRequest, { useDismissableError } from 'hooks/useRequest'; import useRequest, { useDismissableError } from 'hooks/useRequest';
import { WorkflowApproval } from 'types'; import { WorkflowApproval } from 'types';
import StatusLabel from 'components/StatusLabel'; import StatusLabel from 'components/StatusLabel';
@@ -67,8 +63,10 @@ function WorkflowApprovalDetail({ workflowApproval, fetchWorkflowApproval }) {
const { error: deleteError, dismissError: dismissDeleteError } = const { error: deleteError, dismissError: dismissDeleteError } =
useDismissableError(deleteApprovalError); useDismissableError(deleteApprovalError);
const workflowJobTemplateId = const sourceWorkflowJob =
workflowApproval.summary_fields.workflow_job_template.id; workflowApproval?.summary_fields?.source_workflow_job;
const sourceWorkflowJobTemplate =
workflowApproval?.summary_fields?.workflow_job_template;
const { const {
error: fetchWorkflowJobError, error: fetchWorkflowJobError,
@@ -77,23 +75,10 @@ function WorkflowApprovalDetail({ workflowApproval, fetchWorkflowApproval }) {
result: workflowJob, result: workflowJob,
} = useRequest( } = useRequest(
useCallback(async () => { useCallback(async () => {
if (!workflowJobTemplateId) { if (!sourceWorkflowJob?.id) return {};
return {}; const { data } = await WorkflowJobsAPI.readDetail(sourceWorkflowJob?.id);
}
const { data: workflowJobTemplate } =
await WorkflowJobTemplatesAPI.readDetail(workflowJobTemplateId);
let jobId = null;
if (workflowJobTemplate.summary_fields?.current_job) {
jobId = workflowJobTemplate.summary_fields.current_job.id;
} else if (workflowJobTemplate.summary_fields?.last_job) {
jobId = workflowJobTemplate.summary_fields.last_job.id;
}
const { data } = await WorkflowJobsAPI.readDetail(jobId);
return data; return data;
}, [workflowJobTemplateId]), }, [sourceWorkflowJob?.id]),
{ {
workflowJob: null, workflowJob: null,
isLoading: true, isLoading: true,
@@ -116,11 +101,6 @@ function WorkflowApprovalDetail({ workflowApproval, fetchWorkflowApproval }) {
}, },
[addToast, fetchWorkflowApproval] [addToast, fetchWorkflowApproval]
); );
const sourceWorkflowJob =
workflowApproval?.summary_fields?.source_workflow_job;
const sourceWorkflowJobTemplate =
workflowApproval?.summary_fields?.workflow_job_template;
const isLoading = isDeleteLoading || isLoadingWorkflowJob; const isLoading = isDeleteLoading || isLoadingWorkflowJob;

View File

@@ -1,10 +1,6 @@
import React from 'react'; import React from 'react';
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import { import { WorkflowApprovalsAPI, WorkflowJobsAPI } from 'api';
WorkflowApprovalsAPI,
WorkflowJobTemplatesAPI,
WorkflowJobsAPI,
} from 'api';
import { formatDateString } from 'util/dates'; import { formatDateString } from 'util/dates';
import { import {
mountWithContexts, mountWithContexts,
@@ -23,146 +19,6 @@ jest.mock('react-router-dom', () => ({
}), }),
})); }));
const workflowJobTemplate = {
id: 8,
type: 'workflow_job_template',
url: '/api/v2/workflow_job_templates/8/',
related: {
named_url: '/api/v2/workflow_job_templates/00++/',
created_by: '/api/v2/users/1/',
modified_by: '/api/v2/users/1/',
last_job: '/api/v2/workflow_jobs/111/',
workflow_jobs: '/api/v2/workflow_job_templates/8/workflow_jobs/',
schedules: '/api/v2/workflow_job_templates/8/schedules/',
launch: '/api/v2/workflow_job_templates/8/launch/',
webhook_key: '/api/v2/workflow_job_templates/8/webhook_key/',
webhook_receiver: '/api/v2/workflow_job_templates/8/github/',
workflow_nodes: '/api/v2/workflow_job_templates/8/workflow_nodes/',
labels: '/api/v2/workflow_job_templates/8/labels/',
activity_stream: '/api/v2/workflow_job_templates/8/activity_stream/',
notification_templates_started:
'/api/v2/workflow_job_templates/8/notification_templates_started/',
notification_templates_success:
'/api/v2/workflow_job_templates/8/notification_templates_success/',
notification_templates_error:
'/api/v2/workflow_job_templates/8/notification_templates_error/',
notification_templates_approvals:
'/api/v2/workflow_job_templates/8/notification_templates_approvals/',
access_list: '/api/v2/workflow_job_templates/8/access_list/',
object_roles: '/api/v2/workflow_job_templates/8/object_roles/',
survey_spec: '/api/v2/workflow_job_templates/8/survey_spec/',
copy: '/api/v2/workflow_job_templates/8/copy/',
},
summary_fields: {
last_job: {
id: 111,
name: '00',
description: '',
finished: '2022-05-10T17:29:52.978531Z',
status: 'successful',
failed: false,
},
last_update: {
id: 111,
name: '00',
description: '',
status: 'successful',
failed: false,
},
created_by: {
id: 1,
username: 'admin',
first_name: '',
last_name: '',
},
modified_by: {
id: 1,
username: 'admin',
first_name: '',
last_name: '',
},
object_roles: {
admin_role: {
description: 'Can manage all aspects of the workflow job template',
name: 'Admin',
id: 34,
},
execute_role: {
description: 'May run the workflow job template',
name: 'Execute',
id: 35,
},
read_role: {
description: 'May view settings for the workflow job template',
name: 'Read',
id: 36,
},
approval_role: {
description: 'Can approve or deny a workflow approval node',
name: 'Approve',
id: 37,
},
},
user_capabilities: {
edit: true,
delete: true,
start: true,
schedule: true,
copy: true,
},
labels: {
count: 1,
results: [
{
id: 2,
name: 'Test2',
},
],
},
survey: {
title: '',
description: '',
},
recent_jobs: [
{
id: 111,
status: 'successful',
finished: '2022-05-10T17:29:52.978531Z',
canceled_on: null,
type: 'workflow_job',
},
{
id: 104,
status: 'failed',
finished: '2022-05-10T15:26:22.233170Z',
canceled_on: null,
type: 'workflow_job',
},
],
},
created: '2022-05-05T14:13:36.123027Z',
modified: '2022-05-05T17:44:44.071447Z',
name: '00',
description: '',
last_job_run: '2022-05-10T17:29:52.978531Z',
last_job_failed: false,
next_job_run: null,
status: 'successful',
extra_vars: '{\n "foo": "bar",\n "baz": "qux"\n}',
organization: null,
survey_enabled: true,
allow_simultaneous: true,
ask_variables_on_launch: true,
inventory: null,
limit: null,
scm_branch: '',
ask_inventory_on_launch: true,
ask_scm_branch_on_launch: true,
ask_limit_on_launch: true,
webhook_service: 'github',
webhook_credential: null,
};
const workflowJob = { const workflowJob = {
id: 111, id: 111,
type: 'workflow_job', type: 'workflow_job',
@@ -270,9 +126,6 @@ const workflowJob = {
describe('<WorkflowApprovalDetail />', () => { describe('<WorkflowApprovalDetail />', () => {
beforeEach(() => { beforeEach(() => {
WorkflowJobTemplatesAPI.readDetail.mockResolvedValue({
data: workflowJobTemplate,
});
WorkflowJobsAPI.readDetail.mockResolvedValue({ data: workflowJob }); WorkflowJobsAPI.readDetail.mockResolvedValue({ data: workflowJob });
}); });
@@ -482,9 +335,6 @@ describe('<WorkflowApprovalDetail />', () => {
}); });
test('should not load Labels', async () => { test('should not load Labels', async () => {
WorkflowJobTemplatesAPI.readDetail.mockResolvedValue({
data: workflowJobTemplate,
});
WorkflowJobsAPI.readDetail.mockResolvedValue({ WorkflowJobsAPI.readDetail.mockResolvedValue({
data: { data: {
...workflowApproval, ...workflowApproval,
@@ -621,4 +471,16 @@ describe('<WorkflowApprovalDetail />', () => {
(el) => el.length === 0 (el) => el.length === 0
); );
}); });
test('should fetch its workflow job details', async () => {
let wrapper;
await act(async () => {
wrapper = mountWithContexts(
<WorkflowApprovalDetail workflowApproval={workflowApproval} />
);
});
waitForElement(wrapper, 'WorkflowApprovalDetail', (el) => el.length > 0);
expect(WorkflowJobsAPI.readDetail).toHaveBeenCalledTimes(1);
expect(WorkflowJobsAPI.readDetail).toHaveBeenCalledWith(216);
});
}); });

View File

@@ -10,7 +10,7 @@ from awx.main.models import WorkflowJob
@pytest.mark.django_db @pytest.mark.django_db
def test_bulk_job_launch(run_module, admin_user, job_template): def test_bulk_job_launch(run_module, admin_user, job_template):
jobs = [dict(unified_job_template=job_template.id)] jobs = [dict(unified_job_template=job_template.id)]
run_module( result = run_module(
'bulk_job_launch', 'bulk_job_launch',
{ {
'name': "foo-bulk-job", 'name': "foo-bulk-job",
@@ -21,6 +21,8 @@ def test_bulk_job_launch(run_module, admin_user, job_template):
}, },
admin_user, admin_user,
) )
assert not result.get('failed', False), result.get('msg', result)
assert result.get('changed'), result
bulk_job = WorkflowJob.objects.get(name="foo-bulk-job") bulk_job = WorkflowJob.objects.get(name="foo-bulk-job")
assert bulk_job.extra_vars == '{"animal": "owl"}' assert bulk_job.extra_vars == '{"animal": "owl"}'
@@ -30,7 +32,7 @@ def test_bulk_job_launch(run_module, admin_user, job_template):
@pytest.mark.django_db @pytest.mark.django_db
def test_bulk_host_create(run_module, admin_user, inventory): def test_bulk_host_create(run_module, admin_user, inventory):
hosts = [dict(name="127.0.0.1"), dict(name="foo.dns.org")] hosts = [dict(name="127.0.0.1"), dict(name="foo.dns.org")]
run_module( result = run_module(
'bulk_host_create', 'bulk_host_create',
{ {
'inventory': inventory.name, 'inventory': inventory.name,
@@ -38,6 +40,8 @@ def test_bulk_host_create(run_module, admin_user, inventory):
}, },
admin_user, admin_user,
) )
assert not result.get('failed', False), result.get('msg', result)
assert result.get('changed'), result
resp_hosts = inventory.hosts.all().values_list('name', flat=True) resp_hosts = inventory.hosts.all().values_list('name', flat=True)
for h in hosts: for h in hosts:
assert h['name'] in resp_hosts assert h['name'] in resp_hosts

View File

@@ -46,27 +46,24 @@
that: that:
- "command is changed" - "command is changed"
- name: Timeout waiting for the command to cancel - name: Cancel the command
ad_hoc_command_cancel: ad_hoc_command_cancel:
command_id: "{{ command.id }}" command_id: "{{ command.id }}"
timeout: -1
register: results register: results
ignore_errors: true ignore_errors: true
- assert: - assert:
that: that:
- results is failed - results is changed
- "results['msg'] == 'Monitoring of ad hoc command aborted due to timeout'"
- block: - name: "Wait for up to a minute until the job enters the can_cancel: False state"
- name: "Wait for up to a minute until the job enters the can_cancel: False state" debug:
debug: msg: "The job can_cancel status has transitioned into False, we can proceed with testing"
msg: "The job can_cancel status has transitioned into False, we can proceed with testing" until: not job_status
until: not job_status retries: 6
retries: 6 delay: 10
delay: 10 vars:
vars: job_status: "{{ lookup('awx.awx.controller_api', 'ad_hoc_commands/'+ command.id | string +'/cancel')['can_cancel'] }}"
job_status: "{{ lookup('awx.awx.controller_api', 'ad_hoc_commands/'+ command.id | string +'/cancel')['can_cancel'] }}"
- name: Cancel the command with hard error if it's not running - name: Cancel the command with hard error if it's not running
ad_hoc_command_cancel: ad_hoc_command_cancel:

View File

@@ -108,9 +108,8 @@
- assert: - assert:
that: that:
- wait_results is failed - wait_results is successful
- 'wait_results.status == "canceled"' - 'wait_results.status == "successful"'
- "wait_results.msg == 'The ad hoc command - {{ command.id }}, failed'"
- name: Delete the Credential - name: Delete the Credential
credential: credential:

View File

@@ -2,6 +2,7 @@
- name: Generate a test id - name: Generate a test id
set_fact: set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
when: test_id is not defined
- name: Generate names - name: Generate names
set_fact: set_fact:

View File

@@ -1,7 +1,12 @@
--- ---
- name: Generate a test ID
set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
when: test_id is not defined
- name: Generate names - name: Generate names
set_fact: set_fact:
cred_type_name: "AWX-Collection-tests-credential_type-cred-type-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" cred_type_name: "AWX-Collection-tests-credential_type-cred-type-{{ test_id }}"
- block: - block:
- name: Add Tower credential type - name: Add Tower credential type

View File

@@ -1,13 +1,18 @@
--- ---
- name: Generate a test ID
set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
when: test_id is not defined
- name: Generate names - name: Generate names
set_fact: set_fact:
group_name1: "AWX-Collection-tests-group-group-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" group_name1: "AWX-Collection-tests-group-group1-{{ test_id }}"
group_name2: "AWX-Collection-tests-group-group-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" group_name2: "AWX-Collection-tests-group-group2-{{ test_id }}"
group_name3: "AWX-Collection-tests-group-group-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" group_name3: "AWX-Collection-tests-group-group3-{{ test_id }}"
inv_name: "AWX-Collection-tests-group-inv-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" inv_name: "AWX-Collection-tests-group-inv-{{ test_id }}"
host_name1: "AWX-Collection-tests-group-host-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" host_name1: "AWX-Collection-tests-group-host1-{{ test_id }}"
host_name2: "AWX-Collection-tests-group-host-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" host_name2: "AWX-Collection-tests-group-host2-{{ test_id }}"
host_name3: "AWX-Collection-tests-group-host-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" host_name3: "AWX-Collection-tests-group-host3-{{ test_id }}"
- name: Create an Inventory - name: Create an Inventory
inventory: inventory:
@@ -117,9 +122,10 @@
state: absent state: absent
register: result register: result
# In this case, group 2 was last a child of group1 so deleting group1 deleted group2
- assert: - assert:
that: that:
- "result is changed" - "result is not changed"
- name: Delete a Group - name: Delete a Group
group: group:

View File

@@ -1,8 +1,13 @@
--- ---
- name: Generate a test ID
set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
when: test_id is not defined
- name: Generate names - name: Generate names
set_fact: set_fact:
host_name: "AWX-Collection-tests-host-host-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" host_name: "AWX-Collection-tests-host-host-{{ test_id }}"
inv_name: "AWX-Collection-tests-host-inv-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" inv_name: "AWX-Collection-tests-host-inv-{{ test_id }}"
- name: Create an Inventory - name: Create an Inventory
inventory: inventory:

View File

@@ -1,14 +1,25 @@
--- ---
- name: Generate a test ID
set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
when: test_id is not defined
- name: Generate hostnames - name: Generate hostnames
set_fact: set_fact:
hostname1: "AWX-Collection-tests-instance1.{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}.example.com" hostname1: "AWX-Collection-tests-instance1.{{ test_id }}.example.com"
hostname2: "AWX-Collection-tests-instance2.{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}.example.com" hostname2: "AWX-Collection-tests-instance2.{{ test_id }}.example.com"
hostname3: "AWX-Collection-tests-instance3.{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}.example.com" hostname3: "AWX-Collection-tests-instance3.{{ test_id }}.example.com"
register: facts register: facts
- name: Show hostnames - name: Get the k8s setting
debug: set_fact:
var: facts IS_K8S: "{{ controller_settings['IS_K8S'] | default(False) }}"
vars:
controller_settings: "{{ lookup('awx.awx.controller_api', 'settings/all') }}"
- debug:
msg: "Skipping instance test since this is instance is not running on a K8s platform"
when: not IS_K8S
- block: - block:
- name: Create an instance - name: Create an instance
@@ -57,3 +68,5 @@
- "{{ hostname1 }}" - "{{ hostname1 }}"
- "{{ hostname2 }}" - "{{ hostname2 }}"
- "{{ hostname3 }}" - "{{ hostname3 }}"
when: IS_K8S

View File

@@ -2,6 +2,7 @@
- name: Generate test id - name: Generate test id
set_fact: set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
when: test_id is not defined
- name: Generate names - name: Generate names
set_fact: set_fact:

View File

@@ -2,6 +2,7 @@
- name: Generate a test ID - name: Generate a test ID
set_fact: set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
when: test_id is not defined
- name: Generate names - name: Generate names
set_fact: set_fact:

View File

@@ -1,9 +1,14 @@
--- ---
- name: Generate a test ID
set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
when: test_id is not defined
- name: Generate names - name: Generate names
set_fact: set_fact:
openstack_cred: "AWX-Collection-tests-inventory_source-cred-openstack-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" openstack_cred: "AWX-Collection-tests-inventory_source-cred-openstack-{{ test_id }}"
openstack_inv: "AWX-Collection-tests-inventory_source-inv-openstack-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" openstack_inv: "AWX-Collection-tests-inventory_source-inv-openstack-{{ test_id }}"
openstack_inv_source: "AWX-Collection-tests-inventory_source-inv-source-openstack-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" openstack_inv_source: "AWX-Collection-tests-inventory_source-inv-source-openstack-{{ test_id }}"
- name: Add a credential - name: Add a credential
credential: credential:

View File

@@ -2,6 +2,7 @@
- name: Generate a test ID - name: Generate a test ID
set_fact: set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
when: test_id is not defined
- name: Generate names - name: Generate names
set_fact: set_fact:

View File

@@ -1,9 +1,14 @@
--- ---
- name: Generate a test ID
set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
when: test_id is not defined
- name: Generate names - name: Generate names
set_fact: set_fact:
jt_name1: "AWX-Collection-tests-job_launch-jt1-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" jt_name1: "AWX-Collection-tests-job_launch-jt1-{{ test_id }}"
jt_name2: "AWX-Collection-tests-job_launch-jt2-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" jt_name2: "AWX-Collection-tests-job_launch-jt2-{{ test_id }}"
proj_name: "AWX-Collection-tests-job_launch-project-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" proj_name: "AWX-Collection-tests-job_launch-project-{{ test_id }}"
- name: Launch a Job Template - name: Launch a Job Template
job_launch: job_launch:

View File

@@ -2,6 +2,7 @@
- name: Generate a random string for test - name: Generate a random string for test
set_fact: set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
when: test_id is not defined
- name: generate random string for project - name: generate random string for project
set_fact: set_fact:

View File

@@ -1,8 +1,13 @@
--- ---
- name: Generate a test ID
set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
when: test_id is not defined
- name: Generate random string for template and project - name: Generate random string for template and project
set_fact: set_fact:
jt_name: "AWX-Collection-tests-job_wait-long_running-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" jt_name: "AWX-Collection-tests-job_wait-long_running-{{ test_id }}"
proj_name: "AWX-Collection-tests-job_wait-long_running-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" proj_name: "AWX-Collection-tests-job_wait-long_running-{{ test_id }}"
- name: Assure that the demo project exists - name: Assure that the demo project exists
project: project:

View File

@@ -1,7 +1,12 @@
--- ---
- name: Generate a test ID
set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
when: test_id is not defined
- name: Generate names - name: Generate names
set_fact: set_fact:
label_name: "AWX-Collection-tests-label-label-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" label_name: "AWX-Collection-tests-label-label-{{ test_id }}"
- name: Create a Label - name: Create a Label
label: label:

View File

@@ -101,6 +101,9 @@
set_fact: set_fact:
users: "{{ query(plugin_name, 'users', query_params={ 'username__endswith': test_id, 'page_size': 2 }, return_ids=True ) }}" users: "{{ query(plugin_name, 'users', query_params={ 'username__endswith': test_id, 'page_size': 2 }, return_ids=True ) }}"
- debug:
msg: "{{ users }}"
- name: Assert that user list has 2 ids only and that they are strings, not ints - name: Assert that user list has 2 ids only and that they are strings, not ints
assert: assert:
that: that:

View File

@@ -1,12 +1,17 @@
--- ---
- name: Generate a test ID
set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
when: test_id is not defined
- name: Generate names - name: Generate names
set_fact: set_fact:
slack_not: "AWX-Collection-tests-notification_template-slack-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" slack_not: "AWX-Collection-tests-notification_template-slack-not-{{ test_id }}"
webhook_not: "AWX-Collection-tests-notification_template-wehbook-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" webhook_not: "AWX-Collection-tests-notification_template-wehbook-not-{{ test_id }}"
email_not: "AWX-Collection-tests-notification_template-email-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" email_not: "AWX-Collection-tests-notification_template-email-not-{{ test_id }}"
twillo_not: "AWX-Collection-tests-notification_template-twillo-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" twillo_not: "AWX-Collection-tests-notification_template-twillo-not-{{ test_id }}"
pd_not: "AWX-Collection-tests-notification_template-pd-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" pd_not: "AWX-Collection-tests-notification_template-pd-not-{{ test_id }}"
irc_not: "AWX-Collection-tests-notification_template-irc-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" irc_not: "AWX-Collection-tests-notification_template-irc-not-{{ test_id }}"
- name: Create Slack notification with custom messages - name: Create Slack notification with custom messages
notification_template: notification_template:

View File

@@ -2,6 +2,7 @@
- name: Generate a test ID - name: Generate a test ID
set_fact: set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
when: test_id is not defined
- name: Generate an org name - name: Generate an org name
set_fact: set_fact:

View File

@@ -1,13 +1,18 @@
--- ---
- name: Generate a test ID
set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
when: test_id is not defined
- name: Generate names - name: Generate names
set_fact: set_fact:
project_name1: "AWX-Collection-tests-project-project1-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" project_name1: "AWX-Collection-tests-project-project1-{{ test_id }}"
project_name2: "AWX-Collection-tests-project-project2-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" project_name2: "AWX-Collection-tests-project-project2-{{ test_id }}"
project_name3: "AWX-Collection-tests-project-project3-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" project_name3: "AWX-Collection-tests-project-project3-{{ test_id }}"
jt1: "AWX-Collection-tests-project-jt1-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" jt1: "AWX-Collection-tests-project-jt1-{{ test_id }}"
scm_cred_name: "AWX-Collection-tests-project-scm-cred-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" scm_cred_name: "AWX-Collection-tests-project-scm-cred-{{ test_id }}"
org_name: "AWX-Collection-tests-project-org-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" org_name: "AWX-Collection-tests-project-org-{{ test_id }}"
cred_name: "AWX-Collection-tests-project-cred-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" cred_name: "AWX-Collection-tests-project-cred-{{ test_id }}"
- block: - block:
- name: Create an SCM Credential - name: Create an SCM Credential

View File

@@ -1,56 +0,0 @@
---
- name: Load the UI settings
set_fact:
project_base_dir: "{{ controller_settings.project_base_dir }}"
vars:
controller_settings: "{{ lookup('awx.awx.controller_api', 'config/') }}"
- inventory:
name: localhost
organization: Default
- host:
name: localhost
inventory: localhost
variables:
ansible_connection: local
- name: Create an unused SSH / Machine credential
credential:
name: dummy
credential_type: Machine
inputs:
ssh_key_data: |
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIIUl6R1xgzR6siIUArz4XBPtGZ09aetma2eWf1v3uYymoAoGCCqGSM49
AwEHoUQDQgAENJNjgeZDAh/+BY860s0yqrLDprXJflY0GvHIr7lX3ieCtrzOMCVU
QWzw35pc5tvuP34SSi0ZE1E+7cVMDDOF3w==
-----END EC PRIVATE KEY-----
organization: Default
- block:
- name: Add a path to a setting
settings:
name: AWX_ISOLATION_SHOW_PATHS
value: "[{{ project_base_dir }}]"
- name: Create a directory for manual project
ad_hoc_command:
credential: dummy
inventory: localhost
job_type: run
module_args: "mkdir -p {{ project_base_dir }}/{{ project_dir_name }}"
module_name: command
wait: true
always:
- name: Delete path from setting
settings:
name: AWX_ISOLATION_SHOW_PATHS
value: []
- name: Delete dummy credential
credential:
name: dummy
credential_type: Machine
state: absent

View File

@@ -1,38 +0,0 @@
---
- name: Generate random string for project
set_fact:
rand_string: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
- name: Generate manual project name
set_fact:
project_name: "Manual_Project_{{ rand_string }}"
- name: Generate manual project dir name
set_fact:
project_dir_name: "proj_{{ rand_string }}"
- name: Create a project directory for manual project
import_tasks: create_project_dir.yml
- name: Create a manual project
project:
name: "{{ project_name }}"
organization: Default
scm_type: manual
local_path: "{{ project_dir_name }}"
register: result
- assert:
that:
- "result is changed"
- name: Delete a manual project
project:
name: "{{ project_name }}"
organization: Default
state: absent
register: result
- assert:
that:
- "result is changed"

View File

@@ -2,6 +2,7 @@
- name: Generate a test id - name: Generate a test id
set_fact: set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
when: test_id is not defined
- name: Generate names - name: Generate names
set_fact: set_fact:

View File

@@ -2,6 +2,7 @@
- name: Generate a random string for test - name: Generate a random string for test
set_fact: set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
when: test_id is not defined
- name: generate random string for schedule - name: generate random string for schedule
set_fact: set_fact:

View File

@@ -1,7 +1,12 @@
--- ---
- name: Generate a test ID
set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
when: test_id is not defined
- name: Generate names - name: Generate names
set_fact: set_fact:
team_name: "AWX-Collection-tests-team-team-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" team_name: "AWX-Collection-tests-team-team-{{ test_id }}"
- name: Attempt to add a team to a non-existant Organization - name: Attempt to add a team to a non-existant Organization
team: team:

View File

@@ -1,7 +1,12 @@
--- ---
- name: Generate a test ID
set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
when: test_id is not defined
- name: Generate names - name: Generate names
set_fact: set_fact:
token_description: "AWX-Collection-tests-token-description-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" token_description: "AWX-Collection-tests-token-description-{{ test_id }}"
- name: Try to use a token as a dict which is missing the token parameter - name: Try to use a token as a dict which is missing the token parameter
job_list: job_list:

View File

@@ -1,7 +1,12 @@
--- ---
- name: Generate a test ID
set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
when: test_id is not defined
- name: Generate names - name: Generate names
set_fact: set_fact:
username: "AWX-Collection-tests-user-user-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" username: "AWX-Collection-tests-user-user-{{ test_id }}"
- name: Create a User - name: Create a User
user: user:
@@ -131,10 +136,6 @@
'Can not verify ssl with non-https protocol' in result.exception" 'Can not verify ssl with non-https protocol' in result.exception"
- block: - block:
- name: Generate a test ID
set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
- name: Generate an org name - name: Generate an org name
set_fact: set_fact:
org_name: "AWX-Collection-tests-organization-org-{{ test_id }}" org_name: "AWX-Collection-tests-organization-org-{{ test_id }}"

View File

@@ -2,13 +2,15 @@
- name: Generate a random string for names - name: Generate a random string for names
set_fact: set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
test_prefix: AWX-Collection-tests-workflow_approval when: test_id is not defined
- name: Generate random names for test objects - name: Generate random names for test objects
set_fact: set_fact:
org_name: "{{ test_prefix }}-org-{{ test_id }}" org_name: "{{ test_prefix }}-org-{{ test_id }}"
approval_node_name: "{{ test_prefix }}-node-{{ test_id }}" approval_node_name: "{{ test_prefix }}-node-{{ test_id }}"
wfjt_name: "{{ test_prefix }}-wfjt-{{ test_id }}" wfjt_name: "{{ test_prefix }}-wfjt-{{ test_id }}"
vars:
test_prefix: AWX-Collection-tests-workflow_approval
- block: - block:
- name: Create a new organization for test isolation - name: Create a new organization for test isolation

View File

@@ -2,6 +2,7 @@
- name: Generate a random string for names - name: Generate a random string for names
set_fact: set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
when: test_id is not defined
- name: Generate random names for test objects - name: Generate random names for test objects
set_fact: set_fact:
@@ -17,8 +18,8 @@
webhook_wfjt_name: "AWX-Collection-tests-workflow_job_template-webhook-wfjt-{{ test_id }}" webhook_wfjt_name: "AWX-Collection-tests-workflow_job_template-webhook-wfjt-{{ test_id }}"
email_not: "AWX-Collection-tests-job_template-email-not-{{ test_id }}" email_not: "AWX-Collection-tests-job_template-email-not-{{ test_id }}"
webhook_notification: "AWX-Collection-tests-notification_template-wehbook-not-{{ test_id }}" webhook_notification: "AWX-Collection-tests-notification_template-wehbook-not-{{ test_id }}"
project_inv: "AWX-Collection-tests-inventory_source-inv-project-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" project_inv: "AWX-Collection-tests-inventory_source-inv-project-{{ test_id }}"
project_inv_source: "AWX-Collection-tests-inventory_source-inv-source-project-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" project_inv_source: "AWX-Collection-tests-inventory_source-inv-source-project-{{ test_id }}"
github_webhook_credential_name: "AWX-Collection-tests-credential-webhook-{{ test_id }}_github" github_webhook_credential_name: "AWX-Collection-tests-credential-webhook-{{ test_id }}_github"
ee1: "AWX-Collection-tests-workflow_job_template-ee1-{{ test_id }}" ee1: "AWX-Collection-tests-workflow_job_template-ee1-{{ test_id }}"
label1: "AWX-Collection-tests-workflow_job_template-l1-{{ test_id }}" label1: "AWX-Collection-tests-workflow_job_template-l1-{{ test_id }}"

View File

@@ -12,8 +12,13 @@ To encrypt secret fields, AWX uses AES in CBC mode with a 256-bit key for encryp
If necessary, credentials and encrypted settings can be extracted using the AWX shell: If necessary, credentials and encrypted settings can be extracted using the AWX shell:
```python ```python
# awx-manage shell_plus $ awx-manage shell_plus
>>> from awx.main.utils import decrypt_field >>> from awx.main.utils import decrypt_field
>>> cred = Credential.objects.get(name="my private key") >>> print(decrypt_field(Credential.objects.get(name="my private key"), "ssh_key_data")) # Example for a credential
>>> print(decrypt_field(cred, "ssh_key_data")) >>> print(decrypt_field(Setting.objects.get(key='SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET'), 'value')) # Example for a setting
```
If you are running a kubernetes based deployment, you can execute awx-manage like this:
```bash
$ kubectl exec --stdin --tty [instance name]-task-[...] -c [instance name]-task -- awx-manage shell_plus
``` ```

View File

@@ -1,5 +1,5 @@
[pytest] [pytest]
DJANGO_SETTINGS_MODULE = awx.settings.development DJANGO_SETTINGS_MODULE = awx.main.tests.settings_for_test
python_paths = /var/lib/awx/venv/tower/lib/python3.8/site-packages python_paths = /var/lib/awx/venv/tower/lib/python3.8/site-packages
site_dirs = /var/lib/awx/venv/tower/lib/python3.8/site-packages site_dirs = /var/lib/awx/venv/tower/lib/python3.8/site-packages
python_files = *.py python_files = *.py

View File

@@ -4,8 +4,7 @@
gather_facts: true gather_facts: true
tasks: tasks:
- name: Get version from SCM if not explicitly provided - name: Get version from SCM if not explicitly provided
shell: | command: make version-for-buildyml
make print-VERSION | cut -d + -f -1
args: args:
chdir: '../../' chdir: '../../'
register: scm_version register: scm_version

View File

@@ -232,7 +232,7 @@ ADD {{ template_dest }}/supervisor_rsyslog.conf /etc/supervisord_rsyslog.conf
{% endif %} {% endif %}
{% if (build_dev|bool) or (kube_dev|bool) %} {% if (build_dev|bool) or (kube_dev|bool) %}
ADD tools/docker-compose/awx.egg-link /tmp/awx.egg-link RUN echo /awx_devel > /var/lib/awx/venv/awx/lib/python3.9/site-packages/awx.egg-link
ADD tools/docker-compose/awx-manage /usr/local/bin/awx-manage ADD tools/docker-compose/awx-manage /usr/local/bin/awx-manage
ADD tools/scripts/awx-python /usr/bin/awx-python ADD tools/scripts/awx-python /usr/bin/awx-python
{% endif %} {% endif %}
@@ -285,8 +285,7 @@ RUN for dir in \
/var/lib/shared/overlay-layers/layers.lock \ /var/lib/shared/overlay-layers/layers.lock \
/var/lib/shared/vfs-images/images.lock \ /var/lib/shared/vfs-images/images.lock \
/var/lib/shared/vfs-layers/layers.lock \ /var/lib/shared/vfs-layers/layers.lock \
/var/run/nginx.pid \ /var/run/nginx.pid; \
/var/lib/awx/venv/awx/lib/python3.9/site-packages/awx.egg-link ; \
do touch $file ; chmod g+rw $file ; done && \ do touch $file ; chmod g+rw $file ; done && \
echo "\setenv PAGER 'less -SXF'" > /var/lib/awx/.psqlrc echo "\setenv PAGER 'less -SXF'" > /var/lib/awx/.psqlrc
{% endif %} {% endif %}

View File

@@ -1,8 +1,7 @@
# Copyright (c) 2015 Ansible, Inc. (formerly AnsibleWorks, Inc.) # Copyright (c) 2015 Ansible, Inc. (formerly AnsibleWorks, Inc.)
# All Rights Reserved. # All Rights Reserved.
# Local Django settings for AWX project. Rename to "local_settings.py" and # Local Django settings for AWX project.
# edit as needed for your development environment.
# All variables defined in awx/settings/development.py will already be loaded # All variables defined in awx/settings/development.py will already be loaded
# into the global namespace before this file is loaded, to allow for reading # into the global namespace before this file is loaded, to allow for reading

View File

@@ -1 +0,0 @@
/awx_devel

View File

@@ -5,8 +5,6 @@ cd /awx_devel
make clean make clean
make awx-link make awx-link
cp tools/docker-compose/ansible/roles/sources/files/local_settings.py awx/settings/local_settings.py
if [[ ! $@ ]]; then if [[ ! $@ ]]; then
make test make test
else else

View File

@@ -4,7 +4,7 @@ minfds = 4096
nodaemon=true nodaemon=true
[program:awx-dispatcher] [program:awx-dispatcher]
command = make dispatcher command = awx-manage run_dispatcher
autorestart = true autorestart = true
stopasgroup=true stopasgroup=true
killasgroup=true killasgroup=true
@@ -12,7 +12,7 @@ stdout_events_enabled = true
stderr_events_enabled = true stderr_events_enabled = true
[program:awx-receiver] [program:awx-receiver]
command = make receiver command = awx-manage run_callback_receiver
autorestart = true autorestart = true
stopasgroup=true stopasgroup=true
killasgroup=true killasgroup=true
@@ -20,7 +20,7 @@ stdout_events_enabled = true
stderr_events_enabled = true stderr_events_enabled = true
[program:awx-wsrelay] [program:awx-wsrelay]
command = make run-wsrelay command = awx-manage run_wsrelay
autorestart = true autorestart = true
autorestart = true autorestart = true
stopasgroup=true stopasgroup=true
@@ -29,7 +29,7 @@ stdout_events_enabled = true
stderr_events_enabled = true stderr_events_enabled = true
[program:awx-heartbeet] [program:awx-heartbeet]
command = make run-heartbeet command = awx-manage run_heartbeet
autorestart = true autorestart = true
autorestart = true autorestart = true
stopasgroup=true stopasgroup=true
@@ -38,7 +38,7 @@ stdout_events_enabled = true
stderr_events_enabled = true stderr_events_enabled = true
[program:awx-rsyslog-configurer] [program:awx-rsyslog-configurer]
command = make run-rsyslog-configurer command = awx-manage run_rsyslog_configurer
autorestart = true autorestart = true
stopasgroup=true stopasgroup=true
killasgroup=true killasgroup=true
@@ -48,7 +48,7 @@ stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0 stderr_logfile_maxbytes=0
[program:awx-cache-clear] [program:awx-cache-clear]
command = make run-cache-clear command = awx-manage run_cache_clear
autorestart = true autorestart = true
stopasgroup=true stopasgroup=true
killasgroup=true killasgroup=true