Initial commit
This commit is contained in:
commit
4ff9eac274
45 files changed
+2911
No files matched your search
@@ -0,0 +1,244 @@
|
||||
import pytest
|
||||
|
||||
from netbox_services.engine.resolver import Resolver, ConflictError
|
||||
from netbox_services.models import Service, ServiceResourceBinding, ServiceTemplate
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def site(db):
|
||||
from dcim.models import Site
|
||||
return Site.objects.create(name="resolver-site", slug="resolver-site")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def device_type(db):
|
||||
from dcim.models import DeviceType, Manufacturer
|
||||
mfr = Manufacturer.objects.create(name="resolver-mfr", slug="resolver-mfr")
|
||||
return DeviceType.objects.create(manufacturer=mfr, model="resolver-model", slug="resolver-model")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def role(db):
|
||||
from dcim.models import DeviceRole
|
||||
return DeviceRole.objects.create(name="resolver-role", slug="resolver-role")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def devices(site, device_type, role, db):
|
||||
from dcim.models import Device
|
||||
return [
|
||||
Device.objects.create(name="dev-a", site=site, device_type=device_type, role=role),
|
||||
Device.objects.create(name="dev-b", site=site, device_type=device_type, role=role),
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def prefix(db):
|
||||
from ipam.models import Prefix
|
||||
return Prefix.objects.create(prefix="10.200.0.0/24")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tenant(db):
|
||||
from tenancy.models import Tenant
|
||||
return Tenant.objects.create(name="res-tenant", slug="res-tenant")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def service_with_devices(db, devices, tenant):
|
||||
tmpl = ServiceTemplate.objects.create(
|
||||
name="dev-tmpl",
|
||||
status="active",
|
||||
tree=[{
|
||||
"name": "routers",
|
||||
"object_type": "dcim.device",
|
||||
"quantity": 2,
|
||||
"selector": {"type": "netbox_filter", "filters": {"role": ["resolver-role"]}},
|
||||
"scope": "service",
|
||||
"children": [],
|
||||
}],
|
||||
)
|
||||
return Service.objects.create(name="dev-svc", service_template=tmpl, tenant=tenant)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def service_with_ip_alloc(db, prefix, tenant):
|
||||
tmpl = ServiceTemplate.objects.create(
|
||||
name="ip-tmpl",
|
||||
status="active",
|
||||
tree=[{
|
||||
"name": "ip",
|
||||
"object_type": "ipam.ipaddress",
|
||||
"quantity": 1,
|
||||
"allocation": {
|
||||
"strategy": "first_free",
|
||||
"prefix": {"type": "prefix", "filters": {}},
|
||||
},
|
||||
"scope": "service",
|
||||
"children": [],
|
||||
}],
|
||||
)
|
||||
return Service.objects.create(name="ip-svc", service_template=tmpl, tenant=tenant)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestResolverDryRun:
|
||||
def test_dry_run_no_bindings(self, service_with_devices):
|
||||
resolver = Resolver()
|
||||
result = resolver.resolve(service_with_devices, mode="dry_run")
|
||||
assert result.total_planned == 2
|
||||
assert ServiceResourceBinding.objects.filter(service=service_with_devices).count() == 0
|
||||
|
||||
def test_dry_run_rolls_back_allocation(self, service_with_ip_alloc):
|
||||
from ipam.models import IPAddress
|
||||
resolver = Resolver()
|
||||
result = resolver.resolve(service_with_ip_alloc, mode="dry_run")
|
||||
assert result.total_planned == 1
|
||||
assert IPAddress.objects.count() == 0
|
||||
assert ServiceResourceBinding.objects.count() == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestResolverCommit:
|
||||
def test_commit_creates_bindings(self, service_with_devices):
|
||||
resolver = Resolver()
|
||||
result = resolver.resolve(service_with_devices, mode="commit")
|
||||
assert result.succeeded
|
||||
assert ServiceResourceBinding.objects.filter(service=service_with_devices).count() == 2
|
||||
|
||||
def test_commit_allocates_ip(self, service_with_ip_alloc):
|
||||
from ipam.models import IPAddress
|
||||
resolver = Resolver()
|
||||
result = resolver.resolve(service_with_ip_alloc, mode="commit")
|
||||
assert IPAddress.objects.count() == 1
|
||||
assert ServiceResourceBinding.objects.filter(service=service_with_ip_alloc).count() == 1
|
||||
|
||||
def test_commit_nested(self, db, devices, prefix, tenant):
|
||||
from ipam.models import IPAddress
|
||||
from dcim.models import Interface
|
||||
|
||||
for dev in devices:
|
||||
Interface.objects.create(device=dev, name="eth0")
|
||||
|
||||
tmpl = ServiceTemplate.objects.create(
|
||||
name="nested-tmpl",
|
||||
status="active",
|
||||
tree=[{
|
||||
"name": "routers",
|
||||
"object_type": "dcim.device",
|
||||
"quantity": 1,
|
||||
"selector": {"type": "netbox_filter", "filters": {"role": ["resolver-role"]}},
|
||||
"scope": "service",
|
||||
"children": [{
|
||||
"name": "ip",
|
||||
"object_type": "ipam.ipaddress",
|
||||
"quantity": 1,
|
||||
"allocation": {
|
||||
"strategy": "first_free",
|
||||
"prefix": {"type": "prefix", "filters": {}},
|
||||
},
|
||||
"scope": "parent",
|
||||
"children": [],
|
||||
}],
|
||||
}],
|
||||
)
|
||||
svc = Service.objects.create(name="nested-svc", service_template=tmpl, tenant=tenant)
|
||||
|
||||
resolver = Resolver()
|
||||
result = resolver.resolve(svc, mode="commit")
|
||||
assert result.succeeded
|
||||
assert ServiceResourceBinding.objects.filter(service=svc).count() == 2
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestReconciliation:
|
||||
def test_reresolve_no_change(self, service_with_devices):
|
||||
resolver = Resolver()
|
||||
resolver.resolve(service_with_devices, mode="commit")
|
||||
result = resolver.resolve(service_with_devices, mode="commit")
|
||||
assert len(result.to_add) == 0
|
||||
assert len(result.to_remove) == 0
|
||||
assert ServiceResourceBinding.objects.filter(service=service_with_devices).count() == 2
|
||||
|
||||
def test_reresolve_reuses_allocated(self, service_with_ip_alloc):
|
||||
from ipam.models import IPAddress
|
||||
resolver = Resolver()
|
||||
resolver.resolve(service_with_ip_alloc, mode="commit")
|
||||
ip_count_before = IPAddress.objects.count()
|
||||
result = resolver.resolve(service_with_ip_alloc, mode="commit")
|
||||
assert len(result.to_add) == 0
|
||||
assert IPAddress.objects.count() == ip_count_before
|
||||
|
||||
def test_reresolve_removes_stale(self, service_with_devices, devices):
|
||||
resolver = Resolver()
|
||||
result = resolver.resolve(service_with_devices, mode="commit")
|
||||
assert ServiceResourceBinding.objects.filter(service=service_with_devices).count() == 2
|
||||
|
||||
svc_tmpl = service_with_devices.service_template
|
||||
svc_tmpl.tree[0]["quantity"] = 1
|
||||
# Template is locked — bypass via queryset
|
||||
ServiceTemplate.objects.filter(pk=svc_tmpl.pk).update(tree=svc_tmpl.tree)
|
||||
service_with_devices.refresh_from_db()
|
||||
service_with_devices.service_template.refresh_from_db()
|
||||
|
||||
result2 = resolver.resolve(service_with_devices, mode="commit")
|
||||
assert len(result2.to_remove) >= 1
|
||||
assert ServiceResourceBinding.objects.filter(service=service_with_devices).count() == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestConflictDetection:
|
||||
def test_exclusion_when_device_already_bound(self, db, site, device_type, role, tenant):
|
||||
from dcim.models import Device
|
||||
|
||||
dev = Device.objects.create(name="conflict-dev", site=site, device_type=device_type, role=role)
|
||||
tmpl = ServiceTemplate.objects.create(
|
||||
name="conflict-tmpl",
|
||||
status="active",
|
||||
tree=[{
|
||||
"name": "dev",
|
||||
"object_type": "dcim.device",
|
||||
"quantity": 1,
|
||||
"selector": {"type": "netbox_filter", "filters": {"role": ["resolver-role"]}},
|
||||
"scope": "service",
|
||||
"children": [],
|
||||
}],
|
||||
)
|
||||
svc1 = Service.objects.create(name="c-svc-1", service_template=tmpl, tenant=tenant)
|
||||
svc2 = Service.objects.create(name="c-svc-2", service_template=tmpl, tenant=tenant)
|
||||
|
||||
resolver = Resolver()
|
||||
r1 = resolver.resolve(svc1, mode="commit")
|
||||
assert r1.succeeded
|
||||
assert ServiceResourceBinding.objects.filter(service=svc1).count() == 1
|
||||
|
||||
r2 = resolver.resolve(svc2, mode="commit")
|
||||
assert r2.succeeded
|
||||
assert ServiceResourceBinding.objects.filter(service=svc2).count() == 0
|
||||
assert any("only 0 of 1" in w for w in r2.warnings)
|
||||
|
||||
def test_no_conflict_different_devices(self, db, devices, tenant):
|
||||
tmpl = ServiceTemplate.objects.create(
|
||||
name="noconf-tmpl",
|
||||
status="active",
|
||||
tree=[{
|
||||
"name": "dev",
|
||||
"object_type": "dcim.device",
|
||||
"quantity": 1,
|
||||
"selector": {"type": "netbox_filter", "filters": {"role": ["resolver-role"]}},
|
||||
"scope": "service",
|
||||
"children": [],
|
||||
}],
|
||||
)
|
||||
svc1 = Service.objects.create(name="nc-svc-1", service_template=tmpl, tenant=tenant)
|
||||
svc2 = Service.objects.create(name="nc-svc-2", service_template=tmpl, tenant=tenant)
|
||||
|
||||
resolver = Resolver()
|
||||
r1 = resolver.resolve(svc1, mode="commit")
|
||||
assert r1.succeeded
|
||||
r2 = resolver.resolve(svc2, mode="commit")
|
||||
assert r2.succeeded
|
||||
|
||||
b1 = ServiceResourceBinding.objects.filter(service=svc1).first()
|
||||
b2 = ServiceResourceBinding.objects.filter(service=svc2).first()
|
||||
assert b1.object_id != b2.object_id
|
||||
Reference in new issue
Block a user