Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EE model changes #8644

Merged
merged 11 commits into from
Dec 10, 2020
11 changes: 7 additions & 4 deletions awx/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@
'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',),
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand All @@ -1420,6 +1420,9 @@ def get_related(self, obj):
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',
Expand Down
1 change: 1 addition & 0 deletions awx/conf/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
BooleanField, CharField, ChoiceField, DictField, DateTimeField, EmailField,
IntegerField, ListField, NullBooleanField
)
from rest_framework.serializers import PrimaryKeyRelatedField # noqa

logger = logging.getLogger('awx.conf.fields')

Expand Down
12 changes: 7 additions & 5 deletions awx/main/access.py
Original file line number Diff line number Diff line change
Expand Up @@ -1312,39 +1312,41 @@ class ExecutionEnvironmentAccess(BaseAccess):
"""
I can see an execution environment when:
- I'm a superuser
- I'm a member of the organization
- I'm a member of the same organization
- it is a global ExecutionEnvironment
I can create/change an execution environment when:
- I'm a superuser
- I'm an admin for the organization(s)
"""

model = ExecutionEnvironment
select_related = ('organization',)
prefetch_related = ('organization__admin_role', 'organization__execution_environment_admin_role')

def filtered_queryset(self):
return ExecutionEnvironment.objects.filter(
Q(organization__in=Organization.accessible_pk_qs(self.user, 'admin_role')) |
Q(organization__in=Organization.accessible_pk_qs(self.user, 'execution_environment_admin_role')) |
Q(organization__isnull=True)
).distinct()

@check_superuser
def can_add(self, data):
if not data: # So the browseable API will work
return Organization.accessible_objects(self.user, 'admin_role').exists()
return Organization.accessible_objects(self.user, 'execution_environment_admin_role').exists()
return self.check_related('organization', Organization, data)

@check_superuser
def can_change(self, obj, data):
if obj and obj.organization_id is None:
raise PermissionDenied
if self.user not in obj.organization.admin_role:
if self.user not in obj.organization.execution_environment_admin_role:
raise PermissionDenied
org_pk = get_pk_from_dict(data, 'organization')
if obj and obj.organization_id != org_pk:
# Prevent moving an EE to a different organization, unless a superuser or admin on both orgs.
if obj.organization_id is None or org_pk is None:
raise PermissionDenied
if self.user not in Organization.objects.get(id=org_pk).admin_role:
if self.user not in Organization.objects.get(id=org_pk).execution_environment_admin_role:
raise PermissionDenied

return True
Expand Down
13 changes: 13 additions & 0 deletions awx/main/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

# Tower
from awx.conf import fields, register, register_validate
from awx.main.models import ExecutionEnvironment


logger = logging.getLogger('awx.main.conf')
Expand Down Expand Up @@ -176,6 +177,18 @@
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,
Expand Down
46 changes: 46 additions & 0 deletions awx/main/migrations/0125_more_ee_modeling_changes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# 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.AlterField(
model_name='credentialtype',
name='kind',
field=models.CharField(choices=[('ssh', 'Machine'), ('vault', 'Vault'), ('net', 'Network'), ('scm', 'Source Control'), ('cloud', 'Cloud'), ('registry', 'Container Registry'), ('token', 'Personal Access Token'), ('insights', 'Insights'), ('external', 'External'), ('kubernetes', 'Kubernetes'), ('galaxy', 'Galaxy/Automation Hub')], max_length=32),
),
migrations.AlterUniqueTogether(
name='executionenvironment',
unique_together=set(),
),
]
4 changes: 2 additions & 2 deletions awx/main/models/ad_hoc_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,8 @@ def task_impact(self):
def copy(self):
data = {}
for field in ('job_type', 'inventory_id', 'limit', 'credential_id',
'module_name', 'module_args', 'forks', 'verbosity',
'extra_vars', 'become_enabled', 'diff_mode'):
'execution_environment_id', 'module_name', 'module_args',
'forks', 'verbosity', 'extra_vars', 'become_enabled', 'diff_mode'):
data[field] = getattr(self, field)
return AdHocCommand.objects.create(**data)

Expand Down
33 changes: 32 additions & 1 deletion awx/main/models/credential/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ class Meta:
('net', _('Network')),
('scm', _('Source Control')),
('cloud', _('Cloud')),
('registry', _('Container Registry')),
('token', _('Personal Access Token')),
('insights', _('Insights')),
('external', _('External')),
Expand Down Expand Up @@ -1128,7 +1129,6 @@ def create(self):
},
)


ManagedCredentialType(
namespace='kubernetes_bearer_token',
kind='kubernetes',
Expand Down Expand Up @@ -1160,6 +1160,37 @@ def create(self):
}
)

