From 008ec8f86e190afcda7993d0a5d892dd792747ab Mon Sep 17 00:00:00 2001 From: Chris Church Date: Mon, 31 Mar 2014 19:26:03 -0400 Subject: [PATCH] AC-601 Add groups/hosts collections to inventory sources, add inventory sources collections to groups/hosts to indicate which inventory source(s) have created/updated the given groups/hosts. Prevent setting an explicit inventory source for a group that is being managed by another parent with cloud inventory source. --- awx/api/serializers.py | 8 ++++---- awx/api/urls.py | 8 ++++---- awx/api/views.py | 33 ++++++++++++++++++++++++++++++++- awx/main/models/inventory.py | 10 ++++++++++ awx/main/tests/inventory.py | 32 ++++++++++++++++++++++++++++++++ 5 files changed, 82 insertions(+), 9 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 2bf695408d..7cf971d093 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -722,7 +722,7 @@ class HostSerializer(BaseSerializerWithVariables): job_events = reverse('api:host_job_events_list', args=(obj.pk,)), job_host_summaries = reverse('api:host_job_host_summaries_list', args=(obj.pk,)), activity_stream = reverse('api:host_activity_stream_list', args=(obj.pk,)), - #inventory_sources = reverse('api:host_inventory_sources_list', args=(obj.pk,)), + inventory_sources = reverse('api:host_inventory_sources_list', args=(obj.pk,)), )) if obj.inventory and obj.inventory.active: res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,)) @@ -833,7 +833,7 @@ class GroupSerializer(BaseSerializerWithVariables): job_events = reverse('api:group_job_events_list', args=(obj.pk,)), job_host_summaries = reverse('api:group_job_host_summaries_list', args=(obj.pk,)), activity_stream = reverse('api:group_activity_stream_list', args=(obj.pk,)), - #inventory_sources = reverse('api:group_inventory_sources_list', args=(obj.pk,)), + inventory_sources = reverse('api:group_inventory_sources_list', args=(obj.pk,)), )) if obj.inventory and obj.inventory.active: res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,)) @@ -981,8 +981,8 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt inventory_updates = reverse('api:inventory_source_updates_list', args=(obj.pk,)), schedules = reverse('api:inventory_source_schedules_list', args=(obj.pk,)), activity_stream = reverse('api:inventory_activity_stream_list', args=(obj.pk,)), - #hosts = reverse('api:inventory_source_hosts_list', args=(obj.pk,)), - #groups = reverse('api:inventory_source_groups_list', args=(obj.pk,)), + hosts = reverse('api:inventory_source_hosts_list', args=(obj.pk,)), + groups = reverse('api:inventory_source_groups_list', args=(obj.pk,)), )) if obj.inventory and obj.inventory.active: res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,)) diff --git a/awx/api/urls.py b/awx/api/urls.py index 1e8bf28da8..6aa7e9d3cb 100644 --- a/awx/api/urls.py +++ b/awx/api/urls.py @@ -81,7 +81,7 @@ host_urls = patterns('awx.api.views', url(r'^(?P[0-9]+)/job_events/', 'host_job_events_list'), url(r'^(?P[0-9]+)/job_host_summaries/$', 'host_job_host_summaries_list'), url(r'^(?P[0-9]+)/activity_stream/$', 'host_activity_stream_list'), - #url(r'^(?P[0-9]+)/inventory_sources/$', 'host_inventory_sources_list'), + url(r'^(?P[0-9]+)/inventory_sources/$', 'host_inventory_sources_list'), ) group_urls = patterns('awx.api.views', @@ -95,7 +95,7 @@ group_urls = patterns('awx.api.views', url(r'^(?P[0-9]+)/job_host_summaries/$', 'group_job_host_summaries_list'), url(r'^(?P[0-9]+)/potential_children/$', 'group_potential_children_list'), url(r'^(?P[0-9]+)/activity_stream/$', 'group_activity_stream_list'), - #url(r'^(?P[0-9]+)/inventory_sources/$', 'group_inventory_sources_list'), + url(r'^(?P[0-9]+)/inventory_sources/$', 'group_inventory_sources_list'), ) inventory_source_urls = patterns('awx.api.views', @@ -105,8 +105,8 @@ inventory_source_urls = patterns('awx.api.views', url(r'^(?P[0-9]+)/inventory_updates/$', 'inventory_source_updates_list'), url(r'^(?P[0-9]+)/activity_stream/$', 'inventory_source_activity_stream_list'), url(r'^(?P[0-9]+)/schedules/$', 'inventory_source_schedules_list'), - #url(r'^(?P[0-9]+)/groups/$', 'inventory_source_groups_list'), - #url(r'^(?P[0-9]+)/hosts/$', 'inventory_source_hosts_list'), + url(r'^(?P[0-9]+)/groups/$', 'inventory_source_groups_list'), + url(r'^(?P[0-9]+)/hosts/$', 'inventory_source_hosts_list'), ) inventory_update_urls = patterns('awx.api.views', diff --git a/awx/api/views.py b/awx/api/views.py index 99fb82b9a2..82feabe415 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -722,6 +722,14 @@ class HostAllGroupsList(SubListAPIView): sublist_qs = parent.all_groups.distinct() return qs & sublist_qs +class HostInventorySourcesList(SubListAPIView): + + model = InventorySource + serializer_class = InventorySourceSerializer + parent_model = Host + relationship = 'inventory_sources' + new_in_148 = True + class HostActivityStreamList(SubListAPIView): model = ActivityStream @@ -815,6 +823,14 @@ class GroupAllHostsList(SubListAPIView): sublist_qs = parent.all_hosts.distinct() return qs & sublist_qs +class GroupInventorySourcesList(SubListAPIView): + + model = InventorySource + serializer_class = InventorySourceSerializer + parent_model = Group + relationship = 'inventory_sources' + new_in_148 = True + class GroupActivityStreamList(SubListAPIView): model = ActivityStream @@ -980,7 +996,6 @@ class InventorySourceDetail(RetrieveUpdateAPIView): serializer_class = InventorySourceSerializer new_in_14 = True - class InventorySourceSchedulesList(SubListCreateAPIView): view_name = "Inventory Source Schedules" @@ -1000,6 +1015,22 @@ class InventorySourceActivityStreamList(SubListAPIView): relationship = 'activitystream_set' new_in_145 = True +class InventorySourceHostsList(SubListAPIView): + + model = Host + serializer_class = HostSerializer + parent_model = InventorySource + relationship = 'hosts' + new_in_148 = True + +class InventorySourceGroupsList(SubListAPIView): + + model = Group + serializer_class = GroupSerializer + parent_model = InventorySource + relationship = 'groups' + new_in_148 = True + class InventorySourceUpdatesList(SubListAPIView): model = InventoryUpdate diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 734cd002f8..bc722ea7a8 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -682,6 +682,16 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions): return True return False + def clean_source(self): + source = self.source + if source and self.group: + qs = self.group.inventory_sources.filter(source__in=CLOUD_INVENTORY_SOURCES, active=True, group__active=True) + 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): ''' diff --git a/awx/main/tests/inventory.py b/awx/main/tests/inventory.py index e33baaa4bd..5bc256a251 100644 --- a/awx/main/tests/inventory.py +++ b/awx/main/tests/inventory.py @@ -1046,15 +1046,30 @@ class InventoryUpdatesTest(BaseTransactionTest): if initial: self.assertEqual(inventory.groups.count(), 1) self.assertEqual(inventory.hosts.count(), 0) + self.assertEqual(inventory_source.groups.count(), 0) + self.assertEqual(inventory_source.hosts.count(), 0) inventory_update = self.check_inventory_update(inventory_source) inventory_source = InventorySource.objects.get(pk=inventory_source.pk) self.assertNotEqual(inventory.groups.count(), 1) self.assertNotEqual(inventory.hosts.count(), 0) + self.assertNotEqual(inventory_source.groups.count(), 0) + self.assertNotEqual(inventory_source.hosts.count(), 0) + with self.current_user(self.super_django_user): + url = reverse('api:inventory_source_groups_list', args=(inventory_source.pk,)) + response = self.get(url, expect=200) + self.assertNotEqual(response['count'], 0) + url = reverse('api:inventory_source_hosts_list', args=(inventory_source.pk,)) + response = self.get(url, expect=200) + self.assertNotEqual(response['count'], 0) for host in inventory.hosts.all(): source_pks = host.inventory_sources.values_list('pk', flat=True) self.assertTrue(inventory_source.pk in source_pks) self.assertTrue(host.has_inventory_sources) self.assertTrue(host.enabled) + with self.current_user(self.super_django_user): + url = reverse('api:host_inventory_sources_list', args=(host.pk,)) + response = self.get(url, expect=200) + self.assertNotEqual(response['count'], 0) for group in inventory.groups.all(): source_pks = group.inventory_sources.values_list('pk', flat=True) self.assertTrue(inventory_source.pk in source_pks) @@ -1062,6 +1077,23 @@ class InventoryUpdatesTest(BaseTransactionTest): # Make sure EC2 instance ID groups are excluded. self.assertFalse(re.match(r'^i-[0-9a-f]{8}$', group.name, re.I), group.name) + with self.current_user(self.super_django_user): + url = reverse('api:group_inventory_sources_list', args=(group.pk,)) + response = self.get(url, expect=200) + self.assertNotEqual(response['count'], 0) + # Try to set a source on a child group that was imported. Should not + # be allowed. + for group in inventory_source.group.children.all(): + inv_src_2 = group.inventory_source + inv_src_url2 = reverse('api:inventory_source_detail', args=(inv_src_2.pk,)) + with self.current_user(self.super_django_user): + data = self.get(inv_src_url2, expect=200) + data.update({ + 'source': inventory_source.source, + 'credential': inventory_source.credential.pk, + }) + response = self.put(inv_src_url2, data, expect=400) + self.assertTrue('source' in response, response) def test_put_inventory_source_detail_with_regions(self): creds_url = reverse('api:credential_list')