diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 9f42facf99..ab84ca3bb0 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -5482,7 +5482,7 @@ class ReceptorAddressSerializer(BaseSerializer): class Meta: model = ReceptorAddress - fields = ('id', 'address', 'port', 'protocol', 'websocket_path', 'is_internal', 'instance', 'full_address') + fields = ('id', 'url', 'address', 'port', 'protocol', 'websocket_path', 'is_internal', 'instance', 'full_address') read_only = 'full_address' def get_full_address(self, obj): @@ -5557,7 +5557,7 @@ class InstanceSerializer(BaseSerializer): def get_related(self, obj): res = super(InstanceSerializer, self).get_related(obj) - res['receptor_addresses'] = self.reverse('api:receptor_addresses_list', kwargs={'pk': obj.pk}) + res['receptor_addresses'] = self.reverse('api:instance_receptor_addresses_list', kwargs={'pk': obj.pk}) res['jobs'] = self.reverse('api:instance_unified_jobs_list', kwargs={'pk': obj.pk}) res['instance_groups'] = self.reverse('api:instance_instance_groups_list', kwargs={'pk': obj.pk}) if obj.node_type in [Instance.Types.EXECUTION, Instance.Types.HOP]: diff --git a/awx/api/urls/instance.py b/awx/api/urls/instance.py index fbd29ac38e..84a3904657 100644 --- a/awx/api/urls/instance.py +++ b/awx/api/urls/instance.py @@ -10,7 +10,7 @@ from awx.api.views import ( InstanceInstanceGroupsList, InstanceHealthCheck, InstancePeersList, - ReceptorAddressesList, + InstanceReceptorAddressesList, ) from awx.api.views.instance_install_bundle import InstanceInstallBundle @@ -22,7 +22,7 @@ urls = [ re_path(r'^(?P[0-9]+)/instance_groups/$', InstanceInstanceGroupsList.as_view(), name='instance_instance_groups_list'), re_path(r'^(?P[0-9]+)/health_check/$', InstanceHealthCheck.as_view(), name='instance_health_check'), re_path(r'^(?P[0-9]+)/peers/$', InstancePeersList.as_view(), name='instance_peers_list'), - re_path(r'^(?P[0-9]+)/receptor_addresses/$', ReceptorAddressesList.as_view(), name='receptor_addresses_list'), + re_path(r'^(?P[0-9]+)/receptor_addresses/$', InstanceReceptorAddressesList.as_view(), name='instance_receptor_addresses_list'), re_path(r'^(?P[0-9]+)/install_bundle/$', InstanceInstallBundle.as_view(), name='instance_install_bundle'), ] diff --git a/awx/api/urls/receptor_address.py b/awx/api/urls/receptor_address.py new file mode 100644 index 0000000000..fe630f3da4 --- /dev/null +++ b/awx/api/urls/receptor_address.py @@ -0,0 +1,17 @@ +# Copyright (c) 2017 Ansible, Inc. +# All Rights Reserved. + +from django.urls import re_path + +from awx.api.views import ( + ReceptorAddressesList, + ReceptorAddressDetail, +) + + +urls = [ + re_path(r'^$', ReceptorAddressesList.as_view(), name='receptor_addresses_list'), + re_path(r'^(?P[0-9]+)/$', ReceptorAddressDetail.as_view(), name='receptor_address_detail'), +] + +__all__ = ['urls'] diff --git a/awx/api/urls/urls.py b/awx/api/urls/urls.py index 030ba25ede..c2218e5ed8 100644 --- a/awx/api/urls/urls.py +++ b/awx/api/urls/urls.py @@ -85,6 +85,7 @@ from .oauth2_root import urls as oauth2_root_urls from .workflow_approval_template import urls as workflow_approval_template_urls from .workflow_approval import urls as workflow_approval_urls from .analytics import urls as analytics_urls +from .receptor_address import urls as receptor_address_urls v2_urls = [ re_path(r'^$', ApiV2RootView.as_view(), name='api_v2_root_view'), @@ -155,6 +156,7 @@ v2_urls = [ re_path(r'^bulk/host_create/$', BulkHostCreateView.as_view(), name='bulk_host_create'), re_path(r'^bulk/host_delete/$', BulkHostDeleteView.as_view(), name='bulk_host_delete'), re_path(r'^bulk/job_launch/$', BulkJobLaunchView.as_view(), name='bulk_job_launch'), + re_path(r'^receptor_addresses/', include(receptor_address_urls)), ] diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index 714d8f9977..00314ec9e8 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -384,7 +384,7 @@ class InstancePeersList(SubListAPIView): relationship = 'peers' -class ReceptorAddressesList(ListCreateAPIView): +class InstanceReceptorAddressesList(ListCreateAPIView): name = _("Receptor Addresses") model = models.ReceptorAddress serializer_class = serializers.ReceptorAddressSerializer @@ -397,6 +397,18 @@ class ReceptorAddressesList(ListCreateAPIView): return super().post(request, *args, **kwargs) +class ReceptorAddressesList(ListAPIView): + name = _("Receptor Addresses") + model = models.ReceptorAddress + serializer_class = serializers.ReceptorAddressSerializer + + +class ReceptorAddressDetail(RetrieveUpdateDestroyAPIView): + name = _("Receptor Address Detail") + model = models.ReceptorAddress + serializer_class = serializers.ReceptorAddressSerializer + + class InstanceInstanceGroupsList(InstanceGroupMembershipMixin, SubListCreateAttachDetachAPIView): name = _("Instance's Instance Groups") model = models.InstanceGroup diff --git a/awx/api/views/root.py b/awx/api/views/root.py index 3a9a910e1c..65f3e0e1a6 100644 --- a/awx/api/views/root.py +++ b/awx/api/views/root.py @@ -84,6 +84,7 @@ class ApiVersionRootView(APIView): data['ping'] = reverse('api:api_v2_ping_view', request=request) data['instances'] = reverse('api:instance_list', request=request) data['instance_groups'] = reverse('api:instance_group_list', request=request) + data['receptor_addresses'] = reverse('api:receptor_addresses_list', request=request) data['config'] = reverse('api:api_v2_config_view', request=request) data['settings'] = reverse('api:setting_category_list', request=request) data['me'] = reverse('api:user_me_list', request=request) diff --git a/awx/main/access.py b/awx/main/access.py index 5b29735356..c768e74c4c 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -57,6 +57,7 @@ from awx.main.models import ( Project, ProjectUpdate, ProjectUpdateEvent, + ReceptorAddress, Role, Schedule, SystemJob, @@ -2430,6 +2431,29 @@ class InventoryUpdateEventAccess(BaseAccess): return False +class ReceptorAddressAccess(BaseAccess): + """ + I can see receptor address records whenever I can access the instance + """ + + model = ReceptorAddress + + def filtered_queryset(self): + return self.model.objects.filter(Q(instance__in=Instance.accessible_pk_qs(self.user, 'read_role'))) + + @check_superuser + def can_add(self, data): + return False + + @check_superuser + def can_change(self, obj, data): + return False + + @check_superuser + def can_delete(self, obj): + return False + + class SystemJobEventAccess(BaseAccess): """ I can only see manage System Jobs events if I'm a super user diff --git a/awx/main/management/commands/add_receptor_address.py b/awx/main/management/commands/add_receptor_address.py index 2428b3dfdb..ad23230556 100644 --- a/awx/main/management/commands/add_receptor_address.py +++ b/awx/main/management/commands/add_receptor_address.py @@ -1,8 +1,6 @@ # Copyright (c) 2015 Ansible, Inc. # All Rights Reserved -import os - from django.core.management.base import BaseCommand from django.db import transaction @@ -28,11 +26,20 @@ class Command(BaseCommand): ) def _add_address(self, **kwargs): - i = Instance.objects.get(hostname=kwargs.pop('hostname')) - kwargs['instance'] = i - ReceptorAddress.objects.create(**kwargs) + try: + instance = Instance.objects.get(hostname=kwargs.pop('hostname')) + kwargs['instance'] = instance + addr = ReceptorAddress.objects.create(**kwargs) + print(f"Successfully added receptor address {addr.get_full_address()}") + self.changed = True + except Exception as e: + self.changed = False + print(f"Error adding receptor address: {e}") @transaction.atomic def handle(self, **options): + self.changed = False address_options = {k: options[k] for k in ('hostname', 'address', 'port', 'protocol', 'websocket_path', 'is_internal')} self._add_address(**address_options) + if self.changed: + print("(changed: True)") diff --git a/awx/main/migrations/0188_inbound_ingress.py b/awx/main/migrations/0188_inbound_ingress.py index 3158aefd29..8ed17864ea 100644 --- a/awx/main/migrations/0188_inbound_ingress.py +++ b/awx/main/migrations/0188_inbound_ingress.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-10-03 18:31 +# Generated by Django 4.2.5 on 2023-10-04 06:51 from django.db import migrations, models import django.db.models.deletion @@ -22,4 +22,22 @@ class Migration(migrations.Migration): ('instance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='receptor_addresses', to='main.instance')), ], ), + migrations.AddConstraint( + model_name='receptoraddress', + constraint=models.UniqueConstraint( + condition=models.Q(('port', None)), + fields=('address', 'protocol', 'websocket_path'), + name='unique_receptor_address_no_port', + violation_error_message='Receptor address must be unique.', + ), + ), + migrations.AddConstraint( + model_name='receptoraddress', + constraint=models.UniqueConstraint( + condition=models.Q(('port', None), _negated=True), + fields=('address', 'port', 'protocol', 'websocket_path'), + name='unique_receptor_address_with_port', + violation_error_message='Receptor address must be unique.', + ), + ), ] diff --git a/awx/main/models/receptor_address.py b/awx/main/models/receptor_address.py index d4377823d7..4936f16124 100644 --- a/awx/main/models/receptor_address.py +++ b/awx/main/models/receptor_address.py @@ -1,7 +1,27 @@ from django.db import models +from django.utils.translation import gettext_lazy as _ +from awx.api.versioning import reverse +from django.db.models import Sum, Q class ReceptorAddress(models.Model): + class Meta: + app_label = 'main' + constraints = [ + models.UniqueConstraint( + fields=["address", "protocol", "websocket_path"], + condition=Q(port=None), + name="unique_receptor_address_no_port", + violation_error_message=_("Receptor address must be unique."), + ), + models.UniqueConstraint( + fields=["address", "port", "protocol", "websocket_path"], + condition=~Q(port=None), + name="unique_receptor_address_with_port", + violation_error_message=_("Receptor address must be unique."), + ), + ] + address = models.CharField(max_length=255) port = models.IntegerField(null=True) protocol = models.CharField(max_length=10) @@ -35,3 +55,6 @@ class ReceptorAddress(models.Model): return 'ws-peer' else: return None + + def get_absolute_url(self, request=None): + return reverse('api:receptor_address_detail', kwargs={'pk': self.pk}, request=request)