mirror of
https://github.com/ansible/awx.git
synced 2026-05-09 18:37:36 -02:30
Merge pull request #1277 from AlanCoding/inv_multicred
Use the m2m field for inventory source credentials
This commit is contained in:
@@ -1540,6 +1540,9 @@ class CustomInventoryScriptSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class InventorySourceOptionsSerializer(BaseSerializer):
|
class InventorySourceOptionsSerializer(BaseSerializer):
|
||||||
|
credential = models.PositiveIntegerField(
|
||||||
|
blank=True, null=True, default=None,
|
||||||
|
help_text='This resource has been deprecated and will be removed in a future release')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
fields = ('*', 'source', 'source_path', 'source_script', 'source_vars', 'credential',
|
fields = ('*', 'source', 'source_path', 'source_script', 'source_vars', 'credential',
|
||||||
@@ -1548,9 +1551,9 @@ class InventorySourceOptionsSerializer(BaseSerializer):
|
|||||||
|
|
||||||
def get_related(self, obj):
|
def get_related(self, obj):
|
||||||
res = super(InventorySourceOptionsSerializer, self).get_related(obj)
|
res = super(InventorySourceOptionsSerializer, self).get_related(obj)
|
||||||
if obj.credential:
|
if obj.credential: # TODO: remove when 'credential' field is removed
|
||||||
res['credential'] = self.reverse('api:credential_detail',
|
res['credential'] = self.reverse('api:credential_detail',
|
||||||
kwargs={'pk': obj.credential.pk})
|
kwargs={'pk': obj.credential})
|
||||||
if obj.source_script:
|
if obj.source_script:
|
||||||
res['source_script'] = self.reverse('api:inventory_script_detail', kwargs={'pk': obj.source_script.pk})
|
res['source_script'] = self.reverse('api:inventory_script_detail', kwargs={'pk': obj.source_script.pk})
|
||||||
return res
|
return res
|
||||||
@@ -1590,13 +1593,19 @@ class InventorySourceOptionsSerializer(BaseSerializer):
|
|||||||
|
|
||||||
return super(InventorySourceOptionsSerializer, self).validate(attrs)
|
return super(InventorySourceOptionsSerializer, self).validate(attrs)
|
||||||
|
|
||||||
def to_representation(self, obj):
|
# TODO: remove when old 'credential' fields are removed
|
||||||
ret = super(InventorySourceOptionsSerializer, self).to_representation(obj)
|
def get_summary_fields(self, obj):
|
||||||
if obj is None:
|
summary_fields = super(InventorySourceOptionsSerializer, self).get_summary_fields(obj)
|
||||||
return ret
|
if 'credential' in summary_fields:
|
||||||
if 'credential' in ret and not obj.credential:
|
cred = obj.get_cloud_credential()
|
||||||
ret['credential'] = None
|
if cred:
|
||||||
return ret
|
summary_fields['credential'] = {
|
||||||
|
'id': cred.id, 'name': cred.name, 'description': cred.description,
|
||||||
|
'kind': cred.kind, 'cloud': True, 'credential_type_id': cred.credential_type_id
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
summary_fields.pop('credential')
|
||||||
|
return summary_fields
|
||||||
|
|
||||||
|
|
||||||
class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOptionsSerializer):
|
class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOptionsSerializer):
|
||||||
@@ -1620,6 +1629,7 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt
|
|||||||
update = self.reverse('api:inventory_source_update_view', kwargs={'pk': obj.pk}),
|
update = self.reverse('api:inventory_source_update_view', kwargs={'pk': obj.pk}),
|
||||||
inventory_updates = self.reverse('api:inventory_source_updates_list', kwargs={'pk': obj.pk}),
|
inventory_updates = self.reverse('api:inventory_source_updates_list', kwargs={'pk': obj.pk}),
|
||||||
schedules = self.reverse('api:inventory_source_schedules_list', kwargs={'pk': obj.pk}),
|
schedules = self.reverse('api:inventory_source_schedules_list', kwargs={'pk': obj.pk}),
|
||||||
|
credentials = self.reverse('api:inventory_source_credentials_list', kwargs={'pk': obj.pk}),
|
||||||
activity_stream = self.reverse('api:inventory_source_activity_stream_list', kwargs={'pk': obj.pk}),
|
activity_stream = self.reverse('api:inventory_source_activity_stream_list', kwargs={'pk': obj.pk}),
|
||||||
hosts = self.reverse('api:inventory_source_hosts_list', kwargs={'pk': obj.pk}),
|
hosts = self.reverse('api:inventory_source_hosts_list', kwargs={'pk': obj.pk}),
|
||||||
groups = self.reverse('api:inventory_source_groups_list', kwargs={'pk': obj.pk}),
|
groups = self.reverse('api:inventory_source_groups_list', kwargs={'pk': obj.pk}),
|
||||||
@@ -1673,6 +1683,14 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt
|
|||||||
field_kwargs.pop('queryset', None)
|
field_kwargs.pop('queryset', None)
|
||||||
return field_class, field_kwargs
|
return field_class, field_kwargs
|
||||||
|
|
||||||
|
# TODO: remove when old 'credential' fields are removed
|
||||||
|
def build_field(self, field_name, info, model_class, nested_depth):
|
||||||
|
# have to special-case the field so that DRF will not automagically make it
|
||||||
|
# read-only because it's a property on the model.
|
||||||
|
if field_name == 'credential':
|
||||||
|
return self.build_standard_field(field_name, self.credential)
|
||||||
|
return super(InventorySourceOptionsSerializer, self).build_field(field_name, info, model_class, nested_depth)
|
||||||
|
|
||||||
def to_representation(self, obj):
|
def to_representation(self, obj):
|
||||||
ret = super(InventorySourceSerializer, self).to_representation(obj)
|
ret = super(InventorySourceSerializer, self).to_representation(obj)
|
||||||
if obj is None:
|
if obj is None:
|
||||||
@@ -1702,7 +1720,44 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt
|
|||||||
raise serializers.ValidationError({"detail": _("Cannot create Inventory Source for Smart Inventory")})
|
raise serializers.ValidationError({"detail": _("Cannot create Inventory Source for Smart Inventory")})
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
# TODO: remove when old 'credential' fields are removed
|
||||||
|
def create(self, validated_data):
|
||||||
|
deprecated_fields = {}
|
||||||
|
if 'credential' in validated_data:
|
||||||
|
deprecated_fields['credential'] = validated_data.pop('credential')
|
||||||
|
obj = super(InventorySourceSerializer, self).create(validated_data)
|
||||||
|
if deprecated_fields:
|
||||||
|
self._update_deprecated_fields(deprecated_fields, obj)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
# TODO: remove when old 'credential' fields are removed
|
||||||
|
def update(self, obj, validated_data):
|
||||||
|
deprecated_fields = {}
|
||||||
|
if 'credential' in validated_data:
|
||||||
|
deprecated_fields['credential'] = validated_data.pop('credential')
|
||||||
|
obj = super(InventorySourceSerializer, self).update(obj, validated_data)
|
||||||
|
if deprecated_fields:
|
||||||
|
self._update_deprecated_fields(deprecated_fields, obj)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
# TODO: remove when old 'credential' fields are removed
|
||||||
|
def _update_deprecated_fields(self, fields, obj):
|
||||||
|
if 'credential' in fields:
|
||||||
|
new_cred = fields['credential']
|
||||||
|
existing_creds = obj.credentials.exclude(credential_type__kind='vault')
|
||||||
|
for cred in existing_creds:
|
||||||
|
# Remove all other cloud credentials
|
||||||
|
if cred != new_cred:
|
||||||
|
obj.credentials.remove(cred)
|
||||||
|
if new_cred:
|
||||||
|
# Add new credential
|
||||||
|
obj.credentials.add(new_cred)
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
|
deprecated_fields = {}
|
||||||
|
if 'credential' in attrs: # TODO: remove when 'credential' field removed
|
||||||
|
deprecated_fields['credential'] = attrs.pop('credential')
|
||||||
|
|
||||||
def get_field_from_model_or_attrs(fd):
|
def get_field_from_model_or_attrs(fd):
|
||||||
return attrs.get(fd, self.instance and getattr(self.instance, fd) or None)
|
return attrs.get(fd, self.instance and getattr(self.instance, fd) or None)
|
||||||
|
|
||||||
@@ -1716,7 +1771,25 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt
|
|||||||
{"detail": _("Cannot set %s if not SCM type." % ' '.join(redundant_scm_fields))}
|
{"detail": _("Cannot set %s if not SCM type." % ' '.join(redundant_scm_fields))}
|
||||||
)
|
)
|
||||||
|
|
||||||
return super(InventorySourceSerializer, self).validate(attrs)
|
attrs = super(InventorySourceSerializer, self).validate(attrs)
|
||||||
|
|
||||||
|
# Check type consistency of source and cloud credential, if provided
|
||||||
|
if 'credential' in deprecated_fields: # TODO: remove when v2 API is deprecated
|
||||||
|
cred = deprecated_fields['credential']
|
||||||
|
attrs['credential'] = cred
|
||||||
|
if cred is not None:
|
||||||
|
cred = Credential.objects.get(pk=cred)
|
||||||
|
view = self.context.get('view', None)
|
||||||
|
if (not view) or (not view.request) or (view.request.user not in cred.use_role):
|
||||||
|
raise PermissionDenied()
|
||||||
|
cred_error = InventorySource.cloud_credential_validation(
|
||||||
|
get_field_from_model_or_attrs('source'),
|
||||||
|
cred
|
||||||
|
)
|
||||||
|
if cred_error:
|
||||||
|
raise serializers.ValidationError({"detail": cred_error})
|
||||||
|
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
class InventorySourceUpdateSerializer(InventorySourceSerializer):
|
class InventorySourceUpdateSerializer(InventorySourceSerializer):
|
||||||
@@ -1746,6 +1819,7 @@ class InventoryUpdateSerializer(UnifiedJobSerializer, InventorySourceOptionsSeri
|
|||||||
res.update(dict(
|
res.update(dict(
|
||||||
cancel = self.reverse('api:inventory_update_cancel', kwargs={'pk': obj.pk}),
|
cancel = self.reverse('api:inventory_update_cancel', kwargs={'pk': obj.pk}),
|
||||||
notifications = self.reverse('api:inventory_update_notifications_list', kwargs={'pk': obj.pk}),
|
notifications = self.reverse('api:inventory_update_notifications_list', kwargs={'pk': obj.pk}),
|
||||||
|
credentials = self.reverse('api:inventory_update_credentials_list', kwargs={'pk': obj.pk}),
|
||||||
events = self.reverse('api:inventory_update_events_list', kwargs={'pk': obj.pk}),
|
events = self.reverse('api:inventory_update_events_list', kwargs={'pk': obj.pk}),
|
||||||
))
|
))
|
||||||
if obj.source_project_update_id:
|
if obj.source_project_update_id:
|
||||||
@@ -2183,8 +2257,6 @@ class CredentialSerializer(BaseSerializer):
|
|||||||
for rel in (
|
for rel in (
|
||||||
'ad_hoc_commands',
|
'ad_hoc_commands',
|
||||||
'insights_inventories',
|
'insights_inventories',
|
||||||
'inventorysources',
|
|
||||||
'inventoryupdates',
|
|
||||||
'unifiedjobs',
|
'unifiedjobs',
|
||||||
'unifiedjobtemplates',
|
'unifiedjobtemplates',
|
||||||
'projects',
|
'projects',
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from awx.api.views import (
|
|||||||
InventorySourceUpdatesList,
|
InventorySourceUpdatesList,
|
||||||
InventorySourceActivityStreamList,
|
InventorySourceActivityStreamList,
|
||||||
InventorySourceSchedulesList,
|
InventorySourceSchedulesList,
|
||||||
|
InventorySourceCredentialsList,
|
||||||
InventorySourceGroupsList,
|
InventorySourceGroupsList,
|
||||||
InventorySourceHostsList,
|
InventorySourceHostsList,
|
||||||
InventorySourceNotificationTemplatesAnyList,
|
InventorySourceNotificationTemplatesAnyList,
|
||||||
@@ -25,6 +26,7 @@ urls = [
|
|||||||
url(r'^(?P<pk>[0-9]+)/inventory_updates/$', InventorySourceUpdatesList.as_view(), name='inventory_source_updates_list'),
|
url(r'^(?P<pk>[0-9]+)/inventory_updates/$', InventorySourceUpdatesList.as_view(), name='inventory_source_updates_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/activity_stream/$', InventorySourceActivityStreamList.as_view(), name='inventory_source_activity_stream_list'),
|
url(r'^(?P<pk>[0-9]+)/activity_stream/$', InventorySourceActivityStreamList.as_view(), name='inventory_source_activity_stream_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/schedules/$', InventorySourceSchedulesList.as_view(), name='inventory_source_schedules_list'),
|
url(r'^(?P<pk>[0-9]+)/schedules/$', InventorySourceSchedulesList.as_view(), name='inventory_source_schedules_list'),
|
||||||
|
url(r'^(?P<pk>[0-9]+)/credentials/$', InventorySourceCredentialsList.as_view(), name='inventory_source_credentials_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/groups/$', InventorySourceGroupsList.as_view(), name='inventory_source_groups_list'),
|
url(r'^(?P<pk>[0-9]+)/groups/$', InventorySourceGroupsList.as_view(), name='inventory_source_groups_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/hosts/$', InventorySourceHostsList.as_view(), name='inventory_source_hosts_list'),
|
url(r'^(?P<pk>[0-9]+)/hosts/$', InventorySourceHostsList.as_view(), name='inventory_source_hosts_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/notification_templates_any/$', InventorySourceNotificationTemplatesAnyList.as_view(),
|
url(r'^(?P<pk>[0-9]+)/notification_templates_any/$', InventorySourceNotificationTemplatesAnyList.as_view(),
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from awx.api.views import (
|
|||||||
InventoryUpdateCancel,
|
InventoryUpdateCancel,
|
||||||
InventoryUpdateStdout,
|
InventoryUpdateStdout,
|
||||||
InventoryUpdateNotificationsList,
|
InventoryUpdateNotificationsList,
|
||||||
|
InventoryUpdateCredentialsList,
|
||||||
InventoryUpdateEventsList,
|
InventoryUpdateEventsList,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ urls = [
|
|||||||
url(r'^(?P<pk>[0-9]+)/cancel/$', InventoryUpdateCancel.as_view(), name='inventory_update_cancel'),
|
url(r'^(?P<pk>[0-9]+)/cancel/$', InventoryUpdateCancel.as_view(), name='inventory_update_cancel'),
|
||||||
url(r'^(?P<pk>[0-9]+)/stdout/$', InventoryUpdateStdout.as_view(), name='inventory_update_stdout'),
|
url(r'^(?P<pk>[0-9]+)/stdout/$', InventoryUpdateStdout.as_view(), name='inventory_update_stdout'),
|
||||||
url(r'^(?P<pk>[0-9]+)/notifications/$', InventoryUpdateNotificationsList.as_view(), name='inventory_update_notifications_list'),
|
url(r'^(?P<pk>[0-9]+)/notifications/$', InventoryUpdateNotificationsList.as_view(), name='inventory_update_notifications_list'),
|
||||||
|
url(r'^(?P<pk>[0-9]+)/credentials/$', InventoryUpdateCredentialsList.as_view(), name='inventory_update_credentials_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/events/$', InventoryUpdateEventsList.as_view(), name='inventory_update_events_list'),
|
url(r'^(?P<pk>[0-9]+)/events/$', InventoryUpdateEventsList.as_view(), name='inventory_update_events_list'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -2723,6 +2723,28 @@ class InventorySourceUpdatesList(SubListAPIView):
|
|||||||
relationship = 'inventory_updates'
|
relationship = 'inventory_updates'
|
||||||
|
|
||||||
|
|
||||||
|
class InventorySourceCredentialsList(SubListAttachDetachAPIView):
|
||||||
|
|
||||||
|
parent_model = InventorySource
|
||||||
|
model = Credential
|
||||||
|
serializer_class = CredentialSerializer
|
||||||
|
relationship = 'credentials'
|
||||||
|
|
||||||
|
def is_valid_relation(self, parent, sub, created=False):
|
||||||
|
error = InventorySource.cloud_credential_validation(parent.source, sub)
|
||||||
|
if error:
|
||||||
|
return {'msg': error}
|
||||||
|
if sub.credential_type == 'vault':
|
||||||
|
# TODO: support this
|
||||||
|
return {"msg": _("Vault credentials are not yet supported for inventory sources.")}
|
||||||
|
else:
|
||||||
|
# Cloud credentials are exclusive with all other cloud credentials
|
||||||
|
cloud_cred_qs = parent.credentials.exclude(credential_type__kind='vault')
|
||||||
|
if cloud_cred_qs.exists():
|
||||||
|
return {'msg': _("Source already has cloud credential assigned.")}
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class InventorySourceUpdateView(RetrieveAPIView):
|
class InventorySourceUpdateView(RetrieveAPIView):
|
||||||
|
|
||||||
model = InventorySource
|
model = InventorySource
|
||||||
@@ -2757,6 +2779,14 @@ class InventoryUpdateDetail(UnifiedJobDeletionMixin, RetrieveDestroyAPIView):
|
|||||||
serializer_class = InventoryUpdateSerializer
|
serializer_class = InventoryUpdateSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryUpdateCredentialsList(SubListAPIView):
|
||||||
|
|
||||||
|
parent_model = InventoryUpdate
|
||||||
|
model = Credential
|
||||||
|
serializer_class = CredentialSerializer
|
||||||
|
relationship = 'credentials'
|
||||||
|
|
||||||
|
|
||||||
class InventoryUpdateCancel(RetrieveAPIView):
|
class InventoryUpdateCancel(RetrieveAPIView):
|
||||||
|
|
||||||
model = InventoryUpdate
|
model = InventoryUpdate
|
||||||
|
|||||||
@@ -830,7 +830,8 @@ class InventorySourceAccess(BaseAccess):
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
model = InventorySource
|
model = InventorySource
|
||||||
select_related = ('created_by', 'modified_by', 'inventory',)
|
select_related = ('created_by', 'modified_by', 'inventory')
|
||||||
|
prefetch_related = ('credentials',)
|
||||||
|
|
||||||
def filtered_queryset(self):
|
def filtered_queryset(self):
|
||||||
return self.model.objects.filter(inventory__in=Inventory.accessible_pk_qs(self.user, 'read_role'))
|
return self.model.objects.filter(inventory__in=Inventory.accessible_pk_qs(self.user, 'read_role'))
|
||||||
@@ -878,6 +879,21 @@ class InventorySourceAccess(BaseAccess):
|
|||||||
return self.user in obj.inventory.update_role
|
return self.user in obj.inventory.update_role
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@check_superuser
|
||||||
|
def can_attach(self, obj, sub_obj, relationship, data, skip_sub_obj_read_check=False):
|
||||||
|
if relationship == 'credentials' and isinstance(sub_obj, Credential):
|
||||||
|
return (
|
||||||
|
obj and obj.inventory and self.user in obj.inventory.admin_role and
|
||||||
|
self.user in sub_obj.use_role)
|
||||||
|
return super(InventorySourceAccess, self).can_attach(
|
||||||
|
obj, sub_obj, relationship, data, skip_sub_obj_read_check=skip_sub_obj_read_check)
|
||||||
|
|
||||||
|
@check_superuser
|
||||||
|
def can_unattach(self, obj, sub_obj, relationship, *args, **kwargs):
|
||||||
|
if relationship == 'credentials' and isinstance(sub_obj, Credential):
|
||||||
|
return obj and obj.inventory and self.user in obj.inventory.admin_role
|
||||||
|
return super(InventorySourceAccess, self).can_attach(obj, sub_obj, relationship, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class InventoryUpdateAccess(BaseAccess):
|
class InventoryUpdateAccess(BaseAccess):
|
||||||
'''
|
'''
|
||||||
@@ -888,7 +904,7 @@ class InventoryUpdateAccess(BaseAccess):
|
|||||||
|
|
||||||
model = InventoryUpdate
|
model = InventoryUpdate
|
||||||
select_related = ('created_by', 'modified_by', 'inventory_source__inventory',)
|
select_related = ('created_by', 'modified_by', 'inventory_source__inventory',)
|
||||||
prefetch_related = ('unified_job_template', 'instance_group',)
|
prefetch_related = ('unified_job_template', 'instance_group', 'credentials',)
|
||||||
|
|
||||||
def filtered_queryset(self):
|
def filtered_queryset(self):
|
||||||
return self.model.objects.filter(inventory_source__inventory__in=Inventory.accessible_pk_qs(self.user, 'read_role'))
|
return self.model.objects.filter(inventory_source__inventory__in=Inventory.accessible_pk_qs(self.user, 'read_role'))
|
||||||
|
|||||||
32
awx/main/migrations/0023_v330_inventory_multicred.py
Normal file
32
awx/main/migrations/0023_v330_inventory_multicred.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.7 on 2018-02-14 16:14
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
from awx.main.migrations import _migration_utils as migration_utils
|
||||||
|
from awx.main.migrations._multi_cred import (
|
||||||
|
migrate_inventory_source_cred,
|
||||||
|
migrate_inventory_source_cred_reverse
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('main', '0022_v330_create_new_rbac_roles'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
# Run data migration before removing the old credential field
|
||||||
|
migrations.RunPython(migration_utils.set_current_apps_for_migrations, migrations.RunPython.noop),
|
||||||
|
migrations.RunPython(migrate_inventory_source_cred, migrate_inventory_source_cred_reverse),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='inventorysource',
|
||||||
|
name='credential',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='inventoryupdate',
|
||||||
|
name='credential',
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -32,3 +32,25 @@ def migrate_workflow_cred_reverse(app, schema_editor):
|
|||||||
if cred:
|
if cred:
|
||||||
node.credential = cred
|
node.credential = cred
|
||||||
node.save()
|
node.save()
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_inventory_source_cred(app, schema_editor):
|
||||||
|
InventoryUpdate = app.get_model('main', 'InventoryUpdate')
|
||||||
|
InventorySource = app.get_model('main', 'InventorySource')
|
||||||
|
|
||||||
|
for cls in (InventoryUpdate, InventorySource):
|
||||||
|
for obj in cls.objects.iterator():
|
||||||
|
if obj.credential:
|
||||||
|
obj.credentials.add(obj.credential)
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_inventory_source_cred_reverse(app, schema_editor):
|
||||||
|
InventoryUpdate = app.get_model('main', 'InventoryUpdate')
|
||||||
|
InventorySource = app.get_model('main', 'InventorySource')
|
||||||
|
|
||||||
|
for cls in (InventoryUpdate, InventorySource):
|
||||||
|
for obj in cls.objects.iterator():
|
||||||
|
cred = obj.credentials.first()
|
||||||
|
if cred:
|
||||||
|
obj.credential = cred
|
||||||
|
obj.save()
|
||||||
|
|||||||
@@ -1084,14 +1084,6 @@ class InventorySourceOptions(BaseModel):
|
|||||||
default='',
|
default='',
|
||||||
help_text=_('Inventory source variables in YAML or JSON format.'),
|
help_text=_('Inventory source variables in YAML or JSON format.'),
|
||||||
)
|
)
|
||||||
credential = models.ForeignKey(
|
|
||||||
'Credential',
|
|
||||||
related_name='%(class)ss',
|
|
||||||
null=True,
|
|
||||||
default=None,
|
|
||||||
blank=True,
|
|
||||||
on_delete=models.SET_NULL,
|
|
||||||
)
|
|
||||||
source_regions = models.CharField(
|
source_regions = models.CharField(
|
||||||
max_length=1024,
|
max_length=1024,
|
||||||
blank=True,
|
blank=True,
|
||||||
@@ -1223,30 +1215,48 @@ class InventorySourceOptions(BaseModel):
|
|||||||
"""No region supprt"""
|
"""No region supprt"""
|
||||||
return [('all', 'All')]
|
return [('all', 'All')]
|
||||||
|
|
||||||
def clean_credential(self):
|
@staticmethod
|
||||||
if not self.source:
|
def cloud_credential_validation(source, cred):
|
||||||
|
if not source:
|
||||||
return None
|
return None
|
||||||
cred = self.credential
|
if cred and source not in ('custom', 'scm'):
|
||||||
if cred and self.source not in ('custom', 'scm'):
|
|
||||||
# If a credential was provided, it's important that it matches
|
# If a credential was provided, it's important that it matches
|
||||||
# the actual inventory source being used (Amazon requires Amazon
|
# the actual inventory source being used (Amazon requires Amazon
|
||||||
# credentials; Rackspace requires Rackspace credentials; etc...)
|
# credentials; Rackspace requires Rackspace credentials; etc...)
|
||||||
if self.source.replace('ec2', 'aws') != cred.kind:
|
if source.replace('ec2', 'aws') != cred.kind:
|
||||||
raise ValidationError(
|
return _('Cloud-based inventory sources (such as %s) require '
|
||||||
_('Cloud-based inventory sources (such as %s) require '
|
'credentials for the matching cloud service.') % source
|
||||||
'credentials for the matching cloud service.') % self.source
|
|
||||||
)
|
|
||||||
# Allow an EC2 source to omit the credential. If Tower is running on
|
# Allow an EC2 source to omit the credential. If Tower is running on
|
||||||
# an EC2 instance with an IAM Role assigned, boto will use credentials
|
# an EC2 instance with an IAM Role assigned, boto will use credentials
|
||||||
# from the instance metadata instead of those explicitly provided.
|
# from the instance metadata instead of those explicitly provided.
|
||||||
elif self.source in CLOUD_PROVIDERS and self.source != 'ec2':
|
elif source in CLOUD_PROVIDERS and source != 'ec2':
|
||||||
raise ValidationError(_('Credential is required for a cloud source.'))
|
return _('Credential is required for a cloud source.')
|
||||||
elif self.source == 'custom' and cred and cred.credential_type.kind in ('scm', 'ssh', 'insights', 'vault'):
|
elif source == 'custom' and cred and cred.credential_type.kind in ('scm', 'ssh', 'insights', 'vault'):
|
||||||
raise ValidationError(_(
|
return _(
|
||||||
'Credentials of type machine, source control, insights and vault are '
|
'Credentials of type machine, source control, insights and vault are '
|
||||||
'disallowed for custom inventory sources.'
|
'disallowed for custom inventory sources.'
|
||||||
))
|
)
|
||||||
return cred
|
return None
|
||||||
|
|
||||||
|
def get_deprecated_credential(self, kind):
|
||||||
|
for cred in self.credentials.all():
|
||||||
|
if cred.credential_type.kind == kind:
|
||||||
|
return cred
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_cloud_credential(self):
|
||||||
|
credential = None
|
||||||
|
for cred in self.credentials.all():
|
||||||
|
if cred.credential_type.kind != 'vault':
|
||||||
|
credential = cred
|
||||||
|
return credential
|
||||||
|
|
||||||
|
@property
|
||||||
|
def credential(self):
|
||||||
|
cred = self.get_cloud_credential()
|
||||||
|
if cred is not None:
|
||||||
|
return cred.pk
|
||||||
|
|
||||||
def clean_source_regions(self):
|
def clean_source_regions(self):
|
||||||
regions = self.source_regions
|
regions = self.source_regions
|
||||||
@@ -1376,7 +1386,7 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def _get_unified_job_field_names(cls):
|
def _get_unified_job_field_names(cls):
|
||||||
return set(f.name for f in InventorySourceOptions._meta.fields) | set(
|
return set(f.name for f in InventorySourceOptions._meta.fields) | set(
|
||||||
['name', 'description', 'schedule']
|
['name', 'description', 'schedule', 'credentials']
|
||||||
)
|
)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
@@ -1621,7 +1631,7 @@ class InventoryUpdate(UnifiedJob, InventorySourceOptions, JobNotificationMixin,
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
if (self.source not in ('custom', 'ec2', 'scm') and
|
if (self.source not in ('custom', 'ec2', 'scm') and
|
||||||
not (self.credential)):
|
not (self.get_cloud_credential())):
|
||||||
return False
|
return False
|
||||||
elif self.source == 'scm' and not self.inventory_source.source_project:
|
elif self.source == 'scm' and not self.inventory_source.source_project:
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -924,8 +924,11 @@ class BaseTask(LogErrorsTask):
|
|||||||
credentials = []
|
credentials = []
|
||||||
if isinstance(instance, Job):
|
if isinstance(instance, Job):
|
||||||
credentials = instance.credentials.all()
|
credentials = instance.credentials.all()
|
||||||
elif hasattr(instance, 'credential'):
|
elif isinstance(instance, InventoryUpdate):
|
||||||
# once other UnifiedJobs (project updates, inventory updates)
|
# TODO: allow multiple custom creds for inv updates
|
||||||
|
credentials = [instance.get_cloud_credential()]
|
||||||
|
elif isinstance(instance, Project):
|
||||||
|
# once (or if) project updates
|
||||||
# move from a .credential -> .credentials model, we can
|
# move from a .credential -> .credentials model, we can
|
||||||
# lose this block
|
# lose this block
|
||||||
credentials = [instance.credential]
|
credentials = [instance.credential]
|
||||||
@@ -1719,14 +1722,13 @@ class RunInventoryUpdate(BaseTask):
|
|||||||
If no private data is needed, return None.
|
If no private data is needed, return None.
|
||||||
"""
|
"""
|
||||||
private_data = {'credentials': {}}
|
private_data = {'credentials': {}}
|
||||||
|
credential = inventory_update.get_cloud_credential()
|
||||||
# If this is GCE, return the RSA key
|
# If this is GCE, return the RSA key
|
||||||
if inventory_update.source == 'gce':
|
if inventory_update.source == 'gce':
|
||||||
credential = inventory_update.credential
|
|
||||||
private_data['credentials'][credential] = decrypt_field(credential, 'ssh_key_data')
|
private_data['credentials'][credential] = decrypt_field(credential, 'ssh_key_data')
|
||||||
return private_data
|
return private_data
|
||||||
|
|
||||||
if inventory_update.source == 'openstack':
|
if inventory_update.source == 'openstack':
|
||||||
credential = inventory_update.credential
|
|
||||||
openstack_auth = dict(auth_url=credential.host,
|
openstack_auth = dict(auth_url=credential.host,
|
||||||
username=credential.username,
|
username=credential.username,
|
||||||
password=decrypt_field(credential, "password"),
|
password=decrypt_field(credential, "password"),
|
||||||
@@ -1803,7 +1805,6 @@ class RunInventoryUpdate(BaseTask):
|
|||||||
cp.set(section, k, six.text_type(v))
|
cp.set(section, k, six.text_type(v))
|
||||||
# Allow custom options to vmware inventory script.
|
# Allow custom options to vmware inventory script.
|
||||||
elif inventory_update.source == 'vmware':
|
elif inventory_update.source == 'vmware':
|
||||||
credential = inventory_update.credential
|
|
||||||
|
|
||||||
section = 'vmware'
|
section = 'vmware'
|
||||||
cp.add_section(section)
|
cp.add_section(section)
|
||||||
@@ -1838,7 +1839,6 @@ class RunInventoryUpdate(BaseTask):
|
|||||||
else:
|
else:
|
||||||
cp.set(section, k, six.text_type(v))
|
cp.set(section, k, six.text_type(v))
|
||||||
|
|
||||||
credential = inventory_update.credential
|
|
||||||
if credential:
|
if credential:
|
||||||
cp.set(section, 'url', credential.host)
|
cp.set(section, 'url', credential.host)
|
||||||
cp.set(section, 'user', credential.username)
|
cp.set(section, 'user', credential.username)
|
||||||
@@ -1859,7 +1859,6 @@ class RunInventoryUpdate(BaseTask):
|
|||||||
section = 'cloudforms'
|
section = 'cloudforms'
|
||||||
cp.add_section(section)
|
cp.add_section(section)
|
||||||
|
|
||||||
credential = inventory_update.credential
|
|
||||||
if credential:
|
if credential:
|
||||||
cp.set(section, 'url', credential.host)
|
cp.set(section, 'url', credential.host)
|
||||||
cp.set(section, 'username', credential.username)
|
cp.set(section, 'username', credential.username)
|
||||||
@@ -1897,7 +1896,7 @@ class RunInventoryUpdate(BaseTask):
|
|||||||
if cp.sections():
|
if cp.sections():
|
||||||
f = cStringIO.StringIO()
|
f = cStringIO.StringIO()
|
||||||
cp.write(f)
|
cp.write(f)
|
||||||
private_data['credentials'][inventory_update.credential] = f.getvalue()
|
private_data['credentials'][credential] = f.getvalue()
|
||||||
return private_data
|
return private_data
|
||||||
|
|
||||||
def build_passwords(self, inventory_update, **kwargs):
|
def build_passwords(self, inventory_update, **kwargs):
|
||||||
@@ -1912,7 +1911,7 @@ class RunInventoryUpdate(BaseTask):
|
|||||||
|
|
||||||
# Take key fields from the credential in use and add them to the
|
# Take key fields from the credential in use and add them to the
|
||||||
# passwords dictionary.
|
# passwords dictionary.
|
||||||
credential = inventory_update.credential
|
credential = inventory_update.get_cloud_credential()
|
||||||
if credential:
|
if credential:
|
||||||
for subkey in ('username', 'host', 'project', 'client', 'tenant', 'subscription'):
|
for subkey in ('username', 'host', 'project', 'client', 'tenant', 'subscription'):
|
||||||
passwords['source_%s' % subkey] = getattr(credential, subkey)
|
passwords['source_%s' % subkey] = getattr(credential, subkey)
|
||||||
@@ -1957,7 +1956,9 @@ class RunInventoryUpdate(BaseTask):
|
|||||||
}
|
}
|
||||||
if inventory_update.source in ini_mapping:
|
if inventory_update.source in ini_mapping:
|
||||||
cred_data = kwargs.get('private_data_files', {}).get('credentials', '')
|
cred_data = kwargs.get('private_data_files', {}).get('credentials', '')
|
||||||
env[ini_mapping[inventory_update.source]] = cred_data.get(inventory_update.credential, '')
|
env[ini_mapping[inventory_update.source]] = cred_data.get(
|
||||||
|
inventory_update.get_cloud_credential(), ''
|
||||||
|
)
|
||||||
|
|
||||||
if inventory_update.source == 'gce':
|
if inventory_update.source == 'gce':
|
||||||
env['GCE_ZONE'] = inventory_update.source_regions if inventory_update.source_regions != 'all' else '' # noqa
|
env['GCE_ZONE'] = inventory_update.source_regions if inventory_update.source_regions != 'all' else '' # noqa
|
||||||
|
|||||||
@@ -1426,9 +1426,9 @@ def test_field_removal(put, organization, admin, credentialtype_ssh, version, pa
|
|||||||
@pytest.mark.parametrize('relation, related_obj', [
|
@pytest.mark.parametrize('relation, related_obj', [
|
||||||
['ad_hoc_commands', AdHocCommand()],
|
['ad_hoc_commands', AdHocCommand()],
|
||||||
['insights_inventories', Inventory()],
|
['insights_inventories', Inventory()],
|
||||||
['inventorysources', InventorySource()],
|
|
||||||
['unifiedjobs', Job()],
|
['unifiedjobs', Job()],
|
||||||
['unifiedjobtemplates', JobTemplate()],
|
['unifiedjobtemplates', JobTemplate()],
|
||||||
|
['unifiedjobtemplates', InventorySource()],
|
||||||
['projects', Project()],
|
['projects', Project()],
|
||||||
['workflowjobnodes', WorkflowJobNode()],
|
['workflowjobnodes', WorkflowJobNode()],
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -103,18 +103,22 @@ def test_safe_env_returns_new_copy():
|
|||||||
|
|
||||||
def test_openstack_client_config_generation(mocker):
|
def test_openstack_client_config_generation(mocker):
|
||||||
update = tasks.RunInventoryUpdate()
|
update = tasks.RunInventoryUpdate()
|
||||||
|
credential = mocker.Mock(**{
|
||||||
|
'host': 'https://keystone.openstack.example.org',
|
||||||
|
'username': 'demo',
|
||||||
|
'password': 'secrete',
|
||||||
|
'project': 'demo-project',
|
||||||
|
'domain': 'my-demo-domain',
|
||||||
|
})
|
||||||
|
cred_method = mocker.Mock(return_value=credential)
|
||||||
inventory_update = mocker.Mock(**{
|
inventory_update = mocker.Mock(**{
|
||||||
'source': 'openstack',
|
'source': 'openstack',
|
||||||
'credential.host': 'https://keystone.openstack.example.org',
|
'source_vars_dict': {},
|
||||||
'credential.username': 'demo',
|
'get_cloud_credential': cred_method
|
||||||
'credential.password': 'secrete',
|
|
||||||
'credential.project': 'demo-project',
|
|
||||||
'credential.domain': 'my-demo-domain',
|
|
||||||
'source_vars_dict': {}
|
|
||||||
})
|
})
|
||||||
cloud_config = update.build_private_data(inventory_update)
|
cloud_config = update.build_private_data(inventory_update)
|
||||||
cloud_credential = yaml.load(
|
cloud_credential = yaml.load(
|
||||||
cloud_config.get('credentials')[inventory_update.credential]
|
cloud_config.get('credentials')[credential]
|
||||||
)
|
)
|
||||||
assert cloud_credential['clouds'] == {
|
assert cloud_credential['clouds'] == {
|
||||||
'devstack': {
|
'devstack': {
|
||||||
@@ -135,18 +139,22 @@ def test_openstack_client_config_generation(mocker):
|
|||||||
])
|
])
|
||||||
def test_openstack_client_config_generation_with_private_source_vars(mocker, source, expected):
|
def test_openstack_client_config_generation_with_private_source_vars(mocker, source, expected):
|
||||||
update = tasks.RunInventoryUpdate()
|
update = tasks.RunInventoryUpdate()
|
||||||
|
credential = mocker.Mock(**{
|
||||||
|
'host': 'https://keystone.openstack.example.org',
|
||||||
|
'username': 'demo',
|
||||||
|
'password': 'secrete',
|
||||||
|
'project': 'demo-project',
|
||||||
|
'domain': None,
|
||||||
|
})
|
||||||
|
cred_method = mocker.Mock(return_value=credential)
|
||||||
inventory_update = mocker.Mock(**{
|
inventory_update = mocker.Mock(**{
|
||||||
'source': 'openstack',
|
'source': 'openstack',
|
||||||
'credential.host': 'https://keystone.openstack.example.org',
|
'source_vars_dict': {'private': source},
|
||||||
'credential.username': 'demo',
|
'get_cloud_credential': cred_method
|
||||||
'credential.password': 'secrete',
|
|
||||||
'credential.project': 'demo-project',
|
|
||||||
'credential.domain': None,
|
|
||||||
'source_vars_dict': {'private': source}
|
|
||||||
})
|
})
|
||||||
cloud_config = update.build_private_data(inventory_update)
|
cloud_config = update.build_private_data(inventory_update)
|
||||||
cloud_credential = yaml.load(
|
cloud_credential = yaml.load(
|
||||||
cloud_config.get('credentials')[inventory_update.credential]
|
cloud_config.get('credentials')[credential]
|
||||||
)
|
)
|
||||||
assert cloud_credential['clouds'] == {
|
assert cloud_credential['clouds'] == {
|
||||||
'devstack': {
|
'devstack': {
|
||||||
@@ -1519,8 +1527,9 @@ class TestInventoryUpdateCredentials(TestJobExecution):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_source_without_credential(self):
|
def test_source_without_credential(self, mocker):
|
||||||
self.instance.source = 'ec2'
|
self.instance.source = 'ec2'
|
||||||
|
self.instance.get_cloud_credential = mocker.Mock(return_value=None)
|
||||||
|
|
||||||
def run_pexpect_side_effect(*args, **kwargs):
|
def run_pexpect_side_effect(*args, **kwargs):
|
||||||
args, cwd, env, stdout = args
|
args, cwd, env, stdout = args
|
||||||
@@ -1538,7 +1547,7 @@ class TestInventoryUpdateCredentials(TestJobExecution):
|
|||||||
self.task.run(self.pk)
|
self.task.run(self.pk)
|
||||||
|
|
||||||
@pytest.mark.parametrize('with_credential', [True, False])
|
@pytest.mark.parametrize('with_credential', [True, False])
|
||||||
def test_custom_source(self, with_credential):
|
def test_custom_source(self, with_credential, mocker):
|
||||||
self.instance.source = 'custom'
|
self.instance.source = 'custom'
|
||||||
self.instance.source_vars = '{"FOO": "BAR"}'
|
self.instance.source_vars = '{"FOO": "BAR"}'
|
||||||
patch = mock.patch.object(InventoryUpdate, 'source_script', mock.Mock(
|
patch = mock.patch.object(InventoryUpdate, 'source_script', mock.Mock(
|
||||||
@@ -1549,16 +1558,22 @@ class TestInventoryUpdateCredentials(TestJobExecution):
|
|||||||
|
|
||||||
if with_credential:
|
if with_credential:
|
||||||
azure_rm = CredentialType.defaults['azure_rm']()
|
azure_rm = CredentialType.defaults['azure_rm']()
|
||||||
self.instance.credential = Credential(
|
|
||||||
pk=1,
|
def get_cred():
|
||||||
credential_type=azure_rm,
|
cred = Credential(
|
||||||
inputs = {
|
pk=1,
|
||||||
'client': 'some-client',
|
credential_type=azure_rm,
|
||||||
'secret': 'some-secret',
|
inputs = {
|
||||||
'tenant': 'some-tenant',
|
'client': 'some-client',
|
||||||
'subscription': 'some-subscription',
|
'secret': 'some-secret',
|
||||||
}
|
'tenant': 'some-tenant',
|
||||||
)
|
'subscription': 'some-subscription',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return cred
|
||||||
|
self.instance.get_cloud_credential = get_cred
|
||||||
|
else:
|
||||||
|
self.instance.get_cloud_credential = mocker.Mock(return_value=None)
|
||||||
|
|
||||||
def run_pexpect_side_effect(*args, **kwargs):
|
def run_pexpect_side_effect(*args, **kwargs):
|
||||||
args, cwd, env, stdout = args
|
args, cwd, env, stdout = args
|
||||||
@@ -1580,14 +1595,16 @@ class TestInventoryUpdateCredentials(TestJobExecution):
|
|||||||
def test_ec2_source(self):
|
def test_ec2_source(self):
|
||||||
aws = CredentialType.defaults['aws']()
|
aws = CredentialType.defaults['aws']()
|
||||||
self.instance.source = 'ec2'
|
self.instance.source = 'ec2'
|
||||||
self.instance.credential = Credential(
|
|
||||||
pk=1,
|
def get_cred():
|
||||||
credential_type=aws,
|
cred = Credential(
|
||||||
inputs = {'username': 'bob', 'password': 'secret'}
|
pk=1,
|
||||||
)
|
credential_type=aws,
|
||||||
self.instance.credential.inputs['password'] = encrypt_field(
|
inputs = {'username': 'bob', 'password': 'secret'}
|
||||||
self.instance.credential, 'password'
|
)
|
||||||
)
|
cred.inputs['password'] = encrypt_field(cred, 'password')
|
||||||
|
return cred
|
||||||
|
self.instance.get_cloud_credential = get_cred
|
||||||
|
|
||||||
def run_pexpect_side_effect(*args, **kwargs):
|
def run_pexpect_side_effect(*args, **kwargs):
|
||||||
args, cwd, env, stdout = args
|
args, cwd, env, stdout = args
|
||||||
@@ -1608,14 +1625,16 @@ class TestInventoryUpdateCredentials(TestJobExecution):
|
|||||||
def test_vmware_source(self):
|
def test_vmware_source(self):
|
||||||
vmware = CredentialType.defaults['vmware']()
|
vmware = CredentialType.defaults['vmware']()
|
||||||
self.instance.source = 'vmware'
|
self.instance.source = 'vmware'
|
||||||
self.instance.credential = Credential(
|
|
||||||
pk=1,
|
def get_cred():
|
||||||
credential_type=vmware,
|
cred = Credential(
|
||||||
inputs = {'username': 'bob', 'password': 'secret', 'host': 'https://example.org'}
|
pk=1,
|
||||||
)
|
credential_type=vmware,
|
||||||
self.instance.credential.inputs['password'] = encrypt_field(
|
inputs = {'username': 'bob', 'password': 'secret', 'host': 'https://example.org'}
|
||||||
self.instance.credential, 'password'
|
)
|
||||||
)
|
cred.inputs['password'] = encrypt_field(cred, 'password')
|
||||||
|
return cred
|
||||||
|
self.instance.get_cloud_credential = get_cred
|
||||||
|
|
||||||
def run_pexpect_side_effect(*args, **kwargs):
|
def run_pexpect_side_effect(*args, **kwargs):
|
||||||
args, cwd, env, stdout = args
|
args, cwd, env, stdout = args
|
||||||
@@ -1634,17 +1653,21 @@ class TestInventoryUpdateCredentials(TestJobExecution):
|
|||||||
azure_rm = CredentialType.defaults['azure_rm']()
|
azure_rm = CredentialType.defaults['azure_rm']()
|
||||||
self.instance.source = 'azure_rm'
|
self.instance.source = 'azure_rm'
|
||||||
self.instance.source_regions = 'north, south, east, west'
|
self.instance.source_regions = 'north, south, east, west'
|
||||||
self.instance.credential = Credential(
|
|
||||||
pk=1,
|
def get_cred():
|
||||||
credential_type=azure_rm,
|
cred = Credential(
|
||||||
inputs = {
|
pk=1,
|
||||||
'client': 'some-client',
|
credential_type=azure_rm,
|
||||||
'secret': 'some-secret',
|
inputs = {
|
||||||
'tenant': 'some-tenant',
|
'client': 'some-client',
|
||||||
'subscription': 'some-subscription',
|
'secret': 'some-secret',
|
||||||
'cloud_environment': 'foobar'
|
'tenant': 'some-tenant',
|
||||||
}
|
'subscription': 'some-subscription',
|
||||||
)
|
'cloud_environment': 'foobar'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return cred
|
||||||
|
self.instance.get_cloud_credential = get_cred
|
||||||
|
|
||||||
def run_pexpect_side_effect(*args, **kwargs):
|
def run_pexpect_side_effect(*args, **kwargs):
|
||||||
args, cwd, env, stdout = args
|
args, cwd, env, stdout = args
|
||||||
@@ -1671,16 +1694,20 @@ class TestInventoryUpdateCredentials(TestJobExecution):
|
|||||||
azure_rm = CredentialType.defaults['azure_rm']()
|
azure_rm = CredentialType.defaults['azure_rm']()
|
||||||
self.instance.source = 'azure_rm'
|
self.instance.source = 'azure_rm'
|
||||||
self.instance.source_regions = 'all'
|
self.instance.source_regions = 'all'
|
||||||
self.instance.credential = Credential(
|
|
||||||
pk=1,
|
def get_cred():
|
||||||
credential_type=azure_rm,
|
cred = Credential(
|
||||||
inputs = {
|
pk=1,
|
||||||
'subscription': 'some-subscription',
|
credential_type=azure_rm,
|
||||||
'username': 'bob',
|
inputs = {
|
||||||
'password': 'secret',
|
'subscription': 'some-subscription',
|
||||||
'cloud_environment': 'foobar'
|
'username': 'bob',
|
||||||
}
|
'password': 'secret',
|
||||||
)
|
'cloud_environment': 'foobar'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return cred
|
||||||
|
self.instance.get_cloud_credential = get_cred
|
||||||
|
|
||||||
def run_pexpect_side_effect(*args, **kwargs):
|
def run_pexpect_side_effect(*args, **kwargs):
|
||||||
args, cwd, env, stdout = args
|
args, cwd, env, stdout = args
|
||||||
@@ -1706,18 +1733,23 @@ class TestInventoryUpdateCredentials(TestJobExecution):
|
|||||||
gce = CredentialType.defaults['gce']()
|
gce = CredentialType.defaults['gce']()
|
||||||
self.instance.source = 'gce'
|
self.instance.source = 'gce'
|
||||||
self.instance.source_regions = 'all'
|
self.instance.source_regions = 'all'
|
||||||
self.instance.credential = Credential(
|
|
||||||
pk=1,
|
def get_cred():
|
||||||
credential_type=gce,
|
cred = Credential(
|
||||||
inputs = {
|
pk=1,
|
||||||
'username': 'bob',
|
credential_type=gce,
|
||||||
'project': 'some-project',
|
inputs = {
|
||||||
'ssh_key_data': self.EXAMPLE_PRIVATE_KEY
|
'username': 'bob',
|
||||||
}
|
'project': 'some-project',
|
||||||
)
|
'ssh_key_data': self.EXAMPLE_PRIVATE_KEY
|
||||||
self.instance.credential.inputs['ssh_key_data'] = encrypt_field(
|
}
|
||||||
self.instance.credential, 'ssh_key_data'
|
)
|
||||||
)
|
cred.inputs['ssh_key_data'] = encrypt_field(
|
||||||
|
cred, 'ssh_key_data'
|
||||||
|
)
|
||||||
|
return cred
|
||||||
|
self.instance.get_cloud_credential = get_cred
|
||||||
|
|
||||||
expected_gce_zone = ''
|
expected_gce_zone = ''
|
||||||
|
|
||||||
def run_pexpect_side_effect(*args, **kwargs):
|
def run_pexpect_side_effect(*args, **kwargs):
|
||||||
@@ -1745,19 +1777,23 @@ class TestInventoryUpdateCredentials(TestJobExecution):
|
|||||||
def test_openstack_source(self):
|
def test_openstack_source(self):
|
||||||
openstack = CredentialType.defaults['openstack']()
|
openstack = CredentialType.defaults['openstack']()
|
||||||
self.instance.source = 'openstack'
|
self.instance.source = 'openstack'
|
||||||
self.instance.credential = Credential(
|
|
||||||
pk=1,
|
def get_cred():
|
||||||
credential_type=openstack,
|
cred = Credential(
|
||||||
inputs = {
|
pk=1,
|
||||||
'username': 'bob',
|
credential_type=openstack,
|
||||||
'password': 'secret',
|
inputs = {
|
||||||
'project': 'tenant-name',
|
'username': 'bob',
|
||||||
'host': 'https://keystone.example.org'
|
'password': 'secret',
|
||||||
}
|
'project': 'tenant-name',
|
||||||
)
|
'host': 'https://keystone.example.org'
|
||||||
self.instance.credential.inputs['ssh_key_data'] = encrypt_field(
|
}
|
||||||
self.instance.credential, 'ssh_key_data'
|
)
|
||||||
)
|
cred.inputs['ssh_key_data'] = encrypt_field(
|
||||||
|
cred, 'ssh_key_data'
|
||||||
|
)
|
||||||
|
return cred
|
||||||
|
self.instance.get_cloud_credential = get_cred
|
||||||
|
|
||||||
def run_pexpect_side_effect(*args, **kwargs):
|
def run_pexpect_side_effect(*args, **kwargs):
|
||||||
args, cwd, env, stdout = args
|
args, cwd, env, stdout = args
|
||||||
@@ -1780,18 +1816,23 @@ class TestInventoryUpdateCredentials(TestJobExecution):
|
|||||||
def test_satellite6_source(self):
|
def test_satellite6_source(self):
|
||||||
satellite6 = CredentialType.defaults['satellite6']()
|
satellite6 = CredentialType.defaults['satellite6']()
|
||||||
self.instance.source = 'satellite6'
|
self.instance.source = 'satellite6'
|
||||||
self.instance.credential = Credential(
|
|
||||||
pk=1,
|
def get_cred():
|
||||||
credential_type=satellite6,
|
cred = Credential(
|
||||||
inputs = {
|
pk=1,
|
||||||
'username': 'bob',
|
credential_type=satellite6,
|
||||||
'password': 'secret',
|
inputs = {
|
||||||
'host': 'https://example.org'
|
'username': 'bob',
|
||||||
}
|
'password': 'secret',
|
||||||
)
|
'host': 'https://example.org'
|
||||||
self.instance.credential.inputs['password'] = encrypt_field(
|
}
|
||||||
self.instance.credential, 'password'
|
)
|
||||||
)
|
cred.inputs['password'] = encrypt_field(
|
||||||
|
cred, 'password'
|
||||||
|
)
|
||||||
|
return cred
|
||||||
|
self.instance.get_cloud_credential = get_cred
|
||||||
|
|
||||||
self.instance.source_vars = '{"satellite6_group_patterns": "[a,b,c]", "satellite6_group_prefix": "hey_"}'
|
self.instance.source_vars = '{"satellite6_group_patterns": "[a,b,c]", "satellite6_group_prefix": "hey_"}'
|
||||||
|
|
||||||
def run_pexpect_side_effect(*args, **kwargs):
|
def run_pexpect_side_effect(*args, **kwargs):
|
||||||
@@ -1811,18 +1852,22 @@ class TestInventoryUpdateCredentials(TestJobExecution):
|
|||||||
def test_cloudforms_source(self):
|
def test_cloudforms_source(self):
|
||||||
cloudforms = CredentialType.defaults['cloudforms']()
|
cloudforms = CredentialType.defaults['cloudforms']()
|
||||||
self.instance.source = 'cloudforms'
|
self.instance.source = 'cloudforms'
|
||||||
self.instance.credential = Credential(
|
|
||||||
pk=1,
|
def get_cred():
|
||||||
credential_type=cloudforms,
|
cred = Credential(
|
||||||
inputs = {
|
pk=1,
|
||||||
'username': 'bob',
|
credential_type=cloudforms,
|
||||||
'password': 'secret',
|
inputs = {
|
||||||
'host': 'https://example.org'
|
'username': 'bob',
|
||||||
}
|
'password': 'secret',
|
||||||
)
|
'host': 'https://example.org'
|
||||||
self.instance.credential.inputs['password'] = encrypt_field(
|
}
|
||||||
self.instance.credential, 'password'
|
)
|
||||||
)
|
cred.inputs['password'] = encrypt_field(
|
||||||
|
cred, 'password'
|
||||||
|
)
|
||||||
|
return cred
|
||||||
|
self.instance.get_cloud_credential = get_cred
|
||||||
|
|
||||||
self.instance.source_vars = '{"prefer_ipv4": True}'
|
self.instance.source_vars = '{"prefer_ipv4": True}'
|
||||||
|
|
||||||
@@ -1847,14 +1892,19 @@ class TestInventoryUpdateCredentials(TestJobExecution):
|
|||||||
def test_awx_task_env(self):
|
def test_awx_task_env(self):
|
||||||
gce = CredentialType.defaults['gce']()
|
gce = CredentialType.defaults['gce']()
|
||||||
self.instance.source = 'gce'
|
self.instance.source = 'gce'
|
||||||
self.instance.credential = Credential(
|
|
||||||
pk=1,
|
def get_cred():
|
||||||
credential_type=gce,
|
cred = Credential(
|
||||||
inputs = {
|
pk=1,
|
||||||
'username': 'bob',
|
credential_type=gce,
|
||||||
'project': 'some-project',
|
inputs = {
|
||||||
}
|
'username': 'bob',
|
||||||
)
|
'project': 'some-project',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return cred
|
||||||
|
self.instance.get_cloud_credential = get_cred
|
||||||
|
|
||||||
patch = mock.patch('awx.main.tasks.settings.AWX_TASK_ENV', {'FOO': 'BAR'})
|
patch = mock.patch('awx.main.tasks.settings.AWX_TASK_ENV', {'FOO': 'BAR'})
|
||||||
patch.start()
|
patch.start()
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ Multi-Credential Assignment
|
|||||||
===========================
|
===========================
|
||||||
|
|
||||||
awx has added support for assigning zero or more credentials to
|
awx has added support for assigning zero or more credentials to
|
||||||
a JobTemplate via a singular, unified interface.
|
JobTemplates and InventoryUpdates via a singular, unified interface.
|
||||||
|
|
||||||
Background
|
Background
|
||||||
----------
|
----------
|
||||||
@@ -220,3 +220,16 @@ POST /api/v2/job_templates/N/launch/
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Inventory Source Credentials
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
Inventory sources and inventory updates that they spawn also use the same
|
||||||
|
relationship. The new endpoints for this are
|
||||||
|
- `/api/v2/inventory_sources/N/credentials/` and
|
||||||
|
- `/api/v2/inventory_updates/N/credentials/`
|
||||||
|
|
||||||
|
Most cloud sources will continue to adhere to the constraint that they
|
||||||
|
must have a single credential that corresponds to their cloud type.
|
||||||
|
However, this relationship allows users to associate multiple vault
|
||||||
|
credentials of different ids to inventory sources.
|
||||||
|
|||||||
Reference in New Issue
Block a user