Compare commits

..

30 Commits

Author SHA1 Message Date
David O Neill
5ca017d359 Update PR template
Signed-off-by: David O Neill <daoneill@redhat.com>
2024-03-29 10:09:32 +00: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
Dimitri Savineau
b44bb98c7e Dockerfile: Fix collectstatic command (#15035)
Recent changes in awx and/or django ansible base cause the django
collectstatic command to fail when using an empty settings file.
Instead, use the defaults settings file from controller via
DJANGO_SETTINGS_MODULE=awx.settings.defaults

[linux/amd64 builder 13/13] RUN AWX_SETTINGS_FILE=/dev/null
SKIP_SECRET_KEY_CHECK=yes SKIP_PG_VERSION_CHECK=yes
/var/lib/awx/venv/awx/bin/awx-manage collectstatic --noinput --clear
Traceback (most recent call last):
(...)
django.core.exceptions.ImproperlyConfigured: settings.DATABASES is improperly
configured. Please supply the ENGINE value. Check settings documentation for
more details.

Signed-off-by: Dimitri Savineau <dsavinea@redhat.com>
2024-03-26 14:19:51 -04:00
Hao Liu
8cafdf0400 Fix wsrelay KeyboardInteruption not respected (#15036)
- stop wsrelay on keyboard interuption
- restart wsrelay for any other failure reason
2024-03-26 17:29:15 +00:00
Hao Liu
3f566c8737 Fix wsrelay not retry to establish db connection (#15031)
- run_wsrelay retry to run wsrelay forever with 10 second sleep
- wsrelay restart on`on_ws_heartbeat` task if fail to db connection goes away
2024-03-26 11:56:16 -04:00
Hao Liu
c8021a25bf Fix keycloak doc (#15024) 2024-03-25 15:01:49 -04:00
Matt Martz
934646a0f6 Address first_found skip bug (#15017)
* Address first_found skip bug

* Don't attempt installing project root requirements.yml as v2 collection format
2024-03-22 12:06:43 +01:00
Michael Abashian
9bb97dd658 Fix bug where extra variables were reset on schedule edit
Fix survey prompt presentation inconsistencies

Remove unnecessary conditional

This conditional always returned true.  See the following warning: This condition will always return 'true' since JavaScript compares objects by reference, not value.

Fix schedule edit tests
2024-03-20 10:30:10 -04:00
Hao Liu
7150f5edc6 Editable dependencies in docker compose development environment (#14979)
* Editable dependencies in docker compose development environment
2024-03-19 15:09:15 -04:00
Hao Liu
93da15c0ee Setting modification to address requests from UI_NEXT devs (#14996)
Modification to settings

- Add hidden to indicate to UI_NEXT to hide the field
- Add warning_text to indicate to UI_NEXT to display the warning when specific setting is modified
- Address some non required field being marked as required
2024-03-19 15:08:41 -04:00
Hao Liu
ab593bda45 Add setting for configuring optional URL prefix for /api (#14939)
* Add setting for configuring optional URL prefix for /api

Add OPTIONAL_API_URLPATTERN_PREFIX setting

examples:
- if set to `''` (empty string) API pattern will be `/api`
- if set to 'controller' API pattern will be `/api` AND `/api/controller`
2024-03-19 15:56:33 +00:00
TVo
065bd3ae2a Backported from product-docs PR #2001 (misc doc cleanup) (#14980)
* Backported from product-docs PR #2001 (misc doc cleanup)

* Update docs/docsite/rst/administration/awx-manage.rst
2024-03-15 12:20:02 -06:00
Hao Liu
8ff7260bc6 Add dump_auth_config management cmd (for SAML and LDAP) (#14947)
* Add dump_auth_config management cmd

- Dump SAML config from AWX to DAB authenticator config in json format

* Add dumping of LDAP settings

* add test for command

* Fix is_enabled

* fix command name typo

Co-authored-by: Hao Liu <44379968+TheRealHaoLiu@users.noreply.github.com>

* add fields to config, add name to data

* break out IDP values

* change test fields and value comparison

* edit help text, reformat settings

---------

Co-authored-by: jessicamack <jmack@redhat.com>
2024-03-15 13:47:30 -04:00
Hao Liu
a635445082 Fix failing bulk launch job due to create partition race
https://github.com/ansible/awx/pull/14910/files

introduced a bug where we no longer accept the right exceptions

when 2 job launch at the sametime and try to create jobevent table partition 1 of the job will fail
2024-03-15 10:10:38 -04:00
Hao Liu
949e7efab1 Fix wsrelay hanging after db outage
TCP keepalive settings was moved out from settings.DATABASE to settings.LISTENER_DATABASES and it's not longer being respected by wsrelay
2024-03-14 15:51:30 -04:00
Hao Liu
615f09226f Fix awx-manage run_wsrelay --status (#14997)
by don't start the metrics server if --status is passed in
2024-03-14 18:55:05 +00:00
Dave
d903c524f5 Fix for 14924 - Unformatted help text toast message (#14990)
Fix for 14924  - Unformatted help text is popped out when peers for intances are changed

Co-authored-by: David O Neill <daoneill@redhat.com>
2024-03-14 13:24:53 -04:00
Cesar Francisco San Nicolas Martinez
393d9c39c6 Mismatch dependencies version (#14986)
* Fixed mismatch between setuptools version in the makefile and requirements file

* Fix mismatch of versions in makefile and requirements

* Added maturin license
2024-03-14 13:32:56 +01:00
Hao Liu
dfab342bb4 Skip replicas test for awx-operator (#14987)
speed up CI, also AWX code change won't effect that test
2024-03-13 18:01:02 +00:00
Dave
12843eccf7 AAP-13369 Python 3.9 -> 3.11 upgrade (#14771)
* Python 3.9 -> 3.11 upgrade

* Test: updating azure-keyvault to 4.2.0

* Revert "Test: updating azure-keyvault to 4.2.0"

This reverts commit cf0b83699442e0c0de4a1152d4af8543a5e05b88.

* Test: updating azure-keyvault to latest and adding azure-identity

* Fix licenses

* Adding new licenses

* Revert "Fix licenses"

This reverts commit da3876911ef5ebbe7a8adbddd336ced3039b6228.

* Fixing dependencies

* Test: updating azure-keyvault to 4.2.0

* Fix licenses

* Revert "Fix licenses"

This reverts commit da3876911ef5ebbe7a8adbddd336ced3039b6228.

* Fixing dependencies

---------

Co-authored-by: César Francisco San Nicolás Martínez <csannico@redhat.com>
2024-03-13 14:41:40 +01:00
96 changed files with 1578 additions and 468 deletions

View File

@@ -7,6 +7,14 @@ commit message and your description; but you should still explain what
the change does.
-->
##### Depends on
<!---
Please provide links to any other PR dependanices.
Indicating these should be merged first prior to this PR.
-->
- #12345
- https://github.com/xxx/yyy/pulls/1234
##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->
- Breaking Change

View File

@@ -107,7 +107,7 @@ jobs:
ansible-galaxy collection install -r molecule/requirements.yml
sudo rm -f $(which kustomize)
make kustomize
KUSTOMIZE_PATH=$(readlink -f bin/kustomize) molecule -v test -s kind
KUSTOMIZE_PATH=$(readlink -f bin/kustomize) molecule -v test -s kind -- --skip-tags=replicas
env:
AWX_TEST_IMAGE: awx
AWX_TEST_VERSION: ci

5
.gitignore vendored
View File

@@ -46,6 +46,11 @@ tools/docker-compose/overrides/
tools/docker-compose-minikube/_sources
tools/docker-compose/keycloak.awx.realm.json
!tools/docker-compose/editable_dependencies
tools/docker-compose/editable_dependencies/*
!tools/docker-compose/editable_dependencies/README.md
!tools/docker-compose/editable_dependencies/install.sh
# Tower setup playbook testing
setup/test/roles/postgresql
**/provision_docker

View File

@@ -1,6 +1,6 @@
-include awx/ui_next/Makefile
PYTHON := $(notdir $(shell for i in python3.9 python3; do command -v $$i; done|sed 1q))
PYTHON := $(notdir $(shell for i in python3.11 python3; do command -v $$i; done|sed 1q))
SHELL := bash
DOCKER_COMPOSE ?= docker-compose
OFFICIAL ?= no
@@ -47,6 +47,8 @@ VAULT ?= false
VAULT_TLS ?= false
# If set to true docker-compose will also start a tacacs+ instance
TACACS ?= false
# If set to true docker-compose will install editable dependencies
EDITABLE_DEPENDENCIES ?= false
VENV_BASE ?= /var/lib/awx/venv
@@ -63,7 +65,7 @@ RECEPTOR_IMAGE ?= quay.io/ansible/receptor:devel
SRC_ONLY_PKGS ?= cffi,pycparser,psycopg,twilio
# These should be upgraded in the AWX and Ansible venv before attempting
# to install the actual requirements
VENV_BOOTSTRAP ?= pip==21.2.4 setuptools==65.6.3 setuptools_scm[toml]==8.0.4 wheel==0.38.4
VENV_BOOTSTRAP ?= pip==21.2.4 setuptools==69.0.2 setuptools_scm[toml]==8.0.4 wheel==0.42.0
NAME ?= awx
@@ -533,6 +535,7 @@ docker-compose-sources: .git/hooks/pre-commit
-e enable_vault=$(VAULT) \
-e vault_tls=$(VAULT_TLS) \
-e enable_tacacs=$(TACACS) \
-e install_editable_dependencies=$(EDITABLE_DEPENDENCIES) \
$(EXTRA_SOURCES_ANSIBLE_OPTS)
docker-compose: awx/projects docker-compose-sources
@@ -540,9 +543,15 @@ docker-compose: awx/projects docker-compose-sources
ansible-playbook -i tools/docker-compose/inventory tools/docker-compose/ansible/initialize_containers.yml \
-e enable_vault=$(VAULT) \
-e vault_tls=$(VAULT_TLS) \
-e enable_ldap=$(LDAP);
-e enable_ldap=$(LDAP); \
$(MAKE) docker-compose-up
docker-compose-up:
$(DOCKER_COMPOSE) -f tools/docker-compose/_sources/docker-compose.yml $(COMPOSE_OPTS) up $(COMPOSE_UP_OPTS) --remove-orphans
docker-compose-down:
$(DOCKER_COMPOSE) -f tools/docker-compose/_sources/docker-compose.yml $(COMPOSE_OPTS) down --remove-orphans
docker-compose-credential-plugins: awx/projects docker-compose-sources
echo -e "\033[0;31mTo generate a CyberArk Conjur API key: docker exec -it tools_conjur_1 conjurctl account create quick-start\033[0m"
$(DOCKER_COMPOSE) -f tools/docker-compose/_sources/docker-compose.yml -f tools/docker-credential-plugins-override.yml up --no-recreate awx_1 --remove-orphans
@@ -607,7 +616,7 @@ docker-clean:
-$(foreach image_id,$(shell docker images --filter=reference='*/*/*awx_devel*' --filter=reference='*/*awx_devel*' --filter=reference='*awx_devel*' -aq),docker rmi --force $(image_id);)
docker-clean-volumes: docker-compose-clean docker-compose-container-group-clean
docker volume rm -f tools_awx_db tools_vault_1 tools_ldap_1 tools_grafana_storage tools_prometheus_storage $(docker volume ls --filter name=tools_redis_socket_ -q)
docker volume rm -f tools_var_lib_awx tools_awx_db tools_vault_1 tools_ldap_1 tools_grafana_storage tools_prometheus_storage $(docker volume ls --filter name=tools_redis_socket_ -q)
docker-refresh: docker-clean docker-compose

View File

@@ -93,6 +93,7 @@ register(
default='',
label=_('Login redirect override URL'),
help_text=_('URL to which unauthorized users will be redirected to log in. If blank, users will be sent to the login page.'),
warning_text=_('Changing the redirect URL could impact the ability to login if local authentication is also disabled.'),
category=_('Authentication'),
category_slug='authentication',
)

View File

@@ -36,11 +36,13 @@ class Metadata(metadata.SimpleMetadata):
field_info = OrderedDict()
field_info['type'] = self.label_lookup[field]
field_info['required'] = getattr(field, 'required', False)
field_info['hidden'] = getattr(field, 'hidden', False)
text_attrs = [
'read_only',
'label',
'help_text',
'warning_text',
'min_length',
'max_length',
'min_value',

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

@@ -24,6 +24,10 @@ def drf_reverse(viewname, args=None, kwargs=None, request=None, format=None, **e
else:
url = _reverse(viewname, args, kwargs, request, format, **extra)
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 url

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
@@ -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

@@ -55,6 +55,7 @@ register(
# Optional; category_slug will be slugified version of category if not
# explicitly provided.
category_slug='cows',
hidden=True,
)

View File

@@ -127,6 +127,8 @@ class SettingsRegistry(object):
encrypted = bool(field_kwargs.pop('encrypted', False))
defined_in_file = bool(field_kwargs.pop('defined_in_file', False))
unit = field_kwargs.pop('unit', None)
hidden = field_kwargs.pop('hidden', False)
warning_text = field_kwargs.pop('warning_text', None)
if getattr(field_kwargs.get('child', None), 'source', None) is not None:
field_kwargs['child'].source = None
field_instance = field_class(**field_kwargs)
@@ -134,12 +136,14 @@ class SettingsRegistry(object):
field_instance.category = category
field_instance.depends_on = depends_on
field_instance.unit = unit
field_instance.hidden = hidden
if placeholder is not empty:
field_instance.placeholder = placeholder
field_instance.defined_in_file = defined_in_file
if field_instance.defined_in_file:
field_instance.help_text = str(_('This value has been set manually in a settings file.')) + '\n\n' + str(field_instance.help_text)
field_instance.encrypted = encrypted
field_instance.warning_text = warning_text
original_field_instance = field_instance
if field_class != original_field_class:
original_field_instance = original_field_class(**field_kwargs)

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

@@ -92,6 +92,7 @@ register(
),
category=_('System'),
category_slug='system',
required=False,
)
register(
@@ -774,6 +775,7 @@ register(
allow_null=True,
category=_('System'),
category_slug='system',
required=False,
)
register(
'AUTOMATION_ANALYTICS_LAST_ENTRIES',
@@ -815,6 +817,7 @@ register(
help_text=_('Max jobs to allow bulk jobs to launch'),
category=_('Bulk Actions'),
category_slug='bulk',
hidden=True,
)
register(
@@ -825,6 +828,7 @@ register(
help_text=_('Max number of hosts to allow to be created in a single bulk action'),
category=_('Bulk Actions'),
category_slug='bulk',
hidden=True,
)
register(
@@ -835,6 +839,7 @@ register(
help_text=_('Max number of hosts to allow to be deleted in a single bulk action'),
category=_('Bulk Actions'),
category_slug='bulk',
hidden=True,
)
register(
@@ -845,6 +850,7 @@ register(
help_text=_('Enable preview of new user interface.'),
category=_('System'),
category_slug='system',
hidden=True,
)
register(

View File

@@ -1,9 +1,10 @@
from azure.keyvault.secrets import SecretClient
from azure.identity import ClientSecretCredential
from msrestazure import azure_cloud
from .plugin import CredentialPlugin
from django.utils.translation import gettext_lazy as _
from azure.keyvault import KeyVaultClient, KeyVaultAuthentication
from azure.common.credentials import ServicePrincipalCredentials
from msrestazure import azure_cloud
# https://github.com/Azure/msrestazure-for-python/blob/master/msrestazure/azure_cloud.py
@@ -54,22 +55,9 @@ azure_keyvault_inputs = {
def azure_keyvault_backend(**kwargs):
url = kwargs['url']
[cloud] = [c for c in clouds if c.name == kwargs.get('cloud_name', default_cloud.name)]
def auth_callback(server, resource, scope):
credentials = ServicePrincipalCredentials(
url=url,
client_id=kwargs['client'],
secret=kwargs['secret'],
tenant=kwargs['tenant'],
resource=f"https://{cloud.suffixes.keyvault_dns.split('.', 1).pop()}",
)
token = credentials.token
return token['token_type'], token['access_token']
kv = KeyVaultClient(KeyVaultAuthentication(auth_callback))
return kv.get_secret(url, kwargs['secret_field'], kwargs.get('secret_version', '')).value
csc = ClientSecretCredential(tenant_id=kwargs['tenant'], client_id=kwargs['client'], client_secret=kwargs['secret'])
kv = SecretClient(credential=csc, vault_url=kwargs['url'])
return kv.get_secret(name=kwargs['secret_field'], version=kwargs.get('secret_version', '')).value
azure_keyvault_plugin = CredentialPlugin('Microsoft Azure Key Vault', inputs=azure_keyvault_inputs, backend=azure_keyvault_backend)

View File

@@ -0,0 +1,179 @@
import json
import os
import sys
import re
from typing import Any
from django.core.management.base import BaseCommand
from django.conf import settings
from awx.conf import settings_registry
class Command(BaseCommand):
help = 'Dump the current auth configuration in django_ansible_base.authenticator format, currently supports LDAP and SAML'
DAB_SAML_AUTHENTICATOR_KEYS = {
"SP_ENTITY_ID": True,
"SP_PUBLIC_CERT": True,
"SP_PRIVATE_KEY": True,
"ORG_INFO": True,
"TECHNICAL_CONTACT": True,
"SUPPORT_CONTACT": True,
"SP_EXTRA": False,
"SECURITY_CONFIG": False,
"EXTRA_DATA": False,
"ENABLED_IDPS": True,
"CALLBACK_URL": False,
}
DAB_LDAP_AUTHENTICATOR_KEYS = {
"SERVER_URI": True,
"BIND_DN": False,
"BIND_PASSWORD": False,
"CONNECTION_OPTIONS": False,
"GROUP_TYPE": True,
"GROUP_TYPE_PARAMS": True,
"GROUP_SEARCH": False,
"START_TLS": False,
"USER_DN_TEMPLATE": True,
"USER_ATTR_MAP": True,
"USER_SEARCH": False,
}
def get_awx_ldap_settings(self) -> dict[str, dict[str, Any]]:
awx_ldap_settings = {}
for awx_ldap_setting in settings_registry.get_registered_settings(category_slug='ldap'):
key = awx_ldap_setting.removeprefix("AUTH_LDAP_")
value = getattr(settings, awx_ldap_setting, None)
awx_ldap_settings[key] = value
grouped_settings = {}
for key, value in awx_ldap_settings.items():
match = re.search(r'(\d+)', key)
index = int(match.group()) if match else 0
new_key = re.sub(r'\d+_', '', key)
if index not in grouped_settings:
grouped_settings[index] = {}
grouped_settings[index][new_key] = value
if new_key == "GROUP_TYPE" and value:
grouped_settings[index][new_key] = type(value).__name__
if new_key == "SERVER_URI" and value:
value = value.split(", ")
return grouped_settings
def is_enabled(self, settings, keys):
for key, required in keys.items():
if required and not settings.get(key):
return False
return True
def get_awx_saml_settings(self) -> dict[str, Any]:
awx_saml_settings = {}
for awx_saml_setting in settings_registry.get_registered_settings(category_slug='saml'):
awx_saml_settings[awx_saml_setting.removeprefix("SOCIAL_AUTH_SAML_")] = getattr(settings, awx_saml_setting, None)
return awx_saml_settings
def format_config_data(self, enabled, awx_settings, type, keys, name):
config = {
"type": f"awx.authentication.authenticator_plugins.{type}",
"name": name,
"enabled": enabled,
"create_objects": True,
"users_unique": False,
"remove_users": True,
"configuration": {},
}
for k in keys:
v = awx_settings.get(k)
config["configuration"].update({k: v})
if type == "saml":
idp_to_key_mapping = {
"url": "IDP_URL",
"x509cert": "IDP_X509_CERT",
"entity_id": "IDP_ENTITY_ID",
"attr_email": "IDP_ATTR_EMAIL",
"attr_groups": "IDP_GROUPS",
"attr_username": "IDP_ATTR_USERNAME",
"attr_last_name": "IDP_ATTR_LAST_NAME",
"attr_first_name": "IDP_ATTR_FIRST_NAME",
"attr_user_permanent_id": "IDP_ATTR_USER_PERMANENT_ID",
}
for idp_name in awx_settings.get("ENABLED_IDPS", {}):
for key in idp_to_key_mapping:
value = awx_settings["ENABLED_IDPS"][idp_name].get(key)
if value is not None:
config["name"] = idp_name
config["configuration"].update({idp_to_key_mapping[key]: value})
return config
def add_arguments(self, parser):
parser.add_argument(
"output_file",
nargs="?",
type=str,
default=None,
help="Output JSON file path",
)
def handle(self, *args, **options):
try:
data = []
# dump SAML settings
awx_saml_settings = self.get_awx_saml_settings()
awx_saml_enabled = self.is_enabled(awx_saml_settings, self.DAB_SAML_AUTHENTICATOR_KEYS)
if awx_saml_enabled:
awx_saml_name = awx_saml_settings["ENABLED_IDPS"]
data.append(
self.format_config_data(
awx_saml_enabled,
awx_saml_settings,
"saml",
self.DAB_SAML_AUTHENTICATOR_KEYS,
awx_saml_name,
)
)
# dump LDAP settings
awx_ldap_group_settings = self.get_awx_ldap_settings()
for awx_ldap_name, awx_ldap_settings in enumerate(awx_ldap_group_settings.values()):
enabled = self.is_enabled(awx_ldap_settings, self.DAB_LDAP_AUTHENTICATOR_KEYS)
if enabled:
data.append(
self.format_config_data(
enabled,
awx_ldap_settings,
"ldap",
self.DAB_LDAP_AUTHENTICATOR_KEYS,
str(awx_ldap_name),
)
)
# write to file if requested
if options["output_file"]:
# Define the path for the output JSON file
output_file = options["output_file"]
# Ensure the directory exists
os.makedirs(os.path.dirname(output_file), exist_ok=True)
# Write data to the JSON file
with open(output_file, "w") as f:
json.dump(data, f, indent=4)
self.stdout.write(self.style.SUCCESS(f"Auth config data dumped to {output_file}"))
else:
self.stdout.write(json.dumps(data, indent=4))
except Exception as e:
self.stdout.write(self.style.ERROR(f"An error occurred: {str(e)}"))
sys.exit(1)

View File

@@ -92,8 +92,6 @@ class Command(BaseCommand):
return host_stats
def handle(self, *arg, **options):
WebsocketsMetricsServer().start()
# it's necessary to delay this import in case
# database migrations are still running
from awx.main.models.ha import Instance
@@ -166,8 +164,15 @@ class Command(BaseCommand):
return
try:
websocket_relay_manager = WebSocketRelayManager()
asyncio.run(websocket_relay_manager.run())
except KeyboardInterrupt:
logger.info('Terminating Websocket Relayer')
WebsocketsMetricsServer().start()
websocket_relay_manager = WebSocketRelayManager()
while True:
try:
asyncio.run(websocket_relay_manager.run())
except KeyboardInterrupt:
logger.info('Shutting down Websocket Relayer')
break
except Exception as e:
logger.exception('Error in Websocket Relayer, exception: {}. Restarting in 10 seconds'.format(e))
time.sleep(10)

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

@@ -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

@@ -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

@@ -0,0 +1,122 @@
from io import StringIO
import json
from django.core.management import call_command
from django.test import TestCase, override_settings
settings_dict = {
"SOCIAL_AUTH_SAML_SP_ENTITY_ID": "SP_ENTITY_ID",
"SOCIAL_AUTH_SAML_SP_PUBLIC_CERT": "SP_PUBLIC_CERT",
"SOCIAL_AUTH_SAML_SP_PRIVATE_KEY": "SP_PRIVATE_KEY",
"SOCIAL_AUTH_SAML_ORG_INFO": "ORG_INFO",
"SOCIAL_AUTH_SAML_TECHNICAL_CONTACT": "TECHNICAL_CONTACT",
"SOCIAL_AUTH_SAML_SUPPORT_CONTACT": "SUPPORT_CONTACT",
"SOCIAL_AUTH_SAML_SP_EXTRA": "SP_EXTRA",
"SOCIAL_AUTH_SAML_SECURITY_CONFIG": "SECURITY_CONFIG",
"SOCIAL_AUTH_SAML_EXTRA_DATA": "EXTRA_DATA",
"SOCIAL_AUTH_SAML_ENABLED_IDPS": {
"Keycloak": {
"attr_last_name": "last_name",
"attr_groups": "groups",
"attr_email": "email",
"attr_user_permanent_id": "name_id",
"attr_username": "username",
"entity_id": "https://example.com/auth/realms/awx",
"url": "https://example.com/auth/realms/awx/protocol/saml",
"x509cert": "-----BEGIN CERTIFICATE-----\nMIIDDjCCAfYCCQCPBeVvpo8+VzANBgkqhkiG9w0BAQsFADBJMQswCQYDVQQGEwJV\nUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBkR1cmhhbTEMMAoGA1UECgwDYXd4MQ4w\nDAYDVQQDDAVsb2NhbDAeFw0yNDAxMTgxNDA4MzFaFw0yNTAxMTcxNDA4MzFaMEkx\nCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGRHVyaGFtMQwwCgYD\nVQQKDANhd3gxDjAMBgNVBAMMBWxvY2FsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\nMIIBCgKCAQEAzouj93oyFXsHEABdPESh3CYpp5QJJBM4TLYIIolk6PFOFIVwBuFY\nfExi5w7Hh4A42lPM6RkrT+u3h7LV39H9MRUfqygOSmaxICTOI0sU9ROHc44fWWzN\n756OP4B5zSiqG82q8X7nYVkcID+2F/3ekPLMOlWn53OrcdfKKDIcqavoTkQJefc2\nggXU3WgVCxGki/qCm+e5cZ1Cpl/ykSLOT8dWMEzDd12kin66zJ3KYz9F2Q5kQTh4\nKRAChnBBoEqzOfENHEAaHALiXOlVSy61VcLbtvskRMMwBtsydlnd9n/HGnktgrid\n3Ca0z5wBTHWjAOBvCKxKJuDa+jmyHEnpcQIDAQABMA0GCSqGSIb3DQEBCwUAA4IB\nAQBXvmyPWgXhC26cHYJBgQqj57dZ+n7p00kM1J+27oDMjGmbmX+XIKXLWazw/rG3\ngDjw9MXI2tVCrQMX0ohjphaULXhb/VBUPDOiW+k7C6AB3nZySFRflcR3cM4f83zF\nMoBd0549h5Red4p72FeOKNJRTN8YO4ooH9YNh5g0FQkgqn7fV9w2CNlomeKIW9zP\nm8tjFw0cJUk2wEYBVl8O7ko5rgNlzhkLoZkMvJhKa99AQJA6MAdyoLl1lv56Kq4X\njk+mMEiz9SaInp+ILQ1uQxZEwuC7DoGRW76rV4Fnie6+DLft4WKZfX1497mx8NV3\noR0abutJaKnCj07dwRu4/EsK\n-----END CERTIFICATE-----",
"attr_first_name": "first_name",
}
},
"SOCIAL_AUTH_SAML_CALLBACK_URL": "CALLBACK_URL",
"AUTH_LDAP_1_SERVER_URI": "SERVER_URI",
"AUTH_LDAP_1_BIND_DN": "BIND_DN",
"AUTH_LDAP_1_BIND_PASSWORD": "BIND_PASSWORD",
"AUTH_LDAP_1_GROUP_SEARCH": ["GROUP_SEARCH"],
"AUTH_LDAP_1_GROUP_TYPE": "string object",
"AUTH_LDAP_1_GROUP_TYPE_PARAMS": {"member_attr": "member", "name_attr": "cn"},
"AUTH_LDAP_1_USER_DN_TEMPLATE": "USER_DN_TEMPLATE",
"AUTH_LDAP_1_USER_SEARCH": ["USER_SEARCH"],
"AUTH_LDAP_1_USER_ATTR_MAP": {
"email": "email",
"last_name": "last_name",
"first_name": "first_name",
},
"AUTH_LDAP_1_CONNECTION_OPTIONS": {},
"AUTH_LDAP_1_START_TLS": None,
}
@override_settings(**settings_dict)
class TestDumpAuthConfigCommand(TestCase):
def setUp(self):
super().setUp()
self.expected_config = [
{
"type": "awx.authentication.authenticator_plugins.saml",
"name": "Keycloak",
"enabled": True,
"create_objects": True,
"users_unique": False,
"remove_users": True,
"configuration": {
"SP_ENTITY_ID": "SP_ENTITY_ID",
"SP_PUBLIC_CERT": "SP_PUBLIC_CERT",
"SP_PRIVATE_KEY": "SP_PRIVATE_KEY",
"ORG_INFO": "ORG_INFO",
"TECHNICAL_CONTACT": "TECHNICAL_CONTACT",
"SUPPORT_CONTACT": "SUPPORT_CONTACT",
"SP_EXTRA": "SP_EXTRA",
"SECURITY_CONFIG": "SECURITY_CONFIG",
"EXTRA_DATA": "EXTRA_DATA",
"ENABLED_IDPS": {
"Keycloak": {
"attr_last_name": "last_name",
"attr_groups": "groups",
"attr_email": "email",
"attr_user_permanent_id": "name_id",
"attr_username": "username",
"entity_id": "https://example.com/auth/realms/awx",
"url": "https://example.com/auth/realms/awx/protocol/saml",
"x509cert": "-----BEGIN CERTIFICATE-----\nMIIDDjCCAfYCCQCPBeVvpo8+VzANBgkqhkiG9w0BAQsFADBJMQswCQYDVQQGEwJV\nUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBkR1cmhhbTEMMAoGA1UECgwDYXd4MQ4w\nDAYDVQQDDAVsb2NhbDAeFw0yNDAxMTgxNDA4MzFaFw0yNTAxMTcxNDA4MzFaMEkx\nCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGRHVyaGFtMQwwCgYD\nVQQKDANhd3gxDjAMBgNVBAMMBWxvY2FsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\nMIIBCgKCAQEAzouj93oyFXsHEABdPESh3CYpp5QJJBM4TLYIIolk6PFOFIVwBuFY\nfExi5w7Hh4A42lPM6RkrT+u3h7LV39H9MRUfqygOSmaxICTOI0sU9ROHc44fWWzN\n756OP4B5zSiqG82q8X7nYVkcID+2F/3ekPLMOlWn53OrcdfKKDIcqavoTkQJefc2\nggXU3WgVCxGki/qCm+e5cZ1Cpl/ykSLOT8dWMEzDd12kin66zJ3KYz9F2Q5kQTh4\nKRAChnBBoEqzOfENHEAaHALiXOlVSy61VcLbtvskRMMwBtsydlnd9n/HGnktgrid\n3Ca0z5wBTHWjAOBvCKxKJuDa+jmyHEnpcQIDAQABMA0GCSqGSIb3DQEBCwUAA4IB\nAQBXvmyPWgXhC26cHYJBgQqj57dZ+n7p00kM1J+27oDMjGmbmX+XIKXLWazw/rG3\ngDjw9MXI2tVCrQMX0ohjphaULXhb/VBUPDOiW+k7C6AB3nZySFRflcR3cM4f83zF\nMoBd0549h5Red4p72FeOKNJRTN8YO4ooH9YNh5g0FQkgqn7fV9w2CNlomeKIW9zP\nm8tjFw0cJUk2wEYBVl8O7ko5rgNlzhkLoZkMvJhKa99AQJA6MAdyoLl1lv56Kq4X\njk+mMEiz9SaInp+ILQ1uQxZEwuC7DoGRW76rV4Fnie6+DLft4WKZfX1497mx8NV3\noR0abutJaKnCj07dwRu4/EsK\n-----END CERTIFICATE-----",
"attr_first_name": "first_name",
}
},
"CALLBACK_URL": "CALLBACK_URL",
"IDP_URL": "https://example.com/auth/realms/awx/protocol/saml",
"IDP_X509_CERT": "-----BEGIN CERTIFICATE-----\nMIIDDjCCAfYCCQCPBeVvpo8+VzANBgkqhkiG9w0BAQsFADBJMQswCQYDVQQGEwJV\nUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBkR1cmhhbTEMMAoGA1UECgwDYXd4MQ4w\nDAYDVQQDDAVsb2NhbDAeFw0yNDAxMTgxNDA4MzFaFw0yNTAxMTcxNDA4MzFaMEkx\nCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGRHVyaGFtMQwwCgYD\nVQQKDANhd3gxDjAMBgNVBAMMBWxvY2FsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\nMIIBCgKCAQEAzouj93oyFXsHEABdPESh3CYpp5QJJBM4TLYIIolk6PFOFIVwBuFY\nfExi5w7Hh4A42lPM6RkrT+u3h7LV39H9MRUfqygOSmaxICTOI0sU9ROHc44fWWzN\n756OP4B5zSiqG82q8X7nYVkcID+2F/3ekPLMOlWn53OrcdfKKDIcqavoTkQJefc2\nggXU3WgVCxGki/qCm+e5cZ1Cpl/ykSLOT8dWMEzDd12kin66zJ3KYz9F2Q5kQTh4\nKRAChnBBoEqzOfENHEAaHALiXOlVSy61VcLbtvskRMMwBtsydlnd9n/HGnktgrid\n3Ca0z5wBTHWjAOBvCKxKJuDa+jmyHEnpcQIDAQABMA0GCSqGSIb3DQEBCwUAA4IB\nAQBXvmyPWgXhC26cHYJBgQqj57dZ+n7p00kM1J+27oDMjGmbmX+XIKXLWazw/rG3\ngDjw9MXI2tVCrQMX0ohjphaULXhb/VBUPDOiW+k7C6AB3nZySFRflcR3cM4f83zF\nMoBd0549h5Red4p72FeOKNJRTN8YO4ooH9YNh5g0FQkgqn7fV9w2CNlomeKIW9zP\nm8tjFw0cJUk2wEYBVl8O7ko5rgNlzhkLoZkMvJhKa99AQJA6MAdyoLl1lv56Kq4X\njk+mMEiz9SaInp+ILQ1uQxZEwuC7DoGRW76rV4Fnie6+DLft4WKZfX1497mx8NV3\noR0abutJaKnCj07dwRu4/EsK\n-----END CERTIFICATE-----",
"IDP_ENTITY_ID": "https://example.com/auth/realms/awx",
"IDP_ATTR_EMAIL": "email",
"IDP_GROUPS": "groups",
"IDP_ATTR_USERNAME": "username",
"IDP_ATTR_LAST_NAME": "last_name",
"IDP_ATTR_FIRST_NAME": "first_name",
"IDP_ATTR_USER_PERMANENT_ID": "name_id",
},
},
{
"type": "awx.authentication.authenticator_plugins.ldap",
"name": "1",
"enabled": True,
"create_objects": True,
"users_unique": False,
"remove_users": True,
"configuration": {
"SERVER_URI": "SERVER_URI",
"BIND_DN": "BIND_DN",
"BIND_PASSWORD": "BIND_PASSWORD",
"CONNECTION_OPTIONS": {},
"GROUP_TYPE": "str",
"GROUP_TYPE_PARAMS": {"member_attr": "member", "name_attr": "cn"},
"GROUP_SEARCH": ["GROUP_SEARCH"],
"START_TLS": None,
"USER_DN_TEMPLATE": "USER_DN_TEMPLATE",
"USER_ATTR_MAP": {"email": "email", "last_name": "last_name", "first_name": "first_name"},
"USER_SEARCH": ["USER_SEARCH"],
},
},
]
def test_json_returned_from_cmd(self):
output = StringIO()
call_command("dump_auth_config", stdout=output)
assert json.loads(output.getvalue()) == self.expected_config

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

@@ -1160,14 +1160,13 @@ def create_partition(tblname, start=None):
except (ProgrammingError, IntegrityError) as e:
cause = e.__cause__
if cause and hasattr(cause, 'sqlstate'):
# 42P07 = DuplicateTable
sqlstate = cause.sqlstate
sqlstate_str = psycopg.errors.lookup(sqlstate)
sqlstate_cls = psycopg.errors.lookup(sqlstate)
if psycopg.errors.DuplicateTable == sqlstate:
if psycopg.errors.DuplicateTable == sqlstate_cls or psycopg.errors.UniqueViolation == sqlstate_cls:
logger.info(f'Caught known error due to partition creation race: {e}')
else:
logger.error('SQL Error state: {} - {}'.format(sqlstate, sqlstate_str))
logger.error('SQL Error state: {} - {}'.format(sqlstate, sqlstate_cls))
raise
except DatabaseError as e:
cause = e.__cause__

View File

@@ -302,20 +302,36 @@ 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']
async_conn = await psycopg.AsyncConnection.connect(
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)
event_loop.create_task(self.on_ws_heartbeat(async_conn))
database_conf = settings.DATABASES['default'].copy()
database_conf['OPTIONS'] = database_conf.get('OPTIONS', {}).copy()
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
task = None
# Establishes a websocket connection to /websocket/relay on all API servers
while True:
if not task or task.done():
try:
async_conn = await psycopg.AsyncConnection.connect(
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.")
except Exception as e:
logger.warning(f"Failed to connect to database for pg_notify: {e}")
future_remote_hosts = self.known_hosts.keys()
current_remote_hosts = self.relay_connections.keys()
deleted_remote_hosts = set(current_remote_hosts) - set(future_remote_hosts)

View File

@@ -221,8 +221,10 @@
vars:
req_file: "{{ lookup('ansible.builtin.first_found', req_candidates, skip=True) }}"
req_candidates:
- "{{ project_path | quote }}/roles/requirements.yml"
- "{{ project_path | quote }}/roles/requirements.yaml"
files:
- "{{ project_path | quote }}/roles/requirements.yml"
- "{{ project_path | quote }}/roles/requirements.yaml"
skip: True
changed_when: "'was installed successfully' in galaxy_result.stdout"
when:
- roles_enabled | bool
@@ -237,10 +239,10 @@
vars:
req_file: "{{ lookup('ansible.builtin.first_found', req_candidates, skip=True) }}"
req_candidates:
- "{{ project_path | quote }}/collections/requirements.yml"
- "{{ project_path | quote }}/collections/requirements.yaml"
- "{{ project_path | quote }}/requirements.yml"
- "{{ project_path | quote }}/requirements.yaml"
files:
- "{{ project_path | quote }}/collections/requirements.yml"
- "{{ project_path | quote }}/collections/requirements.yaml"
skip: True
changed_when: "'Nothing to do.' not in galaxy_collection_result.stdout"
when:
- "ansible_version.full is version_compare('2.9', '>=')"
@@ -249,6 +251,7 @@
tags:
- install_collections
# requirements.yml in project root can be either "old" (roles only) or "new" (collections+roles) format
- name: Fetch galaxy roles and collections from requirements.(yml/yaml)
ansible.builtin.command:
cmd: "ansible-galaxy install -r {{ req_file }} {{ verbosity }}"
@@ -256,8 +259,10 @@
vars:
req_file: "{{ lookup('ansible.builtin.first_found', req_candidates, skip=True) }}"
req_candidates:
- "{{ project_path | quote }}/requirements.yaml"
- "{{ project_path | quote }}/requirements.yml"
files:
- "{{ project_path | quote }}/requirements.yaml"
- "{{ project_path | quote }}/requirements.yml"
skip: True
changed_when: "'Nothing to do.' not in galaxy_combined_result.stdout"
when:
- "ansible_version.full is version_compare('2.10', '>=')"

View File

@@ -1126,3 +1126,11 @@ from ansible_base.lib import dynamic_config # noqa: E402
settings_file = os.path.join(os.path.dirname(dynamic_config.__file__), 'dynamic_settings.py')
include(settings_file)
# Add a postfix to the API URL patterns
# 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

@@ -59,6 +59,7 @@ register(
help_text=_('Maximum number of job events for the UI to retrieve within a single request.'),
category=_('UI'),
category_slug='ui',
hidden=True,
)
register(
@@ -68,4 +69,5 @@ register(
help_text=_('If disabled, the page will not refresh when events are received. Reloading the page will be required to get the latest details.'),
category=_('UI'),
category_slug='ui',
hidden=True,
)

View File

@@ -67,27 +67,18 @@ function getInitialValues(launchConfig, surveyConfig, resource) {
const values = {};
if (surveyConfig?.spec) {
surveyConfig.spec.forEach((question) => {
if (question.type === 'multiselect') {
if (resource?.extra_data && resource?.extra_data[question.variable]) {
values[`survey_${question.variable}`] =
resource.extra_data[question.variable];
} else if (question.type === 'multiselect') {
values[`survey_${question.variable}`] = question.default
? question.default.split('\n')
: [];
} else {
values[`survey_${question.variable}`] = question.default ?? '';
}
if (resource?.extra_data) {
Object.entries(resource.extra_data).forEach(([key, value]) => {
if (key === question.variable) {
if (question.type === 'multiselect') {
values[`survey_${question.variable}`] = value;
} else {
values[`survey_${question.variable}`] = value;
}
}
});
}
});
}
return values;
}

View File

@@ -13,6 +13,18 @@ import ScheduleForm from '../shared/ScheduleForm';
import buildRuleSet from '../shared/buildRuleSet';
import { CardBody } from '../../Card';
function generateExtraData(extra_vars, surveyValues, surveyConfiguration) {
const extraVars = parseVariableField(
yaml.dump(mergeExtraVars(extra_vars, surveyValues))
);
surveyConfiguration.spec.forEach((q) => {
if (!surveyValues[q.variable]) {
delete extraVars[q.variable];
}
});
return extraVars;
}
function ScheduleEdit({
hasDaysToKeepField,
schedule,
@@ -33,10 +45,12 @@ function ScheduleEdit({
surveyConfiguration,
originalInstanceGroups,
originalLabels,
scheduleCredentials = []
scheduleCredentials = [],
isPromptTouched = false
) => {
const {
execution_environment,
extra_vars = null,
instance_groups,
inventory,
credentials = [],
@@ -48,45 +62,54 @@ function ScheduleEdit({
labels,
...submitValues
} = values;
let extraVars;
const surveyValues = getSurveyValues(values);
if (
!Object.values(surveyValues).length &&
surveyConfiguration?.spec?.length
isPromptTouched &&
surveyConfiguration?.spec &&
launchConfiguration?.ask_variables_on_launch
) {
surveyConfiguration.spec.forEach((q) => {
surveyValues[q.variable] = q.default;
});
submitValues.extra_data = generateExtraData(
extra_vars,
surveyValues,
surveyConfiguration
);
} else if (
isPromptTouched &&
surveyConfiguration?.spec &&
!launchConfiguration?.ask_variables_on_launch
) {
submitValues.extra_data = generateExtraData(
schedule.extra_data,
surveyValues,
surveyConfiguration
);
} else if (
isPromptTouched &&
launchConfiguration?.ask_variables_on_launch
) {
submitValues.extra_data = parseVariableField(extra_vars);
}
const initialExtraVars =
launchConfiguration?.ask_variables_on_launch &&
(values.extra_vars || '---');
if (surveyConfiguration?.spec) {
extraVars = yaml.dump(mergeExtraVars(initialExtraVars, surveyValues));
} else {
extraVars = yaml.dump(mergeExtraVars(initialExtraVars, {}));
}
submitValues.extra_data = extraVars && parseVariableField(extraVars);
if (
Object.keys(submitValues.extra_data).length === 0 &&
Object.keys(schedule.extra_data).length > 0
isPromptTouched &&
launchConfiguration?.ask_inventory_on_launch &&
inventory
) {
submitValues.extra_data = schedule.extra_data;
}
delete values.extra_vars;
if (inventory) {
submitValues.inventory = inventory.id;
}
if (execution_environment) {
if (
isPromptTouched &&
launchConfiguration?.ask_execution_environment_on_launch &&
execution_environment
) {
submitValues.execution_environment = execution_environment.id;
}
try {
if (launchConfiguration?.ask_labels_on_launch) {
if (isPromptTouched && launchConfiguration?.ask_labels_on_launch) {
const { labelIds, error } = createNewLabels(
values.labels,
resource.organization
@@ -120,9 +143,16 @@ function ScheduleEdit({
}
}
const cleanedRequestData = Object.keys(requestData)
.filter((key) => !key.startsWith('survey_'))
.reduce((acc, key) => {
acc[key] = requestData[key];
return acc;
}, {});
const {
data: { id: scheduleId },
} = await SchedulesAPI.update(schedule.id, requestData);
} = await SchedulesAPI.update(schedule.id, cleanedRequestData);
const { added: addedCredentials, removed: removedCredentials } =
getAddedAndRemoved(

View File

@@ -6,6 +6,7 @@ import {
InventoriesAPI,
CredentialsAPI,
CredentialTypesAPI,
JobTemplatesAPI,
} from 'api';
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
import ScheduleEdit from './ScheduleEdit';
@@ -125,6 +126,7 @@ describe('<ScheduleEdit />', () => {
id: 27,
},
});
await act(async () => {
wrapper = mountWithContexts(
<ScheduleEdit
@@ -206,7 +208,6 @@ describe('<ScheduleEdit />', () => {
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
description: 'test description',
name: 'Run once schedule',
extra_data: {},
rrule:
'DTSTART;TZID=America/New_York:20200325T100000 RRULE:INTERVAL=1;COUNT=1;FREQ=MINUTELY',
});
@@ -233,7 +234,6 @@ describe('<ScheduleEdit />', () => {
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
description: 'test description',
name: 'Run every 10 minutes 10 times',
extra_data: {},
rrule:
'DTSTART;TZID=America/New_York:20200325T103000 RRULE:INTERVAL=10;FREQ=MINUTELY;COUNT=10',
});
@@ -262,7 +262,6 @@ describe('<ScheduleEdit />', () => {
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
description: 'test description',
name: 'Run every hour until date',
extra_data: {},
rrule:
'DTSTART;TZID=America/New_York:20200325T104500 RRULE:INTERVAL=1;FREQ=HOURLY;UNTIL=20200326T144500Z',
});
@@ -288,7 +287,6 @@ describe('<ScheduleEdit />', () => {
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
description: 'test description',
name: 'Run daily',
extra_data: {},
rrule:
'DTSTART;TZID=America/New_York:20200325T104500 RRULE:INTERVAL=1;FREQ=DAILY',
});
@@ -316,7 +314,6 @@ describe('<ScheduleEdit />', () => {
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
description: 'test description',
name: 'Run weekly on mon/wed/fri',
extra_data: {},
rrule: `DTSTART;TZID=America/New_York:20200325T104500 RRULE:INTERVAL=1;FREQ=WEEKLY;BYDAY=${RRule.MO},${RRule.WE},${RRule.FR}`,
});
});
@@ -344,7 +341,6 @@ describe('<ScheduleEdit />', () => {
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
description: 'test description',
name: 'Run on the first day of the month',
extra_data: {},
rrule:
'DTSTART;TZID=America/New_York:20200401T104500 RRULE:INTERVAL=1;FREQ=MONTHLY;BYMONTHDAY=1',
});
@@ -376,7 +372,6 @@ describe('<ScheduleEdit />', () => {
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
description: 'test description',
name: 'Run monthly on the last Tuesday',
extra_data: {},
rrule:
'DTSTART;TZID=America/New_York:20200331T110000 RRULE:INTERVAL=1;FREQ=MONTHLY;BYSETPOS=-1;BYDAY=TU',
});
@@ -406,7 +401,6 @@ describe('<ScheduleEdit />', () => {
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
description: 'test description',
name: 'Yearly on the first day of March',
extra_data: {},
rrule:
'DTSTART;TZID=America/New_York:20200301T000000 RRULE:INTERVAL=1;FREQ=YEARLY;BYMONTH=3;BYMONTHDAY=1',
});
@@ -437,7 +431,6 @@ describe('<ScheduleEdit />', () => {
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
description: 'test description',
name: 'Yearly on the second Friday in April',
extra_data: {},
rrule:
'DTSTART;TZID=America/New_York:20200410T111500 RRULE:INTERVAL=1;FREQ=YEARLY;BYSETPOS=2;BYDAY=FR;BYMONTH=4',
});
@@ -468,7 +461,6 @@ describe('<ScheduleEdit />', () => {
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
description: 'test description',
name: 'Yearly on the first weekday in October',
extra_data: {},
rrule:
'DTSTART;TZID=America/New_York:20200410T111500 RRULE:INTERVAL=1;FREQ=YEARLY;BYSETPOS=1;BYDAY=MO,TU,WE,TH,FR;BYMONTH=10',
});
@@ -562,7 +554,6 @@ describe('<ScheduleEdit />', () => {
wrapper.update();
expect(SchedulesAPI.update).toBeCalledWith(27, {
extra_data: {},
name: 'mock schedule',
rrule:
'DTSTART;TZID=America/New_York:20210128T141500 RRULE:INTERVAL=1;COUNT=1;FREQ=MINUTELY',
@@ -633,15 +624,13 @@ describe('<ScheduleEdit />', () => {
endDateTime: undefined,
startDateTime: undefined,
description: '',
extra_data: {},
name: 'foo',
inventory: 702,
rrule:
'DTSTART;TZID=America/New_York:20200402T144500 RRULE:INTERVAL=1;COUNT=1;FREQ=MINUTELY',
});
});
test('should submit survey with default values properly, without opening prompt wizard', async () => {
test('should submit update values properly when prompt is not opened', async () => {
let scheduleSurveyWrapper;
await act(async () => {
scheduleSurveyWrapper = mountWithContexts(
@@ -746,9 +735,195 @@ describe('<ScheduleEdit />', () => {
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
description: 'test description',
name: 'Run once schedule',
extra_data: { mc: 'first', text: 'text variable' },
rrule:
'DTSTART;TZID=America/New_York:20200325T100000 RRULE:INTERVAL=1;COUNT=1;FREQ=MINUTELY',
});
});
test('should submit update values properly when survey values change', async () => {
JobTemplatesAPI.readSurvey.mockResolvedValue({
data: {
spec: [
{
question_name: 'text',
question_description: '',
required: true,
type: 'text',
variable: 'text',
min: 0,
max: 1024,
default: 'text variable',
choices: '',
new_question: true,
},
],
},
});
JobTemplatesAPI.readLaunch.mockResolvedValue({
data: {
can_start_without_user_input: false,
passwords_needed_to_start: [],
ask_scm_branch_on_launch: false,
ask_variables_on_launch: false,
ask_tags_on_launch: false,
ask_diff_mode_on_launch: false,
ask_skip_tags_on_launch: false,
ask_job_type_on_launch: false,
ask_limit_on_launch: false,
ask_verbosity_on_launch: false,
ask_inventory_on_launch: true,
ask_credential_on_launch: true,
survey_enabled: true,
variables_needed_to_start: [],
credential_needed_to_start: true,
inventory_needed_to_start: true,
job_template_data: {
name: 'Demo Job Template',
id: 7,
description: '',
},
defaults: {
extra_vars: '---',
diff_mode: false,
limit: '',
job_tags: '',
skip_tags: '',
job_type: 'run',
verbosity: 0,
inventory: {
name: null,
id: null,
},
scm_branch: '',
credentials: [],
},
},
});
let scheduleSurveyWrapper;
await act(async () => {
scheduleSurveyWrapper = mountWithContexts(
<ScheduleEdit
schedule={mockSchedule}
resource={{
id: 700,
type: 'job_template',
iventory: 1,
summary_fields: {
credentials: [
{ name: 'job template credential', id: 75, kind: 'ssh' },
],
},
name: 'Foo Job Template',
description: '',
}}
resourceDefaultCredentials={[]}
launchConfig={{
can_start_without_user_input: false,
passwords_needed_to_start: [],
ask_scm_branch_on_launch: false,
ask_variables_on_launch: false,
ask_tags_on_launch: false,
ask_diff_mode_on_launch: false,
ask_skip_tags_on_launch: false,
ask_job_type_on_launch: false,
ask_limit_on_launch: false,
ask_verbosity_on_launch: false,
ask_inventory_on_launch: true,
ask_credential_on_launch: true,
survey_enabled: true,
variables_needed_to_start: [],
credential_needed_to_start: true,
inventory_needed_to_start: true,
job_template_data: {
name: 'Demo Job Template',
id: 7,
description: '',
},
defaults: {
extra_vars: '---',
diff_mode: false,
limit: '',
job_tags: '',
skip_tags: '',
job_type: 'run',
verbosity: 0,
inventory: {
name: null,
id: null,
},
scm_branch: '',
credentials: [],
},
}}
surveyConfig={{
spec: [
{
question_name: 'text',
question_description: '',
required: true,
type: 'text',
variable: 'text',
min: 0,
max: 1024,
default: 'text variable',
choices: '',
new_question: true,
},
],
}}
/>
);
});
scheduleSurveyWrapper.update();
await act(async () =>
scheduleSurveyWrapper
.find('Button[aria-label="Prompt"]')
.prop('onClick')()
);
scheduleSurveyWrapper.update();
expect(scheduleSurveyWrapper.find('WizardNavItem').length).toBe(4);
await act(async () =>
scheduleSurveyWrapper.find('WizardFooterInternal').prop('onNext')()
);
scheduleSurveyWrapper.update();
await act(async () =>
scheduleSurveyWrapper.find('WizardFooterInternal').prop('onNext')()
);
scheduleSurveyWrapper.update();
await act(async () =>
scheduleSurveyWrapper
.find('input#survey-question-text')
.simulate('change', {
target: { value: 'foo', name: 'survey_text' },
})
);
scheduleSurveyWrapper.update();
await act(async () =>
scheduleSurveyWrapper.find('WizardFooterInternal').prop('onNext')()
);
scheduleSurveyWrapper.update();
await act(async () =>
scheduleSurveyWrapper.find('WizardFooterInternal').prop('onNext')()
);
scheduleSurveyWrapper.update();
expect(scheduleSurveyWrapper.find('Wizard').length).toBe(0);
await act(async () =>
scheduleSurveyWrapper.find('Button[aria-label="Save"]').prop('onClick')()
);
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
description: '',
name: 'mock schedule',
inventory: 702,
extra_data: {
text: 'foo',
},
rrule:
'DTSTART;TZID=America/New_York:20200402T144500 RRULE:INTERVAL=1;COUNT=1;FREQ=MINUTELY',
});
});
});

View File

@@ -40,6 +40,7 @@ function ScheduleForm({
resourceDefaultCredentials,
}) {
const [isWizardOpen, setIsWizardOpen] = useState(false);
const [isPromptTouched, setIsPromptTouched] = useState(false);
const [isSaveDisabled, setIsSaveDisabled] = useState(false);
const originalLabels = useRef([]);
const originalInstanceGroups = useRef([]);
@@ -492,7 +493,8 @@ function ScheduleForm({
surveyConfig,
originalInstanceGroups.current,
originalLabels.current,
credentials
credentials,
isPromptTouched
);
}}
validate={validate}
@@ -518,6 +520,7 @@ function ScheduleForm({
onSave={() => {
setIsWizardOpen(false);
setIsSaveDisabled(false);
setIsPromptTouched(true);
}}
resourceDefaultCredentials={resourceDefaultCredentials}
labels={originalLabels.current}

View File

@@ -191,7 +191,7 @@ function InstancePeerList({ setBreadcrumb }) {
fetchPeers();
addToast({
id: instancesPeerToAssociate,
title: t`Peers update on ${instance.hostname}. Please be sure to run the install bundle for ${instance.hostname} again in order to see changes take effect.`,
title: t`Please be sure to run the install bundle for the selected instance(s) again in order to see changes take effect.`,
variant: AlertVariant.success,
hasTimeout: true,
});

View File

@@ -1,7 +1,7 @@
export default function getSurveyValues(values) {
const surveyValues = {};
Object.keys(values).forEach((key) => {
if (key.startsWith('survey_') && values[key] !== []) {
if (key.startsWith('survey_')) {
if (Array.isArray(values[key]) && values[key].length === 0) {
return;
}

View File

@@ -1,7 +1,12 @@
import yaml from 'js-yaml';
export default function mergeExtraVars(extraVars = '', survey = {}) {
const vars = yaml.load(extraVars) || {};
let vars = {};
if (typeof extraVars === 'string') {
vars = yaml.load(extraVars);
} else if (typeof extraVars === 'object') {
vars = extraVars;
}
return {
...vars,
...survey,

View File

@@ -2,7 +2,7 @@
# All Rights Reserved.
from django.conf import settings
from django.urls import re_path, include
from django.urls import path, re_path, include
from ansible_base.resource_registry.urls import urlpatterns as resource_api_urls
@@ -12,7 +12,15 @@ from awx.main.views import handle_400, handle_403, handle_404, handle_500, handl
urlpatterns = [
re_path(r'', include('awx.ui.urls', namespace='ui')),
re_path(r'^ui_next/.*', include('awx.ui_next.urls', namespace='ui_next')),
re_path(r'^api/', include('awx.api.urls', namespace='api')),
path('api/', include('awx.api.urls', namespace='api')),
]
if settings.OPTIONAL_API_URLPATTERN_PREFIX:
urlpatterns += [
path(f'api/{settings.OPTIONAL_API_URLPATTERN_PREFIX}/', include('awx.api.urls')),
]
urlpatterns += [
re_path(r'^api/v2/', include(resource_api_urls)),
re_path(r'^sso/', include('awx.sso.urls', namespace='sso')),
re_path(r'^sso/', include('social_django.urls', namespace='social')),

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

@@ -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

@@ -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

@@ -175,9 +175,10 @@ class TestOptions(unittest.TestCase):
assert '--verbosity {0,1,2,3,4,5}' in out.getvalue()
def test_actions_with_primary_key(self):
page = OptionsPage.from_json({'actions': {'GET': {}, 'POST': {}}})
ResourceOptionsParser(None, page, 'jobs', self.parser)
for method in ('get', 'modify', 'delete'):
page = OptionsPage.from_json({'actions': {'GET': {}, 'POST': {}}})
ResourceOptionsParser(None, page, 'jobs', self.parser)
assert method in self.parser.choices
out = StringIO()

View File

@@ -8,7 +8,7 @@ skip_missing_interpreters = true
# skipsdist = true
[testenv]
basepython = python3.9
basepython = python3.11
setenv =
PYTHONPATH = {toxinidir}:{env:PYTHONPATH:}:.
deps =

View File

@@ -6,7 +6,7 @@ The *awx-manage* Utility
.. index::
single: awx-manage
The ``awx-manage`` utility is used to access detailed internal information of AWX. Commands for ``awx-manage`` should run as the ``awx`` or ``root`` user.
The ``awx-manage`` utility is used to access detailed internal information of AWX. Commands for ``awx-manage`` should run as the ``awx`` user only.
.. warning::
Running awx-manage commands via playbook is not recommended or supported.

View File

@@ -557,7 +557,7 @@ Terminal Access Controller Access-Control System Plus (TACACS+) is a protocol th
Generic OIDC settings
----------------------
Similar to SAML, OpenID Connect (OIDC) is uses the OAuth 2.0 framework. It allows third-party applications to verify the identity and obtain basic end-user information. The main difference between OIDC and SMAL is that SAML has a service provider (SP)-to-IdP trust relationship, whereas OIDC establishes the trust with the channel (HTTPS) that is used to obtain the security token. To obtain the credentials needed to setup OIDC with AWX, refer to the documentation from the identity provider (IdP) of your choice that has OIDC support.
Similar to SAML, OpenID Connect (OIDC) is uses the OAuth 2.0 framework. It allows third-party applications to verify the identity and obtain basic end-user information. The main difference between OIDC and SAML is that SAML has a service provider (SP)-to-IdP trust relationship, whereas OIDC establishes the trust with the channel (HTTPS) that is used to obtain the security token. To obtain the credentials needed to setup OIDC with AWX, refer to the documentation from the identity provider (IdP) of your choice that has OIDC support.
To configure OIDC in AWX:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

View File

@@ -146,7 +146,7 @@ If you have a VMware instance that uses a self-signed certificate, then you will
.. code-block:: text
"source_vars": "---\nvalidate_certs: False",
"source_vars": ---validate_certs: False
You can set this in inventory source for VMware vCenter as follows:

View File

@@ -202,7 +202,7 @@ The following table lists the RBAC system roles and a brief description of the h
+-----------------------------------------------------------------------+------------------------------------------------------------------------------------------+
| Execute Role - Job Templates | Runs assigned Job Template |
+-----------------------------------------------------------------------+------------------------------------------------------------------------------------------+
| Member Role - Organization, Team | Manages all of the settings associated with that Organization or Team |
| Member Role - Organization, Team | User is a member of a defined Organization or Team |
+-----------------------------------------------------------------------+------------------------------------------------------------------------------------------+
| Read Role - Organizations, Teams, Inventory, Projects, Job Templates | Views all aspects of a defined Organization, Team, Inventory, Project, or Job Template |
+-----------------------------------------------------------------------+------------------------------------------------------------------------------------------+

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,9 +1,8 @@
The MIT License (MIT)
Copyright (c) 2017 Laurent LAPORTE
Copyright (c) 2020 aiohttp_retry Authors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2022 the contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,21 @@
Copyright (c) Microsoft Corporation.
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,21 @@
Copyright (c) Microsoft Corporation.
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,21 @@
Copyright (c) Microsoft Corporation.
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Binary file not shown.

View File

@@ -0,0 +1,21 @@
Copyright (c) Microsoft Corporation.
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,10 +0,0 @@
# As listed on https://pypi.python.org/pypi/irc
The MIT License (MIT)
Copyright (c) <year> <copyright holders>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,19 @@
Copyright (c) 2022 Julian Berman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

Binary file not shown.

Binary file not shown.

25
licenses/maturin.txt Normal file
View File

@@ -0,0 +1,25 @@
Copyright (c) 2018 konstin
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) Microsoft Corporation. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE

24
licenses/msal.txt Normal file
View File

@@ -0,0 +1,24 @@
The MIT License (MIT)
Copyright (c) Microsoft Corporation.
All rights reserved.
This code is licensed under the MIT License.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files(the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions :
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

11
licenses/portalocker.txt Normal file
View File

@@ -0,0 +1,11 @@
Copyright 2022 Rick van Hattem
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. 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.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
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.

Binary file not shown.

Binary file not shown.

View File

@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2016 Microsoft
Copyright (c) 2022 Samuel Colvin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.

View File

@@ -1,22 +0,0 @@
Copyright (c) 2019 Tobias Gustafsson
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

19
licenses/referencing.txt Normal file
View File

@@ -0,0 +1,19 @@
Copyright (c) 2022 Julian Berman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

19
licenses/rpds-py.txt Normal file
View File

@@ -0,0 +1,19 @@
Copyright (c) 2023 Julian Berman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

Binary file not shown.

Binary file not shown.

View File

@@ -1,24 +0,0 @@
Copyright (c) 2013-2023, Graham Dumpleton
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.

6
package-lock.json generated Normal file
View File

@@ -0,0 +1,6 @@
{
"name": "awx",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

View File

@@ -110,16 +110,6 @@ OpenID Connect work that was done in
https://github.com/jazzband/django-oauth-toolkit/pull/915. This may
be fixable by creating a migration on our end?
### azure-keyvault
Upgrading to 4.0.0 causes error because imports changed.
```
File "/var/lib/awx/venv/awx/lib64/python3.6/site-packages/awx/main/credential_plugins/azure_kv.py", line 4, in <module>
from azure.keyvault import KeyVaultClient, KeyVaultAuthentication
ImportError: cannot import name 'KeyVaultClient'
```
### pip, setuptools and setuptools_scm
If modifying these libraries make sure testing with the offline build is performed to confirm they are functionally working.

View File

@@ -3,12 +3,13 @@ ansiconv==1.0.0 # UPGRADE BLOCKER: from 2013, consider replacing instead of upg
asciichartpy
asn1
asyncpg
azure-keyvault==1.1.0 # see UPGRADE BLOCKERs
azure-identity
azure-keyvault
boto3
botocore
channels
channels-redis==3.4.1 # see UPGRADE BLOCKERs
cryptography>=41.0.6 # CVE-2023-49083
cryptography>=41.0.7 # CVE-2023-49083
Cython<3 # this is needed as a build dependency, one day we may have separated build deps
daphne
distro
@@ -33,6 +34,9 @@ jinja2>=3.1.3 # CVE-2024-22195
JSON-log-formatter
jsonschema
Markdown # used for formatting API help
maturin # pydantic-core build dep
msgpack<1.0.6 # 1.0.6+ requires cython>=3
msrestazure
openshift
pexpect==4.7.0 # see library notes
prometheus_client

View File

@@ -2,66 +2,89 @@ adal==1.2.7
# via msrestazure
aiohttp==3.9.3
# via -r /awx_devel/requirements/requirements.in
# aiohttp-retry
# twilio
aiohttp-retry==2.8.3
# via twilio
aioredis==1.3.1
# via channels-redis
aiosignal==1.3.1
# via aiohttp
annotated-types==0.6.0
# via pydantic
# via -r /awx_devel/requirements/requirements_git.txt
ansiconv==1.0.0
# via -r /awx_devel/requirements/requirements.in
asciichartpy==1.5.25
# via -r /awx_devel/requirements/requirements.in
asgiref==3.6.0
asgiref==3.7.2
# via
# channels
# channels-redis
# daphne
# django
asn1==2.6.0
# django-cors-headers
asn1==2.7.0
# via -r /awx_devel/requirements/requirements.in
async-timeout==4.0.2
async-timeout==4.0.3
# via
# aiohttp
# aioredis
# asyncpg
# redis
asyncpg==0.27.0
asyncpg==0.29.0
# via -r /awx_devel/requirements/requirements.in
attrs==22.1.0
attrs==23.2.0
# via
# aiohttp
# automat
# jsonschema
# referencing
# service-identity
# twisted
autobahn==22.7.1
autobahn==23.6.2
# via daphne
autocommand==2.2.2
# via jaraco-text
automat==22.10.0
# via twisted
azure-common==1.1.28
# via azure-keyvault
azure-core==1.26.1
# via msrest
azure-keyvault==1.1.0
# via
# azure-keyvault-certificates
# azure-keyvault-keys
# azure-keyvault-secrets
azure-core==1.30.0
# via
# azure-identity
# azure-keyvault-certificates
# azure-keyvault-keys
# azure-keyvault-secrets
# msrest
azure-identity==1.15.0
# via -r /awx_devel/requirements/requirements.in
azure-nspkg==3.0.2
# via azure-keyvault
boto3==1.26.102
azure-keyvault==4.2.0
# via -r /awx_devel/requirements/requirements.in
botocore==1.29.102
azure-keyvault-certificates==4.7.0
# via azure-keyvault
azure-keyvault-keys==4.8.0
# via azure-keyvault
azure-keyvault-secrets==4.7.0
# via azure-keyvault
boto3==1.34.47
# via -r /awx_devel/requirements/requirements.in
botocore==1.34.47
# via
# -r /awx_devel/requirements/requirements.in
# boto3
# s3transfer
cachetools==5.2.0
cachetools==5.3.2
# via google-auth
# via
# -r /awx_devel/requirements/requirements_git.txt
# kubernetes
# msrest
# requests
cffi==1.15.1
cffi==1.16.0
# via cryptography
channels==3.0.5
# via
@@ -69,24 +92,27 @@ channels==3.0.5
# channels-redis
channels-redis==3.4.1
# via -r /awx_devel/requirements/requirements.in
charset-normalizer==2.1.1
charset-normalizer==3.3.2
# via requests
click==8.1.3
click==8.1.7
# via receptorctl
constantly==15.1.0
constantly==23.10.4
# via twisted
cryptography==41.0.7
# via
# -r /awx_devel/requirements/requirements.in
# adal
# autobahn
# azure-keyvault
# azure-identity
# azure-keyvault-keys
# django-ansible-base
# jwcrypto
# msal
# pyjwt
# pyopenssl
# service-identity
# social-auth-core
cython==0.29.32
cython==0.29.37
# via -r /awx_devel/requirements/requirements.in
daphne==3.0.2
# via
@@ -96,9 +122,7 @@ defusedxml==0.7.1
# via
# python3-openid
# social-auth-core
deprecated==1.2.13
# via jwcrypto
distro==1.8.0
distro==1.9.0
# via -r /awx_devel/requirements/requirements.in
django==4.2.6
# via
@@ -116,15 +140,15 @@ django==4.2.6
# djangorestframework
# social-auth-app-django
# via -r /awx_devel/requirements/requirements_git.txt
django-auth-ldap==4.1.0
django-auth-ldap==4.6.0
# via -r /awx_devel/requirements/requirements.in
django-cors-headers==3.13.0
django-cors-headers==4.3.1
# via -r /awx_devel/requirements/requirements.in
django-crum==0.7.9
# via
# -r /awx_devel/requirements/requirements.in
# django-ansible-base
django-extensions==3.2.1
django-extensions==3.2.3
# via -r /awx_devel/requirements/requirements.in
django-guid==3.2.1
# via -r /awx_devel/requirements/requirements.in
@@ -135,7 +159,7 @@ django-pglocks==1.0.4
django-polymorphic==3.1.0
# via -r /awx_devel/requirements/requirements.in
# via -r /awx_devel/requirements/requirements_git.txt
django-solo==2.0.0
django-solo==2.2.0
# via -r /awx_devel/requirements/requirements.in
django-split-settings==1.0.0
# via
@@ -147,23 +171,23 @@ djangorestframework==3.14.0
# django-ansible-base
djangorestframework-yaml==2.0.0
# via -r /awx_devel/requirements/requirements.in
docutils==0.19
docutils==0.20.1
# via python-daemon
ecdsa==0.18.0
# via python-jose
enum-compat==0.0.3
# via asn1
filelock==3.8.0
filelock==3.13.1
# via -r /awx_devel/requirements/requirements.in
frozenlist==1.3.3
frozenlist==1.4.1
# via
# aiohttp
# aiosignal
gitdb==4.0.10
gitdb==4.0.11
# via gitpython
gitpython==3.1.42
# via -r /awx_devel/requirements/requirements.in
google-auth==2.14.1
google-auth==2.28.1
# via kubernetes
hiredis==2.0.0
# via
@@ -173,44 +197,45 @@ hyperlink==21.0.0
# via
# autobahn
# twisted
idna==3.4
idna==3.6
# via
# hyperlink
# requests
# twisted
# yarl
importlib-metadata==4.6.4
importlib-metadata==6.2.1
# via
# ansible-runner
# markdown
incremental==22.10.0
# via twisted
inflect==6.0.2
inflect==7.0.0
# via jaraco-text
inflection==0.5.1
# via django-ansible-base
irc==20.1.0
irc==20.3.1
# via -r /awx_devel/requirements/requirements.in
isodate==0.6.1
# via
# azure-keyvault-certificates
# azure-keyvault-keys
# azure-keyvault-secrets
# msrest
# python3-saml
jaraco-classes==3.2.3
# via jaraco-collections
jaraco-collections==3.8.0
jaraco-collections==5.0.0
# via irc
jaraco-context==4.2.0
jaraco-context==4.3.0
# via jaraco-text
jaraco-functools==3.5.2
jaraco-functools==4.0.0
# via
# irc
# jaraco-text
# tempora
jaraco-logging==3.1.2
jaraco-logging==3.3.0
# via irc
jaraco-stream==3.0.3
# via irc
jaraco-text==3.11.0
jaraco-text==3.12.0
# via
# irc
# jaraco-collections
@@ -220,57 +245,67 @@ jmespath==1.0.1
# via
# boto3
# botocore
json-log-formatter==0.5.1
json-log-formatter==0.5.2
# via -r /awx_devel/requirements/requirements.in
jsonschema==4.17.3
jsonschema==4.21.1
# via -r /awx_devel/requirements/requirements.in
jwcrypto==1.4.2
jsonschema-specifications==2023.12.1
# via jsonschema
jwcrypto==1.5.4
# via django-oauth-toolkit
kubernetes==25.3.0
kubernetes==29.0.0
# via openshift
lockfile==0.12.2
# via python-daemon
lxml==4.9.1
lxml==4.9.4
# via
# python3-saml
# xmlsec
markdown==3.4.1
markdown==3.5.2
# via -r /awx_devel/requirements/requirements.in
markupsafe==2.1.1
markupsafe==2.1.5
# via jinja2
more-itertools==9.0.0
maturin==1.5.0
# via -r /awx_devel/requirements/requirements.in
more-itertools==10.2.0
# via
# irc
# jaraco-classes
# jaraco-functools
# jaraco-text
msgpack==1.0.4
# via channels-redis
msrest==0.7.1
msal==1.26.0
# via
# azure-keyvault
# msrestazure
# azure-identity
# msal-extensions
msal-extensions==1.1.0
# via azure-identity
msgpack==1.0.5
# via
# -r /awx_devel/requirements/requirements.in
# channels-redis
msrest==0.7.1
# via msrestazure
msrestazure==0.6.4
# via azure-keyvault
multidict==6.0.2
# via -r /awx_devel/requirements/requirements.in
multidict==6.0.5
# via
# aiohttp
# yarl
netaddr==0.8.0
netaddr==1.2.1
# via pyrad
oauthlib==3.2.2
# via
# django-oauth-toolkit
# kubernetes
# requests-oauthlib
# social-auth-core
openshift==0.13.1
openshift==0.13.2
# via -r /awx_devel/requirements/requirements.in
packaging==21.3
packaging==23.2
# via
# ansible-runner
# redis
# msal-extensions
# setuptools-scm
pbr==5.11.0
pbr==6.0.0
# via -r /awx_devel/requirements/requirements.in
pexpect==4.7.0
# via
@@ -278,50 +313,53 @@ pexpect==4.7.0
# ansible-runner
pkgconfig==1.5.5
# via -r /awx_devel/requirements/requirements.in
prometheus-client==0.15.0
portalocker==2.8.2
# via msal-extensions
prometheus-client==0.20.0
# via -r /awx_devel/requirements/requirements.in
psutil==5.9.4
psutil==5.9.8
# via -r /awx_devel/requirements/requirements.in
psycopg==3.1.9
psycopg==3.1.18
# via -r /awx_devel/requirements/requirements.in
ptyprocess==0.7.0
# via pexpect
pyasn1==0.4.8
pyasn1==0.5.1
# via
# pyasn1-modules
# python-jose
# python-ldap
# rsa
# service-identity
pyasn1-modules==0.2.8
pyasn1-modules==0.3.0
# via
# google-auth
# python-ldap
# service-identity
pycparser==2.21
# via cffi
pydantic==1.10.2
pydantic==2.5.0
# via inflect
pydantic-core==2.14.1
# via
# -r /awx_devel/requirements/requirements.in
# pydantic
pygerduty==0.38.3
# via -r /awx_devel/requirements/requirements.in
pyjwt==2.6.0
pyjwt[crypto]==2.8.0
# via
# adal
# django-ansible-base
# msal
# social-auth-core
# twilio
pyopenssl==23.2.0
pyopenssl==24.0.0
# via
# -r /awx_devel/requirements/requirements.in
# twisted
pyparsing==2.4.6
# via
# -r /awx_devel/requirements/requirements.in
# packaging
# via -r /awx_devel/requirements/requirements.in
pyrad==2.4
# via django-radius
pyrsistent==0.19.2
# via jsonschema
python-daemon==3.0.1
# via
# -r /awx_devel/requirements/requirements.in
@@ -336,23 +374,22 @@ python-dsv-sdk==1.0.4
# via -r /awx_devel/requirements/requirements.in
python-jose==3.3.0
# via social-auth-core
python-ldap==3.4.3
python-ldap==3.4.4
# via
# -r /awx_devel/requirements/requirements.in
# django-auth-ldap
python-string-utils==1.0.0
# via openshift
python-tss-sdk==1.2.1
python-tss-sdk==1.2.2
# via -r /awx_devel/requirements/requirements.in
python3-openid==3.2.0
# via social-auth-core
# via -r /awx_devel/requirements/requirements_git.txt
pytz==2022.6
pytz==2024.1
# via
# djangorestframework
# irc
# tempora
# twilio
pyyaml==6.0.1
# via
# -r /awx_devel/requirements/requirements.in
@@ -362,17 +399,21 @@ pyyaml==6.0.1
# receptorctl
receptorctl==1.4.4
# via -r /awx_devel/requirements/requirements.in
redis==4.3.5
redis==5.0.1
# via -r /awx_devel/requirements/requirements.in
requests==2.28.1
referencing==0.33.0
# via
# jsonschema
# jsonschema-specifications
requests==2.31.0
# via
# -r /awx_devel/requirements/requirements.in
# adal
# azure-core
# azure-keyvault
# django-ansible-base
# django-oauth-toolkit
# kubernetes
# msal
# msrest
# python-dsv-sdk
# python-tss-sdk
@@ -384,17 +425,21 @@ requests-oauthlib==1.3.1
# kubernetes
# msrest
# social-auth-core
rpds-py==0.18.0
# via
# jsonschema
# referencing
rsa==4.9
# via
# google-auth
# python-jose
s3transfer==0.6.0
s3transfer==0.10.0
# via boto3
semantic-version==2.10.0
# via setuptools-rust
service-identity==21.1.0
service-identity==24.1.0
# via twisted
setuptools-rust==1.5.2
setuptools-rust==1.8.1
# via -r /awx_devel/requirements/requirements.in
setuptools-scm[toml]==8.0.4
# via -r /awx_devel/requirements/requirements.in
@@ -404,7 +449,6 @@ six==1.16.0
# azure-core
# django-pglocks
# ecdsa
# google-auth
# isodate
# kubernetes
# msrestazure
@@ -412,11 +456,10 @@ six==1.16.0
# pygerduty
# pyrad
# python-dateutil
# service-identity
# tacacs-plus
slack-sdk==3.19.4
slack-sdk==3.27.0
# via -r /awx_devel/requirements/requirements.in
smmap==5.0.0
smmap==5.0.1
# via gitdb
social-auth-app-django==5.4.0
# via -r /awx_devel/requirements/requirements.in
@@ -430,60 +473,67 @@ sqlparse==0.4.4
# django
tacacs-plus==1.0
# via -r /awx_devel/requirements/requirements.in
tempora==5.1.0
tempora==5.5.1
# via
# irc
# jaraco-logging
tomli==2.0.1
# via setuptools-scm
twilio==7.15.3
# via
# maturin
# setuptools-rust
# setuptools-scm
twilio==8.13.0
# via -r /awx_devel/requirements/requirements.in
twisted[tls]==23.10.0
# via
# -r /awx_devel/requirements/requirements.in
# daphne
txaio==22.2.1
txaio==23.1.1
# via autobahn
typing-extensions==4.4.0
typing-extensions==4.9.0
# via
# asgiref
# azure-core
# azure-keyvault-certificates
# azure-keyvault-keys
# azure-keyvault-secrets
# inflect
# jwcrypto
# psycopg
# setuptools-rust
# pydantic
# pydantic-core
# setuptools-scm
# twisted
urllib3==1.26.17
urllib3==1.26.18
# via
# botocore
# kubernetes
# requests
uwsgi==2.0.21
uwsgi==2.0.24
# via -r /awx_devel/requirements/requirements.in
uwsgitop==0.11
# via -r /awx_devel/requirements/requirements.in
websocket-client==1.4.2
websocket-client==1.7.0
# via kubernetes
wheel==0.38.4
wheel==0.42.0
# via -r /awx_devel/requirements/requirements.in
wrapt==1.15.0
# via deprecated
xmlsec==1.3.13
# via python3-saml
yarl==1.8.1
yarl==1.9.4
# via aiohttp
zipp==3.11.0
zipp==3.17.0
# via importlib-metadata
zope-interface==5.5.2
zope-interface==6.2
# via twisted
# The following packages are considered to be unsafe in a requirements file:
pip==21.2.4
# via -r /awx_devel/requirements/requirements.in
setuptools==65.6.3
setuptools==69.0.2
# via
# -r /awx_devel/requirements/requirements.in
# asciichartpy
# autobahn
# kubernetes
# python-daemon
# setuptools-rust
# setuptools-scm

View File

@@ -39,15 +39,18 @@ RUN dnf -y update && dnf install -y 'dnf-command(config-manager)' && \
patch \
postgresql \
postgresql-devel \
python3-devel \
python3-pip \
python3-setuptools \
python3.11 \
"python3.11-devel" \
"python3.11-pip" \
"python3.11-setuptools" \
"python3.11-packaging" \
"python3.11-psycopg2" \
swig \
unzip \
xmlsec1-devel \
xmlsec1-openssl-devel
RUN pip3 install virtualenv build psycopg
RUN pip3.11 install -vv build
{% if image_architecture == 'ppc64le' %}
RUN dnf -y update && dnf install -y wget && \
@@ -87,7 +90,7 @@ WORKDIR /tmp/src/
RUN make sdist && /var/lib/awx/venv/awx/bin/pip install dist/awx.tar.gz
{% if not headless|bool %}
RUN AWX_SETTINGS_FILE=/dev/null SKIP_SECRET_KEY_CHECK=yes SKIP_PG_VERSION_CHECK=yes /var/lib/awx/venv/awx/bin/awx-manage collectstatic --noinput --clear
RUN DJANGO_SETTINGS_MODULE=awx.settings.defaults SKIP_SECRET_KEY_CHECK=yes SKIP_PG_VERSION_CHECK=yes /var/lib/awx/venv/awx/bin/awx-manage collectstatic --noinput --clear
{% endif %}
{% endif %}
@@ -118,10 +121,12 @@ RUN dnf -y update && dnf install -y 'dnf-command(config-manager)' && \
nginx \
"openldap >= 2.6.2-3" \
postgresql \
python3-devel \
python3-libselinux \
python3-pip \
python3-setuptools \
python3.11 \
"python3.11-devel" \
"python3.11-pip*" \
"python3.11-setuptools" \
"python3.11-packaging" \
"python3.11-psycopg2" \
rsync \
rsyslog-8.2102.0-106.el9 \
subversion \
@@ -132,7 +137,7 @@ RUN dnf -y update && dnf install -y 'dnf-command(config-manager)' && \
xmlsec1-openssl && \
dnf -y clean all
RUN pip3 install virtualenv supervisor dumb-init psycopg
RUN pip3.11 install -vv virtualenv supervisor dumb-init build
RUN rm -rf /root/.cache && rm -rf /tmp/*
@@ -164,7 +169,8 @@ RUN dnf -y install \
unzip && \
npm install -g n && n 16.13.1 && npm install -g npm@8.5.0 && dnf remove -y nodejs
RUN pip3 install black git+https://github.com/coderanger/supervisor-stdout setuptools-scm
RUN pip3.11 install -vv git+https://github.com/coderanger/supervisor-stdout.git@973ba19967cdaf46d9c1634d1675fc65b9574f6e
RUN pip3.11 install -vv black setuptools-scm build
# This package randomly fails to download.
# It is nice to have in the dev env, but not necessary.
@@ -235,7 +241,7 @@ ADD tools/scripts/awx-python /usr/bin/awx-python
{% endif %}
{% if (build_dev|bool) or (kube_dev|bool) %}
RUN echo /awx_devel > /var/lib/awx/venv/awx/lib/python3.9/site-packages/awx.egg-link
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 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
@@ -270,8 +276,8 @@ RUN for dir in \
/var/lib/awx/.local \
/var/lib/awx/venv \
/var/lib/awx/venv/awx/bin \
/var/lib/awx/venv/awx/lib/python3.9 \
/var/lib/awx/venv/awx/lib/python3.9/site-packages \
/var/lib/awx/venv/awx/lib/python3.11 \
/var/lib/awx/venv/awx/lib/python3.11/site-packages \
/var/lib/awx/projects \
/var/lib/awx/rsyslog \
/var/run/awx-rsyslog \

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

@@ -354,7 +354,7 @@ We are now ready to run two one time commands to build and pre-populate the Keyc
The first one time command will be creating a Keycloak database in your postgres database by running:
```bash
docker exec tools_postgres_1 /usr/bin/psql -U awx --command "create database keycloak with encoding 'UTF8';"
docker exec tools_postgres_1 /usr/bin/psql -U postgres --command 'CREATE DATABASE keycloak WITH OWNER=awx encoding "UTF8";'
```
After running this command the following message should appear and you should be returned to your prompt:
@@ -365,7 +365,7 @@ CREATE DATABASE
The second one time command will be to start a Keycloak container to build our admin user; be sure to set pg_username and pg_password to work for you installation. Note: the command below set the username as admin with a password of admin, you can change this if you want. Also, if you are using your own container or have changed the pg_username please update the command accordingly.
```bash
PG_PASSWORD=`cat tools/docker-compose/_sources/secrets/pg_password.yml | cut -f 2 -d \'`
docker run --rm -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin --net=_sources_default \
docker run --rm -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin --net=sources_awx \
-e DB_VENDOR=postgres -e DB_ADDR=postgres -e DB_DATABASE=keycloak -e DB_USER=awx -e DB_PASSWORD=${PG_PASSWORD} \
quay.io/keycloak/keycloak:15.0.2
```

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

@@ -59,3 +59,7 @@ scrape_interval: '5s'
enable_pgbouncer: false
pgbouncer_port: 6432
pgbouncer_max_pool_size: 70
# editable_dependencies
install_editable_dependencies: false
editable_dependencies: []

View File

@@ -105,6 +105,30 @@
include_tasks: vault_tls.yml
when: enable_vault | bool
- name: Iterate through ../editable_dependencies and get symlinked directories and register the paths
find:
paths: "{{ playbook_dir }}/../editable_dependencies"
file_type: link
recurse: no
register: _editable_dependencies_links
when: install_editable_dependencies | bool
- name: Warn about empty editable_dependnecies
fail:
msg: "[WARNING] No editable_dependencies found in ../editable_dependencies"
when: install_editable_dependencies | bool and not _editable_dependencies_links.files
ignore_errors: true
- name: Set fact with editable_dependencies
set_fact:
editable_dependencies: "{{ _editable_dependencies_links.files | map(attribute='path') | list }}"
when: install_editable_dependencies | bool and _editable_dependencies_links.files
- name: Set install_editable_dependnecies to false if no editable_dependencies are found
set_fact:
install_editable_dependencies: false
when: install_editable_dependencies | bool and not _editable_dependencies_links.files
- name: Render Docker-Compose
template:
src: docker-compose.yml.j2

View File

@@ -2,6 +2,22 @@
---
version: '2.1'
services:
{% if editable_dependencies | length > 0 %}
init_awx:
image: "{{ awx_image }}:{{ awx_image_tag }}"
container_name: tools_init_awx
command: /awx_devel/tools/docker-compose/editable_dependencies/install.sh
user: root
working_dir: "/"
environment:
RUN_MIGRATIONS: 1
volumes:
- "../../../:/awx_devel"
{% for editable_dependency in editable_dependencies %}
- "{{ editable_dependency }}:/editable_dependencies/{{ editable_dependency | basename }}"
{% endfor %}
- "var_lib_awx:/var/lib/awx"
{% endif %}
{% for i in range(control_plane_node_count|int) %}
{% set container_postfix = loop.index %}
{% set awx_sdb_port_start = 7899 + (loop.index0*1000) | int %}
@@ -39,6 +55,12 @@ services:
- service-mesh
working_dir: "/awx_devel"
volumes:
{% if editable_dependencies | length > 0 %}
- "var_lib_awx:/var/lib/awx"
{% for editable_dependency in editable_dependencies %}
- "{{ editable_dependency }}:/editable_dependencies/{{ editable_dependency | basename }}"
{% endfor %}
{% endif %}
- "../../../:/awx_devel"
- "../../docker-compose/supervisor.conf:/etc/supervisord.conf"
- "../../docker-compose/_sources/database.py:/etc/tower/conf.d/database.py"
@@ -58,6 +80,11 @@ services:
- "~/.kube/config:/var/lib/awx/.kube/config"
- "redis_socket_{{ container_postfix }}:/var/run/redis/:rw"
privileged: true
{% if editable_dependencies | length > 0 %}
depends_on:
init_awx:
condition: service_completed_successfully
{% endif %}
tty: true
ports:
- "{{ awx_sdb_port_start }}-{{ awx_sdb_port_end }}:{{ awx_sdb_port_start }}-{{ awx_sdb_port_end }}" # sdb-listen
@@ -304,6 +331,10 @@ services:
{% endif %}
volumes:
{% if editable_dependencies | length > 0 %}
var_lib_awx:
name: tools_var_lib_awx
{% endif %}
{# For the postgres 15 db upgrade we changed the mount name because 15 can't load a 12 DB #}
awx_db_15:
name: tools_awx_db_15
@@ -329,6 +360,7 @@ volumes:
grafana_storage:
name: tools_grafana_storage
{% endif %}
networks:
awx:
service-mesh:

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

@@ -0,0 +1,66 @@
# Editable dependencies in AWX Docker Compose Development Environment
This folder contains the symlink to editable dependencies for AWX
During the bootstrap of awx development environment we will try to crawl through the symlinks and mount (the source of the symlink) to `tools_awx_` containers and `init_awx` containers than install all the dependencies in editable mode
## How to enable/disable editable dependnecies
### Enable
Set `EDITABLE_DEPENDENCIES=true` either as an Environment Variable with before invoking `make docker-compose`
```bash
export EDITABLE_DEPENDENCIES=true
```
or during invocation of `make docker-compose`
```bash
EDITABLE_DEPENDENCIES=true make docker-compose
```
will cause the `make docker-compose-source` to template out docker-compose file with editable dependencies.
### Disable
To disable editable dependency simply `unset EDITABLE_DEPENDENCIES`
## How to add editable dependencies
Adding symlink to the directory that contains the source of the editable dependencies will cause the dependency to be mounted and installed in the docker-compose development environment.
Both relative path or absolute path will work
### Examples
I have `awx` checked out at `~/projects/src/github.com/TheRealHaoLiu/awx`
I have `django-ansible-base` checked out at `~/projects/src/github.com/TheRealHaoLiu/ansible-runner`
From root of AWX project `~/projects/src/github.com/TheRealHaoLiu/awx`
I can either do
```bash
ln -s ~/projects/src/github.com/TheRealHaoLiu/ansible-runner tools/docker-compose/editable_dependencies/
```
or
```bash
ln -s ../ansible-runner tools/docker-compose/editable_dependencies/
```
## How to remove indivisual editable dependencies
Simply removing the symlink from `tools/docker-compose/editable_dependencies` **will cause problem**!
and the volume `tools_awx_var_lib` need to be deleted with
```bash
make docker-compose-down
docker volume rm tools_awx_var_lib
```
TODO(TheRealHaoLiu): bear proof this? maybe just always delete tools_awx_var_lib?

View File

@@ -0,0 +1,4 @@
for file in `ls /editable_dependencies`; do
echo "Installing $file"
pip install -e /editable_dependencies/$file
done