Merge pull request #5974 from wwitzel3/issue-5741

Remove group AutoOneToOne from InventorySource
This commit is contained in:
Wayne Witzel III
2017-04-11 15:54:39 -04:00
committed by GitHub
16 changed files with 223 additions and 633 deletions

View File

@@ -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

View File

@@ -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):

View File

@@ -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)

View File

@@ -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=[

View 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),
]

View 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()

View File

@@ -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)

View File

@@ -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(

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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})

View File

@@ -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})