diff --git a/awx/api/serializers.py b/awx/api/serializers.py index dcf403c47e..68e507a6d2 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -107,8 +107,8 @@ SUMMARIZABLE_FK_FIELDS = { 'insights_credential_id',), 'host': DEFAULT_SUMMARY_FIELDS, 'group': DEFAULT_SUMMARY_FIELDS, - 'default_environment': ('id', 'organization_id', 'image', 'description'), - 'execution_environment': ('id', 'organization_id', 'image', 'description'), + 'default_environment': DEFAULT_SUMMARY_FIELDS + ('image',), + 'execution_environment': DEFAULT_SUMMARY_FIELDS + ('image',), 'project': DEFAULT_SUMMARY_FIELDS + ('status', 'scm_type'), 'source_project': DEFAULT_SUMMARY_FIELDS + ('status', 'scm_type'), 'project_update': DEFAULT_SUMMARY_FIELDS + ('status', 'failed',), @@ -1365,7 +1365,7 @@ class ExecutionEnvironmentSerializer(BaseSerializer): class Meta: model = ExecutionEnvironment - fields = ('*', '-name', 'organization', 'image', 'managed_by_tower', 'credential') + fields = ('*', 'organization', 'image', 'managed_by_tower', 'credential') def get_related(self, obj): res = super(ExecutionEnvironmentSerializer, self).get_related(obj) @@ -1395,7 +1395,7 @@ class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer): class Meta: model = Project fields = ('*', 'organization', 'scm_update_on_launch', - 'scm_update_cache_timeout', 'allow_override', 'custom_virtualenv',) + \ + 'scm_update_cache_timeout', 'allow_override', 'custom_virtualenv', 'default_environment') + \ ('last_update_failed', 'last_updated') # Backwards compatibility def get_related(self, obj): @@ -1420,6 +1420,9 @@ class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer): if obj.organization: res['organization'] = self.reverse('api:organization_detail', kwargs={'pk': obj.organization.pk}) + if obj.default_environment: + res['default_environment'] = self.reverse('api:execution_environment_detail', + kwargs={'pk': obj.default_environment_id}) # Backwards compatibility. if obj.current_update: res['current_update'] = self.reverse('api:project_update_detail', diff --git a/awx/conf/fields.py b/awx/conf/fields.py index 7c9a94969d..90f495e293 100644 --- a/awx/conf/fields.py +++ b/awx/conf/fields.py @@ -14,6 +14,7 @@ from rest_framework.fields import ( # noqa BooleanField, CharField, ChoiceField, DictField, DateTimeField, EmailField, IntegerField, ListField, NullBooleanField ) +from rest_framework.serializers import PrimaryKeyRelatedField logger = logging.getLogger('awx.conf.fields') diff --git a/awx/main/access.py b/awx/main/access.py index 24e6bbc569..f1edc58006 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1320,6 +1320,8 @@ class ExecutionEnvironmentAccess(BaseAccess): """ model = ExecutionEnvironment + select_related = ('organization',) + prefetch_related = ('organization__admin_role',) def filtered_queryset(self): return ExecutionEnvironment.objects.filter( diff --git a/awx/main/conf.py b/awx/main/conf.py index 6bf86db214..f46371e22b 100644 --- a/awx/main/conf.py +++ b/awx/main/conf.py @@ -10,6 +10,7 @@ from rest_framework.fields import FloatField # Tower from awx.conf import fields, register, register_validate +from awx.main.models import ExecutionEnvironment logger = logging.getLogger('awx.main.conf') @@ -176,6 +177,18 @@ register( read_only=True, ) +register( + 'DEFAULT_EXECUTION_ENVIRONMENT', + field_class=fields.PrimaryKeyRelatedField, + allow_null=True, + default=None, + queryset=ExecutionEnvironment.objects.all(), + label=_('Global default execution environment'), + help_text=_('.'), + category=_('System'), + category_slug='system', +) + register( 'CUSTOM_VENV_PATHS', field_class=fields.StringListPathField, diff --git a/awx/main/migrations/0125_more_ee_modeling_changes.py b/awx/main/migrations/0125_more_ee_modeling_changes.py new file mode 100644 index 0000000000..3d5a076d8d --- /dev/null +++ b/awx/main/migrations/0125_more_ee_modeling_changes.py @@ -0,0 +1,41 @@ +# Generated by Django 2.2.16 on 2020-11-19 16:20 +import uuid + +import awx.main.fields +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0124_execution_environments'), + ] + + operations = [ + migrations.AlterModelOptions( + name='executionenvironment', + options={'ordering': ('-created',)}, + ), + migrations.AddField( + model_name='executionenvironment', + name='name', + field=models.CharField(default=uuid.uuid4, max_length=512, unique=True), + preserve_default=False, + ), + migrations.AddField( + model_name='organization', + name='execution_environment_admin_role', + field=awx.main.fields.ImplicitRoleField(editable=False, null='True', on_delete=django.db.models.deletion.CASCADE, parent_role='admin_role', related_name='+', to='main.Role'), + preserve_default='True', + ), + migrations.AddField( + model_name='project', + name='default_environment', + field=models.ForeignKey(blank=True, default=None, help_text='The default execution environment for jobs run using this project.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='main.ExecutionEnvironment'), + ), + migrations.AlterUniqueTogether( + name='executionenvironment', + unique_together=set(), + ), + ] diff --git a/awx/main/models/credential/__init__.py b/awx/main/models/credential/__init__.py index ebab3bc22f..00af665969 100644 --- a/awx/main/models/credential/__init__.py +++ b/awx/main/models/credential/__init__.py @@ -1133,7 +1133,6 @@ ManagedCredentialType( }, ) - ManagedCredentialType( namespace='kubernetes_bearer_token', kind='kubernetes', @@ -1165,6 +1164,37 @@ ManagedCredentialType( } ) +ManagedCredentialType( + namespace='registry', + kind='registry', + name=ugettext_noop('Container Registry'), + inputs={ + 'fields': [{ + 'id': 'host', + 'label': ugettext_noop('Authentication URL'), + 'type': 'string', + 'help_text': ugettext_noop('Authentication endpoint for the container registry.'), + }, { + 'id': 'username', + 'label': ugettext_noop('Username'), + 'type': 'string', + }, { + 'id': 'password', + 'label': ugettext_noop('Password'), + 'type': 'string', + 'secret': True, + }, { + 'id': 'token', + 'label': ugettext_noop('Access Token'), + 'type': 'string', + 'secret': True, + 'help_text': ugettext_noop('A token to use to authenticate with. ' + 'This should not be set if username/password are being used.'), + }], + 'required': ['host'], + } +) + ManagedCredentialType( namespace='galaxy_api_token', diff --git a/awx/main/models/execution_environments.py b/awx/main/models/execution_environments.py index bdbe75eb49..51c7c251ea 100644 --- a/awx/main/models/execution_environments.py +++ b/awx/main/models/execution_environments.py @@ -2,16 +2,15 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ from awx.api.versioning import reverse -from awx.main.models.base import PrimordialModel +from awx.main.models.base import CommonModel __all__ = ['ExecutionEnvironment'] -class ExecutionEnvironment(PrimordialModel): +class ExecutionEnvironment(CommonModel): class Meta: - unique_together = ('organization', 'image') - ordering = (models.F('organization_id').asc(nulls_first=True), 'image') + ordering = ('-created',) organization = models.ForeignKey( 'Organization', diff --git a/awx/main/models/organization.py b/awx/main/models/organization.py index 3730fe9af1..bdf1e38d7d 100644 --- a/awx/main/models/organization.py +++ b/awx/main/models/organization.py @@ -95,6 +95,9 @@ class Organization(CommonModel, NotificationFieldsModel, ResourceMixin, CustomVi job_template_admin_role = ImplicitRoleField( parent_role='admin_role', ) + execution_environment_admin_role = ImplicitRoleField( + parent_role='admin_role', + ) auditor_role = ImplicitRoleField( parent_role='singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR, ) diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index 65fb8304ce..ec14a2ef76 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -259,6 +259,15 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin, CustomVirtualEn app_label = 'main' ordering = ('id',) + default_environment = models.ForeignKey( + 'ExecutionEnvironment', + null=True, + blank=True, + default=None, + on_delete=models.SET_NULL, + related_name='+', + help_text=_('The default execution environment for jobs run using this project.'), + ) scm_update_on_launch = models.BooleanField( default=False, help_text=_('Update the project when a job is launched that uses the project.'), diff --git a/awx/main/models/rbac.py b/awx/main/models/rbac.py index 67d21e873d..fe8d622ac6 100644 --- a/awx/main/models/rbac.py +++ b/awx/main/models/rbac.py @@ -40,6 +40,7 @@ role_names = { 'inventory_admin_role': _('Inventory Admin'), 'credential_admin_role': _('Credential Admin'), 'job_template_admin_role': _('Job Template Admin'), + 'execution_environment_admin_role': _('Execution Environment Admin'), 'workflow_admin_role': _('Workflow Admin'), 'notification_admin_role': _('Notification Admin'), 'auditor_role': _('Auditor'), @@ -60,6 +61,7 @@ role_descriptions = { 'inventory_admin_role': _('Can manage all inventories of the %s'), 'credential_admin_role': _('Can manage all credentials of the %s'), 'job_template_admin_role': _('Can manage all job templates of the %s'), + 'execution_environment_admin_role': _('Can manage all execution environments of the %s'), 'workflow_admin_role': _('Can manage all workflows of the %s'), 'notification_admin_role': _('Can manage all notifications of the %s'), 'auditor_role': _('Can view all aspects of the %s'), diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index f4a9e1ba45..53406be172 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -40,6 +40,7 @@ from awx.main.dispatch import get_local_queuename from awx.main.dispatch.control import Control as ControlDispatcher from awx.main.registrar import activity_stream_registrar from awx.main.models.mixins import ResourceMixin, TaskManagerUnifiedJobMixin, ExecutionEnvironmentMixin +from awx.main.models.execution_environments import ExecutionEnvironment from awx.main.utils import ( camelcase_to_underscore, get_model_for_type, encrypt_dict, decrypt_field, _inventory_updates, @@ -338,6 +339,23 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, ExecutionEn from awx.main.models.notifications import NotificationTemplate return NotificationTemplate.objects.none() + def resolve_execution_environment(self): + """ + Return the execution environment that should be used when creating a new job. + """ + if self.execution_environment is not None: + return self.execution_environment + if getattr(self, 'project_id', None) and self.project.default_environment is not None: + return self.project.default_environment + if getattr(self, 'organization', None) and self.organization.default_environment is not None: + return self.organization.default_environment + if getattr(self, 'inventory', None) and self.inventory.organization is not None: + if self.inventory.organization.default_environment is not None: + return self.inventory.organization.default_environment + if settings.DEFAULT_EXECUTION_ENVIRONMENT is not None: + return settings.DEFAULT_EXECUTION_ENVIRONMENT + return ExecutionEnvironment.objects.filter(organization=None, managed_by_tower=True).first() + def create_unified_job(self, **kwargs): ''' Create a new unified job based on this unified job template. @@ -376,6 +394,8 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, ExecutionEn for fd, val in eager_fields.items(): setattr(unified_job, fd, val) + unified_job.execution_environment = self.resolve_execution_environment() + # NOTE: slice workflow jobs _get_parent_field_name method # is not correct until this is set if not parent_field_name: diff --git a/awx/main/tests/functional/test_credential.py b/awx/main/tests/functional/test_credential.py index 27f67b96f4..4f87c249be 100644 --- a/awx/main/tests/functional/test_credential.py +++ b/awx/main/tests/functional/test_credential.py @@ -90,6 +90,7 @@ def test_default_cred_types(): 'kubernetes_bearer_token', 'net', 'openstack', + 'registry', 'rhv', 'satellite6', 'scm', diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 058d6aeaee..52a044afa0 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -175,6 +175,7 @@ REMOTE_HOST_HEADERS = ['REMOTE_ADDR', 'REMOTE_HOST'] PROXY_IP_ALLOWED_LIST = [] CUSTOM_VENV_PATHS = [] +DEFAULT_EXECUTION_ENVIRONMENT = None # Note: This setting may be overridden by database settings. STDOUT_MAX_BYTES_DISPLAY = 1048576 diff --git a/awxkit/awxkit/api/pages/execution_environments.py b/awxkit/awxkit/api/pages/execution_environments.py index 87225d1052..c3bcecb4bf 100644 --- a/awxkit/awxkit/api/pages/execution_environments.py +++ b/awxkit/awxkit/api/pages/execution_environments.py @@ -18,7 +18,7 @@ log = logging.getLogger(__name__) class ExecutionEnvironment(HasCreate, base.Base): dependencies = [Organization, Credential] - NATURAL_KEY = ('organization', 'image') + NATURAL_KEY = ('name',) # fields are image, organization, managed_by_tower, credential def create(self, image='quay.io/ansible/ansible-runner:devel', credential=None, **kwargs):