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

Make cloud providers dynamic #15537

Open
wants to merge 6 commits into
base: devel
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions awx/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,6 @@
WorkflowJobTemplate,
WorkflowJobTemplateNode,
StdoutMaxBytesExceeded,
CLOUD_INVENTORY_SOURCES,
)
from awx.main.models.base import VERBOSITY_CHOICES, NEW_JOB_TYPE_CHOICES
from awx.main.models.rbac import role_summary_fields_generator, give_creator_permissions, get_role_codenames, to_permissions, get_role_from_object_role
Expand All @@ -119,7 +118,9 @@
truncate_stdout,
get_licenser,
)

from awx.main.utils.filters import SmartFilter
from awx.main.utils.plugins import return_inventory_source_options
from awx.main.utils.named_url_graph import reset_counters
from awx.main.scheduler.task_manager_models import TaskManagerModels
from awx.main.redact import UriCleaner, REPLACE_STR
Expand Down Expand Up @@ -2300,6 +2301,7 @@ class Meta:

class InventorySourceOptionsSerializer(BaseSerializer):
credential = DeprecatedCredentialField(help_text=_('Cloud credential to use for inventory updates.'))
source = serializers.ChoiceField(choices=[])

class Meta:
fields = (
Expand All @@ -2321,6 +2323,11 @@ class Meta:
)
read_only_fields = ('*', 'custom_virtualenv')

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if 'source' in self.fields:
self.fields['source'].choices = return_inventory_source_options() or {}

def get_related(self, obj):
res = super(InventorySourceOptionsSerializer, self).get_related(obj)
if obj.credential: # TODO: remove when 'credential' field is removed
Expand Down Expand Up @@ -5500,7 +5507,7 @@ def get_summary_fields(self, obj):
return summary_fields

