Compare commits
29 Commits
23.8.1
...
daoneill-i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfb0d15e6f | ||
|
|
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 |
10
.github/actions/awx_devel_image/action.yml
vendored
@@ -11,6 +11,12 @@ runs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: echo py_version=`make PYTHON_VERSION` >> $GITHUB_ENV
|
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
|
- name: Log in to registry
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
@@ -18,11 +24,11 @@ runs:
|
|||||||
|
|
||||||
- name: Pre-pull latest devel image to warm cache
|
- name: Pre-pull latest devel image to warm cache
|
||||||
shell: bash
|
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
|
- name: Build image for current source checkout
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
DEV_DOCKER_TAG_BASE=ghcr.io/${{ github.repository_owner }} \
|
DEV_DOCKER_TAG_BASE=ghcr.io/${OWNER_LC} \
|
||||||
COMPOSE_TAG=${{ github.base_ref }} \
|
COMPOSE_TAG=${{ github.base_ref }} \
|
||||||
make docker-compose-build
|
make docker-compose-build
|
||||||
|
|||||||
40
.github/actions/issue_metrics/issue_metrics.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
name: Monthly issue metrics
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
- cron: '3 2 1 * *'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
pull-requests: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: issue metrics
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Get dates for last month
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
# Calculate the first day of the previous month
|
||||||
|
first_day=$(date -d "last month" +%Y-%m-01)
|
||||||
|
|
||||||
|
# Calculate the last day of the previous month
|
||||||
|
last_day=$(date -d "$first_day +1 month -1 day" +%Y-%m-%d)
|
||||||
|
|
||||||
|
#Set an environment variable with the date range
|
||||||
|
echo "$first_day..$last_day"
|
||||||
|
echo "last_month=$first_day..$last_day" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Run issue-metrics tool
|
||||||
|
uses: github/issue-metrics@v2
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
SEARCH_QUERY: 'repo:ansible/awx is:issue created:${{ env.last_month }} -reason:"not planned"'
|
||||||
|
|
||||||
|
- name: Create issue
|
||||||
|
uses: peter-evans/create-issue-from-file@v4
|
||||||
|
with:
|
||||||
|
title: Monthly issue metrics report
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
content-filepath: ./issue_metrics.md
|
||||||
2
.github/actions/run_awx_devel/action.yml
vendored
@@ -35,7 +35,7 @@ runs:
|
|||||||
- name: Start AWX
|
- name: Start AWX
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
DEV_DOCKER_TAG_BASE=ghcr.io/${{ github.repository_owner }} \
|
DEV_DOCKER_OWNER=${{ github.repository_owner }} \
|
||||||
COMPOSE_TAG=${{ github.base_ref }} \
|
COMPOSE_TAG=${{ github.base_ref }} \
|
||||||
COMPOSE_UP_OPTS="-d" \
|
COMPOSE_UP_OPTS="-d" \
|
||||||
make docker-compose
|
make docker-compose
|
||||||
|
|||||||
3
.github/pr_labeler.yml
vendored
@@ -15,5 +15,4 @@
|
|||||||
|
|
||||||
"dependencies":
|
"dependencies":
|
||||||
- any: ["awx/ui/package.json"]
|
- any: ["awx/ui/package.json"]
|
||||||
- any: ["requirements/*.txt"]
|
- any: ["requirements/*"]
|
||||||
- any: ["requirements/requirements.in"]
|
|
||||||
|
|||||||
63
.github/workflows/devel_images.yml
vendored
@@ -9,22 +9,43 @@ on:
|
|||||||
- release_*
|
- release_*
|
||||||
- feature_*
|
- feature_*
|
||||||
jobs:
|
jobs:
|
||||||
push:
|
push-development-images:
|
||||||
if: endsWith(github.repository, '/awx') || startsWith(github.ref, 'refs/heads/release_')
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 60
|
timeout-minutes: 120
|
||||||
permissions:
|
permissions:
|
||||||
packages: write
|
packages: write
|
||||||
contents: read
|
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:
|
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
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Get python version from Makefile
|
- name: Set up QEMU
|
||||||
run: echo py_version=`make PYTHON_VERSION` >> $GITHUB_ENV
|
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: |
|
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:
|
env:
|
||||||
OWNER: '${{ github.repository_owner }}'
|
OWNER: '${{ github.repository_owner }}'
|
||||||
|
|
||||||
@@ -37,23 +58,19 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
|
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
|
||||||
|
|
||||||
- name: Pre-pull image to warm build cache
|
- name: Setup node and npm
|
||||||
run: |
|
uses: actions/setup-node@v2
|
||||||
docker pull ghcr.io/${OWNER_LC}/awx_devel:${GITHUB_REF##*/} || :
|
with:
|
||||||
docker pull ghcr.io/${OWNER_LC}/awx_kube_devel:${GITHUB_REF##*/} || :
|
node-version: '16.13.1'
|
||||||
docker pull ghcr.io/${OWNER_LC}/awx:${GITHUB_REF##*/} || :
|
if: matrix.build-targets.image-name == 'awx'
|
||||||
|
|
||||||
- name: Build images
|
- name: Prebuild UI for awx image (to speed up build process)
|
||||||
run: |
|
run: |
|
||||||
DEV_DOCKER_TAG_BASE=ghcr.io/${OWNER_LC} COMPOSE_TAG=${GITHUB_REF##*/} make docker-compose-build
|
sudo apt-get install gettext
|
||||||
DEV_DOCKER_TAG_BASE=ghcr.io/${OWNER_LC} COMPOSE_TAG=${GITHUB_REF##*/} make awx-kube-dev-build
|
make ui-release
|
||||||
DEV_DOCKER_TAG_BASE=ghcr.io/${OWNER_LC} COMPOSE_TAG=${GITHUB_REF##*/} make awx-kube-build
|
make ui-next
|
||||||
|
if: matrix.build-targets.image-name == 'awx'
|
||||||
|
|
||||||
- name: Push development images
|
- name: Build and push AWX devel images
|
||||||
run: |
|
run: |
|
||||||
docker push ghcr.io/${OWNER_LC}/awx_devel:${GITHUB_REF##*/}
|
make ${{ matrix.build-targets.make-target }}
|
||||||
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')
|
|
||||||
|
|||||||
62
Makefile
@@ -75,6 +75,9 @@ SDIST_TAR_FILE ?= $(SDIST_TAR_NAME).tar.gz
|
|||||||
|
|
||||||
I18N_FLAG_FILE = .i18n_built
|
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 \
|
.PHONY: awx-link clean clean-tmp clean-venv requirements requirements_dev \
|
||||||
develop refresh adduser migrate dbchange \
|
develop refresh adduser migrate dbchange \
|
||||||
receiver test test_unit test_coverage coverage_html \
|
receiver test test_unit test_coverage coverage_html \
|
||||||
@@ -586,28 +589,20 @@ docker-compose-build: Dockerfile.dev
|
|||||||
--build-arg BUILDKIT_INLINE_CACHE=1 \
|
--build-arg BUILDKIT_INLINE_CACHE=1 \
|
||||||
--cache-from=$(DEV_DOCKER_TAG_BASE)/awx_devel:$(COMPOSE_TAG) .
|
--cache-from=$(DEV_DOCKER_TAG_BASE)/awx_devel:$(COMPOSE_TAG) .
|
||||||
|
|
||||||
# ## Build awx_devel image for docker compose development environment for multiple architectures
|
|
||||||
# docker-compose-buildx: Dockerfile.dev
|
|
||||||
# DOCKER_BUILDKIT=1 docker build \
|
|
||||||
# -f Dockerfile.dev \
|
|
||||||
# -t $(DEVEL_IMAGE_NAME) \
|
|
||||||
# --build-arg BUILDKIT_INLINE_CACHE=1 \
|
|
||||||
# --cache-from=$(DEV_DOCKER_TAG_BASE)/awx_devel:$(COMPOSE_TAG) .
|
|
||||||
|
|
||||||
## Build awx_devel image for docker compose development environment for multiple architectures
|
|
||||||
# PLATFORMS defines the target platforms for the manager image be build to provide support to multiple
|
|
||||||
# architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to:
|
|
||||||
# - able to use docker buildx . More info: https://docs.docker.com/build/buildx/
|
|
||||||
# - have enable BuildKit, More info: https://docs.docker.com/develop/develop-images/build_enhancements/
|
|
||||||
# - be able to push the image for your registry (i.e. if you do not inform a valid value via IMG=<myregistry/image:<tag>> than the export will fail)
|
|
||||||
# To properly provided solutions that supports more than one platform you should use this option.
|
|
||||||
PLATFORMS ?= linux/amd64,linux/arm64 # linux/ppc64le,linux/s390x
|
|
||||||
.PHONY: docker-compose-buildx
|
.PHONY: docker-compose-buildx
|
||||||
docker-compose-buildx: Dockerfile.dev ## Build and push docker image for the manager for cross-platform support
|
## Build awx_devel image for docker compose development environment for multiple architectures
|
||||||
- docker buildx create --name project-v3-builder
|
docker-compose-buildx: Dockerfile.dev
|
||||||
docker buildx use project-v3-builder
|
- docker buildx create --name docker-compose-buildx
|
||||||
- docker buildx build --push $(BUILD_ARGS) --platform=$(PLATFORMS) --tag $(DEVEL_IMAGE_NAME) -f Dockerfile.dev .
|
docker buildx use docker-compose-buildx
|
||||||
- docker buildx rm project-v3-builder
|
- 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:
|
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 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);)
|
||||||
@@ -671,6 +666,21 @@ awx-kube-build: Dockerfile
|
|||||||
--build-arg HEADLESS=$(HEADLESS) \
|
--build-arg HEADLESS=$(HEADLESS) \
|
||||||
-t $(DEV_DOCKER_TAG_BASE)/awx:$(COMPOSE_TAG) .
|
-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
|
.PHONY: Dockerfile.kube-dev
|
||||||
## Generate Docker.kube-dev for awx_kube_devel image
|
## Generate Docker.kube-dev for awx_kube_devel image
|
||||||
Dockerfile.kube-dev: tools/ansible/roles/dockerfile/templates/Dockerfile.j2
|
Dockerfile.kube-dev: tools/ansible/roles/dockerfile/templates/Dockerfile.j2
|
||||||
@@ -687,6 +697,18 @@ awx-kube-dev-build: Dockerfile.kube-dev
|
|||||||
--cache-from=$(DEV_DOCKER_TAG_BASE)/awx_kube_devel:$(COMPOSE_TAG) \
|
--cache-from=$(DEV_DOCKER_TAG_BASE)/awx_kube_devel:$(COMPOSE_TAG) \
|
||||||
-t $(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-dev-load: awx-kube-dev-build
|
||||||
$(KIND_BIN) load docker-image $(DEV_DOCKER_TAG_BASE)/awx_kube_devel:$(COMPOSE_TAG)
|
$(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['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['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})
|
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})
|
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 self.context['request'].user.is_superuser or self.context['request'].user.is_system_auditor:
|
||||||
if obj.node_type == 'execution':
|
if obj.node_type == 'execution':
|
||||||
|
|||||||
@@ -272,16 +272,24 @@ class DashboardJobsGraphView(APIView):
|
|||||||
|
|
||||||
success_query = user_unified_jobs.filter(status='successful')
|
success_query = user_unified_jobs.filter(status='successful')
|
||||||
failed_query = user_unified_jobs.filter(status='failed')
|
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':
|
if job_type == 'inv_sync':
|
||||||
success_query = success_query.filter(instance_of=models.InventoryUpdate)
|
success_query = success_query.filter(instance_of=models.InventoryUpdate)
|
||||||
failed_query = failed_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':
|
elif job_type == 'playbook_run':
|
||||||
success_query = success_query.filter(instance_of=models.Job)
|
success_query = success_query.filter(instance_of=models.Job)
|
||||||
failed_query = failed_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':
|
elif job_type == 'scm_update':
|
||||||
success_query = success_query.filter(instance_of=models.ProjectUpdate)
|
success_query = success_query.filter(instance_of=models.ProjectUpdate)
|
||||||
failed_query = failed_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()
|
end = now()
|
||||||
interval = 'day'
|
interval = 'day'
|
||||||
@@ -297,10 +305,12 @@ class DashboardJobsGraphView(APIView):
|
|||||||
else:
|
else:
|
||||||
return Response({'error': _('Unknown period "%s"') % str(period)}, status=status.HTTP_400_BAD_REQUEST)
|
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']
|
succ_list = dashboard_data['jobs']['successful']
|
||||||
fail_list = dashboard_data['jobs']['failed']
|
fail_list = dashboard_data['jobs']['failed']
|
||||||
|
canceled_list = dashboard_data['jobs']['canceled']
|
||||||
|
error_list = dashboard_data['jobs']['error']
|
||||||
|
|
||||||
qs_s = (
|
qs_s = (
|
||||||
success_query.filter(finished__range=(start, end))
|
success_query.filter(finished__range=(start, end))
|
||||||
@@ -318,6 +328,22 @@ class DashboardJobsGraphView(APIView):
|
|||||||
.annotate(agg=Count('id', distinct=True))
|
.annotate(agg=Count('id', distinct=True))
|
||||||
)
|
)
|
||||||
data_f = {item['d']: item['agg'] for item in qs_f}
|
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)
|
start_date = start.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
for d in itertools.count():
|
for d in itertools.count():
|
||||||
@@ -326,6 +352,8 @@ class DashboardJobsGraphView(APIView):
|
|||||||
break
|
break
|
||||||
succ_list.append([time.mktime(date.timetuple()), data_s.get(date, 0)])
|
succ_list.append([time.mktime(date.timetuple()), data_s.get(date, 0)])
|
||||||
fail_list.append([time.mktime(date.timetuple()), data_f.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)
|
return Response(dashboard_data)
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ import logging
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import logout
|
from django.contrib.auth import logout
|
||||||
from django.contrib.auth.models import User
|
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.db import connection
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.apps import apps
|
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.utils.translation import gettext_lazy as _
|
||||||
from django.urls import reverse, resolve
|
from django.urls import reverse, resolve
|
||||||
|
|
||||||
|
from awx.main import migrations
|
||||||
from awx.main.utils.named_url_graph import generate_graph, GraphNode
|
from awx.main.utils.named_url_graph import generate_graph, GraphNode
|
||||||
from awx.conf import fields, register
|
from awx.conf import fields, register
|
||||||
from awx.main.utils.profiling import AWXProfiler
|
from awx.main.utils.profiling import AWXProfiler
|
||||||
|
from awx.main.utils.common import memoize
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger('awx.main.middleware')
|
logger = logging.getLogger('awx.main.middleware')
|
||||||
@@ -198,9 +201,22 @@ class URLModificationMiddleware(MiddlewareMixin):
|
|||||||
request.path_info = new_path
|
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):
|
class MigrationRanCheckMiddleware(MiddlewareMixin):
|
||||||
def process_request(self, request):
|
def process_request(self, request):
|
||||||
executor = MigrationExecutor(connection)
|
if is_migrating() and getattr(resolve(request.path), 'url_name', '') != 'migrations_notran':
|
||||||
plan = executor.migration_plan(executor.loader.graph.leaf_nodes())
|
|
||||||
if bool(plan) and getattr(resolve(request.path), 'url_name', '') != 'migrations_notran':
|
|
||||||
return redirect(reverse("ui:migrations_notran"))
|
return redirect(reverse("ui:migrations_notran"))
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# Copyright (c) 2019 Ansible, Inc.
|
# Copyright (c) 2019 Ansible, Inc.
|
||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
|
# -*-coding:utf-8-*-
|
||||||
|
|
||||||
|
|
||||||
class CustomNotificationBase(object):
|
class CustomNotificationBase(object):
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from . import consumers
|
|||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger('awx.main.routing')
|
logger = logging.getLogger('awx.main.routing')
|
||||||
|
_application = None
|
||||||
|
|
||||||
|
|
||||||
class AWXProtocolTypeRouter(ProtocolTypeRouter):
|
class AWXProtocolTypeRouter(ProtocolTypeRouter):
|
||||||
@@ -66,7 +67,9 @@ websocket_relay_urlpatterns = [
|
|||||||
re_path(r'websocket/relay/$', consumers.RelayConsumer.as_asgi()),
|
re_path(r'websocket/relay/$', consumers.RelayConsumer.as_asgi()),
|
||||||
]
|
]
|
||||||
|
|
||||||
application = AWXProtocolTypeRouter(
|
|
||||||
|
def application_func(cls=AWXProtocolTypeRouter) -> ProtocolTypeRouter:
|
||||||
|
return cls(
|
||||||
{
|
{
|
||||||
'websocket': MultipleURLRouterAdapter(
|
'websocket': MultipleURLRouterAdapter(
|
||||||
URLRouter(websocket_relay_urlpatterns),
|
URLRouter(websocket_relay_urlpatterns),
|
||||||
@@ -74,3 +77,42 @@ application = AWXProtocolTypeRouter(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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}")
|
||||||
|
|||||||
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"
|
||||||
@@ -216,35 +216,46 @@
|
|||||||
- block:
|
- block:
|
||||||
- name: Fetch galaxy roles from roles/requirements.(yml/yaml)
|
- name: Fetch galaxy roles from roles/requirements.(yml/yaml)
|
||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
cmd: "ansible-galaxy role install -r {{ item }} {{ verbosity }}"
|
cmd: "ansible-galaxy role install -r {{ req_file }} {{ verbosity }}"
|
||||||
register: galaxy_result
|
register: galaxy_result
|
||||||
with_fileglob:
|
vars:
|
||||||
- "{{ project_path | quote }}/roles/requirements.yaml"
|
req_file: "{{ lookup('ansible.builtin.first_found', req_candidates, skip=True) }}"
|
||||||
|
req_candidates:
|
||||||
- "{{ project_path | quote }}/roles/requirements.yml"
|
- "{{ project_path | quote }}/roles/requirements.yml"
|
||||||
|
- "{{ project_path | quote }}/roles/requirements.yaml"
|
||||||
changed_when: "'was installed successfully' in galaxy_result.stdout"
|
changed_when: "'was installed successfully' in galaxy_result.stdout"
|
||||||
when: roles_enabled | bool
|
when:
|
||||||
|
- roles_enabled | bool
|
||||||
|
- req_file
|
||||||
tags:
|
tags:
|
||||||
- install_roles
|
- install_roles
|
||||||
|
|
||||||
- name: Fetch galaxy collections from collections/requirements.(yml/yaml)
|
- name: Fetch galaxy collections from collections/requirements.(yml/yaml)
|
||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
cmd: "ansible-galaxy collection install -r {{ item }} {{ verbosity }}"
|
cmd: "ansible-galaxy collection install -r {{ req_file }} {{ verbosity }}"
|
||||||
register: galaxy_collection_result
|
register: galaxy_collection_result
|
||||||
with_fileglob:
|
vars:
|
||||||
- "{{ project_path | quote }}/collections/requirements.yaml"
|
req_file: "{{ lookup('ansible.builtin.first_found', req_candidates, skip=True) }}"
|
||||||
|
req_candidates:
|
||||||
- "{{ project_path | quote }}/collections/requirements.yml"
|
- "{{ 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"
|
changed_when: "'Nothing to do.' not in galaxy_collection_result.stdout"
|
||||||
when:
|
when:
|
||||||
- "ansible_version.full is version_compare('2.9', '>=')"
|
- "ansible_version.full is version_compare('2.9', '>=')"
|
||||||
- collections_enabled | bool
|
- collections_enabled | bool
|
||||||
|
- req_file
|
||||||
tags:
|
tags:
|
||||||
- install_collections
|
- install_collections
|
||||||
|
|
||||||
- name: Fetch galaxy roles and collections from requirements.(yml/yaml)
|
- name: Fetch galaxy roles and collections from requirements.(yml/yaml)
|
||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
cmd: "ansible-galaxy install -r {{ item }} {{ verbosity }}"
|
cmd: "ansible-galaxy install -r {{ req_file }} {{ verbosity }}"
|
||||||
register: galaxy_combined_result
|
register: galaxy_combined_result
|
||||||
with_fileglob:
|
vars:
|
||||||
|
req_file: "{{ lookup('ansible.builtin.first_found', req_candidates, skip=True) }}"
|
||||||
|
req_candidates:
|
||||||
- "{{ project_path | quote }}/requirements.yaml"
|
- "{{ project_path | quote }}/requirements.yaml"
|
||||||
- "{{ project_path | quote }}/requirements.yml"
|
- "{{ project_path | quote }}/requirements.yml"
|
||||||
changed_when: "'Nothing to do.' not in galaxy_combined_result.stdout"
|
changed_when: "'Nothing to do.' not in galaxy_combined_result.stdout"
|
||||||
@@ -252,6 +263,7 @@
|
|||||||
- "ansible_version.full is version_compare('2.10', '>=')"
|
- "ansible_version.full is version_compare('2.10', '>=')"
|
||||||
- collections_enabled | bool
|
- collections_enabled | bool
|
||||||
- roles_enabled | bool
|
- roles_enabled | bool
|
||||||
|
- req_file
|
||||||
tags:
|
tags:
|
||||||
- install_collections
|
- install_collections
|
||||||
- install_roles
|
- install_roles
|
||||||
|
|||||||
@@ -257,12 +257,17 @@ function PromptDetail({
|
|||||||
numChips={5}
|
numChips={5}
|
||||||
ouiaId="prompt-job-tag-chips"
|
ouiaId="prompt-job-tag-chips"
|
||||||
totalChips={
|
totalChips={
|
||||||
!overrides.job_tags || overrides.job_tags === ''
|
overrides.job_tags === undefined ||
|
||||||
|
overrides.job_tags === null ||
|
||||||
|
overrides.job_tags === ''
|
||||||
? 0
|
? 0
|
||||||
: overrides.job_tags.split(',').length
|
: 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) => (
|
overrides.job_tags.split(',').map((jobTag) => (
|
||||||
<Chip
|
<Chip
|
||||||
key={jobTag}
|
key={jobTag}
|
||||||
@@ -284,13 +289,18 @@ function PromptDetail({
|
|||||||
<ChipGroup
|
<ChipGroup
|
||||||
numChips={5}
|
numChips={5}
|
||||||
totalChips={
|
totalChips={
|
||||||
!overrides.skip_tags || overrides.skip_tags === ''
|
overrides.skip_tags === undefined ||
|
||||||
|
overrides.skip_tags === null ||
|
||||||
|
overrides.skip_tags === ''
|
||||||
? 0
|
? 0
|
||||||
: overrides.skip_tags.split(',').length
|
: overrides.skip_tags.split(',').length
|
||||||
}
|
}
|
||||||
ouiaId="prompt-skip-tag-chips"
|
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) => (
|
overrides.skip_tags.split(',').map((skipTag) => (
|
||||||
<Chip
|
<Chip
|
||||||
key={skipTag}
|
key={skipTag}
|
||||||
|
|||||||
@@ -115,8 +115,11 @@ function SessionProvider({ children }) {
|
|||||||
}, [setSessionTimeout, setSessionCountdown]);
|
}, [setSessionTimeout, setSessionCountdown]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const isRedirectCondition = (location, histLength) =>
|
||||||
|
location.pathname === '/login' && histLength === 2;
|
||||||
|
|
||||||
const unlisten = history.listen((location, action) => {
|
const unlisten = history.listen((location, action) => {
|
||||||
if (action === 'POP') {
|
if (action === 'POP' || isRedirectCondition(location, history.length)) {
|
||||||
setIsRedirectLinkReceived(true);
|
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
|
#: 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."
|
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
|
#: components/About/About.js:45
|
||||||
msgid "Brand Image"
|
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
|
#: 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."
|
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
|
#: screens/CredentialType/CredentialTypeDetails/CredentialTypeDetails.js:87
|
||||||
msgid "Environment variables or extra variables that specify the values a credential type can inject."
|
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
|
#: components/Search/RelatedLookupTypeInput.js:38
|
||||||
msgid "Exact search on name field."
|
msgid "Exact search on name field."
|
||||||
msgstr ""
|
msgstr "Recherche exacte sur le champ nom."
|
||||||
|
|
||||||
#: screens/Project/shared/Project.helptext.js:23
|
#: screens/Project/shared/Project.helptext.js:23
|
||||||
msgid "Example URLs for GIT Source Control include:"
|
msgid "Example URLs for GIT Source Control include:"
|
||||||
@@ -3242,7 +3242,7 @@ msgstr "Jobs ayant échoué"
|
|||||||
|
|
||||||
#: screens/WorkflowApproval/WorkflowApprovalList/WorkflowApprovalList.js:262
|
#: screens/WorkflowApproval/WorkflowApprovalList/WorkflowApprovalList.js:262
|
||||||
msgid "Failed to approve one or more workflow approval."
|
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
|
#: screens/WorkflowApproval/shared/WorkflowApprovalButton.js:56
|
||||||
msgid "Failed to approve {0}."
|
msgid "Failed to approve {0}."
|
||||||
@@ -3474,7 +3474,7 @@ msgstr "N'a pas réussi à supprimer {name}."
|
|||||||
|
|
||||||
#: screens/WorkflowApproval/WorkflowApprovalList/WorkflowApprovalList.js:263
|
#: screens/WorkflowApproval/WorkflowApprovalList/WorkflowApprovalList.js:263
|
||||||
msgid "Failed to deny one or more workflow approval."
|
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
|
#: screens/WorkflowApproval/shared/WorkflowDenyButton.js:51
|
||||||
msgid "Failed to deny {0}."
|
msgid "Failed to deny {0}."
|
||||||
@@ -3520,7 +3520,7 @@ msgstr "Echec du lancement du Job."
|
|||||||
|
|
||||||
#: screens/Inventory/InventoryHosts/InventoryHostItem.js:121
|
#: screens/Inventory/InventoryHosts/InventoryHostItem.js:121
|
||||||
msgid "Failed to load related groups."
|
msgid "Failed to load related groups."
|
||||||
msgstr ""
|
msgstr "Impossible de charger les groupes associés."
|
||||||
|
|
||||||
#: screens/Instances/InstanceDetail/InstanceDetail.js:388
|
#: screens/Instances/InstanceDetail/InstanceDetail.js:388
|
||||||
#: screens/Instances/InstanceList/InstanceList.js:266
|
#: 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/InstanceDetail/InstanceDetail.js:234
|
||||||
#: screens/Instances/InstanceList/InstanceListItem.js:242
|
#: screens/Instances/InstanceList/InstanceListItem.js:242
|
||||||
msgid "Health checks are asynchronous tasks. See the"
|
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/InstanceGroup/Instances/InstanceList.js:286
|
||||||
#: screens/Instances/InstanceList/InstanceList.js:219
|
#: screens/Instances/InstanceList/InstanceList.js:219
|
||||||
msgid "Health checks can only be run on execution nodes."
|
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
|
#: components/StatusLabel/StatusLabel.js:42
|
||||||
msgid "Healthy"
|
msgid "Healthy"
|
||||||
@@ -5048,7 +5048,7 @@ msgstr "Lancer"
|
|||||||
|
|
||||||
#: components/TemplateList/TemplateListItem.js:214
|
#: components/TemplateList/TemplateListItem.js:214
|
||||||
msgid "Launch Template"
|
msgid "Launch Template"
|
||||||
msgstr "Lacer le modèle."
|
msgstr "Lancer le modèle."
|
||||||
|
|
||||||
#: screens/ManagementJob/ManagementJobList/LaunchManagementPrompt.js:32
|
#: screens/ManagementJob/ManagementJobList/LaunchManagementPrompt.js:32
|
||||||
#: screens/ManagementJob/ManagementJobList/LaunchManagementPrompt.js:34
|
#: screens/ManagementJob/ManagementJobList/LaunchManagementPrompt.js:34
|
||||||
@@ -9637,7 +9637,7 @@ msgstr "Utilisateur"
|
|||||||
|
|
||||||
#: components/AppContainer/PageHeaderToolbar.js:160
|
#: components/AppContainer/PageHeaderToolbar.js:160
|
||||||
msgid "User Details"
|
msgid "User Details"
|
||||||
msgstr "Détails de l'erreur"
|
msgstr "Détails de l'utilisateur"
|
||||||
|
|
||||||
#: screens/Setting/SettingList.js:121
|
#: screens/Setting/SettingList.js:121
|
||||||
#: screens/Setting/Settings.js:118
|
#: screens/Setting/Settings.js:118
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Modal, Tab, Tabs, TabTitleText } from '@patternfly/react-core';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { encode } from 'html-entities';
|
import { encode } from 'html-entities';
|
||||||
|
import { jsonToYaml } from 'util/yaml';
|
||||||
import StatusLabel from '../../../components/StatusLabel';
|
import StatusLabel from '../../../components/StatusLabel';
|
||||||
import { DetailList, Detail } from '../../../components/DetailList';
|
import { DetailList, Detail } from '../../../components/DetailList';
|
||||||
import ContentEmpty from '../../../components/ContentEmpty';
|
import ContentEmpty from '../../../components/ContentEmpty';
|
||||||
@@ -144,9 +145,28 @@ function HostEventModal({ onClose, hostEvent = {}, isOpen = false }) {
|
|||||||
<ContentEmpty title={t`No JSON Available`} />
|
<ContentEmpty title={t`No JSON Available`} />
|
||||||
)}
|
)}
|
||||||
</Tab>
|
</Tab>
|
||||||
{stdOut?.length ? (
|
|
||||||
<Tab
|
<Tab
|
||||||
eventKey={2}
|
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={3}
|
||||||
title={<TabTitleText>{t`Output`}</TabTitleText>}
|
title={<TabTitleText>{t`Output`}</TabTitleText>}
|
||||||
aria-label={t`Output tab`}
|
aria-label={t`Output tab`}
|
||||||
ouiaId="standard-out-tab"
|
ouiaId="standard-out-tab"
|
||||||
@@ -163,7 +183,7 @@ function HostEventModal({ onClose, hostEvent = {}, isOpen = false }) {
|
|||||||
) : null}
|
) : null}
|
||||||
{stdErr?.length ? (
|
{stdErr?.length ? (
|
||||||
<Tab
|
<Tab
|
||||||
eventKey={3}
|
eventKey={4}
|
||||||
title={<TabTitleText>{t`Standard Error`}</TabTitleText>}
|
title={<TabTitleText>{t`Standard Error`}</TabTitleText>}
|
||||||
aria-label={t`Standard error tab`}
|
aria-label={t`Standard error tab`}
|
||||||
ouiaId="standard-error-tab"
|
ouiaId="standard-error-tab"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
import HostEventModal from './HostEventModal';
|
import HostEventModal from './HostEventModal';
|
||||||
|
import { jsonToYaml } from 'util/yaml';
|
||||||
|
|
||||||
const hostEvent = {
|
const hostEvent = {
|
||||||
changed: true,
|
changed: true,
|
||||||
@@ -167,6 +168,8 @@ const jsonValue = `{
|
|||||||
]
|
]
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
|
const yamlValue = jsonToYaml(jsonValue);
|
||||||
|
|
||||||
describe('HostEventModal', () => {
|
describe('HostEventModal', () => {
|
||||||
test('initially renders successfully', () => {
|
test('initially renders successfully', () => {
|
||||||
const wrapper = shallow(
|
const wrapper = shallow(
|
||||||
@@ -187,7 +190,7 @@ describe('HostEventModal', () => {
|
|||||||
<HostEventModal hostEvent={hostEvent} onClose={() => {}} isOpen />
|
<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', () => {
|
test('should initially show details tab', () => {
|
||||||
@@ -287,7 +290,7 @@ describe('HostEventModal', () => {
|
|||||||
expect(codeEditor.prop('value')).toEqual(jsonValue);
|
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(
|
const wrapper = shallow(
|
||||||
<HostEventModal hostEvent={hostEvent} onClose={() => {}} isOpen />
|
<HostEventModal hostEvent={hostEvent} onClose={() => {}} isOpen />
|
||||||
);
|
);
|
||||||
@@ -299,6 +302,21 @@ describe('HostEventModal', () => {
|
|||||||
const codeEditor = wrapper.find('Tab[eventKey=2] CodeEditor');
|
const codeEditor = wrapper.find('Tab[eventKey=2] CodeEditor');
|
||||||
expect(codeEditor.prop('mode')).toBe('javascript');
|
expect(codeEditor.prop('mode')).toBe('javascript');
|
||||||
expect(codeEditor.prop('readOnly')).toBe(true);
|
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);
|
expect(codeEditor.prop('value')).toEqual(hostEvent.event_data.res.stdout);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -316,10 +334,10 @@ describe('HostEventModal', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleTabClick = wrapper.find('Tabs').prop('onSelect');
|
const handleTabClick = wrapper.find('Tabs').prop('onSelect');
|
||||||
handleTabClick(null, 3);
|
handleTabClick(null, 4);
|
||||||
wrapper.update();
|
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('mode')).toBe('javascript');
|
||||||
expect(codeEditor.prop('readOnly')).toBe(true);
|
expect(codeEditor.prop('readOnly')).toBe(true);
|
||||||
expect(codeEditor.prop('value')).toEqual('error content');
|
expect(codeEditor.prop('value')).toEqual('error content');
|
||||||
@@ -351,10 +369,10 @@ describe('HostEventModal', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleTabClick = wrapper.find('Tabs').prop('onSelect');
|
const handleTabClick = wrapper.find('Tabs').prop('onSelect');
|
||||||
handleTabClick(null, 2);
|
handleTabClick(null, 3);
|
||||||
wrapper.update();
|
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('mode')).toBe('javascript');
|
||||||
expect(codeEditor.prop('readOnly')).toBe(true);
|
expect(codeEditor.prop('readOnly')).toBe(true);
|
||||||
expect(codeEditor.prop('value')).toEqual('foo bar');
|
expect(codeEditor.prop('value')).toEqual('foo bar');
|
||||||
@@ -375,10 +393,10 @@ describe('HostEventModal', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleTabClick = wrapper.find('Tabs').prop('onSelect');
|
const handleTabClick = wrapper.find('Tabs').prop('onSelect');
|
||||||
handleTabClick(null, 2);
|
handleTabClick(null, 3);
|
||||||
wrapper.update();
|
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('mode')).toBe('javascript');
|
||||||
expect(codeEditor.prop('readOnly')).toBe(true);
|
expect(codeEditor.prop('readOnly')).toBe(true);
|
||||||
expect(codeEditor.prop('value')).toEqual('baz\nbar');
|
expect(codeEditor.prop('value')).toEqual('baz\nbar');
|
||||||
@@ -394,10 +412,10 @@ describe('HostEventModal', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleTabClick = wrapper.find('Tabs').prop('onSelect');
|
const handleTabClick = wrapper.find('Tabs').prop('onSelect');
|
||||||
handleTabClick(null, 2);
|
handleTabClick(null, 3);
|
||||||
wrapper.update();
|
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('mode')).toBe('javascript');
|
||||||
expect(codeEditor.prop('readOnly')).toBe(true);
|
expect(codeEditor.prop('readOnly')).toBe(true);
|
||||||
expect(codeEditor.prop('value')).toEqual(
|
expect(codeEditor.prop('value')).toEqual(
|
||||||
|
|||||||
@@ -201,7 +201,11 @@ function NodeViewModal({ readOnly }) {
|
|||||||
overrides.limit = originalNodeObject.limit;
|
overrides.limit = originalNodeObject.limit;
|
||||||
}
|
}
|
||||||
if (launchConfig.ask_verbosity_on_launch) {
|
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) {
|
if (launchConfig.ask_credential_on_launch) {
|
||||||
overrides.credentials = originalNodeCredentials || [];
|
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.
|
## 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
|
$(UI_NEXT_DIR)/src/build/awx: $(UI_NEXT_DIR)/src $(UI_NEXT_DIR)/src/node_modules/webpack
|
||||||
@echo "=== Building ui_next ==="
|
@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
|
@mv $(UI_NEXT_DIR)/src/build/awx/index.html $(UI_NEXT_DIR)/src/build/awx/index_awx.html
|
||||||
|
|
||||||
.PHONY: ui-next/src
|
.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/
|
homepage: https://www.ansible.com/
|
||||||
issues: https://github.com/ansible/awx/issues?q=is%3Aissue+label%3Acomponent%3Aawx_collection
|
issues: https://github.com/ansible/awx/issues?q=is%3Aissue+label%3Acomponent%3Aawx_collection
|
||||||
license:
|
license:
|
||||||
- GPL-3.0-only
|
- GPL-3.0-or-later
|
||||||
name: awx
|
name: awx
|
||||||
namespace: awx
|
namespace: awx
|
||||||
readme: README.md
|
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
|
resource_class = resource_module.ControllerAWXKitModule
|
||||||
elif getattr(resource_module, 'ControllerAPIModule', None):
|
elif getattr(resource_module, 'ControllerAPIModule', None):
|
||||||
resource_class = resource_module.ControllerAPIModule
|
resource_class = resource_module.ControllerAPIModule
|
||||||
elif getattr(resource_module, 'TowerLegacyModule', None):
|
|
||||||
resource_class = resource_module.TowerLegacyModule
|
|
||||||
else:
|
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):
|
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
|
# Call the test utility (like a mock server) instead of issuing HTTP requests
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ homepage: https://www.ansible.com/
|
|||||||
issues: https://github.com/ansible/awx/issues?q=is%3Aissue+label%3Acomponent%3Aawx_collection
|
issues: https://github.com/ansible/awx/issues?q=is%3Aissue+label%3Acomponent%3Aawx_collection
|
||||||
license:
|
license:
|
||||||
- GPL-3.0-or-later
|
- GPL-3.0-or-later
|
||||||
# plugins/module_utils/tower_legacy.py
|
|
||||||
- BSD-2-Clause
|
|
||||||
name: {{ collection_package }}
|
name: {{ collection_package }}
|
||||||
namespace: {{ collection_namespace }}
|
namespace: {{ collection_namespace }}
|
||||||
readme: README.md
|
readme: README.md
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ credential_type_name_to_config_kind_map = {
|
|||||||
'vault': 'vault',
|
'vault': 'vault',
|
||||||
'vmware vcenter': 'vmware',
|
'vmware vcenter': 'vmware',
|
||||||
'gpg public key': 'gpg_public_key',
|
'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()}
|
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
|
# Subscription group types
|
||||||
|
|
||||||
def __init__(
|
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
|
# delay this import, because this is an optional dependency
|
||||||
import websocket
|
import websocket
|
||||||
@@ -68,6 +77,7 @@ class WSClient(object):
|
|||||||
hostname = result.hostname
|
hostname = result.hostname
|
||||||
|
|
||||||
self.port = port
|
self.port = port
|
||||||
|
self.suffix = ws_suffix
|
||||||
self._use_ssl = secure
|
self._use_ssl = secure
|
||||||
self.hostname = hostname
|
self.hostname = hostname
|
||||||
self.token = token
|
self.token = token
|
||||||
@@ -85,7 +95,7 @@ class WSClient(object):
|
|||||||
else:
|
else:
|
||||||
auth_cookie = ''
|
auth_cookie = ''
|
||||||
pref = 'wss://' if self._use_ssl else 'ws://'
|
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(
|
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
|
url, on_open=self._on_open, on_message=self._on_message, on_error=self._on_error, on_close=self._on_close, cookie=auth_cookie
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ def test_explicit_hostname():
|
|||||||
assert client.token == "token"
|
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(
|
@pytest.mark.parametrize(
|
||||||
'url, result',
|
'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.
|
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
|
:alt: Site A pointing to Site B and dotted arrows to two hosts from Site B
|
||||||
|
|
||||||
Automation mesh is useful for:
|
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.
|
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.
|
:alt: Control node pointing to hop node, which is pointing to two execution nodes.
|
||||||
|
|
||||||
|
|
||||||
@@ -78,11 +78,11 @@ Hop nodes can be added to sit between the control plane of AWX and standalone ex
|
|||||||
|
|
||||||
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.
|
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
|
.. 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.
|
:alt: AWX task pod with a hop node between the control plane of AWX and standalone execution nodes.
|
||||||
|
|
||||||
|
|
||||||
An example of a simple topology may look like the following:
|
Below are sample values used to configure each node in a simple topology:
|
||||||
|
|
||||||
.. list-table::
|
.. list-table::
|
||||||
:widths: 20 30 10 20 15
|
:widths: 20 30 10 20 15
|
||||||
@@ -95,9 +95,9 @@ An example of a simple topology may look like the following:
|
|||||||
- Peers
|
- Peers
|
||||||
* - Control plane
|
* - Control plane
|
||||||
- awx-task-65d6d96987-mgn9j
|
- awx-task-65d6d96987-mgn9j
|
||||||
- 27199
|
- n/a
|
||||||
- True
|
- n/a
|
||||||
- []
|
- [hop node]
|
||||||
* - Hop node
|
* - Hop node
|
||||||
- awx-hop-node
|
- awx-hop-node
|
||||||
- 27199
|
- 27199
|
||||||
@@ -107,7 +107,7 @@ An example of a simple topology may look like the following:
|
|||||||
- awx-example.com
|
- awx-example.com
|
||||||
- n/a
|
- n/a
|
||||||
- False
|
- False
|
||||||
- ["hop node"]
|
- [hop node]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -117,11 +117,11 @@ 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.
|
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.png
|
.. image:: ../common/images/instances_mesh_ingress_topology.drawio.png
|
||||||
:alt: Mesh ingress architecture showing the peering relationship between nodes.
|
:alt: Mesh ingress architecture showing the peering relationship between nodes.
|
||||||
|
|
||||||
|
|
||||||
An example of a topology that uses mesh ingress may look like the following:
|
Below are sample values used to configure each node in a mesh ingress topology:
|
||||||
|
|
||||||
.. list-table::
|
.. list-table::
|
||||||
:widths: 20 30 10 20 15
|
:widths: 20 30 10 20 15
|
||||||
@@ -133,12 +133,12 @@ An example of a topology that uses mesh ingress may look like the following:
|
|||||||
- Peers from control nodes
|
- Peers from control nodes
|
||||||
- Peers
|
- Peers
|
||||||
* - Control plane
|
* - Control plane
|
||||||
- awx-task-xyz
|
- awx-task-65d6d96987-mgn9j
|
||||||
- 27199
|
- n/a
|
||||||
- True
|
- n/a
|
||||||
- []
|
- [hop node]
|
||||||
* - Hop node
|
* - Hop node
|
||||||
- awx-hop-node
|
- awx-mesh-ingress-1
|
||||||
- 27199
|
- 27199
|
||||||
- True
|
- True
|
||||||
- []
|
- []
|
||||||
@@ -146,13 +146,14 @@ An example of a topology that uses mesh ingress may look like the following:
|
|||||||
- awx-example.com
|
- awx-example.com
|
||||||
- n/a
|
- n/a
|
||||||
- False
|
- False
|
||||||
- ["hop node"]
|
- [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.
|
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
|
.. 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.
|
:alt: Job template using the instance group with the execution node to run jobs.
|
||||||
|
:width: 1400px
|
||||||
|
|
||||||
|
|
||||||
.. _ag_instances_add:
|
.. _ag_instances_add:
|
||||||
@@ -168,6 +169,7 @@ To create an instance in AWX:
|
|||||||
|
|
||||||
.. image:: ../common/images/instances_create_new.png
|
.. image:: ../common/images/instances_create_new.png
|
||||||
:alt: Create a new instance form.
|
:alt: Create a new instance form.
|
||||||
|
:width: 1400px
|
||||||
|
|
||||||
An instance has several attributes that may be configured:
|
An instance has several attributes that may be configured:
|
||||||
|
|
||||||
@@ -190,6 +192,7 @@ Upon successful creation, the Details of the one of the created instances opens.
|
|||||||
|
|
||||||
.. image:: ../common/images/instances_create_details.png
|
.. image:: ../common/images/instances_create_details.png
|
||||||
:alt: Details of the newly created instance.
|
:alt: Details of the newly created instance.
|
||||||
|
:width: 1400px
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
@@ -199,6 +202,7 @@ Upon successful creation, the Details of the one of the created instances opens.
|
|||||||
|
|
||||||
.. image:: ../common/images/instances_install_bundle.png
|
.. image:: ../common/images/instances_install_bundle.png
|
||||||
:alt: Instance details showing the Download button in the Install Bundle field of the Details tab.
|
: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:
|
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:
|
||||||
|
|
||||||
@@ -237,11 +241,13 @@ Wait a few minutes for the periodic AWX task to do a health check against the ne
|
|||||||
|
|
||||||
.. image:: ../common/images/instances_peers_tab.png
|
.. image:: ../common/images/instances_peers_tab.png
|
||||||
:alt: "Peers" tab showing two peers.
|
: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.
|
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
|
.. image:: ../common/images/instances_associate_peer.png
|
||||||
:alt: Instances available to peer with the example hop node.
|
: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.
|
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.
|
||||||
|
|
||||||
@@ -265,6 +271,7 @@ Click **Instances** from the left side navigation menu to access the Instances l
|
|||||||
|
|
||||||
.. image:: ../common/images/instances_list_view.png
|
.. 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:
|
The Instances list displays all the current nodes in your topology, along with relevant details:
|
||||||
|
|
||||||
@@ -290,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:
|
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
|
.. image:: ../common/images/instances_delete_prompt.png
|
||||||
:alt: Prompt for deleting instances in AWX.
|
:alt: Prompt for deleting instances in AWX
|
||||||
|
:width: 1400px
|
||||||
|
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
@@ -304,6 +313,7 @@ If running a health check on an instance, at the top of the Details page, a mess
|
|||||||
|
|
||||||
.. image:: ../common/images/instances_health_check.png
|
.. 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.
|
Click **Reload** to refresh the instance status.
|
||||||
|
|
||||||
@@ -318,6 +328,7 @@ The example health check shows the status updates with an error on node 'one':
|
|||||||
|
|
||||||
.. image:: ../common/images/topology-viewer-instance-with-errors.png
|
.. image:: ../common/images/topology-viewer-instance-with-errors.png
|
||||||
:alt: Health check showing an error in one of the instances.
|
:alt: Health check showing an error in one of the instances.
|
||||||
|
:width: 1400px
|
||||||
|
|
||||||
|
|
||||||
Using a custom Receptor CA
|
Using a custom Receptor CA
|
||||||
|
|||||||
|
After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 172 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: 60 KiB |
@@ -11,19 +11,13 @@ Release Notes
|
|||||||
pair: release notes; v23.3.0
|
pair: release notes; v23.3.0
|
||||||
pair: release notes; v23.3.1
|
pair: release notes; v23.3.1
|
||||||
pair: release notes; v23.4.0
|
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>`_.
|
|
||||||
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
|
### Releasing the AWX operator
|
||||||
|
|
||||||
Once the AWX image is live, we can now release the AWX operator.
|
Once the AWX image is live, we can now release the AWX operator.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ ipython>=7.31.1 # https://github.com/ansible/awx/security/dependabot/30
|
|||||||
unittest2
|
unittest2
|
||||||
black
|
black
|
||||||
pytest!=7.0.0
|
pytest!=7.0.0
|
||||||
|
pytest-asyncio
|
||||||
pytest-cov
|
pytest-cov
|
||||||
pytest-django
|
pytest-django
|
||||||
pytest-mock==1.11.1
|
pytest-mock==1.11.1
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ stdout_logfile=/dev/stdout
|
|||||||
stdout_logfile_maxbytes=0
|
stdout_logfile_maxbytes=0
|
||||||
stderr_logfile=/dev/stderr
|
stderr_logfile=/dev/stderr
|
||||||
stderr_logfile_maxbytes=0
|
stderr_logfile_maxbytes=0
|
||||||
|
|
||||||
[program:awx-rsyslogd]
|
[program:awx-rsyslogd]
|
||||||
command = rsyslogd -n -i /var/run/awx-rsyslog/rsyslog.pid -f /var/lib/awx/rsyslog/rsyslog.conf
|
command = rsyslogd -n -i /var/run/awx-rsyslog/rsyslog.pid -f /var/lib/awx/rsyslog/rsyslog.conf
|
||||||
autorestart = true
|
autorestart = true
|
||||||
|
|||||||