mirror of
https://github.com/ansible/awx.git
synced 2026-04-19 17:00:20 -02:30
Merge pull request #7817 from ryanpetrello/galaxy-credentials
Support Organization-scoped Galaxy installs Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
@@ -1269,6 +1269,7 @@ class OrganizationSerializer(BaseSerializer):
|
|||||||
object_roles = self.reverse('api:organization_object_roles_list', kwargs={'pk': obj.pk}),
|
object_roles = self.reverse('api:organization_object_roles_list', kwargs={'pk': obj.pk}),
|
||||||
access_list = self.reverse('api:organization_access_list', kwargs={'pk': obj.pk}),
|
access_list = self.reverse('api:organization_access_list', kwargs={'pk': obj.pk}),
|
||||||
instance_groups = self.reverse('api:organization_instance_groups_list', kwargs={'pk': obj.pk}),
|
instance_groups = self.reverse('api:organization_instance_groups_list', kwargs={'pk': obj.pk}),
|
||||||
|
galaxy_credentials = self.reverse('api:organization_galaxy_credentials_list', kwargs={'pk': obj.pk}),
|
||||||
))
|
))
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@@ -2536,10 +2537,11 @@ class CredentialTypeSerializer(BaseSerializer):
|
|||||||
class CredentialSerializer(BaseSerializer):
|
class CredentialSerializer(BaseSerializer):
|
||||||
show_capabilities = ['edit', 'delete', 'copy', 'use']
|
show_capabilities = ['edit', 'delete', 'copy', 'use']
|
||||||
capabilities_prefetch = ['admin', 'use']
|
capabilities_prefetch = ['admin', 'use']
|
||||||
|
managed_by_tower = serializers.ReadOnlyField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Credential
|
model = Credential
|
||||||
fields = ('*', 'organization', 'credential_type', 'inputs', 'kind', 'cloud', 'kubernetes')
|
fields = ('*', 'organization', 'credential_type', 'managed_by_tower', 'inputs', 'kind', 'cloud', 'kubernetes')
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'credential_type': {
|
'credential_type': {
|
||||||
'label': _('Credential Type'),
|
'label': _('Credential Type'),
|
||||||
@@ -2603,6 +2605,13 @@ class CredentialSerializer(BaseSerializer):
|
|||||||
|
|
||||||
return summary_dict
|
return summary_dict
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
if self.instance and self.instance.managed_by_tower:
|
||||||
|
raise PermissionDenied(
|
||||||
|
detail=_("Modifications not allowed for managed credentials")
|
||||||
|
)
|
||||||
|
return super(CredentialSerializer, self).validate(attrs)
|
||||||
|
|
||||||
def get_validation_exclusions(self, obj=None):
|
def get_validation_exclusions(self, obj=None):
|
||||||
ret = super(CredentialSerializer, self).get_validation_exclusions(obj)
|
ret = super(CredentialSerializer, self).get_validation_exclusions(obj)
|
||||||
for field in ('credential_type', 'inputs'):
|
for field in ('credential_type', 'inputs'):
|
||||||
@@ -2610,6 +2619,17 @@ class CredentialSerializer(BaseSerializer):
|
|||||||
ret.remove(field)
|
ret.remove(field)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
def validate_organization(self, org):
|
||||||
|
if (
|
||||||
|
self.instance and
|
||||||
|
self.instance.credential_type.kind == 'galaxy' and
|
||||||
|
org is None
|
||||||
|
):
|
||||||
|
raise serializers.ValidationError(_(
|
||||||
|
"Galaxy credentials must be owned by an Organization."
|
||||||
|
))
|
||||||
|
return org
|
||||||
|
|
||||||
def validate_credential_type(self, credential_type):
|
def validate_credential_type(self, credential_type):
|
||||||
if self.instance and credential_type.pk != self.instance.credential_type.pk:
|
if self.instance and credential_type.pk != self.instance.credential_type.pk:
|
||||||
for related_objects in (
|
for related_objects in (
|
||||||
@@ -2674,6 +2694,14 @@ class CredentialSerializerCreate(CredentialSerializer):
|
|||||||
if attrs.get('team'):
|
if attrs.get('team'):
|
||||||
attrs['organization'] = attrs['team'].organization
|
attrs['organization'] = attrs['team'].organization
|
||||||
|
|
||||||
|
if (
|
||||||
|
attrs['credential_type'].kind == 'galaxy' and
|
||||||
|
list(owner_fields) != ['organization']
|
||||||
|
):
|
||||||
|
raise serializers.ValidationError({"organization": _(
|
||||||
|
"Galaxy credentials must be owned by an Organization."
|
||||||
|
)})
|
||||||
|
|
||||||
return super(CredentialSerializerCreate, self).validate(attrs)
|
return super(CredentialSerializerCreate, self).validate(attrs)
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from awx.api.views import (
|
|||||||
OrganizationNotificationTemplatesSuccessList,
|
OrganizationNotificationTemplatesSuccessList,
|
||||||
OrganizationNotificationTemplatesApprovalList,
|
OrganizationNotificationTemplatesApprovalList,
|
||||||
OrganizationInstanceGroupsList,
|
OrganizationInstanceGroupsList,
|
||||||
|
OrganizationGalaxyCredentialsList,
|
||||||
OrganizationObjectRolesList,
|
OrganizationObjectRolesList,
|
||||||
OrganizationAccessList,
|
OrganizationAccessList,
|
||||||
OrganizationApplicationList,
|
OrganizationApplicationList,
|
||||||
@@ -49,6 +50,7 @@ urls = [
|
|||||||
url(r'^(?P<pk>[0-9]+)/notification_templates_approvals/$', OrganizationNotificationTemplatesApprovalList.as_view(),
|
url(r'^(?P<pk>[0-9]+)/notification_templates_approvals/$', OrganizationNotificationTemplatesApprovalList.as_view(),
|
||||||
name='organization_notification_templates_approvals_list'),
|
name='organization_notification_templates_approvals_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/instance_groups/$', OrganizationInstanceGroupsList.as_view(), name='organization_instance_groups_list'),
|
url(r'^(?P<pk>[0-9]+)/instance_groups/$', OrganizationInstanceGroupsList.as_view(), name='organization_instance_groups_list'),
|
||||||
|
url(r'^(?P<pk>[0-9]+)/galaxy_credentials/$', OrganizationGalaxyCredentialsList.as_view(), name='organization_galaxy_credentials_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/object_roles/$', OrganizationObjectRolesList.as_view(), name='organization_object_roles_list'),
|
url(r'^(?P<pk>[0-9]+)/object_roles/$', OrganizationObjectRolesList.as_view(), name='organization_object_roles_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/access_list/$', OrganizationAccessList.as_view(), name='organization_access_list'),
|
url(r'^(?P<pk>[0-9]+)/access_list/$', OrganizationAccessList.as_view(), name='organization_access_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/applications/$', OrganizationApplicationList.as_view(), name='organization_applications_list'),
|
url(r'^(?P<pk>[0-9]+)/applications/$', OrganizationApplicationList.as_view(), name='organization_applications_list'),
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ from awx.api.views.organization import ( # noqa
|
|||||||
OrganizationNotificationTemplatesSuccessList,
|
OrganizationNotificationTemplatesSuccessList,
|
||||||
OrganizationNotificationTemplatesApprovalList,
|
OrganizationNotificationTemplatesApprovalList,
|
||||||
OrganizationInstanceGroupsList,
|
OrganizationInstanceGroupsList,
|
||||||
|
OrganizationGalaxyCredentialsList,
|
||||||
OrganizationAccessList,
|
OrganizationAccessList,
|
||||||
OrganizationObjectRolesList,
|
OrganizationObjectRolesList,
|
||||||
)
|
)
|
||||||
@@ -1355,6 +1356,13 @@ class CredentialDetail(RetrieveUpdateDestroyAPIView):
|
|||||||
model = models.Credential
|
model = models.Credential
|
||||||
serializer_class = serializers.CredentialSerializer
|
serializer_class = serializers.CredentialSerializer
|
||||||
|
|
||||||
|
def destroy(self, request, *args, **kwargs):
|
||||||
|
instance = self.get_object()
|
||||||
|
if instance.managed_by_tower:
|
||||||
|
raise PermissionDenied(detail=_("Deletion not allowed for managed credentials"))
|
||||||
|
return super(CredentialDetail, self).destroy(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class CredentialActivityStreamList(SubListAPIView):
|
class CredentialActivityStreamList(SubListAPIView):
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import logging
|
|||||||
# Django
|
# Django
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
# AWX
|
# AWX
|
||||||
from awx.main.models import (
|
from awx.main.models import (
|
||||||
@@ -20,7 +21,8 @@ from awx.main.models import (
|
|||||||
Role,
|
Role,
|
||||||
User,
|
User,
|
||||||
Team,
|
Team,
|
||||||
InstanceGroup
|
InstanceGroup,
|
||||||
|
Credential
|
||||||
)
|
)
|
||||||
from awx.api.generics import (
|
from awx.api.generics import (
|
||||||
ListCreateAPIView,
|
ListCreateAPIView,
|
||||||
@@ -42,7 +44,8 @@ from awx.api.serializers import (
|
|||||||
RoleSerializer,
|
RoleSerializer,
|
||||||
NotificationTemplateSerializer,
|
NotificationTemplateSerializer,
|
||||||
InstanceGroupSerializer,
|
InstanceGroupSerializer,
|
||||||
ProjectSerializer, JobTemplateSerializer, WorkflowJobTemplateSerializer
|
ProjectSerializer, JobTemplateSerializer, WorkflowJobTemplateSerializer,
|
||||||
|
CredentialSerializer
|
||||||
)
|
)
|
||||||
from awx.api.views.mixin import (
|
from awx.api.views.mixin import (
|
||||||
RelatedJobsPreventDeleteMixin,
|
RelatedJobsPreventDeleteMixin,
|
||||||
@@ -214,6 +217,20 @@ class OrganizationInstanceGroupsList(SubListAttachDetachAPIView):
|
|||||||
relationship = 'instance_groups'
|
relationship = 'instance_groups'
|
||||||
|
|
||||||
|
|
||||||
|
class OrganizationGalaxyCredentialsList(SubListAttachDetachAPIView):
|
||||||
|
|
||||||
|
model = Credential
|
||||||
|
serializer_class = CredentialSerializer
|
||||||
|
parent_model = Organization
|
||||||
|
relationship = 'galaxy_credentials'
|
||||||
|
|
||||||
|
def is_valid_relation(self, parent, sub, created=False):
|
||||||
|
if sub.kind != 'galaxy_api_token':
|
||||||
|
return {'msg': _(
|
||||||
|
f"Credential must be a Galaxy credential, not {sub.credential_type.name}."
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
class OrganizationAccessList(ResourceAccessList):
|
class OrganizationAccessList(ResourceAccessList):
|
||||||
|
|
||||||
model = User # needs to be User for AccessLists's
|
model = User # needs to be User for AccessLists's
|
||||||
|
|||||||
@@ -1103,11 +1103,6 @@ class CredentialTypeAccess(BaseAccess):
|
|||||||
def can_use(self, obj):
|
def can_use(self, obj):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def get_method_capability(self, method, obj, parent_obj):
|
|
||||||
if obj.managed_by_tower:
|
|
||||||
return False
|
|
||||||
return super(CredentialTypeAccess, self).get_method_capability(method, obj, parent_obj)
|
|
||||||
|
|
||||||
def filtered_queryset(self):
|
def filtered_queryset(self):
|
||||||
return self.model.objects.all()
|
return self.model.objects.all()
|
||||||
|
|
||||||
@@ -1182,6 +1177,8 @@ class CredentialAccess(BaseAccess):
|
|||||||
def get_user_capabilities(self, obj, **kwargs):
|
def get_user_capabilities(self, obj, **kwargs):
|
||||||
user_capabilities = super(CredentialAccess, self).get_user_capabilities(obj, **kwargs)
|
user_capabilities = super(CredentialAccess, self).get_user_capabilities(obj, **kwargs)
|
||||||
user_capabilities['use'] = self.can_use(obj)
|
user_capabilities['use'] = self.can_use(obj)
|
||||||
|
if getattr(obj, 'managed_by_tower', False) is True:
|
||||||
|
user_capabilities['edit'] = user_capabilities['delete'] = False
|
||||||
return user_capabilities
|
return user_capabilities
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
162
awx/main/conf.py
162
awx/main/conf.py
@@ -2,7 +2,6 @@
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from distutils.version import LooseVersion as Version
|
|
||||||
|
|
||||||
# Django
|
# Django
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
@@ -436,87 +435,6 @@ register(
|
|||||||
category_slug='jobs',
|
category_slug='jobs',
|
||||||
)
|
)
|
||||||
|
|
||||||
register(
|
|
||||||
'PRIMARY_GALAXY_URL',
|
|
||||||
field_class=fields.URLField,
|
|
||||||
required=False,
|
|
||||||
allow_blank=True,
|
|
||||||
label=_('Primary Galaxy Server URL'),
|
|
||||||
help_text=_(
|
|
||||||
'For organizations that run their own Galaxy service, this gives the option to specify a '
|
|
||||||
'host as the primary galaxy server. Requirements will be downloaded from the primary if the '
|
|
||||||
'specific role or collection is available there. If the content is not avilable in the primary, '
|
|
||||||
'or if this field is left blank, it will default to galaxy.ansible.com.'
|
|
||||||
),
|
|
||||||
category=_('Jobs'),
|
|
||||||
category_slug='jobs'
|
|
||||||
)
|
|
||||||
|
|
||||||
register(
|
|
||||||
'PRIMARY_GALAXY_USERNAME',
|
|
||||||
field_class=fields.CharField,
|
|
||||||
required=False,
|
|
||||||
allow_blank=True,
|
|
||||||
label=_('Primary Galaxy Server Username'),
|
|
||||||
help_text=_('(This setting is deprecated and will be removed in a future release) '
|
|
||||||
'For using a galaxy server at higher precedence than the public Ansible Galaxy. '
|
|
||||||
'The username to use for basic authentication against the Galaxy instance, '
|
|
||||||
'this is mutually exclusive with PRIMARY_GALAXY_TOKEN.'),
|
|
||||||
category=_('Jobs'),
|
|
||||||
category_slug='jobs'
|
|
||||||
)
|
|
||||||
|
|
||||||
register(
|
|
||||||
'PRIMARY_GALAXY_PASSWORD',
|
|
||||||
field_class=fields.CharField,
|
|
||||||
encrypted=True,
|
|
||||||
required=False,
|
|
||||||
allow_blank=True,
|
|
||||||
label=_('Primary Galaxy Server Password'),
|
|
||||||
help_text=_('(This setting is deprecated and will be removed in a future release) '
|
|
||||||
'For using a galaxy server at higher precedence than the public Ansible Galaxy. '
|
|
||||||
'The password to use for basic authentication against the Galaxy instance, '
|
|
||||||
'this is mutually exclusive with PRIMARY_GALAXY_TOKEN.'),
|
|
||||||
category=_('Jobs'),
|
|
||||||
category_slug='jobs'
|
|
||||||
)
|
|
||||||
|
|
||||||
register(
|
|
||||||
'PRIMARY_GALAXY_TOKEN',
|
|
||||||
field_class=fields.CharField,
|
|
||||||
encrypted=True,
|
|
||||||
required=False,
|
|
||||||
allow_blank=True,
|
|
||||||
label=_('Primary Galaxy Server Token'),
|
|
||||||
help_text=_('For using a galaxy server at higher precedence than the public Ansible Galaxy. '
|
|
||||||
'The token to use for connecting with the Galaxy instance, '
|
|
||||||
'this is mutually exclusive with corresponding username and password settings.'),
|
|
||||||
category=_('Jobs'),
|
|
||||||
category_slug='jobs'
|
|
||||||
)
|
|
||||||
|
|
||||||
register(
|
|
||||||
'PRIMARY_GALAXY_AUTH_URL',
|
|
||||||
field_class=fields.CharField,
|
|
||||||
required=False,
|
|
||||||
allow_blank=True,
|
|
||||||
label=_('Primary Galaxy Authentication URL'),
|
|
||||||
help_text=_('For using a galaxy server at higher precedence than the public Ansible Galaxy. '
|
|
||||||
'The token_endpoint of a Keycloak server.'),
|
|
||||||
category=_('Jobs'),
|
|
||||||
category_slug='jobs'
|
|
||||||
)
|
|
||||||
|
|
||||||
register(
|
|
||||||
'PUBLIC_GALAXY_ENABLED',
|
|
||||||
field_class=fields.BooleanField,
|
|
||||||
default=True,
|
|
||||||
label=_('Allow Access to Public Galaxy'),
|
|
||||||
help_text=_('Allow or deny access to the public Ansible Galaxy during project updates.'),
|
|
||||||
category=_('Jobs'),
|
|
||||||
category_slug='jobs'
|
|
||||||
)
|
|
||||||
|
|
||||||
register(
|
register(
|
||||||
'GALAXY_IGNORE_CERTS',
|
'GALAXY_IGNORE_CERTS',
|
||||||
field_class=fields.BooleanField,
|
field_class=fields.BooleanField,
|
||||||
@@ -856,84 +774,4 @@ def logging_validate(serializer, attrs):
|
|||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
def galaxy_validate(serializer, attrs):
|
|
||||||
"""Ansible Galaxy config options have mutual exclusivity rules, these rules
|
|
||||||
are enforced here on serializer validation so that users will not be able
|
|
||||||
to save settings which obviously break all project updates.
|
|
||||||
"""
|
|
||||||
prefix = 'PRIMARY_GALAXY_'
|
|
||||||
errors = {}
|
|
||||||
|
|
||||||
def _new_value(setting_name):
|
|
||||||
if setting_name in attrs:
|
|
||||||
return attrs[setting_name]
|
|
||||||
elif not serializer.instance:
|
|
||||||
return ''
|
|
||||||
return getattr(serializer.instance, setting_name, '')
|
|
||||||
|
|
||||||
if not _new_value('PRIMARY_GALAXY_URL'):
|
|
||||||
if _new_value('PUBLIC_GALAXY_ENABLED') is False:
|
|
||||||
msg = _('A URL for Primary Galaxy must be defined before disabling public Galaxy.')
|
|
||||||
# put error in both keys because UI has trouble with errors in toggles
|
|
||||||
for key in ('PRIMARY_GALAXY_URL', 'PUBLIC_GALAXY_ENABLED'):
|
|
||||||
errors.setdefault(key, [])
|
|
||||||
errors[key].append(msg)
|
|
||||||
raise serializers.ValidationError(errors)
|
|
||||||
|
|
||||||
from awx.main.constants import GALAXY_SERVER_FIELDS
|
|
||||||
if not any('{}{}'.format(prefix, subfield.upper()) in attrs for subfield in GALAXY_SERVER_FIELDS):
|
|
||||||
return attrs
|
|
||||||
|
|
||||||
galaxy_data = {}
|
|
||||||
for subfield in GALAXY_SERVER_FIELDS:
|
|
||||||
galaxy_data[subfield] = _new_value('{}{}'.format(prefix, subfield.upper()))
|
|
||||||
if not galaxy_data['url']:
|
|
||||||
for k, v in galaxy_data.items():
|
|
||||||
if v:
|
|
||||||
setting_name = '{}{}'.format(prefix, k.upper())
|
|
||||||
errors.setdefault(setting_name, [])
|
|
||||||
errors[setting_name].append(_(
|
|
||||||
'Cannot provide field if PRIMARY_GALAXY_URL is not set.'
|
|
||||||
))
|
|
||||||
for k in GALAXY_SERVER_FIELDS:
|
|
||||||
if galaxy_data[k]:
|
|
||||||
setting_name = '{}{}'.format(prefix, k.upper())
|
|
||||||
if (not serializer.instance) or (not getattr(serializer.instance, setting_name, '')):
|
|
||||||
# new auth is applied, so check if compatible with version
|
|
||||||
from awx.main.utils import get_ansible_version
|
|
||||||
current_version = get_ansible_version()
|
|
||||||
min_version = '2.9'
|
|
||||||
if Version(current_version) < Version(min_version):
|
|
||||||
errors.setdefault(setting_name, [])
|
|
||||||
errors[setting_name].append(_(
|
|
||||||
'Galaxy server settings are not available until Ansible {min_version}, '
|
|
||||||
'you are running {current_version}.'
|
|
||||||
).format(min_version=min_version, current_version=current_version))
|
|
||||||
if (galaxy_data['password'] or galaxy_data['username']) and (galaxy_data['token'] or galaxy_data['auth_url']):
|
|
||||||
for k in ('password', 'username', 'token', 'auth_url'):
|
|
||||||
setting_name = '{}{}'.format(prefix, k.upper())
|
|
||||||
if setting_name in attrs:
|
|
||||||
errors.setdefault(setting_name, [])
|
|
||||||
errors[setting_name].append(_(
|
|
||||||
'Setting Galaxy token and authentication URL is mutually exclusive with username and password.'
|
|
||||||
))
|
|
||||||
if bool(galaxy_data['username']) != bool(galaxy_data['password']):
|
|
||||||
msg = _('If authenticating via username and password, both must be provided.')
|
|
||||||
for k in ('username', 'password'):
|
|
||||||
setting_name = '{}{}'.format(prefix, k.upper())
|
|
||||||
errors.setdefault(setting_name, [])
|
|
||||||
errors[setting_name].append(msg)
|
|
||||||
if bool(galaxy_data['token']) != bool(galaxy_data['auth_url']):
|
|
||||||
msg = _('If authenticating via token, both token and authentication URL must be provided.')
|
|
||||||
for k in ('token', 'auth_url'):
|
|
||||||
setting_name = '{}{}'.format(prefix, k.upper())
|
|
||||||
errors.setdefault(setting_name, [])
|
|
||||||
errors[setting_name].append(msg)
|
|
||||||
|
|
||||||
if errors:
|
|
||||||
raise serializers.ValidationError(errors)
|
|
||||||
return attrs
|
|
||||||
|
|
||||||
|
|
||||||
register_validate('logging', logging_validate)
|
register_validate('logging', logging_validate)
|
||||||
register_validate('jobs', galaxy_validate)
|
|
||||||
|
|||||||
@@ -50,7 +50,3 @@ LOGGER_BLOCKLIST = (
|
|||||||
# loggers that may be called getting logging settings
|
# loggers that may be called getting logging settings
|
||||||
'awx.conf'
|
'awx.conf'
|
||||||
)
|
)
|
||||||
|
|
||||||
# these correspond to both AWX and Ansible settings to keep naming consistent
|
|
||||||
# for instance, settings.PRIMARY_GALAXY_AUTH_URL vs env var ANSIBLE_GALAXY_SERVER_FOO_AUTH_URL
|
|
||||||
GALAXY_SERVER_FIELDS = ('url', 'username', 'password', 'token', 'auth_url')
|
|
||||||
|
|||||||
@@ -42,6 +42,16 @@ class Command(BaseCommand):
|
|||||||
},
|
},
|
||||||
created_by=superuser)
|
created_by=superuser)
|
||||||
c.admin_role.members.add(superuser)
|
c.admin_role.members.add(superuser)
|
||||||
|
public_galaxy_credential = Credential(
|
||||||
|
name='Ansible Galaxy',
|
||||||
|
managed_by_tower=True,
|
||||||
|
credential_type=CredentialType.objects.get(kind='galaxy'),
|
||||||
|
inputs = {
|
||||||
|
'url': 'https://galaxy.ansible.com/'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
public_galaxy_credential.save()
|
||||||
|
o.galaxy_credentials.add(public_galaxy_credential)
|
||||||
i = Inventory.objects.create(name='Demo Inventory',
|
i = Inventory.objects.create(name='Demo Inventory',
|
||||||
organization=o,
|
organization=o,
|
||||||
created_by=superuser)
|
created_by=superuser)
|
||||||
|
|||||||
51
awx/main/migrations/0120_galaxy_credentials.py
Normal file
51
awx/main/migrations/0120_galaxy_credentials.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Generated by Django 2.2.11 on 2020-08-04 15:19
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import awx.main.fields
|
||||||
|
from awx.main.utils.encryption import encrypt_field, decrypt_field
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.utils.timezone import now
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
from awx.main.migrations import _galaxy as galaxy
|
||||||
|
from awx.main.models import CredentialType as ModernCredentialType
|
||||||
|
from awx.main.utils.common import set_current_apps
|
||||||
|
|
||||||
|
logger = logging.getLogger('awx.main.migrations')
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('main', '0119_inventory_plugins'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='credentialtype',
|
||||||
|
name='kind',
|
||||||
|
field=models.CharField(choices=[('ssh', 'Machine'), ('vault', 'Vault'), ('net', 'Network'), ('scm', 'Source Control'), ('cloud', 'Cloud'), ('token', 'Personal Access Token'), ('insights', 'Insights'), ('external', 'External'), ('kubernetes', 'Kubernetes'), ('galaxy', 'Galaxy/Automation Hub')], max_length=32),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='OrganizationGalaxyCredentialMembership',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('position', models.PositiveIntegerField(db_index=True, default=None, null=True)),
|
||||||
|
('credential', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='main.Credential')),
|
||||||
|
('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='main.Organization')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='organization',
|
||||||
|
name='galaxy_credentials',
|
||||||
|
field=awx.main.fields.OrderedManyToManyField(blank=True, related_name='organization_galaxy_credentials', through='main.OrganizationGalaxyCredentialMembership', to='main.Credential'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='credential',
|
||||||
|
name='managed_by_tower',
|
||||||
|
field=models.BooleanField(default=False, editable=False),
|
||||||
|
),
|
||||||
|
migrations.RunPython(galaxy.migrate_galaxy_settings)
|
||||||
|
]
|
||||||
125
awx/main/migrations/_galaxy.py
Normal file
125
awx/main/migrations/_galaxy.py
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
# Generated by Django 2.2.11 on 2020-08-04 15:19
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from awx.main.utils.encryption import encrypt_field, decrypt_field
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils.timezone import now
|
||||||
|
|
||||||
|
from awx.main.models import CredentialType as ModernCredentialType
|
||||||
|
from awx.main.utils.common import set_current_apps
|
||||||
|
|
||||||
|
logger = logging.getLogger('awx.main.migrations')
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_galaxy_settings(apps, schema_editor):
|
||||||
|
Organization = apps.get_model('main', 'Organization')
|
||||||
|
if Organization.objects.count() == 0:
|
||||||
|
# nothing to migrate
|
||||||
|
return
|
||||||
|
set_current_apps(apps)
|
||||||
|
ModernCredentialType.setup_tower_managed_defaults()
|
||||||
|
CredentialType = apps.get_model('main', 'CredentialType')
|
||||||
|
Credential = apps.get_model('main', 'Credential')
|
||||||
|
Setting = apps.get_model('conf', 'Setting')
|
||||||
|
|
||||||
|
galaxy_type = CredentialType.objects.get(kind='galaxy')
|
||||||
|
private_galaxy_url = Setting.objects.filter(key='PRIMARY_GALAXY_URL').first()
|
||||||
|
|
||||||
|
# by default, prior versions of AWX/Tower automatically pulled content
|
||||||
|
# from galaxy.ansible.com
|
||||||
|
public_galaxy_enabled = True
|
||||||
|
public_galaxy_setting = Setting.objects.filter(key='PUBLIC_GALAXY_ENABLED').first()
|
||||||
|
if public_galaxy_setting and public_galaxy_setting.value is False:
|
||||||
|
# ...UNLESS this behavior was explicitly disabled via this setting
|
||||||
|
public_galaxy_enabled = False
|
||||||
|
|
||||||
|
public_galaxy_credential = Credential(
|
||||||
|
created=now(),
|
||||||
|
modified=now(),
|
||||||
|
name='Ansible Galaxy',
|
||||||
|
managed_by_tower=True,
|
||||||
|
credential_type=galaxy_type,
|
||||||
|
inputs = {
|
||||||
|
'url': 'https://galaxy.ansible.com/'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
public_galaxy_credential.save()
|
||||||
|
|
||||||
|
for org in Organization.objects.all():
|
||||||
|
if private_galaxy_url and private_galaxy_url.value:
|
||||||
|
# If a setting exists for a private Galaxy URL, make a credential for it
|
||||||
|
username = Setting.objects.filter(key='PRIMARY_GALAXY_USERNAME').first()
|
||||||
|
password = Setting.objects.filter(key='PRIMARY_GALAXY_PASSWORD').first()
|
||||||
|
if (username and username.value) or (password and password.value):
|
||||||
|
logger.error(
|
||||||
|
f'Specifying HTTP basic auth for the Ansible Galaxy API '
|
||||||
|
f'({private_galaxy_url.value}) is no longer supported. '
|
||||||
|
'Please provide an API token instead after your upgrade '
|
||||||
|
'has completed',
|
||||||
|
)
|
||||||
|
inputs = {
|
||||||
|
'url': private_galaxy_url.value
|
||||||
|
}
|
||||||
|
token = Setting.objects.filter(key='PRIMARY_GALAXY_TOKEN').first()
|
||||||
|
if token and token.value:
|
||||||
|
inputs['token'] = decrypt_field(token, 'value')
|
||||||
|
auth_url = Setting.objects.filter(key='PRIMARY_GALAXY_AUTH_URL').first()
|
||||||
|
if auth_url and auth_url.value:
|
||||||
|
inputs['auth_url'] = auth_url.value
|
||||||
|
name = f'Private Galaxy ({private_galaxy_url.value})'
|
||||||
|
if 'cloud.redhat.com' in inputs['url']:
|
||||||
|
name = f'Ansible Automation Hub ({private_galaxy_url.value})'
|
||||||
|
cred = Credential(
|
||||||
|
created=now(),
|
||||||
|
modified=now(),
|
||||||
|
name=name,
|
||||||
|
organization=org,
|
||||||
|
credential_type=galaxy_type,
|
||||||
|
inputs=inputs
|
||||||
|
)
|
||||||
|
cred.save()
|
||||||
|
if token and token.value:
|
||||||
|
# encrypt based on the primary key from the prior save
|
||||||
|
cred.inputs['token'] = encrypt_field(cred, 'token')
|
||||||
|
cred.save()
|
||||||
|
org.galaxy_credentials.add(cred)
|
||||||
|
|
||||||
|
fallback_servers = getattr(settings, 'FALLBACK_GALAXY_SERVERS', [])
|
||||||
|
for fallback in fallback_servers:
|
||||||
|
url = fallback.get('url', None)
|
||||||
|
auth_url = fallback.get('auth_url', None)
|
||||||
|
username = fallback.get('username', None)
|
||||||
|
password = fallback.get('password', None)
|
||||||
|
token = fallback.get('token', None)
|
||||||
|
if username or password:
|
||||||
|
logger.error(
|
||||||
|
f'Specifying HTTP basic auth for the Ansible Galaxy API '
|
||||||
|
f'({url}) is no longer supported. '
|
||||||
|
'Please provide an API token instead after your upgrade '
|
||||||
|
'has completed',
|
||||||
|
)
|
||||||
|
inputs = {'url': url}
|
||||||
|
if token:
|
||||||
|
inputs['token'] = token
|
||||||
|
if auth_url:
|
||||||
|
inputs['auth_url'] = auth_url
|
||||||
|
cred = Credential(
|
||||||
|
created=now(),
|
||||||
|
modified=now(),
|
||||||
|
name=f'Ansible Galaxy ({url})',
|
||||||
|
organization=org,
|
||||||
|
credential_type=galaxy_type,
|
||||||
|
inputs=inputs
|
||||||
|
)
|
||||||
|
cred.save()
|
||||||
|
if token:
|
||||||
|
# encrypt based on the primary key from the prior save
|
||||||
|
cred.inputs['token'] = encrypt_field(cred, 'token')
|
||||||
|
cred.save()
|
||||||
|
org.galaxy_credentials.add(cred)
|
||||||
|
|
||||||
|
if public_galaxy_enabled:
|
||||||
|
# If public Galaxy was enabled, associate it to the org
|
||||||
|
org.galaxy_credentials.add(public_galaxy_credential)
|
||||||
@@ -96,6 +96,10 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
|
|||||||
help_text=_('Specify the type of credential you want to create. Refer '
|
help_text=_('Specify the type of credential you want to create. Refer '
|
||||||
'to the Ansible Tower documentation for details on each type.')
|
'to the Ansible Tower documentation for details on each type.')
|
||||||
)
|
)
|
||||||
|
managed_by_tower = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
editable=False
|
||||||
|
)
|
||||||
organization = models.ForeignKey(
|
organization = models.ForeignKey(
|
||||||
'Organization',
|
'Organization',
|
||||||
null=True,
|
null=True,
|
||||||
@@ -331,6 +335,7 @@ class CredentialType(CommonModelNameNotUnique):
|
|||||||
('insights', _('Insights')),
|
('insights', _('Insights')),
|
||||||
('external', _('External')),
|
('external', _('External')),
|
||||||
('kubernetes', _('Kubernetes')),
|
('kubernetes', _('Kubernetes')),
|
||||||
|
('galaxy', _('Galaxy/Automation Hub')),
|
||||||
)
|
)
|
||||||
|
|
||||||
kind = models.CharField(
|
kind = models.CharField(
|
||||||
@@ -1173,6 +1178,38 @@ ManagedCredentialType(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
ManagedCredentialType(
|
||||||
|
namespace='galaxy_api_token',
|
||||||
|
kind='galaxy',
|
||||||
|
name=ugettext_noop('Ansible Galaxy/Automation Hub API Token'),
|
||||||
|
inputs={
|
||||||
|
'fields': [{
|
||||||
|
'id': 'url',
|
||||||
|
'label': ugettext_noop('Galaxy Server URL'),
|
||||||
|
'type': 'string',
|
||||||
|
'help_text': ugettext_noop('The URL of the Galaxy instance to connect to.')
|
||||||
|
},{
|
||||||
|
'id': 'auth_url',
|
||||||
|
'label': ugettext_noop('Auth Server URL'),
|
||||||
|
'type': 'string',
|
||||||
|
'help_text': ugettext_noop(
|
||||||
|
'The URL of a Keycloak server token_endpoint, if using '
|
||||||
|
'SSO auth.'
|
||||||
|
)
|
||||||
|
},{
|
||||||
|
'id': 'token',
|
||||||
|
'label': ugettext_noop('API Token'),
|
||||||
|
'type': 'string',
|
||||||
|
'secret': True,
|
||||||
|
'help_text': ugettext_noop(
|
||||||
|
'A token to use for authentication against the Galaxy instance.'
|
||||||
|
)
|
||||||
|
}],
|
||||||
|
'required': ['url'],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CredentialInputSource(PrimordialModel):
|
class CredentialInputSource(PrimordialModel):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -45,6 +45,12 @@ class Organization(CommonModel, NotificationFieldsModel, ResourceMixin, CustomVi
|
|||||||
blank=True,
|
blank=True,
|
||||||
through='OrganizationInstanceGroupMembership'
|
through='OrganizationInstanceGroupMembership'
|
||||||
)
|
)
|
||||||
|
galaxy_credentials = OrderedManyToManyField(
|
||||||
|
'Credential',
|
||||||
|
blank=True,
|
||||||
|
through='OrganizationGalaxyCredentialMembership',
|
||||||
|
related_name='%(class)s_galaxy_credentials'
|
||||||
|
)
|
||||||
max_hosts = models.PositiveIntegerField(
|
max_hosts = models.PositiveIntegerField(
|
||||||
blank=True,
|
blank=True,
|
||||||
default=0,
|
default=0,
|
||||||
@@ -108,6 +114,23 @@ class Organization(CommonModel, NotificationFieldsModel, ResourceMixin, CustomVi
|
|||||||
return UnifiedJob.objects.non_polymorphic().filter(organization=self)
|
return UnifiedJob.objects.non_polymorphic().filter(organization=self)
|
||||||
|
|
||||||
|
|
||||||
|
class OrganizationGalaxyCredentialMembership(models.Model):
|
||||||
|
|
||||||
|
organization = models.ForeignKey(
|
||||||
|
'Organization',
|
||||||
|
on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
credential = models.ForeignKey(
|
||||||
|
'Credential',
|
||||||
|
on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
position = models.PositiveIntegerField(
|
||||||
|
null=True,
|
||||||
|
default=None,
|
||||||
|
db_index=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Team(CommonModelNameNotUnique, ResourceMixin):
|
class Team(CommonModelNameNotUnique, ResourceMixin):
|
||||||
'''
|
'''
|
||||||
A team is a group of users that work on common projects.
|
A team is a group of users that work on common projects.
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import re
|
import re
|
||||||
import urllib.parse as urlparse
|
import urllib.parse as urlparse
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
REPLACE_STR = '$encrypted$'
|
REPLACE_STR = '$encrypted$'
|
||||||
|
|
||||||
|
|
||||||
@@ -12,12 +10,6 @@ class UriCleaner(object):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def remove_sensitive(cleartext):
|
def remove_sensitive(cleartext):
|
||||||
# exclude_list contains the items that will _not_ be redacted
|
|
||||||
exclude_list = [settings.PUBLIC_GALAXY_SERVER['url']]
|
|
||||||
if settings.PRIMARY_GALAXY_URL:
|
|
||||||
exclude_list += [settings.PRIMARY_GALAXY_URL]
|
|
||||||
if settings.FALLBACK_GALAXY_SERVERS:
|
|
||||||
exclude_list += [server['url'] for server in settings.FALLBACK_GALAXY_SERVERS]
|
|
||||||
redactedtext = cleartext
|
redactedtext = cleartext
|
||||||
text_index = 0
|
text_index = 0
|
||||||
while True:
|
while True:
|
||||||
@@ -25,10 +17,6 @@ class UriCleaner(object):
|
|||||||
if not match:
|
if not match:
|
||||||
break
|
break
|
||||||
uri_str = match.group(1)
|
uri_str = match.group(1)
|
||||||
# Do not redact items from the exclude list
|
|
||||||
if any(uri_str.startswith(exclude_uri) for exclude_uri in exclude_list):
|
|
||||||
text_index = match.start() + len(uri_str)
|
|
||||||
continue
|
|
||||||
try:
|
try:
|
||||||
# May raise a ValueError if invalid URI for one reason or another
|
# May raise a ValueError if invalid URI for one reason or another
|
||||||
o = urlparse.urlsplit(uri_str)
|
o = urlparse.urlsplit(uri_str)
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ import ansible_runner
|
|||||||
|
|
||||||
# AWX
|
# AWX
|
||||||
from awx import __version__ as awx_application_version
|
from awx import __version__ as awx_application_version
|
||||||
from awx.main.constants import PRIVILEGE_ESCALATION_METHODS, STANDARD_INVENTORY_UPDATE_ENV, GALAXY_SERVER_FIELDS
|
from awx.main.constants import PRIVILEGE_ESCALATION_METHODS, STANDARD_INVENTORY_UPDATE_ENV
|
||||||
from awx.main.access import access_registry
|
from awx.main.access import access_registry
|
||||||
from awx.main.redact import UriCleaner
|
from awx.main.redact import UriCleaner
|
||||||
from awx.main.models import (
|
from awx.main.models import (
|
||||||
@@ -2015,35 +2015,25 @@ class RunProjectUpdate(BaseTask):
|
|||||||
env['PROJECT_UPDATE_ID'] = str(project_update.pk)
|
env['PROJECT_UPDATE_ID'] = str(project_update.pk)
|
||||||
if settings.GALAXY_IGNORE_CERTS:
|
if settings.GALAXY_IGNORE_CERTS:
|
||||||
env['ANSIBLE_GALAXY_IGNORE'] = True
|
env['ANSIBLE_GALAXY_IGNORE'] = True
|
||||||
# Set up the public Galaxy server, if enabled
|
|
||||||
galaxy_configured = False
|
# build out env vars for Galaxy credentials (in order)
|
||||||
if settings.PUBLIC_GALAXY_ENABLED:
|
galaxy_server_list = []
|
||||||
galaxy_servers = [settings.PUBLIC_GALAXY_SERVER] # static setting
|
if project_update.project.organization:
|
||||||
else:
|
for i, cred in enumerate(
|
||||||
galaxy_configured = True
|
project_update.project.organization.galaxy_credentials.all()
|
||||||
galaxy_servers = []
|
):
|
||||||
# Set up fallback Galaxy servers, if configured
|
env[f'ANSIBLE_GALAXY_SERVER_SERVER{i}_URL'] = cred.get_input('url')
|
||||||
if settings.FALLBACK_GALAXY_SERVERS:
|
auth_url = cred.get_input('auth_url', default=None)
|
||||||
galaxy_configured = True
|
token = cred.get_input('token', default=None)
|
||||||
galaxy_servers = settings.FALLBACK_GALAXY_SERVERS + galaxy_servers
|
if token:
|
||||||
# Set up the primary Galaxy server, if configured
|
env[f'ANSIBLE_GALAXY_SERVER_SERVER{i}_TOKEN'] = token
|
||||||
if settings.PRIMARY_GALAXY_URL:
|
if auth_url:
|
||||||
galaxy_configured = True
|
env[f'ANSIBLE_GALAXY_SERVER_SERVER{i}_AUTH_URL'] = auth_url
|
||||||
galaxy_servers = [{'id': 'primary_galaxy'}] + galaxy_servers
|
galaxy_server_list.append(f'server{i}')
|
||||||
for key in GALAXY_SERVER_FIELDS:
|
|
||||||
value = getattr(settings, 'PRIMARY_GALAXY_{}'.format(key.upper()))
|
if galaxy_server_list:
|
||||||
if value:
|
env['ANSIBLE_GALAXY_SERVER_LIST'] = ','.join(galaxy_server_list)
|
||||||
galaxy_servers[0][key] = value
|
|
||||||
if galaxy_configured:
|
|
||||||
for server in galaxy_servers:
|
|
||||||
for key in GALAXY_SERVER_FIELDS:
|
|
||||||
if not server.get(key):
|
|
||||||
continue
|
|
||||||
env_key = ('ANSIBLE_GALAXY_SERVER_{}_{}'.format(server.get('id', 'unnamed'), key)).upper()
|
|
||||||
env[env_key] = server[key]
|
|
||||||
if galaxy_servers:
|
|
||||||
# now set the precedence of galaxy servers
|
|
||||||
env['ANSIBLE_GALAXY_SERVER_LIST'] = ','.join([server.get('id', 'unnamed') for server in galaxy_servers])
|
|
||||||
return env
|
return env
|
||||||
|
|
||||||
def _build_scm_url_extra_vars(self, project_update):
|
def _build_scm_url_extra_vars(self, project_update):
|
||||||
@@ -2116,6 +2106,19 @@ class RunProjectUpdate(BaseTask):
|
|||||||
raise RuntimeError('Could not determine a revision to run from project.')
|
raise RuntimeError('Could not determine a revision to run from project.')
|
||||||
elif not scm_branch:
|
elif not scm_branch:
|
||||||
scm_branch = {'hg': 'tip'}.get(project_update.scm_type, 'HEAD')
|
scm_branch = {'hg': 'tip'}.get(project_update.scm_type, 'HEAD')
|
||||||
|
|
||||||
|
galaxy_creds_are_defined = (
|
||||||
|
project_update.project.organization and
|
||||||
|
project_update.project.organization.galaxy_credentials.exists()
|
||||||
|
)
|
||||||
|
if not galaxy_creds_are_defined and (
|
||||||
|
settings.AWX_ROLES_ENABLED or settings.AWX_COLLECTIONS_ENABLED
|
||||||
|
):
|
||||||
|
logger.debug(
|
||||||
|
'Galaxy role/collection syncing is enabled, but no '
|
||||||
|
f'credentials are configured for {project_update.project.organization}.'
|
||||||
|
)
|
||||||
|
|
||||||
extra_vars.update({
|
extra_vars.update({
|
||||||
'projects_root': settings.PROJECTS_ROOT.rstrip('/'),
|
'projects_root': settings.PROJECTS_ROOT.rstrip('/'),
|
||||||
'local_path': os.path.basename(project_update.project.local_path),
|
'local_path': os.path.basename(project_update.project.local_path),
|
||||||
@@ -2126,8 +2129,8 @@ class RunProjectUpdate(BaseTask):
|
|||||||
'scm_url': scm_url,
|
'scm_url': scm_url,
|
||||||
'scm_branch': scm_branch,
|
'scm_branch': scm_branch,
|
||||||
'scm_clean': project_update.scm_clean,
|
'scm_clean': project_update.scm_clean,
|
||||||
'roles_enabled': settings.AWX_ROLES_ENABLED,
|
'roles_enabled': galaxy_creds_are_defined and settings.AWX_ROLES_ENABLED,
|
||||||
'collections_enabled': settings.AWX_COLLECTIONS_ENABLED,
|
'collections_enabled': galaxy_creds_are_defined and settings.AWX_COLLECTIONS_ENABLED,
|
||||||
})
|
})
|
||||||
# apply custom refspec from user for PR refs and the like
|
# apply custom refspec from user for PR refs and the like
|
||||||
if project_update.scm_refspec:
|
if project_update.scm_refspec:
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ def test_create_valid_kind(kind, get, post, admin):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('kind', ['ssh', 'vault', 'scm', 'insights', 'kubernetes'])
|
@pytest.mark.parametrize('kind', ['ssh', 'vault', 'scm', 'insights', 'kubernetes', 'galaxy'])
|
||||||
def test_create_invalid_kind(kind, get, post, admin):
|
def test_create_invalid_kind(kind, get, post, admin):
|
||||||
response = post(reverse('api:credential_type_list'), {
|
response = post(reverse('api:credential_type_list'), {
|
||||||
'kind': kind,
|
'kind': kind,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from django.conf import settings
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
# AWX
|
# AWX
|
||||||
from awx.main.models import ProjectUpdate
|
from awx.main.models import ProjectUpdate, CredentialType, Credential
|
||||||
from awx.api.versioning import reverse
|
from awx.api.versioning import reverse
|
||||||
|
|
||||||
|
|
||||||
@@ -288,3 +288,90 @@ def test_organization_delete_with_active_jobs(delete, admin, organization, organ
|
|||||||
|
|
||||||
assert resp.data['error'] == u"Resource is being used by running jobs."
|
assert resp.data['error'] == u"Resource is being used by running jobs."
|
||||||
assert resp_sorted == expect_sorted
|
assert resp_sorted == expect_sorted
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_galaxy_credential_association_forbidden(alice, organization, post):
|
||||||
|
galaxy = CredentialType.defaults['galaxy_api_token']()
|
||||||
|
galaxy.save()
|
||||||
|
|
||||||
|
cred = Credential.objects.create(
|
||||||
|
credential_type=galaxy,
|
||||||
|
name='Public Galaxy',
|
||||||
|
organization=organization,
|
||||||
|
inputs={
|
||||||
|
'url': 'https://galaxy.ansible.com/'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
url = reverse('api:organization_galaxy_credentials_list', kwargs={'pk': organization.id})
|
||||||
|
post(
|
||||||
|
url,
|
||||||
|
{'associate': True, 'id': cred.pk},
|
||||||
|
user=alice,
|
||||||
|
expect=403
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_galaxy_credential_type_enforcement(admin, organization, post):
|
||||||
|
ssh = CredentialType.defaults['ssh']()
|
||||||
|
ssh.save()
|
||||||
|
|
||||||
|
cred = Credential.objects.create(
|
||||||
|
credential_type=ssh,
|
||||||
|
name='SSH Credential',
|
||||||
|
organization=organization,
|
||||||
|
)
|
||||||
|
url = reverse('api:organization_galaxy_credentials_list', kwargs={'pk': organization.id})
|
||||||
|
resp = post(
|
||||||
|
url,
|
||||||
|
{'associate': True, 'id': cred.pk},
|
||||||
|
user=admin,
|
||||||
|
expect=400
|
||||||
|
)
|
||||||
|
assert resp.data['msg'] == 'Credential must be a Galaxy credential, not Machine.'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_galaxy_credential_association(alice, admin, organization, post, get):
|
||||||
|
galaxy = CredentialType.defaults['galaxy_api_token']()
|
||||||
|
galaxy.save()
|
||||||
|
|
||||||
|
for i in range(5):
|
||||||
|
cred = Credential.objects.create(
|
||||||
|
credential_type=galaxy,
|
||||||
|
name=f'Public Galaxy {i + 1}',
|
||||||
|
organization=organization,
|
||||||
|
inputs={
|
||||||
|
'url': 'https://galaxy.ansible.com/'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
url = reverse('api:organization_galaxy_credentials_list', kwargs={'pk': organization.id})
|
||||||
|
post(
|
||||||
|
url,
|
||||||
|
{'associate': True, 'id': cred.pk},
|
||||||
|
user=admin,
|
||||||
|
expect=204
|
||||||
|
)
|
||||||
|
resp = get(url, user=admin)
|
||||||
|
assert [cred['name'] for cred in resp.data['results']] == [
|
||||||
|
'Public Galaxy 1',
|
||||||
|
'Public Galaxy 2',
|
||||||
|
'Public Galaxy 3',
|
||||||
|
'Public Galaxy 4',
|
||||||
|
'Public Galaxy 5',
|
||||||
|
]
|
||||||
|
|
||||||
|
post(
|
||||||
|
url,
|
||||||
|
{'disassociate': True, 'id': Credential.objects.get(name='Public Galaxy 3').pk},
|
||||||
|
user=admin,
|
||||||
|
expect=204
|
||||||
|
)
|
||||||
|
resp = get(url, user=admin)
|
||||||
|
assert [cred['name'] for cred in resp.data['results']] == [
|
||||||
|
'Public Galaxy 1',
|
||||||
|
'Public Galaxy 2',
|
||||||
|
'Public Galaxy 4',
|
||||||
|
'Public Galaxy 5',
|
||||||
|
]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from awx.main.models import Project
|
from awx.main.models import Project, Credential, CredentialType
|
||||||
from awx.main.models.organization import Organization
|
from awx.main.models.organization import Organization
|
||||||
|
|
||||||
|
|
||||||
@@ -57,3 +57,31 @@ def test_foreign_key_change_changes_modified_by(project, organization):
|
|||||||
def test_project_related_jobs(project):
|
def test_project_related_jobs(project):
|
||||||
update = project.create_unified_job()
|
update = project.create_unified_job()
|
||||||
assert update.id in [u.id for u in project._get_related_jobs()]
|
assert update.id in [u.id for u in project._get_related_jobs()]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_galaxy_credentials(project):
|
||||||
|
org = project.organization
|
||||||
|
galaxy = CredentialType.defaults['galaxy_api_token']()
|
||||||
|
galaxy.save()
|
||||||
|
for i in range(5):
|
||||||
|
cred = Credential.objects.create(
|
||||||
|
name=f'Ansible Galaxy {i + 1}',
|
||||||
|
organization=org,
|
||||||
|
credential_type=galaxy,
|
||||||
|
inputs={
|
||||||
|
'url': 'https://galaxy.ansible.com/'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
cred.save()
|
||||||
|
org.galaxy_credentials.add(cred)
|
||||||
|
|
||||||
|
assert [
|
||||||
|
cred.name for cred in org.galaxy_credentials.all()
|
||||||
|
] == [
|
||||||
|
'Ansible Galaxy 1',
|
||||||
|
'Ansible Galaxy 2',
|
||||||
|
'Ansible Galaxy 3',
|
||||||
|
'Ansible Galaxy 4',
|
||||||
|
'Ansible Galaxy 5',
|
||||||
|
]
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ def test_default_cred_types():
|
|||||||
'azure_rm',
|
'azure_rm',
|
||||||
'cloudforms',
|
'cloudforms',
|
||||||
'conjur',
|
'conjur',
|
||||||
|
'galaxy_api_token',
|
||||||
'gce',
|
'gce',
|
||||||
'github_token',
|
'github_token',
|
||||||
'gitlab_token',
|
'gitlab_token',
|
||||||
|
|||||||
115
awx/main/tests/functional/test_galaxy_credential_migration.py
Normal file
115
awx/main/tests/functional/test_galaxy_credential_migration.py
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import importlib
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from awx.main.models import Credential, Organization
|
||||||
|
from awx.conf.models import Setting
|
||||||
|
from awx.main.migrations import _galaxy as galaxy
|
||||||
|
|
||||||
|
|
||||||
|
class FakeApps(object):
|
||||||
|
def get_model(self, app, model):
|
||||||
|
if app == 'contenttypes':
|
||||||
|
return ContentType
|
||||||
|
return getattr(importlib.import_module(f'awx.{app}.models'), model)
|
||||||
|
|
||||||
|
|
||||||
|
apps = FakeApps()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_default_public_galaxy():
|
||||||
|
org = Organization.objects.create()
|
||||||
|
assert org.galaxy_credentials.count() == 0
|
||||||
|
galaxy.migrate_galaxy_settings(apps, None)
|
||||||
|
assert org.galaxy_credentials.count() == 1
|
||||||
|
creds = org.galaxy_credentials.all()
|
||||||
|
assert creds[0].name == 'Ansible Galaxy'
|
||||||
|
assert creds[0].inputs['url'] == 'https://galaxy.ansible.com/'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_public_galaxy_disabled():
|
||||||
|
Setting.objects.create(key='PUBLIC_GALAXY_ENABLED', value=False)
|
||||||
|
org = Organization.objects.create()
|
||||||
|
assert org.galaxy_credentials.count() == 0
|
||||||
|
galaxy.migrate_galaxy_settings(apps, None)
|
||||||
|
assert org.galaxy_credentials.count() == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_rh_automation_hub():
|
||||||
|
Setting.objects.create(key='PRIMARY_GALAXY_URL', value='https://cloud.redhat.com/api/automation-hub/')
|
||||||
|
Setting.objects.create(key='PRIMARY_GALAXY_TOKEN', value='secret123')
|
||||||
|
org = Organization.objects.create()
|
||||||
|
assert org.galaxy_credentials.count() == 0
|
||||||
|
galaxy.migrate_galaxy_settings(apps, None)
|
||||||
|
assert org.galaxy_credentials.count() == 2
|
||||||
|
assert org.galaxy_credentials.first().name == 'Ansible Automation Hub (https://cloud.redhat.com/api/automation-hub/)' # noqa
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_multiple_galaxies():
|
||||||
|
for i in range(5):
|
||||||
|
Organization.objects.create(name=f'Org {i}')
|
||||||
|
|
||||||
|
Setting.objects.create(key='PRIMARY_GALAXY_URL', value='https://example.org/')
|
||||||
|
Setting.objects.create(key='PRIMARY_GALAXY_AUTH_URL', value='https://auth.example.org/')
|
||||||
|
Setting.objects.create(key='PRIMARY_GALAXY_USERNAME', value='user')
|
||||||
|
Setting.objects.create(key='PRIMARY_GALAXY_PASSWORD', value='pass')
|
||||||
|
Setting.objects.create(key='PRIMARY_GALAXY_TOKEN', value='secret123')
|
||||||
|
|
||||||
|
for org in Organization.objects.all():
|
||||||
|
assert org.galaxy_credentials.count() == 0
|
||||||
|
|
||||||
|
galaxy.migrate_galaxy_settings(apps, None)
|
||||||
|
|
||||||
|
for org in Organization.objects.all():
|
||||||
|
assert org.galaxy_credentials.count() == 2
|
||||||
|
creds = org.galaxy_credentials.all()
|
||||||
|
assert creds[0].name == 'Private Galaxy (https://example.org/)'
|
||||||
|
assert creds[0].inputs['url'] == 'https://example.org/'
|
||||||
|
assert creds[0].inputs['auth_url'] == 'https://auth.example.org/'
|
||||||
|
assert creds[0].inputs['token'].startswith('$encrypted$')
|
||||||
|
assert creds[0].get_input('token') == 'secret123'
|
||||||
|
|
||||||
|
assert creds[1].name == 'Ansible Galaxy'
|
||||||
|
assert creds[1].inputs['url'] == 'https://galaxy.ansible.com/'
|
||||||
|
|
||||||
|
public_galaxy_creds = Credential.objects.filter(name='Ansible Galaxy')
|
||||||
|
assert public_galaxy_creds.count() == 1
|
||||||
|
assert public_galaxy_creds.first().managed_by_tower is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_fallback_galaxies():
|
||||||
|
org = Organization.objects.create()
|
||||||
|
assert org.galaxy_credentials.count() == 0
|
||||||
|
Setting.objects.create(key='PRIMARY_GALAXY_URL', value='https://example.org/')
|
||||||
|
Setting.objects.create(key='PRIMARY_GALAXY_AUTH_URL', value='https://auth.example.org/')
|
||||||
|
Setting.objects.create(key='PRIMARY_GALAXY_TOKEN', value='secret123')
|
||||||
|
try:
|
||||||
|
settings.FALLBACK_GALAXY_SERVERS = [{
|
||||||
|
'id': 'abc123',
|
||||||
|
'url': 'https://some-other-galaxy.example.org/',
|
||||||
|
'auth_url': 'https://some-other-galaxy.sso.example.org/',
|
||||||
|
'username': 'user',
|
||||||
|
'password': 'pass',
|
||||||
|
'token': 'fallback123',
|
||||||
|
}]
|
||||||
|
galaxy.migrate_galaxy_settings(apps, None)
|
||||||
|
finally:
|
||||||
|
settings.FALLBACK_GALAXY_SERVERS = []
|
||||||
|
assert org.galaxy_credentials.count() == 3
|
||||||
|
creds = org.galaxy_credentials.all()
|
||||||
|
assert creds[0].name == 'Private Galaxy (https://example.org/)'
|
||||||
|
assert creds[0].inputs['url'] == 'https://example.org/'
|
||||||
|
assert creds[1].name == 'Ansible Galaxy (https://some-other-galaxy.example.org/)'
|
||||||
|
assert creds[1].inputs['url'] == 'https://some-other-galaxy.example.org/'
|
||||||
|
assert creds[1].inputs['auth_url'] == 'https://some-other-galaxy.sso.example.org/'
|
||||||
|
assert creds[1].inputs['token'].startswith('$encrypted$')
|
||||||
|
assert creds[1].get_input('token') == 'fallback123'
|
||||||
|
assert creds[2].name == 'Ansible Galaxy'
|
||||||
|
assert creds[2].inputs['url'] == 'https://galaxy.ansible.com/'
|
||||||
@@ -25,6 +25,7 @@ from awx.main.models import (
|
|||||||
Job,
|
Job,
|
||||||
JobTemplate,
|
JobTemplate,
|
||||||
Notification,
|
Notification,
|
||||||
|
Organization,
|
||||||
Project,
|
Project,
|
||||||
ProjectUpdate,
|
ProjectUpdate,
|
||||||
UnifiedJob,
|
UnifiedJob,
|
||||||
@@ -59,6 +60,19 @@ def patch_Job():
|
|||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def patch_Organization():
|
||||||
|
_credentials = []
|
||||||
|
credentials_mock = mock.Mock(**{
|
||||||
|
'all': lambda: _credentials,
|
||||||
|
'add': _credentials.append,
|
||||||
|
'exists': lambda: len(_credentials) > 0,
|
||||||
|
'spec_set': ['all', 'add', 'exists'],
|
||||||
|
})
|
||||||
|
with mock.patch.object(Organization, 'galaxy_credentials', credentials_mock):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def job():
|
def job():
|
||||||
return Job(
|
return Job(
|
||||||
@@ -131,7 +145,6 @@ def test_send_notifications_list(mock_notifications_filter, mock_job_get, mocker
|
|||||||
('SECRET_KEY', 'SECRET'),
|
('SECRET_KEY', 'SECRET'),
|
||||||
('VMWARE_PASSWORD', 'SECRET'),
|
('VMWARE_PASSWORD', 'SECRET'),
|
||||||
('API_SECRET', 'SECRET'),
|
('API_SECRET', 'SECRET'),
|
||||||
('ANSIBLE_GALAXY_SERVER_PRIMARY_GALAXY_PASSWORD', 'SECRET'),
|
|
||||||
('ANSIBLE_GALAXY_SERVER_PRIMARY_GALAXY_TOKEN', 'SECRET'),
|
('ANSIBLE_GALAXY_SERVER_PRIMARY_GALAXY_TOKEN', 'SECRET'),
|
||||||
])
|
])
|
||||||
def test_safe_env_filtering(key, value):
|
def test_safe_env_filtering(key, value):
|
||||||
@@ -1780,10 +1793,108 @@ class TestJobCredentials(TestJobExecution):
|
|||||||
assert env['FOO'] == 'BAR'
|
assert env['FOO'] == 'BAR'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("patch_Organization")
|
||||||
|
class TestProjectUpdateGalaxyCredentials(TestJobExecution):
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def project_update(self):
|
||||||
|
org = Organization(pk=1)
|
||||||
|
proj = Project(pk=1, organization=org)
|
||||||
|
project_update = ProjectUpdate(pk=1, project=proj, scm_type='git')
|
||||||
|
project_update.websocket_emit_status = mock.Mock()
|
||||||
|
return project_update
|
||||||
|
|
||||||
|
parametrize = {
|
||||||
|
'test_galaxy_credentials_ignore_certs': [
|
||||||
|
dict(ignore=True),
|
||||||
|
dict(ignore=False),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_galaxy_credentials_ignore_certs(self, private_data_dir, project_update, ignore):
|
||||||
|
settings.GALAXY_IGNORE_CERTS = ignore
|
||||||
|
task = tasks.RunProjectUpdate()
|
||||||
|
env = task.build_env(project_update, private_data_dir)
|
||||||
|
if ignore:
|
||||||
|
assert env['ANSIBLE_GALAXY_IGNORE'] is True
|
||||||
|
else:
|
||||||
|
assert 'ANSIBLE_GALAXY_IGNORE' not in env
|
||||||
|
|
||||||
|
def test_galaxy_credentials_empty(self, private_data_dir, project_update):
|
||||||
|
|
||||||
|
class RunProjectUpdate(tasks.RunProjectUpdate):
|
||||||
|
__vars__ = {}
|
||||||
|
|
||||||
|
def _write_extra_vars_file(self, private_data_dir, extra_vars, *kw):
|
||||||
|
self.__vars__ = extra_vars
|
||||||
|
|
||||||
|
task = RunProjectUpdate()
|
||||||
|
env = task.build_env(project_update, private_data_dir)
|
||||||
|
task.build_extra_vars_file(project_update, private_data_dir)
|
||||||
|
assert task.__vars__['roles_enabled'] is False
|
||||||
|
assert task.__vars__['collections_enabled'] is False
|
||||||
|
for k in env:
|
||||||
|
assert not k.startswith('ANSIBLE_GALAXY_SERVER')
|
||||||
|
|
||||||
|
def test_single_public_galaxy(self, private_data_dir, project_update):
|
||||||
|
class RunProjectUpdate(tasks.RunProjectUpdate):
|
||||||
|
__vars__ = {}
|
||||||
|
|
||||||
|
def _write_extra_vars_file(self, private_data_dir, extra_vars, *kw):
|
||||||
|
self.__vars__ = extra_vars
|
||||||
|
|
||||||
|
credential_type = CredentialType.defaults['galaxy_api_token']()
|
||||||
|
public_galaxy = Credential(pk=1, credential_type=credential_type, inputs={
|
||||||
|
'url': 'https://galaxy.ansible.com/',
|
||||||
|
})
|
||||||
|
project_update.project.organization.galaxy_credentials.add(public_galaxy)
|
||||||
|
task = RunProjectUpdate()
|
||||||
|
env = task.build_env(project_update, private_data_dir)
|
||||||
|
task.build_extra_vars_file(project_update, private_data_dir)
|
||||||
|
assert task.__vars__['roles_enabled'] is True
|
||||||
|
assert task.__vars__['collections_enabled'] is True
|
||||||
|
assert sorted([
|
||||||
|
(k, v) for k, v in env.items()
|
||||||
|
if k.startswith('ANSIBLE_GALAXY')
|
||||||
|
]) == [
|
||||||
|
('ANSIBLE_GALAXY_SERVER_LIST', 'server0'),
|
||||||
|
('ANSIBLE_GALAXY_SERVER_SERVER0_URL', 'https://galaxy.ansible.com/'),
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_multiple_galaxy_endpoints(self, private_data_dir, project_update):
|
||||||
|
credential_type = CredentialType.defaults['galaxy_api_token']()
|
||||||
|
public_galaxy = Credential(pk=1, credential_type=credential_type, inputs={
|
||||||
|
'url': 'https://galaxy.ansible.com/',
|
||||||
|
})
|
||||||
|
rh = Credential(pk=2, credential_type=credential_type, inputs={
|
||||||
|
'url': 'https://cloud.redhat.com/api/automation-hub/',
|
||||||
|
'auth_url': 'https://sso.redhat.com/example/openid-connect/token/',
|
||||||
|
'token': 'secret123'
|
||||||
|
})
|
||||||
|
project_update.project.organization.galaxy_credentials.add(public_galaxy)
|
||||||
|
project_update.project.organization.galaxy_credentials.add(rh)
|
||||||
|
task = tasks.RunProjectUpdate()
|
||||||
|
env = task.build_env(project_update, private_data_dir)
|
||||||
|
assert sorted([
|
||||||
|
(k, v) for k, v in env.items()
|
||||||
|
if k.startswith('ANSIBLE_GALAXY')
|
||||||
|
]) == [
|
||||||
|
('ANSIBLE_GALAXY_SERVER_LIST', 'server0,server1'),
|
||||||
|
('ANSIBLE_GALAXY_SERVER_SERVER0_URL', 'https://galaxy.ansible.com/'),
|
||||||
|
('ANSIBLE_GALAXY_SERVER_SERVER1_AUTH_URL', 'https://sso.redhat.com/example/openid-connect/token/'), # noqa
|
||||||
|
('ANSIBLE_GALAXY_SERVER_SERVER1_TOKEN', 'secret123'),
|
||||||
|
('ANSIBLE_GALAXY_SERVER_SERVER1_URL', 'https://cloud.redhat.com/api/automation-hub/'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("patch_Organization")
|
||||||
class TestProjectUpdateCredentials(TestJobExecution):
|
class TestProjectUpdateCredentials(TestJobExecution):
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def project_update(self):
|
def project_update(self):
|
||||||
project_update = ProjectUpdate(pk=1, project=Project(pk=1))
|
project_update = ProjectUpdate(
|
||||||
|
pk=1,
|
||||||
|
project=Project(pk=1, organization=Organization(pk=1)),
|
||||||
|
)
|
||||||
project_update.websocket_emit_status = mock.Mock()
|
project_update.websocket_emit_status = mock.Mock()
|
||||||
return project_update
|
return project_update
|
||||||
|
|
||||||
|
|||||||
@@ -571,28 +571,9 @@ AWX_COLLECTIONS_ENABLED = True
|
|||||||
# Follow symlinks when scanning for playbooks
|
# Follow symlinks when scanning for playbooks
|
||||||
AWX_SHOW_PLAYBOOK_LINKS = False
|
AWX_SHOW_PLAYBOOK_LINKS = False
|
||||||
|
|
||||||
# Settings for primary galaxy server, should be set in the UI
|
|
||||||
PRIMARY_GALAXY_URL = ''
|
|
||||||
PRIMARY_GALAXY_USERNAME = ''
|
|
||||||
PRIMARY_GALAXY_TOKEN = ''
|
|
||||||
PRIMARY_GALAXY_PASSWORD = ''
|
|
||||||
PRIMARY_GALAXY_AUTH_URL = ''
|
|
||||||
|
|
||||||
# Settings for the public galaxy server(s).
|
|
||||||
PUBLIC_GALAXY_ENABLED = True
|
|
||||||
PUBLIC_GALAXY_SERVER = {
|
|
||||||
'id': 'galaxy',
|
|
||||||
'url': 'https://galaxy.ansible.com'
|
|
||||||
}
|
|
||||||
|
|
||||||
# Applies to any galaxy server
|
# Applies to any galaxy server
|
||||||
GALAXY_IGNORE_CERTS = False
|
GALAXY_IGNORE_CERTS = False
|
||||||
|
|
||||||
# List of dicts of fallback (additional) Galaxy servers. If configured, these
|
|
||||||
# will be higher precedence than public Galaxy, but lower than primary Galaxy.
|
|
||||||
# Available options: 'id', 'url', 'username', 'password', 'token', 'auth_url'
|
|
||||||
FALLBACK_GALAXY_SERVERS = []
|
|
||||||
|
|
||||||
# Enable bubblewrap support for running jobs (playbook runs only).
|
# Enable bubblewrap support for running jobs (playbook runs only).
|
||||||
# Note: This setting may be overridden by database settings.
|
# Note: This setting may be overridden by database settings.
|
||||||
AWX_PROOT_ENABLED = True
|
AWX_PROOT_ENABLED = True
|
||||||
|
|||||||
@@ -74,31 +74,6 @@ export default ['i18n', function(i18n) {
|
|||||||
AWX_SHOW_PLAYBOOK_LINKS: {
|
AWX_SHOW_PLAYBOOK_LINKS: {
|
||||||
type: 'toggleSwitch',
|
type: 'toggleSwitch',
|
||||||
},
|
},
|
||||||
PRIMARY_GALAXY_URL: {
|
|
||||||
type: 'text',
|
|
||||||
reset: 'PRIMARY_GALAXY_URL',
|
|
||||||
},
|
|
||||||
PRIMARY_GALAXY_USERNAME: {
|
|
||||||
type: 'text',
|
|
||||||
reset: 'PRIMARY_GALAXY_USERNAME',
|
|
||||||
},
|
|
||||||
PRIMARY_GALAXY_PASSWORD: {
|
|
||||||
type: 'sensitive',
|
|
||||||
hasShowInputButton: true,
|
|
||||||
reset: 'PRIMARY_GALAXY_PASSWORD',
|
|
||||||
},
|
|
||||||
PRIMARY_GALAXY_TOKEN: {
|
|
||||||
type: 'sensitive',
|
|
||||||
hasShowInputButton: true,
|
|
||||||
reset: 'PRIMARY_GALAXY_TOKEN',
|
|
||||||
},
|
|
||||||
PRIMARY_GALAXY_AUTH_URL: {
|
|
||||||
type: 'text',
|
|
||||||
reset: 'PRIMARY_GALAXY_AUTH_URL',
|
|
||||||
},
|
|
||||||
PUBLIC_GALAXY_ENABLED: {
|
|
||||||
type: 'toggleSwitch',
|
|
||||||
},
|
|
||||||
AWX_TASK_ENV: {
|
AWX_TASK_ENV: {
|
||||||
type: 'textarea',
|
type: 'textarea',
|
||||||
reset: 'AWX_TASK_ENV',
|
reset: 'AWX_TASK_ENV',
|
||||||
|
|||||||
@@ -4,11 +4,12 @@
|
|||||||
* All Rights Reserved
|
* All Rights Reserved
|
||||||
*************************************************/
|
*************************************************/
|
||||||
|
|
||||||
export default ['$scope', '$rootScope', '$location', '$stateParams',
|
export default ['$scope', '$rootScope', '$location', '$stateParams', 'OrganizationForm',
|
||||||
'OrganizationForm', 'GenerateForm', 'Rest', 'Alert',
|
'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'GetBasePath', 'Wait', 'CreateSelect2',
|
||||||
'ProcessErrors', 'GetBasePath', 'Wait', 'CreateSelect2', '$state','InstanceGroupsService', 'ConfigData',
|
'$state','InstanceGroupsService', 'ConfigData', 'MultiCredentialService', 'defaultGalaxyCredential',
|
||||||
function($scope, $rootScope, $location, $stateParams, OrganizationForm,
|
function($scope, $rootScope, $location, $stateParams, OrganizationForm,
|
||||||
GenerateForm, Rest, Alert, ProcessErrors, GetBasePath, Wait, CreateSelect2, $state, InstanceGroupsService, ConfigData) {
|
GenerateForm, Rest, Alert, ProcessErrors, GetBasePath, Wait, CreateSelect2,
|
||||||
|
$state, InstanceGroupsService, ConfigData, MultiCredentialService, defaultGalaxyCredential) {
|
||||||
|
|
||||||
Rest.setUrl(GetBasePath('organizations'));
|
Rest.setUrl(GetBasePath('organizations'));
|
||||||
Rest.options()
|
Rest.options()
|
||||||
@@ -37,6 +38,8 @@ export default ['$scope', '$rootScope', '$location', '$stateParams',
|
|||||||
|
|
||||||
// apply form definition's default field values
|
// apply form definition's default field values
|
||||||
GenerateForm.applyDefaults(form, $scope);
|
GenerateForm.applyDefaults(form, $scope);
|
||||||
|
|
||||||
|
$scope.credentials = defaultGalaxyCredential || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save
|
// Save
|
||||||
@@ -57,18 +60,32 @@ export default ['$scope', '$rootScope', '$location', '$stateParams',
|
|||||||
const organization_id = data.id,
|
const organization_id = data.id,
|
||||||
instance_group_url = data.related.instance_groups;
|
instance_group_url = data.related.instance_groups;
|
||||||
|
|
||||||
InstanceGroupsService.addInstanceGroups(instance_group_url, $scope.instance_groups)
|
MultiCredentialService
|
||||||
|
.saveRelatedSequentially({
|
||||||
|
related: {
|
||||||
|
credentials: data.related.galaxy_credentials
|
||||||
|
}
|
||||||
|
}, $scope.credentials)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
Wait('stop');
|
InstanceGroupsService.addInstanceGroups(instance_group_url, $scope.instance_groups)
|
||||||
$rootScope.$broadcast("EditIndicatorChange", "organizations", organization_id);
|
.then(() => {
|
||||||
$state.go('organizations.edit', {organization_id: organization_id}, {reload: true});
|
Wait('stop');
|
||||||
})
|
$rootScope.$broadcast("EditIndicatorChange", "organizations", organization_id);
|
||||||
.catch(({data, status}) => {
|
$state.go('organizations.edit', {organization_id: organization_id}, {reload: true});
|
||||||
|
})
|
||||||
|
.catch(({data, status}) => {
|
||||||
|
ProcessErrors($scope, data, status, form, {
|
||||||
|
hdr: 'Error!',
|
||||||
|
msg: 'Failed to save instance groups. POST returned status: ' + status
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}).catch(({data, status}) => {
|
||||||
ProcessErrors($scope, data, status, form, {
|
ProcessErrors($scope, data, status, form, {
|
||||||
hdr: 'Error!',
|
hdr: 'Error!',
|
||||||
msg: 'Failed to save instance groups. POST returned status: ' + status
|
msg: 'Failed to save Galaxy credentials. POST returned status: ' + status
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
})
|
})
|
||||||
.catch(({data, status}) => {
|
.catch(({data, status}) => {
|
||||||
let explanation = _.has(data, "name") ? data.name[0] : "";
|
let explanation = _.has(data, "name") ? data.name[0] : "";
|
||||||
|
|||||||
@@ -6,10 +6,12 @@
|
|||||||
|
|
||||||
export default ['$scope', '$location', '$stateParams', 'isOrgAdmin', 'isNotificationAdmin',
|
export default ['$scope', '$location', '$stateParams', 'isOrgAdmin', 'isNotificationAdmin',
|
||||||
'OrganizationForm', 'Rest', 'ProcessErrors', 'Prompt', 'i18n', 'isOrgAuditor',
|
'OrganizationForm', 'Rest', 'ProcessErrors', 'Prompt', 'i18n', 'isOrgAuditor',
|
||||||
'GetBasePath', 'Wait', '$state', 'ToggleNotification', 'CreateSelect2', 'InstanceGroupsService', 'InstanceGroupsData', 'ConfigData',
|
'GetBasePath', 'Wait', '$state', 'ToggleNotification', 'CreateSelect2', 'InstanceGroupsService',
|
||||||
|
'InstanceGroupsData', 'ConfigData', 'GalaxyCredentialsData', 'MultiCredentialService',
|
||||||
function($scope, $location, $stateParams, isOrgAdmin, isNotificationAdmin,
|
function($scope, $location, $stateParams, isOrgAdmin, isNotificationAdmin,
|
||||||
OrganizationForm, Rest, ProcessErrors, Prompt, i18n, isOrgAuditor,
|
OrganizationForm, Rest, ProcessErrors, Prompt, i18n, isOrgAuditor,
|
||||||
GetBasePath, Wait, $state, ToggleNotification, CreateSelect2, InstanceGroupsService, InstanceGroupsData, ConfigData) {
|
GetBasePath, Wait, $state, ToggleNotification, CreateSelect2, InstanceGroupsService,
|
||||||
|
InstanceGroupsData, ConfigData, GalaxyCredentialsData, MultiCredentialService) {
|
||||||
|
|
||||||
let form = OrganizationForm(),
|
let form = OrganizationForm(),
|
||||||
defaultUrl = GetBasePath('organizations'),
|
defaultUrl = GetBasePath('organizations'),
|
||||||
@@ -29,6 +31,7 @@ export default ['$scope', '$location', '$stateParams', 'isOrgAdmin', 'isNotifica
|
|||||||
});
|
});
|
||||||
|
|
||||||
$scope.instance_groups = InstanceGroupsData;
|
$scope.instance_groups = InstanceGroupsData;
|
||||||
|
$scope.credentials = GalaxyCredentialsData;
|
||||||
const virtualEnvs = ConfigData.custom_virtualenvs || [];
|
const virtualEnvs = ConfigData.custom_virtualenvs || [];
|
||||||
$scope.custom_virtualenvs_visible = virtualEnvs.length > 1;
|
$scope.custom_virtualenvs_visible = virtualEnvs.length > 1;
|
||||||
$scope.custom_virtualenvs_options = virtualEnvs.filter(
|
$scope.custom_virtualenvs_options = virtualEnvs.filter(
|
||||||
@@ -100,7 +103,14 @@ export default ['$scope', '$location', '$stateParams', 'isOrgAdmin', 'isNotifica
|
|||||||
Rest.setUrl(defaultUrl + id + '/');
|
Rest.setUrl(defaultUrl + id + '/');
|
||||||
Rest.put(params)
|
Rest.put(params)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
InstanceGroupsService.editInstanceGroups(instance_group_url, $scope.instance_groups)
|
MultiCredentialService
|
||||||
|
.saveRelatedSequentially({
|
||||||
|
related: {
|
||||||
|
credentials: $scope.organization_obj.related.galaxy_credentials
|
||||||
|
}
|
||||||
|
}, $scope.credentials)
|
||||||
|
.then(() => {
|
||||||
|
InstanceGroupsService.editInstanceGroups(instance_group_url, $scope.instance_groups)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
Wait('stop');
|
Wait('stop');
|
||||||
$state.go($state.current, {}, { reload: true });
|
$state.go($state.current, {}, { reload: true });
|
||||||
@@ -111,6 +121,12 @@ export default ['$scope', '$location', '$stateParams', 'isOrgAdmin', 'isNotifica
|
|||||||
msg: 'Failed to update instance groups. POST returned status: ' + status
|
msg: 'Failed to update instance groups. POST returned status: ' + status
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}).catch(({data, status}) => {
|
||||||
|
ProcessErrors($scope, data, status, form, {
|
||||||
|
hdr: 'Error!',
|
||||||
|
msg: 'Failed to save Galaxy credentials. POST returned status: ' + status
|
||||||
|
});
|
||||||
|
});
|
||||||
$scope.organization_name = $scope.name;
|
$scope.organization_name = $scope.name;
|
||||||
main = params;
|
main = params;
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
export default ['templateUrl', '$window', function(templateUrl, $window) {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
scope: {
|
||||||
|
galaxyCredentials: '='
|
||||||
|
},
|
||||||
|
templateUrl: templateUrl('organizations/galaxy-credentials-multiselect/galaxy-credentials-modal/galaxy-credentials-modal'),
|
||||||
|
|
||||||
|
link: function(scope, element) {
|
||||||
|
|
||||||
|
$('#galaxy-credentials-modal').on('hidden.bs.modal', function () {
|
||||||
|
$('#galaxy-credentials-modal').off('hidden.bs.modal');
|
||||||
|
$(element).remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
scope.showModal = function() {
|
||||||
|
$('#galaxy-credentials-modal').modal('show');
|
||||||
|
};
|
||||||
|
|
||||||
|
scope.destroyModal = function() {
|
||||||
|
$('#galaxy-credentials-modal').modal('hide');
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
controller: ['$scope', '$compile', 'QuerySet', 'GetBasePath','generateList', 'CredentialList', function($scope, $compile, qs, GetBasePath, GenerateList, CredentialList) {
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
|
||||||
|
$scope.credential_queryset = {
|
||||||
|
order_by: 'name',
|
||||||
|
page_size: 5,
|
||||||
|
credential_type__kind: 'galaxy'
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.credential_default_params = {
|
||||||
|
order_by: 'name',
|
||||||
|
page_size: 5,
|
||||||
|
credential_type__kind: 'galaxy'
|
||||||
|
};
|
||||||
|
|
||||||
|
qs.search(GetBasePath('credentials'), $scope.credential_queryset)
|
||||||
|
.then(res => {
|
||||||
|
$scope.credential_dataset = res.data;
|
||||||
|
$scope.credentials = $scope.credential_dataset.results;
|
||||||
|
|
||||||
|
let credentialList = _.cloneDeep(CredentialList);
|
||||||
|
|
||||||
|
credentialList.listTitle = false;
|
||||||
|
credentialList.well = false;
|
||||||
|
credentialList.multiSelect = true;
|
||||||
|
credentialList.multiSelectPreview = {
|
||||||
|
selectedRows: 'credTags',
|
||||||
|
availableRows: 'credentials'
|
||||||
|
};
|
||||||
|
credentialList.fields.name.ngClick = "linkoutCredential(credential)";
|
||||||
|
credentialList.fields.name.columnClass = 'col-md-11 col-sm-11 col-xs-11';
|
||||||
|
delete credentialList.fields.consumed_capacity;
|
||||||
|
delete credentialList.fields.jobs_running;
|
||||||
|
|
||||||
|
let html = `${GenerateList.build({
|
||||||
|
list: credentialList,
|
||||||
|
input_type: 'galaxy-credentials-modal-body',
|
||||||
|
hideViewPerPage: true,
|
||||||
|
mode: 'lookup'
|
||||||
|
})}`;
|
||||||
|
|
||||||
|
$scope.list = credentialList;
|
||||||
|
$('#galaxy-credentials-modal-body').append($compile(html)($scope));
|
||||||
|
|
||||||
|
if ($scope.galaxyCredentials) {
|
||||||
|
$scope.galaxyCredentials = $scope.galaxyCredentials.map( (item) => {
|
||||||
|
item.isSelected = true;
|
||||||
|
if (!$scope.credTags) {
|
||||||
|
$scope.credTags = [];
|
||||||
|
}
|
||||||
|
$scope.credTags.push(item);
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.showModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.$watch('credentials', function(){
|
||||||
|
angular.forEach($scope.credentials, function(credentialRow) {
|
||||||
|
angular.forEach($scope.credTags, function(selectedCredential){
|
||||||
|
if(selectedCredential.id === credentialRow.id) {
|
||||||
|
credentialRow.isSelected = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
||||||
|
|
||||||
|
$scope.$on("selectedOrDeselected", function(e, value) {
|
||||||
|
let item = value.value;
|
||||||
|
if (value.isSelected) {
|
||||||
|
if(!$scope.credTags) {
|
||||||
|
$scope.credTags = [];
|
||||||
|
}
|
||||||
|
$scope.credTags.push(item);
|
||||||
|
} else {
|
||||||
|
_.remove($scope.credTags, { id: item.id });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.linkoutCredential = function(credential) {
|
||||||
|
$window.open('/#/credentials/' + credential.id,'_blank');
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.cancelForm = function() {
|
||||||
|
$scope.destroyModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.saveForm = function() {
|
||||||
|
$scope.galaxyCredentials = $scope.credTags;
|
||||||
|
$scope.destroyModal();
|
||||||
|
};
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}];
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<div id="galaxy-credentials-modal" class="Lookup modal fade">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header Form-header">
|
||||||
|
<div class="Form-title Form-title--uppercase" translate>Select Galaxy Credentials</div>
|
||||||
|
<div class="Form-header--fields"></div>
|
||||||
|
<div class="Form-exitHolder">
|
||||||
|
<button aria-label="{{'Close'|translate}}" type="button" class="Form-exit" ng-click="cancelForm()">
|
||||||
|
<i class="fa fa-times-circle"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div id="galaxy-credentials-modal-body"> {{ credential }} </div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" ng-click="cancelForm()" class="btn btn-default" translate>CANCEL</button>
|
||||||
|
<button type="button" ng-click="saveForm()" ng-disabled="!credentials || credentials.length === 0" class="Lookup-save btn btn-primary" translate>SAVE</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
export default ['$scope',
|
||||||
|
function($scope) {
|
||||||
|
|
||||||
|
$scope.galaxyCredentialsTags = [];
|
||||||
|
|
||||||
|
$scope.$watch('galaxyCredentials', function() {
|
||||||
|
$scope.galaxyCredentialsTags = $scope.galaxyCredentials;
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
$scope.deleteTag = function(tag){
|
||||||
|
_.remove($scope.galaxyCredentials, {id: tag.id});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#instance-groups-panel {
|
||||||
|
table {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.List-header {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.isActive {
|
||||||
|
border-left: 10px solid @list-row-select-bord;
|
||||||
|
}
|
||||||
|
.instances-list,
|
||||||
|
.instance-jobs-list {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import galaxyCredentialsMultiselectController from './galaxy-credentials-multiselect.controller';
|
||||||
|
export default ['templateUrl', '$compile',
|
||||||
|
function(templateUrl, $compile) {
|
||||||
|
return {
|
||||||
|
scope: {
|
||||||
|
galaxyCredentials: '=',
|
||||||
|
fieldIsDisabled: '='
|
||||||
|
},
|
||||||
|
restrict: 'E',
|
||||||
|
templateUrl: templateUrl('organizations/galaxy-credentials-multiselect/galaxy-credentials'),
|
||||||
|
controller: galaxyCredentialsMultiselectController,
|
||||||
|
link: function(scope) {
|
||||||
|
scope.openInstanceGroupsModal = function() {
|
||||||
|
$('#content-container').append($compile('<galaxy-credentials-modal galaxy-credentials="galaxyCredentials"></galaxy-credentials-modal>')(scope));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<div class="input-group Form-mixedInputGroup">
|
||||||
|
<span class="input-group-btn input-group-prepend Form-variableHeightButtonGroup">
|
||||||
|
<button aria-label="{{'Open Galaxy credentials'|translate}}" type="button" class="Form-lookupButton Form-lookupButton--variableHeight btn btn-default" ng-click="openInstanceGroupsModal()"
|
||||||
|
ng-disabled="fieldIsDisabled">
|
||||||
|
<i class="fa fa-search"></i>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<span id="InstanceGroups" class="form-control Form-textInput Form-textInput--variableHeight input-medium lookup LabelList-lookupTags"
|
||||||
|
ng-disabled="fieldIsDisabled"
|
||||||
|
ng-class="{'LabelList-lookupTags--disabled' : fieldIsDisabled}">
|
||||||
|
<div ng-if="!fieldIsDisabled" class="LabelList-tagContainer" ng-repeat="tag in galaxyCredentialsTags">
|
||||||
|
<at-tag tag="tag.name" remove-tag="deleteTag(tag)"></at-tag>
|
||||||
|
</div>
|
||||||
|
<div ng-if="fieldIsDisabled" class="LabelList-tag" ng-repeat="tag in galaxyCredentialsTags">
|
||||||
|
<span class="LabelList-name">{{tag.name | sanitize}}</span>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
@@ -12,8 +12,10 @@ import organizationsLinkout from './linkout/main';
|
|||||||
import OrganizationsLinkoutStates from './linkout/organizations-linkout.route';
|
import OrganizationsLinkoutStates from './linkout/organizations-linkout.route';
|
||||||
import OrganizationForm from './organizations.form';
|
import OrganizationForm from './organizations.form';
|
||||||
import OrganizationList from './organizations.list';
|
import OrganizationList from './organizations.list';
|
||||||
import { N_ } from '../i18n';
|
import galaxyCredentialsMultiselect from './galaxy-credentials-multiselect/galaxy-credentials.directive';
|
||||||
|
import galaxyCredentialsModal from './galaxy-credentials-multiselect/galaxy-credentials-modal/galaxy-credentials-modal.directive';
|
||||||
|
|
||||||
|
import { N_ } from '../i18n';
|
||||||
|
|
||||||
export default
|
export default
|
||||||
angular.module('Organizations', [
|
angular.module('Organizations', [
|
||||||
@@ -24,6 +26,8 @@ angular.module('Organizations', [
|
|||||||
.controller('OrganizationsEdit', OrganizationsEdit)
|
.controller('OrganizationsEdit', OrganizationsEdit)
|
||||||
.factory('OrganizationForm', OrganizationForm)
|
.factory('OrganizationForm', OrganizationForm)
|
||||||
.factory('OrganizationList', OrganizationList)
|
.factory('OrganizationList', OrganizationList)
|
||||||
|
.directive('galaxyCredentialsMultiselect', galaxyCredentialsMultiselect)
|
||||||
|
.directive('galaxyCredentialsModal', galaxyCredentialsModal)
|
||||||
.config(['$stateProvider', 'stateDefinitionsProvider', '$stateExtenderProvider',
|
.config(['$stateProvider', 'stateDefinitionsProvider', '$stateExtenderProvider',
|
||||||
function($stateProvider, stateDefinitionsProvider, $stateExtenderProvider) {
|
function($stateProvider, stateDefinitionsProvider, $stateExtenderProvider) {
|
||||||
let stateExtender = $stateExtenderProvider.$get(),
|
let stateExtender = $stateExtenderProvider.$get(),
|
||||||
@@ -67,7 +71,29 @@ angular.module('Organizations', [
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
}]
|
}],
|
||||||
|
defaultGalaxyCredential: ['Rest', 'GetBasePath', 'ProcessErrors',
|
||||||
|
function(Rest, GetBasePath, ProcessErrors){
|
||||||
|
Rest.setUrl(GetBasePath('credentials'));
|
||||||
|
return Rest.get({
|
||||||
|
params: {
|
||||||
|
credential_type__kind: 'galaxy',
|
||||||
|
managed_by_tower: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(({data}) => {
|
||||||
|
if (data.results.length > 0) {
|
||||||
|
return data.results;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(({data, status}) => {
|
||||||
|
ProcessErrors(null, data, status, null, {
|
||||||
|
hdr: 'Error!',
|
||||||
|
msg: 'Failed to get default Galaxy credential. GET returned ' +
|
||||||
|
'status: ' + status
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
edit: {
|
edit: {
|
||||||
ConfigData: ['ConfigService', 'ProcessErrors', (ConfigService, ProcessErrors) => {
|
ConfigData: ['ConfigService', 'ProcessErrors', (ConfigService, ProcessErrors) => {
|
||||||
@@ -81,6 +107,24 @@ angular.module('Organizations', [
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}],
|
}],
|
||||||
|
GalaxyCredentialsData: ['$stateParams', 'Rest', 'GetBasePath', 'ProcessErrors',
|
||||||
|
function($stateParams, Rest, GetBasePath, ProcessErrors){
|
||||||
|
let path = `${GetBasePath('organizations')}${$stateParams.organization_id}/galaxy_credentials/`;
|
||||||
|
Rest.setUrl(path);
|
||||||
|
return Rest.get()
|
||||||
|
.then(({data}) => {
|
||||||
|
if (data.results.length > 0) {
|
||||||
|
return data.results;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(({data, status}) => {
|
||||||
|
ProcessErrors(null, data, status, null, {
|
||||||
|
hdr: 'Error!',
|
||||||
|
msg: 'Failed to get credentials. GET returned ' +
|
||||||
|
'status: ' + status
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}],
|
||||||
InstanceGroupsData: ['$stateParams', 'Rest', 'GetBasePath', 'ProcessErrors',
|
InstanceGroupsData: ['$stateParams', 'Rest', 'GetBasePath', 'ProcessErrors',
|
||||||
function($stateParams, Rest, GetBasePath, ProcessErrors){
|
function($stateParams, Rest, GetBasePath, ProcessErrors){
|
||||||
let path = `${GetBasePath('organizations')}${$stateParams.organization_id}/instance_groups/`;
|
let path = `${GetBasePath('organizations')}${$stateParams.organization_id}/instance_groups/`;
|
||||||
|
|||||||
@@ -55,6 +55,15 @@ export default ['NotificationsList', 'i18n',
|
|||||||
ngDisabled: '!(organization_obj.summary_fields.user_capabilities.edit || canAdd)',
|
ngDisabled: '!(organization_obj.summary_fields.user_capabilities.edit || canAdd)',
|
||||||
ngShow: 'custom_virtualenvs_visible'
|
ngShow: 'custom_virtualenvs_visible'
|
||||||
},
|
},
|
||||||
|
credential: {
|
||||||
|
label: i18n._('Galaxy Credentials'),
|
||||||
|
type: 'custom',
|
||||||
|
awPopOver: "<p>" + i18n._("Select Galaxy credentials. The selection order sets the order in which Tower will download roles/collections using `ansible-galaxy`.") + "</p>",
|
||||||
|
dataTitle: i18n._('Galaxy Credentials'),
|
||||||
|
dataContainer: 'body',
|
||||||
|
dataPlacement: 'right',
|
||||||
|
control: '<galaxy-credentials-multiselect galaxy-credentials="credentials" field-is-disabled="!(organization_obj.summary_fields.user_capabilities.edit || canAdd) || (!current_user.is_superuser && isOrgAdmin)"></galaxy-credentials-multiselect>',
|
||||||
|
},
|
||||||
max_hosts: {
|
max_hosts: {
|
||||||
label: i18n._('Max Hosts'),
|
label: i18n._('Max Hosts'),
|
||||||
type: 'number',
|
type: 'number',
|
||||||
@@ -69,7 +78,7 @@ export default ['NotificationsList', 'i18n',
|
|||||||
awPopOver: "<p>" + i18n._("The maximum number of hosts allowed to be managed by this organization. Value defaults to 0 which means no limit. Refer to the Ansible documentation for more details.") + "</p>",
|
awPopOver: "<p>" + i18n._("The maximum number of hosts allowed to be managed by this organization. Value defaults to 0 which means no limit. Refer to the Ansible documentation for more details.") + "</p>",
|
||||||
ngDisabled: '!current_user.is_superuser',
|
ngDisabled: '!current_user.is_superuser',
|
||||||
ngShow: 'BRAND_NAME === "Tower"'
|
ngShow: 'BRAND_NAME === "Tower"'
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
buttons: { //for now always generates <button> tags
|
buttons: { //for now always generates <button> tags
|
||||||
|
|||||||
@@ -46,6 +46,52 @@ function MultiCredentialService (Rest, ProcessErrors, $q, GetBasePath) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.saveRelatedSequentially = ({ related }, credentials) => {
|
||||||
|
Rest.setUrl(related.credentials);
|
||||||
|
return Rest
|
||||||
|
.get()
|
||||||
|
.then(res => {
|
||||||
|
const { data: { results = [] } } = res;
|
||||||
|
const updatedCredentialIds = (credentials || []).map(({ id }) => id);
|
||||||
|
const currentCredentialIds = results.map(({ id }) => id);
|
||||||
|
const credentialIdsToAssociate = [];
|
||||||
|
const credentialIdsToDisassociate = [];
|
||||||
|
let disassociateRemainingIds = false;
|
||||||
|
|
||||||
|
currentCredentialIds.forEach((currentId, position) => {
|
||||||
|
if (!disassociateRemainingIds && updatedCredentialIds[position] !== currentId) {
|
||||||
|
disassociateRemainingIds = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (disassociateRemainingIds) {
|
||||||
|
credentialIdsToDisassociate.push(currentId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
updatedCredentialIds.forEach(updatedId => {
|
||||||
|
if (credentialIdsToDisassociate.includes(updatedId)) {
|
||||||
|
credentialIdsToAssociate.push(updatedId);
|
||||||
|
} else if (!currentCredentialIds.includes(updatedId)) {
|
||||||
|
credentialIdsToAssociate.push(updatedId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let disassociationPromise = Promise.resolve();
|
||||||
|
credentialIdsToDisassociate.forEach(id => {
|
||||||
|
disassociationPromise = disassociationPromise.then(() => disassociate({ related }, id));
|
||||||
|
});
|
||||||
|
|
||||||
|
return disassociationPromise
|
||||||
|
.then(() => {
|
||||||
|
let associationPromise = Promise.resolve();
|
||||||
|
credentialIdsToAssociate.forEach(id => {
|
||||||
|
associationPromise = associationPromise.then(() => associate({ related }, id));
|
||||||
|
});
|
||||||
|
return associationPromise;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
this.getRelated = ({ related }, params = { permitted: [] }) => {
|
this.getRelated = ({ related }, params = { permitted: [] }) => {
|
||||||
Rest.setUrl(related.credentials);
|
Rest.setUrl(related.credentials);
|
||||||
return Rest
|
return Rest
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ def config_cred_from_kind(kind):
|
|||||||
|
|
||||||
credential_type_name_to_config_kind_map = {
|
credential_type_name_to_config_kind_map = {
|
||||||
'amazon web services': 'aws',
|
'amazon web services': 'aws',
|
||||||
|
'ansible galaxy/automation hub api token': 'galaxy',
|
||||||
'ansible tower': 'tower',
|
'ansible tower': 'tower',
|
||||||
'google compute engine': 'gce',
|
'google compute engine': 'gce',
|
||||||
'insights': 'insights',
|
'insights': 'insights',
|
||||||
|
|||||||
@@ -22,6 +22,24 @@ class Organization(HasCreate, HasInstanceGroups, HasNotifications, base.Base):
|
|||||||
with suppress(exc.NoContent):
|
with suppress(exc.NoContent):
|
||||||
self.related.users.post(user)
|
self.related.users.post(user)
|
||||||
|
|
||||||
|
def add_galaxy_credential(self, credential):
|
||||||
|
if isinstance(credential, page.Page):
|
||||||
|
credential = credential.json
|
||||||
|
with suppress(exc.NoContent):
|
||||||
|
self.related.galaxy_credentials.post({
|
||||||
|
"id": credential.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
def remove_galaxy_credential(self, credential):
|
||||||
|
if isinstance(credential, page.Page):
|
||||||
|
credential = credential.json
|
||||||
|
with suppress(exc.NoContent):
|
||||||
|
self.related.galaxy_credentials.post({
|
||||||
|
"id": credential.id,
|
||||||
|
"disassociate": True,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
def payload(self, **kwargs):
|
def payload(self, **kwargs):
|
||||||
payload = PseudoNamespace(name=kwargs.get('name') or 'Organization - {}'.format(random_title()),
|
payload = PseudoNamespace(name=kwargs.get('name') or 'Organization - {}'.format(random_title()),
|
||||||
description=kwargs.get('description') or random_title(10))
|
description=kwargs.get('description') or random_title(10))
|
||||||
|
|||||||
@@ -85,28 +85,22 @@ job runs. It will populate the cache id set by the last "check" type update.
|
|||||||
|
|
||||||
### Galaxy Server Selection
|
### Galaxy Server Selection
|
||||||
|
|
||||||
Ansible core default settings will download collections from the public
|
For details on how Galaxy servers are configured in Ansible in general see:
|
||||||
Galaxy server at `https://galaxy.ansible.com`. For details on
|
|
||||||
how Galaxy servers are configured in Ansible in general see:
|
|
||||||
|
|
||||||
https://docs.ansible.com/ansible/devel/user_guide/collections_using.html
|
https://docs.ansible.com/ansible/devel/user_guide/collections_using.html
|
||||||
(if "devel" link goes stale in the future, it is for Ansible 2.9)
|
(if "devel" link goes stale in the future, it is for Ansible 2.9)
|
||||||
|
|
||||||
You can set a different server to be the primary Galaxy server to download
|
You can specify a list of zero or more servers to download roles and
|
||||||
roles and collections from in AWX project updates.
|
collections from for AWX Project Updates. This is done by associating Galaxy
|
||||||
This is done via the setting `PRIMARY_GALAXY_URL` and similar
|
credentials (in sequential order) via the API at
|
||||||
`PRIMARY_GALAXY_xxxx` settings for authentication.
|
`/api/v2/organizations/N/galaxy_credentials/`. Authentication
|
||||||
|
via an API token is optional (i.e., https://galaxy.ansible.com/), but other
|
||||||
|
content sources (such as Red Hat Ansible Automation Hub) require proper
|
||||||
|
configuration of the Auth URL and Token.
|
||||||
|
|
||||||
If the `PRIMARY_GALAXY_URL` setting is not blank, then the server list is defined
|
If no credentials are defined at this endpoint for an Organization, then roles and
|
||||||
to be `primary_galaxy,galaxy`. The `primary_galaxy` server definition uses the URL
|
collections will *not* be installed based on requirements.yml for Project Updates
|
||||||
from those settings, as well as username, password, and/or token and auth_url if applicable.
|
in that Organization.
|
||||||
the `galaxy` server definition uses public Galaxy (`https://galaxy.ansible.com`)
|
|
||||||
with no authentication.
|
|
||||||
|
|
||||||
This configuration causes requirements to be downloaded from the user-specified
|
|
||||||
primary galaxy server if they are available there. If a requirement is
|
|
||||||
not available from the primary galaxy server, then it will fallback to
|
|
||||||
downloading it from the public Galaxy server.
|
|
||||||
|
|
||||||
Even when these settings are enabled, this can still be bypassed for a specific
|
Even when these settings are enabled, this can still be bypassed for a specific
|
||||||
requirement by using the `source:` option, as described in Ansible documentation.
|
requirement by using the `source:` option, as described in Ansible documentation.
|
||||||
|
|||||||
Reference in New Issue
Block a user