mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-10 04:06:47 +00:00
fix(reconciliation): serialize compute sync and enforce UUID identity
This commit is contained in:
parent
30c436ecde
commit
2cdf5dfca1
6 changed files with 534 additions and 96 deletions
|
|
@ -285,26 +285,28 @@ class CreateInstanceViewSet(viewsets.ViewSet):
|
|||
machine = "q35"
|
||||
firmware["secure"] = "yes"
|
||||
|
||||
ret = conn.create_instance(
|
||||
name=serializer.validated_data["name"],
|
||||
memory=serializer.validated_data["memory"],
|
||||
vcpu=serializer.validated_data["vcpu"],
|
||||
vcpu_mode=serializer.validated_data["vcpu_mode"],
|
||||
uuid=util.randomUUID(),
|
||||
arch=arch,
|
||||
machine=machine,
|
||||
firmware=firmware,
|
||||
volumes=volume_list,
|
||||
networks=serializer.validated_data["networks"],
|
||||
nwfilter=serializer.validated_data["nwfilter"],
|
||||
graphics=serializer.validated_data["graphics"],
|
||||
virtio=serializer.validated_data["virtio"],
|
||||
listener_addr=serializer.validated_data["listener_addr"],
|
||||
video=serializer.validated_data["video"],
|
||||
console_pass=serializer.validated_data["console_pass"],
|
||||
mac=serializer.validated_data["mac"],
|
||||
qemu_ga=serializer.validated_data["qemu_ga"],
|
||||
)
|
||||
with utils.libvirt_compute_lock(compute):
|
||||
ret = conn.create_instance(
|
||||
name=serializer.validated_data["name"],
|
||||
memory=serializer.validated_data["memory"],
|
||||
vcpu=serializer.validated_data["vcpu"],
|
||||
vcpu_mode=serializer.validated_data["vcpu_mode"],
|
||||
uuid=util.randomUUID(),
|
||||
arch=arch,
|
||||
machine=machine,
|
||||
firmware=firmware,
|
||||
volumes=volume_list,
|
||||
networks=serializer.validated_data["networks"],
|
||||
nwfilter=serializer.validated_data["nwfilter"],
|
||||
graphics=serializer.validated_data["graphics"],
|
||||
virtio=serializer.validated_data["virtio"],
|
||||
listener_addr=serializer.validated_data["listener_addr"],
|
||||
video=serializer.validated_data["video"],
|
||||
console_pass=serializer.validated_data["console_pass"],
|
||||
mac=serializer.validated_data["mac"],
|
||||
qemu_ga=serializer.validated_data["qemu_ga"],
|
||||
)
|
||||
utils.refresh_instance_database(compute)
|
||||
msg = f"Instance {serializer.validated_data['name']} is created"
|
||||
return Response({"status": msg})
|
||||
else:
|
||||
|
|
|
|||
94
instances/migrations/0012_instance_unique_compute_uuid.py
Normal file
94
instances/migrations/0012_instance_unique_compute_uuid.py
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
# Generated by Django 4.2.30 on 2026-09-27 10:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def deduplicate_instances(apps, schema_editor):
|
||||
Instance = apps.get_model('instances', 'Instance')
|
||||
UserInstance = apps.get_model('accounts', 'UserInstance')
|
||||
from django.db.models import Count, Q
|
||||
import uuid as uuid_mod
|
||||
|
||||
# Assign distinct UUIDs to legacy/corrupt records with null, empty, or whitespace UUIDs
|
||||
# so they are not treated as duplicates or collapsed together
|
||||
empty_uuid_instances = Instance.objects.filter(
|
||||
Q(uuid__isnull=True) | Q(uuid="") | Q(uuid__regex=r"^\s*$")
|
||||
)
|
||||
for inst in empty_uuid_instances:
|
||||
inst.uuid = str(uuid_mod.uuid4())
|
||||
inst.save(update_fields=['uuid'])
|
||||
|
||||
duplicates = (
|
||||
Instance.objects.exclude(uuid__isnull=True).exclude(uuid="")
|
||||
.values('compute_id', 'uuid')
|
||||
.annotate(count=Count('id'))
|
||||
.filter(count__gt=1)
|
||||
)
|
||||
for item in duplicates:
|
||||
instances = list(Instance.objects.filter(
|
||||
compute_id=item['compute_id'],
|
||||
uuid=item['uuid']
|
||||
).order_by('-id'))
|
||||
primary = instances[0]
|
||||
# Preserve is_template and non-default drbd across duplicate rows
|
||||
merged_is_template = any(getattr(inst, 'is_template', False) for inst in instances)
|
||||
non_default_drbd = next((inst.drbd for inst in instances if inst.drbd and inst.drbd != 'None'), None)
|
||||
updated = False
|
||||
if primary.is_template != merged_is_template:
|
||||
primary.is_template = merged_is_template
|
||||
updated = True
|
||||
if non_default_drbd and primary.drbd != non_default_drbd:
|
||||
primary.drbd = non_default_drbd
|
||||
updated = True
|
||||
if updated:
|
||||
primary.save()
|
||||
|
||||
for dup in instances[1:]:
|
||||
# Reassign or merge UserInstance relationships
|
||||
for ui in UserInstance.objects.filter(instance=dup):
|
||||
existing = UserInstance.objects.filter(instance=primary, user=ui.user).first()
|
||||
if not existing:
|
||||
ui.instance = primary
|
||||
ui.save()
|
||||
else:
|
||||
# Merge permissions with logical OR so no stronger permission is lost
|
||||
updated = False
|
||||
if ui.is_change and not existing.is_change:
|
||||
existing.is_change = True
|
||||
updated = True
|
||||
if ui.is_delete and not existing.is_delete:
|
||||
existing.is_delete = True
|
||||
updated = True
|
||||
if ui.is_vnc and not existing.is_vnc:
|
||||
existing.is_vnc = True
|
||||
updated = True
|
||||
if updated:
|
||||
existing.save()
|
||||
ui.delete()
|
||||
dup.delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('instances', '0011_createinstance_migrateinstance'),
|
||||
('accounts', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(deduplicate_instances, migrations.RunPython.noop),
|
||||
migrations.AlterField(
|
||||
model_name='flavor',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='instance',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='instance',
|
||||
constraint=models.UniqueConstraint(fields=('compute', 'uuid'), name='unique_compute_uuid'),
|
||||
),
|
||||
]
|
||||
|
|
@ -32,6 +32,11 @@ class Instance(models.Model):
|
|||
|
||||
objects = InstanceManager()
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=["compute", "uuid"], name="unique_compute_uuid")
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.compute}/{self.name}"
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ import string
|
|||
|
||||
from accounts.models import UserInstance, UserAttributes
|
||||
from appsettings.settings import app_settings
|
||||
from computes.utils import libvirt_compute_lock
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from vrtManager.connection import connection_manager
|
||||
from vrtManager.instance import wvmInstance, wvmInstances
|
||||
|
|
@ -124,45 +126,86 @@ def migrate_instance(
|
|||
return
|
||||
if new_compute == instance.compute:
|
||||
return
|
||||
try:
|
||||
conn_migrate = wvmInstances(
|
||||
new_compute.hostname,
|
||||
new_compute.login,
|
||||
new_compute.password,
|
||||
new_compute.type,
|
||||
)
|
||||
c1, c2 = (
|
||||
(instance.compute, new_compute)
|
||||
if instance.compute.id < new_compute.id
|
||||
else (new_compute, instance.compute)
|
||||
)
|
||||
with libvirt_compute_lock(c1):
|
||||
with libvirt_compute_lock(c2):
|
||||
conn_migrate = None
|
||||
try:
|
||||
conn_migrate = wvmInstances(
|
||||
new_compute.hostname,
|
||||
new_compute.login,
|
||||
new_compute.password,
|
||||
new_compute.type,
|
||||
)
|
||||
|
||||
autostart = instance.autostart
|
||||
conn_migrate.moveto(
|
||||
instance.proxy,
|
||||
instance.name,
|
||||
live,
|
||||
unsafe,
|
||||
xml_del,
|
||||
offline,
|
||||
autoconverge,
|
||||
compress,
|
||||
postcopy,
|
||||
)
|
||||
finally:
|
||||
conn_migrate.close()
|
||||
autostart = instance.autostart
|
||||
conn_migrate.moveto(
|
||||
instance.proxy,
|
||||
instance.name,
|
||||
live,
|
||||
unsafe,
|
||||
xml_del,
|
||||
offline,
|
||||
autoconverge,
|
||||
compress,
|
||||
postcopy,
|
||||
)
|
||||
finally:
|
||||
if conn_migrate is not None:
|
||||
conn_migrate.close()
|
||||
|
||||
try:
|
||||
conn_new = wvmInstance(
|
||||
new_compute.hostname,
|
||||
new_compute.login,
|
||||
new_compute.password,
|
||||
new_compute.type,
|
||||
instance.name,
|
||||
)
|
||||
conn_new = None
|
||||
try:
|
||||
conn_new = wvmInstance(
|
||||
new_compute.hostname,
|
||||
new_compute.login,
|
||||
new_compute.password,
|
||||
new_compute.type,
|
||||
instance.name,
|
||||
)
|
||||
|
||||
if autostart:
|
||||
conn_new.set_autostart(1)
|
||||
finally:
|
||||
conn_new.close()
|
||||
if autostart:
|
||||
conn_new.set_autostart(1)
|
||||
finally:
|
||||
if conn_new is not None:
|
||||
conn_new.close()
|
||||
|
||||
instance.compute = new_compute
|
||||
instance.save()
|
||||
with transaction.atomic():
|
||||
target_inst = Instance.objects.filter(
|
||||
compute=new_compute, uuid=instance.uuid
|
||||
).first()
|
||||
if target_inst and target_inst.id != instance.id:
|
||||
for ui in UserInstance.objects.filter(instance=instance):
|
||||
existing_ui = UserInstance.objects.filter(
|
||||
instance=target_inst, user=ui.user
|
||||
).first()
|
||||
if not existing_ui:
|
||||
ui.instance = target_inst
|
||||
ui.save()
|
||||
else:
|
||||
updated = False
|
||||
if ui.is_change and not existing_ui.is_change:
|
||||
existing_ui.is_change = True
|
||||
updated = True
|
||||
if ui.is_delete and not existing_ui.is_delete:
|
||||
existing_ui.is_delete = True
|
||||
updated = True
|
||||
if ui.is_vnc and not existing_ui.is_vnc:
|
||||
existing_ui.is_vnc = True
|
||||
updated = True
|
||||
if updated:
|
||||
existing_ui.save()
|
||||
ui.delete()
|
||||
instance.delete()
|
||||
instance.id = target_inst.id
|
||||
instance.compute = new_compute
|
||||
else:
|
||||
instance.compute = new_compute
|
||||
instance.save()
|
||||
|
||||
|
||||
def refr(compute):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue