mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
* WIP * Convert checkout() context manager to a class * Misc cleanup * Drop unique constraint from Change model * Extend staging tests * Misc cleanup * Incorporate M2M changes * Don't cancel wipe out creation records when an object is deleted * Rename Change to StagedChange * Add documentation for change staging
This commit is contained in:
@@ -182,3 +182,20 @@ class WebhookHttpMethodChoices(ChoiceSet):
|
||||
(METHOD_PATCH, 'PATCH'),
|
||||
(METHOD_DELETE, 'DELETE'),
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Staging
|
||||
#
|
||||
|
||||
class ChangeActionChoices(ChoiceSet):
|
||||
|
||||
ACTION_CREATE = 'create'
|
||||
ACTION_UPDATE = 'update'
|
||||
ACTION_DELETE = 'delete'
|
||||
|
||||
CHOICES = (
|
||||
(ACTION_CREATE, 'Create'),
|
||||
(ACTION_UPDATE, 'Update'),
|
||||
(ACTION_DELETE, 'Delete'),
|
||||
)
|
||||
|
45
netbox/extras/migrations/0084_staging.py
Normal file
45
netbox/extras/migrations/0084_staging.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('extras', '0083_savedfilter'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Branch',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
('name', models.CharField(max_length=100, unique=True)),
|
||||
('description', models.CharField(blank=True, max_length=200)),
|
||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ('name',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='StagedChange',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
('action', models.CharField(max_length=20)),
|
||||
('object_id', models.PositiveBigIntegerField(blank=True, null=True)),
|
||||
('data', models.JSONField(blank=True, null=True)),
|
||||
('branch', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='staged_changes', to='extras.branch')),
|
||||
('object_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='contenttypes.contenttype')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('pk',),
|
||||
},
|
||||
),
|
||||
]
|
@@ -3,9 +3,11 @@ from .configcontexts import ConfigContext, ConfigContextModel
|
||||
from .customfields import CustomField
|
||||
from .models import *
|
||||
from .search import *
|
||||
from .staging import *
|
||||
from .tags import Tag, TaggedItem
|
||||
|
||||
__all__ = (
|
||||
'Branch',
|
||||
'CachedValue',
|
||||
'ConfigContext',
|
||||
'ConfigContextModel',
|
||||
@@ -20,6 +22,7 @@ __all__ = (
|
||||
'Report',
|
||||
'SavedFilter',
|
||||
'Script',
|
||||
'StagedChange',
|
||||
'Tag',
|
||||
'TaggedItem',
|
||||
'Webhook',
|
||||
|
114
netbox/extras/models/staging.py
Normal file
114
netbox/extras/models/staging.py
Normal file
@@ -0,0 +1,114 @@
|
||||
import logging
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models, transaction
|
||||
|
||||
from extras.choices import ChangeActionChoices
|
||||
from netbox.models import ChangeLoggedModel
|
||||
from utilities.utils import deserialize_object
|
||||
|
||||
__all__ = (
|
||||
'Branch',
|
||||
'StagedChange',
|
||||
)
|
||||
|
||||
logger = logging.getLogger('netbox.staging')
|
||||
|
||||
|
||||
class Branch(ChangeLoggedModel):
|
||||
"""
|
||||
A collection of related StagedChanges.
|
||||
"""
|
||||
name = models.CharField(
|
||||
max_length=100,
|
||||
unique=True
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
user = models.ForeignKey(
|
||||
to=get_user_model(),
|
||||
on_delete=models.SET_NULL,
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ('name',)
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.name} ({self.pk})'
|
||||
|
||||
def merge(self):
|
||||
logger.info(f'Merging changes in branch {self}')
|
||||
with transaction.atomic():
|
||||
for change in self.staged_changes.all():
|
||||
change.apply()
|
||||
self.staged_changes.all().delete()
|
||||
|
||||
|
||||
class StagedChange(ChangeLoggedModel):
|
||||
"""
|
||||
The prepared creation, modification, or deletion of an object to be applied to the active database at a
|
||||
future point.
|
||||
"""
|
||||
branch = models.ForeignKey(
|
||||
to=Branch,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='staged_changes'
|
||||
)
|
||||
action = models.CharField(
|
||||
max_length=20,
|
||||
choices=ChangeActionChoices
|
||||
)
|
||||
object_type = models.ForeignKey(
|
||||
to=ContentType,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='+'
|
||||
)
|
||||
object_id = models.PositiveBigIntegerField(
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
object = GenericForeignKey(
|
||||
ct_field='object_type',
|
||||
fk_field='object_id'
|
||||
)
|
||||
data = models.JSONField(
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ('pk',)
|
||||
|
||||
def __str__(self):
|
||||
action = self.get_action_display()
|
||||
app_label, model_name = self.object_type.natural_key()
|
||||
return f"{action} {app_label}.{model_name} ({self.object_id})"
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
return self.object_type.model_class()
|
||||
|
||||
def apply(self):
|
||||
"""
|
||||
Apply the staged create/update/delete action to the database.
|
||||
"""
|
||||
if self.action == ChangeActionChoices.ACTION_CREATE:
|
||||
instance = deserialize_object(self.model, self.data, pk=self.object_id)
|
||||
logger.info(f'Creating {self.model._meta.verbose_name} {instance}')
|
||||
instance.save()
|
||||
|
||||
if self.action == ChangeActionChoices.ACTION_UPDATE:
|
||||
instance = deserialize_object(self.model, self.data, pk=self.object_id)
|
||||
logger.info(f'Updating {self.model._meta.verbose_name} {instance}')
|
||||
instance.save()
|
||||
|
||||
if self.action == ChangeActionChoices.ACTION_DELETE:
|
||||
instance = self.model.objects.get(pk=self.object_id)
|
||||
logger.info(f'Deleting {self.model._meta.verbose_name} {instance}')
|
||||
instance.delete()
|
Reference in New Issue
Block a user