def validate_unified_job_template(self, value):
if type(value) == InventorySource and value.source not in CLOUD_INVENTORY_SOURCES:
if type(value) == InventorySource and value.source not in compute_cloud_inventory_sources():
raise serializers.ValidationError(_('Inventory Source must be a cloud resource.'))
elif type(value) == Project and value.scm_type == '':
raise serializers.ValidationError(_('Manual Project cannot have a schedule set.'))
Expand Down
5 changes: 3 additions & 2 deletions awx/api/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
)
from awx.main.utils.encryption import encrypt_value
from awx.main.utils.filters import SmartFilter
from awx.main.utils.plugins import compute_cloud_inventory_sources
from awx.main.redact import UriCleaner
from awx.api.permissions import (
JobTemplateCallbackPermission,
Expand Down Expand Up @@ -2196,9 +2197,9 @@ class InventorySourceNotificationTemplatesAnyList(SubListCreateAttachDetachAPIVi

def post(self, request, *args, **kwargs):
parent = self.get_parent_object()
if parent.source not in models.CLOUD_INVENTORY_SOURCES:
if parent.source not in compute_cloud_inventory_sources():
return Response(
dict(msg=_("Notification Templates can only be assigned when source is one of {}.").format(models.CLOUD_INVENTORY_SOURCES, parent.source)),
dict(msg=_("Notification Templates can only be assigned when source is one of {}.").format(compute_cloud_inventory_sources(), parent.source)),
status=status.HTTP_400_BAD_REQUEST,
)
return super(InventorySourceNotificationTemplatesAnyList, self).post(request, *args, **kwargs)
Expand Down
20 changes: 0 additions & 20 deletions awx/main/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,13 @@
from django.utils.translation import gettext_lazy as _
djyasin marked this conversation as resolved.
Show resolved Hide resolved

__all__ = [
'CLOUD_PROVIDERS',
'PRIVILEGE_ESCALATION_METHODS',
'ANSI_SGR_PATTERN',
'CAN_CANCEL',
'ACTIVE_STATES',
'STANDARD_INVENTORY_UPDATE_ENV',
]

CLOUD_PROVIDERS = (
'azure_rm',
'ec2',
'gce',
'vmware',
'openstack',
'rhv',
'satellite6',
'controller',
'insights',
'terraform',
'openshift_virtualization',
'controller_supported',
'rhv_supported',
'openshift_virtualization_supported',
'insights_supported',
'satellite6_supported',
)

PRIVILEGE_ESCALATION_METHODS = [
('sudo', _('Sudo')),
('su', _('Su')),
Expand Down
23 changes: 23 additions & 0 deletions awx/main/migrations/0196_alter_inventorysource_source_and_more.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 4.2.10 on 2024-09-24 15:53

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('main', '0195_EE_permissions'),
]

operations = [
migrations.AlterField(
model_name='inventorysource',
name='source',
field=models.CharField(default=None, max_length=32),
),
migrations.AlterField(
model_name='inventoryupdate',
name='source',
field=models.CharField(default=None, max_length=32),
),
]
13 changes: 13 additions & 0 deletions awx/main/migrations/0198_merge_20241016_1759.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Generated by Django 4.2.10 on 2024-10-16 17:59

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('main', '0196_alter_inventorysource_source_and_more'),
('main', '0197_remove_sso_app_content'),
]

operations = []
2 changes: 1 addition & 1 deletion awx/main/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from ansible_base.lib.utils.models import user_summary_fields

# AWX
from awx.main.models.base import BaseModel, PrimordialModel, accepts_json, CLOUD_INVENTORY_SOURCES, VERBOSITY_CHOICES # noqa
from awx.main.models.base import BaseModel, PrimordialModel, accepts_json, VERBOSITY_CHOICES # noqa
from awx.main.models.unified_jobs import UnifiedJob, UnifiedJobTemplate, StdoutMaxBytesExceeded # noqa
from awx.main.models.organization import Organization, Team, UserSessionMembership # noqa
from awx.main.models.credential import Credential, CredentialType, CredentialInputSource, ManagedCredentialType, build_safe_env # noqa
Expand Down
3 changes: 0 additions & 3 deletions awx/main/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@

# AWX
from awx.main.utils import encrypt_field, parse_yaml_or_json
from awx.main.constants import CLOUD_PROVIDERS

__all__ = [
'VarsDictProperty',
Expand All @@ -32,7 +31,6 @@
'JOB_TYPE_CHOICES',
'AD_HOC_JOB_TYPE_CHOICES',
'PROJECT_UPDATE_JOB_TYPE_CHOICES',
'CLOUD_INVENTORY_SOURCES',
'VERBOSITY_CHOICES',
]

Expand Down Expand Up @@ -61,7 +59,6 @@
(PERM_INVENTORY_CHECK, _('Check')),
]

CLOUD_INVENTORY_SOURCES = list(CLOUD_PROVIDERS) + ['scm']