ManagedCredentialType(
namespace='registry',
kind='registry',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the credential is applied to the podman process, not the ansible-playbook process, I think it does make sense to add a new kind option of "registry" for pulling from an image registry.

A big "but" here - this doesn't actually add it as an option. You would need to add it to the KIND_CHOICES listing, and this PR does not do that now.

With the way that ansible-runner works... this might be accomplish-able by modifying os.environ, and not making other ansible-runner code changes. There are still a lot of question marks there.

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',
Expand Down
7 changes: 3 additions & 4 deletions awx/main/models/execution_environments.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,15 @@
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',
Expand Down
19 changes: 19 additions & 0 deletions awx/main/models/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,25 @@ class Meta:
help_text=_('The container image to be used for execution.'),
)

def resolve_execution_environment(self):
"""
Return the execution environment that should be used when creating a new job.
"""
from awx.main.models.execution_environments import ExecutionEnvironment

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()


class CustomVirtualEnvMixin(models.Model):
class Meta:
Expand Down
3 changes: 3 additions & 0 deletions awx/main/models/organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ class Meta:
job_template_admin_role = ImplicitRoleField(
parent_role='admin_role',
)
execution_environment_admin_role = ImplicitRoleField(
parent_role='admin_role',
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the addition of this, I'd expect access.py to switch a few things from the organization admin_role to the execution_environment_admin_role.

Q(organization__in=Organization.accessible_pk_qs(self.user, 'admin_role')) |

and

return Organization.accessible_objects(self.user, 'admin_role').exists()

and maybe?

if self.user not in obj.organization.admin_role:

that one might depend on intent

auditor_role = ImplicitRoleField(
parent_role='singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR,
)
Expand Down
9 changes: 9 additions & 0 deletions awx/main/models/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,15 @@ class Meta:
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.'),
Expand Down
2 changes: 2 additions & 0 deletions awx/main/models/rbac.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
'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'),
Expand All @@ -60,6 +61,7 @@
'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'),
Expand Down
2 changes: 2 additions & 0 deletions awx/main/models/unified_jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,8 @@ def create_unified_job(self, **kwargs):
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:
Expand Down
13 changes: 5 additions & 8 deletions awx/main/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -881,14 +881,11 @@ def get_path_to(self, *args):
return os.path.abspath(os.path.join(os.path.dirname(__file__), *args))

def build_execution_environment_params(self, instance):
if getattr(instance, 'execution_environment', None):
# TODO: process heirarchy, JT-project-org, maybe here
# or maybe in create_unified_job
logger.info('using custom image {}'.format(instance.execution_environment.image))
image = instance.execution_environment.image
else:
logger.info('using default image')
image = settings.AWX_EXECUTION_ENVIRONMENT_DEFAULT_IMAGE
if instance.execution_environment_id is None:
self.instance = instance = self.update_model(
instance.pk, execution_environment=instance.resolve_execution_environment())

image = instance.execution_environment.image
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For whatever reason, some jobs still wind up with a null execution_environment, so this needs to be changed to account for that. I'm thinking, this is probably from the project syncs that happen before a job runs.

params = {
"container_image": image,
"process_isolation": True
Expand Down
1 change: 1 addition & 0 deletions awx/main/tests/functional/test_credential.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ def test_default_cred_types():
'kubernetes_bearer_token',
'net',
'openstack',
'registry',
'rhv',
'satellite6',
'scm',
Expand Down
4 changes: 3 additions & 1 deletion awx/main/tests/functional/test_inventory_source_injectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from collections import namedtuple

from awx.main.tasks import RunInventoryUpdate
from awx.main.models import InventorySource, Credential, CredentialType, UnifiedJob
from awx.main.models import InventorySource, Credential, CredentialType, UnifiedJob, ExecutionEnvironment
from awx.main.constants import CLOUD_PROVIDERS, STANDARD_INVENTORY_UPDATE_ENV
from awx.main.tests import data

Expand Down Expand Up @@ -183,6 +183,8 @@ def create_reference_data(source_dir, env, content):
@pytest.mark.django_db
@pytest.mark.parametrize('this_kind', CLOUD_PROVIDERS)
def test_inventory_update_injected_content(this_kind, inventory, fake_credential_factory):
ExecutionEnvironment.objects.create(name='test EE', managed_by_tower=True)

injector = InventorySource.injectors[this_kind]
if injector.plugin_name is None:
pytest.skip('Use of inventory plugin is not enabled for this source')
Expand Down
4 changes: 4 additions & 0 deletions awx/main/tests/unit/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
AdHocCommand,
Credential,
CredentialType,
ExecutionEnvironment,
Inventory,
InventorySource,
InventoryUpdate,
Expand Down Expand Up @@ -610,9 +611,12 @@ def test_awx_task_env(self, patch_Job, private_data_dir):
assert env['FOO'] == 'BAR'


@pytest.mark.django_db
class TestAdhocRun(TestJobExecution):

def test_options_jinja_usage(self, adhoc_job, adhoc_update_model_wrapper):
ExecutionEnvironment.objects.create(name='test EE', managed_by_tower=True)

adhoc_job.module_args = '{{ ansible_ssh_pass }}'
adhoc_job.websocket_emit_status = mock.Mock()
adhoc_job.send_notification_templates = mock.Mock()
Expand Down
Loading