mirror of
https://github.com/ansible/awx.git
synced 2026-05-23 00:37:37 -02:30
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
This commit is contained in:
@@ -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)
|
||||
]
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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():
|
||||
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
|
||||
"adding %s credential type" % default.name
|
||||
))
|
||||
default_.save()
|
||||
created = default.create()
|
||||
created.inputs = created.injectors = {}
|
||||
created.save()
|
||||
|
||||
@classmethod
|
||||
def from_v1_kind(cls, kind, data={}):
|
||||
@@ -701,12 +721,39 @@ class CredentialType(CommonModelNameNotUnique):
|
||||
safe_args.extend(['-e', '@%s' % path])
|
||||
|
||||
|
||||
@CredentialType.default
|
||||
def ssh(cls):
|
||||
return cls(
|
||||
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'),
|
||||
managed_by_tower=True,
|
||||
inputs={
|
||||
'fields': [{
|
||||
'id': 'username',
|
||||
@@ -753,12 +800,10 @@ def ssh(cls):
|
||||
'ssh_key_unlock': ['ssh_key_data'],
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@CredentialType.default
|
||||
def scm(cls):
|
||||
return cls(
|
||||
ManagedCredentialType(
|
||||
namespace='scm',
|
||||
kind='scm',
|
||||
name=ugettext_noop('Source Control'),
|
||||
managed_by_tower=True,
|
||||
@@ -789,12 +834,10 @@ def scm(cls):
|
||||
'ssh_key_unlock': ['ssh_key_data'],
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@CredentialType.default
|
||||
def vault(cls):
|
||||
return cls(
|
||||
ManagedCredentialType(
|
||||
namespace='vault',
|
||||
kind='vault',
|
||||
name=ugettext_noop('Vault'),
|
||||
managed_by_tower=True,
|
||||
@@ -818,12 +861,10 @@ def vault(cls):
|
||||
}],
|
||||
'required': ['vault_password'],
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@CredentialType.default
|
||||
def net(cls):
|
||||
return cls(
|
||||
ManagedCredentialType(
|
||||
namespace='net',
|
||||
kind='net',
|
||||
name=ugettext_noop('Network'),
|
||||
managed_by_tower=True,
|
||||
@@ -865,12 +906,10 @@ def net(cls):
|
||||
},
|
||||
'required': ['username'],
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@CredentialType.default
|
||||
def aws(cls):
|
||||
return cls(
|
||||
ManagedCredentialType(
|
||||
namespace='aws',
|
||||
kind='cloud',
|
||||
name=ugettext_noop('Amazon Web Services'),
|
||||
managed_by_tower=True,
|
||||
@@ -896,12 +935,10 @@ def aws(cls):
|
||||
}],
|
||||
'required': ['username', 'password']
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@CredentialType.default
|
||||
def openstack(cls):
|
||||
return cls(
|
||||
ManagedCredentialType(
|
||||
namespace='openstack',
|
||||
kind='cloud',
|
||||
name=ugettext_noop('OpenStack'),
|
||||
managed_by_tower=True,
|
||||
@@ -936,12 +973,10 @@ def openstack(cls):
|
||||
}],
|
||||
'required': ['username', 'password', 'host', 'project']
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@CredentialType.default
|
||||
def vmware(cls):
|
||||
return cls(
|
||||
ManagedCredentialType(
|
||||
namespace='vmware',
|
||||
kind='cloud',
|
||||
name=ugettext_noop('VMware vCenter'),
|
||||
managed_by_tower=True,
|
||||
@@ -964,12 +999,10 @@ def vmware(cls):
|
||||
}],
|
||||
'required': ['host', 'username', 'password']
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@CredentialType.default
|
||||
def satellite6(cls):
|
||||
return cls(
|
||||
ManagedCredentialType(
|
||||
namespace='satellite6',
|
||||
kind='cloud',
|
||||
name=ugettext_noop('Red Hat Satellite 6'),
|
||||
managed_by_tower=True,
|
||||
@@ -992,12 +1025,10 @@ def satellite6(cls):
|
||||
}],
|
||||
'required': ['host', 'username', 'password'],
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@CredentialType.default
|
||||
def cloudforms(cls):
|
||||
return cls(
|
||||
ManagedCredentialType(
|
||||
namespace='cloudforms',
|
||||
kind='cloud',
|
||||
name=ugettext_noop('Red Hat CloudForms'),
|
||||
managed_by_tower=True,
|
||||
@@ -1021,12 +1052,10 @@ def cloudforms(cls):
|
||||
}],
|
||||
'required': ['host', 'username', 'password'],
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@CredentialType.default
|
||||
def gce(cls):
|
||||
return cls(
|
||||
ManagedCredentialType(
|
||||
namespace='gce',
|
||||
kind='cloud',
|
||||
name=ugettext_noop('Google Compute Engine'),
|
||||
managed_by_tower=True,
|
||||
@@ -1057,12 +1086,10 @@ def gce(cls):
|
||||
}],
|
||||
'required': ['username', 'ssh_key_data'],
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@CredentialType.default
|
||||
def azure_rm(cls):
|
||||
return cls(
|
||||
ManagedCredentialType(
|
||||
namespace='azure_rm',
|
||||
kind='cloud',
|
||||
name=ugettext_noop('Microsoft Azure Resource Manager'),
|
||||
managed_by_tower=True,
|
||||
@@ -1104,12 +1131,10 @@ def azure_rm(cls):
|
||||
}],
|
||||
'required': ['subscription'],
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@CredentialType.default
|
||||
def insights(cls):
|
||||
return cls(
|
||||
ManagedCredentialType(
|
||||
namespace='insights',
|
||||
kind='insights',
|
||||
name=ugettext_noop('Insights'),
|
||||
managed_by_tower=True,
|
||||
@@ -1132,12 +1157,10 @@ def insights(cls):
|
||||
"scm_password": "{{password}}",
|
||||
},
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@CredentialType.default
|
||||
def rhv(cls):
|
||||
return cls(
|
||||
ManagedCredentialType(
|
||||
namespace='rhv',
|
||||
kind='cloud',
|
||||
name=ugettext_noop('Red Hat Virtualization'),
|
||||
managed_by_tower=True,
|
||||
@@ -1184,12 +1207,10 @@ def rhv(cls):
|
||||
'OVIRT_PASSWORD': '{{password}}'
|
||||
}
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@CredentialType.default
|
||||
def tower(cls):
|
||||
return cls(
|
||||
ManagedCredentialType(
|
||||
namespace='tower',
|
||||
kind='cloud',
|
||||
name=ugettext_noop('Ansible Tower'),
|
||||
managed_by_tower=True,
|
||||
@@ -1224,4 +1245,4 @@ def tower(cls):
|
||||
'TOWER_VERIFY_SSL': '{{verify_ssl}}'
|
||||
}
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user