VERBOSITY_CHOICES = [
(0, '0 (Normal)'),
Expand Down
35 changes: 7 additions & 28 deletions awx/main/models/inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,15 @@

# AWX
from awx.api.versioning import reverse
from awx.main.constants import CLOUD_PROVIDERS
from awx.main.utils.plugins import discover_available_cloud_provider_plugin_names, compute_cloud_inventory_sources
from awx.main.consumers import emit_channel_notification
from awx.main.fields import (
ImplicitRoleField,
SmartFilterField,
OrderedManyToManyField,
)
from awx.main.managers import HostManager, HostMetricActiveManager
from awx.main.models.base import BaseModel, CommonModelNameNotUnique, VarsDictProperty, CLOUD_INVENTORY_SOURCES, accepts_json
from awx.main.models.base import BaseModel, CommonModelNameNotUnique, VarsDictProperty, accepts_json
from awx.main.models.events import InventoryUpdateEvent, UnpartitionedInventoryUpdateEvent
from awx.main.models.unified_jobs import UnifiedJob, UnifiedJobTemplate
from awx.main.models.mixins import (
Expand Down Expand Up @@ -394,7 +394,7 @@ def update_computed_fields(self):
if self.kind == 'smart':
active_inventory_sources = self.inventory_sources.none()
else:
active_inventory_sources = self.inventory_sources.filter(source__in=CLOUD_INVENTORY_SOURCES)
active_inventory_sources = self.inventory_sources.filter(source__in=compute_cloud_inventory_sources())
failed_inventory_sources = active_inventory_sources.filter(last_job_failed=True)
total_hosts = active_hosts.count()
# if total_hosts has changed, set update_task_impact to True
Expand Down Expand Up @@ -914,23 +914,6 @@ class InventorySourceOptions(BaseModel):

injectors = dict()

SOURCE_CHOICES = [
('file', _('File, Directory or Script')),
djyasin marked this conversation as resolved.
Show resolved Hide resolved
('constructed', _('Template additional groups and hostvars at runtime')),
('scm', _('Sourced from a Project')),
('ec2', _('Amazon EC2')),
('gce', _('Google Compute Engine')),
('azure_rm', _('Microsoft Azure Resource Manager')),
('vmware', _('VMware vCenter')),
('satellite6', _('Red Hat Satellite 6')),
('openstack', _('OpenStack')),
('rhv', _('Red Hat Virtualization')),
('controller', _('Red Hat Ansible Automation Platform')),
('insights', _('Red Hat Insights')),
('terraform', _('Terraform State')),
('openshift_virtualization', _('OpenShift Virtualization')),
]

# From the options of the Django management base command
INVENTORY_UPDATE_VERBOSITY_CHOICES = [
(0, '0 (WARNING)'),
Expand All @@ -943,7 +926,6 @@ class Meta:

source = models.CharField(
max_length=32,
choices=SOURCE_CHOICES,
djyasin marked this conversation as resolved.
Show resolved Hide resolved
blank=False,
default=None,
)
Expand Down Expand Up @@ -1047,7 +1029,7 @@ def cloud_credential_validation(source, cred):
# Allow an EC2 source to omit the credential. If Tower is running on
# an EC2 instance with an IAM Role assigned, boto will use credentials
# from the instance metadata instead of those explicitly provided.
elif source in CLOUD_PROVIDERS and source not in ['ec2', 'openshift_virtualization']:
elif source in discover_available_cloud_provider_plugin_names() and source not in ['ec2', 'openshift_virtualization']:
return _('Credential is required for a cloud source.')
elif source == 'custom' and cred and cred.credential_type.kind in ('scm', 'ssh', 'insights', 'vault'):
return _('Credentials of type machine, source control, insights and vault are disallowed for custom inventory sources.')
Expand All @@ -1061,11 +1043,8 @@ def get_cloud_credential(self):
"""Return the credential which is directly tied to the inventory source type."""
credential = None
for cred in self.credentials.all():
if self.source in CLOUD_PROVIDERS:
source = self.source.replace('ec2', 'aws')
if source.endswith('_supported'):
source = source[:-10]
if cred.kind == source:
if self.source in discover_available_cloud_provider_plugin_names():
if cred.kind == self.source.replace('ec2', 'aws'):
credential = cred
break
else:
Expand All @@ -1080,7 +1059,7 @@ def get_extra_credentials(self):
These are all credentials that should run their own inject_credential logic.
"""
special_cred = None
if self.source in CLOUD_PROVIDERS:
if self.source in discover_available_cloud_provider_plugin_names():
# these have special injection logic associated with them
special_cred = self.get_cloud_credential()
extra_creds = []
Expand Down
6 changes: 3 additions & 3 deletions awx/main/tests/functional/models/test_inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@

# AWX
from awx.main.models import Host, Inventory, InventorySource, InventoryUpdate, CredentialType, Credential, Job
from awx.main.constants import CLOUD_PROVIDERS
from awx.main.utils.filters import SmartFilter
from awx.main.utils.plugins import discover_available_cloud_provider_plugin_names


@pytest.mark.django_db
Expand Down Expand Up @@ -166,11 +166,11 @@ def test_extra_credentials(self, project, credential):

def test_all_cloud_sources_covered(self):
"""Code in several places relies on the fact that the older
CLOUD_PROVIDERS constant contains the same names as what are
discover_cloud_provider_plugin_names returns the same names as what are
defined within the injectors
"""
# slight exception case for constructed, because it has a FQCN but is not a cloud source
assert set(CLOUD_PROVIDERS) | set(['constructed']) == set(InventorySource.injectors.keys())
assert set(discover_available_cloud_provider_plugin_names()) | set(['constructed']) == set(InventorySource.injectors.keys())

@pytest.mark.parametrize('source,filename', [('ec2', 'aws_ec2.yml'), ('openstack', 'openstack.yml'), ('gce', 'gcp_compute.yml')])
def test_plugin_filenames(self, source, filename):
Expand Down
8 changes: 3 additions & 5 deletions awx/main/tests/functional/test_inventory_source_injectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@

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

from awx.main.utils.plugins import discover_available_cloud_provider_plugin_names
from django.conf import settings

DATA = os.path.join(os.path.dirname(data.__file__), 'inventory')
Expand Down Expand Up @@ -193,7 +193,7 @@ def create_reference_data(source_dir, env, content):


@pytest.mark.django_db
@pytest.mark.parametrize('this_kind', CLOUD_PROVIDERS)
@pytest.mark.parametrize('this_kind', discover_available_cloud_provider_plugin_names())
djyasin marked this conversation as resolved.
Show resolved Hide resolved
djyasin marked this conversation as resolved.
Show resolved Hide resolved
def test_inventory_update_injected_content(this_kind, inventory, fake_credential_factory, mock_me):
if this_kind.endswith('_supported'):
this_kind = this_kind[:-10]
Expand All @@ -202,8 +202,6 @@ def test_inventory_update_injected_content(this_kind, inventory, fake_credential
ExecutionEnvironment.objects.create(name='Default Job EE', managed=False)

injector = InventorySource.injectors[this_kind]
if injector.plugin_name is None:
pytest.skip('Use of inventory plugin is not enabled for this source')

src_vars = dict(base_source_var='value_of_var')
src_vars['plugin'] = injector.get_proper_name()
Expand Down
55 changes: 55 additions & 0 deletions awx/main/utils/plugins.py
djyasin marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Copyright (c) 2024 Ansible, Inc.
# All Rights Reserved.

"""
This module contains the code responsible for extracting the lists of dynamically discovered plugins.
"""

from functools import cache


@cache
def discover_available_cloud_provider_plugin_names() -> list[str]:
"""Return a list of cloud plugin names available in runtime.
The discovery result is cached since it does not change throughout
the life cycle of the server run.
:returns: List of plugin cloud names.
:rtype: list[str]
"""
from awx.main.models.inventory import InventorySourceOptions

plugin_names = list(InventorySourceOptions.injectors.keys())

plugin_names.remove('constructed')

return plugin_names


@cache
def compute_cloud_inventory_sources() -> dict[str, str]:
djyasin marked this conversation as resolved.
Show resolved Hide resolved
"""Return a dictionary of cloud provider plugin names
available plus source control management and constructed.
:returns: Dictionary of plugin cloud names plus source control.
:rtype: dict[str, str]
"""

plugins = discover_available_cloud_provider_plugin_names()

return dict(zip(plugins, plugins), scm='scm', constructed='constructed')


@cache
def return_inventory_source_options() -> dict[str, str]:
"""Return a dictionary of cloud provider plugin names
plus file. File needed to be separate since it is needs to be consumed directly
by the serializer.
:returns: Dictionary of plugin cloud names plus source control.
:rtype: dict[str, str]
"""
plugins = compute_cloud_inventory_sources()

return dict(zip(plugins, plugins)) | dict(file='file')
Loading