diff --git a/.github/workflows/promote.yml b/.github/workflows/promote.yml index 016a0c4..6cf218a 100644 --- a/.github/workflows/promote.yml +++ b/.github/workflows/promote.yml @@ -69,7 +69,7 @@ jobs: # - name: Build awxkit and upload to pypi # run: | # git reset --hard -# cd awxkit && python3 setup.py bdist_wheel +# cd awxkit && python3 setup.py sdist bdist_wheel # twine upload \ # -r ${{ env.pypi_repo }} \ # -u ${{ secrets.PYPI_USERNAME }} \ diff --git a/awx/api/generics.py b/awx/api/generics.py index 1081b02..4020bb6 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -30,8 +30,8 @@ from rest_framework.renderers import StaticHTMLRenderer from rest_framework.negotiation import DefaultContentNegotiation -from ansible_base.filters.rest_framework.field_lookup_backend import FieldLookupBackend -from ansible_base.utils.models import get_all_field_names +from ansible_base.rest_filters.rest_framework.field_lookup_backend import FieldLookupBackend +from ansible_base.lib.utils.models import get_all_field_names # AWX from awx.main.models import UnifiedJob, UnifiedJobTemplate, User, Role, Credential, WorkflowJobTemplateNode, WorkflowApprovalTemplate @@ -91,7 +91,7 @@ def post(self, request, *args, **kwargs): ret = super(LoggedLoginView, self).post(request, *args, **kwargs) if request.user.is_authenticated: logger.info(smart_str(u"User {} logged in from {}".format(self.request.user.username, request.META.get('REMOTE_ADDR', None)))) - ret.set_cookie('userLoggedIn', 'true') + ret.set_cookie('userLoggedIn', 'true', secure=getattr(settings, 'SESSION_COOKIE_SECURE', False)) ret.setdefault('X-API-Session-Cookie-Name', getattr(settings, 'SESSION_COOKIE_NAME', 'awx_sessionid')) return ret @@ -107,7 +107,7 @@ def dispatch(self, request, *args, **kwargs): original_user = getattr(request, 'user', None) ret = super(LoggedLogoutView, self).dispatch(request, *args, **kwargs) current_user = getattr(request, 'user', None) - ret.set_cookie('userLoggedIn', 'false') + ret.set_cookie('userLoggedIn', 'false', secure=getattr(settings, 'SESSION_COOKIE_SECURE', False)) if (not current_user or not getattr(current_user, 'pk', True)) and current_user != original_user: logger.info("User {} logged out.".format(original_user.username)) return ret diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 523b21a..806e46c 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -43,7 +43,7 @@ # Django-Polymorphic from polymorphic.models import PolymorphicModel -from ansible_base.utils.models import get_type_for_model +from ansible_base.lib.utils.models import get_type_for_model # AWX from awx.main.access import get_user_capabilities diff --git a/awx/api/urls/webhooks.py b/awx/api/urls/webhooks.py index b57ca13..bbbf1eb 100644 --- a/awx/api/urls/webhooks.py +++ b/awx/api/urls/webhooks.py @@ -1,10 +1,11 @@ from django.urls import re_path -from awx.api.views.webhooks import WebhookKeyView, GithubWebhookReceiver, GitlabWebhookReceiver +from awx.api.views.webhooks import WebhookKeyView, GithubWebhookReceiver, GitlabWebhookReceiver, BitbucketDcWebhookReceiver urlpatterns = [ re_path(r'^webhook_key/$', WebhookKeyView.as_view(), name='webhook_key'), re_path(r'^github/$', GithubWebhookReceiver.as_view(), name='webhook_receiver_github'), re_path(r'^gitlab/$', GitlabWebhookReceiver.as_view(), name='webhook_receiver_gitlab'), + re_path(r'^bitbucket_dc/$', BitbucketDcWebhookReceiver.as_view(), name='webhook_receiver_bitbucket_dc'), ] diff --git a/awx/api/views/webhooks.py b/awx/api/views/webhooks.py index a1d3e27..c0fa813 100644 --- a/awx/api/views/webhooks.py +++ b/awx/api/views/webhooks.py @@ -1,4 +1,4 @@ -from hashlib import sha1 +from hashlib import sha1, sha256 import hmac import logging import urllib.parse @@ -99,14 +99,31 @@ def get_event_ref(self): def get_signature(self): raise NotImplementedError + def must_check_signature(self): + return True + + def is_ignored_request(self): + return False + def check_signature(self, obj): if not obj.webhook_key: raise PermissionDenied + if not self.must_check_signature(): + logger.debug("skipping signature validation") + return + + hash_alg, expected_digest = self.get_signature() + if hash_alg == 'sha1': + mac = hmac.new(force_bytes(obj.webhook_key), msg=force_bytes(self.request.body), digestmod=sha1) + elif hash_alg == 'sha256': + mac = hmac.new(force_bytes(obj.webhook_key), msg=force_bytes(self.request.body), digestmod=sha256) + else: + logger.debug("Unsupported signature type, supported: sha1, sha256, received: {}".format(hash_alg)) + raise PermissionDenied - mac = hmac.new(force_bytes(obj.webhook_key), msg=force_bytes(self.request.body), digestmod=sha1) - logger.debug("header signature: %s", self.get_signature()) + logger.debug("header signature: %s", expected_digest) logger.debug("calculated signature: %s", force_bytes(mac.hexdigest())) - if not hmac.compare_digest(force_bytes(mac.hexdigest()), self.get_signature()): + if not hmac.compare_digest(force_bytes(mac.hexdigest()), expected_digest): raise PermissionDenied @csrf_exempt @@ -118,6 +135,10 @@ def post(self, request, *args, **kwargs): obj = self.get_object() self.check_signature(obj) + if self.is_ignored_request(): + # This was an ignored request type (e.g. ping), don't act on it + return Response({'message': _("Webhook ignored")}, status=status.HTTP_200_OK) + event_type = self.get_event_type() event_guid = self.get_event_guid() event_ref = self.get_event_ref() @@ -186,7 +207,7 @@ def get_signature(self): if hash_alg != 'sha1': logger.debug("Unsupported signature type, expected: sha1, received: {}".format(hash_alg)) raise PermissionDenied - return force_bytes(signature) + return hash_alg, force_bytes(signature) class GitlabWebhookReceiver(WebhookReceiverBase): @@ -214,15 +235,73 @@ def get_event_status_api(self): return "{}://{}/api/v4/projects/{}/statuses/{}".format(parsed.scheme, parsed.netloc, project['id'], self.get_event_ref()) - def get_signature(self): - return force_bytes(self.request.META.get('HTTP_X_GITLAB_TOKEN') or '') - def check_signature(self, obj): if not obj.webhook_key: raise PermissionDenied + token_from_request = force_bytes(self.request.META.get('HTTP_X_GITLAB_TOKEN') or '') + # GitLab only returns the secret token, not an hmac hash. Use # the hmac `compare_digest` helper function to prevent timing # analysis by attackers. - if not hmac.compare_digest(force_bytes(obj.webhook_key), self.get_signature()): + if not hmac.compare_digest(force_bytes(obj.webhook_key), token_from_request): raise PermissionDenied + + +class BitbucketDcWebhookReceiver(WebhookReceiverBase): + service = 'bitbucket_dc' + + ref_keys = { + 'repo:refs_changed': 'changes.0.toHash', + 'mirror:repo_synchronized': 'changes.0.toHash', + 'pr:opened': 'pullRequest.toRef.latestCommit', + 'pr:from_ref_updated': 'pullRequest.toRef.latestCommit', + 'pr:modified': 'pullRequest.toRef.latestCommit', + } + + def get_event_type(self): + return self.request.META.get('HTTP_X_EVENT_KEY') + + def get_event_guid(self): + return self.request.META.get('HTTP_X_REQUEST_ID') + + def get_event_status_api(self): + # https:///rest/build-status/1.0/commits/ + if self.get_event_type() not in self.ref_keys.keys(): + return + if self.get_event_ref() is None: + return + any_url = None + if 'actor' in self.request.data: + any_url = self.request.data['actor'].get('links', {}).get('self') + if any_url is None and 'repository' in self.request.data: + any_url = self.request.data['repository'].get('links', {}).get('self') + if any_url is None: + return + any_url = any_url[0].get('href') + if any_url is None: + return + parsed = urllib.parse.urlparse(any_url) + + return "{}://{}/rest/build-status/1.0/commits/{}".format(parsed.scheme, parsed.netloc, self.get_event_ref()) + + def is_ignored_request(self): + return self.get_event_type() not in [ + 'repo:refs_changed', + 'mirror:repo_synchronized', + 'pr:opened', + 'pr:from_ref_updated', + 'pr:modified', + ] + + def must_check_signature(self): + # Bitbucket does not sign ping requests... + return self.get_event_type() != 'diagnostics:ping' + + def get_signature(self): + header_sig = self.request.META.get('HTTP_X_HUB_SIGNATURE') + if not header_sig: + logger.debug("Expected signature missing from header key HTTP_X_HUB_SIGNATURE") + raise PermissionDenied + hash_alg, signature = header_sig.split('=') + return hash_alg, force_bytes(signature) diff --git a/awx/conf/models.py b/awx/conf/models.py index 9127414..e4442fc 100644 --- a/awx/conf/models.py +++ b/awx/conf/models.py @@ -7,7 +7,7 @@ # Django from django.db import models -from ansible_base.utils.models import prevent_search +from ansible_base.lib.utils.models import prevent_search # AWX from awx.main.models.base import CreatedModifiedModel diff --git a/awx/main/access.py b/awx/main/access.py index 6b57723..5b29735 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -20,7 +20,7 @@ # Django OAuth Toolkit from awx.main.models.oauth import OAuth2Application, OAuth2AccessToken -from ansible_base.utils.validation import to_python_boolean +from ansible_base.lib.utils.validation import to_python_boolean # AWX from awx.main.utils import ( diff --git a/awx/main/credential_plugins/aim.py b/awx/main/credential_plugins/aim.py index 048bd1b..2476042 100644 --- a/awx/main/credential_plugins/aim.py +++ b/awx/main/credential_plugins/aim.py @@ -58,7 +58,7 @@ 'id': 'object_property', 'label': _('Object Property'), 'type': 'string', - 'help_text': _('The property of the object to return. Default: Content Ex: Username, Address, etc.'), + 'help_text': _('The property of the object to return. Available properties: Username, Password and Address.'), }, { 'id': 'reason', @@ -111,8 +111,12 @@ def aim_backend(**kwargs): object_property = 'Content' elif object_property.lower() == 'username': object_property = 'UserName' + elif object_property.lower() == 'password': + object_property = 'Content' + elif object_property.lower() == 'address': + object_property = 'Address' elif object_property not in res: - raise KeyError('Property {} not found in object'.format(object_property)) + raise KeyError('Property {} not found in object, available properties: Username, Password and Address'.format(object_property)) else: object_property = object_property.capitalize() diff --git a/awx/main/credential_plugins/hashivault.py b/awx/main/credential_plugins/hashivault.py index 62aabad..f3dcd53 100644 --- a/awx/main/credential_plugins/hashivault.py +++ b/awx/main/credential_plugins/hashivault.py @@ -87,6 +87,20 @@ ' see https://www.vaultproject.io/docs/auth/kubernetes#configuration' ), }, + { + 'id': 'username', + 'label': _('Username'), + 'type': 'string', + 'secret': False, + 'help_text': _('Username for user authentication.'), + }, + { + 'id': 'password', + 'label': _('Password'), + 'type': 'string', + 'secret': True, + 'help_text': _('Password for user authentication.'), + }, { 'id': 'default_auth_path', 'label': _('Path to Auth'), @@ -185,9 +199,10 @@ def handle_auth(**kwargs): token = None - if kwargs.get('token'): token = kwargs['token'] + elif kwargs.get('username') and kwargs.get('password'): + token = method_auth(**kwargs, auth_param=userpass_auth(**kwargs)) elif kwargs.get('role_id') and kwargs.get('secret_id'): token = method_auth(**kwargs, auth_param=approle_auth(**kwargs)) elif kwargs.get('kubernetes_role'): @@ -195,11 +210,14 @@ def handle_auth(**kwargs): elif kwargs.get('client_cert_public') and kwargs.get('client_cert_private'): token = method_auth(**kwargs, auth_param=client_cert_auth(**kwargs)) else: - raise Exception('Either a token or AppRole, Kubernetes, or TLS authentication parameters must be set') - + raise Exception('Token, Username/Password, AppRole, Kubernetes, or TLS authentication parameters must be set') return token +def userpass_auth(**kwargs): + return {'username': kwargs['username'], 'password': kwargs['password']} + + def approle_auth(**kwargs): return {'role_id': kwargs['role_id'], 'secret_id': kwargs['secret_id']} @@ -227,11 +245,14 @@ def method_auth(**kwargs): cacert = kwargs.get('cacert', None) sess = requests.Session() + sess.mount(url, requests.adapters.HTTPAdapter(max_retries=5)) # Namespace support if kwargs.get('namespace'): sess.headers['X-Vault-Namespace'] = kwargs['namespace'] request_url = '/'.join([url, 'auth', auth_path, 'login']).rstrip('/') + if kwargs['auth_param'].get('username'): + request_url = request_url + '/' + (kwargs['username']) with CertFiles(cacert) as cert: request_kwargs['verify'] = cert # TLS client certificate support @@ -263,6 +284,7 @@ def kv_backend(**kwargs): } sess = requests.Session() + sess.mount(url, requests.adapters.HTTPAdapter(max_retries=5)) sess.headers['Authorization'] = 'Bearer {}'.format(token) # Compatibility header for older installs of Hashicorp Vault sess.headers['X-Vault-Token'] = token @@ -333,6 +355,7 @@ def ssh_backend(**kwargs): request_kwargs['json']['valid_principals'] = kwargs['valid_principals'] sess = requests.Session() + sess.mount(url, requests.adapters.HTTPAdapter(max_retries=5)) sess.headers['Authorization'] = 'Bearer {}'.format(token) if kwargs.get('namespace'): sess.headers['X-Vault-Namespace'] = kwargs['namespace'] diff --git a/awx/main/dispatch/__init__.py b/awx/main/dispatch/__init__.py index d5349e0..c0261e0 100644 --- a/awx/main/dispatch/__init__.py +++ b/awx/main/dispatch/__init__.py @@ -93,6 +93,22 @@ def close(self): self.conn.close() +def create_listener_connection(): + conf = settings.DATABASES['default'].copy() + conf['OPTIONS'] = conf.get('OPTIONS', {}).copy() + # Modify the application name to distinguish from other connections the process might use + conf['OPTIONS']['application_name'] = get_application_name(settings.CLUSTER_HOST_ID, function='listener') + + # Apply overrides specifically for the listener connection + for k, v in settings.LISTENER_DATABASES.get('default', {}).items(): + conf[k] = v + for k, v in settings.LISTENER_DATABASES.get('default', {}).get('OPTIONS', {}).items(): + conf['OPTIONS'][k] = v + + connection_data = f"dbname={conf['NAME']} host={conf['HOST']} user={conf['USER']} password={conf['PASSWORD']} port={conf['PORT']}" + return psycopg.connect(connection_data, autocommit=True, **conf['OPTIONS']) + + @contextmanager def pg_bus_conn(new_connection=False, select_timeout=None): ''' @@ -106,12 +122,7 @@ def pg_bus_conn(new_connection=False, select_timeout=None): ''' if new_connection: - conf = settings.DATABASES['default'].copy() - conf['OPTIONS'] = conf.get('OPTIONS', {}).copy() - # Modify the application name to distinguish from other connections the process might use - conf['OPTIONS']['application_name'] = get_application_name(settings.CLUSTER_HOST_ID, function='listener') - connection_data = f"dbname={conf['NAME']} host={conf['HOST']} user={conf['USER']} password={conf['PASSWORD']} port={conf['PORT']}" - conn = psycopg.connect(connection_data, autocommit=True, **conf['OPTIONS']) + conn = create_listener_connection() else: if pg_connection.connection is None: pg_connection.connect() diff --git a/awx/main/dispatch/worker/base.py b/awx/main/dispatch/worker/base.py index 2ff8752..05b51b2 100644 --- a/awx/main/dispatch/worker/base.py +++ b/awx/main/dispatch/worker/base.py @@ -214,7 +214,10 @@ def run_periodic_tasks(self): # bypasses pg_notify for scheduled tasks self.dispatch_task(body) - self.pg_is_down = False + if self.pg_is_down: + logger.info('Dispatcher listener connection established') + self.pg_is_down = False + self.listen_start = time.time() return self.scheduler.time_until_next_run() diff --git a/awx/main/migrations/0188_add_bitbucket_dc_webhook.py b/awx/main/migrations/0188_add_bitbucket_dc_webhook.py new file mode 100644 index 0000000..ae067b2 --- /dev/null +++ b/awx/main/migrations/0188_add_bitbucket_dc_webhook.py @@ -0,0 +1,52 @@ +# Generated by Django 4.2.6 on 2023-11-16 21:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('main', '0187_hop_nodes'), + ] + + operations = [ + migrations.AlterField( + model_name='job', + name='webhook_service', + field=models.CharField( + blank=True, + choices=[('github', 'GitHub'), ('gitlab', 'GitLab'), ('bitbucket_dc', 'BitBucket DataCenter')], + help_text='Service that webhook requests will be accepted from', + max_length=16, + ), + ), + migrations.AlterField( + model_name='jobtemplate', + name='webhook_service', + field=models.CharField( + blank=True, + choices=[('github', 'GitHub'), ('gitlab', 'GitLab'), ('bitbucket_dc', 'BitBucket DataCenter')], + help_text='Service that webhook requests will be accepted from', + max_length=16, + ), + ), + migrations.AlterField( + model_name='workflowjob', + name='webhook_service', + field=models.CharField( + blank=True, + choices=[('github', 'GitHub'), ('gitlab', 'GitLab'), ('bitbucket_dc', 'BitBucket DataCenter')], + help_text='Service that webhook requests will be accepted from', + max_length=16, + ), + ), + migrations.AlterField( + model_name='workflowjobtemplate', + name='webhook_service', + field=models.CharField( + blank=True, + choices=[('github', 'GitHub'), ('gitlab', 'GitLab'), ('bitbucket_dc', 'BitBucket DataCenter')], + help_text='Service that webhook requests will be accepted from', + max_length=16, + ), + ), + ] diff --git a/awx/main/migrations/_inventory_source_vars.py b/awx/main/migrations/_inventory_source_vars.py index 7660d63..779655d 100644 --- a/awx/main/migrations/_inventory_source_vars.py +++ b/awx/main/migrations/_inventory_source_vars.py @@ -76,7 +76,7 @@ def inventory_as_dict(self, inventory_source, private_data_dir): user_filters = [] old_filterables = [ ('resource_groups', 'resource_group'), - ('tags', 'tags') + ('tags', 'tags'), # locations / location would be an entry # but this would conflict with source_regions ] diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index 53b7ea6..2363543 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -6,7 +6,7 @@ from django.db import connection from django.db.models.signals import pre_delete # noqa -from ansible_base.utils.models import prevent_search +from ansible_base.lib.utils.models import prevent_search # AWX from awx.main.models.base import BaseModel, PrimordialModel, accepts_json, CLOUD_INVENTORY_SOURCES, VERBOSITY_CHOICES # noqa diff --git a/awx/main/models/ad_hoc_commands.py b/awx/main/models/ad_hoc_commands.py index 9143f30..0583e07 100644 --- a/awx/main/models/ad_hoc_commands.py +++ b/awx/main/models/ad_hoc_commands.py @@ -12,7 +12,7 @@ from django.utils.translation import gettext_lazy as _ from django.core.exceptions import ValidationError -from ansible_base.utils.models import prevent_search +from ansible_base.lib.utils.models import prevent_search # AWX from awx.api.versioning import reverse diff --git a/awx/main/models/credential/__init__.py b/awx/main/models/credential/__init__.py index 3f964ed..bae3853 100644 --- a/awx/main/models/credential/__init__.py +++ b/awx/main/models/credential/__init__.py @@ -931,6 +931,25 @@ def create(self): }, ) +ManagedCredentialType( + namespace='bitbucket_dc_token', + kind='token', + name=gettext_noop('Bitbucket Data Center HTTP Access Token'), + managed=True, + inputs={ + 'fields': [ + { + 'id': 'token', + 'label': gettext_noop('Token'), + 'type': 'string', + 'secret': True, + 'help_text': gettext_noop('This token needs to come from your user settings in Bitbucket'), + } + ], + 'required': ['token'], + }, +) + ManagedCredentialType( namespace='ascender', kind='cloud', diff --git a/awx/main/models/ha.py b/awx/main/models/ha.py index 0849023..4792d4e 100644 --- a/awx/main/models/ha.py +++ b/awx/main/models/ha.py @@ -17,7 +17,7 @@ import redis from solo.models import SingletonModel -from ansible_base.utils.models import prevent_search +from ansible_base.lib.utils.models import prevent_search # AWX from awx import __version__ as awx_application_version diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 47eba65..916f0ae 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -28,7 +28,7 @@ # REST Framework from rest_framework.exceptions import ParseError -from ansible_base.utils.models import prevent_search +from ansible_base.lib.utils.models import prevent_search # AWX from awx.api.versioning import reverse diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index a01f791..7bd52d4 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -20,7 +20,7 @@ # REST Framework from rest_framework.exceptions import ParseError -from ansible_base.utils.models import prevent_search +from ansible_base.lib.utils.models import prevent_search # AWX from awx.api.versioning import reverse diff --git a/awx/main/models/mixins.py b/awx/main/models/mixins.py index fd92b0b..210d4f4 100644 --- a/awx/main/models/mixins.py +++ b/awx/main/models/mixins.py @@ -16,7 +16,7 @@ from django.utils.crypto import get_random_string from django.utils.translation import gettext_lazy as _ -from ansible_base.utils.models import prevent_search +from ansible_base.lib.utils.models import prevent_search # AWX from awx.main.models.rbac import Role, RoleAncestorEntry @@ -527,7 +527,6 @@ def clean_custom_virtualenv(self): class RelatedJobsMixin(object): - """ This method is intended to be overwritten. Called by get_active_jobs() @@ -562,6 +561,7 @@ class Meta: SERVICES = [ ('github', "GitHub"), ('gitlab', "GitLab"), + ('bitbucket_dc', "BitBucket DataCenter"), ] webhook_service = models.CharField(max_length=16, choices=SERVICES, blank=True, help_text=_('Service that webhook requests will be accepted from')) @@ -622,6 +622,7 @@ def update_webhook_status(self, status): service_header = { 'github': ('Authorization', 'token {}'), 'gitlab': ('PRIVATE-TOKEN', '{}'), + 'bitbucket_dc': ('Authorization', 'Bearer {}'), } service_statuses = { 'github': { @@ -639,6 +640,14 @@ def update_webhook_status(self, status): 'error': 'failed', # GitLab doesn't have an 'error' status distinct from 'failed' :( 'canceled': 'canceled', }, + 'bitbucket_dc': { + 'pending': 'INPROGRESS', # Bitbucket DC doesn't have any other statuses distinct from INPROGRESS, SUCCESSFUL, FAILED :( + 'running': 'INPROGRESS', + 'successful': 'SUCCESSFUL', + 'failed': 'FAILED', + 'error': 'FAILED', + 'canceled': 'FAILED', + }, } statuses = service_statuses[self.webhook_service] @@ -647,11 +656,18 @@ def update_webhook_status(self, status): return try: license_type = get_licenser().validate().get('license_type') - data = { - 'state': statuses[status], - 'context': 'ansible/awx' if license_type == 'open' else 'ansible/tower', - 'target_url': self.get_ui_url(), - } + if self.webhook_service == 'bitbucket_dc': + data = { + 'state': statuses[status], + 'key': 'ansible/awx' if license_type == 'open' else 'ansible/tower', + 'url': self.get_ui_url(), + } + else: + data = { + 'state': statuses[status], + 'context': 'ansible/awx' if license_type == 'open' else 'ansible/tower', + 'target_url': self.get_ui_url(), + } k, v = service_header[self.webhook_service] headers = {k: v.format(self.webhook_credential.get_input('token')), 'Content-Type': 'application/json'} response = requests.post(status_api, data=json.dumps(data), headers=headers, timeout=30) diff --git a/awx/main/models/notifications.py b/awx/main/models/notifications.py index 4b5ce89..aa72f98 100644 --- a/awx/main/models/notifications.py +++ b/awx/main/models/notifications.py @@ -15,7 +15,7 @@ from jinja2 import sandbox, ChainableUndefined from jinja2.exceptions import TemplateSyntaxError, UndefinedError, SecurityError -from ansible_base.utils.models import prevent_search +from ansible_base.lib.utils.models import prevent_search # AWX from awx.api.versioning import reverse diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index 83696d1..a169fee 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -30,7 +30,7 @@ # Django-Polymorphic from polymorphic.models import PolymorphicModel -from ansible_base.utils.models import prevent_search, get_type_for_model +from ansible_base.lib.utils.models import prevent_search, get_type_for_model # AWX from awx.main.models.base import CommonModelNameNotUnique, PasswordFieldsModel, NotificationFieldsModel diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py index 5f0cbff..9e8eb1a 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -23,7 +23,7 @@ from jinja2 import sandbox from jinja2.exceptions import TemplateSyntaxError, UndefinedError, SecurityError -from ansible_base.utils.models import prevent_search +from ansible_base.lib.utils.models import prevent_search # AWX from awx.api.versioning import reverse diff --git a/awx/main/scheduler/task_manager.py b/awx/main/scheduler/task_manager.py index 18ebec4..d064549 100644 --- a/awx/main/scheduler/task_manager.py +++ b/awx/main/scheduler/task_manager.py @@ -17,7 +17,7 @@ from django.conf import settings from django.contrib.contenttypes.models import ContentType -from ansible_base.utils.models import get_type_for_model +from ansible_base.lib.utils.models import get_type_for_model # AWX from awx.main.dispatch.reaper import reap_job diff --git a/awx/main/tests/functional/api/test_survey_spec.py b/awx/main/tests/functional/api/test_survey_spec.py index 97a7f65..ec20806 100644 --- a/awx/main/tests/functional/api/test_survey_spec.py +++ b/awx/main/tests/functional/api/test_survey_spec.py @@ -2,7 +2,7 @@ import pytest import json -from ansible_base.utils.models import get_type_for_model +from ansible_base.lib.utils.models import get_type_for_model from awx.api.versioning import reverse from awx.main.models.jobs import JobTemplate, Job diff --git a/awx/main/tests/functional/test_credential.py b/awx/main/tests/functional/test_credential.py index d61f2e0..c018e73 100644 --- a/awx/main/tests/functional/test_credential.py +++ b/awx/main/tests/functional/test_credential.py @@ -81,6 +81,7 @@ def test_default_cred_types(): 'aws_secretsmanager_credential', 'azure_kv', 'azure_rm', + 'bitbucket_dc_token', 'centrify_vault_kv', 'conjur', 'controller', diff --git a/awx/main/tests/functional/test_credential_plugins.py b/awx/main/tests/functional/test_credential_plugins.py index 9d199c3..3ee29e9 100644 --- a/awx/main/tests/functional/test_credential_plugins.py +++ b/awx/main/tests/functional/test_credential_plugins.py @@ -60,6 +60,13 @@ def test_hashivault_client_cert_auth_no_role(): assert res == expected_res +def test_hashivault_userpass_auth(): + kwargs = {'username': 'the_username', 'password': 'the_password'} + expected_res = {'username': 'the_username', 'password': 'the_password'} + res = hashivault.userpass_auth(**kwargs) + assert res == expected_res + + def test_hashivault_handle_auth_token(): kwargs = { 'token': 'the_token', diff --git a/awx/main/tests/unit/api/test_filters.py b/awx/main/tests/unit/api/test_filters.py index 0066fdd..29c2e3a 100644 --- a/awx/main/tests/unit/api/test_filters.py +++ b/awx/main/tests/unit/api/test_filters.py @@ -5,7 +5,7 @@ # Django from rest_framework.exceptions import PermissionDenied -from ansible_base.filters.rest_framework.field_lookup_backend import FieldLookupBackend +from ansible_base.rest_filters.rest_framework.field_lookup_backend import FieldLookupBackend from awx.main.models import ( AdHocCommand, diff --git a/awx/main/tests/unit/utils/test_common.py b/awx/main/tests/unit/utils/test_common.py index 1d960c4..3114fda 100644 --- a/awx/main/tests/unit/utils/test_common.py +++ b/awx/main/tests/unit/utils/test_common.py @@ -12,7 +12,7 @@ from rest_framework.exceptions import ParseError -from ansible_base.utils.models import get_type_for_model +from ansible_base.lib.utils.models import get_type_for_model from awx.main.utils import common from awx.api.validators import HostnameRegexValidator diff --git a/awx/main/tests/unit/utils/test_filters.py b/awx/main/tests/unit/utils/test_filters.py index ad11150..55dd8dd 100644 --- a/awx/main/tests/unit/utils/test_filters.py +++ b/awx/main/tests/unit/utils/test_filters.py @@ -69,7 +69,7 @@ def __init__(self): @mock.patch('awx.main.utils.filters.get_model', return_value=mockHost()) class TestSmartFilterQueryFromString: @mock.patch( - 'ansible_base.filters.rest_framework.field_lookup_backend.get_fields_from_path', lambda model, path: ([model], path) + 'ansible_base.rest_filters.rest_framework.field_lookup_backend.get_fields_from_path', lambda model, path: ([model], path) ) # disable field filtering, because a__b isn't a real Host field @pytest.mark.parametrize( "filter_string,q_expected", diff --git a/awx/main/utils/filters.py b/awx/main/utils/filters.py index 682938c..6aa882a 100644 --- a/awx/main/utils/filters.py +++ b/awx/main/utils/filters.py @@ -161,7 +161,7 @@ def __init__(self, t): else: # detect loops and restrict access to sensitive fields # this import is intentional here to avoid a circular import - from ansible_base.filters.rest_framework.field_lookup_backend import FieldLookupBackend + from ansible_base.rest_filters.rest_framework.field_lookup_backend import FieldLookupBackend FieldLookupBackend().get_field_from_lookup(Host, k) kwargs[k] = v diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index b12c547..74f29fa 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -40,6 +40,18 @@ } } +# Special database overrides for dispatcher connections listening to pg_notify +LISTENER_DATABASES = { + 'default': { + 'OPTIONS': { + 'keepalives': 1, + 'keepalives_idle': 5, + 'keepalives_interval': 5, + 'keepalives_count': 5, + }, + } +} + # Whether or not the deployment is a K8S-based deployment # In K8S-based deployments, instances have zero capacity - all playbook # automation is intended to flow through defined Container Groups that @@ -343,7 +355,7 @@ 'awx.ui', 'awx.sso', 'solo', - 'ansible_base', + 'ansible_base.rest_filters', ] INTERNAL_IPS = ('127.0.0.1',) @@ -1068,9 +1080,10 @@ # django-ansible-base -ANSIBLE_BASE_FEATURES = {'AUTHENTICATION': False, 'SWAGGER': False, 'FILTERING': True} +ANSIBLE_BASE_TEAM_MODEL = 'main.Team' +ANSIBLE_BASE_ORGANIZATION_MODEL = 'main.Organization' -from ansible_base import settings # noqa: E402 +from ansible_base.lib import dynamic_config # noqa: E402 -settings_file = os.path.join(os.path.dirname(settings.__file__), 'dynamic_settings.py') +settings_file = os.path.join(os.path.dirname(dynamic_config.__file__), 'dynamic_settings.py') include(settings_file) diff --git a/awx/settings/development.py b/awx/settings/development.py index 25ec408..bdf882c 100644 --- a/awx/settings/development.py +++ b/awx/settings/development.py @@ -21,7 +21,7 @@ from .defaults import * # NOQA # awx-manage shell_plus --notebook -NOTEBOOK_ARGUMENTS = ['--NotebookApp.token=', '--ip', '0.0.0.0', '--port', '8888', '--allow-root', '--no-browser'] +NOTEBOOK_ARGUMENTS = ['--NotebookApp.token=', '--ip', '0.0.0.0', '--port', '9888', '--allow-root', '--no-browser'] # print SQL queries in shell_plus SHELL_PLUS_PRINT_SQL = False diff --git a/awx/sso/backends.py b/awx/sso/backends.py index 82f5387..bd4cb6c 100644 --- a/awx/sso/backends.py +++ b/awx/sso/backends.py @@ -68,7 +68,6 @@ def __init__(self, prefix='AUTH_LDAP_', defaults={}): class LDAPBackend(BaseLDAPBackend): - """ Custom LDAP backend for AWX. """ diff --git a/awx/sso/views.py b/awx/sso/views.py index c4ecdc7..c23ee44 100644 --- a/awx/sso/views.py +++ b/awx/sso/views.py @@ -38,7 +38,7 @@ def dispatch(self, request, *args, **kwargs): response = super(CompleteView, self).dispatch(request, *args, **kwargs) if self.request.user and self.request.user.is_authenticated: logger.info(smart_str(u"User {} logged in".format(self.request.user.username))) - response.set_cookie('userLoggedIn', 'true') + response.set_cookie('userLoggedIn', 'true', secure=getattr(settings, 'SESSION_COOKIE_SECURE', False)) response.setdefault('X-API-Session-Cookie-Name', getattr(settings, 'SESSION_COOKIE_NAME', 'awx_sessionid')) return response diff --git a/awx/ui/src/screens/SubscriptionUsage/SubscriptionUsageChart.js b/awx/ui/src/screens/SubscriptionUsage/SubscriptionUsageChart.js index 134447d..19103d2 100644 --- a/awx/ui/src/screens/SubscriptionUsage/SubscriptionUsageChart.js +++ b/awx/ui/src/screens/SubscriptionUsage/SubscriptionUsageChart.js @@ -1,6 +1,5 @@ import React, { useCallback, useEffect, useState } from 'react'; import styled from 'styled-components'; - import { t } from '@lingui/macro'; import { Card, @@ -47,7 +46,7 @@ function SubscriptionUsageChart() { const [periodSelection, setPeriodSelection] = useState('year'); const userProfile = useUserProfile(); - const calculateDateRange = () => { + const calculateDateRange = useCallback(() => { const today = new Date(); let date = ''; switch (periodSelection) { @@ -77,7 +76,7 @@ function SubscriptionUsageChart() { break; } return date; - }; + }, [periodSelection]); const { isLoading, @@ -89,7 +88,7 @@ function SubscriptionUsageChart() { calculateDateRange() ); return data.data.results; - }, [periodSelection]), + }, [calculateDateRange]), [] ); diff --git a/awx/ui/src/screens/Template/shared/WebhookSubForm.js b/awx/ui/src/screens/Template/shared/WebhookSubForm.js index ed5cf7a..0f64ffd 100644 --- a/awx/ui/src/screens/Template/shared/WebhookSubForm.js +++ b/awx/ui/src/screens/Template/shared/WebhookSubForm.js @@ -112,6 +112,12 @@ function WebhookSubForm({ templateType }) { label: t`GitLab`, isDisabled: false, }, + { + value: 'bitbucket_dc', + key: 'bitbucket_dc', + label: t`Bitbucket Data Center`, + isDisabled: false, + }, ]; if (error || webhookKeyError) { diff --git a/awxkit/awxkit/cli/docs/source/conf.py b/awxkit/awxkit/cli/docs/source/conf.py index db66c62..490ddb4 100644 --- a/awxkit/awxkit/cli/docs/source/conf.py +++ b/awxkit/awxkit/cli/docs/source/conf.py @@ -18,7 +18,7 @@ # -- Project information ----------------------------------------------------- project = 'AWX CLI' -copyright = '2019, Ansible by Red Hat' +copyright = '2024, Ansible by Red Hat' author = 'Ansible by Red Hat' diff --git a/licenses/django-filter.txt b/licenses/django-filter.txt deleted file mode 100644 index 4201174..0000000 --- a/licenses/django-filter.txt +++ /dev/null @@ -1,24 +0,0 @@ -Copyright (c) Alex Gaynor and individual contributors. -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. - * The names of its contributors may not 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 OWNER 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. \ No newline at end of file diff --git a/licenses/drf-spectacular.txt b/licenses/drf-spectacular.txt deleted file mode 100644 index 0b55a2c..0000000 --- a/licenses/drf-spectacular.txt +++ /dev/null @@ -1,30 +0,0 @@ -Copyright © 2011-present, Encode OSS Ltd. -Copyright © 2019-2021, T. Franzel , Cashlink Technologies GmbH. -Copyright © 2021-present, T. Franzel . - -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. - -* 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. \ No newline at end of file diff --git a/licenses/tabulate.txt b/licenses/tabulate.txt deleted file mode 100644 index f23421b..0000000 --- a/licenses/tabulate.txt +++ /dev/null @@ -1,20 +0,0 @@ -Copyright (c) 2011-2020 Sergey Astanin and 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. \ No newline at end of file diff --git a/licenses/uritemplate.txt b/licenses/uritemplate.txt deleted file mode 100644 index 7a4a3ea..0000000 --- a/licenses/uritemplate.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file diff --git a/requirements/requirements.txt b/requirements/requirements.txt index f7ba93a..305091a 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -111,19 +111,15 @@ django==4.2.6 # django-cors-headers # django-crum # django-extensions - # django-filter # django-guid # django-oauth-toolkit # django-polymorphic # django-solo # djangorestframework - # drf-spectacular # social-auth-app-django # via -r /awx_devel/requirements/requirements_git.txt django-auth-ldap==4.1.0 - # via - # -r /awx_devel/requirements/requirements.in - # django-ansible-base + # via -r /awx_devel/requirements/requirements.in django-cors-headers==3.13.0 # via -r /awx_devel/requirements/requirements.in django-crum==0.7.9 @@ -132,8 +128,6 @@ django-crum==0.7.9 # django-ansible-base django-extensions==3.2.1 # via -r /awx_devel/requirements/requirements.in -django-filter==23.5 - # via django-ansible-base django-guid==3.2.1 # via -r /awx_devel/requirements/requirements.in django-oauth-toolkit==1.7.1 @@ -146,18 +140,17 @@ django-polymorphic==3.1.0 django-solo==2.0.0 # via -r /awx_devel/requirements/requirements.in django-split-settings==1.0.0 - # via -r /awx_devel/requirements/requirements.in + # via + # -r /awx_devel/requirements/requirements.in + # django-ansible-base djangorestframework==3.14.0 # via # -r /awx_devel/requirements/requirements.in # django-ansible-base - # drf-spectacular djangorestframework-yaml==2.0.0 # via -r /awx_devel/requirements/requirements.in docutils==0.19 # via python-daemon -drf-spectacular==0.26.5 - # via django-ansible-base ecdsa==0.18.0 # via python-jose enum-compat==0.0.3 @@ -197,9 +190,7 @@ incremental==22.10.0 inflect==6.0.2 # via jaraco-text inflection==0.5.1 - # via - # django-ansible-base - # drf-spectacular + # via django-ansible-base irc==20.1.0 # via -r /awx_devel/requirements/requirements.in isodate==0.6.1 @@ -234,9 +225,7 @@ jmespath==1.0.1 json-log-formatter==0.5.1 # via -r /awx_devel/requirements/requirements.in jsonschema==4.17.3 - # via - # -r /awx_devel/requirements/requirements.in - # drf-spectacular + # via -r /awx_devel/requirements/requirements.in jwcrypto==1.4.2 # via django-oauth-toolkit kubernetes==25.3.0 @@ -320,6 +309,7 @@ pygerduty==0.38.3 pyjwt==2.6.0 # via # adal + # django-ansible-base # social-auth-core # twilio pyopenssl==23.2.0 @@ -351,7 +341,6 @@ python-jose==3.3.0 python-ldap==3.4.3 # via # -r /awx_devel/requirements/requirements.in - # django-ansible-base # django-auth-ldap python-string-utils==1.0.0 # via openshift @@ -359,9 +348,7 @@ python-tss-sdk==1.2.1 # via -r /awx_devel/requirements/requirements.in python3-openid==3.2.0 # via social-auth-core - # via - # -r /awx_devel/requirements/requirements_git.txt - # django-ansible-base + # via -r /awx_devel/requirements/requirements_git.txt pytz==2022.6 # via # djangorestframework @@ -373,7 +360,6 @@ pyyaml==6.0.1 # -r /awx_devel/requirements/requirements.in # ansible-runner # djangorestframework-yaml - # drf-spectacular # kubernetes # receptorctl receptorctl==1.4.2 @@ -386,6 +372,7 @@ requests==2.31.0 # adal # azure-core # azure-keyvault + # django-ansible-base # django-oauth-toolkit # kubernetes # msrest @@ -434,9 +421,7 @@ slack-sdk==3.19.4 smmap==5.0.0 # via gitdb social-auth-app-django==5.4.0 - # via - # -r /awx_devel/requirements/requirements.in - # django-ansible-base + # via -r /awx_devel/requirements/requirements.in social-auth-core[openidconnect]==4.4.2 # via # -r /awx_devel/requirements/requirements.in @@ -445,8 +430,6 @@ sqlparse==0.4.4 # via # -r /awx_devel/requirements/requirements.in # django -tabulate==0.9.0 - # via django-ansible-base tacacs-plus==1.0 # via -r /awx_devel/requirements/requirements.in tempora==5.1.0 @@ -470,9 +453,7 @@ typing-extensions==4.4.0 # setuptools-rust # setuptools-scm # twisted -uritemplate==4.1.1 - # via drf-spectacular -urllib3==1.26.18 +urllib3==1.26.17 # via # botocore # kubernetes diff --git a/requirements/requirements_git.txt b/requirements/requirements_git.txt index fdcab86..19886ca 100644 --- a/requirements/requirements_git.txt +++ b/requirements/requirements_git.txt @@ -5,4 +5,4 @@ git+https://github.com/ansible/ansible-runner.git@devel#egg=ansible-runner # specifically need https://github.com/robgolding/django-radius/pull/27 git+https://github.com/ansible/django-radius.git@develop#egg=django-radius git+https://github.com/ansible/python3-saml.git@devel#egg=python3-saml -git+https://github.com/ansible/django-ansible-base.git@devel#egg=django-ansible-base +django-ansible-base @ git+https://github.com/ansible/django-ansible-base@devel#egg=django-ansible-base[rest_filters,jwt_consumer] diff --git a/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 b/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 index 670b43c..c933443 100644 --- a/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 +++ b/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 @@ -35,6 +35,9 @@ services: links: - postgres - redis_{{ container_postfix }} + networks: + - awx + - service-mesh working_dir: "/awx_devel" volumes: - "../../../:/awx_devel" @@ -66,7 +69,7 @@ services: {% if control_plane_node_count|int == 1 %} - "6899:6899" - "8080:8080" # unused but mapped for debugging - - "8888:8888" # jupyter notebook + - "${AWX_JUPYTER_PORT:-9888}:9888" # jupyter notebook - "8013:8013" # http - "8043:8043" # https - "2222:2222" # receptor foo node @@ -78,6 +81,8 @@ services: volumes: - "../../redis/redis.conf:/usr/local/etc/redis/redis.conf:Z" - "redis_socket_{{ container_postfix }}:/var/run/redis/:rw" + networks: + - awx entrypoint: ["redis-server"] command: ["/usr/local/etc/redis/redis.conf"] {% endfor %} @@ -87,6 +92,8 @@ services: user: "{{ ansible_user_uid }}" volumes: - "./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:Z" + networks: + - awx ports: - "8013:8013" - "8043:8043" @@ -103,6 +110,8 @@ services: container_name: tools_keycloak_1 hostname: keycloak user: "{{ ansible_user_uid }}" + networks: + - awx ports: - "8443:8443" environment: @@ -120,6 +129,8 @@ services: container_name: tools_ldap_1 hostname: ldap user: "{{ ansible_user_uid }}" + networks: + - awx ports: - "389:1389" - "636:1636" @@ -142,6 +153,8 @@ services: image: splunk/splunk:latest container_name: tools_splunk_1 hostname: splunk + networks: + - awx ports: - "8000:8000" - "8089:8089" @@ -155,6 +168,8 @@ services: image: prom/prometheus:latest container_name: tools_prometheus_1 hostname: prometheus + networks: + - awx ports: - "9090:9090" volumes: @@ -170,6 +185,8 @@ services: image: grafana/grafana-enterprise:latest container_name: tools_grafana_1 hostname: grafana + networks: + - awx ports: - "3001:3000" volumes: @@ -206,11 +223,17 @@ services: POSTGRES_PASSWORD: {{ pg_password }} volumes: - "awx_db:/var/lib/postgresql/data" + networks: + - awx + ports: + - "${AWX_PG_PORT:-5441}:5432" {% if enable_pgbouncer|bool %} pgbouncer: image: bitnami/pgbouncer:latest container_name: tools_pgbouncer_1 hostname: pgbouncer + networks: + - awx environment: POSTGRESQL_USERNAME: {{ pg_username }} POSTGRESQL_DATABASE: {{ pg_database }} @@ -234,6 +257,8 @@ services: command: 'receptor --config /etc/receptor/receptor.conf' links: - awx_1 + networks: + - awx ports: - "5555:5555" volumes: @@ -249,6 +274,8 @@ services: RECEPTORCTL_SOCKET: {{ receptor_socket_file }} links: - receptor-hop + networks: + - awx volumes: - "../../../:/awx_devel" # not used, but mounted so that any in-place installs can be used for whole cluster - "../../docker-compose/_sources/receptor/receptor-worker-{{ loop.index }}.conf:/etc/receptor/receptor.conf" @@ -263,6 +290,8 @@ services: container_name: tools_vault_1 command: server hostname: vault + networks: + - awx ports: - "1234:1234" environment: @@ -305,8 +334,11 @@ volumes: grafana_storage: name: tools_grafana_storage {% endif %} -{% if minikube_container_group|bool %} networks: + awx: + service-mesh: + name: service-mesh +{% if minikube_container_group|bool %} default: external: name: minikube diff --git a/tools/docker-compose/ansible/roles/sources/templates/nginx.locations.conf.j2 b/tools/docker-compose/ansible/roles/sources/templates/nginx.locations.conf.j2 index be8496e..ca82f92 100644 --- a/tools/docker-compose/ansible/roles/sources/templates/nginx.locations.conf.j2 +++ b/tools/docker-compose/ansible/roles/sources/templates/nginx.locations.conf.j2 @@ -34,7 +34,7 @@ location {{ (ingress_path + '/websocket').replace('//', '/') }} { location {{ ingress_path }} { # Add trailing / if missing - rewrite ^(.*[^/])$ $1/ permanent; + rewrite ^(.*)$http_host(.*[^/])$ $1$http_host$2/ permanent; uwsgi_read_timeout 120s; uwsgi_pass uwsgi; include /etc/nginx/uwsgi_params;