mirror of
https://github.com/ansible/awx.git
synced 2026-03-13 23:17:32 -02:30
Merge pull request #5974 from wwitzel3/issue-5741
Remove group AutoOneToOne from InventorySource
This commit is contained in:
@@ -1238,7 +1238,7 @@ class HostSerializer(BaseSerializerWithVariables):
|
||||
|
||||
|
||||
class GroupSerializer(BaseSerializerWithVariables):
|
||||
show_capabilities = ['start', 'copy', 'schedule', 'edit', 'delete']
|
||||
show_capabilities = ['copy', 'edit', 'delete']
|
||||
|
||||
class Meta:
|
||||
model = Group
|
||||
@@ -1270,8 +1270,6 @@ class GroupSerializer(BaseSerializerWithVariables):
|
||||
))
|
||||
if obj.inventory:
|
||||
res['inventory'] = self.reverse('api:inventory_detail', kwargs={'pk': obj.inventory.pk})
|
||||
if obj.inventory_source:
|
||||
res['inventory_source'] = self.reverse('api:inventory_source_detail', kwargs={'pk': obj.inventory_source.pk})
|
||||
return res
|
||||
|
||||
def validate_name(self, value):
|
||||
@@ -1429,13 +1427,12 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt
|
||||
status = serializers.ChoiceField(choices=InventorySource.INVENTORY_SOURCE_STATUS_CHOICES, read_only=True)
|
||||
last_update_failed = serializers.BooleanField(read_only=True)
|
||||
last_updated = serializers.DateTimeField(read_only=True)
|
||||
show_capabilities = ['start', 'schedule', 'edit', 'delete']
|
||||
|
||||
class Meta:
|
||||
model = InventorySource
|
||||
fields = ('*', 'inventory', 'group', 'update_on_launch',
|
||||
'update_cache_timeout') + \
|
||||
fields = ('*', 'name', 'inventory', 'update_on_launch', 'update_cache_timeout') + \
|
||||
('last_update_failed', 'last_updated') # Backwards compatibility.
|
||||
read_only_fields = ('*', 'name', 'inventory', 'group')
|
||||
|
||||
def get_related(self, obj):
|
||||
res = super(InventorySourceSerializer, self).get_related(obj)
|
||||
@@ -1452,8 +1449,6 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt
|
||||
))
|
||||
if obj.inventory:
|
||||
res['inventory'] = self.reverse('api:inventory_detail', kwargs={'pk': obj.inventory.pk})
|
||||
if obj.group:
|
||||
res['group'] = self.reverse('api:group_detail', kwargs={'pk': obj.group.pk})
|
||||
# Backwards compatibility.
|
||||
if obj.current_update:
|
||||
res['current_update'] = self.reverse('api:inventory_update_detail',
|
||||
@@ -1469,8 +1464,6 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt
|
||||
return ret
|
||||
if 'inventory' in ret and not obj.inventory:
|
||||
ret['inventory'] = None
|
||||
if 'group' in ret and not obj.group:
|
||||
ret['group'] = None
|
||||
return ret
|
||||
|
||||
|
||||
|
||||
@@ -2153,8 +2153,7 @@ class InventoryTreeView(RetrieveAPIView):
|
||||
group_children_map = inventory.get_group_children_map()
|
||||
root_group_pks = inventory.root_groups.order_by('name').values_list('pk', flat=True)
|
||||
groups_qs = inventory.groups
|
||||
groups_qs = groups_qs.select_related('inventory')
|
||||
groups_qs = groups_qs.prefetch_related('inventory_source')
|
||||
groups_qs = groups_qs.prefetch_related('inventory_sources')
|
||||
all_group_data = GroupSerializer(groups_qs, many=True).data
|
||||
all_group_data_map = dict((x['id'], x) for x in all_group_data)
|
||||
tree_data = [all_group_data_map[x] for x in root_group_pks]
|
||||
@@ -2164,22 +2163,17 @@ class InventoryTreeView(RetrieveAPIView):
|
||||
return Response(tree_data)
|
||||
|
||||
|
||||
class InventoryInventorySourcesList(SubListAPIView):
|
||||
class InventoryInventorySourcesList(SubListCreateAPIView):
|
||||
|
||||
view_name = _('Inventory Source List')
|
||||
|
||||
model = InventorySource
|
||||
serializer_class = InventorySourceSerializer
|
||||
parent_model = Inventory
|
||||
relationship = None # Not defined since using get_queryset().
|
||||
view_name = _('Inventory Source List')
|
||||
relationship = 'inventory_sources'
|
||||
parent_key = 'inventory'
|
||||
new_in_14 = True
|
||||
|
||||
def get_queryset(self):
|
||||
parent = self.get_parent_object()
|
||||
self.check_parent_access(parent)
|
||||
qs = self.request.user.get_queryset(self.model)
|
||||
return qs.filter(Q(inventory__pk=parent.pk) |
|
||||
Q(group__inventory__pk=parent.pk))
|
||||
|
||||
|
||||
class InventorySourceList(ListAPIView):
|
||||
|
||||
|
||||
@@ -346,10 +346,6 @@ class BaseAccess(object):
|
||||
elif display_method == 'copy' and isinstance(obj, WorkflowJobTemplate) and obj.organization_id is None:
|
||||
user_capabilities[display_method] = self.user.is_superuser
|
||||
continue
|
||||
elif display_method in ['start', 'schedule'] and isinstance(obj, Group):
|
||||
if obj.inventory_source and not obj.inventory_source._can_update():
|
||||
user_capabilities[display_method] = False
|
||||
continue
|
||||
elif display_method in ['start', 'schedule'] and isinstance(obj, (Project)):
|
||||
if obj.scm_type == '':
|
||||
user_capabilities[display_method] = False
|
||||
@@ -674,7 +670,7 @@ class GroupAccess(BaseAccess):
|
||||
def get_queryset(self):
|
||||
qs = Group.objects.filter(inventory__in=Inventory.accessible_objects(self.user, 'read_role'))
|
||||
qs = qs.select_related('created_by', 'modified_by', 'inventory')
|
||||
return qs.prefetch_related('parents', 'children', 'inventory_source').all()
|
||||
return qs.prefetch_related('parents', 'children').all()
|
||||
|
||||
def can_read(self, obj):
|
||||
return obj and self.user in obj.inventory.read_role
|
||||
@@ -724,12 +720,6 @@ class GroupAccess(BaseAccess):
|
||||
"active_jobs": active_jobs})
|
||||
return True
|
||||
|
||||
def can_start(self, obj, validate_license=True):
|
||||
# Used as another alias to inventory_source start access for user_capabilities
|
||||
if obj and obj.inventory_source:
|
||||
return self.user.can_access(InventorySource, 'start', obj.inventory_source, validate_license=validate_license)
|
||||
return False
|
||||
|
||||
|
||||
class InventorySourceAccess(BaseAccess):
|
||||
'''
|
||||
@@ -741,28 +731,27 @@ class InventorySourceAccess(BaseAccess):
|
||||
|
||||
def get_queryset(self):
|
||||
qs = self.model.objects.all()
|
||||
qs = qs.select_related('created_by', 'modified_by', 'group', 'inventory')
|
||||
qs = qs.select_related('created_by', 'modified_by', 'inventory')
|
||||
inventory_ids = self.user.get_queryset(Inventory)
|
||||
return qs.filter(Q(inventory_id__in=inventory_ids) |
|
||||
Q(group__inventory_id__in=inventory_ids))
|
||||
return qs.filter(Q(inventory_id__in=inventory_ids))
|
||||
|
||||
def can_read(self, obj):
|
||||
if obj and obj.group:
|
||||
return self.user.can_access(Group, 'read', obj.group)
|
||||
elif obj and obj.inventory:
|
||||
if obj and obj.inventory:
|
||||
return self.user.can_access(Inventory, 'read', obj.inventory)
|
||||
else:
|
||||
return False
|
||||
|
||||
def can_add(self, data):
|
||||
# Automatically created from group or management command.
|
||||
return False
|
||||
if not data or 'inventory' not in data:
|
||||
return False
|
||||
# Checks for admin or change permission on inventory.
|
||||
return self.check_related('inventory', Inventory, data)
|
||||
|
||||
def can_change(self, obj, data):
|
||||
# Checks for admin or change permission on group.
|
||||
if obj and obj.group:
|
||||
if obj and obj.inventory:
|
||||
return (
|
||||
self.user.can_access(Group, 'change', obj.group, None) and
|
||||
self.user.can_access(Inventory, 'change', obj.inventory, None) and
|
||||
self.check_related('credential', Credential, data, obj=obj, role_field='use_role')
|
||||
)
|
||||
# Can't change inventory sources attached to only the inventory, since
|
||||
@@ -771,9 +760,7 @@ class InventorySourceAccess(BaseAccess):
|
||||
return False
|
||||
|
||||
def can_start(self, obj, validate_license=True):
|
||||
if obj and obj.group:
|
||||
return obj.can_update and self.user in obj.group.inventory.update_role
|
||||
elif obj and obj.inventory:
|
||||
if obj and obj.inventory:
|
||||
return obj.can_update and self.user in obj.inventory.update_role
|
||||
return False
|
||||
|
||||
@@ -789,8 +776,7 @@ class InventoryUpdateAccess(BaseAccess):
|
||||
|
||||
def get_queryset(self):
|
||||
qs = InventoryUpdate.objects.distinct()
|
||||
qs = qs.select_related('created_by', 'modified_by', 'inventory_source__group',
|
||||
'inventory_source__inventory')
|
||||
qs = qs.select_related('created_by', 'modified_by', 'inventory_source__inventory')
|
||||
inventory_sources_qs = self.user.get_queryset(InventorySource)
|
||||
return qs.filter(inventory_source__in=inventory_sources_qs)
|
||||
|
||||
|
||||
@@ -19,6 +19,24 @@ class Migration(migrations.Migration):
|
||||
]
|
||||
|
||||
operations = [
|
||||
# Inventory Refresh
|
||||
migrations.RenameField(
|
||||
'InventorySource',
|
||||
'group',
|
||||
'deprecated_group'
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventorysource',
|
||||
name='deprecated_group',
|
||||
field=models.ForeignKey(related_name='deprecated_inventory_source', default=None, null=True, to='main.Group'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventorysource',
|
||||
name='inventory',
|
||||
field=models.ForeignKey(related_name='inventory_sources', default=None, to='main.Inventory', null=True),
|
||||
),
|
||||
|
||||
# Facts Latest
|
||||
migrations.CreateModel(
|
||||
name='FactLatest',
|
||||
fields=[
|
||||
25
awx/main/migrations/0038_v320_data_migrations.py
Normal file
25
awx/main/migrations/0038_v320_data_migrations.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Python
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# Django
|
||||
from django.db import migrations
|
||||
|
||||
# AWX
|
||||
from awx.main.migrations import _inventory_source as invsrc
|
||||
from awx.main.migrations import _migration_utils as migration_utils
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('main', '0037_v320_release'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# Inventory Refresh
|
||||
migrations.RunPython(migration_utils.set_current_apps_for_migrations),
|
||||
migrations.RunPython(invsrc.remove_manual_inventory_sources),
|
||||
migrations.RunPython(invsrc.remove_inventory_source_with_no_inventory_link),
|
||||
migrations.RunPython(invsrc.rename_inventory_sources),
|
||||
]
|
||||
44
awx/main/migrations/_inventory_source.py
Normal file
44
awx/main/migrations/_inventory_source.py
Normal file
@@ -0,0 +1,44 @@
|
||||
import logging
|
||||
|
||||
from django.db.models import Q
|
||||
|
||||
logger = logging.getLogger('awx.main.migrations')
|
||||
|
||||
|
||||
def remove_manual_inventory_sources(apps, schema_editor):
|
||||
'''Previously we would automatically create inventory sources after
|
||||
Group creation and we would use the parent Group as our interface for the user.
|
||||
During that process we would create InventorySource that had a source of "manual".
|
||||
'''
|
||||
InventorySource = apps.get_model('main', 'InventorySource')
|
||||
# see models/inventory.py SOURCE_CHOICES - ('', _('Manual'))
|
||||
logger.debug("Removing all Manual InventorySource from database.")
|
||||
InventorySource.objects.filter(source='').delete()
|
||||
|
||||
|
||||
def rename_inventory_sources(apps, schema_editor):
|
||||
'''Rename existing InventorySource entries using the following format.
|
||||
{{ inventory_source.name }} - {{ inventory.module }} - {{ number }}
|
||||
The number will be incremented for each InventorySource for the organization.
|
||||
'''
|
||||
Organization = apps.get_model('main', 'Organization')
|
||||
InventorySource = apps.get_model('main', 'InventorySource')
|
||||
|
||||
for org in Organization.objects.iterator():
|
||||
for i, invsrc in enumerate(InventorySource.objects.filter(Q(inventory__organization=org) |
|
||||
Q(deprecated_group__inventory__organization=org)).distinct().all()):
|
||||
|
||||
inventory = invsrc.deprecated_group.inventory if invsrc.deprecated_group else invsrc.inventory
|
||||
name = '{0} - {1} - {2}'.format(invsrc.name, inventory.name, i)
|
||||
logger.debug("Renaming InventorySource({0}) {1} -> {2}".format(invsrc.pk, invsrc.name, name))
|
||||
invsrc.name = name
|
||||
invsrc.save()
|
||||
|
||||
|
||||
def remove_inventory_source_with_no_inventory_link(apps, schema_editor):
|
||||
'''If we cannot determine the Inventory for which an InventorySource exists
|
||||
we can safely remove it.
|
||||
'''
|
||||
InventorySource = apps.get_model('main', 'InventorySource')
|
||||
logger.debug("Removing all InventorySource that have no link to an Inventory from database.")
|
||||
InventorySource.objects.filter(Q(inventory__organization=None) & Q(deprecated_group__inventory=None)).delete()
|
||||
@@ -19,7 +19,7 @@ from django.utils.timezone import now
|
||||
# AWX
|
||||
from awx.api.versioning import reverse
|
||||
from awx.main.constants import CLOUD_PROVIDERS
|
||||
from awx.main.fields import AutoOneToOneField, ImplicitRoleField
|
||||
from awx.main.fields import ImplicitRoleField
|
||||
from awx.main.managers import HostManager
|
||||
from awx.main.models.base import * # noqa
|
||||
from awx.main.models.unified_jobs import * # noqa
|
||||
@@ -1060,17 +1060,17 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions):
|
||||
related_name='inventory_sources',
|
||||
null=True,
|
||||
default=None,
|
||||
editable=False,
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
group = AutoOneToOneField(
|
||||
|
||||
deprecated_group = models.ForeignKey(
|
||||
'Group',
|
||||
related_name='inventory_source',
|
||||
related_name='deprecated_inventory_source',
|
||||
null=True,
|
||||
default=None,
|
||||
editable=False,
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
|
||||
update_on_launch = models.BooleanField(
|
||||
default=False,
|
||||
)
|
||||
@@ -1092,20 +1092,12 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions):
|
||||
# If update_fields has been specified, add our field names to it,
|
||||
# if it hasn't been specified, then we're just doing a normal save.
|
||||
update_fields = kwargs.get('update_fields', [])
|
||||
# Update inventory from group (if available).
|
||||
if self.group and not self.inventory:
|
||||
self.inventory = self.group.inventory
|
||||
if 'inventory' not in update_fields:
|
||||
update_fields.append('inventory')
|
||||
|
||||
# Set name automatically. Include PK (or placeholder) to make sure the names are always unique.
|
||||
replace_text = '__replace_%s__' % now()
|
||||
old_name_re = re.compile(r'^inventory_source \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.*?$')
|
||||
if not self.name or old_name_re.match(self.name) or '__replace_' in self.name:
|
||||
if self.inventory and self.group and self.pk:
|
||||
self.name = '%s (%s - %s)' % (self.group.name, self.inventory.name, self.pk)
|
||||
elif self.inventory and self.group:
|
||||
self.name = '%s (%s - %s)' % (self.group.name, self.inventory.name, replace_text)
|
||||
elif self.inventory and self.pk:
|
||||
if self.inventory and self.pk:
|
||||
self.name = '%s (%s)' % (self.inventory.name, self.pk)
|
||||
elif self.inventory:
|
||||
self.name = '%s (%s)' % (self.inventory.name, replace_text)
|
||||
@@ -1122,7 +1114,8 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions):
|
||||
self.name = self.name.replace(replace_text, str(self.pk))
|
||||
super(InventorySource, self).save(update_fields=['name'])
|
||||
if not getattr(_inventory_updates, 'is_updating', False):
|
||||
self.inventory.update_computed_fields(update_groups=False, update_hosts=False)
|
||||
if self.inventory is not None:
|
||||
self.inventory.update_computed_fields(update_groups=False, update_hosts=False)
|
||||
|
||||
def _get_current_status(self):
|
||||
if self.source:
|
||||
@@ -1185,16 +1178,6 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions):
|
||||
success=list(success_notification_templates),
|
||||
any=list(any_notification_templates))
|
||||
|
||||
def clean_source(self):
|
||||
source = self.source
|
||||
if source and self.group:
|
||||
qs = self.group.inventory_sources.filter(source__in=CLOUD_INVENTORY_SOURCES)
|
||||
existing_sources = qs.exclude(pk=self.pk)
|
||||
if existing_sources.count():
|
||||
s = u', '.join([x.group.name for x in existing_sources])
|
||||
raise ValidationError(_('Unable to configure this item for cloud sync. It is already managed by %s.') % s)
|
||||
return source
|
||||
|
||||
|
||||
class InventoryUpdate(UnifiedJob, InventorySourceOptions, JobNotificationMixin):
|
||||
'''
|
||||
@@ -1229,18 +1212,13 @@ class InventoryUpdate(UnifiedJob, InventorySourceOptions, JobNotificationMixin):
|
||||
|
||||
def websocket_emit_data(self):
|
||||
websocket_data = super(InventoryUpdate, self).websocket_emit_data()
|
||||
if self.inventory_source.group is not None:
|
||||
websocket_data.update(dict(group_id=self.inventory_source.group.id))
|
||||
return websocket_data
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
update_fields = kwargs.get('update_fields', [])
|
||||
inventory_source = self.inventory_source
|
||||
if inventory_source.inventory and self.name == inventory_source.name:
|
||||
if inventory_source.group:
|
||||
self.name = '%s (%s)' % (inventory_source.group.name, inventory_source.inventory.name)
|
||||
else:
|
||||
self.name = inventory_source.inventory.name
|
||||
self.name = inventory_source.inventory.name
|
||||
if 'name' not in update_fields:
|
||||
update_fields.append('name')
|
||||
super(InventoryUpdate, self).save(*args, **kwargs)
|
||||
|
||||
@@ -372,9 +372,6 @@ model_serializer_mapping = {
|
||||
|
||||
def activity_stream_create(sender, instance, created, **kwargs):
|
||||
if created and activity_stream_enabled:
|
||||
# Skip recording any inventory source directly associated with a group.
|
||||
if isinstance(instance, InventorySource) and instance.group:
|
||||
return
|
||||
object1 = camelcase_to_underscore(instance.__class__.__name__)
|
||||
changes = model_to_dict(instance, model_serializer_mapping)
|
||||
# Special case where Job survey password variables need to be hidden
|
||||
@@ -420,9 +417,6 @@ def activity_stream_update(sender, instance, **kwargs):
|
||||
def activity_stream_delete(sender, instance, **kwargs):
|
||||
if not activity_stream_enabled:
|
||||
return
|
||||
# Skip recording any inventory source directly associated with a group.
|
||||
if isinstance(instance, InventorySource) and instance.group:
|
||||
return
|
||||
changes = model_to_dict(instance)
|
||||
object1 = camelcase_to_underscore(instance.__class__.__name__)
|
||||
activity_entry = ActivityStream(
|
||||
|
||||
@@ -4,17 +4,19 @@ from awx.api.versioning import reverse
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_inventory_source_notification_on_cloud_only(get, post, group_factory, user, notification_template):
|
||||
def test_inventory_source_notification_on_cloud_only(get, post, inventory_source_factory, user, notification_template):
|
||||
u = user('admin', True)
|
||||
g_cloud = group_factory('cloud')
|
||||
g_not = group_factory('not_cloud')
|
||||
cloud_is = g_cloud.inventory_source
|
||||
not_is = g_not.inventory_source
|
||||
cloud_is.source = 'ec2'
|
||||
|
||||
cloud_is = inventory_source_factory("ec2")
|
||||
cloud_is.source = "ec2"
|
||||
cloud_is.save()
|
||||
|
||||
not_is = inventory_source_factory("not_ec2")
|
||||
|
||||
url = reverse('api:inventory_source_notification_templates_any_list', kwargs={'pk': cloud_is.id})
|
||||
response = post(url, dict(id=notification_template.id), u)
|
||||
assert response.status_code == 204
|
||||
|
||||
url = reverse('api:inventory_source_notification_templates_success_list', kwargs={'pk': not_is.id})
|
||||
response = post(url, dict(id=notification_template.id), u)
|
||||
assert response.status_code == 400
|
||||
@@ -95,6 +97,21 @@ def test_edit_inventory_group(put, group, alice, role_field, expected_status_cod
|
||||
put(reverse('api:group_detail', kwargs={'pk': group.id}), data, alice, expect=expected_status_code)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("role_field,expected_status_code", [
|
||||
(None, 403),
|
||||
('admin_role', 201),
|
||||
('update_role', 403),
|
||||
('adhoc_role', 403),
|
||||
('use_role', 403)
|
||||
])
|
||||
@pytest.mark.django_db
|
||||
def test_create_inventory_inventory_source(post, inventory, alice, role_field, expected_status_code):
|
||||
data = { 'source': 'ec2', 'name': 'ec2-inv-source'}
|
||||
if role_field:
|
||||
getattr(inventory, role_field).members.add(alice)
|
||||
post(reverse('api:inventory_inventory_sources_list', kwargs={'pk': inventory.id}), data, alice, expect=expected_status_code)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("role_field,expected_status_code", [
|
||||
(None, 403),
|
||||
('admin_role', 204),
|
||||
@@ -178,5 +195,5 @@ def test_delete_inventory_host(delete, host, alice, role_field, expected_status_
|
||||
@pytest.mark.django_db
|
||||
def test_inventory_source_update(post, inventory_source, alice, role_field, expected_status_code):
|
||||
if role_field:
|
||||
getattr(inventory_source.group.inventory, role_field).members.add(alice)
|
||||
getattr(inventory_source.inventory, role_field).members.add(alice)
|
||||
post(reverse('api:inventory_source_update_view', kwargs={'pk': inventory_source.id}), {}, alice, expect=expected_status_code)
|
||||
|
||||
@@ -4,10 +4,7 @@ from awx.api.versioning import reverse
|
||||
from django.test.client import RequestFactory
|
||||
|
||||
from awx.main.models import Role, Group, UnifiedJobTemplate, JobTemplate
|
||||
from awx.main.access import (
|
||||
access_registry,
|
||||
get_user_capabilities
|
||||
)
|
||||
from awx.main.access import access_registry
|
||||
from awx.main.utils import cache_list_capabilities
|
||||
from awx.api.serializers import JobTemplateSerializer
|
||||
|
||||
@@ -186,7 +183,7 @@ class TestAccessListCapabilities:
|
||||
"Establish that exactly 1 type of access exists so we know the entry is the right one"
|
||||
assert len(data['results']) == 1
|
||||
assert len(data['results'][0]['summary_fields'][sublist]) == 1
|
||||
|
||||
|
||||
def test_access_list_direct_access_capability(
|
||||
self, inventory, rando, get, mocker, mock_access_method):
|
||||
inventory.admin_role.members.add(rando)
|
||||
@@ -341,28 +338,6 @@ def test_manual_projects_no_update(project, get, admin_user):
|
||||
assert not response.data['summary_fields']['user_capabilities']['schedule']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_group_update_capabilities_possible(group, inventory_source, admin_user):
|
||||
group.inventory_source = inventory_source
|
||||
group.save()
|
||||
|
||||
capabilities = get_user_capabilities(admin_user, group, method_list=['start'])
|
||||
assert capabilities['start']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_group_update_capabilities_impossible(group, inventory_source, admin_user):
|
||||
"Manual groups can not be updated or scheduled"
|
||||
inventory_source.source = ""
|
||||
inventory_source.save()
|
||||
group.inventory_source = inventory_source
|
||||
group.save()
|
||||
|
||||
capabilities = get_user_capabilities(admin_user, group, method_list=['edit', 'start', 'schedule'])
|
||||
assert not capabilities['start']
|
||||
assert not capabilities['schedule']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_license_check_not_called(mocker, job_template, project, org_admin, get):
|
||||
job_template.project = project
|
||||
|
||||
@@ -342,11 +342,25 @@ def group(inventory):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def inventory_source(group, inventory):
|
||||
return InventorySource.objects.create(name=group.name, group=group,
|
||||
def inventory_source(inventory):
|
||||
return InventorySource.objects.create(name='single-inv-src',
|
||||
inventory=inventory, source='gce')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def inventory_source_factory(inventory_factory):
|
||||
def invsrc(name, source=None, inventory=None):
|
||||
if inventory is None:
|
||||
inventory = inventory_factory("inv-is-%s" % name)
|
||||
if source is None:
|
||||
source = 'file'
|
||||
try:
|
||||
return inventory.inventory_sources.get(name=name)
|
||||
except:
|
||||
return inventory.inventory_sources.create(name=name, source=source)
|
||||
return invsrc
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def inventory_update(inventory_source):
|
||||
return InventoryUpdate.objects.create(inventory_source=inventory_source)
|
||||
|
||||
45
awx/main/tests/functional/test_inventory_source_migration.py
Normal file
45
awx/main/tests/functional/test_inventory_source_migration.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import pytest
|
||||
|
||||
from awx.main.migrations import _inventory_source as invsrc
|
||||
from awx.main.models import InventorySource
|
||||
|
||||
from django.apps import apps
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_inv_src_manual_removal(inventory_source):
|
||||
inventory_source.source = ''
|
||||
inventory_source.save()
|
||||
|
||||
assert InventorySource.objects.filter(pk=inventory_source.pk).exists()
|
||||
invsrc.remove_manual_inventory_sources(apps, None)
|
||||
assert not InventorySource.objects.filter(pk=inventory_source.pk).exists()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_inv_src_rename(inventory_source_factory):
|
||||
inv_src01 = inventory_source_factory('t1')
|
||||
|
||||
invsrc.rename_inventory_sources(apps, None)
|
||||
|
||||
inv_src01.refresh_from_db()
|
||||
# inv-is-t1 is generated in the inventory_source_factory
|
||||
assert inv_src01.name == 't1 - inv-is-t1 - 0'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_inv_src_nolink_removal(inventory_source_factory):
|
||||
inventory_source_factory('t1')
|
||||
inv_src02 = inventory_source_factory('t2')
|
||||
|
||||
inv_src02.inventory = None
|
||||
inv_src02.deprecated_group = None
|
||||
inv_src02.save()
|
||||
|
||||
assert InventorySource.objects.count() == 2
|
||||
|
||||
invsrc.remove_inventory_source_with_no_inventory_link(apps, None)
|
||||
|
||||
objs = InventorySource.objects.all()
|
||||
assert len(objs) == 1
|
||||
assert 't1' in objs[0].name
|
||||
@@ -3,7 +3,7 @@ import pytest
|
||||
|
||||
from awx.api.versioning import reverse
|
||||
from awx.main.models.notifications import NotificationTemplate, Notification
|
||||
from awx.main.models.inventory import Inventory, Group
|
||||
from awx.main.models.inventory import Inventory, InventorySource
|
||||
from awx.main.models.jobs import JobTemplate
|
||||
|
||||
|
||||
@@ -84,8 +84,8 @@ def test_inherited_notification_templates(get, post, user, organization, project
|
||||
notification_templates.append(response.data['id'])
|
||||
i = Inventory.objects.create(name='test', organization=organization)
|
||||
i.save()
|
||||
g = Group.objects.create(name='test', inventory=i)
|
||||
g.save()
|
||||
isrc = InventorySource.objects.create(name='test', inventory=i)
|
||||
isrc.save()
|
||||
jt = JobTemplate.objects.create(name='test', inventory=i, project=project, playbook='debug.yml')
|
||||
jt.save()
|
||||
url = reverse('api:organization_notification_templates_any_list', kwargs={'pk': organization.id})
|
||||
@@ -99,7 +99,7 @@ def test_inherited_notification_templates(get, post, user, organization, project
|
||||
assert response.status_code == 204
|
||||
assert len(jt.notification_templates['any']) == 3
|
||||
assert len(project.notification_templates['any']) == 2
|
||||
assert len(g.inventory_source.notification_templates['any']) == 1
|
||||
assert len(isrc.notification_templates['any']) == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
||||
@@ -33,7 +33,7 @@ class TestProjectUpdateLatestDictDict():
|
||||
pu = ProjectUpdate.objects.create(project=p, status='successful', finished=tz_now() - timedelta(seconds=20))
|
||||
|
||||
return (p, pu)
|
||||
|
||||
|
||||
# Failed project updates newer than successful ones
|
||||
@pytest.fixture
|
||||
def multiple_project_updates(self):
|
||||
@@ -42,9 +42,9 @@ class TestProjectUpdateLatestDictDict():
|
||||
epoch = tz_now()
|
||||
|
||||
successful_pus = [ProjectUpdate.objects.create(project=p,
|
||||
status='successful',
|
||||
status='successful',
|
||||
finished=epoch - timedelta(seconds=100 + i)) for i in xrange(0, 5)]
|
||||
failed_pus = [ProjectUpdate.objects.create(project=p,
|
||||
failed_pus = [ProjectUpdate.objects.create(project=p,
|
||||
status='failed',
|
||||
finished=epoch - timedelta(seconds=100 - len(successful_pus) + i)) for i in xrange(0, 5)]
|
||||
return (p, failed_pus, successful_pus)
|
||||
@@ -73,9 +73,8 @@ class TestInventoryUpdateDict():
|
||||
@pytest.fixture
|
||||
def waiting_inventory_update(self, org):
|
||||
i = Inventory.objects.create(name='inv1', organization=org)
|
||||
g = Group.objects.create(name='group1', inventory=i)
|
||||
#Inventory.groups.add(g)
|
||||
inv_src = InventorySource.objects.create(group=g)
|
||||
Group.objects.create(name='group1', inventory=i)
|
||||
inv_src = InventorySource.objects.create(inventory=i)
|
||||
iu = InventoryUpdate.objects.create(inventory_source=inv_src, status='waiting')
|
||||
return iu
|
||||
|
||||
@@ -96,13 +95,13 @@ class TestInventoryUpdateLatestDict():
|
||||
|
||||
@pytest.fixture
|
||||
def inventory_updates(self, inventory):
|
||||
g1 = Group.objects.create(name='group1', inventory=inventory)
|
||||
g2 = Group.objects.create(name='group2', inventory=inventory)
|
||||
g3 = Group.objects.create(name='group3', inventory=inventory)
|
||||
Group.objects.create(name='group1', inventory=inventory)
|
||||
Group.objects.create(name='group2', inventory=inventory)
|
||||
Group.objects.create(name='group3', inventory=inventory)
|
||||
|
||||
inv_src1 = InventorySource.objects.create(group=g1, update_on_launch=True, inventory=inventory)
|
||||
inv_src2 = InventorySource.objects.create(group=g2, update_on_launch=False, inventory=inventory)
|
||||
inv_src3 = InventorySource.objects.create(group=g3, update_on_launch=True, inventory=inventory)
|
||||
inv_src1 = InventorySource.objects.create(update_on_launch=True, inventory=inventory)
|
||||
inv_src2 = InventorySource.objects.create(update_on_launch=False, inventory=inventory)
|
||||
inv_src3 = InventorySource.objects.create(update_on_launch=True, inventory=inventory)
|
||||
|
||||
import time
|
||||
iu1 = InventoryUpdate.objects.create(inventory_source=inv_src1, status='successful')
|
||||
@@ -114,7 +113,7 @@ class TestInventoryUpdateLatestDict():
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_filter_partial(self, inventory, inventory_updates):
|
||||
|
||||
|
||||
tasks = InventoryUpdateLatestDict.filter_partial([inventory.id])
|
||||
|
||||
inventory_updates_expected = [inventory_updates[0], inventory_updates[2]]
|
||||
@@ -123,4 +122,4 @@ class TestInventoryUpdateLatestDict():
|
||||
task_ids = [task['id'] for task in tasks]
|
||||
for inventory_update in inventory_updates_expected:
|
||||
inventory_update.id in task_ids
|
||||
|
||||
|
||||
|
||||
@@ -276,7 +276,7 @@ def test_host_access(organization, inventory, group, user, group_factory):
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_inventory_source_credential_check(rando, inventory_source, credential):
|
||||
inventory_source.group.inventory.admin_role.members.add(rando)
|
||||
inventory_source.inventory.admin_role.members.add(rando)
|
||||
access = InventorySourceAccess(rando)
|
||||
assert not access.can_change(inventory_source, {'credential': credential})
|
||||
|
||||
|
||||
@@ -293,498 +293,6 @@ class InventoryTest(BaseTest):
|
||||
organization=self.organizations[0].id)
|
||||
self.post(inventory_scripts, data=failed_no_shebang, expect=400, auth=self.get_super_credentials())
|
||||
|
||||
def test_main_line(self):
|
||||
# some basic URLs...
|
||||
reverse('api:inventory_list')
|
||||
reverse('api:inventory_detail', args=(self.inventory_a.pk,))
|
||||
reverse('api:inventory_detail', args=(self.inventory_b.pk,))
|
||||
hosts = reverse('api:host_list')
|
||||
groups = reverse('api:group_list')
|
||||
self.create_test_license_file()
|
||||
|
||||
# a super user can add hosts (but inventory ID is required)
|
||||
inv = Inventory.objects.create(
|
||||
name = 'test inventory',
|
||||
organization = self.organizations[0]
|
||||
)
|
||||
invalid = dict(name='asdf0.example.com')
|
||||
new_host_a = dict(name=u'asdf\u0162.example.com:1022', inventory=inv.pk)
|
||||
new_host_b = dict(name='asdf1.example.com', inventory=inv.pk)
|
||||
new_host_c = dict(name='127.1.2.3:2022', inventory=inv.pk,
|
||||
variables=json.dumps({'who': 'what?'}))
|
||||
new_host_d = dict(name='asdf3.example.com', inventory=inv.pk)
|
||||
new_host_e = dict(name=u'asdf4.example.com:\u0162', inventory=inv.pk)
|
||||
host_data0 = self.post(hosts, data=invalid, expect=400, auth=self.get_super_credentials())
|
||||
host_data0 = self.post(hosts, data=new_host_a, expect=201, auth=self.get_super_credentials())
|
||||
# Port should be split out into host variables.
|
||||
host_a = Host.objects.get(pk=host_data0['id'])
|
||||
self.assertEqual(host_a.name, u'asdf\u0162.example.com')
|
||||
self.assertEqual(host_a.variables_dict, {'ansible_ssh_port': 1022})
|
||||
|
||||
# an org admin can add hosts (try first with invalid port #).
|
||||
self.post(hosts, data=new_host_e, expect=400, auth=self.get_normal_credentials())
|
||||
new_host_e['name'] = u'asdf4.example.com'
|
||||
self.post(hosts, data=new_host_e, expect=201, auth=self.get_normal_credentials())
|
||||
|
||||
# a normal user cannot add hosts
|
||||
self.post(hosts, data=new_host_b, expect=403, auth=self.get_nobody_credentials())
|
||||
|
||||
# a normal user with inventory edit permissions (on any inventory) can create hosts
|
||||
|
||||
inv.admin_role.members.add(self.other_django_user)
|
||||
host_data3 = self.post(hosts, data=new_host_c, expect=201, auth=self.get_other_credentials())
|
||||
|
||||
# Port should be split out into host variables, other variables kept intact.
|
||||
host_c = Host.objects.get(pk=host_data3['id'])
|
||||
self.assertEqual(host_c.name, '127.1.2.3')
|
||||
self.assertEqual(host_c.variables_dict, {'ansible_ssh_port': 2022, 'who': 'what?'})
|
||||
|
||||
# hostnames must be unique inside an organization
|
||||
self.post(hosts, data=new_host_c, expect=400, auth=self.get_other_credentials())
|
||||
|
||||
# Verify we can update host via PUT.
|
||||
host_url3 = host_data3['url']
|
||||
host_data3['variables'] = ''
|
||||
host_data3 = self.put(host_url3, data=host_data3, expect=200, auth=self.get_other_credentials())
|
||||
self.assertEqual(Host.objects.get(id=host_data3['id']).variables, '')
|
||||
self.assertEqual(Host.objects.get(id=host_data3['id']).variables_dict, {})
|
||||
|
||||
# Should reject invalid data.
|
||||
host_data3['variables'] = 'foo: [bar'
|
||||
self.put(host_url3, data=host_data3, expect=400, auth=self.get_other_credentials())
|
||||
|
||||
# Should accept valid JSON or YAML.
|
||||
host_data3['variables'] = 'bad: monkey'
|
||||
self.put(host_url3, data=host_data3, expect=200, auth=self.get_other_credentials())
|
||||
self.assertEqual(Host.objects.get(id=host_data3['id']).variables, host_data3['variables'])
|
||||
self.assertEqual(Host.objects.get(id=host_data3['id']).variables_dict, {'bad': 'monkey'})
|
||||
|
||||
host_data3['variables'] = '{"angry": "penguin"}'
|
||||
self.put(host_url3, data=host_data3, expect=200, auth=self.get_other_credentials())
|
||||
self.assertEqual(Host.objects.get(id=host_data3['id']).variables, host_data3['variables'])
|
||||
self.assertEqual(Host.objects.get(id=host_data3['id']).variables_dict, {'angry': 'penguin'})
|
||||
|
||||
###########################################
|
||||
# GROUPS
|
||||
|
||||
invalid = dict(name='web1')
|
||||
new_group_a = dict(name='web2', inventory=inv.pk)
|
||||
new_group_b = dict(name='web3', inventory=inv.pk)
|
||||
new_group_c = dict(name='web4', inventory=inv.pk)
|
||||
new_group_d = dict(name='web5', inventory=inv.pk)
|
||||
new_group_e = dict(name='web6', inventory=inv.pk)
|
||||
groups = reverse('api:group_list')
|
||||
|
||||
self.post(groups, data=invalid, expect=400, auth=self.get_super_credentials())
|
||||
self.post(groups, data=new_group_a, expect=201, auth=self.get_super_credentials())
|
||||
|
||||
# an org admin can add groups
|
||||
self.post(groups, data=new_group_e, expect=201, auth=self.get_normal_credentials())
|
||||
|
||||
# a normal user cannot add groups
|
||||
self.post(groups, data=new_group_b, expect=403, auth=self.get_nobody_credentials())
|
||||
|
||||
# a normal user with inventory edit permissions (on any inventory) can create groups
|
||||
# already done!
|
||||
self.post(groups, data=new_group_c, expect=201, auth=self.get_other_credentials())
|
||||
|
||||
# hostnames must be unique inside an organization
|
||||
self.post(groups, data=new_group_c, expect=400, auth=self.get_other_credentials())
|
||||
|
||||
# Check that we don't allow creating reserved group names.
|
||||
data = dict(name='all', inventory=inv.pk)
|
||||
with self.current_user(self.super_django_user):
|
||||
self.post(groups, data=data, expect=400)
|
||||
data = dict(name='_meta', inventory=inv.pk)
|
||||
with self.current_user(self.super_django_user):
|
||||
self.post(groups, data=data, expect=400)
|
||||
|
||||
# A new group should not be able to be added a removed group
|
||||
del_group = inv.groups.create(name='del')
|
||||
inv.groups.create(name='nondel')
|
||||
del_children_url = reverse('api:group_children_list', args=(del_group.pk,))
|
||||
nondel_url = reverse('api:group_detail',
|
||||
args=(Group.objects.get(name='nondel').pk,))
|
||||
assert self.normal_django_user in inv.read_role
|
||||
del_group.delete()
|
||||
nondel_detail = self.get(nondel_url, expect=200, auth=self.get_normal_credentials())
|
||||
self.post(del_children_url, data=nondel_detail, expect=400, auth=self.get_normal_credentials())
|
||||
|
||||
|
||||
#################################################
|
||||
# HOSTS->inventories POST via subcollection
|
||||
url = reverse('api:inventory_hosts_list', args=(self.inventory_a.pk,))
|
||||
new_host_a = dict(name='web100.example.com')
|
||||
new_host_b = dict(name='web101.example.com')
|
||||
new_host_c = dict(name='web102.example.com')
|
||||
new_host_d = dict(name='web103.example.com')
|
||||
new_host_e = dict(name='web104.example.com')
|
||||
|
||||
# a super user can associate hosts with inventories
|
||||
added_by_collection_a = self.post(url, data=new_host_a, expect=201, auth=self.get_super_credentials())
|
||||
|
||||
# an org admin can associate hosts with inventories
|
||||
self.post(url, data=new_host_b, expect=201, auth=self.get_normal_credentials())
|
||||
|
||||
# a normal user cannot associate hosts with inventories
|
||||
self.post(url, data=new_host_c, expect=403, auth=self.get_nobody_credentials())
|
||||
|
||||
# a normal user with edit permission on the inventory can associate hosts with inventories
|
||||
url5 = reverse('api:inventory_hosts_list', args=(inv.pk,))
|
||||
added_by_collection_d = self.post(url5, data=new_host_d, expect=201, auth=self.get_other_credentials())
|
||||
got = self.get(url5, expect=200, auth=self.get_other_credentials())
|
||||
self.assertEquals(got['count'], 4)
|
||||
|
||||
# now remove the host from inventory (still keeps the record)
|
||||
added_by_collection_d['disassociate'] = 1
|
||||
self.post(url5, data=added_by_collection_d, expect=204, auth=self.get_other_credentials())
|
||||
got = self.get(url5, expect=200, auth=self.get_other_credentials())
|
||||
self.assertEquals(got['count'], 3)
|
||||
|
||||
|
||||
##################################################
|
||||
# GROUPS->inventories POST via subcollection
|
||||
|
||||
root_groups = reverse('api:inventory_root_groups_list', args=(self.inventory_a.pk,))
|
||||
|
||||
url = reverse('api:inventory_groups_list', args=(self.inventory_a.pk,))
|
||||
new_group_a = dict(name='web100')
|
||||
new_group_b = dict(name='web101')
|
||||
new_group_c = dict(name='web102')
|
||||
new_group_d = dict(name='web103')
|
||||
new_group_e = dict(name='web104')
|
||||
|
||||
# a super user can associate groups with inventories
|
||||
added_by_collection = self.post(url, data=new_group_a, expect=201, auth=self.get_super_credentials())
|
||||
|
||||
# an org admin can associate groups with inventories
|
||||
added_by_collection = self.post(url, data=new_group_b, expect=201, auth=self.get_normal_credentials())
|
||||
|
||||
# a normal user cannot associate groups with inventories
|
||||
added_by_collection = self.post(url, data=new_group_c, expect=403, auth=self.get_nobody_credentials())
|
||||
|
||||
# a normal user with edit permissions on the inventory can associate groups with inventories
|
||||
url5 = reverse('api:inventory_groups_list', args=(inv.pk,))
|
||||
added_by_collection = self.post(url5, data=new_group_d, expect=201, auth=self.get_other_credentials())
|
||||
# make sure duplicates give 400s
|
||||
self.post(url5, data=new_group_d, expect=400, auth=self.get_other_credentials())
|
||||
got = self.get(url5, expect=200, auth=self.get_other_credentials())
|
||||
self.assertEquals(got['count'], 5)
|
||||
|
||||
# side check: see if root groups URL is operational. These are groups without parents.
|
||||
root_groups = self.get(root_groups, expect=200, auth=self.get_super_credentials())
|
||||
self.assertEquals(root_groups['count'], 2)
|
||||
|
||||
remove_me = added_by_collection
|
||||
remove_me['disassociate'] = 1
|
||||
self.post(url5, data=remove_me, expect=204, auth=self.get_other_credentials())
|
||||
got = self.get(url5, expect=200, auth=self.get_other_credentials())
|
||||
self.assertEquals(got['count'], 4)
|
||||
|
||||
###################################################
|
||||
# VARIABLES
|
||||
|
||||
vars_a = dict(asdf=1234, dog='fido', cat='fluffy', unstructured=dict(a=[1,2,3],b=dict(x=2,y=3)))
|
||||
vars_b = dict(asdf=4321, dog='barky', cat='snarf', unstructured=dict(a=[1,2,3],b=dict(x=2,y=3)))
|
||||
vars_c = dict(asdf=5555, dog='mouse', cat='mogwai', unstructured=dict(a=[3,0,3],b=dict(z=2600)))
|
||||
|
||||
# attempting to get a variable object creates it, even though it does not already exist
|
||||
vdata_url = reverse('api:host_variable_data', args=(added_by_collection_a['id'],))
|
||||
|
||||
got = self.get(vdata_url, expect=200, auth=self.get_super_credentials())
|
||||
self.assertEquals(got, {})
|
||||
|
||||
# super user can create variable objects
|
||||
# an org admin can create variable objects (defers to inventory permissions)
|
||||
got = self.put(vdata_url, data=vars_a, expect=200, auth=self.get_super_credentials())
|
||||
self.assertEquals(got, vars_a)
|
||||
|
||||
# verify that we can update things and get them back
|
||||
got = self.put(vdata_url, data=vars_c, expect=200, auth=self.get_super_credentials())
|
||||
self.assertEquals(got, vars_c)
|
||||
got = self.get(vdata_url, expect=200, auth=self.get_super_credentials())
|
||||
self.assertEquals(got, vars_c)
|
||||
|
||||
# a normal user cannot edit variable objects
|
||||
self.put(vdata_url, data=vars_a, expect=403, auth=self.get_nobody_credentials())
|
||||
|
||||
# a normal user with inventory write permissions can edit variable objects...
|
||||
got = self.put(vdata_url, data=vars_b, expect=200, auth=self.get_normal_credentials())
|
||||
self.assertEquals(got, vars_b)
|
||||
|
||||
###################################################
|
||||
# VARIABLES -> GROUPS
|
||||
|
||||
vars_a = dict(asdf=7777, dog='droopy', cat='battlecat', unstructured=dict(a=[1,1,1],b=dict(x=1,y=2)))
|
||||
vars_b = dict(asdf=8888, dog='snoopy', cat='cheshire', unstructured=dict(a=[2,2,2],b=dict(x=3,y=4)))
|
||||
vars_c = dict(asdf=9999, dog='pluto', cat='five', unstructured=dict(a=[3,3,3],b=dict(z=5)))
|
||||
group = Group.objects.order_by('pk')[0]
|
||||
|
||||
vdata1_url = reverse('api:group_variable_data', args=(group.pk,))
|
||||
|
||||
# a super user can associate variable objects with groups
|
||||
got = self.get(vdata1_url, expect=200, auth=self.get_super_credentials())
|
||||
self.assertEquals(got, {})
|
||||
put = self.put(vdata1_url, data=vars_a, expect=200, auth=self.get_super_credentials())
|
||||
self.assertEquals(put, vars_a)
|
||||
|
||||
# an org admin can associate variable objects with groups
|
||||
put = self.put(vdata1_url, data=vars_b, expect=200, auth=self.get_normal_credentials())
|
||||
|
||||
# a normal user cannot associate variable objects with groups
|
||||
put = self.put(vdata1_url, data=vars_b, expect=403, auth=self.get_nobody_credentials())
|
||||
|
||||
# a normal user with inventory edit permissions can associate variable objects with groups
|
||||
put = self.put(vdata1_url, data=vars_c, expect=200, auth=self.get_normal_credentials())
|
||||
self.assertEquals(put, vars_c)
|
||||
|
||||
###################################################
|
||||
# VARIABLES -> INVENTORY
|
||||
|
||||
vars_a = dict(asdf=9873, dog='lassie', cat='heathcliff', unstructured=dict(a=[1,1,1],b=dict(x=1,y=2)))
|
||||
vars_b = dict(asdf=2736, dog='benji', cat='garfield', unstructured=dict(a=[2,2,2],b=dict(x=3,y=4)))
|
||||
vars_c = dict(asdf=7692, dog='buck', cat='sylvester', unstructured=dict(a=[3,3,3],b=dict(z=5)))
|
||||
|
||||
vdata_url = reverse('api:inventory_variable_data', args=(self.inventory_a.pk,))
|
||||
|
||||
# a super user can associate variable objects with inventory
|
||||
got = self.get(vdata_url, expect=200, auth=self.get_super_credentials())
|
||||
self.assertEquals(got, {})
|
||||
put = self.put(vdata_url, data=vars_a, expect=200, auth=self.get_super_credentials())
|
||||
self.assertEquals(put, vars_a)
|
||||
|
||||
# an org admin can associate variable objects with inventory
|
||||
put = self.put(vdata_url, data=vars_b, expect=200, auth=self.get_normal_credentials())
|
||||
|
||||
# a normal user cannot associate variable objects with inventory
|
||||
put = self.put(vdata_url, data=vars_b, expect=403, auth=self.get_nobody_credentials())
|
||||
|
||||
# a normal user with inventory edit permissions can associate variable objects with inventory
|
||||
put = self.put(vdata_url, data=vars_c, expect=200, auth=self.get_normal_credentials())
|
||||
self.assertEquals(put, vars_c)
|
||||
|
||||
# repeat but request variables in yaml
|
||||
got = self.get(vdata_url, expect=200,
|
||||
auth=self.get_normal_credentials(),
|
||||
accept='application/yaml')
|
||||
self.assertEquals(got, vars_c)
|
||||
|
||||
# repeat but updates variables in yaml
|
||||
put = self.put(vdata_url, data=vars_c, expect=200,
|
||||
auth=self.get_normal_credentials(), data_type='yaml',
|
||||
accept='application/yaml')
|
||||
self.assertEquals(put, vars_c)
|
||||
|
||||
####################################################
|
||||
# ADDING HOSTS TO GROUPS
|
||||
|
||||
groups = Group.objects.order_by('pk')
|
||||
hosts = Host.objects.order_by('pk')
|
||||
host1 = hosts[0]
|
||||
host2 = hosts[1]
|
||||
host3 = hosts[2]
|
||||
groups[0].hosts.add(host1)
|
||||
groups[0].hosts.add(host3)
|
||||
groups[0].save()
|
||||
|
||||
# access
|
||||
url1 = reverse('api:group_hosts_list', args=(groups[0].pk,))
|
||||
alt_group_hosts = reverse('api:group_hosts_list', args=(groups[1].pk,))
|
||||
other_alt_group_hosts = reverse('api:group_hosts_list', args=(groups[2].pk,))
|
||||
|
||||
data = self.get(url1, expect=200, auth=self.get_normal_credentials())
|
||||
self.assertEquals(data['count'], 2)
|
||||
self.assertTrue(host1.pk in [x['id'] for x in data['results']])
|
||||
self.assertTrue(host3.pk in [x['id'] for x in data['results']])
|
||||
|
||||
# addition
|
||||
url = reverse('api:host_detail', args=(host2.pk,))
|
||||
got = self.get(url, expect=200, auth=self.get_normal_credentials())
|
||||
self.assertEquals(got['id'], host2.pk)
|
||||
self.post(url1, data=got, expect=204, auth=self.get_normal_credentials())
|
||||
data = self.get(url1, expect=200, auth=self.get_normal_credentials())
|
||||
self.assertEquals(data['count'], 3)
|
||||
self.assertTrue(host2.pk in [x['id'] for x in data['results']])
|
||||
|
||||
# now add one new completely new host, to test creation+association in one go
|
||||
new_host = dict(inventory=got['inventory'], name='completelynewhost.example.com', description='...')
|
||||
self.post(url1, data=new_host, expect=201, auth=self.get_normal_credentials())
|
||||
|
||||
data = self.get(url1, expect=200, auth=self.get_normal_credentials())
|
||||
self.assertEquals(data['count'], 4)
|
||||
|
||||
# You should be able to add an existing host to a group as a new host and have it be copied
|
||||
existing_host = new_host
|
||||
self.post(alt_group_hosts, data=existing_host, expect=204, auth=self.get_normal_credentials())
|
||||
|
||||
# Not if the variables are different though
|
||||
existing_host['variables'] = '{"booh": "bah"}'
|
||||
self.post(other_alt_group_hosts, data=existing_host, expect=400, auth=self.get_normal_credentials())
|
||||
|
||||
# removal
|
||||
got['disassociate'] = 1
|
||||
self.post(url1, data=got, expect=204, auth=self.get_normal_credentials())
|
||||
data = self.get(url1, expect=200, auth=self.get_normal_credentials())
|
||||
self.assertEquals(data['count'], 3)
|
||||
self.assertFalse(host2.pk in [x['id'] for x in data['results']])
|
||||
|
||||
####################################################
|
||||
# SUBGROUPS
|
||||
|
||||
groups = Group.objects.all()
|
||||
|
||||
# just some more groups for kicks
|
||||
inva = Inventory.objects.get(pk=self.inventory_a.pk)
|
||||
gx1 = Group.objects.create(name='group-X1', inventory=inva)
|
||||
gx2 = Group.objects.create(name='group-X2', inventory=inva)
|
||||
gx2.parents.add(gx1)
|
||||
gx3 = Group.objects.create(name='group-X3', inventory=inva)
|
||||
gx3.parents.add(gx2)
|
||||
gx4 = Group.objects.create(name='group-X4', inventory=inva)
|
||||
gx4.parents.add(gx3)
|
||||
gx5 = Group.objects.create(name='group-X5', inventory=inva)
|
||||
gx5.parents.add(gx4)
|
||||
|
||||
inva.admin_role.members.add(self.other_django_user)
|
||||
|
||||
# data used for testing listing all hosts that are transitive members of a group
|
||||
g2 = Group.objects.get(name='web4')
|
||||
nh = Host.objects.create(name='newhost.example.com', inventory=g2.inventory,
|
||||
created_by=self.super_django_user)
|
||||
g2.hosts.add(nh)
|
||||
g2.save()
|
||||
|
||||
# a super user can set subgroups
|
||||
subgroups_url = reverse('api:group_children_list',
|
||||
args=(Group.objects.get(name='web2').pk,))
|
||||
child_url = reverse('api:group_detail',
|
||||
args=(Group.objects.get(name='web4').pk,))
|
||||
subgroups_url2 = reverse('api:group_children_list',
|
||||
args=(Group.objects.get(name='web6').pk,))
|
||||
subgroups_url3 = reverse('api:group_children_list',
|
||||
args=(Group.objects.get(name='web100').pk,))
|
||||
reverse('api:group_children_list',
|
||||
args=(Group.objects.get(name='web101').pk,))
|
||||
got = self.get(child_url, expect=200, auth=self.get_super_credentials())
|
||||
self.post(subgroups_url, data=got, expect=204, auth=self.get_super_credentials())
|
||||
kids = Group.objects.get(name='web2').children.all()
|
||||
self.assertEqual(len(kids), 1)
|
||||
checked = self.get(subgroups_url, expect=200, auth=self.get_super_credentials())
|
||||
self.assertEquals(checked['count'], 1)
|
||||
|
||||
# an org admin can set subgroups
|
||||
self.post(subgroups_url2, data=got, expect=204, auth=self.get_normal_credentials())
|
||||
|
||||
# see if we can post a completely new subgroup
|
||||
new_data = dict(inventory=inv.pk, name='completely new', description='blarg?')
|
||||
kids = self.get(subgroups_url2, expect=200, auth=self.get_normal_credentials())
|
||||
self.assertEqual(kids['count'], 1)
|
||||
posted2 = self.post(subgroups_url2, data=new_data, expect=201, auth=self.get_normal_credentials())
|
||||
|
||||
# a group can't be it's own grandparent
|
||||
subsub = posted2['related']['children']
|
||||
# this is the grandparent
|
||||
original_url = reverse('api:group_detail', args=(Group.objects.get(name='web6').pk,))
|
||||
parent_data = self.get(original_url, expect=200, auth=self.get_super_credentials())
|
||||
# now posting to kid's children collection...
|
||||
self.post(subsub, data=parent_data, expect=403, auth=self.get_super_credentials())
|
||||
|
||||
with_one_more_kid = self.get(subgroups_url2, expect=200, auth=self.get_normal_credentials())
|
||||
self.assertEqual(with_one_more_kid['count'], 2)
|
||||
|
||||
# double post causes conflict error (actually, should it? -- just got a 204, already associated)
|
||||
# self.post(subgroups_url2, data=got, expect=409, auth=self.get_normal_credentials())
|
||||
checked = self.get(subgroups_url2, expect=200, auth=self.get_normal_credentials())
|
||||
|
||||
# a normal user cannot set subgroups
|
||||
self.post(subgroups_url3, data=got, expect=403, auth=self.get_nobody_credentials())
|
||||
|
||||
# a normal user with inventory edit permissions can associate subgroups (but not when they belong to different inventories!)
|
||||
#self.post(subgroups_url3, data=got, expect=204, auth=self.get_other_credentials())
|
||||
#checked = self.get(subgroups_url3, expect=200, auth=self.get_normal_credentials())
|
||||
#self.assertEqual(checked['count'], 1)
|
||||
|
||||
# slight detour
|
||||
# can see all hosts under a group, even if it has subgroups
|
||||
# this URL is NOT postable
|
||||
all_hosts = reverse('api:group_all_hosts_list',
|
||||
args=(Group.objects.get(name='web2').pk,))
|
||||
self.assertEqual(Group.objects.get(name='web2').hosts.count(), 3)
|
||||
data = self.get(all_hosts, expect=200, auth=self.get_normal_credentials())
|
||||
self.post(all_hosts, data=dict(id=123456, msg='spam'), expect=405, auth=self.get_normal_credentials())
|
||||
self.assertEquals(data['count'], 4)
|
||||
|
||||
# now post it back to remove it, by adding the disassociate bit
|
||||
result = checked['results'][0]
|
||||
result['disassociate'] = 1
|
||||
self.post(subgroups_url3, data=result, expect=204, auth=self.get_other_credentials())
|
||||
checked = self.get(subgroups_url3, expect=200, auth=self.get_normal_credentials())
|
||||
self.assertEqual(checked['count'], 0)
|
||||
# try to double disassociate to see what happens (should no-op)
|
||||
self.post(subgroups_url3, data=result, expect=204, auth=self.get_other_credentials())
|
||||
|
||||
# removed group should be automatically marked inactive once it no longer has any parents.
|
||||
removed_group = Group.objects.get(pk=result['id'])
|
||||
self.assertTrue(removed_group.parents.count())
|
||||
for parent in removed_group.parents.all():
|
||||
parent_children_url = reverse('api:group_children_list', args=(parent.pk,))
|
||||
data = {'id': removed_group.pk, 'disassociate': 1}
|
||||
self.post(parent_children_url, data, expect=204, auth=self.get_super_credentials())
|
||||
removed_group = Group.objects.get(pk=result['id'])
|
||||
|
||||
# Removing a group from a hierarchy should migrate its children to the
|
||||
# parent. The group itself will be deleted (marked inactive), and all
|
||||
# relationships removed.
|
||||
url = reverse('api:group_children_list', args=(gx2.pk,))
|
||||
data = {
|
||||
'id': gx3.pk,
|
||||
'disassociate': 1,
|
||||
}
|
||||
with self.current_user(self.super_django_user):
|
||||
self.post(url, data, expect=204)
|
||||
gx3 = Group.objects.get(pk=gx3.pk)
|
||||
self.assertFalse(gx3 in gx2.children.all())
|
||||
#self.assertTrue(gx4 in gx2.children.all())
|
||||
|
||||
# Try with invalid hostnames and invalid IPs.
|
||||
hosts = reverse('api:host_list')
|
||||
invalid_expect = 400 # hostname validation is disabled for now.
|
||||
data = dict(name='', inventory=inv.pk)
|
||||
with self.current_user(self.super_django_user):
|
||||
self.post(hosts, data=data, expect=400)
|
||||
#data = dict(name='not a valid host name', inventory=inv.pk)
|
||||
#with self.current_user(self.super_django_user):
|
||||
# response = self.post(hosts, data=data, expect=invalid_expect)
|
||||
data = dict(name='validhost:99999', inventory=inv.pk)
|
||||
with self.current_user(self.super_django_user):
|
||||
self.post(hosts, data=data, expect=invalid_expect)
|
||||
#data = dict(name='123.234.345.456', inventory=inv.pk)
|
||||
#with self.current_user(self.super_django_user):
|
||||
# response = self.post(hosts, data=data, expect=invalid_expect)
|
||||
#data = dict(name='2001::1::3F', inventory=inv.pk)
|
||||
#with self.current_user(self.super_django_user):
|
||||
# response = self.post(hosts, data=data, expect=invalid_expect)
|
||||
|
||||
#########################################################
|
||||
# FIXME: TAGS
|
||||
|
||||
# the following objects can be tagged and the tags can be read
|
||||
# inventory
|
||||
# host records
|
||||
# group records
|
||||
# variable records
|
||||
# this may just be in a seperate test file called 'tags'
|
||||
|
||||
#########################################################
|
||||
# FIXME: RELATED FIELDS
|
||||
|
||||
# on an inventory resource, I can see related resources for hosts and groups and permissions
|
||||
# and these work
|
||||
# on a host resource, I can see related resources variables and inventories
|
||||
# and these work
|
||||
# on a group resource, I can see related resources for variables, inventories, and children
|
||||
# and these work
|
||||
|
||||
def test_get_inventory_script_view(self):
|
||||
i_a = self.inventory_a
|
||||
i_a.variables = json.dumps({'i-vars': 123})
|
||||
|
||||
Reference in New Issue
Block a user