From 43ca4526b11bdaa8eb42642d2eb03a62028a64fb Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Tue, 19 Feb 2019 00:36:27 -0500 Subject: [PATCH] define native CredentialType inputs/injectors in code, not in the DB This has a few benefits: 1. It makes adding new fields to built-in CredentialTypes _much_ simpler. In the past, we've had to write a migration every time we want to modify an existing type (changing a label/help text, changing options like the recent become_method changes) or when adding a new field entirely 2. It paves the way for third party credential plugins support, where importable libraries will define their own source code-based schema --- ...v350_track_native_credentialtype_source.py | 27 + awx/main/migrations/_credentialtypes.py | 147 +-- awx/main/models/__init__.py | 2 +- awx/main/models/credential/__init__.py | 1083 +++++++++-------- awx/main/utils/common.py | 16 +- 5 files changed, 611 insertions(+), 664 deletions(-) create mode 100644 awx/main/migrations/0061_v350_track_native_credentialtype_source.py diff --git a/awx/main/migrations/0061_v350_track_native_credentialtype_source.py b/awx/main/migrations/0061_v350_track_native_credentialtype_source.py new file mode 100644 index 0000000000..5df5a32bc6 --- /dev/null +++ b/awx/main/migrations/0061_v350_track_native_credentialtype_source.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.16 on 2019-02-19 04:27 +from __future__ import unicode_literals + +from django.db import migrations, models + +from awx.main.models import CredentialType + + +def migrate_to_static_inputs(apps, schema_editor): + CredentialType.setup_tower_managed_defaults() + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0060_v350_update_schedule_uniqueness_constraint'), + ] + + operations = [ + migrations.AddField( + model_name='credentialtype', + name='namespace', + field=models.CharField(default=None, editable=False, max_length=1024, null=True), + ), + migrations.RunPython(migrate_to_static_inputs) + ] diff --git a/awx/main/migrations/_credentialtypes.py b/awx/main/migrations/_credentialtypes.py index 9d78cec49d..d4d1416be5 100644 --- a/awx/main/migrations/_credentialtypes.py +++ b/awx/main/migrations/_credentialtypes.py @@ -62,155 +62,40 @@ def _disassociate_non_insights_projects(apps, cred): def migrate_to_v2_credentials(apps, schema_editor): - CredentialType.setup_tower_managed_defaults() - deprecated_cred = _generate_deprecated_cred_types() - - # this monkey-patch is necessary to make the implicit role generation save - # signal use the correct Role model (the version active at this point in - # migration, not the one at HEAD) - orig_current_apps = utils.get_current_apps - try: - utils.get_current_apps = lambda: apps - for cred in apps.get_model('main', 'Credential').objects.all(): - job_templates = cred.jobtemplates.all() - jobs = cred.jobs.all() - data = {} - if getattr(cred, 'vault_password', None): - data['vault_password'] = cred.vault_password - if _is_insights_scm(apps, cred): - _disassociate_non_insights_projects(apps, cred) - credential_type = _get_insights_credential_type() - else: - credential_type = _populate_deprecated_cred_types(deprecated_cred, cred.kind) or CredentialType.from_v1_kind(cred.kind, data) - - defined_fields = credential_type.defined_fields - cred.credential_type = apps.get_model('main', 'CredentialType').objects.get(pk=credential_type.pk) - - for field in defined_fields: - if getattr(cred, field, None): - cred.inputs[field] = getattr(cred, field) - if cred.vault_password: - for jt in job_templates: - jt.credential = None - jt.vault_credential = cred - jt.save() - for job in jobs: - job.credential = None - job.vault_credential = cred - job.save() - if data.get('is_insights', False): - cred.kind = 'insights' - cred.save() - - # - # If the credential contains a vault password, create a new - # *additional* credential for the ssh details - # - if cred.vault_password: - # We need to make an ssh credential, too - ssh_type = CredentialType.from_v1_kind('ssh') - new_cred = apps.get_model('main', 'Credential').objects.get(pk=cred.pk) - new_cred.pk = None - new_cred.vault_password = '' - new_cred.credential_type = apps.get_model('main', 'CredentialType').objects.get(pk=ssh_type.pk) - if 'vault_password' in new_cred.inputs: - del new_cred.inputs['vault_password'] - - # unset these attributes so that new roles are properly created - # at save time - new_cred.read_role = None - new_cred.admin_role = None - new_cred.use_role = None - - if any([getattr(cred, field) for field in ssh_type.defined_fields]): - new_cred.save(force_insert=True) - - # copy rbac roles - for role_type in ('read_role', 'admin_role', 'use_role'): - for member in getattr(cred, role_type).members.all(): - getattr(new_cred, role_type).members.add(member) - for role in getattr(cred, role_type).parents.all(): - getattr(new_cred, role_type).parents.add(role) - - for jt in job_templates: - jt.credential = new_cred - jt.save() - for job in jobs: - job.credential = new_cred - job.save() - - # passwords must be decrypted and re-encrypted, because - # their encryption is based on the Credential's primary key - # (which has changed) - for field in ssh_type.defined_fields: - if field in ssh_type.secret_fields: - value = decrypt_field(cred, field) - if value: - setattr(new_cred, field, value) - new_cred.inputs[field] = encrypt_field(new_cred, field) - setattr(new_cred, field, '') - elif getattr(cred, field): - new_cred.inputs[field] = getattr(cred, field) - new_cred.save() - finally: - utils.get_current_apps = orig_current_apps + # TODO: remove once legacy/EOL'd Towers no longer support this upgrade path + pass def migrate_job_credentials(apps, schema_editor): - # this monkey-patch is necessary to make the implicit role generation save - # signal use the correct Role model (the version active at this point in - # migration, not the one at HEAD) - orig_current_apps = utils.get_current_apps - try: - utils.get_current_apps = lambda: apps - for type_ in ('Job', 'JobTemplate'): - for obj in apps.get_model('main', type_).objects.all(): - if obj.cloud_credential: - obj.extra_credentials.add(obj.cloud_credential) - if obj.network_credential: - obj.extra_credentials.add(obj.network_credential) - obj.save() - finally: - utils.get_current_apps = orig_current_apps + # TODO: remove once legacy/EOL'd Towers no longer support this upgrade path + pass def add_vault_id_field(apps, schema_editor): - vault_credtype = CredentialType.objects.get(kind='vault') - vault_credtype.inputs = CredentialType.defaults.get('vault')().inputs - vault_credtype.save() + # this is no longer necessary; schemas are defined in code + pass def remove_vault_id_field(apps, schema_editor): - vault_credtype = CredentialType.objects.get(kind='vault') - idx = 0 - for i, input in enumerate(vault_credtype.inputs['fields']): - if input['id'] == 'vault_id': - idx = i - break - vault_credtype.inputs['fields'].pop(idx) - vault_credtype.save() + # this is no longer necessary; schemas are defined in code + pass def create_rhv_tower_credtype(apps, schema_editor): - CredentialType.setup_tower_managed_defaults() + # this is no longer necessary; schemas are defined in code + pass def add_tower_verify_field(apps, schema_editor): - tower_credtype = CredentialType.objects.get( - kind='cloud', name='Ansible Tower', managed_by_tower=True - ) - tower_credtype.inputs = CredentialType.defaults.get('tower')().inputs - tower_credtype.save() + # this is no longer necessary; schemas are defined in code + pass def add_azure_cloud_environment_field(apps, schema_editor): - azure_rm_credtype = CredentialType.objects.get(kind='cloud', - name='Microsoft Azure Resource Manager') - azure_rm_credtype.inputs = CredentialType.defaults.get('azure_rm')().inputs - azure_rm_credtype.save() + # this is no longer necessary; schemas are defined in code + pass def remove_become_methods(apps, schema_editor): - become_credtype = CredentialType.objects.filter(kind='ssh', managed_by_tower=True).first() - become_credtype.inputs = CredentialType.defaults.get('ssh')().inputs - become_credtype.save() + # this is no longer necessary; schemas are defined in code + pass diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index b00d8d06ef..b1912cdf14 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -16,7 +16,7 @@ from awx.main.models.organization import ( # noqa Organization, Profile, Team, UserSessionMembership ) from awx.main.models.credential import ( # noqa - Credential, CredentialType, V1Credential, build_safe_env + Credential, CredentialType, ManagedCredentialType, V1Credential, build_safe_env ) from awx.main.models.projects import Project, ProjectUpdate # noqa from awx.main.models.inventory import ( # noqa diff --git a/awx/main/models/credential/__init__.py b/awx/main/models/credential/__init__.py index 0df7c32d92..10c5050440 100644 --- a/awx/main/models/credential/__init__.py +++ b/awx/main/models/credential/__init__.py @@ -1,12 +1,13 @@ # Copyright (c) 2015 Ansible, Inc. # All Rights Reserved. -from collections import OrderedDict import functools +import inspect import logging import os import re import stat import tempfile +from types import SimpleNamespace # Jinja2 from jinja2 import Template @@ -22,7 +23,7 @@ from awx.api.versioning import reverse from awx.main.fields import (ImplicitRoleField, CredentialInputField, CredentialTypeInputField, CredentialTypeInjectorField) -from awx.main.utils import decrypt_field +from awx.main.utils import decrypt_field, classproperty from awx.main.utils.safe_yaml import safe_dump from awx.main.validators import validate_ssh_private_key from awx.main.models.base import CommonModelNameNotUnique, PasswordFieldsModel @@ -465,8 +466,6 @@ class CredentialType(CommonModelNameNotUnique): output injectors (i.e., an environment variable that uses the API key). ''' - defaults = OrderedDict() - class Meta: app_label = 'main' ordering = ('kind', 'name') @@ -489,6 +488,12 @@ class CredentialType(CommonModelNameNotUnique): default=False, editable=False ) + namespace = models.CharField( + max_length=1024, + null=True, + default=None, + editable=False + ) inputs = CredentialTypeInputField( blank=True, default={}, @@ -504,6 +509,15 @@ class CredentialType(CommonModelNameNotUnique): 'Ansible Tower documentation for example syntax.') ) + @classmethod + def from_db(cls, db, field_names, values): + instance = super(CredentialType, cls).from_db(db, field_names, values) + if instance.managed_by_tower and instance.namespace: + native = ManagedCredentialType.registry[instance.namespace] + instance.inputs = native.inputs + instance.injectors = native.injectors + return instance + def get_absolute_url(self, request=None): # Page does not exist in API v1 if request.version == 'v1': @@ -539,23 +553,29 @@ class CredentialType(CommonModelNameNotUnique): return field['choices'][0] return {'string': '', 'boolean': False}[field['type']] - @classmethod - def default(cls, f): - func = functools.partial(f, cls) - cls.defaults[f.__name__] = func - return func + @classproperty + def defaults(cls): + return dict( + (k, functools.partial(v.create)) + for k, v in ManagedCredentialType.registry.items() + ) @classmethod - def setup_tower_managed_defaults(cls, persisted=True): - for default in cls.defaults.values(): - default_ = default() - if persisted: - if CredentialType.objects.filter(name=default_.name, kind=default_.kind).count(): - continue - logger.debug(_( - "adding %s credential type" % default_.name - )) - default_.save() + def setup_tower_managed_defaults(cls): + for default in ManagedCredentialType.registry.values(): + existing = CredentialType.objects.filter(name=default.name, kind=default.kind).first() + if existing is not None: + existing.namespace = default.namespace + existing.inputs = {} + existing.injectors = {} + existing.save() + continue + logger.debug(_( + "adding %s credential type" % default.name + )) + created = default.create() + created.inputs = created.injectors = {} + created.save() @classmethod def from_v1_kind(cls, kind, data={}): @@ -701,527 +721,528 @@ class CredentialType(CommonModelNameNotUnique): safe_args.extend(['-e', '@%s' % path]) -@CredentialType.default -def ssh(cls): - return cls( - kind='ssh', - name=ugettext_noop('Machine'), - managed_by_tower=True, - inputs={ - 'fields': [{ - 'id': 'username', - 'label': ugettext_noop('Username'), - 'type': 'string' - }, { - 'id': 'password', - 'label': ugettext_noop('Password'), - 'type': 'string', - 'secret': True, - 'ask_at_runtime': True - }, { - 'id': 'ssh_key_data', - 'label': ugettext_noop('SSH Private Key'), - 'type': 'string', - 'format': 'ssh_private_key', - 'secret': True, - 'multiline': True - }, { - 'id': 'ssh_key_unlock', - 'label': ugettext_noop('Private Key Passphrase'), - 'type': 'string', - 'secret': True, - 'ask_at_runtime': True - }, { - 'id': 'become_method', - 'label': ugettext_noop('Privilege Escalation Method'), - 'type': 'string', - 'help_text': ugettext_noop('Specify a method for "become" operations. This is ' - 'equivalent to specifying the --become-method ' - 'Ansible parameter.') - }, { - 'id': 'become_username', - 'label': ugettext_noop('Privilege Escalation Username'), - 'type': 'string', - }, { - 'id': 'become_password', - 'label': ugettext_noop('Privilege Escalation Password'), - 'type': 'string', - 'secret': True, - 'ask_at_runtime': True - }], - 'dependencies': { - 'ssh_key_unlock': ['ssh_key_data'], - } +class ManagedCredentialType(SimpleNamespace): + + registry = {} + + def __init__(self, namespace, **kwargs): + for k in ('inputs', 'injectors'): + if k not in kwargs: + kwargs[k] = {} + super(ManagedCredentialType, self).__init__(namespace=namespace, **kwargs) + if namespace in ManagedCredentialType.registry: + raise ValueError( + 'a ManagedCredentialType with namespace={} is already defined in {}'.format( + namespace, + inspect.getsourcefile(ManagedCredentialType.registry[namespace].__class__) + ) + ) + ManagedCredentialType.registry[namespace] = self + + def create(self): + return CredentialType( + namespace=self.namespace, + kind=self.kind, + name=self.name, + managed_by_tower=True, + inputs=self.inputs, + injectors=self.injectors, + ) + + +ManagedCredentialType( + namespace='ssh', + kind='ssh', + name=ugettext_noop('Machine'), + inputs={ + 'fields': [{ + 'id': 'username', + 'label': ugettext_noop('Username'), + 'type': 'string' + }, { + 'id': 'password', + 'label': ugettext_noop('Password'), + 'type': 'string', + 'secret': True, + 'ask_at_runtime': True + }, { + 'id': 'ssh_key_data', + 'label': ugettext_noop('SSH Private Key'), + 'type': 'string', + 'format': 'ssh_private_key', + 'secret': True, + 'multiline': True + }, { + 'id': 'ssh_key_unlock', + 'label': ugettext_noop('Private Key Passphrase'), + 'type': 'string', + 'secret': True, + 'ask_at_runtime': True + }, { + 'id': 'become_method', + 'label': ugettext_noop('Privilege Escalation Method'), + 'type': 'string', + 'help_text': ugettext_noop('Specify a method for "become" operations. This is ' + 'equivalent to specifying the --become-method ' + 'Ansible parameter.') + }, { + 'id': 'become_username', + 'label': ugettext_noop('Privilege Escalation Username'), + 'type': 'string', + }, { + 'id': 'become_password', + 'label': ugettext_noop('Privilege Escalation Password'), + 'type': 'string', + 'secret': True, + 'ask_at_runtime': True + }], + 'dependencies': { + 'ssh_key_unlock': ['ssh_key_data'], } - ) + } +) - -@CredentialType.default -def scm(cls): - return cls( - kind='scm', - name=ugettext_noop('Source Control'), - managed_by_tower=True, - inputs={ - 'fields': [{ - 'id': 'username', - 'label': ugettext_noop('Username'), - 'type': 'string' - }, { - 'id': 'password', - 'label': ugettext_noop('Password'), - 'type': 'string', - 'secret': True - }, { - 'id': 'ssh_key_data', - 'label': ugettext_noop('SCM Private Key'), - 'type': 'string', - 'format': 'ssh_private_key', - 'secret': True, - 'multiline': True - }, { - 'id': 'ssh_key_unlock', - 'label': ugettext_noop('Private Key Passphrase'), - 'type': 'string', - 'secret': True - }], - 'dependencies': { - 'ssh_key_unlock': ['ssh_key_data'], - } +ManagedCredentialType( + namespace='scm', + kind='scm', + name=ugettext_noop('Source Control'), + managed_by_tower=True, + inputs={ + 'fields': [{ + 'id': 'username', + 'label': ugettext_noop('Username'), + 'type': 'string' + }, { + 'id': 'password', + 'label': ugettext_noop('Password'), + 'type': 'string', + 'secret': True + }, { + 'id': 'ssh_key_data', + 'label': ugettext_noop('SCM Private Key'), + 'type': 'string', + 'format': 'ssh_private_key', + 'secret': True, + 'multiline': True + }, { + 'id': 'ssh_key_unlock', + 'label': ugettext_noop('Private Key Passphrase'), + 'type': 'string', + 'secret': True + }], + 'dependencies': { + 'ssh_key_unlock': ['ssh_key_data'], } - ) + } +) +ManagedCredentialType( + namespace='vault', + kind='vault', + name=ugettext_noop('Vault'), + managed_by_tower=True, + inputs={ + 'fields': [{ + 'id': 'vault_password', + 'label': ugettext_noop('Vault Password'), + 'type': 'string', + 'secret': True, + 'ask_at_runtime': True + }, { + 'id': 'vault_id', + 'label': ugettext_noop('Vault Identifier'), + 'type': 'string', + 'format': 'vault_id', + 'help_text': ugettext_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'], + } +) -@CredentialType.default -def vault(cls): - return cls( - kind='vault', - name=ugettext_noop('Vault'), - managed_by_tower=True, - inputs={ - 'fields': [{ - 'id': 'vault_password', - 'label': ugettext_noop('Vault Password'), - 'type': 'string', - 'secret': True, - 'ask_at_runtime': True - }, { - 'id': 'vault_id', - 'label': ugettext_noop('Vault Identifier'), - 'type': 'string', - 'format': 'vault_id', - 'help_text': ugettext_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'], - } - ) - - -@CredentialType.default -def net(cls): - return cls( - kind='net', - name=ugettext_noop('Network'), - managed_by_tower=True, - inputs={ - 'fields': [{ - 'id': 'username', - 'label': ugettext_noop('Username'), - 'type': 'string' - }, { - 'id': 'password', - 'label': ugettext_noop('Password'), - 'type': 'string', - 'secret': True, - }, { - 'id': 'ssh_key_data', - 'label': ugettext_noop('SSH Private Key'), - 'type': 'string', - 'format': 'ssh_private_key', - 'secret': True, - 'multiline': True - }, { - 'id': 'ssh_key_unlock', - 'label': ugettext_noop('Private Key Passphrase'), - 'type': 'string', - 'secret': True, - }, { - 'id': 'authorize', - 'label': ugettext_noop('Authorize'), - 'type': 'boolean', - }, { - 'id': 'authorize_password', - 'label': ugettext_noop('Authorize Password'), - 'type': 'string', - 'secret': True, - }], - 'dependencies': { - 'ssh_key_unlock': ['ssh_key_data'], - 'authorize_password': ['authorize'], - }, - 'required': ['username'], - } - ) - - -@CredentialType.default -def aws(cls): - return cls( - kind='cloud', - name=ugettext_noop('Amazon Web Services'), - managed_by_tower=True, - inputs={ - 'fields': [{ - 'id': 'username', - 'label': ugettext_noop('Access Key'), - 'type': 'string' - }, { - 'id': 'password', - 'label': ugettext_noop('Secret Key'), - 'type': 'string', - 'secret': True, - }, { - 'id': 'security_token', - 'label': ugettext_noop('STS Token'), - 'type': 'string', - 'secret': True, - 'help_text': ugettext_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'] - } - ) - - -@CredentialType.default -def openstack(cls): - return cls( - kind='cloud', - name=ugettext_noop('OpenStack'), - managed_by_tower=True, - inputs={ - 'fields': [{ - 'id': 'username', - 'label': ugettext_noop('Username'), - 'type': 'string' - }, { - 'id': 'password', - 'label': ugettext_noop('Password (API Key)'), - 'type': 'string', - 'secret': True, - }, { - 'id': 'host', - 'label': ugettext_noop('Host (Authentication URL)'), - 'type': 'string', - 'help_text': ugettext_noop('The host to authenticate with. For example, ' - 'https://openstack.business.com/v2.0/') - }, { - 'id': 'project', - 'label': ugettext_noop('Project (Tenant Name)'), - 'type': 'string', - }, { - 'id': 'domain', - 'label': ugettext_noop('Domain Name'), - 'type': 'string', - 'help_text': ugettext_noop('OpenStack domains define administrative boundaries. ' - 'It is only needed for Keystone v3 authentication ' - 'URLs. Refer to Ansible Tower documentation for ' - 'common scenarios.') - }], - 'required': ['username', 'password', 'host', 'project'] - } - ) - - -@CredentialType.default -def vmware(cls): - return cls( - kind='cloud', - name=ugettext_noop('VMware vCenter'), - managed_by_tower=True, - inputs={ - 'fields': [{ - 'id': 'host', - 'label': ugettext_noop('VCenter Host'), - 'type': 'string', - 'help_text': ugettext_noop('Enter the hostname or IP address that corresponds ' - 'to your VMware vCenter.') - }, { - 'id': 'username', - 'label': ugettext_noop('Username'), - 'type': 'string' - }, { - 'id': 'password', - 'label': ugettext_noop('Password'), - 'type': 'string', - 'secret': True, - }], - 'required': ['host', 'username', 'password'] - } - ) - - -@CredentialType.default -def satellite6(cls): - return cls( - kind='cloud', - name=ugettext_noop('Red Hat Satellite 6'), - managed_by_tower=True, - inputs={ - 'fields': [{ - 'id': 'host', - 'label': ugettext_noop('Satellite 6 URL'), - 'type': 'string', - 'help_text': ugettext_noop('Enter the URL that corresponds to your Red Hat ' - 'Satellite 6 server. For example, https://satellite.example.org') - }, { - 'id': 'username', - 'label': ugettext_noop('Username'), - 'type': 'string' - }, { - 'id': 'password', - 'label': ugettext_noop('Password'), - 'type': 'string', - 'secret': True, - }], - 'required': ['host', 'username', 'password'], - } - ) - - -@CredentialType.default -def cloudforms(cls): - return cls( - kind='cloud', - name=ugettext_noop('Red Hat CloudForms'), - managed_by_tower=True, - inputs={ - 'fields': [{ - 'id': 'host', - 'label': ugettext_noop('CloudForms URL'), - 'type': 'string', - 'help_text': ugettext_noop('Enter the URL for the virtual machine that ' - 'corresponds to your CloudForms instance. ' - 'For example, https://cloudforms.example.org') - }, { - 'id': 'username', - 'label': ugettext_noop('Username'), - 'type': 'string' - }, { - 'id': 'password', - 'label': ugettext_noop('Password'), - 'type': 'string', - 'secret': True, - }], - 'required': ['host', 'username', 'password'], - } - ) - - -@CredentialType.default -def gce(cls): - return cls( - kind='cloud', - name=ugettext_noop('Google Compute Engine'), - managed_by_tower=True, - inputs={ - 'fields': [{ - 'id': 'username', - 'label': ugettext_noop('Service Account Email Address'), - 'type': 'string', - 'help_text': ugettext_noop('The email address assigned to the Google Compute ' - 'Engine service account.') - }, { - 'id': 'project', - 'label': 'Project', - 'type': 'string', - 'help_text': ugettext_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': ugettext_noop('RSA Private Key'), - 'type': 'string', - 'format': 'ssh_private_key', - 'secret': True, - 'multiline': True, - 'help_text': ugettext_noop('Paste the contents of the PEM file associated ' - 'with the service account email.') - }], - 'required': ['username', 'ssh_key_data'], - } - ) - - -@CredentialType.default -def azure_rm(cls): - return cls( - kind='cloud', - name=ugettext_noop('Microsoft Azure Resource Manager'), - managed_by_tower=True, - inputs={ - 'fields': [{ - 'id': 'subscription', - 'label': ugettext_noop('Subscription ID'), - 'type': 'string', - 'help_text': ugettext_noop('Subscription ID is an Azure construct, which is ' - 'mapped to a username.') - }, { - 'id': 'username', - 'label': ugettext_noop('Username'), - 'type': 'string' - }, { - 'id': 'password', - 'label': ugettext_noop('Password'), - 'type': 'string', - 'secret': True, - }, { - 'id': 'client', - 'label': ugettext_noop('Client ID'), - 'type': 'string' - }, { - 'id': 'secret', - 'label': ugettext_noop('Client Secret'), - 'type': 'string', - 'secret': True, - }, { - 'id': 'tenant', - 'label': ugettext_noop('Tenant ID'), - 'type': 'string' - }, { - 'id': 'cloud_environment', - 'label': ugettext_noop('Azure Cloud Environment'), - 'type': 'string', - 'help_text': ugettext_noop('Environment variable AZURE_CLOUD_ENVIRONMENT when' - ' using Azure GovCloud or Azure stack.') - }], - 'required': ['subscription'], - } - ) - - -@CredentialType.default -def insights(cls): - return cls( - kind='insights', - name=ugettext_noop('Insights'), - managed_by_tower=True, - inputs={ - 'fields': [{ - 'id': 'username', - 'label': ugettext_noop('Username'), - 'type': 'string' - }, { - 'id': 'password', - 'label': ugettext_noop('Password'), - 'type': 'string', - 'secret': True - }], - 'required': ['username', 'password'], +ManagedCredentialType( + namespace='net', + kind='net', + name=ugettext_noop('Network'), + managed_by_tower=True, + inputs={ + 'fields': [{ + 'id': 'username', + 'label': ugettext_noop('Username'), + 'type': 'string' + }, { + 'id': 'password', + 'label': ugettext_noop('Password'), + 'type': 'string', + 'secret': True, + }, { + 'id': 'ssh_key_data', + 'label': ugettext_noop('SSH Private Key'), + 'type': 'string', + 'format': 'ssh_private_key', + 'secret': True, + 'multiline': True + }, { + 'id': 'ssh_key_unlock', + 'label': ugettext_noop('Private Key Passphrase'), + 'type': 'string', + 'secret': True, + }, { + 'id': 'authorize', + 'label': ugettext_noop('Authorize'), + 'type': 'boolean', + }, { + 'id': 'authorize_password', + 'label': ugettext_noop('Authorize Password'), + 'type': 'string', + 'secret': True, + }], + 'dependencies': { + 'ssh_key_unlock': ['ssh_key_data'], + 'authorize_password': ['authorize'], }, - injectors={ - 'extra_vars': { - "scm_username": "{{username}}", - "scm_password": "{{password}}", - }, - }, - ) + 'required': ['username'], + } +) +ManagedCredentialType( + namespace='aws', + kind='cloud', + name=ugettext_noop('Amazon Web Services'), + managed_by_tower=True, + inputs={ + 'fields': [{ + 'id': 'username', + 'label': ugettext_noop('Access Key'), + 'type': 'string' + }, { + 'id': 'password', + 'label': ugettext_noop('Secret Key'), + 'type': 'string', + 'secret': True, + }, { + 'id': 'security_token', + 'label': ugettext_noop('STS Token'), + 'type': 'string', + 'secret': True, + 'help_text': ugettext_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'] + } +) -@CredentialType.default -def rhv(cls): - return cls( - kind='cloud', - name=ugettext_noop('Red Hat Virtualization'), - managed_by_tower=True, - inputs={ - 'fields': [{ - 'id': 'host', - 'label': ugettext_noop('Host (Authentication URL)'), - 'type': 'string', - 'help_text': ugettext_noop('The host to authenticate with.') - }, { - 'id': 'username', - 'label': ugettext_noop('Username'), - 'type': 'string' - }, { - 'id': 'password', - 'label': ugettext_noop('Password'), - 'type': 'string', - 'secret': True, - }, { - 'id': 'ca_file', - 'label': ugettext_noop('CA File'), - 'type': 'string', - 'help_text': ugettext_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='openstack', + kind='cloud', + name=ugettext_noop('OpenStack'), + managed_by_tower=True, + inputs={ + 'fields': [{ + 'id': 'username', + 'label': ugettext_noop('Username'), + 'type': 'string' + }, { + 'id': 'password', + 'label': ugettext_noop('Password (API Key)'), + 'type': 'string', + 'secret': True, + }, { + 'id': 'host', + 'label': ugettext_noop('Host (Authentication URL)'), + 'type': 'string', + 'help_text': ugettext_noop('The host to authenticate with. For example, ' + 'https://openstack.business.com/v2.0/') + }, { + 'id': 'project', + 'label': ugettext_noop('Project (Tenant Name)'), + 'type': 'string', + }, { + 'id': 'domain', + 'label': ugettext_noop('Domain Name'), + 'type': 'string', + 'help_text': ugettext_noop('OpenStack domains define administrative boundaries. ' + 'It is only needed for Keystone v3 authentication ' + 'URLs. Refer to Ansible Tower documentation for ' + 'common scenarios.') + }], + 'required': ['username', 'password', 'host', 'project'] + } +) +ManagedCredentialType( + namespace='vmware', + kind='cloud', + name=ugettext_noop('VMware vCenter'), + managed_by_tower=True, + inputs={ + 'fields': [{ + 'id': 'host', + 'label': ugettext_noop('VCenter Host'), + 'type': 'string', + 'help_text': ugettext_noop('Enter the hostname or IP address that corresponds ' + 'to your VMware vCenter.') + }, { + 'id': 'username', + 'label': ugettext_noop('Username'), + 'type': 'string' + }, { + 'id': 'password', + 'label': ugettext_noop('Password'), + 'type': 'string', + 'secret': True, + }], + 'required': ['host', 'username', 'password'] + } +) -@CredentialType.default -def tower(cls): - return cls( - kind='cloud', - name=ugettext_noop('Ansible Tower'), - managed_by_tower=True, - inputs={ - 'fields': [{ - 'id': 'host', - 'label': ugettext_noop('Ansible Tower Hostname'), - 'type': 'string', - 'help_text': ugettext_noop('The Ansible Tower base URL to authenticate with.') - }, { - 'id': 'username', - 'label': ugettext_noop('Username'), - 'type': 'string' - }, { - 'id': 'password', - 'label': ugettext_noop('Password'), - 'type': 'string', - 'secret': True, - }, { - 'id': 'verify_ssl', - 'label': ugettext_noop('Verify SSL'), - 'type': 'boolean', - 'secret': False - }], - 'required': ['host', 'username', 'password'], +ManagedCredentialType( + namespace='satellite6', + kind='cloud', + name=ugettext_noop('Red Hat Satellite 6'), + managed_by_tower=True, + inputs={ + 'fields': [{ + 'id': 'host', + 'label': ugettext_noop('Satellite 6 URL'), + 'type': 'string', + 'help_text': ugettext_noop('Enter the URL that corresponds to your Red Hat ' + 'Satellite 6 server. For example, https://satellite.example.org') + }, { + 'id': 'username', + 'label': ugettext_noop('Username'), + 'type': 'string' + }, { + 'id': 'password', + 'label': ugettext_noop('Password'), + 'type': 'string', + 'secret': True, + }], + 'required': ['host', 'username', 'password'], + } +) + +ManagedCredentialType( + namespace='cloudforms', + kind='cloud', + name=ugettext_noop('Red Hat CloudForms'), + managed_by_tower=True, + inputs={ + 'fields': [{ + 'id': 'host', + 'label': ugettext_noop('CloudForms URL'), + 'type': 'string', + 'help_text': ugettext_noop('Enter the URL for the virtual machine that ' + 'corresponds to your CloudForms instance. ' + 'For example, https://cloudforms.example.org') + }, { + 'id': 'username', + 'label': ugettext_noop('Username'), + 'type': 'string' + }, { + 'id': 'password', + 'label': ugettext_noop('Password'), + 'type': 'string', + 'secret': True, + }], + 'required': ['host', 'username', 'password'], + } +) + +ManagedCredentialType( + namespace='gce', + kind='cloud', + name=ugettext_noop('Google Compute Engine'), + managed_by_tower=True, + inputs={ + 'fields': [{ + 'id': 'username', + 'label': ugettext_noop('Service Account Email Address'), + 'type': 'string', + 'help_text': ugettext_noop('The email address assigned to the Google Compute ' + 'Engine service account.') + }, { + 'id': 'project', + 'label': 'Project', + 'type': 'string', + 'help_text': ugettext_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': ugettext_noop('RSA Private Key'), + 'type': 'string', + 'format': 'ssh_private_key', + 'secret': True, + 'multiline': True, + 'help_text': ugettext_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=ugettext_noop('Microsoft Azure Resource Manager'), + managed_by_tower=True, + inputs={ + 'fields': [{ + 'id': 'subscription', + 'label': ugettext_noop('Subscription ID'), + 'type': 'string', + 'help_text': ugettext_noop('Subscription ID is an Azure construct, which is ' + 'mapped to a username.') + }, { + 'id': 'username', + 'label': ugettext_noop('Username'), + 'type': 'string' + }, { + 'id': 'password', + 'label': ugettext_noop('Password'), + 'type': 'string', + 'secret': True, + }, { + 'id': 'client', + 'label': ugettext_noop('Client ID'), + 'type': 'string' + }, { + 'id': 'secret', + 'label': ugettext_noop('Client Secret'), + 'type': 'string', + 'secret': True, + }, { + 'id': 'tenant', + 'label': ugettext_noop('Tenant ID'), + 'type': 'string' + }, { + 'id': 'cloud_environment', + 'label': ugettext_noop('Azure Cloud Environment'), + 'type': 'string', + 'help_text': ugettext_noop('Environment variable AZURE_CLOUD_ENVIRONMENT when' + ' using Azure GovCloud or Azure stack.') + }], + 'required': ['subscription'], + } +) + +ManagedCredentialType( + namespace='insights', + kind='insights', + name=ugettext_noop('Insights'), + managed_by_tower=True, + inputs={ + 'fields': [{ + 'id': 'username', + 'label': ugettext_noop('Username'), + 'type': 'string' + }, { + 'id': 'password', + 'label': ugettext_noop('Password'), + 'type': 'string', + 'secret': True + }], + 'required': ['username', 'password'], + }, + injectors={ + 'extra_vars': { + "scm_username": "{{username}}", + "scm_password": "{{password}}", }, - injectors={ - 'env': { - 'TOWER_HOST': '{{host}}', - 'TOWER_USERNAME': '{{username}}', - 'TOWER_PASSWORD': '{{password}}', - 'TOWER_VERIFY_SSL': '{{verify_ssl}}' - } + }, +) + +ManagedCredentialType( + namespace='rhv', + kind='cloud', + name=ugettext_noop('Red Hat Virtualization'), + managed_by_tower=True, + inputs={ + 'fields': [{ + 'id': 'host', + 'label': ugettext_noop('Host (Authentication URL)'), + 'type': 'string', + 'help_text': ugettext_noop('The host to authenticate with.') + }, { + 'id': 'username', + 'label': ugettext_noop('Username'), + 'type': 'string' + }, { + 'id': 'password', + 'label': ugettext_noop('Password'), + 'type': 'string', + 'secret': True, + }, { + 'id': 'ca_file', + 'label': ugettext_noop('CA File'), + 'type': 'string', + 'help_text': ugettext_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='tower', + kind='cloud', + name=ugettext_noop('Ansible Tower'), + managed_by_tower=True, + inputs={ + 'fields': [{ + 'id': 'host', + 'label': ugettext_noop('Ansible Tower Hostname'), + 'type': 'string', + 'help_text': ugettext_noop('The Ansible Tower base URL to authenticate with.') + }, { + 'id': 'username', + 'label': ugettext_noop('Username'), + 'type': 'string' + }, { + 'id': 'password', + 'label': ugettext_noop('Password'), + 'type': 'string', + 'secret': True, + }, { + 'id': 'verify_ssl', + 'label': ugettext_noop('Verify SSL'), + 'type': 'boolean', + 'secret': False + }], + 'required': ['host', 'username', 'password'], + }, + injectors={ + 'env': { + 'TOWER_HOST': '{{host}}', + 'TOWER_USERNAME': '{{username}}', + 'TOWER_PASSWORD': '{{password}}', + 'TOWER_VERIFY_SSL': '{{verify_ssl}}' + } + }, +) diff --git a/awx/main/utils/common.py b/awx/main/utils/common.py index 7fb16310c4..60306ea8e6 100644 --- a/awx/main/utils/common.py +++ b/awx/main/utils/common.py @@ -47,7 +47,7 @@ __all__ = ['get_object_or_400', 'camelcase_to_underscore', 'memoize', 'memoize_d 'wrap_args_with_proot', 'build_proot_temp_dir', 'check_proot_installed', 'model_to_dict', 'model_instance_diff', 'timestamp_apiformat', 'parse_yaml_or_json', 'RequireDebugTrueOrTest', 'has_model_field_prefetched', 'set_environ', 'IllegalArgumentError', 'get_custom_venv_choices', 'get_external_account', - 'task_manager_bulk_reschedule', 'schedule_task_manager'] + 'task_manager_bulk_reschedule', 'schedule_task_manager', 'classproperty'] def get_object_or_400(klass, *args, **kwargs): @@ -1113,3 +1113,17 @@ def get_external_account(user): getattr(settings, 'TACACSPLUS_HOST', None)) and user.enterprise_auth.all(): account_type = "enterprise" return account_type + + +class classproperty: + + def __init__(self, fget=None, fset=None, fdel=None, doc=None): + self.fget = fget + self.fset = fset + self.fdel = fdel + if doc is None and fget is not None: + doc = fget.__doc__ + self.__doc__ = doc + + def __get__(self, instance, ownerclass): + return self.fget(ownerclass)