diff --git a/awx/main/constants.py b/awx/main/constants.py index 5610dde101..4ad2f2b0ee 100644 --- a/awx/main/constants.py +++ b/awx/main/constants.py @@ -14,7 +14,20 @@ __all__ = [ 'STANDARD_INVENTORY_UPDATE_ENV', ] -CLOUD_PROVIDERS = ('azure_rm', 'ec2', 'gce', 'vmware', 'openstack', 'rhv', 'satellite6', 'controller', 'insights', 'terraform', 'openshift_virtualization') +CLOUD_PROVIDERS = ( + 'azure_rm', + 'ec2', + 'gce', + 'vmware', + 'vmware_esxi', + 'openstack', + 'rhv', + 'satellite6', + 'controller', + 'insights', + 'terraform', + 'openshift_virtualization', +) PRIVILEGE_ESCALATION_METHODS = [ ('sudo', _('Sudo')), ('su', _('Su')), diff --git a/awx/main/migrations/0198_alter_inventorysource_source_and_more.py b/awx/main/migrations/0198_alter_inventorysource_source_and_more.py new file mode 100644 index 0000000000..2f7f4dbedf --- /dev/null +++ b/awx/main/migrations/0198_alter_inventorysource_source_and_more.py @@ -0,0 +1,61 @@ +# Generated by Django 4.2.18 on 2025-02-27 20:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [('main', '0197_add_opa_query_path')] + + operations = [ + migrations.AlterField( + model_name='inventorysource', + name='source', + field=models.CharField( + choices=[ + ('file', 'File, Directory or Script'), + ('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'), + ('vmware_esxi', 'VMware ESXi'), + ('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'), + ], + default=None, + max_length=32, + ), + ), + migrations.AlterField( + model_name='inventoryupdate', + name='source', + field=models.CharField( + choices=[ + ('file', 'File, Directory or Script'), + ('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'), + ('vmware_esxi', 'VMware ESXi'), + ('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'), + ], + default=None, + max_length=32, + ), + ), + ] diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 7834b218e1..d6c306671b 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -928,6 +928,7 @@ class InventorySourceOptions(BaseModel): ('gce', _('Google Compute Engine')), ('azure_rm', _('Microsoft Azure Resource Manager')), ('vmware', _('VMware vCenter')), + ('vmware_esxi', _('VMware ESXi')), ('satellite6', _('Red Hat Satellite 6')), ('openstack', _('OpenStack')), ('rhv', _('Red Hat Virtualization')), @@ -1048,7 +1049,9 @@ class InventorySourceOptions(BaseModel): # If a credential was provided, it's important that it matches # the actual inventory source being used (Amazon requires Amazon # credentials; Rackspace requires Rackspace credentials; etc...) - if source.replace('ec2', 'aws') != cred.kind: + if source == 'vmware_esxi' and source.replace('vmware_esxi', 'vmware') != cred.kind: + return _('VMWARE inventory sources (such as %s) require credentials for the matching cloud service.') % source + if source == 'ec2' and source.replace('ec2', 'aws') != cred.kind: return _('Cloud-based inventory sources (such as %s) require credentials for the matching cloud service.') % source # 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 @@ -1068,7 +1071,7 @@ class InventorySourceOptions(BaseModel): credential = None for cred in self.credentials.all(): if self.source in CLOUD_PROVIDERS: - if cred.kind == self.source.replace('ec2', 'aws'): + if cred.kind == self.source.replace('ec2', 'aws') or cred.kind == self.source.replace('vmware_esxi', 'vmware'): credential = cred break else: @@ -1437,7 +1440,7 @@ class PluginFileInjector(object): namespace = None collection = None collection_migration = '2.9' # Starting with this version, we use collections - use_fqcn = False # plugin: name versus plugin: namespace.collection.name + use_fqcn = False # plugin: name versus plugin: namespace.collection.name, set to True in each plugin as needed # TODO: delete this method and update unit tests @classmethod @@ -1568,6 +1571,14 @@ class vmware(PluginFileInjector): collection = 'vmware' +class vmware_esxi(PluginFileInjector): + plugin_name = 'esxi_hosts' + base_injector = 'managed' + namespace = 'vmware' + collection = 'vmware' + use_fqcn = True + + class openstack(PluginFileInjector): plugin_name = 'openstack' namespace = 'openstack' diff --git a/awx/main/tests/data/inventory/plugins/vmware_esxi/env.json b/awx/main/tests/data/inventory/plugins/vmware_esxi/env.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/awx/main/tests/data/inventory/plugins/vmware_esxi/env.json @@ -0,0 +1 @@ +{} diff --git a/awx/main/tests/functional/api/test_inventory.py b/awx/main/tests/functional/api/test_inventory.py index 71a48383c4..829662cc2f 100644 --- a/awx/main/tests/functional/api/test_inventory.py +++ b/awx/main/tests/functional/api/test_inventory.py @@ -529,6 +529,19 @@ class TestInventorySourceCredential: patch(url=inv_src.get_absolute_url(), data={'credential': aws_cred.pk}, expect=200, user=admin_user) assert list(inv_src.credentials.values_list('id', flat=True)) == [aws_cred.pk] + def test_vmware_cred_create_esxi_source(self, inventory, admin_user, organization, post, get): + """Test that a vmware esxi source can be added with a vmware credential""" + from awx.main.models.credential import Credential, CredentialType + + vmware = CredentialType.defaults['vmware']() + vmware.save() + vmware_cred = Credential.objects.create(credential_type=vmware, name="bar", organization=organization) + inv_src = InventorySource.objects.create(inventory=inventory, name='foobar', source='vmware_esxi') + r = post(url=reverse('api:inventory_source_credentials_list', kwargs={'pk': inv_src.pk}), data={'id': vmware_cred.pk}, expect=204, user=admin_user) + g = get(inv_src.get_absolute_url(), admin_user) + assert r.status_code == 204 + assert g.data['credential'] == vmware_cred.pk + @pytest.mark.django_db class TestControlledBySCM: diff --git a/awx/main/tests/functional/test_inventory_source_injectors.py b/awx/main/tests/functional/test_inventory_source_injectors.py index acced6e59d..8fbd9304ae 100644 --- a/awx/main/tests/functional/test_inventory_source_injectors.py +++ b/awx/main/tests/functional/test_inventory_source_injectors.py @@ -48,6 +48,8 @@ def credential_kind(source): """Given the inventory source kind, return expected credential kind""" if source == 'openshift_virtualization': return 'kubernetes_bearer_token' + if source == 'vmware_esxi': + return 'vmware' return source.replace('ec2', 'aws') diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 1b5f61b000..bba0afc7dc 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -746,6 +746,11 @@ VMWARE_EXCLUDE_EMPTY_GROUPS = True VMWARE_VALIDATE_CERTS = False +# ----------------- +# -- VMware ESXi -- +# ----------------- +VMWARE_ESXI_EXCLUDE_EMPTY_GROUPS = True + # --------------------------- # -- Google Compute Engine -- # ---------------------------