Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a8c96a5e6 | ||
|
|
a5d17539c6 | ||
|
|
a49d894cf1 | ||
|
|
b3466d4449 | ||
|
|
237adc6150 | ||
|
|
09b028ee3c | ||
|
|
fb83bfbc31 | ||
|
|
88e406e121 | ||
|
|
59d0bcc63f | ||
|
|
3fb3125bc3 | ||
|
|
d70c6b9474 | ||
|
|
5549516a37 | ||
|
|
14ac91a8a2 | ||
|
|
d5753818a0 | ||
|
|
33010a2e02 | ||
|
|
14454cc670 | ||
|
|
7ab2bca16e | ||
|
|
f0f655f2c3 | ||
|
|
4286d411a7 | ||
|
|
06ad32ed8e | ||
|
|
1ebff23232 | ||
|
|
700de14c76 | ||
|
|
8605e339df | ||
|
|
e50954ce40 | ||
|
|
7caca60308 | ||
|
|
f4e13af056 | ||
|
|
decdb56288 | ||
|
|
bcd4c2e8ef | ||
|
|
d663066ac5 | ||
|
|
1ceebb275c | ||
|
|
f78ba282a6 | ||
|
|
81d88df757 | ||
|
|
0bdb01a9e9 | ||
|
|
cd91fbf59f | ||
|
|
f240e640e5 | ||
|
|
46f489185e | ||
|
|
dbb80fb7e3 | ||
|
|
cb3d357ce1 | ||
|
|
dfa4db9266 | ||
|
|
6906a88dc9 | ||
|
|
1f7be9258c | ||
|
|
dcce024424 | ||
|
|
79d7179c72 | ||
|
|
4d80f886e0 | ||
|
|
5179333185 | ||
|
|
362e11aaf2 | ||
|
|
decff01fa4 | ||
|
|
a14cc8199d | ||
|
|
b6436826f6 | ||
|
|
2109b5039e | ||
|
|
b6f9b73418 | ||
|
|
40a8a3cb2f | ||
|
|
19f80c0a26 | ||
|
|
5d1bb2125e | ||
|
|
99c512bcef | ||
|
|
ed0329f5db | ||
|
|
dd53345397 | ||
|
|
f66cde51d7 | ||
|
|
d1c31687fc | ||
|
|
38424487f1 |
10
.github/actions/awx_devel_image/action.yml
vendored
@@ -11,6 +11,12 @@ runs:
|
||||
shell: bash
|
||||
run: echo py_version=`make PYTHON_VERSION` >> $GITHUB_ENV
|
||||
|
||||
- name: Set lower case owner name
|
||||
shell: bash
|
||||
run: echo "OWNER_LC=${OWNER,,}" >> $GITHUB_ENV
|
||||
env:
|
||||
OWNER: '${{ github.repository_owner }}'
|
||||
|
||||
- name: Log in to registry
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -18,11 +24,11 @@ runs:
|
||||
|
||||
- name: Pre-pull latest devel image to warm cache
|
||||
shell: bash
|
||||
run: docker pull ghcr.io/${{ github.repository_owner }}/awx_devel:${{ github.base_ref }}
|
||||
run: docker pull ghcr.io/${OWNER_LC}/awx_devel:${{ github.base_ref }}
|
||||
|
||||
- name: Build image for current source checkout
|
||||
shell: bash
|
||||
run: |
|
||||
DEV_DOCKER_TAG_BASE=ghcr.io/${{ github.repository_owner }} \
|
||||
DEV_DOCKER_TAG_BASE=ghcr.io/${OWNER_LC} \
|
||||
COMPOSE_TAG=${{ github.base_ref }} \
|
||||
make docker-compose-build
|
||||
|
||||
2
.github/actions/run_awx_devel/action.yml
vendored
@@ -35,7 +35,7 @@ runs:
|
||||
- name: Start AWX
|
||||
shell: bash
|
||||
run: |
|
||||
DEV_DOCKER_TAG_BASE=ghcr.io/${{ github.repository_owner }} \
|
||||
DEV_DOCKER_OWNER=${{ github.repository_owner }} \
|
||||
COMPOSE_TAG=${{ github.base_ref }} \
|
||||
COMPOSE_UP_OPTS="-d" \
|
||||
make docker-compose
|
||||
|
||||
3
.github/pr_labeler.yml
vendored
@@ -15,5 +15,4 @@
|
||||
|
||||
"dependencies":
|
||||
- any: ["awx/ui/package.json"]
|
||||
- any: ["requirements/*.txt"]
|
||||
- any: ["requirements/requirements.in"]
|
||||
- any: ["requirements/*"]
|
||||
|
||||
4
.github/workflows/ci.yml
vendored
@@ -127,10 +127,6 @@ jobs:
|
||||
|
||||
- name: Run sanity tests
|
||||
run: make test_collection_sanity
|
||||
env:
|
||||
# needed due to cgroupsv2. This is fixed, but a stable release
|
||||
# with the fix has not been made yet.
|
||||
ANSIBLE_TEST_PREFER_PODMAN: 1
|
||||
|
||||
collection-integration:
|
||||
name: awx_collection integration
|
||||
|
||||
63
.github/workflows/devel_images.yml
vendored
@@ -9,22 +9,43 @@ on:
|
||||
- release_*
|
||||
- feature_*
|
||||
jobs:
|
||||
push:
|
||||
if: endsWith(github.repository, '/awx') || startsWith(github.ref, 'refs/heads/release_')
|
||||
push-development-images:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
timeout-minutes: 120
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
build-targets:
|
||||
- image-name: awx_devel
|
||||
make-target: docker-compose-buildx
|
||||
- image-name: awx_kube_devel
|
||||
make-target: awx-kube-dev-buildx
|
||||
- image-name: awx
|
||||
make-target: awx-kube-buildx
|
||||
steps:
|
||||
|
||||
- name: Skipping build of awx image for non-awx repository
|
||||
run: |
|
||||
echo "Skipping build of awx image for non-awx repository"
|
||||
exit 0
|
||||
if: matrix.build-targets.image-name == 'awx' && !endsWith(github.repository, '/awx')
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Get python version from Makefile
|
||||
run: echo py_version=`make PYTHON_VERSION` >> $GITHUB_ENV
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set lower case owner name
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Set GITHUB_ENV variables
|
||||
run: |
|
||||
echo "OWNER_LC=${OWNER,,}" >>${GITHUB_ENV}
|
||||
echo "DEV_DOCKER_TAG_BASE=ghcr.io/${OWNER,,}" >> $GITHUB_ENV
|
||||
echo "COMPOSE_TAG=${GITHUB_REF##*/}" >> $GITHUB_ENV
|
||||
echo py_version=`make PYTHON_VERSION` >> $GITHUB_ENV
|
||||
env:
|
||||
OWNER: '${{ github.repository_owner }}'
|
||||
|
||||
@@ -37,23 +58,19 @@ jobs:
|
||||
run: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
|
||||
|
||||
- name: Pre-pull image to warm build cache
|
||||
run: |
|
||||
docker pull ghcr.io/${OWNER_LC}/awx_devel:${GITHUB_REF##*/} || :
|
||||
docker pull ghcr.io/${OWNER_LC}/awx_kube_devel:${GITHUB_REF##*/} || :
|
||||
docker pull ghcr.io/${OWNER_LC}/awx:${GITHUB_REF##*/} || :
|
||||
- name: Setup node and npm
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16.13.1'
|
||||
if: matrix.build-targets.image-name == 'awx'
|
||||
|
||||
- name: Build images
|
||||
- name: Prebuild UI for awx image (to speed up build process)
|
||||
run: |
|
||||
DEV_DOCKER_TAG_BASE=ghcr.io/${OWNER_LC} COMPOSE_TAG=${GITHUB_REF##*/} make docker-compose-build
|
||||
DEV_DOCKER_TAG_BASE=ghcr.io/${OWNER_LC} COMPOSE_TAG=${GITHUB_REF##*/} make awx-kube-dev-build
|
||||
DEV_DOCKER_TAG_BASE=ghcr.io/${OWNER_LC} COMPOSE_TAG=${GITHUB_REF##*/} make awx-kube-build
|
||||
sudo apt-get install gettext
|
||||
make ui-release
|
||||
make ui-next
|
||||
if: matrix.build-targets.image-name == 'awx'
|
||||
|
||||
- name: Push development images
|
||||
- name: Build and push AWX devel images
|
||||
run: |
|
||||
docker push ghcr.io/${OWNER_LC}/awx_devel:${GITHUB_REF##*/}
|
||||
docker push ghcr.io/${OWNER_LC}/awx_kube_devel:${GITHUB_REF##*/}
|
||||
|
||||
- name: Push AWX k8s image, only for upstream and feature branches
|
||||
run: docker push ghcr.io/${OWNER_LC}/awx:${GITHUB_REF##*/}
|
||||
if: endsWith(github.repository, '/awx')
|
||||
make ${{ matrix.build-targets.make-target }}
|
||||
|
||||
12
.github/workflows/feature_branch_deletion.yml
vendored
@@ -2,12 +2,10 @@
|
||||
name: Feature branch deletion cleanup
|
||||
env:
|
||||
LC_ALL: "C.UTF-8" # prevent ERROR: Ansible could not initialize the preferred locale: unsupported locale setting
|
||||
on:
|
||||
delete:
|
||||
branches:
|
||||
- feature_**
|
||||
on: delete
|
||||
jobs:
|
||||
push:
|
||||
branch_delete:
|
||||
if: ${{ github.event.ref_type == 'branch' && startsWith(github.event.ref, 'feature_') }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
@@ -22,6 +20,4 @@ jobs:
|
||||
run: |
|
||||
ansible localhost -c local, -m command -a "{{ ansible_python_interpreter + ' -m pip install boto3'}}"
|
||||
ansible localhost -c local -m aws_s3 \
|
||||
-a "bucket=awx-public-ci-files object=${GITHUB_REF##*/}/schema.json mode=delete permission=public-read"
|
||||
|
||||
|
||||
-a "bucket=awx-public-ci-files object=${GITHUB_REF##*/}/schema.json mode=delobj permission=public-read"
|
||||
|
||||
20
.github/workflows/promote.yml
vendored
@@ -83,11 +83,15 @@ jobs:
|
||||
|
||||
- name: Re-tag and promote awx image
|
||||
run: |
|
||||
docker pull ghcr.io/${{ github.repository }}:${{ github.event.release.tag_name }}
|
||||
docker tag ghcr.io/${{ github.repository }}:${{ github.event.release.tag_name }} quay.io/${{ github.repository }}:${{ github.event.release.tag_name }}
|
||||
docker tag ghcr.io/${{ github.repository }}:${{ github.event.release.tag_name }} quay.io/${{ github.repository }}:latest
|
||||
docker push quay.io/${{ github.repository }}:${{ github.event.release.tag_name }}
|
||||
docker push quay.io/${{ github.repository }}:latest
|
||||
docker pull ghcr.io/${{ github.repository_owner }}/awx-ee:${{ github.event.release.tag_name }}
|
||||
docker tag ghcr.io/${{ github.repository_owner }}/awx-ee:${{ github.event.release.tag_name }} quay.io/${{ github.repository_owner }}/awx-ee:${{ github.event.release.tag_name }}
|
||||
docker push quay.io/${{ github.repository_owner }}/awx-ee:${{ github.event.release.tag_name }}
|
||||
docker buildx imagetools create \
|
||||
ghcr.io/${{ github.repository }}:${{ github.event.release.tag_name }} \
|
||||
--tag quay.io/${{ github.repository }}:${{ github.event.release.tag_name }}
|
||||
docker buildx imagetools create \
|
||||
ghcr.io/${{ github.repository }}:${{ github.event.release.tag_name }} \
|
||||
--tag quay.io/${{ github.repository }}:latest
|
||||
|
||||
- name: Re-tag and promote awx-ee image
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
ghcr.io/${{ github.repository_owner }}/awx-ee:${{ github.event.release.tag_name }} \
|
||||
--tag quay.io/${{ github.repository_owner }}/awx-ee:${{ github.event.release.tag_name }}
|
||||
|
||||
34
.github/workflows/stage.yml
vendored
@@ -86,27 +86,33 @@ jobs:
|
||||
-e push=yes \
|
||||
-e awx_official=yes
|
||||
|
||||
- name: Log in to GHCR
|
||||
run: |
|
||||
echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
|
||||
- name: Log into registry ghcr.io
|
||||
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Log in to Quay
|
||||
run: |
|
||||
echo ${{ secrets.QUAY_TOKEN }} | docker login quay.io -u ${{ secrets.QUAY_USER }} --password-stdin
|
||||
- name: Log into registry quay.io
|
||||
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
|
||||
with:
|
||||
registry: quay.io
|
||||
username: ${{ secrets.QUAY_USER }}
|
||||
password: ${{ secrets.QUAY_TOKEN }}
|
||||
|
||||
- name: tag awx-ee:latest with version input
|
||||
run: |
|
||||
docker pull quay.io/ansible/awx-ee:latest
|
||||
docker tag quay.io/ansible/awx-ee:latest ghcr.io/${{ github.repository_owner }}/awx-ee:${{ github.event.inputs.version }}
|
||||
docker push ghcr.io/${{ github.repository_owner }}/awx-ee:${{ github.event.inputs.version }}
|
||||
docker buildx imagetools create \
|
||||
quay.io/ansible/awx-ee:latest \
|
||||
--tag ghcr.io/${{ github.repository_owner }}/awx-ee:${{ github.event.inputs.version }}
|
||||
|
||||
- name: Build and stage awx-operator
|
||||
- name: Stage awx-operator image
|
||||
working-directory: awx-operator
|
||||
run: |
|
||||
BUILD_ARGS="--build-arg DEFAULT_AWX_VERSION=${{ github.event.inputs.version }} \
|
||||
--build-arg OPERATOR_VERSION=${{ github.event.inputs.operator_version }}" \
|
||||
IMAGE_TAG_BASE=ghcr.io/${{ github.repository_owner }}/awx-operator \
|
||||
VERSION=${{ github.event.inputs.operator_version }} make docker-build docker-push
|
||||
BUILD_ARGS="--build-arg DEFAULT_AWX_VERSION=${{ github.event.inputs.version}} \
|
||||
--build-arg OPERATOR_VERSION=${{ github.event.inputs.operator_version }}" \
|
||||
IMG=ghcr.io/${{ github.repository_owner }}/awx-operator:${{ github.event.inputs.operator_version }} \
|
||||
make docker-buildx
|
||||
|
||||
- name: Run test deployment with awx-operator
|
||||
working-directory: awx-operator
|
||||
|
||||
51
Makefile
@@ -10,7 +10,7 @@ KIND_BIN ?= $(shell which kind)
|
||||
CHROMIUM_BIN=/tmp/chrome-linux/chrome
|
||||
GIT_BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
|
||||
MANAGEMENT_COMMAND ?= awx-manage
|
||||
VERSION ?= $(shell $(PYTHON) tools/scripts/scm_version.py)
|
||||
VERSION ?= $(shell $(PYTHON) tools/scripts/scm_version.py 2> /dev/null)
|
||||
|
||||
# ansible-test requires semver compatable version, so we allow overrides to hack it
|
||||
COLLECTION_VERSION ?= $(shell $(PYTHON) tools/scripts/scm_version.py | cut -d . -f 1-3)
|
||||
@@ -75,6 +75,9 @@ SDIST_TAR_FILE ?= $(SDIST_TAR_NAME).tar.gz
|
||||
|
||||
I18N_FLAG_FILE = .i18n_built
|
||||
|
||||
## PLATFORMS defines the target platforms for the manager image be build to provide support to multiple
|
||||
PLATFORMS ?= linux/amd64,linux/arm64 # linux/ppc64le,linux/s390x
|
||||
|
||||
.PHONY: awx-link clean clean-tmp clean-venv requirements requirements_dev \
|
||||
develop refresh adduser migrate dbchange \
|
||||
receiver test test_unit test_coverage coverage_html \
|
||||
@@ -532,7 +535,7 @@ docker-compose-sources: .git/hooks/pre-commit
|
||||
-e enable_vault=$(VAULT) \
|
||||
-e vault_tls=$(VAULT_TLS) \
|
||||
-e enable_tacacs=$(TACACS) \
|
||||
$(EXTRA_SOURCES_ANSIBLE_OPTS)
|
||||
$(EXTRA_SOURCES_ANSIBLE_OPTS)
|
||||
|
||||
docker-compose: awx/projects docker-compose-sources
|
||||
ansible-galaxy install --ignore-certs -r tools/docker-compose/ansible/requirements.yml;
|
||||
@@ -586,12 +589,27 @@ docker-compose-build: Dockerfile.dev
|
||||
--build-arg BUILDKIT_INLINE_CACHE=1 \
|
||||
--cache-from=$(DEV_DOCKER_TAG_BASE)/awx_devel:$(COMPOSE_TAG) .
|
||||
|
||||
|
||||
.PHONY: docker-compose-buildx
|
||||
## Build awx_devel image for docker compose development environment for multiple architectures
|
||||
docker-compose-buildx: Dockerfile.dev
|
||||
- docker buildx create --name docker-compose-buildx
|
||||
docker buildx use docker-compose-buildx
|
||||
- docker buildx build \
|
||||
--push \
|
||||
--build-arg BUILDKIT_INLINE_CACHE=1 \
|
||||
--cache-from=$(DEV_DOCKER_TAG_BASE)/awx_devel:$(COMPOSE_TAG) \
|
||||
--platform=$(PLATFORMS) \
|
||||
--tag $(DEVEL_IMAGE_NAME) \
|
||||
-f Dockerfile.dev .
|
||||
- docker buildx rm docker-compose-buildx
|
||||
|
||||
docker-clean:
|
||||
-$(foreach container_id,$(shell docker ps -f name=tools_awx -aq && docker ps -f name=tools_receptor -aq),docker stop $(container_id); docker rm -f $(container_id);)
|
||||
-$(foreach image_id,$(shell docker images --filter=reference='*/*/*awx_devel*' --filter=reference='*/*awx_devel*' --filter=reference='*awx_devel*' -aq),docker rmi --force $(image_id);)
|
||||
|
||||
docker-clean-volumes: docker-compose-clean docker-compose-container-group-clean
|
||||
docker volume rm -f tools_awx_db tools_vault_1 tools_grafana_storage tools_prometheus_storage $(docker volume ls --filter name=tools_redis_socket_ -q)
|
||||
docker volume rm -f tools_awx_db tools_vault_1 tools_ldap_1 tools_grafana_storage tools_prometheus_storage $(docker volume ls --filter name=tools_redis_socket_ -q)
|
||||
|
||||
docker-refresh: docker-clean docker-compose
|
||||
|
||||
@@ -648,6 +666,21 @@ awx-kube-build: Dockerfile
|
||||
--build-arg HEADLESS=$(HEADLESS) \
|
||||
-t $(DEV_DOCKER_TAG_BASE)/awx:$(COMPOSE_TAG) .
|
||||
|
||||
## Build multi-arch awx image for deployment on Kubernetes environment.
|
||||
awx-kube-buildx: Dockerfile
|
||||
- docker buildx create --name awx-kube-buildx
|
||||
docker buildx use awx-kube-buildx
|
||||
- docker buildx build \
|
||||
--push \
|
||||
--build-arg VERSION=$(VERSION) \
|
||||
--build-arg SETUPTOOLS_SCM_PRETEND_VERSION=$(VERSION) \
|
||||
--build-arg HEADLESS=$(HEADLESS) \
|
||||
--platform=$(PLATFORMS) \
|
||||
--tag $(DEV_DOCKER_TAG_BASE)/awx:$(COMPOSE_TAG) \
|
||||
-f Dockerfile .
|
||||
- docker buildx rm awx-kube-buildx
|
||||
|
||||
|
||||
.PHONY: Dockerfile.kube-dev
|
||||
## Generate Docker.kube-dev for awx_kube_devel image
|
||||
Dockerfile.kube-dev: tools/ansible/roles/dockerfile/templates/Dockerfile.j2
|
||||
@@ -664,6 +697,18 @@ awx-kube-dev-build: Dockerfile.kube-dev
|
||||
--cache-from=$(DEV_DOCKER_TAG_BASE)/awx_kube_devel:$(COMPOSE_TAG) \
|
||||
-t $(DEV_DOCKER_TAG_BASE)/awx_kube_devel:$(COMPOSE_TAG) .
|
||||
|
||||
## Build and push multi-arch awx_kube_devel image for development on local Kubernetes environment.
|
||||
awx-kube-dev-buildx: Dockerfile.kube-dev
|
||||
- docker buildx create --name awx-kube-dev-buildx
|
||||
docker buildx use awx-kube-dev-buildx
|
||||
- docker buildx build \
|
||||
--push \
|
||||
--build-arg BUILDKIT_INLINE_CACHE=1 \
|
||||
--cache-from=$(DEV_DOCKER_TAG_BASE)/awx_kube_devel:$(COMPOSE_TAG) \
|
||||
--platform=$(PLATFORMS) \
|
||||
--tag $(DEV_DOCKER_TAG_BASE)/awx_kube_devel:$(COMPOSE_TAG) \
|
||||
-f Dockerfile.kube-dev .
|
||||
- docker buildx rm awx-kube-dev-buildx
|
||||
|
||||
kind-dev-load: awx-kube-dev-build
|
||||
$(KIND_BIN) load docker-image $(DEV_DOCKER_TAG_BASE)/awx_kube_devel:$(COMPOSE_TAG)
|
||||
|
||||
@@ -5594,7 +5594,7 @@ class InstanceSerializer(BaseSerializer):
|
||||
res['jobs'] = self.reverse('api:instance_unified_jobs_list', kwargs={'pk': obj.pk})
|
||||
res['peers'] = self.reverse('api:instance_peers_list', kwargs={"pk": obj.pk})
|
||||
res['instance_groups'] = self.reverse('api:instance_instance_groups_list', kwargs={'pk': obj.pk})
|
||||
if obj.node_type in [Instance.Types.EXECUTION, Instance.Types.HOP]:
|
||||
if obj.node_type in [Instance.Types.EXECUTION, Instance.Types.HOP] and not obj.managed:
|
||||
res['install_bundle'] = self.reverse('api:instance_install_bundle', kwargs={'pk': obj.pk})
|
||||
if self.context['request'].user.is_superuser or self.context['request'].user.is_system_auditor:
|
||||
if obj.node_type == 'execution':
|
||||
|
||||
@@ -272,16 +272,24 @@ class DashboardJobsGraphView(APIView):
|
||||
|
||||
success_query = user_unified_jobs.filter(status='successful')
|
||||
failed_query = user_unified_jobs.filter(status='failed')
|
||||
canceled_query = user_unified_jobs.filter(status='canceled')
|
||||
error_query = user_unified_jobs.filter(status='error')
|
||||
|
||||
if job_type == 'inv_sync':
|
||||
success_query = success_query.filter(instance_of=models.InventoryUpdate)
|
||||
failed_query = failed_query.filter(instance_of=models.InventoryUpdate)
|
||||
canceled_query = canceled_query.filter(instance_of=models.InventoryUpdate)
|
||||
error_query = error_query.filter(instance_of=models.InventoryUpdate)
|
||||
elif job_type == 'playbook_run':
|
||||
success_query = success_query.filter(instance_of=models.Job)
|
||||
failed_query = failed_query.filter(instance_of=models.Job)
|
||||
canceled_query = canceled_query.filter(instance_of=models.Job)
|
||||
error_query = error_query.filter(instance_of=models.Job)
|
||||
elif job_type == 'scm_update':
|
||||
success_query = success_query.filter(instance_of=models.ProjectUpdate)
|
||||
failed_query = failed_query.filter(instance_of=models.ProjectUpdate)
|
||||
canceled_query = canceled_query.filter(instance_of=models.ProjectUpdate)
|
||||
error_query = error_query.filter(instance_of=models.ProjectUpdate)
|
||||
|
||||
end = now()
|
||||
interval = 'day'
|
||||
@@ -297,10 +305,12 @@ class DashboardJobsGraphView(APIView):
|
||||
else:
|
||||
return Response({'error': _('Unknown period "%s"') % str(period)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
dashboard_data = {"jobs": {"successful": [], "failed": []}}
|
||||
dashboard_data = {"jobs": {"successful": [], "failed": [], "canceled": [], "error": []}}
|
||||
|
||||
succ_list = dashboard_data['jobs']['successful']
|
||||
fail_list = dashboard_data['jobs']['failed']
|
||||
canceled_list = dashboard_data['jobs']['canceled']
|
||||
error_list = dashboard_data['jobs']['error']
|
||||
|
||||
qs_s = (
|
||||
success_query.filter(finished__range=(start, end))
|
||||
@@ -318,6 +328,22 @@ class DashboardJobsGraphView(APIView):
|
||||
.annotate(agg=Count('id', distinct=True))
|
||||
)
|
||||
data_f = {item['d']: item['agg'] for item in qs_f}
|
||||
qs_c = (
|
||||
canceled_query.filter(finished__range=(start, end))
|
||||
.annotate(d=Trunc('finished', interval, tzinfo=end.tzinfo))
|
||||
.order_by()
|
||||
.values('d')
|
||||
.annotate(agg=Count('id', distinct=True))
|
||||
)
|
||||
data_c = {item['d']: item['agg'] for item in qs_c}
|
||||
qs_e = (
|
||||
error_query.filter(finished__range=(start, end))
|
||||
.annotate(d=Trunc('finished', interval, tzinfo=end.tzinfo))
|
||||
.order_by()
|
||||
.values('d')
|
||||
.annotate(agg=Count('id', distinct=True))
|
||||
)
|
||||
data_e = {item['d']: item['agg'] for item in qs_e}
|
||||
|
||||
start_date = start.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
for d in itertools.count():
|
||||
@@ -326,6 +352,8 @@ class DashboardJobsGraphView(APIView):
|
||||
break
|
||||
succ_list.append([time.mktime(date.timetuple()), data_s.get(date, 0)])
|
||||
fail_list.append([time.mktime(date.timetuple()), data_f.get(date, 0)])
|
||||
canceled_list.append([time.mktime(date.timetuple()), data_c.get(date, 0)])
|
||||
error_list.append([time.mktime(date.timetuple()), data_e.get(date, 0)])
|
||||
|
||||
return Response(dashboard_data)
|
||||
|
||||
|
||||
@@ -162,7 +162,7 @@ class AWXConsumerRedis(AWXConsumerBase):
|
||||
class AWXConsumerPG(AWXConsumerBase):
|
||||
def __init__(self, *args, schedule=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.pg_max_wait = settings.DISPATCHER_DB_DOWNTIME_TOLERANCE
|
||||
self.pg_max_wait = getattr(settings, 'DISPATCHER_DB_DOWNTOWN_TOLLERANCE', settings.DISPATCHER_DB_DOWNTIME_TOLERANCE)
|
||||
# if no successful loops have ran since startup, then we should fail right away
|
||||
self.pg_is_down = True # set so that we fail if we get database errors on startup
|
||||
init_time = time.time()
|
||||
@@ -259,6 +259,12 @@ class AWXConsumerPG(AWXConsumerBase):
|
||||
current_downtime = time.time() - self.pg_down_time
|
||||
if current_downtime > self.pg_max_wait:
|
||||
logger.exception(f"Postgres event consumer has not recovered in {current_downtime} s, exiting")
|
||||
# Sending QUIT to multiprocess queue to signal workers to exit
|
||||
for worker in self.pool.workers:
|
||||
try:
|
||||
worker.quit()
|
||||
except Exception:
|
||||
logger.exception(f"Error sending QUIT to worker {worker}")
|
||||
raise
|
||||
# Wait for a second before next attempt, but still listen for any shutdown signals
|
||||
for i in range(10):
|
||||
@@ -270,6 +276,12 @@ class AWXConsumerPG(AWXConsumerBase):
|
||||
except Exception:
|
||||
# Log unanticipated exception in addition to writing to stderr to get timestamps and other metadata
|
||||
logger.exception('Encountered unhandled error in dispatcher main loop')
|
||||
# Sending QUIT to multiprocess queue to signal workers to exit
|
||||
for worker in self.pool.workers:
|
||||
try:
|
||||
worker.quit()
|
||||
except Exception:
|
||||
logger.exception(f"Error sending QUIT to worker {worker}")
|
||||
raise
|
||||
|
||||
|
||||
|
||||
@@ -5,11 +5,12 @@ import logging
|
||||
import threading
|
||||
import time
|
||||
import urllib.parse
|
||||
from pathlib import Path
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import logout
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.migrations.executor import MigrationExecutor
|
||||
from django.db.migrations.recorder import MigrationRecorder
|
||||
from django.db import connection
|
||||
from django.shortcuts import redirect
|
||||
from django.apps import apps
|
||||
@@ -17,9 +18,11 @@ from django.utils.deprecation import MiddlewareMixin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.urls import reverse, resolve
|
||||
|
||||
from awx.main import migrations
|
||||
from awx.main.utils.named_url_graph import generate_graph, GraphNode
|
||||
from awx.conf import fields, register
|
||||
from awx.main.utils.profiling import AWXProfiler
|
||||
from awx.main.utils.common import memoize
|
||||
|
||||
|
||||
logger = logging.getLogger('awx.main.middleware')
|
||||
@@ -198,9 +201,22 @@ class URLModificationMiddleware(MiddlewareMixin):
|
||||
request.path_info = new_path
|
||||
|
||||
|
||||
@memoize(ttl=20)
|
||||
def is_migrating():
|
||||
latest_number = 0
|
||||
latest_name = ''
|
||||
for migration_path in Path(migrations.__path__[0]).glob('[0-9]*.py'):
|
||||
try:
|
||||
migration_number = int(migration_path.name.split('_', 1)[0])
|
||||
except ValueError:
|
||||
continue
|
||||
if migration_number > latest_number:
|
||||
latest_number = migration_number
|
||||
latest_name = migration_path.name[: -len('.py')]
|
||||
return not MigrationRecorder(connection).migration_qs.filter(app='main', name=latest_name).exists()
|
||||
|
||||
|
||||
class MigrationRanCheckMiddleware(MiddlewareMixin):
|
||||
def process_request(self, request):
|
||||
executor = MigrationExecutor(connection)
|
||||
plan = executor.migration_plan(executor.loader.graph.leaf_nodes())
|
||||
if bool(plan) and getattr(resolve(request.path), 'url_name', '') != 'migrations_notran':
|
||||
if is_migrating() and getattr(resolve(request.path), 'url_name', '') != 'migrations_notran':
|
||||
return redirect(reverse("ui:migrations_notran"))
|
||||
|
||||
@@ -5,6 +5,7 @@ from copy import deepcopy
|
||||
import datetime
|
||||
import logging
|
||||
import json
|
||||
import traceback
|
||||
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
@@ -484,14 +485,29 @@ class JobNotificationMixin(object):
|
||||
if msg_template:
|
||||
try:
|
||||
msg = env.from_string(msg_template).render(**context)
|
||||
except (TemplateSyntaxError, UndefinedError, SecurityError):
|
||||
msg = ''
|
||||
except (TemplateSyntaxError, UndefinedError, SecurityError) as e:
|
||||
msg = '\r\n'.join([e.message, ''.join(traceback.format_exception(None, e, e.__traceback__).replace('\n', '\r\n'))])
|
||||
|
||||
if body_template:
|
||||
try:
|
||||
body = env.from_string(body_template).render(**context)
|
||||
except (TemplateSyntaxError, UndefinedError, SecurityError):
|
||||
body = ''
|
||||
except (TemplateSyntaxError, UndefinedError, SecurityError) as e:
|
||||
body = '\r\n'.join([e.message, ''.join(traceback.format_exception(None, e, e.__traceback__).replace('\n', '\r\n'))])
|
||||
|
||||
# https://datatracker.ietf.org/doc/html/rfc2822#section-2.2
|
||||
# Body should have at least 2 CRLF, some clients will interpret
|
||||
# the email incorrectly with blank body. So we will check that
|
||||
|
||||
if len(body.strip().splitlines()) <= 2:
|
||||
# blank body
|
||||
body = '\r\n'.join(
|
||||
[
|
||||
"The template rendering return a blank body.",
|
||||
"Please check the template.",
|
||||
"Refer to https://github.com/ansible/awx/issues/13983",
|
||||
"for further information.",
|
||||
]
|
||||
)
|
||||
|
||||
return (msg, body)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Copyright (c) 2019 Ansible, Inc.
|
||||
# All Rights Reserved.
|
||||
# -*-coding:utf-8-*-
|
||||
|
||||
|
||||
class CustomNotificationBase(object):
|
||||
|
||||
@@ -12,6 +12,7 @@ from . import consumers
|
||||
|
||||
|
||||
logger = logging.getLogger('awx.main.routing')
|
||||
_application = None
|
||||
|
||||
|
||||
class AWXProtocolTypeRouter(ProtocolTypeRouter):
|
||||
@@ -27,14 +28,91 @@ class AWXProtocolTypeRouter(ProtocolTypeRouter):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class MultipleURLRouterAdapter:
|
||||
"""
|
||||
Django channels doesn't nicely support Auth_1(urls_1), Auth_2(urls_2), ..., Auth_n(urls_n)
|
||||
This class allows assocating a websocket url with an auth
|
||||
Ordering matters. The first matching url will be used.
|
||||
"""
|
||||
|
||||
def __init__(self, *auths):
|
||||
self._auths = [a for a in auths]
|
||||
|
||||
async def __call__(self, scope, receive, send):
|
||||
"""
|
||||
Loop through the list of passed in URLRouter's (they may or may not be wrapped by auth).
|
||||
We know we have exhausted the list of URLRouter patterns when we get a
|
||||
ValueError('No route found for path %s'). When that happens, move onto the next
|
||||
URLRouter.
|
||||
If the final URLRouter raises an error, re-raise it in the end.
|
||||
|
||||
We know that we found a match when no error is raised, end the loop.
|
||||
"""
|
||||
last_index = len(self._auths) - 1
|
||||
for i, auth in enumerate(self._auths):
|
||||
try:
|
||||
return await auth.__call__(scope, receive, send)
|
||||
except ValueError as e:
|
||||
if str(e).startswith('No route found for path'):
|
||||
# Only surface the error if on the last URLRouter
|
||||
if i == last_index:
|
||||
raise
|
||||
|
||||
|
||||
websocket_urlpatterns = [
|
||||
re_path(r'api/websocket/$', consumers.EventConsumer.as_asgi()),
|
||||
re_path(r'websocket/$', consumers.EventConsumer.as_asgi()),
|
||||
]
|
||||
websocket_relay_urlpatterns = [
|
||||
re_path(r'websocket/relay/$', consumers.RelayConsumer.as_asgi()),
|
||||
]
|
||||
|
||||
application = AWXProtocolTypeRouter(
|
||||
{
|
||||
'websocket': DrfAuthMiddlewareStack(URLRouter(websocket_urlpatterns)),
|
||||
}
|
||||
)
|
||||
|
||||
def application_func(cls=AWXProtocolTypeRouter) -> ProtocolTypeRouter:
|
||||
return cls(
|
||||
{
|
||||
'websocket': MultipleURLRouterAdapter(
|
||||
URLRouter(websocket_relay_urlpatterns),
|
||||
DrfAuthMiddlewareStack(URLRouter(websocket_urlpatterns)),
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def __getattr__(name: str) -> ProtocolTypeRouter:
|
||||
"""
|
||||
Defer instantiating application.
|
||||
For testing, we just need it to NOT run on import.
|
||||
|
||||
https://peps.python.org/pep-0562/#specification
|
||||
|
||||
Normally, someone would get application from this module via:
|
||||
from awx.main.routing import application
|
||||
|
||||
and do something with the application:
|
||||
application.do_something()
|
||||
|
||||
What does the callstack look like when the import runs?
|
||||
...
|
||||
awx.main.routing.__getattribute__(...) # <-- we don't define this so NOOP as far as we are concerned
|
||||
if '__getattr__' in awx.main.routing.__dict__: # <-- this triggers the function we are in
|
||||
return awx.main.routing.__dict__.__getattr__("application")
|
||||
|
||||
Why isn't this function simply implemented as:
|
||||
def __getattr__(name):
|
||||
if not _application:
|
||||
_application = application_func()
|
||||
return _application
|
||||
|
||||
It could. I manually tested it and it passes test_routing.py.
|
||||
|
||||
But my understanding after reading the PEP-0562 specification link above is that
|
||||
performance would be a bit worse due to the extra __getattribute__ calls when
|
||||
we reference non-global variables.
|
||||
"""
|
||||
if name == "application":
|
||||
globs = globals()
|
||||
if not globs['_application']:
|
||||
globs['_application'] = application_func()
|
||||
return globs['_application']
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
@@ -29,7 +29,7 @@ class RunnerCallback:
|
||||
self.safe_env = {}
|
||||
self.event_ct = 0
|
||||
self.model = model
|
||||
self.update_attempts = int(settings.DISPATCHER_DB_DOWNTIME_TOLERANCE / 5)
|
||||
self.update_attempts = int(getattr(settings, 'DISPATCHER_DB_DOWNTOWN_TOLLERANCE', settings.DISPATCHER_DB_DOWNTIME_TOLERANCE) / 5)
|
||||
self.wrapup_event_dispatched = False
|
||||
self.artifacts_processed = False
|
||||
self.extra_update_fields = {}
|
||||
|
||||
@@ -114,7 +114,7 @@ class BaseTask(object):
|
||||
|
||||
def __init__(self):
|
||||
self.cleanup_paths = []
|
||||
self.update_attempts = int(settings.DISPATCHER_DB_DOWNTIME_TOLERANCE / 5)
|
||||
self.update_attempts = int(getattr(settings, 'DISPATCHER_DB_DOWNTOWN_TOLLERANCE', settings.DISPATCHER_DB_DOWNTIME_TOLERANCE) / 5)
|
||||
self.runner_callback = self.callback_class(model=self.model)
|
||||
|
||||
def update_model(self, pk, _attempt=0, **updates):
|
||||
|
||||
90
awx/main/tests/functional/test_routing.py
Normal file
@@ -0,0 +1,90 @@
|
||||
import pytest
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
|
||||
from channels.routing import ProtocolTypeRouter
|
||||
from channels.testing.websocket import WebsocketCommunicator
|
||||
|
||||
|
||||
from awx.main.consumers import WebsocketSecretAuthHelper
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def application():
|
||||
# code in routing hits the db on import because .. settings cache
|
||||
from awx.main.routing import application_func
|
||||
|
||||
yield application_func(ProtocolTypeRouter)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def websocket_server_generator(application):
|
||||
def fn(endpoint):
|
||||
return WebsocketCommunicator(application, endpoint)
|
||||
|
||||
return fn
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.django_db
|
||||
class TestWebsocketRelay:
|
||||
@pytest.fixture
|
||||
def websocket_relay_secret_generator(self, settings):
|
||||
def fn(secret, set_broadcast_websocket_secret=False):
|
||||
secret_backup = settings.BROADCAST_WEBSOCKET_SECRET
|
||||
settings.BROADCAST_WEBSOCKET_SECRET = 'foobar'
|
||||
res = ('secret'.encode('utf-8'), WebsocketSecretAuthHelper.construct_secret().encode('utf-8'))
|
||||
if set_broadcast_websocket_secret is False:
|
||||
settings.BROADCAST_WEBSOCKET_SECRET = secret_backup
|
||||
return res
|
||||
|
||||
return fn
|
||||
|
||||
@pytest.fixture
|
||||
def websocket_relay_secret(self, settings, websocket_relay_secret_generator):
|
||||
return websocket_relay_secret_generator('foobar', set_broadcast_websocket_secret=True)
|
||||
|
||||
async def test_authorized(self, websocket_server_generator, websocket_relay_secret):
|
||||
server = websocket_server_generator('/websocket/relay/')
|
||||
|
||||
server.scope['headers'] = (websocket_relay_secret,)
|
||||
connected, _ = await server.connect()
|
||||
assert connected is True
|
||||
|
||||
async def test_not_authorized(self, websocket_server_generator):
|
||||
server = websocket_server_generator('/websocket/relay/')
|
||||
connected, _ = await server.connect()
|
||||
assert connected is False, "Connection to the relay websocket without auth. We expected the client to be denied."
|
||||
|
||||
async def test_wrong_secret(self, websocket_server_generator, websocket_relay_secret_generator):
|
||||
server = websocket_server_generator('/websocket/relay/')
|
||||
|
||||
server.scope['headers'] = (websocket_relay_secret_generator('foobar', set_broadcast_websocket_secret=False),)
|
||||
connected, _ = await server.connect()
|
||||
assert connected is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.django_db
|
||||
class TestWebsocketEventConsumer:
|
||||
async def test_unauthorized_anonymous(self, websocket_server_generator):
|
||||
server = websocket_server_generator('/websocket/')
|
||||
|
||||
server.scope['user'] = AnonymousUser()
|
||||
connected, _ = await server.connect()
|
||||
assert connected is False, "Anonymous user should NOT be allowed to login."
|
||||
|
||||
@pytest.mark.skip(reason="Ran out of coding time.")
|
||||
async def test_authorized(self, websocket_server_generator, application, admin):
|
||||
server = websocket_server_generator('/websocket/')
|
||||
|
||||
"""
|
||||
I ran out of time. Here is what I was thinking ...
|
||||
Inject a valid session into the cookies in the header
|
||||
|
||||
server.scope['headers'] = (
|
||||
(b'cookie', ...),
|
||||
)
|
||||
"""
|
||||
connected, _ = await server.connect()
|
||||
assert connected is True, "User should be allowed in via cookies auth via a session key in the cookies"
|
||||
@@ -339,7 +339,7 @@ class WebSocketRelayManager(object):
|
||||
|
||||
if deleted_remote_hosts:
|
||||
logger.info(f"Removing {deleted_remote_hosts} from websocket broadcast list")
|
||||
await asyncio.gather(self.cleanup_offline_host(h) for h in deleted_remote_hosts)
|
||||
await asyncio.gather(*[self.cleanup_offline_host(h) for h in deleted_remote_hosts])
|
||||
|
||||
if new_remote_hosts:
|
||||
logger.info(f"Adding {new_remote_hosts} to websocket broadcast list")
|
||||
|
||||
@@ -216,42 +216,54 @@
|
||||
- block:
|
||||
- name: Fetch galaxy roles from roles/requirements.(yml/yaml)
|
||||
ansible.builtin.command:
|
||||
cmd: "ansible-galaxy role install -r {{ item }} {{ verbosity }}"
|
||||
cmd: "ansible-galaxy role install -r {{ req_file }} {{ verbosity }}"
|
||||
register: galaxy_result
|
||||
with_fileglob:
|
||||
- "{{ project_path | quote }}/roles/requirements.yaml"
|
||||
- "{{ project_path | quote }}/roles/requirements.yml"
|
||||
vars:
|
||||
req_file: "{{ lookup('ansible.builtin.first_found', req_candidates, skip=True) }}"
|
||||
req_candidates:
|
||||
- "{{ project_path | quote }}/roles/requirements.yml"
|
||||
- "{{ project_path | quote }}/roles/requirements.yaml"
|
||||
changed_when: "'was installed successfully' in galaxy_result.stdout"
|
||||
when: roles_enabled | bool
|
||||
when:
|
||||
- roles_enabled | bool
|
||||
- req_file
|
||||
tags:
|
||||
- install_roles
|
||||
|
||||
- name: Fetch galaxy collections from collections/requirements.(yml/yaml)
|
||||
ansible.builtin.command:
|
||||
cmd: "ansible-galaxy collection install -r {{ item }} {{ verbosity }}"
|
||||
cmd: "ansible-galaxy collection install -r {{ req_file }} {{ verbosity }}"
|
||||
register: galaxy_collection_result
|
||||
with_fileglob:
|
||||
- "{{ project_path | quote }}/collections/requirements.yaml"
|
||||
- "{{ project_path | quote }}/collections/requirements.yml"
|
||||
vars:
|
||||
req_file: "{{ lookup('ansible.builtin.first_found', req_candidates, skip=True) }}"
|
||||
req_candidates:
|
||||
- "{{ project_path | quote }}/collections/requirements.yml"
|
||||
- "{{ project_path | quote }}/collections/requirements.yaml"
|
||||
- "{{ project_path | quote }}/requirements.yml"
|
||||
- "{{ project_path | quote }}/requirements.yaml"
|
||||
changed_when: "'Nothing to do.' not in galaxy_collection_result.stdout"
|
||||
when:
|
||||
- "ansible_version.full is version_compare('2.9', '>=')"
|
||||
- collections_enabled | bool
|
||||
- req_file
|
||||
tags:
|
||||
- install_collections
|
||||
|
||||
- name: Fetch galaxy roles and collections from requirements.(yml/yaml)
|
||||
ansible.builtin.command:
|
||||
cmd: "ansible-galaxy install -r {{ item }} {{ verbosity }}"
|
||||
cmd: "ansible-galaxy install -r {{ req_file }} {{ verbosity }}"
|
||||
register: galaxy_combined_result
|
||||
with_fileglob:
|
||||
- "{{ project_path | quote }}/requirements.yaml"
|
||||
- "{{ project_path | quote }}/requirements.yml"
|
||||
vars:
|
||||
req_file: "{{ lookup('ansible.builtin.first_found', req_candidates, skip=True) }}"
|
||||
req_candidates:
|
||||
- "{{ project_path | quote }}/requirements.yaml"
|
||||
- "{{ project_path | quote }}/requirements.yml"
|
||||
changed_when: "'Nothing to do.' not in galaxy_combined_result.stdout"
|
||||
when:
|
||||
- "ansible_version.full is version_compare('2.10', '>=')"
|
||||
- collections_enabled | bool
|
||||
- roles_enabled | bool
|
||||
- req_file
|
||||
tags:
|
||||
- install_collections
|
||||
- install_roles
|
||||
|
||||
@@ -353,6 +353,7 @@ INSTALLED_APPS = [
|
||||
'awx.sso',
|
||||
'solo',
|
||||
'ansible_base.rest_filters',
|
||||
'ansible_base.jwt_consumer',
|
||||
]
|
||||
|
||||
INTERNAL_IPS = ('127.0.0.1',)
|
||||
@@ -362,6 +363,7 @@ REST_FRAMEWORK = {
|
||||
'DEFAULT_PAGINATION_CLASS': 'awx.api.pagination.Pagination',
|
||||
'PAGE_SIZE': 25,
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||
'ansible_base.jwt_consumer.awx.auth.AwxJWTAuthentication',
|
||||
'awx.api.authentication.LoggedOAuth2Authentication',
|
||||
'awx.api.authentication.SessionAuthentication',
|
||||
'awx.api.authentication.LoggedBasicAuthentication',
|
||||
|
||||
@@ -6,6 +6,7 @@ import sortErrorMessages from './sortErrorMessages';
|
||||
|
||||
function FormSubmitError({ error }) {
|
||||
const [errorMessage, setErrorMessage] = useState(null);
|
||||
const [fieldError, setFieldsMessage] = useState(null);
|
||||
const { values, setErrors } = useFormikContext();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -15,6 +16,7 @@ function FormSubmitError({ error }) {
|
||||
}
|
||||
if (fieldErrors) {
|
||||
setErrors(fieldErrors);
|
||||
setFieldsMessage(fieldErrors);
|
||||
}
|
||||
}, [error, setErrors, values]);
|
||||
|
||||
@@ -30,10 +32,34 @@ function FormSubmitError({ error }) {
|
||||
ouiaId="form-submit-error-alert"
|
||||
title={
|
||||
Array.isArray(errorMessage)
|
||||
? errorMessage.map((msg) => <div key={msg}>{msg}</div>)
|
||||
: errorMessage
|
||||
? errorMessage.map((msg) => (
|
||||
<div key={msg}>
|
||||
{msg.messages ? msg.messages : JSON.stringify(msg)}
|
||||
</div>
|
||||
))
|
||||
: errorMessage && (
|
||||
<div>
|
||||
{errorMessage.messages
|
||||
? errorMessage.messages
|
||||
: JSON.stringify(errorMessage)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
/>
|
||||
>
|
||||
{Array.isArray(fieldError)
|
||||
? fieldError.map((msg) => (
|
||||
<div key={msg}>
|
||||
{msg.messages ? msg.messages : JSON.stringify(msg)}
|
||||
</div>
|
||||
))
|
||||
: fieldError && (
|
||||
<div>
|
||||
{fieldError.messages
|
||||
? fieldError.messages
|
||||
: JSON.stringify(fieldError)}
|
||||
</div>
|
||||
)}
|
||||
</Alert>
|
||||
</FormFullWidthLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,14 +5,15 @@ import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||
import FormSubmitError from './FormSubmitError';
|
||||
|
||||
describe('<FormSubmitError>', () => {
|
||||
test('should render null when no error present', () => {
|
||||
test('should render null when no error present', async () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<Formik>{() => <FormSubmitError error={null} />}</Formik>
|
||||
);
|
||||
expect(wrapper.find('FormSubmitError').text()).toEqual('');
|
||||
const ele = await wrapper.find('FormSubmitError').text();
|
||||
expect(ele).toEqual('');
|
||||
});
|
||||
|
||||
test('should pass field errors to Formik', () => {
|
||||
test('should pass field errors to Formik', async () => {
|
||||
const error = {
|
||||
response: {
|
||||
data: {
|
||||
@@ -30,26 +31,7 @@ describe('<FormSubmitError>', () => {
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
expect(wrapper.find('p').text()).toEqual('invalid');
|
||||
});
|
||||
|
||||
test('should display error message if field errors not provided', async () => {
|
||||
const realConsole = global.console;
|
||||
global.console = {
|
||||
error: jest.fn(),
|
||||
};
|
||||
const error = {
|
||||
message: 'There was an error',
|
||||
};
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Formik>{() => <FormSubmitError error={error} />}</Formik>
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('Alert').prop('title')).toEqual('There was an error');
|
||||
expect(global.console.error).toHaveBeenCalledWith(error);
|
||||
global.console = realConsole;
|
||||
const pp = await wrapper.find('p').text();
|
||||
expect(pp).toEqual('invalid');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,15 +9,18 @@ export default function sortErrorMessages(error, formValues = {}) {
|
||||
Object.keys(error.response.data).length > 0
|
||||
) {
|
||||
const parsed = parseFieldErrors(error.response.data, formValues);
|
||||
|
||||
return {
|
||||
formError: parsed.formErrors.join('; '),
|
||||
formError:
|
||||
parsed.formErrors.indexOf(';') > -1
|
||||
? parsed.formErrors.join('; ')
|
||||
: 'Error in fields',
|
||||
fieldErrors: Object.keys(parsed.fieldErrors).length
|
||||
? parsed.fieldErrors
|
||||
: null,
|
||||
};
|
||||
}
|
||||
/* eslint-disable-next-line no-console */
|
||||
console.error(error);
|
||||
|
||||
return {
|
||||
formError: error.message,
|
||||
fieldErrors: null,
|
||||
|
||||
@@ -35,7 +35,7 @@ describe('sortErrorMessages', () => {
|
||||
};
|
||||
const parsed = sortErrorMessages(error, { foo: '', baz: '' });
|
||||
expect(parsed).toEqual({
|
||||
formError: '',
|
||||
formError: 'Error in fields',
|
||||
fieldErrors: {
|
||||
foo: 'bar',
|
||||
baz: 'bam',
|
||||
@@ -54,7 +54,7 @@ describe('sortErrorMessages', () => {
|
||||
};
|
||||
const parsed = sortErrorMessages(error, { foo: '', baz: '' });
|
||||
expect(parsed).toEqual({
|
||||
formError: 'oopsie',
|
||||
formError: 'Error in fields',
|
||||
fieldErrors: {
|
||||
baz: 'bam',
|
||||
},
|
||||
@@ -72,7 +72,7 @@ describe('sortErrorMessages', () => {
|
||||
};
|
||||
const parsed = sortErrorMessages(error, { foo: '', baz: '' });
|
||||
expect(parsed).toEqual({
|
||||
formError: '',
|
||||
formError: 'Error in fields',
|
||||
fieldErrors: {
|
||||
foo: 'bar; bar2',
|
||||
baz: 'bam',
|
||||
@@ -103,7 +103,7 @@ describe('sortErrorMessages', () => {
|
||||
};
|
||||
const parsed = sortErrorMessages(error, formValues);
|
||||
expect(parsed).toEqual({
|
||||
formError: '',
|
||||
formError: 'Error in fields',
|
||||
fieldErrors: {
|
||||
inputs: {
|
||||
url: 'URL Error',
|
||||
@@ -135,7 +135,7 @@ describe('sortErrorMessages', () => {
|
||||
};
|
||||
const parsed = sortErrorMessages(error, formValues);
|
||||
expect(parsed).toEqual({
|
||||
formError: 'Other stuff error',
|
||||
formError: 'Error in fields',
|
||||
fieldErrors: {
|
||||
inputs: {
|
||||
url: 'URL Error',
|
||||
|
||||
@@ -257,12 +257,17 @@ function PromptDetail({
|
||||
numChips={5}
|
||||
ouiaId="prompt-job-tag-chips"
|
||||
totalChips={
|
||||
!overrides.job_tags || overrides.job_tags === ''
|
||||
overrides.job_tags === undefined ||
|
||||
overrides.job_tags === null ||
|
||||
overrides.job_tags === ''
|
||||
? 0
|
||||
: overrides.job_tags.split(',').length
|
||||
}
|
||||
>
|
||||
{overrides.job_tags.length > 0 &&
|
||||
{overrides.job_tags !== undefined &&
|
||||
overrides.job_tags !== null &&
|
||||
overrides.job_tags !== '' &&
|
||||
overrides.job_tags.length > 0 &&
|
||||
overrides.job_tags.split(',').map((jobTag) => (
|
||||
<Chip
|
||||
key={jobTag}
|
||||
@@ -284,13 +289,18 @@ function PromptDetail({
|
||||
<ChipGroup
|
||||
numChips={5}
|
||||
totalChips={
|
||||
!overrides.skip_tags || overrides.skip_tags === ''
|
||||
overrides.skip_tags === undefined ||
|
||||
overrides.skip_tags === null ||
|
||||
overrides.skip_tags === ''
|
||||
? 0
|
||||
: overrides.skip_tags.split(',').length
|
||||
}
|
||||
ouiaId="prompt-skip-tag-chips"
|
||||
>
|
||||
{overrides.skip_tags.length > 0 &&
|
||||
{overrides.skip_tags !== undefined &&
|
||||
overrides.skip_tags !== null &&
|
||||
overrides.skip_tags !== '' &&
|
||||
overrides.skip_tags.length > 0 &&
|
||||
overrides.skip_tags.split(',').map((skipTag) => (
|
||||
<Chip
|
||||
key={skipTag}
|
||||
|
||||
@@ -115,8 +115,11 @@ function SessionProvider({ children }) {
|
||||
}, [setSessionTimeout, setSessionCountdown]);
|
||||
|
||||
useEffect(() => {
|
||||
const isRedirectCondition = (location, histLength) =>
|
||||
location.pathname === '/login' && histLength === 2;
|
||||
|
||||
const unlisten = history.listen((location, action) => {
|
||||
if (action === 'POP') {
|
||||
if (action === 'POP' || isRedirectCondition(location, history.length)) {
|
||||
setIsRedirectLinkReceived(true);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -784,7 +784,7 @@ msgstr "Branche à utiliser dans l’exécution de la tâche. Projet par défaut
|
||||
|
||||
#: screens/Inventory/shared/Inventory.helptext.js:155
|
||||
msgid "Branch to use on inventory sync. Project default used if blank. Only allowed if project allow_override field is set to true."
|
||||
msgstr ""
|
||||
msgstr "Branche à utiliser pour la synchronisation de l'inventaire. La valeur par défaut du projet est utilisée si elle est vide. Cette option n'est autorisée que si le champ allow_override du projet est défini sur vrai."
|
||||
|
||||
#: components/About/About.js:45
|
||||
msgid "Brand Image"
|
||||
@@ -2832,7 +2832,7 @@ msgstr "Entrez les variables avec la syntaxe JSON ou YAML. Consultez la documen
|
||||
|
||||
#: screens/Inventory/shared/SmartInventoryForm.js:94
|
||||
msgid "Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Controller documentation for example syntax."
|
||||
msgstr ""
|
||||
msgstr "Entrez les variables d'inventaire en utilisant la syntaxe JSON ou YAML. Utilisez le bouton d'option pour basculer entre les deux. Référez-vous à la documentation du contrôleur Ansible pour les exemples de syntaxe."
|
||||
|
||||
#: screens/CredentialType/CredentialTypeDetails/CredentialTypeDetails.js:87
|
||||
msgid "Environment variables or extra variables that specify the values a credential type can inject."
|
||||
@@ -3015,7 +3015,7 @@ msgstr "Recherche exacte sur le champ d'identification."
|
||||
|
||||
#: components/Search/RelatedLookupTypeInput.js:38
|
||||
msgid "Exact search on name field."
|
||||
msgstr ""
|
||||
msgstr "Recherche exacte sur le champ nom."
|
||||
|
||||
#: screens/Project/shared/Project.helptext.js:23
|
||||
msgid "Example URLs for GIT Source Control include:"
|
||||
@@ -3242,7 +3242,7 @@ msgstr "Jobs ayant échoué"
|
||||
|
||||
#: screens/WorkflowApproval/WorkflowApprovalList/WorkflowApprovalList.js:262
|
||||
msgid "Failed to approve one or more workflow approval."
|
||||
msgstr ""
|
||||
msgstr "Échec de l'approbation d'une ou plusieurs validations de flux de travail."
|
||||
|
||||
#: screens/WorkflowApproval/shared/WorkflowApprovalButton.js:56
|
||||
msgid "Failed to approve {0}."
|
||||
@@ -3474,7 +3474,7 @@ msgstr "N'a pas réussi à supprimer {name}."
|
||||
|
||||
#: screens/WorkflowApproval/WorkflowApprovalList/WorkflowApprovalList.js:263
|
||||
msgid "Failed to deny one or more workflow approval."
|
||||
msgstr ""
|
||||
msgstr "Échec du refus d'une ou plusieurs validations de flux de travail."
|
||||
|
||||
#: screens/WorkflowApproval/shared/WorkflowDenyButton.js:51
|
||||
msgid "Failed to deny {0}."
|
||||
@@ -3520,7 +3520,7 @@ msgstr "Echec du lancement du Job."
|
||||
|
||||
#: screens/Inventory/InventoryHosts/InventoryHostItem.js:121
|
||||
msgid "Failed to load related groups."
|
||||
msgstr ""
|
||||
msgstr "Impossible de charger les groupes associés."
|
||||
|
||||
#: screens/Instances/InstanceDetail/InstanceDetail.js:388
|
||||
#: screens/Instances/InstanceList/InstanceList.js:266
|
||||
@@ -3972,12 +3972,12 @@ msgstr "Demande(s) de bilan de santé soumise(s). Veuillez patienter et recharge
|
||||
#: screens/Instances/InstanceDetail/InstanceDetail.js:234
|
||||
#: screens/Instances/InstanceList/InstanceListItem.js:242
|
||||
msgid "Health checks are asynchronous tasks. See the"
|
||||
msgstr ""
|
||||
msgstr "Les bilans de santé sont des tâches asynchrones. Veuillez consulter la documentation pour plus d'informations."
|
||||
|
||||
#: screens/InstanceGroup/Instances/InstanceList.js:286
|
||||
#: screens/Instances/InstanceList/InstanceList.js:219
|
||||
msgid "Health checks can only be run on execution nodes."
|
||||
msgstr ""
|
||||
msgstr "Les bilans de santé ne peuvent être exécutées que sur les nœuds d'exécution."
|
||||
|
||||
#: components/StatusLabel/StatusLabel.js:42
|
||||
msgid "Healthy"
|
||||
@@ -5048,7 +5048,7 @@ msgstr "Lancer"
|
||||
|
||||
#: components/TemplateList/TemplateListItem.js:214
|
||||
msgid "Launch Template"
|
||||
msgstr "Lacer le modèle."
|
||||
msgstr "Lancer le modèle."
|
||||
|
||||
#: screens/ManagementJob/ManagementJobList/LaunchManagementPrompt.js:32
|
||||
#: screens/ManagementJob/ManagementJobList/LaunchManagementPrompt.js:34
|
||||
@@ -9637,7 +9637,7 @@ msgstr "Utilisateur"
|
||||
|
||||
#: components/AppContainer/PageHeaderToolbar.js:160
|
||||
msgid "User Details"
|
||||
msgstr "Détails de l'erreur"
|
||||
msgstr "Détails de l'utilisateur"
|
||||
|
||||
#: screens/Setting/SettingList.js:121
|
||||
#: screens/Setting/Settings.js:118
|
||||
|
||||
@@ -80,7 +80,7 @@ function Dashboard() {
|
||||
<Trans>
|
||||
<p>
|
||||
<InfoCircleIcon /> A tech preview of the new {brandName} user
|
||||
interface can be found <a href="/ui_next/dashboard">here</a>.
|
||||
interface can be found <a href="/ui_next">here</a>.
|
||||
</p>
|
||||
</Trans>
|
||||
</Banner>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Modal, Tab, Tabs, TabTitleText } from '@patternfly/react-core';
|
||||
import PropTypes from 'prop-types';
|
||||
import { t } from '@lingui/macro';
|
||||
import { encode } from 'html-entities';
|
||||
import { jsonToYaml } from 'util/yaml';
|
||||
import StatusLabel from '../../../components/StatusLabel';
|
||||
import { DetailList, Detail } from '../../../components/DetailList';
|
||||
import ContentEmpty from '../../../components/ContentEmpty';
|
||||
@@ -144,9 +145,28 @@ function HostEventModal({ onClose, hostEvent = {}, isOpen = false }) {
|
||||
<ContentEmpty title={t`No JSON Available`} />
|
||||
)}
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey={2}
|
||||
title={<TabTitleText>{t`YAML`}</TabTitleText>}
|
||||
aria-label={t`YAML tab`}
|
||||
ouiaId="yaml-tab"
|
||||
>
|
||||
{activeTabKey === 2 && jsonObj ? (
|
||||
<CodeEditor
|
||||
mode="javascript"
|
||||
readOnly
|
||||
value={jsonToYaml(JSON.stringify(jsonObj))}
|
||||
onChange={() => {}}
|
||||
rows={20}
|
||||
hasErrors={false}
|
||||
/>
|
||||
) : (
|
||||
<ContentEmpty title={t`No YAML Available`} />
|
||||
)}
|
||||
</Tab>
|
||||
{stdOut?.length ? (
|
||||
<Tab
|
||||
eventKey={2}
|
||||
eventKey={3}
|
||||
title={<TabTitleText>{t`Output`}</TabTitleText>}
|
||||
aria-label={t`Output tab`}
|
||||
ouiaId="standard-out-tab"
|
||||
@@ -163,7 +183,7 @@ function HostEventModal({ onClose, hostEvent = {}, isOpen = false }) {
|
||||
) : null}
|
||||
{stdErr?.length ? (
|
||||
<Tab
|
||||
eventKey={3}
|
||||
eventKey={4}
|
||||
title={<TabTitleText>{t`Standard Error`}</TabTitleText>}
|
||||
aria-label={t`Standard error tab`}
|
||||
ouiaId="standard-error-tab"
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||
import HostEventModal from './HostEventModal';
|
||||
import { jsonToYaml } from 'util/yaml';
|
||||
|
||||
const hostEvent = {
|
||||
changed: true,
|
||||
@@ -167,6 +168,8 @@ const jsonValue = `{
|
||||
]
|
||||
}`;
|
||||
|
||||
const yamlValue = jsonToYaml(jsonValue);
|
||||
|
||||
describe('HostEventModal', () => {
|
||||
test('initially renders successfully', () => {
|
||||
const wrapper = shallow(
|
||||
@@ -187,7 +190,7 @@ describe('HostEventModal', () => {
|
||||
<HostEventModal hostEvent={hostEvent} onClose={() => {}} isOpen />
|
||||
);
|
||||
|
||||
expect(wrapper.find('Tabs Tab').length).toEqual(4);
|
||||
expect(wrapper.find('Tabs Tab').length).toEqual(5);
|
||||
});
|
||||
|
||||
test('should initially show details tab', () => {
|
||||
@@ -287,7 +290,7 @@ describe('HostEventModal', () => {
|
||||
expect(codeEditor.prop('value')).toEqual(jsonValue);
|
||||
});
|
||||
|
||||
test('should display Standard Out tab content on tab click', () => {
|
||||
test('should display YAML tab content on tab click', () => {
|
||||
const wrapper = shallow(
|
||||
<HostEventModal hostEvent={hostEvent} onClose={() => {}} isOpen />
|
||||
);
|
||||
@@ -299,6 +302,21 @@ describe('HostEventModal', () => {
|
||||
const codeEditor = wrapper.find('Tab[eventKey=2] CodeEditor');
|
||||
expect(codeEditor.prop('mode')).toBe('javascript');
|
||||
expect(codeEditor.prop('readOnly')).toBe(true);
|
||||
expect(codeEditor.prop('value')).toEqual(yamlValue);
|
||||
});
|
||||
|
||||
test('should display Standard Out tab content on tab click', () => {
|
||||
const wrapper = shallow(
|
||||
<HostEventModal hostEvent={hostEvent} onClose={() => {}} isOpen />
|
||||
);
|
||||
|
||||
const handleTabClick = wrapper.find('Tabs').prop('onSelect');
|
||||
handleTabClick(null, 3);
|
||||
wrapper.update();
|
||||
|
||||
const codeEditor = wrapper.find('Tab[eventKey=3] CodeEditor');
|
||||
expect(codeEditor.prop('mode')).toBe('javascript');
|
||||
expect(codeEditor.prop('readOnly')).toBe(true);
|
||||
expect(codeEditor.prop('value')).toEqual(hostEvent.event_data.res.stdout);
|
||||
});
|
||||
|
||||
@@ -316,10 +334,10 @@ describe('HostEventModal', () => {
|
||||
);
|
||||
|
||||
const handleTabClick = wrapper.find('Tabs').prop('onSelect');
|
||||
handleTabClick(null, 3);
|
||||
handleTabClick(null, 4);
|
||||
wrapper.update();
|
||||
|
||||
const codeEditor = wrapper.find('Tab[eventKey=3] CodeEditor');
|
||||
const codeEditor = wrapper.find('Tab[eventKey=4] CodeEditor');
|
||||
expect(codeEditor.prop('mode')).toBe('javascript');
|
||||
expect(codeEditor.prop('readOnly')).toBe(true);
|
||||
expect(codeEditor.prop('value')).toEqual('error content');
|
||||
@@ -351,10 +369,10 @@ describe('HostEventModal', () => {
|
||||
);
|
||||
|
||||
const handleTabClick = wrapper.find('Tabs').prop('onSelect');
|
||||
handleTabClick(null, 2);
|
||||
handleTabClick(null, 3);
|
||||
wrapper.update();
|
||||
|
||||
const codeEditor = wrapper.find('Tab[eventKey=2] CodeEditor');
|
||||
const codeEditor = wrapper.find('Tab[eventKey=3] CodeEditor');
|
||||
expect(codeEditor.prop('mode')).toBe('javascript');
|
||||
expect(codeEditor.prop('readOnly')).toBe(true);
|
||||
expect(codeEditor.prop('value')).toEqual('foo bar');
|
||||
@@ -375,10 +393,10 @@ describe('HostEventModal', () => {
|
||||
);
|
||||
|
||||
const handleTabClick = wrapper.find('Tabs').prop('onSelect');
|
||||
handleTabClick(null, 2);
|
||||
handleTabClick(null, 3);
|
||||
wrapper.update();
|
||||
|
||||
const codeEditor = wrapper.find('Tab[eventKey=2] CodeEditor');
|
||||
const codeEditor = wrapper.find('Tab[eventKey=3] CodeEditor');
|
||||
expect(codeEditor.prop('mode')).toBe('javascript');
|
||||
expect(codeEditor.prop('readOnly')).toBe(true);
|
||||
expect(codeEditor.prop('value')).toEqual('baz\nbar');
|
||||
@@ -394,10 +412,10 @@ describe('HostEventModal', () => {
|
||||
);
|
||||
|
||||
const handleTabClick = wrapper.find('Tabs').prop('onSelect');
|
||||
handleTabClick(null, 2);
|
||||
handleTabClick(null, 3);
|
||||
wrapper.update();
|
||||
|
||||
const codeEditor = wrapper.find('Tab[eventKey=2] CodeEditor');
|
||||
const codeEditor = wrapper.find('Tab[eventKey=3] CodeEditor');
|
||||
expect(codeEditor.prop('mode')).toBe('javascript');
|
||||
expect(codeEditor.prop('readOnly')).toBe(true);
|
||||
expect(codeEditor.prop('value')).toEqual(
|
||||
|
||||
@@ -30,7 +30,7 @@ function SubscriptionUsage() {
|
||||
<Trans>
|
||||
<p>
|
||||
<InfoCircleIcon /> A tech preview of the new {brandName} user
|
||||
interface can be found <a href="/ui_next/dashboard">here</a>.
|
||||
interface can be found <a href="/ui_next">here</a>.
|
||||
</p>
|
||||
</Trans>
|
||||
</Banner>
|
||||
|
||||
@@ -201,7 +201,11 @@ function NodeViewModal({ readOnly }) {
|
||||
overrides.limit = originalNodeObject.limit;
|
||||
}
|
||||
if (launchConfig.ask_verbosity_on_launch) {
|
||||
overrides.verbosity = originalNodeObject.verbosity.toString();
|
||||
overrides.verbosity =
|
||||
originalNodeObject.verbosity !== undefined &&
|
||||
originalNodeObject.verbosity !== null
|
||||
? originalNodeObject.verbosity.toString()
|
||||
: '0';
|
||||
}
|
||||
if (launchConfig.ask_credential_on_launch) {
|
||||
overrides.credentials = originalNodeCredentials || [];
|
||||
|
||||
@@ -35,7 +35,7 @@ ui-next/src/build: $(UI_NEXT_DIR)/src/build/awx
|
||||
## True target for ui-next/src/build. Build ui_next from source.
|
||||
$(UI_NEXT_DIR)/src/build/awx: $(UI_NEXT_DIR)/src $(UI_NEXT_DIR)/src/node_modules/webpack
|
||||
@echo "=== Building ui_next ==="
|
||||
@cd $(UI_NEXT_DIR)/src && PRODUCT="$(PRODUCT)" PUBLIC_PATH=/static/awx/ npm run build:awx
|
||||
@cd $(UI_NEXT_DIR)/src && PRODUCT="$(PRODUCT)" PUBLIC_PATH=/static/awx/ ROUTE_PREFIX=/ui_next npm run build:awx
|
||||
@mv $(UI_NEXT_DIR)/src/build/awx/index.html $(UI_NEXT_DIR)/src/build/awx/index_awx.html
|
||||
|
||||
.PHONY: ui-next/src
|
||||
|
||||
@@ -18,7 +18,7 @@ documentation: https://github.com/ansible/awx/blob/devel/awx_collection/README.m
|
||||
homepage: https://www.ansible.com/
|
||||
issues: https://github.com/ansible/awx/issues?q=is%3Aissue+label%3Acomponent%3Aawx_collection
|
||||
license:
|
||||
- GPL-3.0-only
|
||||
- GPL-3.0-or-later
|
||||
name: awx
|
||||
namespace: awx
|
||||
readme: README.md
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
# This code is part of Ansible, but is an independent component.
|
||||
# This particular file snippet, and this file snippet only, is BSD licensed.
|
||||
# Modules you write using this snippet, which is embedded dynamically by Ansible
|
||||
# still belong to the author of the module, and may assign their own license
|
||||
# to the complete work.
|
||||
#
|
||||
# Copyright (c), Wayne Witzel III <wayne@riotousliving.com>
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without modification,
|
||||
# are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
||||
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
import traceback
|
||||
|
||||
TOWER_CLI_IMP_ERR = None
|
||||
try:
|
||||
import tower_cli.utils.exceptions as exc
|
||||
from tower_cli.utils import parser
|
||||
from tower_cli.api import client
|
||||
|
||||
HAS_TOWER_CLI = True
|
||||
except ImportError:
|
||||
TOWER_CLI_IMP_ERR = traceback.format_exc()
|
||||
HAS_TOWER_CLI = False
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
|
||||
|
||||
def tower_auth_config(module):
|
||||
"""
|
||||
`tower_auth_config` attempts to load the tower-cli.cfg file
|
||||
specified from the `tower_config_file` parameter. If found,
|
||||
if returns the contents of the file as a dictionary, else
|
||||
it will attempt to fetch values from the module params and
|
||||
only pass those values that have been set.
|
||||
"""
|
||||
config_file = module.params.pop('tower_config_file', None)
|
||||
if config_file:
|
||||
if not os.path.exists(config_file):
|
||||
module.fail_json(msg='file not found: %s' % config_file)
|
||||
if os.path.isdir(config_file):
|
||||
module.fail_json(msg='directory can not be used as config file: %s' % config_file)
|
||||
|
||||
with open(config_file, 'r') as f:
|
||||
return parser.string_to_dict(f.read())
|
||||
else:
|
||||
auth_config = {}
|
||||
host = module.params.pop('tower_host', None)
|
||||
if host:
|
||||
auth_config['host'] = host
|
||||
username = module.params.pop('tower_username', None)
|
||||
if username:
|
||||
auth_config['username'] = username
|
||||
password = module.params.pop('tower_password', None)
|
||||
if password:
|
||||
auth_config['password'] = password
|
||||
module.params.pop('tower_verify_ssl', None) # pop alias if used
|
||||
verify_ssl = module.params.pop('validate_certs', None)
|
||||
if verify_ssl is not None:
|
||||
auth_config['verify_ssl'] = verify_ssl
|
||||
return auth_config
|
||||
|
||||
|
||||
def tower_check_mode(module):
|
||||
'''Execute check mode logic for Ansible Tower modules'''
|
||||
if module.check_mode:
|
||||
try:
|
||||
result = client.get('/ping').json()
|
||||
module.exit_json(changed=True, tower_version='{0}'.format(result['version']))
|
||||
except (exc.ServerError, exc.ConnectionError, exc.BadRequest) as excinfo:
|
||||
module.fail_json(changed=False, msg='Failed check mode: {0}'.format(excinfo))
|
||||
|
||||
|
||||
class TowerLegacyModule(AnsibleModule):
|
||||
def __init__(self, argument_spec, **kwargs):
|
||||
args = dict(
|
||||
tower_host=dict(),
|
||||
tower_username=dict(),
|
||||
tower_password=dict(no_log=True),
|
||||
validate_certs=dict(type='bool', aliases=['tower_verify_ssl']),
|
||||
tower_config_file=dict(type='path'),
|
||||
)
|
||||
args.update(argument_spec)
|
||||
|
||||
kwargs.setdefault('mutually_exclusive', [])
|
||||
kwargs['mutually_exclusive'].extend(
|
||||
(
|
||||
('tower_config_file', 'tower_host'),
|
||||
('tower_config_file', 'tower_username'),
|
||||
('tower_config_file', 'tower_password'),
|
||||
('tower_config_file', 'validate_certs'),
|
||||
)
|
||||
)
|
||||
|
||||
super().__init__(argument_spec=args, **kwargs)
|
||||
|
||||
if not HAS_TOWER_CLI:
|
||||
self.fail_json(msg=missing_required_lib('ansible-tower-cli'), exception=TOWER_CLI_IMP_ERR)
|
||||
@@ -181,10 +181,8 @@ def run_module(request, collection_import):
|
||||
resource_class = resource_module.ControllerAWXKitModule
|
||||
elif getattr(resource_module, 'ControllerAPIModule', None):
|
||||
resource_class = resource_module.ControllerAPIModule
|
||||
elif getattr(resource_module, 'TowerLegacyModule', None):
|
||||
resource_class = resource_module.TowerLegacyModule
|
||||
else:
|
||||
raise RuntimeError("The module has neither a TowerLegacyModule, ControllerAWXKitModule or a ControllerAPIModule")
|
||||
raise RuntimeError("The module has neither a ControllerAWXKitModule or a ControllerAPIModule")
|
||||
|
||||
with mock.patch.object(resource_class, '_load_params', new=mock_load_params):
|
||||
# Call the test utility (like a mock server) instead of issuing HTTP requests
|
||||
|
||||
@@ -155,4 +155,4 @@ def test_build_notification_message_undefined(run_module, admin_user, organizati
|
||||
nt = NotificationTemplate.objects.get(id=result['id'])
|
||||
|
||||
body = job.build_notification_message(nt, 'running')
|
||||
assert '{"started_by": "My Placeholder"}' in body[1]
|
||||
assert 'The template rendering return a blank body' in body[1]
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
name: "{{ project_name }}"
|
||||
organization: "{{ org_name }}"
|
||||
scm_type: git
|
||||
scm_url: https://github.com/ansible/test-playbooks
|
||||
scm_url: https://github.com/ansible/ansible-tower-samples
|
||||
wait: true
|
||||
|
||||
- name: Create a git project with same name, different org
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
name: "{{ project_name1 }}"
|
||||
organization: Default
|
||||
scm_type: git
|
||||
scm_url: https://github.com/ansible/test-playbooks
|
||||
scm_url: https://github.com/ansible/ansible-tower-samples
|
||||
wait: true
|
||||
register: result
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
name: "{{ project_name1 }}"
|
||||
organization: Default
|
||||
scm_type: git
|
||||
scm_url: https://github.com/ansible/test-playbooks
|
||||
scm_url: https://github.com/ansible/ansible-tower-samples
|
||||
wait: true
|
||||
state: exists
|
||||
register: result
|
||||
@@ -58,7 +58,7 @@
|
||||
name: "{{ project_name1 }}"
|
||||
organization: Default
|
||||
scm_type: git
|
||||
scm_url: https://github.com/ansible/test-playbooks
|
||||
scm_url: https://github.com/ansible/ansible-tower-samples
|
||||
wait: true
|
||||
state: exists
|
||||
request_timeout: .001
|
||||
@@ -75,7 +75,7 @@
|
||||
name: "{{ project_name1 }}"
|
||||
organization: Default
|
||||
scm_type: git
|
||||
scm_url: https://github.com/ansible/test-playbooks
|
||||
scm_url: https://github.com/ansible/ansible-tower-samples
|
||||
wait: true
|
||||
state: absent
|
||||
register: result
|
||||
@@ -89,7 +89,7 @@
|
||||
name: "{{ project_name1 }}"
|
||||
organization: Default
|
||||
scm_type: git
|
||||
scm_url: https://github.com/ansible/test-playbooks
|
||||
scm_url: https://github.com/ansible/ansible-tower-samples
|
||||
wait: true
|
||||
state: exists
|
||||
register: result
|
||||
@@ -103,7 +103,7 @@
|
||||
name: "{{ project_name1 }}"
|
||||
organization: Default
|
||||
scm_type: git
|
||||
scm_url: https://github.com/ansible/test-playbooks
|
||||
scm_url: https://github.com/ansible/ansible-tower-samples
|
||||
wait: false
|
||||
register: result
|
||||
ignore_errors: true
|
||||
@@ -137,7 +137,7 @@
|
||||
name: "{{ project_name2 }}"
|
||||
organization: "{{ org_name }}"
|
||||
scm_type: git
|
||||
scm_url: https://github.com/ansible/test-playbooks
|
||||
scm_url: https://github.com/ansible/ansible-tower-samples
|
||||
scm_credential: "{{ cred_name }}"
|
||||
check_mode: true
|
||||
|
||||
@@ -162,7 +162,7 @@
|
||||
name: "{{ project_name2 }}"
|
||||
organization: Non_Existing_Org
|
||||
scm_type: git
|
||||
scm_url: https://github.com/ansible/test-playbooks
|
||||
scm_url: https://github.com/ansible/ansible-tower-samples
|
||||
scm_credential: "{{ cred_name }}"
|
||||
register: result
|
||||
ignore_errors: true
|
||||
@@ -179,7 +179,7 @@
|
||||
name: "{{ project_name2 }}"
|
||||
organization: "{{ org_name }}"
|
||||
scm_type: git
|
||||
scm_url: https://github.com/ansible/test-playbooks
|
||||
scm_url: https://github.com/ansible/ansible-tower-samples
|
||||
scm_credential: Non_Existing_Credential
|
||||
register: result
|
||||
ignore_errors: true
|
||||
@@ -191,7 +191,7 @@
|
||||
- "'Non_Existing_Credential' in result.msg"
|
||||
- "result.total_results == 0"
|
||||
|
||||
- name: Create a git project without credentials without waiting
|
||||
- name: Create a git project using a branch and allowing branch override
|
||||
project:
|
||||
name: "{{ project_name3 }}"
|
||||
organization: Default
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
name: "{{ project_name1 }}"
|
||||
organization: Default
|
||||
scm_type: git
|
||||
scm_url: https://github.com/ansible/test-playbooks
|
||||
scm_url: https://github.com/ansible/ansible-tower-samples
|
||||
wait: false
|
||||
register: project_create_result
|
||||
|
||||
|
||||
@@ -19,8 +19,6 @@ homepage: https://www.ansible.com/
|
||||
issues: https://github.com/ansible/awx/issues?q=is%3Aissue+label%3Acomponent%3Aawx_collection
|
||||
license:
|
||||
- GPL-3.0-or-later
|
||||
# plugins/module_utils/tower_legacy.py
|
||||
- BSD-2-Clause
|
||||
name: {{ collection_package }}
|
||||
namespace: {{ collection_namespace }}
|
||||
readme: README.md
|
||||
|
||||
@@ -96,6 +96,7 @@ credential_type_name_to_config_kind_map = {
|
||||
'vault': 'vault',
|
||||
'vmware vcenter': 'vmware',
|
||||
'gpg public key': 'gpg_public_key',
|
||||
'terraform backend configuration': 'terraform',
|
||||
}
|
||||
|
||||
config_kind_to_credential_type_name_map = {kind: name for name, kind in credential_type_name_to_config_kind_map.items()}
|
||||
|
||||
@@ -51,7 +51,16 @@ class WSClient(object):
|
||||
# Subscription group types
|
||||
|
||||
def __init__(
|
||||
self, token=None, hostname='', port=443, secure=True, session_id=None, csrftoken=None, add_received_time=False, session_cookie_name='awx_sessionid'
|
||||
self,
|
||||
token=None,
|
||||
hostname='',
|
||||
port=443,
|
||||
secure=True,
|
||||
ws_suffix='websocket/',
|
||||
session_id=None,
|
||||
csrftoken=None,
|
||||
add_received_time=False,
|
||||
session_cookie_name='awx_sessionid',
|
||||
):
|
||||
# delay this import, because this is an optional dependency
|
||||
import websocket
|
||||
@@ -68,6 +77,7 @@ class WSClient(object):
|
||||
hostname = result.hostname
|
||||
|
||||
self.port = port
|
||||
self.suffix = ws_suffix
|
||||
self._use_ssl = secure
|
||||
self.hostname = hostname
|
||||
self.token = token
|
||||
@@ -85,7 +95,7 @@ class WSClient(object):
|
||||
else:
|
||||
auth_cookie = ''
|
||||
pref = 'wss://' if self._use_ssl else 'ws://'
|
||||
url = '{0}{1.hostname}:{1.port}/websocket/'.format(pref, self)
|
||||
url = '{0}{1.hostname}:{1.port}/{1.suffix}'.format(pref, self)
|
||||
self.ws = websocket.WebSocketApp(
|
||||
url, on_open=self._on_open, on_message=self._on_message, on_error=self._on_error, on_close=self._on_close, cookie=auth_cookie
|
||||
)
|
||||
|
||||
@@ -90,6 +90,7 @@ setup(
|
||||
install_requires=[
|
||||
'PyYAML',
|
||||
'requests',
|
||||
'setuptools',
|
||||
],
|
||||
python_requires=">=3.8",
|
||||
extras_require={'formatting': ['jq'], 'websockets': ['websocket-client==0.57.0'], 'crypto': ['cryptography']},
|
||||
|
||||
@@ -17,6 +17,11 @@ def test_explicit_hostname():
|
||||
assert client.token == "token"
|
||||
|
||||
|
||||
def test_websocket_suffix():
|
||||
client = WSClient("token", "hostname", 566, ws_suffix='my-websocket/')
|
||||
assert client.suffix == 'my-websocket/'
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'url, result',
|
||||
[
|
||||
|
||||
@@ -13,7 +13,7 @@ Scaling your mesh is only available on Openshift and Kubernetes (K8S) deployment
|
||||
|
||||
Instances serve as nodes in your mesh topology. Automation mesh allows you to extend the footprint of your automation. Where you launch a job and where the ``ansible-playbook`` runs can be in different locations.
|
||||
|
||||
.. image:: ../common/images/instances_mesh_concept.png
|
||||
.. image:: ../common/images/instances_mesh_concept.drawio.png
|
||||
:alt: Site A pointing to Site B and dotted arrows to two hosts from Site B
|
||||
|
||||
Automation mesh is useful for:
|
||||
@@ -23,7 +23,7 @@ Automation mesh is useful for:
|
||||
|
||||
The nodes (control, hop, and execution instances) are interconnected via receptor, forming a virtual mesh.
|
||||
|
||||
.. image:: ../common/images/instances_mesh_concept_with_nodes.png
|
||||
.. image:: ../common/images/instances_mesh_concept_with_nodes.drawio.png
|
||||
:alt: Control node pointing to hop node, which is pointing to two execution nodes.
|
||||
|
||||
|
||||
@@ -51,13 +51,227 @@ Prerequisites
|
||||
- To manage instances from the AWX user interface, you must have System Administrator or System Auditor permissions.
|
||||
|
||||
|
||||
Common topologies
|
||||
------------------
|
||||
|
||||
Instances make up the network of devices that communicate with one another. They are the building blocks of an automation mesh. These building blocks serve as nodes in a mesh topology. There are several kinds of instances:
|
||||
|
||||
+-----------+-----------------------------------------------------------------------------------------------------------------+
|
||||
| Node Type | Description |
|
||||
+===========+=================================================================================================================+
|
||||
| Control | Nodes that run persistent Ansible Automation Platform services, and delegate jobs to hybrid and execution nodes |
|
||||
+-----------+-----------------------------------------------------------------------------------------------------------------+
|
||||
| Hybrid | Nodes that run persistent Ansible Automation Platform services and execute jobs |
|
||||
| | (not applicable to operator-based installations) |
|
||||
+-----------+-----------------------------------------------------------------------------------------------------------------+
|
||||
| Hop | Used for relaying across the mesh only |
|
||||
+-----------+-----------------------------------------------------------------------------------------------------------------+
|
||||
| Execution | Nodes that run jobs delivered from control nodes (jobs submitted from the user’s Ansible automation) |
|
||||
+-----------+-----------------------------------------------------------------------------------------------------------------+
|
||||
|
||||
Simple topology
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
One of the ways to expand job capacity is to create a standalone execution node that can be added to run alongside the Kubernetes deployment of AWX. These machines will not be a part of the AWX Kubernetes cluster. The control nodes running in the cluster will connect and submit work to these machines via Receptor. The machines are registered in AWX as type "execution" instances, meaning they will only be used to run AWX jobs, not dispatch work or handle web requests as control nodes do.
|
||||
|
||||
Hop nodes can be added to sit between the control plane of AWX and standalone execution nodes. These machines will not be a part of the AWX Kubernetes cluster and they will be registered in AWX as node type "hop", meaning they will only handle inbound and outbound traffic for otherwise unreachable nodes in a different or more strict network.
|
||||
|
||||
Below is an example of an AWX task pod with two execution nodes. Traffic to execution node 2 flows through a hop node that is setup between it and the control plane.
|
||||
|
||||
.. image:: ../common/images/instances_awx_task_pods_hopnode.drawio.png
|
||||
:alt: AWX task pod with a hop node between the control plane of AWX and standalone execution nodes.
|
||||
|
||||
|
||||
Below are sample values used to configure each node in a simple topology:
|
||||
|
||||
.. list-table::
|
||||
:widths: 20 30 10 20 15
|
||||
:header-rows: 1
|
||||
|
||||
* - Instance type
|
||||
- Hostname
|
||||
- Listener port
|
||||
- Peers from control nodes
|
||||
- Peers
|
||||
* - Control plane
|
||||
- awx-task-65d6d96987-mgn9j
|
||||
- n/a
|
||||
- n/a
|
||||
- [hop node]
|
||||
* - Hop node
|
||||
- awx-hop-node
|
||||
- 27199
|
||||
- True
|
||||
- []
|
||||
* - Execution node
|
||||
- awx-example.com
|
||||
- n/a
|
||||
- False
|
||||
- [hop node]
|
||||
|
||||
|
||||
|
||||
Mesh topology
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Mesh ingress is a feature that allows remote nodes to connect inbound to the control plane. This is especially useful when creating remote nodes in restricted networking environments that disallow inbound traffic.
|
||||
|
||||
|
||||
.. image:: ../common/images/instances_mesh_ingress_topology.drawio.png
|
||||
:alt: Mesh ingress architecture showing the peering relationship between nodes.
|
||||
|
||||
|
||||
Below are sample values used to configure each node in a mesh ingress topology:
|
||||
|
||||
.. list-table::
|
||||
:widths: 20 30 10 20 15
|
||||
:header-rows: 1
|
||||
|
||||
* - Instance type
|
||||
- Hostname
|
||||
- Listener port
|
||||
- Peers from control nodes
|
||||
- Peers
|
||||
* - Control plane
|
||||
- awx-task-65d6d96987-mgn9j
|
||||
- n/a
|
||||
- n/a
|
||||
- [hop node]
|
||||
* - Hop node
|
||||
- awx-mesh-ingress-1
|
||||
- 27199
|
||||
- True
|
||||
- []
|
||||
* - Execution node
|
||||
- awx-example.com
|
||||
- n/a
|
||||
- False
|
||||
- [hop node]
|
||||
|
||||
|
||||
In order to create a mesh ingress for AWX, see the `Mesh Ingress <https://ansible.readthedocs.io/projects/awx-operator/en/latest/user-guide/advanced-configuration/mesh-ingress.html>`_ chapter of the AWX Operator Documentation for information on setting up this type of topology. The last step is to create a remote execution node and add the execution node to an instance group in order for it to be used in your job execution. Whatever execution environment image used to run a playbook needs to be accessible for your remote execution node. Everything you are using in your playbook also needs to be accessible from this remote execution node.
|
||||
|
||||
.. image:: ../common/images/instances-job-template-using-remote-execution-ig.png
|
||||
:alt: Job template using the instance group with the execution node to run jobs.
|
||||
:width: 1400px
|
||||
|
||||
|
||||
.. _ag_instances_add:
|
||||
|
||||
Add an instance
|
||||
----------------
|
||||
|
||||
To create an instance in AWX:
|
||||
|
||||
1. Click **Instances** from the left side navigation menu of the AWX UI.
|
||||
|
||||
2. In the Instances list view, click the **Add** button and the Create new Instance window opens.
|
||||
|
||||
.. image:: ../common/images/instances_create_new.png
|
||||
:alt: Create a new instance form.
|
||||
:width: 1400px
|
||||
|
||||
An instance has several attributes that may be configured:
|
||||
|
||||
- Enter a fully qualified domain name (ping-able DNS) or IP address for your instance in the **Host Name** field (required). This field is equivalent to ``hostname`` in the API.
|
||||
- Optionally enter a **Description** for the instance
|
||||
- The **Instance State** field is auto-populated, indicating that it is being installed, and cannot be modified
|
||||
- Optionally specify the **Listener Port** for the receptor to listen on for incoming connections. This is an open port on the remote machine used to establish inbound TCP connections. This field is equivalent to ``listener_port`` in the API.
|
||||
- Select from the options in **Instance Type** field to specify the type you want to create. Only execution and hop nodes can be created as operator-based installations do not support hybrid nodes. This field is equivalent to ``node_type`` in the API.
|
||||
- In the **Peers** field, select the instance hostnames you want your new instance to connect outbound to.
|
||||
- In the **Options** fields:
|
||||
- Check the **Enable Instance** box to make it available for jobs to run on an execution node.
|
||||
- Check the **Managed by Policy** box to allow policy to dictate how the instance is assigned.
|
||||
- Check the **Peers from control nodes** box to allow control nodes to peer to this instance automatically. Listener port needs to be set if this is enabled or the instance is a peer.
|
||||
|
||||
|
||||
|
||||
3. Once the attributes are configured, click **Save** to proceed.
|
||||
|
||||
Upon successful creation, the Details of the one of the created instances opens.
|
||||
|
||||
.. image:: ../common/images/instances_create_details.png
|
||||
:alt: Details of the newly created instance.
|
||||
:width: 1400px
|
||||
|
||||
.. note::
|
||||
|
||||
The proceeding steps 4-8 are intended to be ran from any computer that has SSH access to the newly created instance.
|
||||
|
||||
4. Click the download button next to the **Install Bundle** field to download the tarball that contain files to allow AWX to make proper TCP connections to the remote machine.
|
||||
|
||||
.. image:: ../common/images/instances_install_bundle.png
|
||||
:alt: Instance details showing the Download button in the Install Bundle field of the Details tab.
|
||||
:width: 1400px
|
||||
|
||||
5. Extract the downloaded ``tar.gz`` file from the location you downloaded it. The install bundle contains TLS certificates and keys, a certificate authority, and a proper Receptor configuration file. To facilitate that these files will be in the right location on the remote machine, the install bundle includes an ``install_receptor.yml`` playbook. The playbook requires the Receptor collection which can be obtained via:
|
||||
|
||||
::
|
||||
|
||||
ansible-galaxy collection install -r requirements.yml
|
||||
|
||||
6. Before running the ``ansible-playbook`` command, edit the following fields in the ``inventory.yml`` file:
|
||||
|
||||
- ``ansible_user`` with the username running the installation
|
||||
- ``ansible_ssh_private_key_file`` to contain the filename of the private key used to connect to the instance
|
||||
|
||||
::
|
||||
|
||||
---
|
||||
all:
|
||||
hosts:
|
||||
remote-execution:
|
||||
ansible_host: <hostname>
|
||||
ansible_user: <username> # user provided
|
||||
ansible_ssh_private_key_file: ~/.ssh/id_rsa
|
||||
|
||||
The content of the ``inventory.yml`` file serves as a template and contains variables for roles that are applied during the installation and configuration of a receptor node in a mesh topology. You may modify some of the other fields, or replace the file in its entirety for advanced scenarios. Refer to `Role Variables <https://github.com/ansible/receptor-collection/blob/main/README.md>`_ for more information on each variable.
|
||||
|
||||
7. Save the file to continue.
|
||||
|
||||
8. Run the following command on the machine you want to update your mesh:
|
||||
|
||||
::
|
||||
|
||||
ansible-playbook -i inventory.yml install_receptor.yml
|
||||
|
||||
Wait a few minutes for the periodic AWX task to do a health check against the new instance. You may run a health check by selecting the node and clicking the **Run health check** button from its Details page at any time. Once the instances endpoint or page reports a "Ready" status for the instance, jobs are now ready to run on this machine!
|
||||
|
||||
9. To view other instances within the same topology or associate peers, click the **Peers** tab.
|
||||
|
||||
.. image:: ../common/images/instances_peers_tab.png
|
||||
:alt: "Peers" tab showing two peers.
|
||||
:width: 1400px
|
||||
|
||||
To associate peers with your node, click the **Associate** button to open a dialog box of instances eligible for peering.
|
||||
|
||||
.. image:: ../common/images/instances_associate_peer.png
|
||||
:alt: Instances available to peer with the example hop node.
|
||||
:width: 1400px
|
||||
|
||||
Execution nodes can peer with either hop nodes or other execution nodes. Hop nodes can only peer with execution nodes unless you check the **Peers from control nodes** check box from the **Options** field.
|
||||
|
||||
.. note::
|
||||
|
||||
If you associate or disassociate a peer, a notification will inform you to re-run the install bundle from the Peer Detail view (the :ref:`ag_topology_viewer` has the download link).
|
||||
|
||||
.. image:: ../common/images/instances_associate_peer_reinstallmsg.png
|
||||
:alt: Notification to re-run the installation bundle due to change in the peering.
|
||||
|
||||
You can remove an instance by clicking **Remove** in the Instances page, or by setting the instance ``node_state = deprovisioning`` via the API. Upon deleting, a pop-up message will appear to notify that you may need to re-run the install bundle to make sure things that were removed are no longer connected.
|
||||
|
||||
|
||||
10. To view a graphical representation of your updated topology, refer to the :ref:`ag_topology_viewer` section of this guide.
|
||||
|
||||
|
||||
Manage instances
|
||||
-----------------
|
||||
|
||||
Click **Instances** from the left side navigation menu to access the Instances list.
|
||||
|
||||
.. image:: ../common/images/instances_list_view.png
|
||||
:alt: List view of instances in AWX
|
||||
:alt: List view of instances in AWX
|
||||
:width: 1400px
|
||||
|
||||
The Instances list displays all the current nodes in your topology, along with relevant details:
|
||||
|
||||
@@ -83,7 +297,9 @@ The Instances list displays all the current nodes in your topology, along with r
|
||||
From this page, you can add, remove or run health checks on your nodes. Use the check boxes next to an instance to select it to remove or run a health check against. When a button is grayed-out, you do not have permission for that particular action. Contact your Administrator to grant you the required level of access. If you are able to remove an instance, you will receive a prompt for confirmation, like the one below:
|
||||
|
||||
.. image:: ../common/images/instances_delete_prompt.png
|
||||
:alt: Prompt for deleting instances in AWX.
|
||||
:alt: Prompt for deleting instances in AWX
|
||||
:width: 1400px
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
@@ -96,7 +312,8 @@ Click **Remove** to confirm.
|
||||
If running a health check on an instance, at the top of the Details page, a message displays that the health check is in progress.
|
||||
|
||||
.. image:: ../common/images/instances_health_check.png
|
||||
:alt: Health check for instances in AWX
|
||||
:alt: Health check for instances in AWX
|
||||
:width: 1400px
|
||||
|
||||
Click **Reload** to refresh the instance status.
|
||||
|
||||
@@ -104,162 +321,20 @@ Click **Reload** to refresh the instance status.
|
||||
|
||||
Health checks are ran asynchronously, and may take up to a minute for the instance status to update, even with a refresh. The status may or may not change after the health check. At the bottom of the Details page, a timer/clock icon displays next to the last known health check date and time stamp if the health check task is currently running.
|
||||
|
||||
.. image:: ../common/images/instances_health_check_pending.png
|
||||
:alt: Health check for instance still in pending state.
|
||||
.. image:: ../common/images/instances_health_check_pending.png
|
||||
:alt: Health check for instance still in pending state.
|
||||
|
||||
The example health check shows the status updates with an error on node 'one':
|
||||
|
||||
.. image:: ../common/images/topology-viewer-instance-with-errors.png
|
||||
:alt: Health check showing an error in one of the instances.
|
||||
|
||||
|
||||
Add an instance
|
||||
----------------
|
||||
|
||||
One of the ways to expand capacity is to create an instance. Standalone execution nodes can be added to run alongside the Kubernetes deployment of AWX. These machines will not be a part of the AWX Kubernetes cluster. The control nodes running in the cluster will connect and submit work to these machines via Receptor. The machines are registered in AWX as type "execution" instances, meaning they will only be used to run AWX jobs, not dispatch work or handle web requests as control nodes do.
|
||||
|
||||
Hop nodes can be added to sit between the control plane of AWX and standalone execution nodes. These machines will not be a part of the AWX Kubernetes cluster and they will be registered in AWX as node type "hop", meaning they will only handle inbound and outbound traffic for otherwise unreachable nodes in a different or more strict network.
|
||||
|
||||
Below is an example of an AWX task pod with two execution nodes. Traffic to execution node 2 flows through a hop node that is setup between it and the control plane.
|
||||
|
||||
.. image:: ../common/images/instances_awx_task_pods_hopnode.png
|
||||
:alt: AWX task pod with a hop node between the control plane of AWX and standalone execution nodes.
|
||||
|
||||
To create an instance in AWV:
|
||||
|
||||
1. Click **Instances** from the left side navigation menu of the AWX UI.
|
||||
|
||||
2. In the Instances list view, click the **Add** button and the Create new Instance window opens.
|
||||
|
||||
.. image:: ../common/images/instances_create_new.png
|
||||
:alt: Create a new instance form.
|
||||
|
||||
An instance has several attributes that may be configured:
|
||||
|
||||
- Enter a fully qualified domain name (ping-able DNS) or IP address for your instance in the **Host Name** field (required). This field is equivalent to ``hostname`` in the API.
|
||||
- Optionally enter a **Description** for the instance
|
||||
- The **Instance State** field is auto-populated, indicating that it is being installed, and cannot be modified
|
||||
- Optionally specify the **Listener Port** for the receptor to listen on for incoming connections. This is an open port on the remote machine used to establish inbound TCP connections. This field is equivalent to ``listener_port`` in the API.
|
||||
- Select from the options in **Instance Type** field to specify the type you want to create. Only execution and hop nodes can be created as operator-based installations do not support hybrid nodes. This field is equivalent to ``node_type`` in the API.
|
||||
- In the **Peers** field, select the instance hostnames you want your new instance to connect outbound to.
|
||||
- In the **Options** fields:
|
||||
- Check the **Enable Instance** box to make it available for jobs to run on an execution node.
|
||||
- Check the **Managed by Policy** box to allow policy to dictate how the instance is assigned.
|
||||
- Check the **Peers from control nodes** box to allow control nodes to peer to this instance automatically. Listener port needs to be set if this is enabled or the instance is a peer.
|
||||
|
||||
In the example diagram above, the configurations are as follows:
|
||||
|
||||
+------------------+---------------+--------------------------+--------------+
|
||||
| instance name | listener_port | peers_from_control_nodes | peers |
|
||||
+==================+===============+==========================+==============+
|
||||
| execution node 1 | 27199 | true | [] |
|
||||
+------------------+---------------+--------------------------+--------------+
|
||||
| hop node | 27199 | true | [] |
|
||||
+------------------+---------------+--------------------------+--------------+
|
||||
| execution node 2 | null | false | ["hop node"] |
|
||||
+------------------+---------------+--------------------------+--------------+
|
||||
|
||||
|
||||
3. Once the attributes are configured, click **Save** to proceed.
|
||||
|
||||
Upon successful creation, the Details of the one of the created instances opens.
|
||||
|
||||
.. image:: ../common/images/instances_create_details.png
|
||||
:alt: Details of the newly created instance.
|
||||
|
||||
.. note::
|
||||
|
||||
The proceeding steps 4-8 are intended to be ran from any computer that has SSH access to the newly created instance.
|
||||
|
||||
4. Click the download button next to the **Install Bundle** field to download the tarball that contain files to allow AWX to make proper TCP connections to the remote machine.
|
||||
|
||||
.. image:: ../common/images/instances_install_bundle.png
|
||||
:alt: Instance details showing the Download button in the Install Bundle field of the Details tab.
|
||||
|
||||
5. Extract the downloaded ``tar.gz`` file from the location you downloaded it. The install bundle contains TLS certificates and keys, a certificate authority, and a proper Receptor configuration file. To facilitate that these files will be in the right location on the remote machine, the install bundle includes an ``install_receptor.yml`` playbook. The playbook requires the Receptor collection which can be obtained via:
|
||||
|
||||
::
|
||||
|
||||
ansible-galaxy collection install -r requirements.yml
|
||||
|
||||
6. Before running the ``ansible-playbook`` command, edit the following fields in the ``inventory.yml`` file:
|
||||
|
||||
- ``ansible_user`` with the username running the installation
|
||||
- ``ansible_ssh_private_key_file`` to contain the filename of the private key used to connect to the instance
|
||||
|
||||
::
|
||||
|
||||
---
|
||||
all:
|
||||
hosts:
|
||||
remote-execution:
|
||||
ansible_host: 18.206.206.34
|
||||
ansible_user: <username> # user provided
|
||||
ansible_ssh_private_key_file: ~/.ssh/id_rsa
|
||||
|
||||
The content of the ``inventory.yml`` file serves as a template and contains variables for roles that are applied during the installation and configuration of a receptor node in a mesh topology. You may modify some of the other fields, or replace the file in its entirety for advanced scenarios. Refer to `Role Variables <https://github.com/ansible/receptor-collection/blob/main/README.md>`_ for more information on each variable.
|
||||
|
||||
7. Save the file to continue.
|
||||
|
||||
8. Run the following command on the machine you want to update your mesh:
|
||||
|
||||
::
|
||||
|
||||
ansible-playbook -i inventory.yml install_receptor.yml
|
||||
|
||||
Wait a few minutes for the periodic AWX task to do a health check against the new instance. You may run a health check by selecting the node and clicking the **Run health check** button from its Details page at any time. Once the instances endpoint or page reports a "Ready" status for the instance, jobs are now ready to run on this machine!
|
||||
|
||||
9. To view other instances within the same topology or associate peers, click the **Peers** tab.
|
||||
|
||||
.. image:: ../common/images/instances_peers_tab.png
|
||||
:alt: "Peers" tab showing two peers.
|
||||
|
||||
To associate peers with your node, click the **Associate** button to open a dialog box of instances eligible for peering.
|
||||
|
||||
.. image:: ../common/images/instances_associate_peer.png
|
||||
:alt: Instances available to peer with the example hop node.
|
||||
|
||||
Execution nodes can peer with either hop nodes or other execution nodes. Hop nodes can only peer with execution nodes unless you check the **Peers from control nodes** check box from the **Options** field.
|
||||
|
||||
.. note::
|
||||
|
||||
If you associate or disassociate a peer, a notification will inform you to re-run the install bundle from the Peer Detail view (the :ref:`ag_topology_viewer` has the download link).
|
||||
|
||||
.. image:: ../common/images/instances_associate_peer_reinstallmsg.png
|
||||
:alt: Notification to re-run the installation bundle due to change in the peering.
|
||||
|
||||
You can remove an instance by clicking **Remove** in the Instances page, or by setting the instance ``node_state = deprovisioning`` via the API. Upon deleting, a pop-up message will appear to notify that you may need to re-run the install bundle to make sure things that were removed are no longer connected.
|
||||
|
||||
|
||||
10. To view a graphical representation of your updated topology, refer to the :ref:`ag_topology_viewer` section of this guide.
|
||||
:alt: Health check showing an error in one of the instances.
|
||||
:width: 1400px
|
||||
|
||||
|
||||
Using a custom Receptor CA
|
||||
---------------------------
|
||||
|
||||
The control nodes on the K8S cluster will communicate with execution nodes via mutual TLS TCP connections, running via Receptor. Execution nodes will verify incoming connections by ensuring the x509 certificate was issued by a trusted Certificate Authority (CA).
|
||||
|
||||
You may choose to provide your own CA for this validation. If no CA is provided, AWX operator will automatically generate one using OpenSSL.
|
||||
|
||||
Given custom ``ca.crt`` and ``ca.key`` stored locally, run the following:
|
||||
|
||||
::
|
||||
|
||||
kubectl create secret tls awx-demo-receptor-ca \
|
||||
--cert=/path/to/ca.crt --key=/path/to/ca.key
|
||||
|
||||
The secret should be named ``{AWX Custom Resource name}-receptor-ca``. In the above, the AWX Custom Resource name is "awx-demo". Replace "awx-demo" with your AWX Custom Resource name.
|
||||
|
||||
If this secret is created after AWX is deployed, run the following to restart the deployment:
|
||||
|
||||
::
|
||||
|
||||
kubectl rollout restart deployment awx-demo
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
Changing the receptor CA will sever connections to any existing execution nodes. These nodes will enter an *Unavailable* state, and jobs will not be able to run on them. You will need to download and re-run the install bundle for each execution node. This will replace the TLS certificate files with those signed by the new CA. The execution nodes will then appear in a *Ready* state after a few minutes.
|
||||
Refer to the AWX Operator Documentation, `Custom Receptor CA <https://ansible.readthedocs.io/projects/awx-operator/en/latest/user-guide/advanced-configuration/custom-receptor-certs.html>`_ for detail.
|
||||
|
||||
|
||||
Using a private image for the default EE
|
||||
|
||||
@@ -7,6 +7,7 @@ Setting up LDAP Authentication
|
||||
single: LDAP
|
||||
pair: authentication; LDAP
|
||||
|
||||
This chapter describes how to integrate LDAP authentication with AWX.
|
||||
|
||||
.. note::
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 45 KiB |
BIN
docs/docsite/rst/common/images/download-icon.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 172 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 208 KiB |
BIN
docs/docsite/rst/common/images/instances_mesh_concept.drawio.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 238 KiB |
@@ -11,19 +11,13 @@ Release Notes
|
||||
pair: release notes; v23.3.0
|
||||
pair: release notes; v23.3.1
|
||||
pair: release notes; v23.4.0
|
||||
pair: release notes; v23.5.0
|
||||
pair: release notes; v23.5.1
|
||||
pair: release notes; v23.6.0
|
||||
pair: release notes; v23.7.0
|
||||
pair: release notes; v23.8.0
|
||||
pair: release notes; v23.8.1
|
||||
|
||||
|
||||
For versions older than 23.0.0, refer to `AWX Release Notes <https://github.com/ansible/awx/releases>`_.
|
||||
Refer to `AWX Release Notes <https://github.com/ansible/awx/releases>`_.
|
||||
|
||||
|
||||
- See `What's Changed for 23.4.0 <https://github.com/ansible/awx/releases/tag/23.4.0>`_.
|
||||
|
||||
- See `What's Changed for 23.3.1 <https://github.com/ansible/awx/releases/tag/23.3.1>`_.
|
||||
|
||||
- See `What's Changed for 23.3.0 <https://github.com/ansible/awx/releases/tag/23.3.0>`_.
|
||||
|
||||
- See `What's Changed for 23.2.0 <https://github.com/ansible/awx/releases/tag/23.2.0>`_.
|
||||
|
||||
- See `What's Changed for 23.1.0 <https://github.com/ansible/awx/releases/tag/23.1.0>`_.
|
||||
|
||||
- See `What's Changed for 23.0.0 <https://github.com/ansible/awx/releases/tag/23.0.0>`_.
|
||||
@@ -10,14 +10,15 @@ Secret Management System
|
||||
|
||||
Users and admins upload machine and cloud credentials so that automation can access machines and external services on their behalf. By default, sensitive credential values (such as SSH passwords, SSH private keys, API tokens for cloud services) are stored in the database after being encrypted. With external credentials backed by credential plugins, you can map credential fields (like a password or an SSH Private key) to values stored in a :term:`secret management system` instead of providing them to AWX directly. AWX provides a secret management system that include integrations for:
|
||||
|
||||
- Centrify Vault Credential Provider Lookup
|
||||
- CyberArk Central Credential Provider Lookup (CCP)
|
||||
- CyberArk Conjur Secrets Manager Lookup
|
||||
- HashiCorp Vault Key-Value Store (KV)
|
||||
- HashiCorp Vault SSH Secrets Engine
|
||||
- Microsoft Azure Key Management System (KMS)
|
||||
- Thycotic DevOps Secrets Vault
|
||||
- Thycotic Secret Server
|
||||
- :ref:`ug_credentials_aws_lookup`
|
||||
- :ref:`ug_credentials_centrify`
|
||||
- :ref:`ug_credentials_cyberarkccp`
|
||||
- :ref:`ug_credentials_cyberarkconjur`
|
||||
- :ref:`ug_credentials_hashivault` (KV)
|
||||
- :ref:`ug_credentials_hashivaultssh`
|
||||
- :ref:`ug_credentials_azurekeyvault` (KMS)
|
||||
- :ref:`ug_credentials_thycoticvault`
|
||||
- :ref:`ug_credentials_thycoticserver`
|
||||
|
||||
These external secret values will be fetched prior to running a playbook that needs them. For more information on specifying these credentials in the User Interface, see :ref:`ug_credentials`.
|
||||
|
||||
@@ -49,11 +50,92 @@ Use the AWX User Interface to configure and use each of the supported 3-party se
|
||||
.. image:: ../common/images/credentials-link-credential-prompt.png
|
||||
:alt: Credential section of the external secret management system dialog
|
||||
|
||||
4. Select the credential you want to link to, and click **Next**. This takes you to the **Metadata** tab of the input source. This example shows the Metadata prompt for HashiVault Secret Lookup. Metadata is specific to the input source you select. See the :ref:`ug_metadata_creds_inputs` table for details.
|
||||
4. Select the credential you want to link to, and click **Next**. This takes you to the **Metadata** tab of the input source. Metadata is specific to the input source you select:
|
||||
|
||||
.. list-table::
|
||||
:widths: 10 10 25
|
||||
:width: 1400px
|
||||
:header-rows: 1
|
||||
|
||||
* - Input Source
|
||||
- Metadata
|
||||
- Description
|
||||
* - *AWS Secrets Manager*
|
||||
- AWS Secrets Manager Region (required)
|
||||
- The region where the secrets manager is located.
|
||||
* -
|
||||
- AWS Secret Name (Required)
|
||||
- Specify the AWS secret name that was generated by the AWS access key.
|
||||
* - *Centrify Vault Credential Provider Lookup*
|
||||
- Account Name (Required)
|
||||
- Name of the system account or domain associated with Centrify Vault.
|
||||
* -
|
||||
- System Name
|
||||
- Specify the name used by the Centrify portal.
|
||||
* - *CyberArk Central Credential Provider Lookup*
|
||||
- Object Query (Required)
|
||||
- Lookup query for the object.
|
||||
* -
|
||||
- Object Query Format
|
||||
- Select ``Exact`` for a specific secret name, or ``Regexp`` for a secret that has a dynamically generated name.
|
||||
* -
|
||||
- Object Property
|
||||
- Specifies the name of the property to return (e.g., ``UserName``, ``Address``, etc.) other than the default of ``Content``.
|
||||
* -
|
||||
- Reason
|
||||
- If required per the object's policy, supply a reason for checking out the secret, as CyberArk logs those.
|
||||
* - *CyberArk Conjur Secrets Lookup*
|
||||
- Secret Identifier
|
||||
- The identifier for the secret.
|
||||
* -
|
||||
- Secret Version
|
||||
- Specify a version of the secret, if necessary, otherwise, leave it empty to use the latest version.
|
||||
* - *HashiVault Secret Lookup*
|
||||
- Name of Secret Backend
|
||||
- Specify the name of the KV backend to use. Leave it blank to use the first path segment of the **Path to Secret** field instead.
|
||||
* -
|
||||
- Path to Secret (required)
|
||||
- Specify the path to where the secret information is stored; for example, ``/path/username``.
|
||||
* -
|
||||
- Key Name (required)
|
||||
- Specify the name of the key to look up the secret information.
|
||||
* -
|
||||
- Secret Version (V2 Only)
|
||||
- Specify a version if necessary, otherwise, leave it empty to use the latest version.
|
||||
* - *HashiCorp Signed SSH*
|
||||
- Unsigned Public Key (required)
|
||||
- Specify the public key of the cert you want to get signed. It needs to be present in the authorized keys file of the target host(s).
|
||||
* -
|
||||
- Path to Secret (required)
|
||||
- Specify the path to where the secret information is stored; for example, ``/path/username``.
|
||||
* -
|
||||
- Role Name (required)
|
||||
- A role is a collection of SSH settings and parameters that are stored in Hashi vault. Typically, you can specify a couple of them with different privileges, timeouts, etc. So you could have a role that is allowed to get a cert signed for root, and other less privileged ones, for example.
|
||||
* -
|
||||
- Valid Principals
|
||||
- Specify a user (or users) other than the default, that you are requesting vault to authorize the cert for the stored key. Hashi vault has a default user for whom it signs (e.g., ec2-user).
|
||||
* - *Azure KMS*
|
||||
- Secret Name (required)
|
||||
- The actual name of the secret as it is referenced in Azure's Key vault app.
|
||||
* -
|
||||
- Secret Version
|
||||
- Specify a version of the secret, if necessary, otherwise, leave it empty to use the latest version.
|
||||
* - *Thycotic DevOps Secrets Vault*
|
||||
- Secret Path (required)
|
||||
- Specify the path to where the secret information is stored (e.g., /path/username).
|
||||
* - *Thycotic Secret Server*
|
||||
- Secret ID (required)
|
||||
- The identifier for the secret.
|
||||
* -
|
||||
- Secret Field
|
||||
- Specify the field to be used from the secret.
|
||||
|
||||
This example shows the Metadata prompt for HashiVault Secret Lookup.
|
||||
|
||||
.. image:: ../common/images/credentials-link-metadata-prompt.png
|
||||
:alt: Metadata section of the external secret management system dialog
|
||||
|
||||
|
||||
5. Click **Test** to verify connection to the secret management system. If the lookup is unsuccessful, an error message like this one displays:
|
||||
|
||||
.. image:: ../common/images/credentials-link-metadata-test-error.png
|
||||
@@ -65,133 +147,37 @@ Use the AWX User Interface to configure and use each of the supported 3-party se
|
||||
|
||||
8. Click **Save** when done.
|
||||
|
||||
.. _ug_metadata_creds_inputs:
|
||||
|
||||
Metadata for credential input sources
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
.. _ug_credentials_aws_lookup:
|
||||
|
||||
**Centrify Vault Credential Provider Lookup**
|
||||
AWS Secrets Manager Lookup
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
.. index::
|
||||
pair: credential types; AWS
|
||||
|
||||
.. list-table::
|
||||
:widths: 25 50
|
||||
:header-rows: 1
|
||||
This plugin allows AWS to be used as a credential input source to pull secrets from AWS SecretsManager. `AWS Secrets Manager <https://aws.amazon.com/secrets-manager/>`_ provides similar service to :ref:`ug_credentials_azurekeyvault`, and the AWS collection provides a lookup plugin for it.
|
||||
|
||||
* - Metadata
|
||||
- Description
|
||||
* - Account Name (Required)
|
||||
- Name of the system account or domain associated with Centrify Vault.
|
||||
* - System Name
|
||||
- Specify the name used by the Centrify portal.
|
||||
When **AWS Secrets Manager lookup** is selected for **Credential Type**, provide the following attributes to properly configure your lookup:
|
||||
|
||||
**CyberArk Central Credential Provider Lookup**
|
||||
- **AWS Access Key** (required): provide the access key used for communicating with AWS' key management system
|
||||
- **AWS Secret Key** (required): provide the secret as obtained by the AWS IAM console
|
||||
|
||||
.. list-table::
|
||||
:widths: 25 50
|
||||
:header-rows: 1
|
||||
|
||||
* - Metadata
|
||||
- Description
|
||||
* - Object Query (Required)
|
||||
- Lookup query for the object.
|
||||
* - Object Query Format
|
||||
- Select ``Exact`` for a specific secret name, or ``Regexp`` for a secret that has a dynamically generated name.
|
||||
* - Object Property
|
||||
- Specifies the name of the property to return (e.g., ``UserName``, ``Address``, etc.) other than the default of ``Content``.
|
||||
* - Reason
|
||||
- If required per the object's policy, supply a reason for checking out the secret, as CyberArk logs those.
|
||||
Below shows an example of a configured AWS Secret Manager credential.
|
||||
|
||||
**CyberArk Conjur Secrets Lookup**
|
||||
.. image:: ../common/images/credentials-create-aws-secret-credential.png
|
||||
:width: 1400px
|
||||
:alt: Example new AWS Secret Manager credential lookup dialog
|
||||
|
||||
.. list-table::
|
||||
:widths: 25 50
|
||||
:header-rows: 1
|
||||
|
||||
* - Metadata
|
||||
- Description
|
||||
* - Secret Identifier
|
||||
- The identifier for the secret.
|
||||
* - Secret Version
|
||||
- Specify a version of the secret, if necessary, otherwise, leave it empty to use the latest version.
|
||||
|
||||
**HashiVault Secret Lookup**
|
||||
|
||||
.. list-table::
|
||||
:widths: 25 50
|
||||
:header-rows: 1
|
||||
|
||||
* - Metadata
|
||||
- Description
|
||||
* - Name of Secret Backend
|
||||
- Specify the name of the KV backend to use. Leave it blank to use the first path segment of the **Path to Secret** field instead.
|
||||
* - Path to Secret (required)
|
||||
- Specify the path to where the secret information is stored; for example, ``/path/username``.
|
||||
* - Key Name (required)
|
||||
- Specify the name of the key to look up the secret information.
|
||||
* - Secret Version (V2 Only)
|
||||
- Specify a version if necessary, otherwise, leave it empty to use the latest version.
|
||||
|
||||
**HashiCorp Signed SSH**
|
||||
|
||||
.. list-table::
|
||||
:widths: 25 50
|
||||
:header-rows: 1
|
||||
|
||||
* - Metadata
|
||||
- Description
|
||||
* - Unsigned Public Key (required)
|
||||
- Specify the public key of the cert you want to get signed. It needs to be present in the authorized keys file of the target host(s).
|
||||
* - Path to Secret (required)
|
||||
- Specify the path to where the secret information is stored; for example, ``/path/username``.
|
||||
* - Role Name (required)
|
||||
- A role is a collection of SSH settings and parameters that are stored in Hashi vault. Typically, you can specify a couple of them with different privileges, timeouts, etc. So you could have a role that is allowed to get a cert signed for root, and other less privileged ones, for example.
|
||||
* - Valid Principals
|
||||
- Specify a user (or users) other than the default, that you are requesting vault to authorize the cert for the stored key. Hashi vault has a default user for whom it signs (e.g., ec2-user).
|
||||
|
||||
**Azure KMS**
|
||||
|
||||
.. list-table::
|
||||
:widths: 25 50
|
||||
:header-rows: 1
|
||||
|
||||
* - Metadata
|
||||
- Description
|
||||
* - Secret Name (required)
|
||||
- The actual name of the secret as it is referenced in Azure's Key vault app.
|
||||
* - Secret Version
|
||||
- Specify a version of the secret, if necessary, otherwise, leave it empty to use the latest version.
|
||||
|
||||
**Thycotic DevOps Secrets Vault**
|
||||
|
||||
.. list-table::
|
||||
:widths: 25 50
|
||||
:header-rows: 1
|
||||
|
||||
* - Metadata
|
||||
- Description
|
||||
* - Secret Path (required)
|
||||
- Specify the path to where the secret information is stored (e.g., /path/username).
|
||||
|
||||
**Thycotic Secret Server**
|
||||
|
||||
.. list-table::
|
||||
:widths: 25 50
|
||||
:header-rows: 1
|
||||
|
||||
* - Metadata
|
||||
- Description
|
||||
* - Secret ID (required)
|
||||
- The identifier for the secret.
|
||||
* - Secret Field
|
||||
- Specify the field to be used from the secret.
|
||||
|
||||
.. _ug_credentials_centrify:
|
||||
|
||||
Centrify Vault Credential Provider Lookup
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
.. index::
|
||||
pair: credential types; Centrify
|
||||
|
||||
You need the Centrify Vault web service running to store secrets in order for this integration to work. When **Centrify Vault Credential Provider Lookup** is selected for **Credential Type**, provide the following metadata to properly configure your lookup:
|
||||
You need the Centrify Vault web service running to store secrets in order for this integration to work. When **Centrify Vault Credential Provider Lookup** is selected for **Credential Type**, provide the following attributes to properly configure your lookup:
|
||||
|
||||
- **Centrify Tenant URL** (required): provide the URL used for communicating with Centrify's secret management system
|
||||
- **Centrify API User** (required): provide the username
|
||||
@@ -208,12 +194,12 @@ Below shows an example of a configured CyberArk AIM credential.
|
||||
.. _ug_credentials_cyberarkccp:
|
||||
|
||||
CyberArk Central Credential Provider (CCP) Lookup
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
.. index::
|
||||
single: CyberArk CCP
|
||||
pair: credential; CyberArk CCP
|
||||
|
||||
You need the CyberArk Central Credential Provider web service running to store secrets in order for this integration to work. When **CyberArk Central Credential Provider Lookup** is selected for **Credential Type**, provide the following metadata to properly configure your lookup:
|
||||
You need the CyberArk Central Credential Provider web service running to store secrets in order for this integration to work. When **CyberArk Central Credential Provider Lookup** is selected for **Credential Type**, provide the following attributes to properly configure your lookup:
|
||||
|
||||
- **CyberArk CCP URL** (required): provide the URL used for communicating with CyberArk CCP's secret management system; must include URL scheme (http, https, etc.)
|
||||
- **Web Service ID**: optionally specify the identifier for the web service; leaving it blank defaults to AIMWebService
|
||||
@@ -230,14 +216,14 @@ Below shows an example of a configured CyberArk CCP credential.
|
||||
.. _ug_credentials_cyberarkconjur:
|
||||
|
||||
CyberArk Conjur Secrets Manager Lookup
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
.. index::
|
||||
single: CyberArk Conjur
|
||||
pair: credential; CyberArk Conjur
|
||||
|
||||
With a Conjur Cloud tenant available to target, configure the CyberArk Conjur Secrets Lookup external management system credential plugin as documented.
|
||||
|
||||
When **CyberArk Conjur Secrets Manager Lookup** is selected for **Credential Type**, provide the following metadata to properly configure your lookup:
|
||||
When **CyberArk Conjur Secrets Manager Lookup** is selected for **Credential Type**, provide the following attributes to properly configure your lookup:
|
||||
|
||||
- **Conjur URL** (required): provide the URL used for communicating with CyberArk Conjur's secret management system; must include URL scheme (http, https, etc.)
|
||||
- **API Key** (required): provide the key given by your Conjur admin
|
||||
@@ -253,12 +239,12 @@ Below shows an example of a configured CyberArk Conjur credential.
|
||||
.. _ug_credentials_hashivault:
|
||||
|
||||
HashiCorp Vault Secret Lookup
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
.. index::
|
||||
single: HashiCorp Secret Lookup
|
||||
pair: credential; HashiCorp KV
|
||||
|
||||
When **HashiCorp Vault Secret Lookup** is selected for **Credential Type**, provide the following metadata to properly configure your lookup:
|
||||
When **HashiCorp Vault Secret Lookup** is selected for **Credential Type**, provide the following attributes to properly configure your lookup:
|
||||
|
||||
- **Server URL** (required): provide the URL used for communicating with HashiCorp Vault's secret management system
|
||||
- **Token**: specify the access token used to authenticate HashiCorp's server
|
||||
@@ -291,7 +277,7 @@ Below shows an example of a configured HashiCorp Vault Secret Lookup credential
|
||||
.. image:: ../common/images/credentials-create-hashicorp-kv-credential.png
|
||||
:alt: Example new HashiCorp Vault Secret lookup dialog
|
||||
|
||||
To test the lookup, create another credential that uses the HashiCorp Vault lookup. The example below shows the metadata for a machine credential configured to look up HashiCorp Vault secret credentials:
|
||||
To test the lookup, create another credential that uses the HashiCorp Vault lookup. The example below shows the attributes for a machine credential configured to look up HashiCorp Vault secret credentials:
|
||||
|
||||
.. image:: ../common/images/credentials-machine-test-hashicorp-metadata.png
|
||||
:alt: Example machine credential lookup metadata for HashiCorp Vault.
|
||||
@@ -300,12 +286,12 @@ To test the lookup, create another credential that uses the HashiCorp Vault look
|
||||
.. _ug_credentials_hashivaultssh:
|
||||
|
||||
HashiCorp Vault Signed SSH
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
.. index::
|
||||
single: HashiCorp SSH Secrets Engine
|
||||
pair: credential; HashiCorp SSH Secrets Engine
|
||||
|
||||
When **HashiCorp Vault Signed SSH** is selected for **Credential Type**, provide the following metadata to properly configure your lookup:
|
||||
When **HashiCorp Vault Signed SSH** is selected for **Credential Type**, provide the following attributes to properly configure your lookup:
|
||||
|
||||
- **Server URL** (required): provide the URL used for communicating with HashiCorp Signed SSH's secret management system
|
||||
- **Token**: specify the access token used to authenticate HashiCorp's server
|
||||
@@ -335,13 +321,13 @@ Below shows an example of a configured HashiCorp SSH Secrets Engine credential.
|
||||
.. _ug_credentials_azurekeyvault:
|
||||
|
||||
Microsoft Azure Key Vault
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
.. index::
|
||||
single: MS Azure KMS
|
||||
pair: credential; MS Azure KMS
|
||||
triple: credential; Azure; KMS
|
||||
|
||||
When **Microsoft Azure Key Vault** is selected for **Credential Type**, provide the following metadata to properly configure your lookup:
|
||||
When **Microsoft Azure Key Vault** is selected for **Credential Type**, provide the following attributes to properly configure your lookup:
|
||||
|
||||
- **Vault URL (DNS Name)** (required): provide the URL used for communicating with MS Azure's key management system
|
||||
- **Client ID** (required): provide the identifier as obtained by the Azure Active Directory
|
||||
@@ -357,12 +343,12 @@ Below shows an example of a configured Microsoft Azure KMS credential.
|
||||
.. _ug_credentials_thycoticvault:
|
||||
|
||||
Thycotic DevOps Secrets Vault
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
.. index::
|
||||
single: Thycotic DevOps Secrets Vault
|
||||
pair: credential; Thycotic DevOps Secrets Vault
|
||||
|
||||
When **Thycotic DevOps Secrets Vault** is selected for **Credential Type**, provide the following metadata to properly configure your lookup:
|
||||
When **Thycotic DevOps Secrets Vault** is selected for **Credential Type**, provide the following attributes to properly configure your lookup:
|
||||
|
||||
- **Tenant** (required): provide the URL used for communicating with Thycotic's secret management system
|
||||
- **Top-level Domain (TLD)** : provide the top-level domain designation (e.g., com, edu, org) associated with the secret vault you want to integrate
|
||||
@@ -379,12 +365,12 @@ Below shows an example of a configured Thycotic DevOps Secrets Vault credential.
|
||||
.. _ug_credentials_thycoticserver:
|
||||
|
||||
Thycotic Secret Server
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
.. index::
|
||||
single: Thycotic Secret Server
|
||||
pair: credential; Thycotic Secret Server
|
||||
|
||||
When **Thycotic Secrets Server** is selected for **Credential Type**, provide the following metadata to properly configure your lookup:
|
||||
When **Thycotic Secrets Server** is selected for **Credential Type**, provide the following attributes to properly configure your lookup:
|
||||
|
||||
- **Secret Server URL** (required): provide the URL used for communicating with the Thycotic Secrets Server management system
|
||||
- **Username** (required): specify the authenticated user for this service
|
||||
|
||||
@@ -128,7 +128,7 @@ The following credential types are supported with AWX:
|
||||
.. contents::
|
||||
:local:
|
||||
|
||||
The credential types associated with Centrify, CyberArk, HashiCorp Vault, Microsoft Azure Key Management System (KMS), and Thycotic are part of the credential plugins capability that allows an external system to lookup your secrets information. See the :ref:`ug_credential_plugins` section for further detail.
|
||||
The credential types associated with AWS Secrets Manager, Centrify, CyberArk, HashiCorp Vault, Microsoft Azure Key Management System (KMS), and Thycotic are part of the credential plugins capability that allows an external system to lookup your secrets information. See the :ref:`ug_credential_plugins` section for further detail.
|
||||
|
||||
|
||||
.. _ug_credentials_aws:
|
||||
@@ -166,6 +166,10 @@ AWX provides support for EC2 STS tokens (sometimes referred to as IAM STS creden
|
||||
|
||||
To use implicit IAM role credentials, do not attach AWS cloud credentials in AWX when relying on IAM roles to access the AWS API. While it may seem to make sense to attach your AWS cloud credential to your job template, doing so will force the use of your AWS credentials and will not "fall through" to use your IAM role credentials (this is due to the use of the boto library.)
|
||||
|
||||
AWS Secrets Manager
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
This is considered part of the secret management capability. See :ref:`ug_credentials_aws_lookup` for more detail.
|
||||
|
||||
|
||||
Ansible Galaxy/Automation Hub API Token
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
@@ -90,19 +90,6 @@ Glossary
|
||||
Node
|
||||
A node corresponds to entries in the instance database model, or the ``/api/v2/instances/`` endpoint, and is a machine participating in the cluster / mesh. The unified jobs API reports ``awx_node`` and ``execution_node`` fields. The execution node is where the job runs, and AWX node interfaces between the job and server functions.
|
||||
|
||||
+-----------+-----------------------------------------------------------------------------------------------------------------+
|
||||
| Node Type | Description |
|
||||
+-----------+-----------------------------------------------------------------------------------------------------------------+
|
||||
| Control | Nodes that run persistent Ansible Automation Platform services, and delegate jobs to hybrid and execution nodes |
|
||||
+-----------+-----------------------------------------------------------------------------------------------------------------+
|
||||
| Hybrid | Nodes that run persistent Ansible Automation Platform services and execute jobs |
|
||||
| | (not applicable to operator-based installations) |
|
||||
+-----------+-----------------------------------------------------------------------------------------------------------------+
|
||||
| Hop | Used for relaying across the mesh only |
|
||||
+-----------+-----------------------------------------------------------------------------------------------------------------+
|
||||
| Execution | Nodes that run jobs delivered from control nodes (jobs submitted from the user’s Ansible automation) |
|
||||
+-----------+-----------------------------------------------------------------------------------------------------------------+
|
||||
|
||||
Notification Template
|
||||
An instance of a notification type (Email, Slack, Webhook, etc.) with a name, description, and a defined configuration.
|
||||
|
||||
|
||||
BIN
docs/img/pypi_files.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
@@ -124,10 +124,14 @@ This workflow will take the generated images and promote them to quay.io in addi
|
||||
|
||||

|
||||
|
||||
8. Go to awxkit's page on [PiPy](https://pypi.org/project/awxkit/#history) and validate the latest release is there:
|
||||
8. Go to awxkit's page on [PyPi](https://pypi.org/project/awxkit/#history) and validate the latest release is there:
|
||||
|
||||

|
||||
|
||||
9. While verifying that awxkit was published on Pypi, also validate that the latest version of the [tar](https://pypi.org/project/awxkit/#files) file is there as well.
|
||||
|
||||

|
||||
|
||||
### Releasing the AWX operator
|
||||
|
||||
Once the AWX image is live, we can now release the AWX operator.
|
||||
|
||||
176
licenses/Cython.txt
Normal file
@@ -0,0 +1,176 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
@@ -1,4 +1,4 @@
|
||||
aiohttp
|
||||
aiohttp>=3.8.6 # CVE-2023-47627
|
||||
ansiconv==1.0.0 # UPGRADE BLOCKER: from 2013, consider replacing instead of upgrading
|
||||
asciichartpy
|
||||
asn1
|
||||
@@ -8,7 +8,8 @@ boto3
|
||||
botocore
|
||||
channels
|
||||
channels-redis==3.4.1 # see UPGRADE BLOCKERs
|
||||
cryptography>=41.0.2 # CVE-2023-38325
|
||||
cryptography>=41.0.6 # CVE-2023-49083
|
||||
Cython<3 # this is needed as a build dependency, one day we may have separated build deps
|
||||
daphne
|
||||
distro
|
||||
django==4.2.6 # CVE-2023-43665
|
||||
@@ -25,15 +26,15 @@ django-split-settings==1.0.0 # We hit a strange issue where the release proce
|
||||
djangorestframework
|
||||
djangorestframework-yaml
|
||||
filelock
|
||||
GitPython>=3.1.32 # CVE-2023-40267
|
||||
GitPython>=3.1.37 # CVE-2023-41040
|
||||
hiredis==2.0.0 # see UPGRADE BLOCKERs
|
||||
irc
|
||||
jinja2
|
||||
jinja2>=3.1.3 # CVE-2024-22195
|
||||
JSON-log-formatter
|
||||
jsonschema
|
||||
Markdown # used for formatting API help
|
||||
openshift
|
||||
pexpect==4.7.0 # see library notes
|
||||
pexpect==4.7.0 # see library notes
|
||||
prometheus_client
|
||||
psycopg
|
||||
psutil
|
||||
@@ -48,20 +49,20 @@ pyyaml>=6.0.1
|
||||
receptorctl
|
||||
social-auth-core[openidconnect]==4.4.2 # see UPGRADE BLOCKERs
|
||||
social-auth-app-django==5.4.0 # see UPGRADE BLOCKERs
|
||||
sqlparse >= 0.4.4 # Required by django https://github.com/ansible/awx/security/dependabot/96
|
||||
sqlparse>=0.4.4 # Required by django https://github.com/ansible/awx/security/dependabot/96
|
||||
redis
|
||||
requests
|
||||
slack-sdk
|
||||
tacacs_plus==1.0 # UPGRADE BLOCKER: auth does not work with later versions
|
||||
twilio
|
||||
twisted[tls]
|
||||
twisted[tls]>=23.10.0 # CVE-2023-46137
|
||||
uWSGI
|
||||
uwsgitop
|
||||
wheel>=0.38.1 # CVE-2022-40898
|
||||
wheel>=0.38.1 # CVE-2022-40898
|
||||
pip==21.2.4 # see UPGRADE BLOCKERs
|
||||
setuptools # see UPGRADE BLOCKERs
|
||||
setuptools_scm[toml] # see UPGRADE BLOCKERs, xmlsec build dep
|
||||
setuptools-rust >= 0.11.4 # cryptography build dep
|
||||
setuptools-rust>=0.11.4 # cryptography build dep
|
||||
pkgconfig>=1.5.1 # xmlsec build dep - needed for offline build
|
||||
|
||||
# Temporarily added to use ansible-runner from git branch, to be removed
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
adal==1.2.7
|
||||
# via msrestazure
|
||||
aiohttp==3.8.3
|
||||
aiohttp==3.9.3
|
||||
# via -r /awx_devel/requirements/requirements.in
|
||||
aioredis==1.3.1
|
||||
# via channels-redis
|
||||
@@ -70,14 +70,12 @@ channels==3.0.5
|
||||
channels-redis==3.4.1
|
||||
# via -r /awx_devel/requirements/requirements.in
|
||||
charset-normalizer==2.1.1
|
||||
# via
|
||||
# aiohttp
|
||||
# requests
|
||||
# via requests
|
||||
click==8.1.3
|
||||
# via receptorctl
|
||||
constantly==15.1.0
|
||||
# via twisted
|
||||
cryptography==41.0.3
|
||||
cryptography==41.0.7
|
||||
# via
|
||||
# -r /awx_devel/requirements/requirements.in
|
||||
# adal
|
||||
@@ -88,6 +86,8 @@ cryptography==41.0.3
|
||||
# pyopenssl
|
||||
# service-identity
|
||||
# social-auth-core
|
||||
cython==0.29.32
|
||||
# via -r /awx_devel/requirements/requirements.in
|
||||
daphne==3.0.2
|
||||
# via
|
||||
# -r /awx_devel/requirements/requirements.in
|
||||
@@ -161,7 +161,7 @@ frozenlist==1.3.3
|
||||
# aiosignal
|
||||
gitdb==4.0.10
|
||||
# via gitpython
|
||||
gitpython==3.1.32
|
||||
gitpython==3.1.42
|
||||
# via -r /awx_devel/requirements/requirements.in
|
||||
google-auth==2.14.1
|
||||
# via kubernetes
|
||||
@@ -214,7 +214,7 @@ jaraco-text==3.11.0
|
||||
# via
|
||||
# irc
|
||||
# jaraco-collections
|
||||
jinja2==3.1.2
|
||||
jinja2==3.1.3
|
||||
# via -r /awx_devel/requirements/requirements.in
|
||||
jmespath==1.0.1
|
||||
# via
|
||||
@@ -360,7 +360,7 @@ pyyaml==6.0.1
|
||||
# djangorestframework-yaml
|
||||
# kubernetes
|
||||
# receptorctl
|
||||
receptorctl==1.4.2
|
||||
receptorctl==1.4.4
|
||||
# via -r /awx_devel/requirements/requirements.in
|
||||
redis==4.3.5
|
||||
# via -r /awx_devel/requirements/requirements.in
|
||||
@@ -438,7 +438,7 @@ tomli==2.0.1
|
||||
# via setuptools-scm
|
||||
twilio==7.15.3
|
||||
# via -r /awx_devel/requirements/requirements.in
|
||||
twisted[tls]==22.10.0
|
||||
twisted[tls]==23.10.0
|
||||
# via
|
||||
# -r /awx_devel/requirements/requirements.in
|
||||
# daphne
|
||||
|
||||
@@ -8,6 +8,7 @@ ipython>=7.31.1 # https://github.com/ansible/awx/security/dependabot/30
|
||||
unittest2
|
||||
black
|
||||
pytest!=7.0.0
|
||||
pytest-asyncio
|
||||
pytest-cov
|
||||
pytest-django
|
||||
pytest-mock==1.11.1
|
||||
|
||||
@@ -23,7 +23,7 @@ generate_requirements() {
|
||||
# FIXME: https://github.com/jazzband/pip-tools/issues/1558
|
||||
${venv}/bin/python3 -m pip install -U 'pip<22.0' pip-tools
|
||||
|
||||
${pip_compile} "$1" --output-file requirements.txt
|
||||
${pip_compile} $1 --output-file requirements.txt
|
||||
# consider the git requirements for purposes of resolving deps
|
||||
# Then remove any git+ lines from requirements.txt
|
||||
if [[ "$sanitize_git" == "1" ]] ; then
|
||||
|
||||
@@ -22,6 +22,7 @@ RUN rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-centosofficial
|
||||
RUN dnf -y update && dnf install -y 'dnf-command(config-manager)' && \
|
||||
dnf config-manager --set-enabled crb && \
|
||||
dnf -y install \
|
||||
iputils \
|
||||
gcc \
|
||||
gcc-c++ \
|
||||
git-core \
|
||||
|
||||
@@ -10,7 +10,7 @@ location {{ (ingress_path + '/favicon.ico').replace('//', '/') }} {
|
||||
alias /awx_devel/awx/public/static/favicon.ico;
|
||||
}
|
||||
|
||||
location ~ ({{ (ingress_path + '/websocket').replace('//', '/') }}|{{ (ingress_path + '/api/websocket').replace('//', '/') }}) {
|
||||
location ~ ^({{ (ingress_path + '/websocket/').replace('//', '/') }}|{{ (ingress_path + '/api/websocket/').replace('//', '/') }}) {
|
||||
# Pass request to the upstream alias
|
||||
proxy_pass http://daphne;
|
||||
# Require http version 1.1 to allow for upgrade requests
|
||||
|
||||
@@ -96,6 +96,7 @@ stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
|
||||
[program:awx-rsyslogd]
|
||||
command = rsyslogd -n -i /var/run/awx-rsyslog/rsyslog.pid -f /var/lib/awx/rsyslog/rsyslog.conf
|
||||
autorestart = true
|
||||
|
||||
@@ -12,7 +12,7 @@ SOSREPORT_CONTROLLER_COMMANDS = [
|
||||
"awx-manage run_dispatcher --status", # controller dispatch worker status
|
||||
"awx-manage run_callback_receiver --status", # controller callback worker status
|
||||
"awx-manage check_license --data", # controller license status
|
||||
"awx-manage run_wsbroadcast --status", # controller broadcast websocket status
|
||||
"awx-manage run_wsrelay --status", # controller websocket relay status
|
||||
"supervisorctl status", # controller process status
|
||||
"/var/lib/awx/venv/awx/bin/pip freeze", # pip package list
|
||||
"/var/lib/awx/venv/awx/bin/pip freeze -l", # pip package list without globally-installed packages
|
||||
|
||||