diff options
author | Chris Meyers <chris.meyers.fsu@gmail.com> | 2024-08-06 16:22:43 +0200 |
---|---|---|
committer | Chris Meyers <chrismeyersfsu@users.noreply.github.com> | 2024-09-04 20:46:22 +0200 |
commit | 376cc35a925764b2535618d194c44f786bab8662 (patch) | |
tree | 0e25ba6258700758c8761ef292cd381caafd5594 | |
parent | Remove references to IRC & Google Groups (#15480) (diff) | |
download | awx-376cc35a925764b2535618d194c44f786bab8662.tar.xz awx-376cc35a925764b2535618d194c44f786bab8662.zip |
move inv and cred plugins into awx_plugins
-rw-r--r-- | awx/main/credential_plugins/__init__.py | 0 | ||||
-rw-r--r-- | awx/main/models/credential/__init__.py | 668 | ||||
-rw-r--r-- | awx/main/models/inventory.py | 301 | ||||
-rw-r--r-- | awx_plugins/credentials/aim.py (renamed from awx/main/credential_plugins/aim.py) | 2 | ||||
-rw-r--r-- | awx_plugins/credentials/aws_secretsmanager.py (renamed from awx/main/credential_plugins/aws_secretsmanager.py) | 2 | ||||
-rw-r--r-- | awx_plugins/credentials/azure_kv.py (renamed from awx/main/credential_plugins/azure_kv.py) | 2 | ||||
-rw-r--r-- | awx_plugins/credentials/centrify_vault.py (renamed from awx/main/credential_plugins/centrify_vault.py) | 2 | ||||
-rw-r--r-- | awx_plugins/credentials/conjur.py (renamed from awx/main/credential_plugins/conjur.py) | 2 | ||||
-rw-r--r-- | awx_plugins/credentials/dsv.py (renamed from awx/main/credential_plugins/dsv.py) | 4 | ||||
-rw-r--r-- | awx_plugins/credentials/hashivault.py (renamed from awx/main/credential_plugins/hashivault.py) | 2 | ||||
-rw-r--r-- | awx_plugins/credentials/injectors.py (renamed from awx/main/models/credential/injectors.py) | 0 | ||||
-rw-r--r-- | awx_plugins/credentials/plugin.py (renamed from awx/main/credential_plugins/plugin.py) | 13 | ||||
-rw-r--r-- | awx_plugins/credentials/plugins.py | 665 | ||||
-rw-r--r-- | awx_plugins/credentials/tss.py (renamed from awx/main/credential_plugins/tss.py) | 2 | ||||
-rw-r--r-- | awx_plugins/inventory/plugins.py | 302 | ||||
-rw-r--r-- | awx_plugins/tests/test_credential_plugins.py (renamed from awx/main/tests/functional/test_credential_plugins.py) | 8 | ||||
-rw-r--r-- | setup.cfg | 20 |
17 files changed, 1010 insertions, 985 deletions
diff --git a/awx/main/credential_plugins/__init__.py b/awx/main/credential_plugins/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 --- a/awx/main/credential_plugins/__init__.py +++ /dev/null diff --git a/awx/main/models/credential/__init__.py b/awx/main/models/credential/__init__.py index e07553e1a9..8eee732c25 100644 --- a/awx/main/models/credential/__init__.py +++ b/awx/main/models/credential/__init__.py @@ -15,7 +15,7 @@ from jinja2 import sandbox # Django from django.db import models -from django.utils.translation import gettext_lazy as _, gettext_noop +from django.utils.translation import gettext_lazy as _ from django.core.exceptions import ValidationError from django.conf import settings from django.utils.encoding import force_str @@ -47,12 +47,12 @@ from awx.main.models.rbac import ( ) from awx.main.models import Team, Organization from awx.main.utils import encrypt_field -from . import injectors as builtin_injectors +from awx_plugins.credentials import injectors as builtin_injectors __all__ = ['Credential', 'CredentialType', 'CredentialInputSource', 'build_safe_env'] logger = logging.getLogger('awx.main.models.credential') -credential_plugins = dict((ep.name, ep.load()) for ep in iter_entry_points('awx.credential_plugins')) +credential_plugins = dict((ep.name, ep.load()) for ep in iter_entry_points('awx.plugins')) HIDDEN_PASSWORD = '**********' @@ -601,666 +601,6 @@ class ManagedCredentialType(SimpleNamespace): return CredentialType(**self.get_creation_params()) -ManagedCredentialType( - namespace='ssh', - kind='ssh', - name=gettext_noop('Machine'), - inputs={ - 'fields': [ - {'id': 'username', 'label': gettext_noop('Username'), 'type': 'string'}, - {'id': 'password', 'label': gettext_noop('Password'), 'type': 'string', 'secret': True, 'ask_at_runtime': True}, - {'id': 'ssh_key_data', 'label': gettext_noop('SSH Private Key'), 'type': 'string', 'format': 'ssh_private_key', 'secret': True, 'multiline': True}, - { - 'id': 'ssh_public_key_data', - 'label': gettext_noop('Signed SSH Certificate'), - 'type': 'string', - 'multiline': True, - 'secret': True, - }, - {'id': 'ssh_key_unlock', 'label': gettext_noop('Private Key Passphrase'), 'type': 'string', 'secret': True, 'ask_at_runtime': True}, - { - 'id': 'become_method', - 'label': gettext_noop('Privilege Escalation Method'), - 'type': 'string', - 'help_text': gettext_noop('Specify a method for "become" operations. This is equivalent to specifying the --become-method Ansible parameter.'), - }, - { - 'id': 'become_username', - 'label': gettext_noop('Privilege Escalation Username'), - 'type': 'string', - }, - {'id': 'become_password', 'label': gettext_noop('Privilege Escalation Password'), 'type': 'string', 'secret': True, 'ask_at_runtime': True}, - ], - }, -) - -ManagedCredentialType( - namespace='scm', - kind='scm', - name=gettext_noop('Source Control'), - managed=True, - inputs={ - 'fields': [ - {'id': 'username', 'label': gettext_noop('Username'), 'type': 'string'}, - {'id': 'password', 'label': gettext_noop('Password'), 'type': 'string', 'secret': True}, - {'id': 'ssh_key_data', 'label': gettext_noop('SCM Private Key'), 'type': 'string', 'format': 'ssh_private_key', 'secret': True, 'multiline': True}, - {'id': 'ssh_key_unlock', 'label': gettext_noop('Private Key Passphrase'), 'type': 'string', 'secret': True}, - ], - }, -) - -ManagedCredentialType( - namespace='vault', - kind='vault', - name=gettext_noop('Vault'), - managed=True, - inputs={ - 'fields': [ - {'id': 'vault_password', 'label': gettext_noop('Vault Password'), 'type': 'string', 'secret': True, 'ask_at_runtime': True}, - { - 'id': 'vault_id', - 'label': gettext_noop('Vault Identifier'), - 'type': 'string', - 'format': 'vault_id', - 'help_text': gettext_noop( - 'Specify an (optional) Vault ID. This is ' - 'equivalent to specifying the --vault-id ' - 'Ansible parameter for providing multiple Vault ' - 'passwords. Note: this feature only works in ' - 'Ansible 2.4+.' - ), - }, - ], - 'required': ['vault_password'], - }, -) - -ManagedCredentialType( - namespace='net', - kind='net', - name=gettext_noop('Network'), - managed=True, - inputs={ - 'fields': [ - {'id': 'username', 'label': gettext_noop('Username'), 'type': 'string'}, - { - 'id': 'password', - 'label': gettext_noop('Password'), - 'type': 'string', - 'secret': True, - }, - {'id': 'ssh_key_data', 'label': gettext_noop('SSH Private Key'), 'type': 'string', 'format': 'ssh_private_key', 'secret': True, 'multiline': True}, - { - 'id': 'ssh_key_unlock', - 'label': gettext_noop('Private Key Passphrase'), - 'type': 'string', - 'secret': True, - }, - { - 'id': 'authorize', - 'label': gettext_noop('Authorize'), - 'type': 'boolean', - }, - { - 'id': 'authorize_password', - 'label': gettext_noop('Authorize Password'), - 'type': 'string', - 'secret': True, - }, - ], - 'dependencies': { - 'authorize_password': ['authorize'], - }, - 'required': ['username'], - }, -) - -ManagedCredentialType( - namespace='aws', - kind='cloud', - name=gettext_noop('Amazon Web Services'), - managed=True, - inputs={ - 'fields': [ - {'id': 'username', 'label': gettext_noop('Access Key'), 'type': 'string'}, - { - 'id': 'password', - 'label': gettext_noop('Secret Key'), - 'type': 'string', - 'secret': True, - }, - { - 'id': 'security_token', - 'label': gettext_noop('STS Token'), - 'type': 'string', - 'secret': True, - 'help_text': gettext_noop( - 'Security Token Service (STS) is a web service ' - 'that enables you to request temporary, ' - 'limited-privilege credentials for AWS Identity ' - 'and Access Management (IAM) users.' - ), - }, - ], - 'required': ['username', 'password'], - }, -) - -ManagedCredentialType( - namespace='openstack', - kind='cloud', - name=gettext_noop('OpenStack'), - managed=True, - inputs={ - 'fields': [ - {'id': 'username', 'label': gettext_noop('Username'), 'type': 'string'}, - { - 'id': 'password', - 'label': gettext_noop('Password (API Key)'), - 'type': 'string', - 'secret': True, - }, - { - 'id': 'host', - 'label': gettext_noop('Host (Authentication URL)'), - 'type': 'string', - 'help_text': gettext_noop('The host to authenticate with. For example, https://openstack.business.com/v2.0/'), - }, - { - 'id': 'project', - 'label': gettext_noop('Project (Tenant Name)'), - 'type': 'string', - }, - { - 'id': 'project_domain_name', - 'label': gettext_noop('Project (Domain Name)'), - 'type': 'string', - }, - { - 'id': 'domain', - 'label': gettext_noop('Domain Name'), - 'type': 'string', - 'help_text': gettext_noop( - 'OpenStack domains define administrative boundaries. ' - 'It is only needed for Keystone v3 authentication ' - 'URLs. Refer to the documentation for ' - 'common scenarios.' - ), - }, - { - 'id': 'region', - 'label': gettext_noop('Region Name'), - 'type': 'string', - 'help_text': gettext_noop('For some cloud providers, like OVH, region must be specified'), - }, - { - 'id': 'verify_ssl', - 'label': gettext_noop('Verify SSL'), - 'type': 'boolean', - 'default': True, - }, - ], - 'required': ['username', 'password', 'host', 'project'], - }, -) - -ManagedCredentialType( - namespace='vmware', - kind='cloud', - name=gettext_noop('VMware vCenter'), - managed=True, - inputs={ - 'fields': [ - { - 'id': 'host', - 'label': gettext_noop('VCenter Host'), - 'type': 'string', - 'help_text': gettext_noop('Enter the hostname or IP address that corresponds to your VMware vCenter.'), - }, - {'id': 'username', 'label': gettext_noop('Username'), 'type': 'string'}, - { - 'id': 'password', - 'label': gettext_noop('Password'), - 'type': 'string', - 'secret': True, - }, - ], - 'required': ['host', 'username', 'password'], - }, -) - -ManagedCredentialType( - namespace='satellite6', - kind='cloud', - name=gettext_noop('Red Hat Satellite 6'), - managed=True, - inputs={ - 'fields': [ - { - 'id': 'host', - 'label': gettext_noop('Satellite 6 URL'), - 'type': 'string', - 'help_text': gettext_noop('Enter the URL that corresponds to your Red Hat Satellite 6 server. For example, https://satellite.example.org'), - }, - {'id': 'username', 'label': gettext_noop('Username'), 'type': 'string'}, - { - 'id': 'password', - 'label': gettext_noop('Password'), - 'type': 'string', - 'secret': True, - }, - ], - 'required': ['host', 'username', 'password'], - }, -) - -ManagedCredentialType( - namespace='gce', - kind='cloud', - name=gettext_noop('Google Compute Engine'), - managed=True, - inputs={ - 'fields': [ - { - 'id': 'username', - 'label': gettext_noop('Service Account Email Address'), - 'type': 'string', - 'help_text': gettext_noop('The email address assigned to the Google Compute Engine service account.'), - }, - { - 'id': 'project', - 'label': 'Project', - 'type': 'string', - 'help_text': gettext_noop( - 'The Project ID is the GCE assigned identification. ' - 'It is often constructed as three words or two words ' - 'followed by a three-digit number. Examples: project-id-000 ' - 'and another-project-id' - ), - }, - { - 'id': 'ssh_key_data', - 'label': gettext_noop('RSA Private Key'), - 'type': 'string', - 'format': 'ssh_private_key', - 'secret': True, - 'multiline': True, - 'help_text': gettext_noop('Paste the contents of the PEM file associated with the service account email.'), - }, - ], - 'required': ['username', 'ssh_key_data'], - }, -) - -ManagedCredentialType( - namespace='azure_rm', - kind='cloud', - name=gettext_noop('Microsoft Azure Resource Manager'), - managed=True, - inputs={ - 'fields': [ - { - 'id': 'subscription', - 'label': gettext_noop('Subscription ID'), - 'type': 'string', - 'help_text': gettext_noop('Subscription ID is an Azure construct, which is mapped to a username.'), - }, - {'id': 'username', 'label': gettext_noop('Username'), 'type': 'string'}, - { - 'id': 'password', - 'label': gettext_noop('Password'), - 'type': 'string', - 'secret': True, - }, - {'id': 'client', 'label': gettext_noop('Client ID'), 'type': 'string'}, - { - 'id': 'secret', - 'label': gettext_noop('Client Secret'), - 'type': 'string', - 'secret': True, - }, - {'id': 'tenant', 'label': gettext_noop('Tenant ID'), 'type': 'string'}, - { - 'id': 'cloud_environment', - 'label': gettext_noop('Azure Cloud Environment'), - 'type': 'string', - 'help_text': gettext_noop('Environment variable AZURE_CLOUD_ENVIRONMENT when using Azure GovCloud or Azure stack.'), - }, - ], - 'required': ['subscription'], - }, -) - -ManagedCredentialType( - namespace='github_token', - kind='token', - name=gettext_noop('GitHub Personal Access Token'), - managed=True, - inputs={ - 'fields': [ - { - 'id': 'token', - 'label': gettext_noop('Token'), - 'type': 'string', - 'secret': True, - 'help_text': gettext_noop('This token needs to come from your profile settings in GitHub'), - } - ], - 'required': ['token'], - }, -) - -ManagedCredentialType( - namespace='gitlab_token', - kind='token', - name=gettext_noop('GitLab Personal Access Token'), - managed=True, - inputs={ - 'fields': [ - { - 'id': 'token', - 'label': gettext_noop('Token'), - 'type': 'string', - 'secret': True, - 'help_text': gettext_noop('This token needs to come from your profile settings in GitLab'), - } - ], - 'required': ['token'], - }, -) - -ManagedCredentialType( - namespace='bitbucket_dc_token', - kind='token', - name=gettext_noop('Bitbucket Data Center HTTP Access Token'), - managed=True, - inputs={ - 'fields': [ - { - 'id': 'token', - 'label': gettext_noop('Token'), - 'type': 'string', - 'secret': True, - 'help_text': gettext_noop('This token needs to come from your user settings in Bitbucket'), - } - ], - 'required': ['token'], - }, -) - -ManagedCredentialType( - namespace='insights', - kind='insights', - name=gettext_noop('Insights'), - managed=True, - inputs={ - 'fields': [ - {'id': 'username', 'label': gettext_noop('Username'), 'type': 'string'}, - {'id': 'password', 'label': gettext_noop('Password'), 'type': 'string', 'secret': True}, - ], - 'required': ['username', 'password'], - }, - injectors={ - 'extra_vars': { - "scm_username": "{{username}}", - "scm_password": "{{password}}", - }, - 'env': { - 'INSIGHTS_USER': '{{username}}', - 'INSIGHTS_PASSWORD': '{{password}}', - }, - }, -) - -ManagedCredentialType( - namespace='rhv', - kind='cloud', - name=gettext_noop('Red Hat Virtualization'), - managed=True, - inputs={ - 'fields': [ - {'id': 'host', 'label': gettext_noop('Host (Authentication URL)'), 'type': 'string', 'help_text': gettext_noop('The host to authenticate with.')}, - {'id': 'username', 'label': gettext_noop('Username'), 'type': 'string'}, - { - 'id': 'password', - 'label': gettext_noop('Password'), - 'type': 'string', - 'secret': True, - }, - { - 'id': 'ca_file', - 'label': gettext_noop('CA File'), - 'type': 'string', - 'help_text': gettext_noop('Absolute file path to the CA file to use (optional)'), - }, - ], - 'required': ['host', 'username', 'password'], - }, - injectors={ - # The duplication here is intentional; the ovirt4 inventory plugin - # writes a .ini file for authentication, while the ansible modules for - # ovirt4 use a separate authentication process that support - # environment variables; by injecting both, we support both - 'file': { - 'template': '\n'.join( - [ - '[ovirt]', - 'ovirt_url={{host}}', - 'ovirt_username={{username}}', - 'ovirt_password={{password}}', - '{% if ca_file %}ovirt_ca_file={{ca_file}}{% endif %}', - ] - ) - }, - 'env': {'OVIRT_INI_PATH': '{{tower.filename}}', 'OVIRT_URL': '{{host}}', 'OVIRT_USERNAME': '{{username}}', 'OVIRT_PASSWORD': '{{password}}'}, - }, -) - -ManagedCredentialType( - namespace='controller', - kind='cloud', - name=gettext_noop('Red Hat Ansible Automation Platform'), - managed=True, - inputs={ - 'fields': [ - { - 'id': 'host', - 'label': gettext_noop('Red Hat Ansible Automation Platform'), - 'type': 'string', - 'help_text': gettext_noop('Red Hat Ansible Automation Platform base URL to authenticate with.'), - }, - { - 'id': 'username', - 'label': gettext_noop('Username'), - 'type': 'string', - 'help_text': gettext_noop( - 'Red Hat Ansible Automation Platform username id to authenticate as.This should not be set if an OAuth token is being used.' - ), - }, - { - 'id': 'password', - 'label': gettext_noop('Password'), - 'type': 'string', - 'secret': True, - }, - { - 'id': 'oauth_token', - 'label': gettext_noop('OAuth Token'), - 'type': 'string', - 'secret': True, - 'help_text': gettext_noop('An OAuth token to use to authenticate with.This should not be set if username/password are being used.'), - }, - {'id': 'verify_ssl', 'label': gettext_noop('Verify SSL'), 'type': 'boolean', 'secret': False}, - ], - 'required': ['host'], - }, - injectors={ - 'env': { - 'TOWER_HOST': '{{host}}', - 'TOWER_USERNAME': '{{username}}', - 'TOWER_PASSWORD': '{{password}}', - 'TOWER_VERIFY_SSL': '{{verify_ssl}}', - 'TOWER_OAUTH_TOKEN': '{{oauth_token}}', - 'CONTROLLER_HOST': '{{host}}', - 'CONTROLLER_USERNAME': '{{username}}', - 'CONTROLLER_PASSWORD': '{{password}}', - 'CONTROLLER_VERIFY_SSL': '{{verify_ssl}}', - 'CONTROLLER_OAUTH_TOKEN': '{{oauth_token}}', - } - }, -) - -ManagedCredentialType( - namespace='kubernetes_bearer_token', - kind='kubernetes', - name=gettext_noop('OpenShift or Kubernetes API Bearer Token'), - inputs={ - 'fields': [ - { - 'id': 'host', - 'label': gettext_noop('OpenShift or Kubernetes API Endpoint'), - 'type': 'string', - 'help_text': gettext_noop('The OpenShift or Kubernetes API Endpoint to authenticate with.'), - }, - { - 'id': 'bearer_token', - 'label': gettext_noop('API authentication bearer token'), - 'type': 'string', - 'secret': True, - }, - { - 'id': 'verify_ssl', - 'label': gettext_noop('Verify SSL'), - 'type': 'boolean', - 'default': True, - }, - { - 'id': 'ssl_ca_cert', - 'label': gettext_noop('Certificate Authority data'), - 'type': 'string', - 'secret': True, - 'multiline': True, - }, - ], - 'required': ['host', 'bearer_token'], - }, -) - -ManagedCredentialType( - namespace='registry', - kind='registry', - name=gettext_noop('Container Registry'), - inputs={ - 'fields': [ - { - 'id': 'host', - 'label': gettext_noop('Authentication URL'), - 'type': 'string', - 'help_text': gettext_noop('Authentication endpoint for the container registry.'), - 'default': 'quay.io', - }, - { - 'id': 'username', - 'label': gettext_noop('Username'), - 'type': 'string', - }, - { - 'id': 'password', - 'label': gettext_noop('Password or Token'), - 'type': 'string', - 'secret': True, - 'help_text': gettext_noop('A password or token used to authenticate with'), - }, - { - 'id': 'verify_ssl', - 'label': gettext_noop('Verify SSL'), - 'type': 'boolean', - 'default': True, - }, - ], - 'required': ['host'], - }, -) - - -ManagedCredentialType( - namespace='galaxy_api_token', - kind='galaxy', - name=gettext_noop('Ansible Galaxy/Automation Hub API Token'), - inputs={ - 'fields': [ - { - 'id': 'url', - 'label': gettext_noop('Galaxy Server URL'), - 'type': 'string', - 'help_text': gettext_noop('The URL of the Galaxy instance to connect to.'), - }, - { - 'id': 'auth_url', - 'label': gettext_noop('Auth Server URL'), - 'type': 'string', - 'help_text': gettext_noop('The URL of a Keycloak server token_endpoint, if using SSO auth.'), - }, - { - 'id': 'token', - 'label': gettext_noop('API Token'), - 'type': 'string', - 'secret': True, - 'help_text': gettext_noop('A token to use for authentication against the Galaxy instance.'), - }, - ], - 'required': ['url'], - }, -) - -ManagedCredentialType( - namespace='gpg_public_key', - kind='cryptography', - name=gettext_noop('GPG Public Key'), - inputs={ - 'fields': [ - { - 'id': 'gpg_public_key', - 'label': gettext_noop('GPG Public Key'), - 'type': 'string', - 'secret': True, - 'multiline': True, - 'help_text': gettext_noop('GPG Public Key used to validate content signatures.'), - }, - ], - 'required': ['gpg_public_key'], - }, -) - -ManagedCredentialType( - namespace='terraform', - kind='cloud', - name=gettext_noop('Terraform backend configuration'), - managed=True, - inputs={ - 'fields': [ - { - 'id': 'configuration', - 'label': gettext_noop('Backend configuration'), - 'type': 'string', - 'secret': True, - 'multiline': True, - 'help_text': gettext_noop('Terraform backend config as Hashicorp configuration language.'), - }, - { - 'id': 'gce_credentials', - 'label': gettext_noop('Google Cloud Platform account credentials'), - 'type': 'string', - 'secret': True, - 'multiline': True, - 'help_text': gettext_noop('Google Cloud Platform account credentials in JSON format.'), - }, - ], - 'required': ['configuration'], - }, -) - - class CredentialInputSource(PrimordialModel): class Meta: app_label = 'main' @@ -1325,5 +665,7 @@ class CredentialInputSource(PrimordialModel): return reverse(view_name, kwargs={'pk': self.pk}, request=request) +from awx_plugins.credentials.plugins import * # noqa + for ns, plugin in credential_plugins.items(): CredentialType.load_plugin(ns, plugin) diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 2b96ed549f..acc900250c 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -10,10 +10,6 @@ import copy import os.path from urllib.parse import urljoin -import yaml -import tempfile -import stat - # Django from django.conf import settings from django.db import models, connection @@ -28,6 +24,7 @@ from django.db.models import Q from rest_framework.exceptions import ParseError from ansible_base.lib.utils.models import prevent_search +from awx_plugins.inventory.plugins import PluginFileInjector # AWX from awx.api.versioning import reverse @@ -52,11 +49,9 @@ from awx.main.models.notifications import ( NotificationTemplate, JobNotificationMixin, ) -from awx.main.models.credential.injectors import _openstack_data from awx.main.utils import _inventory_updates from awx.main.utils.safe_yaml import sanitize_jinja -from awx.main.utils.execution_environments import to_container_path, get_control_plane_execution_environment -from awx.main.utils.licensing import server_product_name +from awx.main.utils.execution_environments import get_control_plane_execution_environment __all__ = ['Inventory', 'Host', 'Group', 'InventorySource', 'InventoryUpdate', 'SmartInventoryMembership', 'HostMetric', 'HostMetricSummaryMonthly'] @@ -1427,297 +1422,5 @@ class CustomInventoryScript(CommonModelNameNotUnique): return reverse('api:inventory_script_detail', kwargs={'pk': self.pk}, request=request) -class PluginFileInjector(object): - plugin_name = None # Ansible core name used to reference plugin - # base injector should be one of None, "managed", or "template" - # this dictates which logic to borrow from playbook injectors - base_injector = None - # every source should have collection, these are for the collection name - 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 - - # TODO: delete this method and update unit tests - @classmethod - def get_proper_name(cls): - if cls.plugin_name is None: - return None - return f'{cls.namespace}.{cls.collection}.{cls.plugin_name}' - - @property - def filename(self): - """Inventory filename for using the inventory plugin - This is created dynamically, but the auto plugin requires this exact naming - """ - return '{0}.yml'.format(self.plugin_name) - - def inventory_contents(self, inventory_update, private_data_dir): - """Returns a string that is the content for the inventory file for the inventory plugin""" - return yaml.safe_dump(self.inventory_as_dict(inventory_update, private_data_dir), default_flow_style=False, width=1000) - - def inventory_as_dict(self, inventory_update, private_data_dir): - source_vars = dict(inventory_update.source_vars_dict) # make a copy - ''' - None conveys that we should use the user-provided plugin. - Note that a plugin value of '' should still be overridden. - ''' - if self.plugin_name is not None: - if hasattr(self, 'downstream_namespace') and server_product_name() != 'AWX': - source_vars['plugin'] = f'{self.downstream_namespace}.{self.downstream_collection}.{self.plugin_name}' - elif self.use_fqcn: - source_vars['plugin'] = f'{self.namespace}.{self.collection}.{self.plugin_name}' - else: - source_vars['plugin'] = self.plugin_name - return source_vars - - def build_env(self, inventory_update, env, private_data_dir, private_data_files): - injector_env = self.get_plugin_env(inventory_update, private_data_dir, private_data_files) - env.update(injector_env) - # All CLOUD_PROVIDERS sources implement as inventory plugin from collection - env['ANSIBLE_INVENTORY_ENABLED'] = 'auto' - return env - - def _get_shared_env(self, inventory_update, private_data_dir, private_data_files): - """By default, we will apply the standard managed injectors""" - injected_env = {} - credential = inventory_update.get_cloud_credential() - # some sources may have no credential, specifically ec2 - if credential is None: - return injected_env - if self.base_injector in ('managed', 'template'): - injected_env['INVENTORY_UPDATE_ID'] = str(inventory_update.pk) # so injector knows this is inventory - if self.base_injector == 'managed': - from awx.main.models.credential import injectors as builtin_injectors - - cred_kind = inventory_update.source.replace('ec2', 'aws') - if cred_kind in dir(builtin_injectors): - getattr(builtin_injectors, cred_kind)(credential, injected_env, private_data_dir) - elif self.base_injector == 'template': - safe_env = injected_env.copy() - args = [] - credential.credential_type.inject_credential(credential, injected_env, safe_env, args, private_data_dir) - # NOTE: safe_env is handled externally to injector class by build_safe_env static method - # that means that managed injectors must only inject detectable env keys - # enforcement of this is accomplished by tests - return injected_env - - def get_plugin_env(self, inventory_update, private_data_dir, private_data_files): - env = self._get_shared_env(inventory_update, private_data_dir, private_data_files) - return env - - def build_private_data(self, inventory_update, private_data_dir): - return self.build_plugin_private_data(inventory_update, private_data_dir) - - def build_plugin_private_data(self, inventory_update, private_data_dir): - return None - - -class azure_rm(PluginFileInjector): - plugin_name = 'azure_rm' - base_injector = 'managed' - namespace = 'azure' - collection = 'azcollection' - - def get_plugin_env(self, *args, **kwargs): - ret = super(azure_rm, self).get_plugin_env(*args, **kwargs) - # We need native jinja2 types so that tags can give JSON null value - ret['ANSIBLE_JINJA2_NATIVE'] = str(True) - return ret - - -class ec2(PluginFileInjector): - plugin_name = 'aws_ec2' - base_injector = 'managed' - namespace = 'amazon' - collection = 'aws' - - def get_plugin_env(self, *args, **kwargs): - ret = super(ec2, self).get_plugin_env(*args, **kwargs) - # We need native jinja2 types so that ec2_state_code will give integer - ret['ANSIBLE_JINJA2_NATIVE'] = str(True) - return ret - - -class gce(PluginFileInjector): - plugin_name = 'gcp_compute' - base_injector = 'managed' - namespace = 'google' - collection = 'cloud' - - def get_plugin_env(self, *args, **kwargs): - ret = super(gce, self).get_plugin_env(*args, **kwargs) - # We need native jinja2 types so that ip addresses can give JSON null value - ret['ANSIBLE_JINJA2_NATIVE'] = str(True) - return ret - - def inventory_as_dict(self, inventory_update, private_data_dir): - ret = super().inventory_as_dict(inventory_update, private_data_dir) - credential = inventory_update.get_cloud_credential() - # InventorySource.source_vars take precedence over ENV vars - if 'projects' not in ret: - ret['projects'] = [credential.get_input('project', default='')] - return ret - - -class vmware(PluginFileInjector): - plugin_name = 'vmware_vm_inventory' - base_injector = 'managed' - namespace = 'community' - collection = 'vmware' - - -class openstack(PluginFileInjector): - plugin_name = 'openstack' - namespace = 'openstack' - collection = 'cloud' - - def _get_clouds_dict(self, inventory_update, cred, private_data_dir): - openstack_data = _openstack_data(cred) - - openstack_data['clouds']['devstack']['private'] = inventory_update.source_vars_dict.get('private', True) - ansible_variables = { - 'use_hostnames': True, - 'expand_hostvars': False, - 'fail_on_errors': True, - } - provided_count = 0 - for var_name in ansible_variables: - if var_name in inventory_update.source_vars_dict: - ansible_variables[var_name] = inventory_update.source_vars_dict[var_name] - provided_count += 1 - if provided_count: - # Must we provide all 3 because the user provides any 1 of these?? - # this probably results in some incorrect mangling of the defaults - openstack_data['ansible'] = ansible_variables - return openstack_data - - def build_plugin_private_data(self, inventory_update, private_data_dir): - credential = inventory_update.get_cloud_credential() - private_data = {'credentials': {}} - - openstack_data = self._get_clouds_dict(inventory_update, credential, private_data_dir) - private_data['credentials'][credential] = yaml.safe_dump(openstack_data, default_flow_style=False, allow_unicode=True) - return private_data - - def get_plugin_env(self, inventory_update, private_data_dir, private_data_files): - env = super(openstack, self).get_plugin_env(inventory_update, private_data_dir, private_data_files) - credential = inventory_update.get_cloud_credential() - cred_data = private_data_files['credentials'] - env['OS_CLIENT_CONFIG_FILE'] = to_container_path(cred_data[credential], private_data_dir) - return env - - -class rhv(PluginFileInjector): - """ovirt uses the custom credential templating, and that is all""" - - plugin_name = 'ovirt' - base_injector = 'template' - initial_version = '2.9' - namespace = 'ovirt' - collection = 'ovirt' - downstream_namespace = 'redhat' - downstream_collection = 'rhv' - use_fqcn = True - - -class satellite6(PluginFileInjector): - plugin_name = 'foreman' - namespace = 'theforeman' - collection = 'foreman' - downstream_namespace = 'redhat' - downstream_collection = 'satellite' - use_fqcn = True - - def get_plugin_env(self, inventory_update, private_data_dir, private_data_files): - # this assumes that this is merged - # https://github.com/ansible/ansible/pull/52693 - credential = inventory_update.get_cloud_credential() - ret = super(satellite6, self).get_plugin_env(inventory_update, private_data_dir, private_data_files) - if credential: - ret['FOREMAN_SERVER'] = credential.get_input('host', default='') - ret['FOREMAN_USER'] = credential.get_input('username', default='') - ret['FOREMAN_PASSWORD'] = credential.get_input('password', default='') - return ret - - -class terraform(PluginFileInjector): - plugin_name = 'terraform_state' - namespace = 'cloud' - collection = 'terraform' - use_fqcn = True - - def inventory_as_dict(self, inventory_update, private_data_dir): - ret = super().inventory_as_dict(inventory_update, private_data_dir) - credential = inventory_update.get_cloud_credential() - config_cred = credential.get_input('configuration') - if config_cred: - handle, path = tempfile.mkstemp(dir=os.path.join(private_data_dir, 'env')) - with os.fdopen(handle, 'w') as f: - os.chmod(path, stat.S_IRUSR | stat.S_IWUSR) - f.write(config_cred) - ret['backend_config_files'] = to_container_path(path, private_data_dir) - return ret - - def build_plugin_private_data(self, inventory_update, private_data_dir): - credential = inventory_update.get_cloud_credential() - - private_data = {'credentials': {}} - gce_cred = credential.get_input('gce_credentials', default=None) - if gce_cred: - private_data['credentials'][credential] = gce_cred - return private_data - - def get_plugin_env(self, inventory_update, private_data_dir, private_data_files): - env = super(terraform, self).get_plugin_env(inventory_update, private_data_dir, private_data_files) - credential = inventory_update.get_cloud_credential() - cred_data = private_data_files['credentials'] - if credential in cred_data: - env['GOOGLE_BACKEND_CREDENTIALS'] = to_container_path(cred_data[credential], private_data_dir) - return env - - -class controller(PluginFileInjector): - plugin_name = 'tower' # TODO: relying on routing for now, update after EEs pick up revised collection - base_injector = 'template' - namespace = 'awx' - collection = 'awx' - downstream_namespace = 'ansible' - downstream_collection = 'controller' - - -class insights(PluginFileInjector): - plugin_name = 'insights' - base_injector = 'template' - namespace = 'redhatinsights' - collection = 'insights' - downstream_namespace = 'redhat' - downstream_collection = 'insights' - use_fqcn = True - - -class openshift_virtualization(PluginFileInjector): - plugin_name = 'kubevirt' - base_injector = 'template' - namespace = 'kubevirt' - collection = 'core' - downstream_namespace = 'redhat' - downstream_collection = 'openshift_virtualization' - use_fqcn = True - - -class constructed(PluginFileInjector): - plugin_name = 'constructed' - namespace = 'ansible' - collection = 'builtin' - - def build_env(self, *args, **kwargs): - env = super().build_env(*args, **kwargs) - # Enable script inventory plugin so we pick up the script files from source inventories - env['ANSIBLE_INVENTORY_ENABLED'] += ',script' - env['ANSIBLE_INVENTORY_ANY_UNPARSED_IS_FAILED'] = 'True' - return env - - for cls in PluginFileInjector.__subclasses__(): InventorySourceOptions.injectors[cls.__name__] = cls diff --git a/awx/main/credential_plugins/aim.py b/awx_plugins/credentials/aim.py index 2476042b5f..dc06b0ea6f 100644 --- a/awx/main/credential_plugins/aim.py +++ b/awx_plugins/credentials/aim.py @@ -2,7 +2,7 @@ from .plugin import CredentialPlugin, CertFiles, raise_for_status from urllib.parse import quote, urlencode, urljoin -from django.utils.translation import gettext_lazy as _ +from .plugin import translate_function as _ import requests aim_inputs = { diff --git a/awx/main/credential_plugins/aws_secretsmanager.py b/awx_plugins/credentials/aws_secretsmanager.py index fa85f5e52a..335113b2a8 100644 --- a/awx/main/credential_plugins/aws_secretsmanager.py +++ b/awx_plugins/credentials/aws_secretsmanager.py @@ -2,7 +2,7 @@ import boto3 from botocore.exceptions import ClientError from .plugin import CredentialPlugin -from django.utils.translation import gettext_lazy as _ +from .plugin import translate_function as _ secrets_manager_inputs = { diff --git a/awx/main/credential_plugins/azure_kv.py b/awx_plugins/credentials/azure_kv.py index 8910a0726d..7579dbee3d 100644 --- a/awx/main/credential_plugins/azure_kv.py +++ b/awx_plugins/credentials/azure_kv.py @@ -4,7 +4,7 @@ from msrestazure import azure_cloud from .plugin import CredentialPlugin -from django.utils.translation import gettext_lazy as _ +from .plugin import translate_function as _ # https://github.com/Azure/msrestazure-for-python/blob/master/msrestazure/azure_cloud.py diff --git a/awx/main/credential_plugins/centrify_vault.py b/awx_plugins/credentials/centrify_vault.py index 1e05625e71..b2d97a1db3 100644 --- a/awx/main/credential_plugins/centrify_vault.py +++ b/awx_plugins/credentials/centrify_vault.py @@ -1,5 +1,5 @@ from .plugin import CredentialPlugin, raise_for_status -from django.utils.translation import gettext_lazy as _ +from .plugin import translate_function as _ from urllib.parse import urljoin import requests diff --git a/awx/main/credential_plugins/conjur.py b/awx_plugins/credentials/conjur.py index e6984bed46..a7fd3a3a65 100644 --- a/awx/main/credential_plugins/conjur.py +++ b/awx_plugins/credentials/conjur.py @@ -2,7 +2,7 @@ from .plugin import CredentialPlugin, CertFiles, raise_for_status from urllib.parse import urljoin, quote -from django.utils.translation import gettext_lazy as _ +from .plugin import translate_function as _ import requests import base64 import binascii diff --git a/awx/main/credential_plugins/dsv.py b/awx_plugins/credentials/dsv.py index 7dc74cab91..8296779bde 100644 --- a/awx/main/credential_plugins/dsv.py +++ b/awx_plugins/credentials/dsv.py @@ -1,7 +1,7 @@ from .plugin import CredentialPlugin -from django.conf import settings -from django.utils.translation import gettext_lazy as _ +from .plugin import settings +from .plugin import translate_function as _ from delinea.secrets.vault import PasswordGrantAuthorizer, SecretsVault from base64 import b64decode diff --git a/awx/main/credential_plugins/hashivault.py b/awx_plugins/credentials/hashivault.py index f3dcd53b5d..81f7770f51 100644 --- a/awx/main/credential_plugins/hashivault.py +++ b/awx_plugins/credentials/hashivault.py @@ -7,7 +7,7 @@ from urllib.parse import urljoin from .plugin import CredentialPlugin, CertFiles, raise_for_status import requests -from django.utils.translation import gettext_lazy as _ +from .plugin import translate_function as _ base_inputs = { 'fields': [ diff --git a/awx/main/models/credential/injectors.py b/awx_plugins/credentials/injectors.py index 29a438f919..29a438f919 100644 --- a/awx/main/models/credential/injectors.py +++ b/awx_plugins/credentials/injectors.py diff --git a/awx/main/credential_plugins/plugin.py b/awx_plugins/credentials/plugin.py index 7219231efc..b8aa294544 100644 --- a/awx/main/credential_plugins/plugin.py +++ b/awx_plugins/credentials/plugin.py @@ -8,6 +8,19 @@ from requests.exceptions import HTTPError CredentialPlugin = namedtuple('CredentialPlugin', ['name', 'inputs', 'backend']) +try: + from django.utils.translation import gettext_lazy as translate_function +except ModuleNotFoundError: + translate_function = lambda *args, **kwargs: None + + +class Settings(): + DEBUG = False + + +settings = Settings() + + def raise_for_status(resp): resp.raise_for_status() if resp.status_code >= 300: diff --git a/awx_plugins/credentials/plugins.py b/awx_plugins/credentials/plugins.py new file mode 100644 index 0000000000..debc5c7032 --- /dev/null +++ b/awx_plugins/credentials/plugins.py @@ -0,0 +1,665 @@ +# Django +from django.utils.translation import gettext_noop + +# AWX +from awx.main.models.credential import ManagedCredentialType + + +ManagedCredentialType( + namespace='ssh', + kind='ssh', + name=gettext_noop('Machine'), + inputs={ + 'fields': [ + {'id': 'username', 'label': gettext_noop('Username'), 'type': 'string'}, + {'id': 'password', 'label': gettext_noop('Password'), 'type': 'string', 'secret': True, 'ask_at_runtime': True}, + {'id': 'ssh_key_data', 'label': gettext_noop('SSH Private Key'), 'type': 'string', 'format': 'ssh_private_key', 'secret': True, 'multiline': True}, + { + 'id': 'ssh_public_key_data', + 'label': gettext_noop('Signed SSH Certificate'), + 'type': 'string', + 'multiline': True, + 'secret': True, + }, + {'id': 'ssh_key_unlock', 'label': gettext_noop('Private Key Passphrase'), 'type': 'string', 'secret': True, 'ask_at_runtime': True}, + { + 'id': 'become_method', + 'label': gettext_noop('Privilege Escalation Method'), + 'type': 'string', + 'help_text': gettext_noop('Specify a method for "become" operations. This is equivalent to specifying the --become-method Ansible parameter.'), + }, + { + 'id': 'become_username', + 'label': gettext_noop('Privilege Escalation Username'), + 'type': 'string', + }, + {'id': 'become_password', 'label': gettext_noop('Privilege Escalation Password'), 'type': 'string', 'secret': True, 'ask_at_runtime': True}, + ], + }, +) + +ManagedCredentialType( + namespace='scm', + kind='scm', + name=gettext_noop('Source Control'), + managed=True, + inputs={ + 'fields': [ + {'id': 'username', 'label': gettext_noop('Username'), 'type': 'string'}, + {'id': 'password', 'label': gettext_noop('Password'), 'type': 'string', 'secret': True}, + {'id': 'ssh_key_data', 'label': gettext_noop('SCM Private Key'), 'type': 'string', 'format': 'ssh_private_key', 'secret': True, 'multiline': True}, + {'id': 'ssh_key_unlock', 'label': gettext_noop('Private Key Passphrase'), 'type': 'string', 'secret': True}, + ], + }, +) + +ManagedCredentialType( + namespace='vault', + kind='vault', + name=gettext_noop('Vault'), + managed=True, + inputs={ + 'fields': [ + {'id': 'vault_password', 'label': gettext_noop('Vault Password'), 'type': 'string', 'secret': True, 'ask_at_runtime': True}, + { + 'id': 'vault_id', + 'label': gettext_noop('Vault Identifier'), + 'type': 'string', + 'format': 'vault_id', + 'help_text': gettext_noop( + 'Specify an (optional) Vault ID. This is ' + 'equivalent to specifying the --vault-id ' + 'Ansible parameter for providing multiple Vault ' + 'passwords. Note: this feature only works in ' + 'Ansible 2.4+.' + ), + }, + ], + 'required': ['vault_password'], + }, +) + +ManagedCredentialType( + namespace='net', + kind='net', + name=gettext_noop('Network'), + managed=True, + inputs={ + 'fields': [ + {'id': 'username', 'label': gettext_noop('Username'), 'type': 'string'}, + { + 'id': 'password', + 'label': gettext_noop('Password'), + 'type': 'string', + 'secret': True, + }, + {'id': 'ssh_key_data', 'label': gettext_noop('SSH Private Key'), 'type': 'string', 'format': 'ssh_private_key', 'secret': True, 'multiline': True}, + { + 'id': 'ssh_key_unlock', + 'label': gettext_noop('Private Key Passphrase'), + 'type': 'string', + 'secret': True, + }, + { + 'id': 'authorize', + 'label': gettext_noop('Authorize'), + 'type': 'boolean', + }, + { + 'id': 'authorize_password', + 'label': gettext_noop('Authorize Password'), + 'type': 'string', + 'secret': True, + }, + ], + 'dependencies': { + 'authorize_password': ['authorize'], + }, + 'required': ['username'], + }, +) + +ManagedCredentialType( + namespace='aws', + kind='cloud', + name=gettext_noop('Amazon Web Services'), + managed=True, + inputs={ + 'fields': [ + {'id': 'username', 'label': gettext_noop('Access Key'), 'type': 'string'}, + { + 'id': 'password', + 'label': gettext_noop('Secret Key'), + 'type': 'string', + 'secret': True, + }, + { + 'id': 'security_token', + 'label': gettext_noop('STS Token'), + 'type': 'string', + 'secret': True, + 'help_text': gettext_noop( + 'Security Token Service (STS) is a web service ' + 'that enables you to request temporary, ' + 'limited-privilege credentials for AWS Identity ' + 'and Access Management (IAM) users.' + ), + }, + ], + 'required': ['username', 'password'], + }, +) + +ManagedCredentialType( + namespace='openstack', + kind='cloud', + name=gettext_noop('OpenStack'), + managed=True, + inputs={ + 'fields': [ + {'id': 'username', 'label': gettext_noop('Username'), 'type': 'string'}, + { + 'id': 'password', + 'label': gettext_noop('Password (API Key)'), + 'type': 'string', + 'secret': True, + }, + { + 'id': 'host', + 'label': gettext_noop('Host (Authentication URL)'), + 'type': 'string', + 'help_text': gettext_noop('The host to authenticate with. For example, https://openstack.business.com/v2.0/'), + }, + { + 'id': 'project', + 'label': gettext_noop('Project (Tenant Name)'), + 'type': 'string', + }, + { + 'id': 'project_domain_name', + 'label': gettext_noop('Project (Domain Name)'), + 'type': 'string', + }, + { + 'id': 'domain', + 'label': gettext_noop('Domain Name'), + 'type': 'string', + 'help_text': gettext_noop( + 'OpenStack domains define administrative boundaries. ' + 'It is only needed for Keystone v3 authentication ' + 'URLs. Refer to the documentation for ' + 'common scenarios.' + ), + }, + { + 'id': 'region', + 'label': gettext_noop('Region Name'), + 'type': 'string', + 'help_text': gettext_noop('For some cloud providers, like OVH, region must be specified'), + }, + { + 'id': 'verify_ssl', + 'label': gettext_noop('Verify SSL'), + 'type': 'boolean', + 'default': True, + }, + ], + 'required': ['username', 'password', 'host', 'project'], + }, +) + +ManagedCredentialType( + namespace='vmware', + kind='cloud', + name=gettext_noop('VMware vCenter'), + managed=True, + inputs={ + 'fields': [ + { + 'id': 'host', + 'label': gettext_noop('VCenter Host'), + 'type': 'string', + 'help_text': gettext_noop('Enter the hostname or IP address that corresponds to your VMware vCenter.'), + }, + {'id': 'username', 'label': gettext_noop('Username'), 'type': 'string'}, + { + 'id': 'password', + 'label': gettext_noop('Password'), + 'type': 'string', + 'secret': True, + }, + ], + 'required': ['host', 'username', 'password'], + }, +) + +ManagedCredentialType( + namespace='satellite6', + kind='cloud', + name=gettext_noop('Red Hat Satellite 6'), + managed=True, + inputs={ + 'fields': [ + { + 'id': 'host', + 'label': gettext_noop('Satellite 6 URL'), + 'type': 'string', + 'help_text': gettext_noop('Enter the URL that corresponds to your Red Hat Satellite 6 server. For example, https://satellite.example.org'), + }, + {'id': 'username', 'label': gettext_noop('Username'), 'type': 'string'}, + { + 'id': 'password', + 'label': gettext_noop('Password'), + 'type': 'string', + 'secret': True, + }, + ], + 'required': ['host', 'username', 'password'], + }, +) + +ManagedCredentialType( + namespace='gce', + kind='cloud', + name=gettext_noop('Google Compute Engine'), + managed=True, + inputs={ + 'fields': [ + { + 'id': 'username', + 'label': gettext_noop('Service Account Email Address'), + 'type': 'string', + 'help_text': gettext_noop('The email address assigned to the Google Compute Engine service account.'), + }, + { + 'id': 'project', + 'label': 'Project', + 'type': 'string', + 'help_text': gettext_noop( + 'The Project ID is the GCE assigned identification. ' + 'It is often constructed as three words or two words ' + 'followed by a three-digit number. Examples: project-id-000 ' + 'and another-project-id' + ), + }, + { + 'id': 'ssh_key_data', + 'label': gettext_noop('RSA Private Key'), + 'type': 'string', + 'format': 'ssh_private_key', + 'secret': True, + 'multiline': True, + 'help_text': gettext_noop('Paste the contents of the PEM file associated with the service account email.'), + }, + ], + 'required': ['username', 'ssh_key_data'], + }, +) + +ManagedCredentialType( + namespace='azure_rm', + kind='cloud', + name=gettext_noop('Microsoft Azure Resource Manager'), + managed=True, + inputs={ + 'fields': [ + { + 'id': 'subscription', + 'label': gettext_noop('Subscription ID'), + 'type': 'string', + 'help_text': gettext_noop('Subscription ID is an Azure construct, which is mapped to a username.'), + }, + {'id': 'username', 'label': gettext_noop('Username'), 'type': 'string'}, + { + 'id': 'password', + 'label': gettext_noop('Password'), + 'type': 'string', + 'secret': True, + }, + {'id': 'client', 'label': gettext_noop('Client ID'), 'type': 'string'}, + { + 'id': 'secret', + 'label': gettext_noop('Client Secret'), + 'type': 'string', + 'secret': True, + }, + {'id': 'tenant', 'label': gettext_noop('Tenant ID'), 'type': 'string'}, + { + 'id': 'cloud_environment', + 'label': gettext_noop('Azure Cloud Environment'), + 'type': 'string', + 'help_text': gettext_noop('Environment variable AZURE_CLOUD_ENVIRONMENT when using Azure GovCloud or Azure stack.'), + }, + ], + 'required': ['subscription'], + }, +) + +ManagedCredentialType( + namespace='github_token', + kind='token', + name=gettext_noop('GitHub Personal Access Token'), + managed=True, + inputs={ + 'fields': [ + { + 'id': 'token', + 'label': gettext_noop('Token'), + 'type': 'string', + 'secret': True, + 'help_text': gettext_noop('This token needs to come from your profile settings in GitHub'), + } + ], + 'required': ['token'], + }, +) + +ManagedCredentialType( + namespace='gitlab_token', + kind='token', + name=gettext_noop('GitLab Personal Access Token'), + managed=True, + inputs={ + 'fields': [ + { + 'id': 'token', + 'label': gettext_noop('Token'), + 'type': 'string', + 'secret': True, + 'help_text': gettext_noop('This token needs to come from your profile settings in GitLab'), + } + ], + 'required': ['token'], + }, +) + +ManagedCredentialType( + namespace='bitbucket_dc_token', + kind='token', + name=gettext_noop('Bitbucket Data Center HTTP Access Token'), + managed=True, + inputs={ + 'fields': [ + { + 'id': 'token', + 'label': gettext_noop('Token'), + 'type': 'string', + 'secret': True, + 'help_text': gettext_noop('This token needs to come from your user settings in Bitbucket'), + } + ], + 'required': ['token'], + }, +) + +ManagedCredentialType( + namespace='insights', + kind='insights', + name=gettext_noop('Insights'), + managed=True, + inputs={ + 'fields': [ + {'id': 'username', 'label': gettext_noop('Username'), 'type': 'string'}, + {'id': 'password', 'label': gettext_noop('Password'), 'type': 'string', 'secret': True}, + ], + 'required': ['username', 'password'], + }, + injectors={ + 'extra_vars': { + "scm_username": "{{username}}", + "scm_password": "{{password}}", + }, + 'env': { + 'INSIGHTS_USER': '{{username}}', + 'INSIGHTS_PASSWORD': '{{password}}', + }, + }, +) + +ManagedCredentialType( + namespace='rhv', + kind='cloud', + name=gettext_noop('Red Hat Virtualization'), + managed=True, + inputs={ + 'fields': [ + {'id': 'host', 'label': gettext_noop('Host (Authentication URL)'), 'type': 'string', 'help_text': gettext_noop('The host to authenticate with.')}, + {'id': 'username', 'label': gettext_noop('Username'), 'type': 'string'}, + { + 'id': 'password', + 'label': gettext_noop('Password'), + 'type': 'string', + 'secret': True, + }, + { + 'id': 'ca_file', + 'label': gettext_noop('CA File'), + 'type': 'string', + 'help_text': gettext_noop('Absolute file path to the CA file to use (optional)'), + }, + ], + 'required': ['host', 'username', 'password'], + }, + injectors={ + # The duplication here is intentional; the ovirt4 inventory plugin + # writes a .ini file for authentication, while the ansible modules for + # ovirt4 use a separate authentication process that support + # environment variables; by injecting both, we support both + 'file': { + 'template': '\n'.join( + [ + '[ovirt]', + 'ovirt_url={{host}}', + 'ovirt_username={{username}}', + 'ovirt_password={{password}}', + '{% if ca_file %}ovirt_ca_file={{ca_file}}{% endif %}', + ] + ) + }, + 'env': {'OVIRT_INI_PATH': '{{tower.filename}}', 'OVIRT_URL': '{{host}}', 'OVIRT_USERNAME': '{{username}}', 'OVIRT_PASSWORD': '{{password}}'}, + }, +) + +ManagedCredentialType( + namespace='controller', + kind='cloud', + name=gettext_noop('Red Hat Ansible Automation Platform'), + managed=True, + inputs={ + 'fields': [ + { + 'id': 'host', + 'label': gettext_noop('Red Hat Ansible Automation Platform'), + 'type': 'string', + 'help_text': gettext_noop('Red Hat Ansible Automation Platform base URL to authenticate with.'), + }, + { + 'id': 'username', + 'label': gettext_noop('Username'), + 'type': 'string', + 'help_text': gettext_noop( + 'Red Hat Ansible Automation Platform username id to authenticate as.This should not be set if an OAuth token is being used.' + ), + }, + { + 'id': 'password', + 'label': gettext_noop('Password'), + 'type': 'string', + 'secret': True, + }, + { + 'id': 'oauth_token', + 'label': gettext_noop('OAuth Token'), + 'type': 'string', + 'secret': True, + 'help_text': gettext_noop('An OAuth token to use to authenticate with.This should not be set if username/password are being used.'), + }, + {'id': 'verify_ssl', 'label': gettext_noop('Verify SSL'), 'type': 'boolean', 'secret': False}, + ], + 'required': ['host'], + }, + injectors={ + 'env': { + 'TOWER_HOST': '{{host}}', + 'TOWER_USERNAME': '{{username}}', + 'TOWER_PASSWORD': '{{password}}', + 'TOWER_VERIFY_SSL': '{{verify_ssl}}', + 'TOWER_OAUTH_TOKEN': '{{oauth_token}}', + 'CONTROLLER_HOST': '{{host}}', + 'CONTROLLER_USERNAME': '{{username}}', + 'CONTROLLER_PASSWORD': '{{password}}', + 'CONTROLLER_VERIFY_SSL': '{{verify_ssl}}', + 'CONTROLLER_OAUTH_TOKEN': '{{oauth_token}}', + } + }, +) + +ManagedCredentialType( + namespace='kubernetes_bearer_token', + kind='kubernetes', + name=gettext_noop('OpenShift or Kubernetes API Bearer Token'), + inputs={ + 'fields': [ + { + 'id': 'host', + 'label': gettext_noop('OpenShift or Kubernetes API Endpoint'), + 'type': 'string', + 'help_text': gettext_noop('The OpenShift or Kubernetes API Endpoint to authenticate with.'), + }, + { + 'id': 'bearer_token', + 'label': gettext_noop('API authentication bearer token'), + 'type': 'string', + 'secret': True, + }, + { + 'id': 'verify_ssl', + 'label': gettext_noop('Verify SSL'), + 'type': 'boolean', + 'default': True, + }, + { + 'id': 'ssl_ca_cert', + 'label': gettext_noop('Certificate Authority data'), + 'type': 'string', + 'secret': True, + 'multiline': True, + }, + ], + 'required': ['host', 'bearer_token'], + }, +) + +ManagedCredentialType( + namespace='registry', + kind='registry', + name=gettext_noop('Container Registry'), + inputs={ + 'fields': [ + { + 'id': 'host', + 'label': gettext_noop('Authentication URL'), + 'type': 'string', + 'help_text': gettext_noop('Authentication endpoint for the container registry.'), + 'default': 'quay.io', + }, + { + 'id': 'username', + 'label': gettext_noop('Username'), + 'type': 'string', + }, + { + 'id': 'password', + 'label': gettext_noop('Password or Token'), + 'type': 'string', + 'secret': True, + 'help_text': gettext_noop('A password or token used to authenticate with'), + }, + { + 'id': 'verify_ssl', + 'label': gettext_noop('Verify SSL'), + 'type': 'boolean', + 'default': True, + }, + ], + 'required': ['host'], + }, +) + + +ManagedCredentialType( + namespace='galaxy_api_token', + kind='galaxy', + name=gettext_noop('Ansible Galaxy/Automation Hub API Token'), + inputs={ + 'fields': [ + { + 'id': 'url', + 'label': gettext_noop('Galaxy Server URL'), + 'type': 'string', + 'help_text': gettext_noop('The URL of the Galaxy instance to connect to.'), + }, + { + 'id': 'auth_url', + 'label': gettext_noop('Auth Server URL'), + 'type': 'string', + 'help_text': gettext_noop('The URL of a Keycloak server token_endpoint, if using SSO auth.'), + }, + { + 'id': 'token', + 'label': gettext_noop('API Token'), + 'type': 'string', + 'secret': True, + 'help_text': gettext_noop('A token to use for authentication against the Galaxy instance.'), + }, + ], + 'required': ['url'], + }, +) + +ManagedCredentialType( + namespace='gpg_public_key', + kind='cryptography', + name=gettext_noop('GPG Public Key'), + inputs={ + 'fields': [ + { + 'id': 'gpg_public_key', + 'label': gettext_noop('GPG Public Key'), + 'type': 'string', + 'secret': True, + 'multiline': True, + 'help_text': gettext_noop('GPG Public Key used to validate content signatures.'), + }, + ], + 'required': ['gpg_public_key'], + }, +) + +ManagedCredentialType( + namespace='terraform', + kind='cloud', + name=gettext_noop('Terraform backend configuration'), + managed=True, + inputs={ + 'fields': [ + { + 'id': 'configuration', + 'label': gettext_noop('Backend configuration'), + 'type': 'string', + 'secret': True, + 'multiline': True, + 'help_text': gettext_noop('Terraform backend config as Hashicorp configuration language.'), + }, + { + 'id': 'gce_credentials', + 'label': gettext_noop('Google Cloud Platform account credentials'), + 'type': 'string', + 'secret': True, + 'multiline': True, + 'help_text': gettext_noop('Google Cloud Platform account credentials in JSON format.'), + }, + ], + 'required': ['configuration'], + }, +) diff --git a/awx/main/credential_plugins/tss.py b/awx_plugins/credentials/tss.py index 682c6c8639..e295072233 100644 --- a/awx/main/credential_plugins/tss.py +++ b/awx_plugins/credentials/tss.py @@ -1,5 +1,5 @@ from .plugin import CredentialPlugin -from django.utils.translation import gettext_lazy as _ +from .plugin import translate_function as _ try: from delinea.secrets.server import DomainPasswordGrantAuthorizer, PasswordGrantAuthorizer, SecretServer, ServerSecret diff --git a/awx_plugins/inventory/plugins.py b/awx_plugins/inventory/plugins.py new file mode 100644 index 0000000000..9a42173482 --- /dev/null +++ b/awx_plugins/inventory/plugins.py @@ -0,0 +1,302 @@ +import yaml +import stat +import tempfile + +import os.path + +from awx_plugins.credentials.injectors import _openstack_data +from awx.main.utils.execution_environments import to_container_path + +from awx.main.utils.licensing import server_product_name + + +class PluginFileInjector(object): + plugin_name = None # Ansible core name used to reference plugin + # base injector should be one of None, "managed", or "template" + # this dictates which logic to borrow from playbook injectors + base_injector = None + # every source should have collection, these are for the collection name + 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 + + # TODO: delete this method and update unit tests + @classmethod + def get_proper_name(cls): + if cls.plugin_name is None: + return None + return f'{cls.namespace}.{cls.collection}.{cls.plugin_name}' + + @property + def filename(self): + """Inventory filename for using the inventory plugin + This is created dynamically, but the auto plugin requires this exact naming + """ + return '{0}.yml'.format(self.plugin_name) + + def inventory_contents(self, inventory_update, private_data_dir): + """Returns a string that is the content for the inventory file for the inventory plugin""" + return yaml.safe_dump(self.inventory_as_dict(inventory_update, private_data_dir), default_flow_style=False, width=1000) + + def inventory_as_dict(self, inventory_update, private_data_dir): + source_vars = dict(inventory_update.source_vars_dict) # make a copy + ''' + None conveys that we should use the user-provided plugin. + Note that a plugin value of '' should still be overridden. + ''' + if self.plugin_name is not None: + if hasattr(self, 'downstream_namespace') and server_product_name() != 'AWX': + source_vars['plugin'] = f'{self.downstream_namespace}.{self.downstream_collection}.{self.plugin_name}' + elif self.use_fqcn: + source_vars['plugin'] = f'{self.namespace}.{self.collection}.{self.plugin_name}' + else: + source_vars['plugin'] = self.plugin_name + return source_vars + + def build_env(self, inventory_update, env, private_data_dir, private_data_files): + injector_env = self.get_plugin_env(inventory_update, private_data_dir, private_data_files) + env.update(injector_env) + # All CLOUD_PROVIDERS sources implement as inventory plugin from collection + env['ANSIBLE_INVENTORY_ENABLED'] = 'auto' + return env + + def _get_shared_env(self, inventory_update, private_data_dir, private_data_files): + """By default, we will apply the standard managed injectors""" + injected_env = {} + credential = inventory_update.get_cloud_credential() + # some sources may have no credential, specifically ec2 + if credential is None: + return injected_env + if self.base_injector in ('managed', 'template'): + injected_env['INVENTORY_UPDATE_ID'] = str(inventory_update.pk) # so injector knows this is inventory + if self.base_injector == 'managed': + from awx_plugins.credentials import injectors as builtin_injectors + + cred_kind = inventory_update.source.replace('ec2', 'aws') + if cred_kind in dir(builtin_injectors): + getattr(builtin_injectors, cred_kind)(credential, injected_env, private_data_dir) + elif self.base_injector == 'template': + safe_env = injected_env.copy() + args = [] + credential.credential_type.inject_credential(credential, injected_env, safe_env, args, private_data_dir) + # NOTE: safe_env is handled externally to injector class by build_safe_env static method + # that means that managed injectors must only inject detectable env keys + # enforcement of this is accomplished by tests + return injected_env + + def get_plugin_env(self, inventory_update, private_data_dir, private_data_files): + env = self._get_shared_env(inventory_update, private_data_dir, private_data_files) + return env + + def build_private_data(self, inventory_update, private_data_dir): + return self.build_plugin_private_data(inventory_update, private_data_dir) + + def build_plugin_private_data(self, inventory_update, private_data_dir): + return None + + +class azure_rm(PluginFileInjector): + plugin_name = 'azure_rm' + base_injector = 'managed' + namespace = 'azure' + collection = 'azcollection' + + def get_plugin_env(self, *args, **kwargs): + ret = super(azure_rm, self).get_plugin_env(*args, **kwargs) + # We need native jinja2 types so that tags can give JSON null value + ret['ANSIBLE_JINJA2_NATIVE'] = str(True) + return ret + + +class ec2(PluginFileInjector): + plugin_name = 'aws_ec2' + base_injector = 'managed' + namespace = 'amazon' + collection = 'aws' + + def get_plugin_env(self, *args, **kwargs): + ret = super(ec2, self).get_plugin_env(*args, **kwargs) + # We need native jinja2 types so that ec2_state_code will give integer + ret['ANSIBLE_JINJA2_NATIVE'] = str(True) + return ret + + +class gce(PluginFileInjector): + plugin_name = 'gcp_compute' + base_injector = 'managed' + namespace = 'google' + collection = 'cloud' + + def get_plugin_env(self, *args, **kwargs): + ret = super(gce, self).get_plugin_env(*args, **kwargs) + # We need native jinja2 types so that ip addresses can give JSON null value + ret['ANSIBLE_JINJA2_NATIVE'] = str(True) + return ret + + def inventory_as_dict(self, inventory_update, private_data_dir): + ret = super().inventory_as_dict(inventory_update, private_data_dir) + credential = inventory_update.get_cloud_credential() + # InventorySource.source_vars take precedence over ENV vars + if 'projects' not in ret: + ret['projects'] = [credential.get_input('project', default='')] + return ret + + +class vmware(PluginFileInjector): + plugin_name = 'vmware_vm_inventory' + base_injector = 'managed' + namespace = 'community' + collection = 'vmware' + + +class openstack(PluginFileInjector): + plugin_name = 'openstack' + namespace = 'openstack' + collection = 'cloud' + + def _get_clouds_dict(self, inventory_update, cred, private_data_dir): + openstack_data = _openstack_data(cred) + + openstack_data['clouds']['devstack']['private'] = inventory_update.source_vars_dict.get('private', True) + ansible_variables = { + 'use_hostnames': True, + 'expand_hostvars': False, + 'fail_on_errors': True, + } + provided_count = 0 + for var_name in ansible_variables: + if var_name in inventory_update.source_vars_dict: + ansible_variables[var_name] = inventory_update.source_vars_dict[var_name] + provided_count += 1 + if provided_count: + # Must we provide all 3 because the user provides any 1 of these?? + # this probably results in some incorrect mangling of the defaults + openstack_data['ansible'] = ansible_variables + return openstack_data + + def build_plugin_private_data(self, inventory_update, private_data_dir): + credential = inventory_update.get_cloud_credential() + private_data = {'credentials': {}} + + openstack_data = self._get_clouds_dict(inventory_update, credential, private_data_dir) + private_data['credentials'][credential] = yaml.safe_dump(openstack_data, default_flow_style=False, allow_unicode=True) + return private_data + + def get_plugin_env(self, inventory_update, private_data_dir, private_data_files): + env = super(openstack, self).get_plugin_env(inventory_update, private_data_dir, private_data_files) + credential = inventory_update.get_cloud_credential() + cred_data = private_data_files['credentials'] + env['OS_CLIENT_CONFIG_FILE'] = to_container_path(cred_data[credential], private_data_dir) + return env + + +class rhv(PluginFileInjector): + """ovirt uses the custom credential templating, and that is all""" + + plugin_name = 'ovirt' + base_injector = 'template' + initial_version = '2.9' + namespace = 'ovirt' + collection = 'ovirt' + downstream_namespace = 'redhat' + downstream_collection = 'rhv' + use_fqcn = True + + +class satellite6(PluginFileInjector): + plugin_name = 'foreman' + namespace = 'theforeman' + collection = 'foreman' + downstream_namespace = 'redhat' + downstream_collection = 'satellite' + use_fqcn = True + + def get_plugin_env(self, inventory_update, private_data_dir, private_data_files): + # this assumes that this is merged + # https://github.com/ansible/ansible/pull/52693 + credential = inventory_update.get_cloud_credential() + ret = super(satellite6, self).get_plugin_env(inventory_update, private_data_dir, private_data_files) + if credential: + ret['FOREMAN_SERVER'] = credential.get_input('host', default='') + ret['FOREMAN_USER'] = credential.get_input('username', default='') + ret['FOREMAN_PASSWORD'] = credential.get_input('password', default='') + return ret + + +class terraform(PluginFileInjector): + plugin_name = 'terraform_state' + namespace = 'cloud' + collection = 'terraform' + use_fqcn = True + + def inventory_as_dict(self, inventory_update, private_data_dir): + ret = super().inventory_as_dict(inventory_update, private_data_dir) + credential = inventory_update.get_cloud_credential() + config_cred = credential.get_input('configuration') + if config_cred: + handle, path = tempfile.mkstemp(dir=os.path.join(private_data_dir, 'env')) + with os.fdopen(handle, 'w') as f: + os.chmod(path, stat.S_IRUSR | stat.S_IWUSR) + f.write(config_cred) + ret['backend_config_files'] = to_container_path(path, private_data_dir) + return ret + + def build_plugin_private_data(self, inventory_update, private_data_dir): + credential = inventory_update.get_cloud_credential() + + private_data = {'credentials': {}} + gce_cred = credential.get_input('gce_credentials', default=None) + if gce_cred: + private_data['credentials'][credential] = gce_cred + return private_data + + def get_plugin_env(self, inventory_update, private_data_dir, private_data_files): + env = super(terraform, self).get_plugin_env(inventory_update, private_data_dir, private_data_files) + credential = inventory_update.get_cloud_credential() + cred_data = private_data_files['credentials'] + if credential in cred_data: + env['GOOGLE_BACKEND_CREDENTIALS'] = to_container_path(cred_data[credential], private_data_dir) + return env + + +class controller(PluginFileInjector): + plugin_name = 'tower' # TODO: relying on routing for now, update after EEs pick up revised collection + base_injector = 'template' + namespace = 'awx' + collection = 'awx' + downstream_namespace = 'ansible' + downstream_collection = 'controller' + + +class insights(PluginFileInjector): + plugin_name = 'insights' + base_injector = 'template' + namespace = 'redhatinsights' + collection = 'insights' + downstream_namespace = 'redhat' + downstream_collection = 'insights' + use_fqcn = True + + +class openshift_virtualization(PluginFileInjector): + plugin_name = 'kubevirt' + base_injector = 'template' + namespace = 'kubevirt' + collection = 'core' + downstream_namespace = 'redhat' + downstream_collection = 'openshift_virtualization' + use_fqcn = True + + +class constructed(PluginFileInjector): + plugin_name = 'constructed' + namespace = 'ansible' + collection = 'builtin' + + def build_env(self, *args, **kwargs): + env = super().build_env(*args, **kwargs) + # Enable script inventory plugin so we pick up the script files from source inventories + env['ANSIBLE_INVENTORY_ENABLED'] += ',script' + env['ANSIBLE_INVENTORY_ANY_UNPARSED_IS_FAILED'] = 'True' + return env diff --git a/awx/main/tests/functional/test_credential_plugins.py b/awx_plugins/tests/test_credential_plugins.py index 3ee29e9ce3..660fdf756b 100644 --- a/awx/main/tests/functional/test_credential_plugins.py +++ b/awx_plugins/tests/test_credential_plugins.py @@ -1,10 +1,10 @@ import pytest from unittest import mock -from awx.main.credential_plugins import hashivault +from awx_plugins.credentials import hashivault def test_imported_azure_cloud_sdk_vars(): - from awx.main.credential_plugins import azure_kv + from awx_plugins.credentials import azure_kv assert len(azure_kv.clouds) > 0 assert all([hasattr(c, 'name') for c in azure_kv.clouds]) @@ -129,13 +129,13 @@ class TestDelineaImports: """ def test_dsv_import(self): - from awx.main.credential_plugins.dsv import SecretsVault # noqa + from awx_plugins.credentials.dsv import SecretsVault # noqa # assert this module as opposed to older thycotic.secrets.vault assert SecretsVault.__module__ == 'delinea.secrets.vault' def test_tss_import(self): - from awx.main.credential_plugins.tss import DomainPasswordGrantAuthorizer, PasswordGrantAuthorizer, SecretServer, ServerSecret # noqa + from awx_plugins.credentials.tss import DomainPasswordGrantAuthorizer, PasswordGrantAuthorizer, SecretServer, ServerSecret # noqa for cls in (DomainPasswordGrantAuthorizer, PasswordGrantAuthorizer, SecretServer, ServerSecret): # assert this module as opposed to older thycotic.secrets.server @@ -13,13 +13,13 @@ include_package_data = True [options.entry_points] console_scripts = awx-manage = awx:manage -awx.credential_plugins = - conjur = awx.main.credential_plugins.conjur:conjur_plugin - hashivault_kv = awx.main.credential_plugins.hashivault:hashivault_kv_plugin - hashivault_ssh = awx.main.credential_plugins.hashivault:hashivault_ssh_plugin - azure_kv = awx.main.credential_plugins.azure_kv:azure_keyvault_plugin - aim = awx.main.credential_plugins.aim:aim_plugin - centrify_vault_kv = awx.main.credential_plugins.centrify_vault:centrify_plugin - thycotic_dsv = awx.main.credential_plugins.dsv:dsv_plugin - thycotic_tss = awx.main.credential_plugins.tss:tss_plugin - aws_secretsmanager_credential = awx.main.credential_plugins.aws_secretsmanager:aws_secretmanager_plugin +awx.plugins = + conjur = awx_plugins.credentials.conjur:conjur_plugin + hashivault_kv = awx_plugins.credentials.hashivault:hashivault_kv_plugin + hashivault_ssh = awx_plugins.credentials.hashivault:hashivault_ssh_plugin + azure_kv = awx_plugins.credentials.azure_kv:azure_keyvault_plugin + aim = awx_plugins.credentials.aim:aim_plugin + centrify_vault_kv = awx_plugins.credentials.centrify_vault:centrify_plugin + thycotic_dsv = awx_plugins.credentials.dsv:dsv_plugin + thycotic_tss = awx_plugins.credentials.tss:tss_plugin + aws_secretsmanager_credential = awx_plugins.credentials.aws_secretsmanager:aws_secretmanager_plugin |