diff --git a/awx/api/serializers.py b/awx/api/serializers.py index d954f532af..996fb627f7 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -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 diff --git a/awx/api/views.py b/awx/api/views.py index 9f6f2d5b1a..fbc555e9c6 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -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): diff --git a/awx/main/access.py b/awx/main/access.py index f4c4486faf..a14ccc1ca1 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -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) diff --git a/awx/main/migrations/0037_v320_fact_recent.py b/awx/main/migrations/0037_v320_release.py similarity index 73% rename from awx/main/migrations/0037_v320_fact_recent.py rename to awx/main/migrations/0037_v320_release.py index d7ca92f154..a9745354bd 100644 --- a/awx/main/migrations/0037_v320_fact_recent.py +++ b/awx/main/migrations/0037_v320_release.py @@ -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=[ diff --git a/awx/main/migrations/0038_v320_data_migrations.py b/awx/main/migrations/0038_v320_data_migrations.py new file mode 100644 index 0000000000..5b0f921cd6 --- /dev/null +++ b/awx/main/migrations/0038_v320_data_migrations.py @@ -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), + ] diff --git a/awx/main/migrations/_inventory_source.py b/awx/main/migrations/_inventory_source.py new file mode 100644 index 0000000000..a157bb2eb6 --- /dev/null +++ b/awx/main/migrations/_inventory_source.py @@ -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() diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index e91e67f134..ecb702adc9 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -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) diff --git a/awx/main/signals.py b/awx/main/signals.py index 9e5fa95692..5f82d8e26a 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -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( diff --git a/awx/main/tests/functional/api/test_inventory.py b/awx/main/tests/functional/api/test_inventory.py index b6f23b6b0a..27b929f372 100644 --- a/awx/main/tests/functional/api/test_inventory.py +++ b/awx/main/tests/functional/api/test_inventory.py @@ -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) diff --git a/awx/main/tests/functional/api/test_rbac_displays.py b/awx/main/tests/functional/api/test_rbac_displays.py index 6463fc533e..c905c9da35 100644 --- a/awx/main/tests/functional/api/test_rbac_displays.py +++ b/awx/main/tests/functional/api/test_rbac_displays.py @@ -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 diff --git a/awx/main/tests/functional/conftest.py b/awx/main/tests/functional/conftest.py index 3d79ca4c4c..fa87ed4df7 100644 --- a/awx/main/tests/functional/conftest.py +++ b/awx/main/tests/functional/conftest.py @@ -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) diff --git a/awx/main/tests/functional/test_inventory_source_migration.py b/awx/main/tests/functional/test_inventory_source_migration.py new file mode 100644 index 0000000000..cfbfdbaff6 --- /dev/null +++ b/awx/main/tests/functional/test_inventory_source_migration.py @@ -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 diff --git a/awx/main/tests/functional/test_notifications.py b/awx/main/tests/functional/test_notifications.py index f6edd42247..2f9787d9f0 100644 --- a/awx/main/tests/functional/test_notifications.py +++ b/awx/main/tests/functional/test_notifications.py @@ -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 diff --git a/awx/main/tests/functional/test_partial.py b/awx/main/tests/functional/test_partial.py index fd6d294cf2..5de7dca18a 100644 --- a/awx/main/tests/functional/test_partial.py +++ b/awx/main/tests/functional/test_partial.py @@ -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 - + diff --git a/awx/main/tests/functional/test_rbac_inventory.py b/awx/main/tests/functional/test_rbac_inventory.py index 6f26cacc54..10390e3b07 100644 --- a/awx/main/tests/functional/test_rbac_inventory.py +++ b/awx/main/tests/functional/test_rbac_inventory.py @@ -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}) diff --git a/awx/main/tests/old/inventory.py b/awx/main/tests/old/inventory.py index a2baf49fe6..9c90e09338 100644 --- a/awx/main/tests/old/inventory.py +++ b/awx/main/tests/old/inventory.py @@ -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})