diff --git a/awx/main/credential_plugins/aim.py b/src/awx_plugins/credentials/aim.py similarity index 98% rename from awx/main/credential_plugins/aim.py rename to src/awx_plugins/credentials/aim.py index 2476042b5f..dc06b0ea6f 100644 --- a/awx/main/credential_plugins/aim.py +++ b/src/awx_plugins/credentials/aim.py @@ -2,7 +2,7 @@ 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/src/awx_plugins/credentials/aws_secretsmanager.py similarity index 97% rename from awx/main/credential_plugins/aws_secretsmanager.py rename to src/awx_plugins/credentials/aws_secretsmanager.py index fa85f5e52a..335113b2a8 100644 --- a/awx/main/credential_plugins/aws_secretsmanager.py +++ b/src/awx_plugins/credentials/aws_secretsmanager.py @@ -2,7 +2,7 @@ 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/src/awx_plugins/credentials/azure_kv.py similarity index 97% rename from awx/main/credential_plugins/azure_kv.py rename to src/awx_plugins/credentials/azure_kv.py index 8910a0726d..7579dbee3d 100644 --- a/awx/main/credential_plugins/azure_kv.py +++ b/src/awx_plugins/credentials/azure_kv.py @@ -4,7 +4,7 @@ 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/src/awx_plugins/credentials/centrify_vault.py similarity index 98% rename from awx/main/credential_plugins/centrify_vault.py rename to src/awx_plugins/credentials/centrify_vault.py index 1e05625e71..b2d97a1db3 100644 --- a/awx/main/credential_plugins/centrify_vault.py +++ b/src/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/src/awx_plugins/credentials/conjur.py similarity index 98% rename from awx/main/credential_plugins/conjur.py rename to src/awx_plugins/credentials/conjur.py index e6984bed46..a7fd3a3a65 100644 --- a/awx/main/credential_plugins/conjur.py +++ b/src/awx_plugins/credentials/conjur.py @@ -2,7 +2,7 @@ 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/src/awx_plugins/credentials/dsv.py similarity index 97% rename from awx/main/credential_plugins/dsv.py rename to src/awx_plugins/credentials/dsv.py index 7dc74cab91..8296779bde 100644 --- a/awx/main/credential_plugins/dsv.py +++ b/src/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/src/awx_plugins/credentials/hashivault.py similarity index 99% rename from awx/main/credential_plugins/hashivault.py rename to src/awx_plugins/credentials/hashivault.py index f3dcd53b5d..81f7770f51 100644 --- a/awx/main/credential_plugins/hashivault.py +++ b/src/awx_plugins/credentials/hashivault.py @@ -7,7 +7,7 @@ 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/src/awx_plugins/credentials/injectors.py similarity index 100% rename from awx/main/models/credential/injectors.py rename to src/awx_plugins/credentials/injectors.py diff --git a/awx/main/credential_plugins/plugin.py b/src/awx_plugins/credentials/plugin.py similarity index 87% rename from awx/main/credential_plugins/plugin.py rename to src/awx_plugins/credentials/plugin.py index 7219231efc..b8aa294544 100644 --- a/awx/main/credential_plugins/plugin.py +++ b/src/awx_plugins/credentials/plugin.py @@ -8,6 +8,19 @@ 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/src/awx_plugins/credentials/plugins.py b/src/awx_plugins/credentials/plugins.py new file mode 100644 index 0000000000..debc5c7032 --- /dev/null +++ b/src/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/src/awx_plugins/credentials/tss.py similarity index 97% rename from awx/main/credential_plugins/tss.py rename to src/awx_plugins/credentials/tss.py index 682c6c8639..e295072233 100644 --- a/awx/main/credential_plugins/tss.py +++ b/src/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/src/awx_plugins/inventory/plugins.py b/src/awx_plugins/inventory/plugins.py new file mode 100644 index 0000000000..9a42173482 --- /dev/null +++ b/src/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/tests/test_credential_plugins.py similarity index 93% rename from awx/main/tests/functional/test_credential_plugins.py rename to tests/test_credential_plugins.py index 3ee29e9ce3..660fdf756b 100644 --- a/awx/main/tests/functional/test_credential_plugins.py +++ b/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