Compare commits

...

22 Commits

Author SHA1 Message Date
Hao Liu
86204cf23b Publish amd64 and arm64 awx image on release (#15053)
* Stage multi-arch awx image

- change CI to use `make awx-kube-build` instead of build playbook
- update staging CI to build and push multiarch awx image
- update doc to use `make awx-kube-build` to build awx image
- remove build playbook (no longer used)
2024-04-09 09:50:09 -04:00
Chris Meyers
468949b899 Remove uneeded drf_reverse overwrite
* `drf_reverse()` was introduced here 1a75b1836e
* There is a comment about monkey patching. I can't find the monkey patch it is referencing.
* AWX `drf_reverse()` is a copy paste of this https://github.com/encode/django-rest-framework/blob/master/rest_framework/reverse.py#L32
  * The only difference is DRF's version calls `preserve_builtin_query_params()`
    * `preserve_builtin_query_params()` only does something if `api_settings.URL_FORMAT_OVERRIDE` is defined.
      * We don't use `REST_FRAMEWORK.URL_FORMAT_OVERRIDE`
2024-04-08 16:14:11 -04:00
TVo
f1d9966224 Added docs for terraform credential/inventory source (#15004)
* Added docs for terraform credential/inventory source

* Updated screen captures for inventories and source to match wfjt example

* Added docs for terraform credential/inventory source

* Updated screen captures for inventories and source to match wfjt example

* Update docs/docsite/rst/userguide/inventories.rst

Co-authored-by: Aoki <lucasaoki@users.noreply.github.com>

* Revised per review feedback.

* Update docs/docsite/rst/userguide/inventories.rst

Co-authored-by: Helen Bailey <hakbailey@gmail.com>

---------

Co-authored-by: Aoki <lucasaoki@users.noreply.github.com>
Co-authored-by: Helen Bailey <hakbailey@gmail.com>
2024-04-05 09:57:27 -06:00
César Francisco San Nicolás Martínez
b022b50966 fix service-index url calling reverse method 2024-04-04 07:48:04 -04:00
Elijah DeLee
e2f4213839 Round out options url prefix edge cases 2024-04-04 07:48:04 -04:00
Chris Meyers
ae1235b223 Rename container hostname from awx_1 to awx-1
* Django and other webservers that care about proper hostnames don't
  like underscores in them.
2024-04-03 15:58:17 -04:00
Tom Page
c061f59f1c Add tags and skip_tags option to awx.awx.workflow_launch (#15011)
Signed-off-by: Tom Page <tpage@redhat.com>
2024-04-03 15:29:43 -04:00
Jeff Bradberry
3edaaebba2 Adjust the awx-manage script to make use of importlib (#15015)
* Adjust the awx-manage script to make use of importlib

removing the deprecation warning.

* Synlink awx-manage in docker-compose

No longer need to rebuild docker-compose devel image to load change for `tools/docker-compose/awx-manage` in development environment

---------

Co-authored-by: Hao Liu <44379968+TheRealHaoLiu@users.noreply.github.com>
2024-04-02 17:20:05 -04:00
Hao Liu
7cdf1c7f96 Update DOCKER_COMPOSE command to docker compose (#15056)
* Update DOCKER_COMPOSE command

docker-compose will stop being supported soon and this is causing CI flake setting DOCKER_COMPOSE default to `docker compose`

* Give AWX network a static name
2024-04-02 15:13:14 -04:00
Hao Liu
d558204192 Make db password optional for wsrelay (#15046)
* Make db password optional for wsrelay

* Change DB setting copy to deepcopy

safer than copy()

Co-Authored-By: Jeff Bradberry <685957+jbradberry@users.noreply.github.com>

---------

Co-authored-by: Jeff Bradberry <685957+jbradberry@users.noreply.github.com>
2024-04-02 11:47:24 -04:00
Chris Meyers
d06ce8f911 Remove json formatter for job lifecycle
* We didn't really make use of json formatting across the app. Remove
  the special case json formatter. Instead, output all of the meta-data
  associated with a job lifecycle event every time. Before, we tried to
  only output this extra meta data when in DEBUG mode. It turns out this
  information is smaller than we thought and more useful than we thought
  so always output it.
2024-04-02 11:39:34 -04:00
Alan Rominger
4b6f7e0ebe Add link to service-index URL 2024-03-29 10:07:15 +00:00
John Barker
370c567be1 Remove /17 magic number from Forum URL 2024-03-29 10:03:45 +00:00
John Barker
9be64f3de5 Improve social documentation release_process.md 2024-03-29 10:03:45 +00:00
Alan Rominger
30500e5a95 Re-parent DAB views from AWX base 2024-03-29 10:03:12 +00:00
David O Neill
bb323c5710 Loosen up body check on template
https://github.com/ansible/awx/issues/14985
https://github.com/ansible/awx/issues/13983
2024-03-29 10:02:18 +00:00
Chris Meyers
7571df49d5 Pass --exclude="list of exclude dirs like this"
* Previously, the params were passed without quotes and each directory
  was being interpreted as a seperate command line flag.
* Added some structure around the error messages returned from
  receptorctl so we can more easily decide how to handle each case. For
  example, releasing the cleanup job from receptor doesn't absolutely
  need to succeed because we have a periodic job that does that. In
  fact, that is the thing that is making it fail .. but I digress.
2024-03-28 14:42:08 -04:00
Jeff Bradberry
1559c21033 Change awx.awx.application to output the OAuth2 client secret
if one was generated.
2024-03-28 10:17:46 -04:00
PabloHiro
d9b81731e9 Fix: broken reference to API url 2024-03-27 20:37:53 +01:00
Adam Miller
2034cca3a9 update playbooks to use fqcn
Signed-off-by: Adam Miller <admiller@redhat.com>
2024-03-27 15:13:43 -04:00
Chris Meyers
0b5e59d9cb Fix websocket relay. Set autocommit so conn.notifies() does not blocks forever (#15043)
Without autocommit conn.notifies() blocks forever
2024-03-27 15:11:17 -04:00
Alan Rominger
f48b2d1ae5 Add resource and ansible_id to serializers (#15020) 2024-03-26 22:37:15 -04:00
59 changed files with 591 additions and 361 deletions

View File

@@ -71,7 +71,7 @@ runs:
id: data
shell: bash
run: |
AWX_IP=$(docker inspect -f '{{.NetworkSettings.Networks._sources_awx.IPAddress}}' tools_awx_1)
AWX_IP=$(docker inspect -f '{{.NetworkSettings.Networks.awx.IPAddress}}' tools_awx_1)
ADMIN_TOKEN=$(docker exec -i tools_awx_1 awx-manage create_oauth2_token --user admin)
echo "ip=$AWX_IP" >> $GITHUB_OUTPUT
echo "admin_token=$ADMIN_TOKEN" >> $GITHUB_OUTPUT

View File

@@ -94,11 +94,11 @@ jobs:
- name: Build AWX image
working-directory: awx
run: |
ansible-playbook -v tools/ansible/build.yml \
-e headless=yes \
-e awx_image=awx \
-e awx_image_tag=ci \
-e ansible_python_interpreter=$(which python3)
VERSION=`make version-for-buildyml` make awx-kube-build
env:
COMPOSE_TAG: ci
DEV_DOCKER_TAG_BASE: local
HEADLESS: yes
- name: Run test deployment with awx-operator
working-directory: awx-operator
@@ -109,7 +109,7 @@ jobs:
make kustomize
KUSTOMIZE_PATH=$(readlink -f bin/kustomize) molecule -v test -s kind -- --skip-tags=replicas
env:
AWX_TEST_IMAGE: awx
AWX_TEST_IMAGE: local/awx
AWX_TEST_VERSION: ci
collection-sanity:

View File

@@ -49,13 +49,11 @@ jobs:
with:
path: awx
- name: Get python version from Makefile
run: echo py_version=`make PYTHON_VERSION` >> $GITHUB_ENV
- name: Install python ${{ env.py_version }}
uses: actions/setup-python@v4
- name: Checkout awx-operator
uses: actions/checkout@v3
with:
python-version: ${{ env.py_version }}
repository: ${{ github.repository_owner }}/awx-operator
path: awx-operator
- name: Checkout awx-logos
uses: actions/checkout@v3
@@ -63,57 +61,85 @@ jobs:
repository: ansible/awx-logos
path: awx-logos
- name: Checkout awx-operator
uses: actions/checkout@v3
- name: Get python version from Makefile
working-directory: awx
run: echo py_version=`make PYTHON_VERSION` >> $GITHUB_ENV
- name: Install python ${{ env.py_version }}
uses: actions/setup-python@v4
with:
repository: ${{ github.repository_owner }}/awx-operator
path: awx-operator
python-version: ${{ env.py_version }}
- name: Install playbook dependencies
run: |
python3 -m pip install docker
- name: Build and stage AWX
working-directory: awx
run: |
ansible-playbook -v tools/ansible/build.yml \
-e registry=ghcr.io \
-e registry_username=${{ github.actor }} \
-e registry_password=${{ secrets.GITHUB_TOKEN }} \
-e awx_image=${{ github.repository }} \
-e awx_version=${{ github.event.inputs.version }} \
-e ansible_python_interpreter=$(which python3) \
-e push=yes \
-e awx_official=yes
- name: Log into registry ghcr.io
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log into registry quay.io
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
- name: Copy logos for inclusion in sdist for official build
working-directory: awx
run: |
cp ../awx-logos/awx/ui/client/assets/* awx/ui/public/static/media/
- name: Setup node and npm
uses: actions/setup-node@v2
with:
registry: quay.io
username: ${{ secrets.QUAY_USER }}
password: ${{ secrets.QUAY_TOKEN }}
node-version: '16.13.1'
- name: Prebuild UI for awx image (to speed up build process)
working-directory: awx
run: |
sudo apt-get install gettext
make ui-release
make ui-next
- name: Set build env variables
run: |
echo "DEV_DOCKER_TAG_BASE=ghcr.io/${OWNER,,}" >> $GITHUB_ENV
echo "COMPOSE_TAG=${{ github.event.inputs.version }}" >> $GITHUB_ENV
echo "VERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV
echo "AWX_TEST_VERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV
echo "AWX_TEST_IMAGE=ghcr.io/${OWNER,,}/awx" >> $GITHUB_ENV
echo "AWX_EE_TEST_IMAGE=ghcr.io/${OWNER,,}/awx-ee:${{ github.event.inputs.version }}" >> $GITHUB_ENV
echo "AWX_OPERATOR_TEST_IMAGE=ghcr.io/${OWNER,,}/awx-operator:${{ github.event.inputs.operator_version }}" >> $GITHUB_ENV
env:
OWNER: ${{ github.repository_owner }}
- name: Build and stage AWX
working-directory: awx
env:
DOCKER_BUILDX_PUSH: true
HEADLESS: false
PLATFORMS: linux/amd64,linux/arm64
run: |
make awx-kube-buildx
- name: tag awx-ee:latest with version input
run: |
docker buildx imagetools create \
quay.io/ansible/awx-ee:latest \
--tag ghcr.io/${{ github.repository_owner }}/awx-ee:${{ github.event.inputs.version }}
--tag ${AWX_EE_TEST_IMAGE}
- 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 }}" \
IMG=ghcr.io/${{ github.repository_owner }}/awx-operator:${{ github.event.inputs.operator_version }} \
IMG=${AWX_OPERATOR_TEST_IMAGE} \
make docker-buildx
- name: Pulling images for test deployment with awx-operator
# awx operator molecue test expect to kind load image and buildx exports image to registry and not local
run: |
docker pull ${AWX_OPERATOR_TEST_IMAGE}
docker pull ${AWX_EE_TEST_IMAGE}
docker pull ${AWX_TEST_IMAGE}:${AWX_TEST_VERSION}
- name: Run test deployment with awx-operator
working-directory: awx-operator
run: |
@@ -122,10 +148,6 @@ jobs:
sudo rm -f $(which kustomize)
make kustomize
KUSTOMIZE_PATH=$(readlink -f bin/kustomize) molecule test -s kind
env:
AWX_TEST_IMAGE: ${{ github.repository }}
AWX_TEST_VERSION: ${{ github.event.inputs.version }}
AWX_EE_TEST_IMAGE: ghcr.io/${{ github.repository_owner }}/awx-ee:${{ github.event.inputs.version }}
- name: Create draft release for AWX
working-directory: awx

View File

@@ -2,7 +2,7 @@
PYTHON := $(notdir $(shell for i in python3.11 python3; do command -v $$i; done|sed 1q))
SHELL := bash
DOCKER_COMPOSE ?= docker-compose
DOCKER_COMPOSE ?= docker compose
OFFICIAL ?= no
NODE ?= node
NPM_BIN ?= npm

View File

@@ -191,6 +191,7 @@ SUMMARIZABLE_FK_FIELDS = {
'webhook_credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud', 'credential_type_id'),
'approved_or_denied_by': ('id', 'username', 'first_name', 'last_name'),
'credential_type': DEFAULT_SUMMARY_FIELDS,
'resource': ('ansible_id', 'resource_type'),
}

View File

@@ -2,32 +2,21 @@
# All Rights Reserved.
from django.conf import settings
from django.urls import NoReverseMatch
from rest_framework.reverse import _reverse
from rest_framework.reverse import reverse as drf_reverse
from rest_framework.versioning import URLPathVersioning as BaseVersioning
def drf_reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra):
"""
Copy and monkey-patch `rest_framework.reverse.reverse` to prevent adding unwarranted
query string parameters.
"""
scheme = getattr(request, 'versioning_scheme', None)
if scheme is not None:
try:
url = scheme.reverse(viewname, args, kwargs, request, format, **extra)
except NoReverseMatch:
# In case the versioning scheme reversal fails, fallback to the
# default implementation
url = _reverse(viewname, args, kwargs, request, format, **extra)
else:
url = _reverse(viewname, args, kwargs, request, format, **extra)
def is_optional_api_urlpattern_prefix_request(request):
if settings.OPTIONAL_API_URLPATTERN_PREFIX and request:
if request.path.startswith(f"/api/{settings.OPTIONAL_API_URLPATTERN_PREFIX}"):
url = url.replace('/api', f"/api/{settings.OPTIONAL_API_URLPATTERN_PREFIX}")
return True
return False
def transform_optional_api_urlpattern_prefix_url(request, url):
if is_optional_api_urlpattern_prefix_request(request):
url = url.replace('/api', f"/api/{settings.OPTIONAL_API_URLPATTERN_PREFIX}")
return url
@@ -40,7 +29,9 @@ def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra
kwargs = {}
if 'version' not in kwargs:
kwargs['version'] = settings.REST_FRAMEWORK['DEFAULT_VERSION']
return drf_reverse(viewname, args, kwargs, request, format, **extra)
url = drf_reverse(viewname, args, kwargs, request, format, **extra)
return transform_optional_api_urlpattern_prefix_url(request, url)
class URLPathVersioning(BaseVersioning):

View File

@@ -48,23 +48,23 @@ class AnalyticsRootView(APIView):
def get(self, request, format=None):
data = OrderedDict()
data['authorized'] = reverse('api:analytics_authorized')
data['reports'] = reverse('api:analytics_reports_list')
data['report_options'] = reverse('api:analytics_report_options_list')
data['adoption_rate'] = reverse('api:analytics_adoption_rate')
data['adoption_rate_options'] = reverse('api:analytics_adoption_rate_options')
data['event_explorer'] = reverse('api:analytics_event_explorer')
data['event_explorer_options'] = reverse('api:analytics_event_explorer_options')
data['host_explorer'] = reverse('api:analytics_host_explorer')
data['host_explorer_options'] = reverse('api:analytics_host_explorer_options')
data['job_explorer'] = reverse('api:analytics_job_explorer')
data['job_explorer_options'] = reverse('api:analytics_job_explorer_options')
data['probe_templates'] = reverse('api:analytics_probe_templates_explorer')
data['probe_templates_options'] = reverse('api:analytics_probe_templates_options')
data['probe_template_for_hosts'] = reverse('api:analytics_probe_template_for_hosts_explorer')
data['probe_template_for_hosts_options'] = reverse('api:analytics_probe_template_for_hosts_options')
data['roi_templates'] = reverse('api:analytics_roi_templates_explorer')
data['roi_templates_options'] = reverse('api:analytics_roi_templates_options')
data['authorized'] = reverse('api:analytics_authorized', request=request)
data['reports'] = reverse('api:analytics_reports_list', request=request)
data['report_options'] = reverse('api:analytics_report_options_list', request=request)
data['adoption_rate'] = reverse('api:analytics_adoption_rate', request=request)
data['adoption_rate_options'] = reverse('api:analytics_adoption_rate_options', request=request)
data['event_explorer'] = reverse('api:analytics_event_explorer', request=request)
data['event_explorer_options'] = reverse('api:analytics_event_explorer_options', request=request)
data['host_explorer'] = reverse('api:analytics_host_explorer', request=request)
data['host_explorer_options'] = reverse('api:analytics_host_explorer_options', request=request)
data['job_explorer'] = reverse('api:analytics_job_explorer', request=request)
data['job_explorer_options'] = reverse('api:analytics_job_explorer_options', request=request)
data['probe_templates'] = reverse('api:analytics_probe_templates_explorer', request=request)
data['probe_templates_options'] = reverse('api:analytics_probe_templates_options', request=request)
data['probe_template_for_hosts'] = reverse('api:analytics_probe_template_for_hosts_explorer', request=request)
data['probe_template_for_hosts_options'] = reverse('api:analytics_probe_template_for_hosts_options', request=request)
data['roi_templates'] = reverse('api:analytics_roi_templates_explorer', request=request)
data['roi_templates_options'] = reverse('api:analytics_roi_templates_options', request=request)
return Response(data)

View File

@@ -13,6 +13,7 @@ from django.utils.decorators import method_decorator
from django.views.decorators.csrf import ensure_csrf_cookie
from django.template.loader import render_to_string
from django.utils.translation import gettext_lazy as _
from django.urls import reverse as django_reverse
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
@@ -27,7 +28,7 @@ from awx.main.analytics import all_collectors
from awx.main.ha import is_ha_environment
from awx.main.utils import get_awx_version, get_custom_venv_choices
from awx.main.utils.licensing import validate_entitlement_manifest
from awx.api.versioning import reverse, drf_reverse
from awx.api.versioning import URLPathVersioning, is_optional_api_urlpattern_prefix_request, reverse, drf_reverse
from awx.main.constants import PRIVILEGE_ESCALATION_METHODS
from awx.main.models import Project, Organization, Instance, InstanceGroup, JobTemplate
from awx.main.utils import set_environ
@@ -39,19 +40,19 @@ logger = logging.getLogger('awx.api.views.root')
class ApiRootView(APIView):
permission_classes = (AllowAny,)
name = _('REST API')
versioning_class = None
versioning_class = URLPathVersioning
swagger_topic = 'Versioning'
@method_decorator(ensure_csrf_cookie)
def get(self, request, format=None):
'''List supported API versions'''
v2 = reverse('api:api_v2_root_view', kwargs={'version': 'v2'})
v2 = reverse('api:api_v2_root_view', request=request, kwargs={'version': 'v2'})
data = OrderedDict()
data['description'] = _('AWX REST API')
data['current_version'] = v2
data['available_versions'] = dict(v2=v2)
data['oauth2'] = drf_reverse('api:oauth_authorization_root_view')
if not is_optional_api_urlpattern_prefix_request(request):
data['oauth2'] = drf_reverse('api:oauth_authorization_root_view')
data['custom_logo'] = settings.CUSTOM_LOGO
data['custom_login_info'] = settings.CUSTOM_LOGIN_INFO
data['login_redirect_override'] = settings.LOGIN_REDIRECT_OVERRIDE
@@ -130,6 +131,7 @@ class ApiVersionRootView(APIView):
data['mesh_visualizer'] = reverse('api:mesh_visualizer_view', request=request)
data['bulk'] = reverse('api:bulk', request=request)
data['analytics'] = reverse('api:analytics_root_view', request=request)
data['service_index'] = django_reverse('service-index-root')
return Response(data)

View File

@@ -639,7 +639,10 @@ class UserAccess(BaseAccess):
"""
model = User
prefetch_related = ('profile',)
prefetch_related = (
'profile',
'resource',
)
def filtered_queryset(self):
if settings.ORG_ADMINS_CAN_SEE_ALL_USERS and (self.user.admin_of_organizations.exists() or self.user.auditor_of_organizations.exists()):
@@ -835,6 +838,7 @@ class OrganizationAccess(NotificationAttachMixin, BaseAccess):
prefetch_related = (
'created_by',
'modified_by',
'resource', # dab_resource_registry
)
# organization admin_role is not a parent of organization auditor_role
notification_attach_roles = ['admin_role', 'auditor_role']
@@ -1303,6 +1307,7 @@ class TeamAccess(BaseAccess):
'created_by',
'modified_by',
'organization',
'resource', # dab_resource_registry
)
def filtered_queryset(self):

View File

@@ -1,6 +1,7 @@
import os
import psycopg
import select
from copy import deepcopy
from contextlib import contextmanager
@@ -94,8 +95,8 @@ class PubSub(object):
def create_listener_connection():
conf = settings.DATABASES['default'].copy()
conf['OPTIONS'] = conf.get('OPTIONS', {}).copy()
conf = deepcopy(settings.DATABASES['default'])
conf['OPTIONS'] = deepcopy(conf.get('OPTIONS', {}))
# Modify the application name to distinguish from other connections the process might use
conf['OPTIONS']['application_name'] = get_application_name(settings.CLUSTER_HOST_ID, function='listener')

View File

@@ -6,6 +6,8 @@ from django.conf import settings # noqa
from django.db import connection
from django.db.models.signals import pre_delete # noqa
# django-ansible-base
from ansible_base.resource_registry.fields import AnsibleResourceField
from ansible_base.lib.utils.models import prevent_search
# AWX
@@ -99,6 +101,7 @@ from awx.main.access import get_user_queryset, check_user_access, check_user_acc
User.add_to_class('get_queryset', get_user_queryset)
User.add_to_class('can_access', check_user_access)
User.add_to_class('can_access_with_errors', check_user_access_with_errors)
User.add_to_class('resource', AnsibleResourceField(primary_key_field="id"))
def convert_jsonfields():

View File

@@ -498,7 +498,7 @@ class JobNotificationMixin(object):
# 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:
if len(body.strip().splitlines()) < 1:
# blank body
body = '\r\n'.join(
[

View File

@@ -10,6 +10,8 @@ from django.contrib.sessions.models import Session
from django.utils.timezone import now as tz_now
from django.utils.translation import gettext_lazy as _
# django-ansible-base
from ansible_base.resource_registry.fields import AnsibleResourceField
# AWX
from awx.api.versioning import reverse
@@ -103,6 +105,7 @@ class Organization(CommonModel, NotificationFieldsModel, ResourceMixin, CustomVi
approval_role = ImplicitRoleField(
parent_role='admin_role',
)
resource = AnsibleResourceField(primary_key_field="id")
def get_absolute_url(self, request=None):
return reverse('api:organization_detail', kwargs={'pk': self.pk}, request=request)
@@ -151,6 +154,7 @@ class Team(CommonModelNameNotUnique, ResourceMixin):
read_role = ImplicitRoleField(
parent_role=['organization.auditor_role', 'member_role'],
)
resource = AnsibleResourceField(primary_key_field="id")
def get_absolute_url(self, request=None):
return reverse('api:team_detail', kwargs={'pk': self.pk}, request=request)

View File

@@ -1599,7 +1599,8 @@ class UnifiedJob(
extra["controller_node"] = self.controller_node or "NOT_SET"
elif state == "execution_node_chosen":
extra["execution_node"] = self.execution_node or "NOT_SET"
logger_job_lifecycle.info(msg, extra=extra)
logger_job_lifecycle.info(f"{msg} {json.dumps(extra)}")
@property
def launched_by(self):

View File

@@ -49,6 +49,70 @@ class ReceptorConnectionType(Enum):
STREAMTLS = 2
"""
Translate receptorctl messages that come in over stdout into
structured messages. Currently, these are error messages.
"""
class ReceptorErrorBase:
_MESSAGE = 'Receptor Error'
def __init__(self, node: str = 'N/A', state_name: str = 'N/A'):
self.node = node
self.state_name = state_name
def __str__(self):
return f"{self.__class__.__name__} '{self._MESSAGE}' on node '{self.node}' with state '{self.state_name}'"
class WorkUnitError(ReceptorErrorBase):
_MESSAGE = 'unknown work unit '
def __init__(self, work_unit_id: str, *args, **kwargs):
super().__init__(*args, **kwargs)
self.work_unit_id = work_unit_id
def __str__(self):
return f"{super().__str__()} work unit id '{self.work_unit_id}'"
class WorkUnitCancelError(WorkUnitError):
_MESSAGE = 'error cancelling remote unit: unknown work unit '
class WorkUnitResultsError(WorkUnitError):
_MESSAGE = 'Failed to get results: unknown work unit '
class UnknownError(ReceptorErrorBase):
_MESSAGE = 'Unknown receptor ctl error'
def __init__(self, msg, *args, **kwargs):
super().__init__(*args, **kwargs)
self._MESSAGE = msg
class FuzzyError:
def __new__(self, e: RuntimeError, node: str, state_name: str):
"""
At the time of writing this comment all of the sub-classes detection
is centralized in this parent class. It's like a Router().
Someone may find it better to push down the error detection logic into
each sub-class.
"""
msg = e.args[0]
common_startswith = (WorkUnitCancelError, WorkUnitResultsError, WorkUnitError)
for klass in common_startswith:
if msg.startswith(klass._MESSAGE):
work_unit_id = msg[len(klass._MESSAGE) :]
return klass(work_unit_id, node=node, state_name=state_name)
return UnknownError(msg, node=node, state_name=state_name)
def read_receptor_config():
# for K8S deployments, getting a lock is necessary as another process
# may be re-writing the config at this time
@@ -185,6 +249,7 @@ def run_until_complete(node, timing_data=None, **kwargs):
timing_data['transmit_timing'] = run_start - transmit_start
run_timing = 0.0
stdout = ''
state_name = 'local var never set'
try:
resultfile = receptor_ctl.get_work_results(unit_id)
@@ -205,13 +270,33 @@ def run_until_complete(node, timing_data=None, **kwargs):
stdout = resultfile.read()
stdout = str(stdout, encoding='utf-8')
except RuntimeError as e:
receptor_e = FuzzyError(e, node, state_name)
if type(receptor_e) in (
WorkUnitError,
WorkUnitResultsError,
):
logger.warning(f'While consuming job results: {receptor_e}')
else:
raise
finally:
if settings.RECEPTOR_RELEASE_WORK:
res = receptor_ctl.simple_command(f"work release {unit_id}")
if res != {'released': unit_id}:
logger.warning(f'Could not confirm release of receptor work unit id {unit_id} from {node}, data: {res}')
try:
res = receptor_ctl.simple_command(f"work release {unit_id}")
receptor_ctl.close()
if res != {'released': unit_id}:
logger.warning(f'Could not confirm release of receptor work unit id {unit_id} from {node}, data: {res}')
receptor_ctl.close()
except RuntimeError as e:
receptor_e = FuzzyError(e, node, state_name)
if type(receptor_e) in (
WorkUnitError,
WorkUnitCancelError,
):
logger.warning(f"While releasing work: {receptor_e}")
else:
logger.error(f"While releasing work: {receptor_e}")
if state_name.lower() == 'failed':
work_detail = status.get('Detail', '')
@@ -275,7 +360,7 @@ def _convert_args_to_cli(vargs):
args = ['cleanup']
for option in ('exclude_strings', 'remove_images'):
if vargs.get(option):
args.append('--{}={}'.format(option.replace('_', '-'), ' '.join(vargs.get(option))))
args.append('--{}="{}"'.format(option.replace('_', '-'), ' '.join(vargs.get(option))))
for option in ('file_pattern', 'image_prune', 'process_isolation_executable', 'grace_period'):
if vargs.get(option) is True:
args.append('--{}'.format(option.replace('_', '-')))

View File

@@ -3,5 +3,5 @@
hosts: all
tasks:
- name: Hello Message
debug:
ansible.builtin.debug:
msg: "Hello World!"

View File

@@ -6,7 +6,7 @@ from django.test import Client
from rest_framework.test import APIRequestFactory
from awx.api.generics import LoggedLoginView
from awx.api.versioning import drf_reverse
from rest_framework.reverse import reverse as drf_reverse
@pytest.mark.django_db

View File

@@ -8,8 +8,10 @@ from django.db import connection
from django.test.utils import override_settings
from django.utils.encoding import smart_str, smart_bytes
from rest_framework.reverse import reverse as drf_reverse
from awx.main.utils.encryption import decrypt_value, get_encryption_key
from awx.api.versioning import reverse, drf_reverse
from awx.api.versioning import reverse
from awx.main.models.oauth import OAuth2Application as Application, OAuth2AccessToken as AccessToken
from awx.main.tests.functional import immediate_on_commit
from awx.sso.models import UserEnterpriseAuth

View File

@@ -0,0 +1,39 @@
import pytest
from ansible_base.resource_registry.models import Resource
from awx.api.versioning import reverse
def assert_has_resource(list_response, obj=None):
data = list_response.data
assert 'resource' in data['results'][0]['summary_fields']
resource_data = data['results'][0]['summary_fields']['resource']
assert resource_data['ansible_id']
resource = Resource.objects.filter(ansible_id=resource_data['ansible_id']).first()
assert resource
assert resource.content_object
if obj:
objects = [Resource.objects.get(ansible_id=entry['summary_fields']['resource']['ansible_id']).content_object for entry in data['results']]
assert obj in objects
@pytest.mark.django_db
def test_organization_ansible_id(organization, admin_user, get):
url = reverse('api:organization_list')
response = get(url=url, user=admin_user, expect=200)
assert_has_resource(response, obj=organization)
@pytest.mark.django_db
def test_team_ansible_id(team, admin_user, get):
url = reverse('api:team_list')
response = get(url=url, user=admin_user, expect=200)
assert_has_resource(response, obj=team)
@pytest.mark.django_db
def test_user_ansible_id(rando, admin_user, get):
url = reverse('api:user_list')
response = get(url=url, user=admin_user, expect=200)
assert_has_resource(response, obj=rando)

View File

@@ -3,7 +3,7 @@ from awx.main.tasks.receptor import _convert_args_to_cli
def test_file_cleanup_scenario():
args = _convert_args_to_cli({'exclude_strings': ['awx_423_', 'awx_582_'], 'file_pattern': '/tmp/awx_*_*'})
assert ' '.join(args) == 'cleanup --exclude-strings=awx_423_ awx_582_ --file-pattern=/tmp/awx_*_*'
assert ' '.join(args) == 'cleanup --exclude-strings="awx_423_ awx_582_" --file-pattern=/tmp/awx_*_*'
def test_image_cleanup_scenario():
@@ -17,5 +17,5 @@ def test_image_cleanup_scenario():
}
)
assert (
' '.join(args) == 'cleanup --remove-images=quay.invalid/foo/bar:latest quay.invalid/foo/bar:devel --image-prune --process-isolation-executable=podman'
' '.join(args) == 'cleanup --remove-images="quay.invalid/foo/bar:latest quay.invalid/foo/bar:devel" --image-prune --process-isolation-executable=podman'
)

View File

@@ -3,7 +3,6 @@
from copy import copy
import json
import json_log_formatter
import logging
import traceback
import socket
@@ -15,15 +14,6 @@ from django.core.serializers.json import DjangoJSONEncoder
from django.conf import settings
class JobLifeCycleFormatter(json_log_formatter.JSONFormatter):
def json_record(self, message: str, extra: dict, record: logging.LogRecord):
if 'time' not in extra:
extra['time'] = now()
if record.exc_info:
extra['exc_info'] = self.formatException(record.exc_info)
return extra
class TimeFormatter(logging.Formatter):
"""
Custom log formatter used for inventory imports

View File

@@ -2,6 +2,7 @@ import json
import logging
import asyncio
from typing import Dict
from copy import deepcopy
import ipaddress
@@ -302,14 +303,17 @@ class WebSocketRelayManager(object):
self.stats_mgr.start()
# Set up a pg_notify consumer for allowing web nodes to "provision" and "deprovision" themselves gracefully.
database_conf = settings.DATABASES['default'].copy()
database_conf['OPTIONS'] = database_conf.get('OPTIONS', {}).copy()
database_conf = deepcopy(settings.DATABASES['default'])
database_conf['OPTIONS'] = deepcopy(database_conf.get('OPTIONS', {}))
for k, v in settings.LISTENER_DATABASES.get('default', {}).items():
database_conf[k] = v
for k, v in settings.LISTENER_DATABASES.get('default', {}).get('OPTIONS', {}).items():
database_conf['OPTIONS'][k] = v
if 'PASSWORD' in database_conf:
database_conf['OPTIONS']['password'] = database_conf.pop('PASSWORD')
task = None
# Establishes a websocket connection to /websocket/relay on all API servers
@@ -320,10 +324,10 @@ class WebSocketRelayManager(object):
dbname=database_conf['NAME'],
host=database_conf['HOST'],
user=database_conf['USER'],
password=database_conf['PASSWORD'],
port=database_conf['PORT'],
**database_conf.get("OPTIONS", {}),
)
await async_conn.set_autocommit(True)
task = event_loop.create_task(self.on_ws_heartbeat(async_conn), name="on_ws_heartbeat")
logger.info("Creating `on_ws_heartbeat` task in event loop.")

View File

@@ -849,7 +849,6 @@ LOGGING = {
'json': {'()': 'awx.main.utils.formatters.LogstashFormatter'},
'timed_import': {'()': 'awx.main.utils.formatters.TimeFormatter', 'format': '%(relativeSeconds)9.3f %(levelname)-8s %(message)s'},
'dispatcher': {'format': '%(asctime)s %(levelname)-8s [%(guid)s] %(name)s PID:%(process)d %(message)s'},
'job_lifecycle': {'()': 'awx.main.utils.formatters.JobLifeCycleFormatter'},
},
# Extended below based on install scenario. You probably don't want to add something directly here.
# See 'handler_config' below.
@@ -917,7 +916,7 @@ handler_config = {
'wsrelay': {'filename': 'wsrelay.log'},
'task_system': {'filename': 'task_system.log'},
'rbac_migrations': {'filename': 'tower_rbac_migrations.log'},
'job_lifecycle': {'filename': 'job_lifecycle.log', 'formatter': 'job_lifecycle'},
'job_lifecycle': {'filename': 'job_lifecycle.log'},
'rsyslog_configurer': {'filename': 'rsyslog_configurer.log'},
'cache_clear': {'filename': 'cache_clear.log'},
'ws_heartbeat': {'filename': 'ws_heartbeat.log'},
@@ -1131,3 +1130,6 @@ include(settings_file)
# example if set to '' API pattern will be /api
# example if set to 'controller' API pattern will be /api AND /api/controller
OPTIONAL_API_URLPATTERN_PREFIX = ''
# Use AWX base view, to give 401 on unauthenticated requests
ANSIBLE_BASE_CUSTOM_VIEW_PARENT = 'awx.api.generics.APIView'

View File

@@ -39,7 +39,7 @@
{% else %}
<li><a href="{% url 'api:login' %}?next={{ request.get_full_path }}" data-toggle="tooltip" data-placement="bottom" data-delay="1000" title="Log in"><span class="glyphicon glyphicon-log-in"></span>Log in</a></li>
{% endif %}
<li><a href="//docs.ansible.com/ansible-tower/{{short_tower_version}}/html/towerapi/index.html" target="_blank" data-toggle="tooltip" data-placement="bottom" data-delay="1000" title="{% trans 'API Guide' %}"><span class="glyphicon glyphicon-question-sign"></span><span class="visible-xs-inline">{% trans 'API Guide' %}</span></a></li>
<li><a href="//ansible.readthedocs.io/projects/awx/en/latest/rest_api/index.html" target="_blank" data-toggle="tooltip" data-placement="bottom" data-delay="1000" title="{% trans 'API Guide' %}"><span class="glyphicon glyphicon-question-sign"></span><span class="visible-xs-inline">{% trans 'API Guide' %}</span></a></li>
<li><a href="/" data-toggle="tooltip" data-placement="bottom" data-delay="1000" title="{% trans 'Back to application' %}"><span class="glyphicon glyphicon-circle-arrow-left"></span><span class="visible-xs-inline">{% trans 'Back to application' %}</span></a></li>
<li class="hidden-xs"><a href="#" class="resize" data-toggle="tooltip" data-placement="bottom" data-delay="1000" title="{% trans 'Resize' %}"><span class="glyphicon glyphicon-resize-full"></span></a></li>
</ul>

View File

@@ -147,8 +147,12 @@ def main():
if redirect_uris is not None:
application_fields['redirect_uris'] = ' '.join(redirect_uris)
# If the state was present and we can let the module build or update the existing application, this will return on its own
module.create_or_update_if_needed(application, application_fields, endpoint='applications', item_type='application')
response = module.create_or_update_if_needed(application, application_fields, endpoint='applications', item_type='application', auto_exit=False)
if 'client_id' in response:
module.json_output['client_id'] = response['client_id']
if 'client_secret' in response:
module.json_output['client_secret'] = response['client_secret']
module.exit_json(**module.json_output)
if __name__ == '__main__':

View File

@@ -39,6 +39,16 @@ options:
description:
- Limit to use for the I(job_template).
type: str
tags:
description:
- Specific tags to apply from the I(job_template).
type: list
elements: str
skip_tags:
description:
- Specific tags to skip from the I(job_template).
type: list
elements: str
scm_branch:
description:
- A specific branch of the SCM project to run the template on.
@@ -100,6 +110,8 @@ def main():
organization=dict(),
inventory=dict(),
limit=dict(),
tags=dict(type='list', elements='str'),
skip_tags=dict(type='list', elements='str'),
scm_branch=dict(),
extra_vars=dict(type='dict'),
wait=dict(required=False, default=True, type='bool'),
@@ -128,6 +140,14 @@ def main():
if field_val is not None:
optional_args[field_name] = field_val
# Special treatment of tags parameters
job_tags = module.params.get('tags')
if job_tags is not None:
optional_args['job_tags'] = ",".join(job_tags)
skip_tags = module.params.get('skip_tags')
if skip_tags is not None:
optional_args['skip_tags'] = ",".join(skip_tags)
# Create a datastructure to pass into our job launch
post_data = {}
for arg_name, arg_value in optional_args.items():
@@ -152,6 +172,8 @@ def main():
check_vars_to_prompts = {
'inventory': 'ask_inventory_on_launch',
'limit': 'ask_limit_on_launch',
'job_tags': 'ask_tags_on_launch',
'skip_tags': 'ask_skip_tags_on_launch',
'scm_branch': 'ask_scm_branch_on_launch',
}

View File

@@ -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 'The template rendering return a blank body' in body[1]
assert '{"started_by": "My Placeholder"}' in body[1]

View File

@@ -4,7 +4,7 @@ __metaclass__ = type
import pytest
from awx.main.models import WorkflowJobTemplate, NotificationTemplate
from awx.main.models import WorkflowJobTemplate, WorkflowJob, NotificationTemplate
@pytest.mark.django_db
@@ -135,6 +135,37 @@ def test_associate_only_on_success(run_module, admin_user, organization, project
assert list(wfjt.notification_templates_error.values_list('id', flat=True)) == [nt1.id]
@pytest.mark.django_db
def test_workflow_launch_with_prompting(run_module, admin_user, organization, inventory):
WorkflowJobTemplate.objects.create(
name='foo-workflow-launch-test',
organization=organization,
ask_variables_on_launch=True,
ask_inventory_on_launch=True,
ask_tags_on_launch=True,
ask_skip_tags_on_launch=True,
)
result = run_module(
'workflow_launch',
dict(
name='foo-workflow-launch-test',
inventory=inventory.name,
wait=False,
extra_vars={"var1": "My First Variable", "var2": "My Second Variable", "var3": "My Third Variable"},
tags=["my_tag"],
skip_tags=["your_tag", "their_tag"],
),
admin_user,
)
assert result.get('changed', True), result
job = WorkflowJob.objects.get(id=result['id'])
assert job.extra_vars == '{"var1": "My First Variable", "var2": "My Second Variable", "var3": "My Third Variable"}'
assert job.inventory == inventory
assert job.job_tags == "my_tag"
assert job.skip_tags == "your_tag,their_tag"
@pytest.mark.django_db
def test_delete_with_spec(run_module, admin_user, organization, survey_spec):
WorkflowJobTemplate.objects.create(organization=organization, name='foo-workflow', survey_enabled=True, survey_spec=survey_spec)

View File

@@ -1,63 +1,63 @@
---
- name: Generate a random string for test
set_fact:
ansible.builtin.set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
when: test_id is not defined
- name: Generate names
set_fact:
ansible.builtin.set_fact:
inv_name: "AWX-Collection-tests-ad_hoc_command_cancel-inventory-{{ test_id }}"
ssh_cred_name: "AWX-Collection-tests-ad_hoc_command_cancel-ssh-cred-{{ test_id }}"
org_name: "AWX-Collection-tests-ad_hoc_command_cancel-org-{{ test_id }}"
- name: Create a New Organization
organization:
awx.awx.organization:
name: "{{ org_name }}"
- name: Create an Inventory
inventory:
awx.awx.inventory:
name: "{{ inv_name }}"
organization: "{{ org_name }}"
state: present
- name: Add localhost to the Inventory
host:
awx.awx.host:
name: localhost
inventory: "{{ inv_name }}"
variables:
ansible_connection: local
- name: Create a Credential
credential:
awx.awx.credential:
name: "{{ ssh_cred_name }}"
organization: "{{ org_name }}"
credential_type: 'Machine'
state: present
- name: Launch an Ad Hoc Command
ad_hoc_command:
awx.awx.ad_hoc_command:
inventory: "{{ inv_name }}"
credential: "{{ ssh_cred_name }}"
module_name: "command"
module_args: "sleep 100"
register: command
- assert:
- ansible.builtin.assert:
that:
- "command is changed"
- name: Cancel the command
ad_hoc_command_cancel:
awx.awx.ad_hoc_command_cancel:
command_id: "{{ command.id }}"
request_timeout: 60
register: results
- assert:
- ansible.builtin.assert:
that:
- results is changed
- name: "Wait for up to a minute until the job enters the can_cancel: False state"
debug:
ansible.builtin.debug:
msg: "The job can_cancel status has transitioned into False, we can proceed with testing"
until: not job_status
retries: 6
@@ -66,51 +66,51 @@
job_status: "{{ lookup('awx.awx.controller_api', 'ad_hoc_commands/'+ command.id | string +'/cancel')['can_cancel'] }}"
- name: Cancel the command with hard error if it's not running
ad_hoc_command_cancel:
awx.awx.ad_hoc_command_cancel:
command_id: "{{ command.id }}"
fail_if_not_running: true
register: results
ignore_errors: true
- assert:
- ansible.builtin.assert:
that:
- results is failed
- name: Cancel an already canceled command (assert failure)
ad_hoc_command_cancel:
awx.awx.ad_hoc_command_cancel:
command_id: "{{ command.id }}"
fail_if_not_running: true
register: results
ignore_errors: true
- assert:
- ansible.builtin.assert:
that:
- results is failed
- name: Check module fails with correct msg
ad_hoc_command_cancel:
awx.awx.ad_hoc_command_cancel:
command_id: 9999999999
register: result
ignore_errors: true
- assert:
- ansible.builtin.assert:
that:
- "result.msg == 'Unable to find command with id 9999999999'"
- name: Delete the Credential
credential:
awx.awx.credential:
name: "{{ ssh_cred_name }}"
organization: "{{ org_name }}"
credential_type: 'Machine'
state: absent
- name: Delete the Inventory
inventory:
awx.awx.inventory:
name: "{{ inv_name }}"
organization: "{{ org_name }}"
state: absent
- name: Remove the Organization
organization:
awx.awx.organization:
name: "{{ org_name }}"
state: absent

View File

@@ -103,6 +103,7 @@
- assert:
that:
- "result is changed"
- "'client_secret' in result"
- name: Rename an inventory
application:

View File

@@ -1,23 +1,23 @@
---
- name: Generate a test ID
set_fact:
ansible.builtin.set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
when: test_id is not defined
- name: Generate hostnames
set_fact:
ansible.builtin.set_fact:
hostname1: "AWX-Collection-tests-instance1.{{ test_id }}.example.com"
hostname2: "AWX-Collection-tests-instance2.{{ test_id }}.example.com"
hostname3: "AWX-Collection-tests-instance3.{{ test_id }}.example.com"
register: facts
- name: Get the k8s setting
set_fact:
ansible.builtin.set_fact:
IS_K8S: "{{ controller_settings['IS_K8S'] | default(False) }}"
vars:
controller_settings: "{{ lookup('awx.awx.controller_api', 'settings/all') }}"
- debug:
- ansible.builtin.debug:
msg: "Skipping instance test since this is instance is not running on a K8s platform"
when: not IS_K8S
@@ -32,7 +32,7 @@
- "{{ hostname2 }}"
register: result
- assert:
- ansible.builtin.assert:
that:
- result is changed
@@ -44,7 +44,7 @@
capacity_adjustment: 0.4
register: result
- assert:
- ansible.builtin.assert:
that:
- result is changed
@@ -54,7 +54,7 @@
capacity_adjustment: 0.7
register: result
- assert:
- ansible.builtin.assert:
that:
- result is changed
@@ -78,7 +78,7 @@
node_state: installed
register: result
- assert:
- ansible.builtin.assert:
that:
- result is changed
@@ -89,7 +89,7 @@
node_state: installed
register: result
- assert:
- ansible.builtin.assert:
that:
- result is changed
@@ -103,7 +103,7 @@
- "{{ hostname2 }}"
register: result
- assert:
- ansible.builtin.assert:
that:
- result is changed
@@ -115,7 +115,7 @@
peers: []
register: result
- assert:
- ansible.builtin.assert:
that:
- result is changed

View File

@@ -1,11 +1,11 @@
---
- name: Generate a random string for test
set_fact:
ansible.builtin.set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
when: test_id is not defined
- name: Generate usernames
set_fact:
ansible.builtin.set_fact:
usernames:
- "AWX-Collection-tests-api_lookup-user1-{{ test_id }}"
- "AWX-Collection-tests-api_lookup-user2-{{ test_id }}"
@@ -20,7 +20,7 @@
register: controller_meta
- name: Generate the name of our plugin
set_fact:
ansible.builtin.set_fact:
plugin_name: "{{ controller_meta.prefix }}.controller_api"
- name: Create all of our users
@@ -38,7 +38,7 @@
register: results
ignore_errors: true
- assert:
- ansible.builtin.assert:
that:
- "'dne' in (results.msg | lower)"
@@ -49,48 +49,48 @@
loop: "{{ hosts }}"
- name: Test too many params (failure from validation of terms)
set_fact:
ansible.builtin.set_fact:
junk: "{{ query(plugin_name, 'users', 'teams', query_params={}, ) }}"
ignore_errors: true
register: result
- assert:
- ansible.builtin.assert:
that:
- result is failed
- "'You must pass exactly one endpoint to query' in result.msg"
- name: Try to load invalid endpoint
set_fact:
ansible.builtin.set_fact:
junk: "{{ query(plugin_name, 'john', query_params={}, ) }}"
ignore_errors: true
register: result
- assert:
- ansible.builtin.assert:
that:
- result is failed
- "'The requested object could not be found at' in result.msg"
- name: Load user of a specific name without promoting objects
set_fact:
ansible.builtin.set_fact:
users_list: "{{ lookup(plugin_name, 'users', query_params={ 'username' : user_creation_results['results'][0]['item'] }, return_objects=False) }}"
- assert:
- ansible.builtin.assert:
that:
- users_list['results'] | length() == 1
- users_list['count'] == 1
- users_list['results'][0]['id'] == user_creation_results['results'][0]['id']
- name: Load user of a specific name with promoting objects
set_fact:
ansible.builtin.set_fact:
user_objects: "{{ query(plugin_name, 'users', query_params={ 'username' : user_creation_results['results'][0]['item'] }, return_objects=True ) }}"
- assert:
- ansible.builtin.assert:
that:
- user_objects | length() == 1
- users_list['results'][0]['id'] == user_objects[0]['id']
- name: Loop over one user with the loop syntax
assert:
ansible.builtin.assert:
that:
- item['id'] == user_creation_results['results'][0]['id']
loop: "{{ query(plugin_name, 'users', query_params={ 'username' : user_creation_results['results'][0]['item'] } ) }}"
@@ -98,91 +98,91 @@
label: "{{ item.id }}"
- name: Get a page of users as just ids
set_fact:
ansible.builtin.set_fact:
users: "{{ query(plugin_name, 'users', query_params={ 'username__endswith': test_id, 'page_size': 2 }, return_ids=True ) }}"
- debug:
msg: "{{ users }}"
- name: Assert that user list has 2 ids only and that they are strings, not ints
assert:
- name: assert that user list has 2 ids only and that they are strings, not ints
ansible.builtin.assert:
that:
- users | length() == 2
- user_creation_results['results'][0]['id'] not in users
- user_creation_results['results'][0]['id'] | string in users
- name: Get all users of a system through next attribute
set_fact:
ansible.builtin.set_fact:
users: "{{ query(plugin_name, 'users', query_params={ 'username__endswith': test_id, 'page_size': 1 }, return_all=true ) }}"
- assert:
- ansible.builtin.assert:
that:
- users | length() >= 3
- name: Get all of the users created with a max_objects of 1
set_fact:
ansible.builtin.set_fact:
users: "{{ lookup(plugin_name, 'users', query_params={ 'username__endswith': test_id, 'page_size': 1 }, return_all=true, max_objects=1 ) }}"
ignore_errors: true
register: max_user_errors
- assert:
- ansible.builtin.assert:
that:
- max_user_errors is failed
- "'List view at users returned 3 objects, which is more than the maximum allowed by max_objects' in max_user_errors.msg"
- name: Get the ID of the first user created and verify that it is correct
assert:
ansible.builtin.assert:
that: "query(plugin_name, 'users', query_params={ 'username' : user_creation_results['results'][0]['item'] }, return_ids=True)[0] == user_creation_results['results'][0]['id'] | string"
- name: Try to get an ID of someone who does not exist
set_fact:
ansible.builtin.set_fact:
failed_user_id: "{{ query(plugin_name, 'users', query_params={ 'username': 'john jacob jingleheimer schmidt' }, expect_one=True) }}"
register: result
ignore_errors: true
- assert:
- ansible.builtin.assert:
that:
- result is failed
- "'Expected one object from endpoint users' in result['msg']"
- name: Lookup too many users
set_fact:
ansible.builtin.set_fact:
too_many_user_ids: " {{ query(plugin_name, 'users', query_params={ 'username__endswith': test_id }, expect_one=True) }}"
register: results
ignore_errors: true
- assert:
- ansible.builtin.assert:
that:
- results is failed
- "'Expected one object from endpoint users, but obtained 3' in results['msg']"
- name: Get the ping page
set_fact:
ansible.builtin.set_fact:
ping_data: "{{ lookup(plugin_name, 'ping' ) }}"
register: results
- assert:
- ansible.builtin.assert:
that:
- results is succeeded
- "'active_node' in ping_data"
- name: "Make sure that expect_objects fails on an API page"
set_fact:
ansible.builtin.set_fact:
my_var: "{{ lookup(plugin_name, 'settings/ui', expect_objects=True) }}"
ignore_errors: true
register: results
- assert:
- ansible.builtin.assert:
that:
- results is failed
- "'Did not obtain a list or detail view at settings/ui, and expect_objects or expect_one is set to True' in results.msg"
# DOCS Example Tests
- name: Load the UI settings
set_fact:
ansible.builtin.set_fact:
controller_settings: "{{ lookup('awx.awx.controller_api', 'settings/ui') }}"
- assert:
- ansible.builtin.assert:
that:
- "'CUSTOM_LOGO' in controller_settings"
@@ -191,7 +191,7 @@
msg: "Admin users: {{ query('awx.awx.controller_api', 'users', query_params={ 'is_superuser': true }) | map(attribute='username') | join(', ') }}"
register: results
- assert:
- ansible.builtin.assert:
that:
- "'admin' in results.msg"
@@ -211,7 +211,7 @@
register: role_revoke
when: "query('awx.awx.controller_api', 'users', query_params={ 'username': 'DNE_TESTING' }) | length == 1"
- assert:
- ansible.builtin.assert:
that:
- role_revoke is skipped
@@ -227,7 +227,7 @@
) | map(attribute='name') | list }}
register: group_creation
- assert:
- ansible.builtin.assert:
that: group_creation is changed
always:

View File

@@ -1,50 +1,50 @@
---
- name: Get our collection package
controller_meta:
awx.awx.controller_meta:
register: controller_meta
- name: Generate the name of our plugin
set_fact:
ansible.builtin.set_fact:
plugin_name: "{{ controller_meta.prefix }}.schedule_rrule"
- name: Test too many params (failure from validation of terms)
debug:
ansible.builtin.debug:
msg: "{{ query(plugin_name | string, 'none', 'weekly', start_date='2020-4-16 03:45:07') }}"
ignore_errors: true
register: result
- assert:
- ansible.builtin.assert:
that:
- result is failed
- "'You may only pass one schedule type in at a time' in result.msg"
- name: Test invalid frequency (failure from validation of term)
debug:
ansible.builtin.debug:
msg: "{{ query(plugin_name, 'john', start_date='2020-4-16 03:45:07') }}"
ignore_errors: true
register: result
- assert:
- ansible.builtin.assert:
that:
- result is failed
- "'Frequency of john is invalid' in result.msg"
- name: Test an invalid start date (generic failure case from get_rrule)
debug:
ansible.builtin.debug:
msg: "{{ query(plugin_name, 'none', start_date='invalid') }}"
ignore_errors: true
register: result
- assert:
- ansible.builtin.assert:
that:
- result is failed
- "'Parameter start_date must be in the format YYYY-MM-DD' in result.msg"
- name: Test end_on as count (generic success case)
debug:
ansible.builtin.debug:
msg: "{{ query(plugin_name, 'minute', start_date='2020-4-16 03:45:07', end_on='2') }}"
register: result
- assert:
- ansible.builtin.assert:
that:
- result.msg == 'DTSTART;TZID=America/New_York:20200416T034507 RRULE:FREQ=MINUTELY;COUNT=2;INTERVAL=1'

View File

@@ -11,7 +11,7 @@
register: result
- name: Changing setting to true should have changed the value
assert:
ansible.builtin.assert:
that:
- "result is changed"
@@ -22,7 +22,7 @@
register: result
- name: Changing setting to true again should not change the value
assert:
ansible.builtin.assert:
that:
- "result is not changed"
@@ -33,17 +33,17 @@
register: result
- name: Changing setting back to false should have changed the value
assert:
ansible.builtin.assert:
that:
- "result is changed"
- name: Set the value of AWX_ISOLATION_SHOW_PATHS to a baseline
settings:
awx.awx.settings:
name: AWX_ISOLATION_SHOW_PATHS
value: '["/var/lib/awx/projects/"]'
- name: Set the value of AWX_ISOLATION_SHOW_PATHS to get an error back from the controller
settings:
awx.awx.settings:
settings:
AWX_ISOLATION_SHOW_PATHS:
'not': 'a valid'
@@ -51,75 +51,75 @@
register: result
ignore_errors: true
- assert:
- ansible.builtin.assert:
that:
- "result is failed"
- name: Set the value of AWX_ISOLATION_SHOW_PATHS
settings:
awx.awx.settings:
name: AWX_ISOLATION_SHOW_PATHS
value: '["/var/lib/awx/projects/", "/tmp"]'
register: result
- assert:
- ansible.builtin.assert:
that:
- "result is changed"
- name: Attempt to set the value of AWX_ISOLATION_BASE_PATH to what it already is
settings:
awx.awx.settings:
name: AWX_ISOLATION_BASE_PATH
value: /tmp
register: result
- debug:
- ansible.builtin.debug:
msg: "{{ result }}"
- assert:
- ansible.builtin.assert:
that:
- "result is not changed"
- name: Apply a single setting via settings
settings:
awx.awx.settings:
name: AWX_ISOLATION_SHOW_PATHS
value: '["/var/lib/awx/projects/", "/var/tmp"]'
register: result
- assert:
- ansible.builtin.assert:
that:
- "result is changed"
- name: Apply multiple setting via settings with no change
settings:
awx.awx.settings:
settings:
AWX_ISOLATION_BASE_PATH: /tmp
AWX_ISOLATION_SHOW_PATHS: ["/var/lib/awx/projects/", "/var/tmp"]
register: result
- debug:
- ansible.builtin.debug:
msg: "{{ result }}"
- assert:
- ansible.builtin.assert:
that:
- "result is not changed"
- name: Apply multiple setting via settings with change
settings:
awx.awx.settings:
settings:
AWX_ISOLATION_BASE_PATH: /tmp
AWX_ISOLATION_SHOW_PATHS: []
register: result
- assert:
- ansible.builtin.assert:
that:
- "result is changed"
- name: Handle an omit value
settings:
awx.awx.settings:
name: AWX_ISOLATION_BASE_PATH
value: '{{ junk_var | default(omit) }}'
register: result
ignore_errors: true
- assert:
- ansible.builtin.assert:
that:
- "'Unable to update settings' in result.msg"

View File

@@ -10,7 +10,7 @@
test: ad_hoc_command,host,role
tasks:
- name: DEBUG - make sure variables are what we expect
debug:
ansible.builtin.debug:
msg: |
Running tests at location:
{{ loc_tests }}
@@ -18,7 +18,7 @@
{{ test | trim | split(',') }}
- name: "Include test targets"
include_tasks: "{{ loc_tests }}{{ test_name }}/tasks/main.yml"
ansible.builtin.include_tasks: "{{ loc_tests }}{{ test_name }}/tasks/main.yml"
loop: "{{ test | trim | split(',') }}"
loop_control:
loop_var: test_name

View File

@@ -2,12 +2,10 @@
## Build & Push Image
To build a custom awx image to use with the awx-operator, use the `build_image` role:
To build a custom awx image to use with the awx-operator:
```
$ ansible-playbook tools/ansible/build.yml \
-e awx_image=registry.example.com/ansible/awx \
-e awx_image_tag=test -v
make awx-kube-build
```
> Note: The development image (`make docker-compose-build`) will not work with the awx-operator, the UI is not built in that image, among other things (see Dockerfile.j2 for more info).

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

View File

@@ -646,6 +646,39 @@ Source Control credentials have several attributes that may be configured:
If you are using a GitHub account for a Source Control credential and you have 2FA (Two Factor Authentication) enabled on your account, you will need to use your Personal Access Token in the password field rather than your account password.
.. _ug_credentials_terraform:
Terraform backend configuration
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. index::
pair: credential types; Terraform
pair: backend configuration; Terraform
Terraform is a HashiCorp tool used to automate various infrastructure tasks. Select this credential type to enable synchronization with the Terraform inventory source.
The Terraform credential requires the **Backend configuration** attribute which should contain the data from a `Terraform backend block <https://developer.hashicorp.com/terraform/language/settings/backends/configuration>`_. You can paste, drag a file, browse to upload a file, or click the (|key icon|) button to populate the field from an external :ref:`ug_credential_plugins`. An example configuration for an S3 backend:
.. |key icon| image:: ../common/images/key-mgmt-button.png
:alt: Credentials - create Terraform backend configuration credential form
::
bucket = "my-terraform-state-bucket"
key = "path/to/terraform-state-file"
region = "us-east-1"
access_key = "my-aws-access-key"
secret_key = "my-aws-secret-access-key"
|Credentials - create terraform credential|
.. |Credentials - create terraform credential| image:: ../common/images/credentials-create-terraform-credential.png
:alt: Credentials - create Terraform backend configuration credential form
Saving it stores the file path to the backend configuration in an environment variable ``TF_BACKEND_CONFIG_FILE`` that is made available to any job with the credential attached.
Thycotic DevOps Secrets Vault
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This is considered part of the secret management capability. See :ref:`ug_credentials_thycoticvault` for more detail.

View File

@@ -481,6 +481,7 @@ Inventory updates use dynamically-generated YAML files which are parsed by their
- :ref:`ug_source_openstack`
- :ref:`ug_source_rhv`
- :ref:`ug_source_rhaap`
- :ref:`ug_source_terraform`
Newly created configurations for inventory sources will contain the default plugin configuration values. If you want your newly created inventory sources to match the output of legacy sources, you must apply a specific set of configuration values for that source. To ensure backward compatibility, AWX uses "templates" for each of these sources to force the output of inventory plugins into the legacy format. Refer to :ref:`ir_inv_plugin_templates_reference` section of this guide for each source and their respective templates to help you migrate to the new style inventory plugin output.
@@ -1084,6 +1085,41 @@ Red Hat Ansible Automation Platform
4. Use the **Source Variables** field to override variables used by the ``controller`` inventory plugin. Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.
.. _ug_source_terraform:
Terraform State
~~~~~~~~~~~~~~~~
.. index::
pair: inventories; Terraform
pair: inventory source; Terraform state
This inventory source uses the `terraform_state <https://github.com/ansible-collections/cloud.terraform/blob/main/plugins/inventory/terraform_state.py>`_ inventory plugin from the `cloud.terraform <https://github.com/ansible-collections/cloud.terraform>`_ collection. The plugin will parse a terraform state file and add hosts for AWS EC2, GCE, and Azure instances.
1. To configure this type of sourced inventory, select **Terraform State** from the Source field.
2. The Create new source window expands with the required **Credential** field. Choose from an existing Terraform backend Credential. For more information, refer to :ref:`ug_credentials`.
3. You can optionally specify the verbosity, host filter, enabled variable/value, and update options as described in the main procedure for :ref:`adding a source <ug_add_inv_common_fields>`. For Terraform, enable **Overwrite** and **Update on launch** options.
4. Use the **Source Variables** field to override variables used by the ``controller`` inventory plugin. Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two. For more information on these variables, see the `terraform_state <https://github.com/ansible-collections/cloud.terraform/blob/main/plugins/inventory/terraform_state.py>`_ file for detail.
The ``backend_type`` variable is required by the Terraform state inventory plugin. This should match the remote backend configured in the Terraform backend credential, here is an example for an Amazon S3 backend:
::
---
backend_type: s3
5. Enter an |ee| in the **Execution Environment** field that contains a Terraform binary. This is required for the inventory plugin to run the Terraform commands that read inventory data from the Terraform state file. Refer to the `Terraform EE readme <https://github.com/ansible-cloud/terraform_ee>`_ that contains an example |ee| configuration with a Terraform binary.
.. image:: ../common/images/inventories-create-source-terraform-example.png
6. To add hosts for AWS EC2, GCE, and Azure instances, the Terraform state file in the backend must contain state for resources already deployed to EC2, GCE, or Azure. Refer to each of the Terraform providers' respective documentation to provision instances.
.. _ug_customscripts:
Export old inventory scripts

View File

@@ -149,15 +149,32 @@ This workflow will take the generated images and promote them to quay.io.
![Verify released awx-operator image](img/verify-released-awx-operator-image.png)
## Send notifications
Send notifications to the following groups:
* [Ansible Community forum](https://forum.ansible.com/)
* #social:ansible.com IRC (@newsbot for inclusion in bullhorn)
* #awx:ansible.com (no @newsbot in this room)
* #aap-controller slack channel
* [#social:ansible.com](https://matrix.to/#/#social:ansible.com) `@newsbot` for inclusion in The Bullhorn)
* [#awx:ansible.com](https://forum.ansible.com/g/AWX/members)
* #aap-controller Slack channel
These messages are templated out for you in the output of `get_next_release.yml`.
Note: the slack message is the same as the IRC message.
Note: The Slack message is the same as the Matrix message.
### Announcements
* Provide enough information for the reader
* Include:
* **What:** What is this, why should someone care
* **Why:** Why is this important
* **How:** How do I use this (docs, config options)
* **Call to action:** What type of feedback are we looking for
* Link to PR(s) for larger features
* `@newsbot` supports [Markdown](https://www.markdownguide.org/cheat-sheet/), so use formatted links, bullet points
* Release Manager posts into social Matrix Channel
* Appears in next weeks [Bulhorn](https://forum.ansible.com/c/news/bullhorn)
## Create operator hub PRs.
Operator hub PRs are generated via an Ansible Playbook. See someone on the AWX team for the location of the playbooks and instructions on how to run them.

View File

@@ -1,25 +0,0 @@
---
- name: Build AWX Docker Images
hosts: localhost
gather_facts: true
tasks:
- name: Get version from SCM if not explicitly provided
command: make version-for-buildyml
args:
chdir: '../../'
register: scm_version
failed_when: not scm_version.stdout
when: awx_version is not defined
- name: Set awx_version
set_fact:
awx_version: "{{ scm_version.stdout }}"
when: awx_version is not defined
- include_role:
name: dockerfile
- include_role:
name: image_build
- include_role:
name: image_push
when: push | default(false) | bool

View File

@@ -242,7 +242,8 @@ ADD tools/scripts/awx-python /usr/bin/awx-python
{% if (build_dev|bool) or (kube_dev|bool) %}
RUN echo /awx_devel > /var/lib/awx/venv/awx/lib/python3.11/site-packages/awx.egg-link
ADD tools/docker-compose/awx-manage /usr/local/bin/awx-manage
RUN echo /awx_devel > /var/lib/awx/venv/awx/lib/python3.11/site-packages/awx.pth
RUN ln -sf /awx_devel/tools/docker-compose/awx-manage /usr/local/bin/awx-manage
RUN ln -sf /awx_devel/tools/scripts/awx-python /usr/bin/awx-python
RUN ln -sf /awx_devel/tools/scripts/rsyslog-4xx-recovery /usr/bin/rsyslog-4xx-recovery
{% endif %}

View File

@@ -1,5 +0,0 @@
---
awx_image: ansible/awx
awx_image_tag: "{{ awx_version }}"
dockerfile_name: 'Dockerfile'
headless: no

View File

@@ -1,30 +0,0 @@
---
- name: Verify awx-logos directory exists for official install
stat:
path: "../../../awx-logos"
register: logosdir
failed_when: logosdir.stat.isdir is not defined or not logosdir.stat.isdir
when: awx_official|default(false)|bool
- name: Copy logos for inclusion in sdist
copy:
src: "../../../awx-logos/awx/ui/client/assets/"
dest: "../../awx/ui/public/static/media/"
when: awx_official|default(false)|bool
- set_fact:
command_to_run: |
docker build -t {{ awx_image }}:{{ awx_image_tag }} \
-f {{ dockerfile_name }} \
--build-arg VERSION={{ awx_version }} \
--build-arg SETUPTOOLS_SCM_PRETEND_VERSION={{ awx_version }} \
--build-arg HEADLESS={{ headless }} \
.
# Calling Docker directly because docker-py doesnt support BuildKit
- name: Build AWX image
shell: "{{ command_to_run }}"
environment:
DOCKER_BUILDKIT: 1
args:
chdir: "{{ playbook_dir }}/../../"

View File

@@ -1,4 +0,0 @@
---
registry: quay.io
awx_image: ansible/awx
awx_image_tag: "{{ awx_version }}"

View File

@@ -1,22 +0,0 @@
---
- name: Authenticate with Docker registry if registry password given
docker_login:
registry: "{{ registry }}"
username: "{{ registry_username }}"
password: "{{ registry_password }}"
reauthorize: true
when:
- registry is defined
- registry_username is defined
- registry_password is defined
- name: Tag and Push Container Images
docker_image:
name: "{{ awx_image }}:{{ awx_image_tag }}"
repository: "{{ registry }}/{{ awx_image }}:{{ item }}"
force_tag: yes
push: true
source: local
with_items:
- "latest"
- "{{ awx_image_tag }}"

View File

@@ -1,27 +1,27 @@
---
- name: Include pre-flight checks
include_tasks: preflight.yml
ansible.builtin.include_tasks: preflight.yml
- name: Create _sources directory
file:
ansible.builtin.file:
path: "{{ sources_dest }}"
state: 'directory'
mode: '0700'
- name: debug minikube_setup
debug:
ansible.builtin.debug:
var: minikube_setup
# Linux block
- block:
- name: Download Minikube
get_url:
ansible.builtin.get_url:
url: "{{ minikube_url_linux }}"
dest: "{{ sources_dest }}/minikube"
mode: 0755
- name: Download Kubectl
get_url:
ansible.builtin.get_url:
url: "{{ kubectl_url_linux }}"
dest: "{{ sources_dest }}/kubectl"
mode: 0755
@@ -33,13 +33,13 @@
# MacOS block
- block:
- name: Download Minikube
get_url:
ansible.builtin.get_url:
url: "{{ minikube_url_macos }}"
dest: "{{ sources_dest }}/minikube"
mode: 0755
- name: Download Kubectl
get_url:
ansible.builtin.get_url:
url: "{{ kubectl_url_macos }}"
dest: "{{ sources_dest }}/kubectl"
mode: 0755
@@ -50,18 +50,18 @@
- block:
- name: Starting Minikube
shell: "{{ sources_dest }}/minikube start --driver={{ driver }} --install-addons=true --addons={{ addons | join(',') }}"
ansible.builtin.shell: "{{ sources_dest }}/minikube start --driver={{ driver }} --install-addons=true --addons={{ addons | join(',') }}"
register: minikube_stdout
- name: Enable Ingress Controller on Minikube
shell: "{{ sources_dest }}/minikube addons enable ingress"
ansible.builtin.shell: "{{ sources_dest }}/minikube addons enable ingress"
when:
- minikube_stdout.rc == 0
register: _minikube_ingress
ignore_errors: true
- name: Show Minikube Ingress known-issue 7332 warning
pause:
ansible.builtin.pause:
seconds: 5
prompt: "The Minikube Ingress addon has been disabled since it looks like you are hitting https://github.com/kubernetes/minikube/issues/7332"
when:
@@ -90,13 +90,13 @@
register: _service_account_secret
- name: Load Minikube Bearer Token
set_fact:
ansible.builtin.set_fact:
service_account_token: '{{ _service_account_secret["resources"][0]["data"]["token"] | b64decode }}'
when:
- _service_account_secret["resources"][0]["data"] | length
- name: Render minikube credential JSON template
template:
ansible.builtin.template:
src: bootstrap_minikube.py.j2
dest: "{{ sources_dest }}/bootstrap_minikube.py"
mode: '0600'

View File

@@ -13,12 +13,12 @@
cert_subject: "/C=US/ST=NC/L=Durham/O=awx/CN="
tasks:
- name: Generate certificates for keycloak
command: 'openssl req -new -x509 -days 365 -nodes -out {{ public_key_file }} -keyout {{ private_key_file }} -subj "{{ cert_subject }}"'
ansible.builtin.command: 'openssl req -new -x509 -days 365 -nodes -out {{ public_key_file }} -keyout {{ private_key_file }} -subj "{{ cert_subject }}"'
args:
creates: "{{ public_key_file }}"
- name: Load certs, existing and new SAML settings
set_fact:
ansible.builtin.set_fact:
private_key: "{{ private_key_content }}"
public_key: "{{ public_key_content }}"
public_key_trimmed: "{{ public_key_content | regex_replace('-----BEGIN CERTIFICATE-----\\\\n', '') | regex_replace('\\\\n-----END CERTIFICATE-----', '') }}"
@@ -32,18 +32,18 @@
private_key_content: "{{ lookup('file', private_key_file) | regex_replace('\n', '\\\\n') }}"
- name: Displauy existing SAML configuration
debug:
ansible.builtin.debug:
msg:
- "Here is your existing SAML configuration for reference:"
- "{{ existing_saml }}"
- "Here is your existing OIDC configuration for reference:"
- "{{ existing_oidc }}"
- pause:
- ansible.builtin.pause:
prompt: "Continuing to run this will replace your existing saml and OIDC settings (displayed above). They will all be captured except for your private key. Be sure that is backed up before continuing"
- name: Write out the existing content
copy:
ansible.builtin.copy:
dest: "../_sources/{{ item.filename }}"
content: "{{ item.content }}"
loop:
@@ -65,7 +65,7 @@
validate_certs: False
- name: Get a keycloak token
uri:
ansible.builtin.uri:
url: "https://localhost:8443/auth/realms/master/protocol/openid-connect/token"
method: POST
body_format: form-urlencoded
@@ -78,12 +78,12 @@
register: keycloak_response
- name: Template the AWX realm
template:
ansible.builtin.template:
src: keycloak.awx.realm.json.j2
dest: "{{ keycloak_realm_template }}"
- name: Create the AWX realm
uri:
ansible.builtin.uri:
url: "https://localhost:8443/auth/admin/realms"
method: POST
body_format: json

View File

@@ -7,21 +7,21 @@
awx_host: "https://localhost:8043"
tasks:
- name: Load existing and new LDAP settings
set_fact:
ansible.builtin.set_fact:
existing_ldap: "{{ lookup('awx.awx.controller_api', 'settings/ldap', host=awx_host, verify_ssl=false) }}"
new_ldap: "{{ lookup('template', 'ldap_settings.json.j2') }}"
- name: Display existing LDAP configuration
debug:
ansible.builtin.debug:
msg:
- "Here is your existing LDAP configuration for reference:"
- "{{ existing_ldap }}"
- pause:
- ansible.builtin.pause:
prompt: "Continuing to run this will replace your existing ldap settings (displayed above). They will all be captured. Be sure that is backed up before continuing"
- name: Write out the existing content
copy:
ansible.builtin.copy:
dest: "../_sources/existing_ldap_adapter_settings.json"
content: "{{ existing_ldap }}"

View File

@@ -26,21 +26,21 @@
ansible_connection: httpapi
- name: Load existing and new Logging settings
set_fact:
ansible.builtin.set_fact:
existing_logging: "{{ lookup('awx.awx.controller_api', 'settings/logging', host=awx_host, verify_ssl=false) }}"
new_logging: "{{ lookup('template', 'logging.json.j2') }}"
- name: Display existing Logging configuration
debug:
ansible.builtin.debug:
msg:
- "Here is your existing SAML configuration for reference:"
- "{{ existing_logging }}"
- pause:
prompt: "Continuing to run this will replace your existing logging settings (displayed above). They will all be captured except for your connection password. Be sure that is backed up before continuing"
ansible.builtin.prompt: "Continuing to run this will replace your existing logging settings (displayed above). They will all be captured except for your connection password. Be sure that is backed up before continuing"
- name: Write out the existing content
copy:
ansible.builtin.copy:
dest: "../_sources/existing_logging.json"
content: "{{ existing_logging }}"

View File

@@ -7,21 +7,21 @@
awx_host: "https://localhost:8043"
tasks:
- name: Load existing and new tacacs+ settings
set_fact:
ansible.builtin.set_fact:
existing_tacacs: "{{ lookup('awx.awx.controller_api', 'settings/tacacsplus', host=awx_host, verify_ssl=false) }}"
new_tacacs: "{{ lookup('template', 'tacacsplus_settings.json.j2') }}"
- name: Display existing tacacs+ configuration
debug:
ansible.builtin.debug:
msg:
- "Here is your existing tacacsplus configuration for reference:"
- "{{ existing_tacacs }}"
- pause:
- ansible.builtin.pause:
prompt: "Continuing to run this will replace your existing tacacs settings (displayed above). They will all be captured. Be sure that is backed up before continuing"
- name: Write out the existing content
copy:
ansible.builtin.copy:
dest: "../_sources/existing_tacacsplus_adapter_settings.json"
content: "{{ existing_tacacs }}"

View File

@@ -27,7 +27,7 @@ services:
user: "{{ ansible_user_uid }}"
image: "{{ awx_image }}:{{ awx_image_tag }}"
container_name: tools_awx_{{ container_postfix }}
hostname: awx_{{ container_postfix }}
hostname: awx-{{ container_postfix }}
command: launch_awx.sh
environment:
OS: "{{ os_info.stdout }}"
@@ -363,6 +363,7 @@ volumes:
networks:
awx:
name: awx
service-mesh:
name: service-mesh
{% if minikube_container_group|bool %}

View File

@@ -4,12 +4,10 @@ global:
scrape_interval: {{ scrape_interval }} # Set the scrape interval to something faster. Default is every 1 minute.
scrape_configs:
- job_name: 'awx'
- job_name: 'awx-metrics'
static_configs:
- targets:
# metrics are broadcast to all nodes in the cluster,
# so no need to track nodes individually.
- awx1:8013
- awx-1:8013
metrics_path: /api/v2/metrics
scrape_interval: {{ scrape_interval }}
scheme: http
@@ -18,3 +16,16 @@ scrape_configs:
basic_auth:
username: admin
password: {{ admin_password }}
- job_name: 'awx-wsrelay'
static_configs:
- targets:
{% for i in range(control_plane_node_count|int) %}
{% set container_postfix = loop.index %}
- awx-{{ container_postfix }}:8016
{% endfor %}
metrics_path: /
scrape_interval: {{ scrape_interval }}
scheme: http
params:
format: ['txt']

View File

@@ -1,9 +1,9 @@
---
- node:
id: awx_{{ item }}
id: awx-{{ item }}
firewallrules:
- action: "reject"
tonode: awx_{{ item }}
tonode: awx-{{ item }}
toservice: "control"
- log-level: info
@@ -24,7 +24,7 @@
{% for i in range(item | int + 1, control_plane_node_count | int + 1) %}
- tcp-peer:
address: awx_{{ i }}:2222
address: awx-{{ i }}:2222
redial: true
{% endfor %}

View File

@@ -4,10 +4,10 @@
gather_facts: False
tasks:
- name: Unseal the vault
include_role:
ansible.builtin.include_role:
name: vault
tasks_from: unseal
- name: Display root token
debug:
ansible.builtin.debug:
var: Initial_Root_Token

View File

@@ -1,8 +1,17 @@
#!/usr/bin/awx-python
# EASY-INSTALL-ENTRY-SCRIPT: 'awx','console_scripts','awx-manage'
import sys
from pkg_resources import load_entry_point
__requires__ = 'awx'
from importlib.metadata import distribution
def load_entry_point(dist, group, name):
dist_name, _, _ = dist.partition('==')
matches = (
entry_point
for entry_point in distribution(dist_name).entry_points
if entry_point.group == group and entry_point.name == name
)
return next(matches).load()
if __name__ == '__main__':
sys.exit(

View File

@@ -49,14 +49,14 @@ awx-manage register_queue --queuename=default --instance_percent=100
if [[ -n "$RUN_MIGRATIONS" ]]; then
for (( i=1; i<$CONTROL_PLANE_NODE_COUNT; i++ )); do
for (( j=i + 1; j<=$CONTROL_PLANE_NODE_COUNT; j++ )); do
awx-manage register_peers "awx_$i" --peers "awx_$j"
awx-manage register_peers "awx-$i" --peers "awx-$j"
done
done
if [[ $EXECUTION_NODE_COUNT > 0 ]]; then
awx-manage provision_instance --hostname="receptor-hop" --node_type="hop"
awx-manage add_receptor_address --instance="receptor-hop" --address="receptor-hop" --port=5555 --canonical
awx-manage register_peers "receptor-hop" --peers "awx_1"
awx-manage register_peers "receptor-hop" --peers "awx-1"
for (( e=1; e<=$EXECUTION_NODE_COUNT; e++ )); do
awx-manage provision_instance --hostname="receptor-$e" --node_type="execution"
awx-manage register_peers "receptor-$e" --peers "receptor-hop"

View File

@@ -179,7 +179,7 @@ groups:
datasourceUid: awx_prometheus
model:
editorMode: code
expr: irate(callback_receiver_events_insert_db{node='awx_1'}[1m])
expr: irate(callback_receiver_events_insert_db{node='awx-1'}[1m])
hide: false
intervalMs: 1000
legendFormat: __auto
@@ -228,7 +228,7 @@ groups:
type: prometheus
uid: awx_prometheus
editorMode: code
expr: callback_receiver_events_queue_size_redis{node='awx_1'}
expr: callback_receiver_events_queue_size_redis{node='awx-1'}
hide: false
intervalMs: 1000
legendFormat: __auto