def setUpTestData(cls): site = Site.objects.create(name="Site 1", slug="site-1") # Create three ConfigContexts for i in range(1, 4): configcontext = ConfigContext(name="Config Context {}".format(i), data={"foo": i}) configcontext.save() configcontext.sites.add(site) cls.form_data = { "name": "Config Context X", "weight": 200, "description": "A new config context", "is_active": True, "regions": [], "sites": [site.pk], "roles": [], "platforms": [], "tenant_groups": [], "tenants": [], "tags": [], "data": '{"foo": 123}', } cls.bulk_edit_data = { "weight": 300, "is_active": False, "description": "New description", }
def test_name_uniqueness(self): """ Verify that two unowned ConfigContexts cannot share the same name (GitHub issue #431). """ ConfigContext.objects.create(name="context 1", weight=100, data={ "a": 123, "b": 456, "c": 777 }) with self.assertRaises(ValidationError): duplicate_context = ConfigContext(name="context 1", weight=200, data={"c": 666}) duplicate_context.validated_save() # If a different context is owned by a GitRepository, that's not considered a duplicate repo = GitRepository( name="Test Git Repository", slug="test-git-repo", remote_url="http://localhost/git.git", username="******", ) repo.save(trigger_resync=False) nonduplicate_context = ConfigContext(name="context 1", weight=300, data={"a": "22"}, owner=repo) nonduplicate_context.validated_save()
def test_render_configcontext_for_object(self): """ Test rendering config context data for a device. """ manufacturer = Manufacturer.objects.create(name="Manufacturer 1", slug="manufacturer-1") devicetype = DeviceType.objects.create(manufacturer=manufacturer, model="Device Type 1", slug="device-type-1") devicerole = DeviceRole.objects.create(name="Device Role 1", slug="device-role-1") site = Site.objects.create(name="Site-1", slug="site-1") device = Device.objects.create(name="Device 1", device_type=devicetype, device_role=devicerole, site=site) # Test default config contexts (created at test setup) rendered_context = device.get_config_context() self.assertEqual(rendered_context["foo"], 123) self.assertEqual(rendered_context["bar"], 456) self.assertEqual(rendered_context["baz"], 789) # Add another context specific to the site configcontext4 = ConfigContext(name="Config Context 4", data={"site_data": "ABC"}) configcontext4.save() configcontext4.sites.add(site) rendered_context = device.get_config_context() self.assertEqual(rendered_context["site_data"], "ABC") # Override one of the default contexts configcontext5 = ConfigContext(name="Config Context 5", weight=2000, data={"foo": 999}) configcontext5.save() configcontext5.sites.add(site) rendered_context = device.get_config_context() self.assertEqual(rendered_context["foo"], 999) # Add a context which does NOT match our device and ensure it does not apply site2 = Site.objects.create(name="Site 2", slug="site-2") configcontext6 = ConfigContext(name="Config Context 6", weight=2000, data={"bar": 999}) configcontext6.save() configcontext6.sites.add(site2) rendered_context = device.get_config_context() self.assertEqual(rendered_context["bar"], 456)
def import_config_context(context_data, repository_record, job_result, logger): """ Parse a given dictionary of data to create/update a ConfigContext record. The dictionary is expected to have a key "_metadata" which defines properties on the ConfigContext record itself (name, weight, description, etc.), while all other keys in the dictionary will go into the record's "data" field. Note that we don't use extras.api.serializers.ConfigContextSerializer, despite superficial similarities; the reason is that the serializer only allows us to identify related objects (Region, Site, DeviceRole, etc.) by their database primary keys, whereas here we need to be able to look them up by other values such as slug. """ git_repository_content_type = ContentType.objects.get_for_model( GitRepository) context_record = None # TODO: check context_data against a schema of some sort? # Set defaults for optional fields context_metadata = context_data.setdefault("_metadata", {}) context_metadata.setdefault("weight", 1000) context_metadata.setdefault("description", "") context_metadata.setdefault("is_active", True) # Translate relationship queries/filters to lists of related objects relations = {} for key, model_class in [ ("regions", Region), ("sites", Site), ("device_types", DeviceType), ("roles", DeviceRole), ("platforms", Platform), ("cluster_groups", ClusterGroup), ("clusters", Cluster), ("tenant_groups", TenantGroup), ("tenants", Tenant), ("tags", Tag), ]: relations[key] = [] for object_data in context_metadata.get(key, ()): try: object_instance = model_class.objects.get(**object_data) except model_class.DoesNotExist as exc: raise RuntimeError( f"No matching {model_class.__name__} found for {object_data}; unable to create/update " f"context {context_metadata.get('name')}") from exc except model_class.MultipleObjectsReturned as exc: raise RuntimeError( f"Multiple {model_class.__name__} found for {object_data}; unable to create/update " f"context {context_metadata.get('name')}") from exc relations[key].append(object_instance) with transaction.atomic(): # FIXME: Normally ObjectChange records are automatically generated every time we save an object, # regardless of whether any fields were actually modified. # Because a single GitRepository may manage dozens of records, this would result in a lot of noise # every time a repository gets resynced. # To reduce that noise until the base issue is fixed, we need to explicitly detect object changes: created = False modified = False save_needed = False try: context_record = ConfigContext.objects.get( name=context_metadata.get("name"), owner_content_type=git_repository_content_type, owner_object_id=repository_record.pk, ) except ConfigContext.DoesNotExist: context_record = ConfigContext( name=context_metadata.get("name"), owner_content_type=git_repository_content_type, owner_object_id=repository_record.pk, ) created = True for field in ("weight", "description", "is_active"): new_value = context_metadata[field] if getattr(context_record, field) != new_value: setattr(context_record, field, new_value) modified = True save_needed = True data = context_data.copy() del data["_metadata"] if context_record.data != data: context_record.data = data modified = True save_needed = True if created: # Save it so that it gets a PK, required before we can set the relations context_record.save() save_needed = False for key, objects in relations.items(): field = getattr(context_record, key) value = list(field.all()) if value != objects: field.set(objects) # Calling set() on a ManyToManyField doesn't require a subsequent save() call modified = True if save_needed: context_record.save() if created: job_result.log( "Successfully created config context", obj=context_record, level_choice=LogLevelChoices.LOG_SUCCESS, grouping="config contexts", logger=logger, ) elif modified: job_result.log( "Successfully refreshed config context", obj=context_record, level_choice=LogLevelChoices.LOG_SUCCESS, grouping="config contexts", logger=logger, ) else: job_result.log( "No change to config context", obj=context_record, level_choice=LogLevelChoices.LOG_INFO, grouping="config contexts", logger=logger, ) return context_record.name if context_record else None