Initial commit
This commit is contained in:
commit
4ff9eac274
45 files changed
+2911
No files matched your search
Whitespace-only changes.
@@ -0,0 +1,171 @@
|
||||
import pytest
|
||||
|
||||
from netbox_services.models import Service, ServiceResourceBinding, ServiceTemplate
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tenant(db):
|
||||
from tenancy.models import Tenant
|
||||
return Tenant.objects.create(name="test-tenant", slug="test-tenant")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def service_template(db):
|
||||
return ServiceTemplate.objects.create(
|
||||
name="test-template",
|
||||
status="draft",
|
||||
description="A test service template",
|
||||
tree=[
|
||||
{
|
||||
"name": "device",
|
||||
"object_type": "dcim.device",
|
||||
"quantity": 1,
|
||||
"selector": {"type": "netbox_filter", "filters": {}},
|
||||
"scope": "service",
|
||||
"children": [],
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def service(db, service_template, tenant):
|
||||
return Service.objects.create(
|
||||
name="test-service",
|
||||
service_template=service_template,
|
||||
tenant=tenant,
|
||||
status="planned",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def admin_user(db):
|
||||
from users.models import User
|
||||
return User.objects.create_superuser("test-admin", "admin@test.local", "test")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def admin_client(db, admin_user):
|
||||
from django.test import Client
|
||||
client = Client()
|
||||
client.force_login(admin_user)
|
||||
return client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def api_client(db, admin_user):
|
||||
from rest_framework.test import APIClient
|
||||
client = APIClient()
|
||||
client.force_authenticate(user=admin_user)
|
||||
return client
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestServiceTemplate:
|
||||
def test_str(self, service_template):
|
||||
assert str(service_template) == "test-template"
|
||||
|
||||
def test_default_status(self, db):
|
||||
st = ServiceTemplate.objects.create(name="default-status")
|
||||
assert st.status == "draft"
|
||||
|
||||
def test_default_tree(self, db):
|
||||
st = ServiceTemplate.objects.create(name="default-tree")
|
||||
assert st.tree == []
|
||||
|
||||
def test_is_locked_false(self, service_template):
|
||||
assert service_template.is_locked is False
|
||||
|
||||
def test_is_locked_true(self, service_template, service):
|
||||
assert service_template.is_locked is True
|
||||
|
||||
def test_ordering(self, db):
|
||||
ServiceTemplate.objects.create(name="z-last")
|
||||
ServiceTemplate.objects.create(name="a-first")
|
||||
names = list(ServiceTemplate.objects.values_list("name", flat=True))
|
||||
assert names == ["a-first", "z-last"]
|
||||
|
||||
def test_tree_validation_valid(self, db):
|
||||
st = ServiceTemplate(
|
||||
name="valid-tree",
|
||||
tree=[{
|
||||
"name": "dev",
|
||||
"object_type": "dcim.device",
|
||||
"quantity": 1,
|
||||
"selector": {"type": "netbox_filter", "filters": {}},
|
||||
"scope": "service",
|
||||
"children": [],
|
||||
}],
|
||||
)
|
||||
st.clean()
|
||||
|
||||
def test_tree_validation_missing_name(self, db):
|
||||
from django.core.exceptions import ValidationError
|
||||
st = ServiceTemplate(
|
||||
name="bad-tree",
|
||||
tree=[{"object_type": "dcim.device", "quantity": 1, "selector": {"type": "netbox_filter", "filters": {}}}],
|
||||
)
|
||||
with pytest.raises(ValidationError):
|
||||
st.clean()
|
||||
|
||||
def test_tree_validation_missing_object_type(self, db):
|
||||
from django.core.exceptions import ValidationError
|
||||
st = ServiceTemplate(
|
||||
name="bad-tree2",
|
||||
tree=[{"name": "dev", "quantity": 1, "selector": {"type": "netbox_filter", "filters": {}}}],
|
||||
)
|
||||
with pytest.raises(ValidationError):
|
||||
st.clean()
|
||||
|
||||
def test_tree_validation_no_selector_or_allocation(self, db):
|
||||
from django.core.exceptions import ValidationError
|
||||
st = ServiceTemplate(
|
||||
name="bad-tree3",
|
||||
tree=[{"name": "dev", "object_type": "dcim.device", "quantity": 1}],
|
||||
)
|
||||
with pytest.raises(ValidationError):
|
||||
st.clean()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestService:
|
||||
def test_str(self, service):
|
||||
assert str(service) == "test-service"
|
||||
|
||||
def test_default_status(self, db, service_template, tenant):
|
||||
s = Service.objects.create(
|
||||
name="status-test", service_template=service_template, tenant=tenant,
|
||||
)
|
||||
assert s.status == "planned"
|
||||
|
||||
def test_tenant_required(self, db, service_template):
|
||||
from django.db import IntegrityError
|
||||
with pytest.raises(IntegrityError):
|
||||
Service.objects.create(
|
||||
name="no-tenant", service_template=service_template,
|
||||
)
|
||||
|
||||
def test_protect_on_template_delete(self, service, service_template):
|
||||
from django.db.models import ProtectedError
|
||||
with pytest.raises(ProtectedError):
|
||||
service_template.delete()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestServiceResourceBinding:
|
||||
def test_str(self, service):
|
||||
from dcim.models import Site
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
site = Site.objects.create(name="bind-test-site", slug="bind-test-site")
|
||||
ct = ContentType.objects.get_for_model(Site)
|
||||
binding = ServiceResourceBinding.objects.create(
|
||||
service=service,
|
||||
content_type=ct,
|
||||
object_id=site.pk,
|
||||
)
|
||||
assert "test-service" in str(binding)
|
||||
assert "bind-test-site" in str(binding)
|
||||
|
||||
def test_private(self):
|
||||
assert ServiceResourceBinding._netbox_private is True
|
||||
@@ -0,0 +1,43 @@
|
||||
import pytest
|
||||
|
||||
from netbox_services.engine.allocators import FirstFreeIPAddressAllocator, SpecificAllocator
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestFirstFreeIPAddressAllocator:
|
||||
def test_allocate_creates_ip(self, db):
|
||||
from ipam.models import IPAddress, Prefix
|
||||
from netbox_services.engine.context import Context
|
||||
|
||||
Prefix.objects.create(prefix="10.99.0.0/24")
|
||||
allocator = FirstFreeIPAddressAllocator({
|
||||
"strategy": "first_free",
|
||||
"prefix": {"type": "prefix", "filters": {}},
|
||||
})
|
||||
ip = allocator.allocate(Context(service=None))
|
||||
assert ip is not None
|
||||
assert "10.99.0." in str(ip.address)
|
||||
|
||||
def test_no_prefix_raises(self, db):
|
||||
from netbox_services.engine.context import Context
|
||||
allocator = FirstFreeIPAddressAllocator({
|
||||
"strategy": "first_free",
|
||||
"prefix": {"type": "prefix", "filters": {"description": "no-such-prefix"}},
|
||||
})
|
||||
with pytest.raises(ValueError):
|
||||
allocator.allocate(Context(service=None))
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestSpecificAllocator:
|
||||
def test_specific_address(self, db):
|
||||
from netbox_services.engine.context import Context
|
||||
allocator = SpecificAllocator({"strategy": "specific", "address": "10.50.0.1/24"})
|
||||
ip = allocator.allocate(Context(service=None))
|
||||
assert str(ip.address) == "10.50.0.1/24"
|
||||
|
||||
def test_specific_requires_address_or_path(self, db):
|
||||
from netbox_services.engine.context import Context
|
||||
allocator = SpecificAllocator({"strategy": "specific"})
|
||||
with pytest.raises(ValueError):
|
||||
allocator.allocate(Context(service=None))
|
||||
@@ -0,0 +1,111 @@
|
||||
import pytest
|
||||
|
||||
from netbox_services.models import Service, ServiceTemplate
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestServiceTemplateAPI:
|
||||
def test_list(self, api_client):
|
||||
ServiceTemplate.objects.create(name="api-tmpl")
|
||||
resp = api_client.get("/api/plugins/services/service-templates/")
|
||||
assert resp.status_code == 200
|
||||
assert resp.data["count"] >= 1
|
||||
|
||||
def test_create_with_tree(self, api_client):
|
||||
resp = api_client.post("/api/plugins/services/service-templates/", {
|
||||
"name": "api-created-tmpl",
|
||||
"status": "draft",
|
||||
"tree": [{"name": "dev", "object_type": "dcim.device", "quantity": 1, "selector": {"type": "netbox_filter", "filters": {}}, "scope": "service", "children": []}],
|
||||
}, format="json")
|
||||
assert resp.status_code == 201
|
||||
assert resp.data["name"] == "api-created-tmpl"
|
||||
assert len(resp.data["tree"]) == 1
|
||||
|
||||
def test_retrieve(self, api_client):
|
||||
st = ServiceTemplate.objects.create(name="api-retrieve-tmpl")
|
||||
resp = api_client.get(f"/api/plugins/services/service-templates/{st.pk}/")
|
||||
assert resp.status_code == 200
|
||||
|
||||
def test_delete(self, api_client):
|
||||
st = ServiceTemplate.objects.create(name="api-delete-tmpl")
|
||||
resp = api_client.delete(f"/api/plugins/services/service-templates/{st.pk}/")
|
||||
assert resp.status_code == 204
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestServiceAPI:
|
||||
def test_list(self, api_client, service_template, tenant):
|
||||
Service.objects.create(name="api-svc", service_template=service_template, tenant=tenant)
|
||||
resp = api_client.get("/api/plugins/services/services/")
|
||||
assert resp.status_code == 200
|
||||
assert resp.data["count"] >= 1
|
||||
|
||||
def test_create(self, api_client, service_template, tenant):
|
||||
resp = api_client.post("/api/plugins/services/services/", {
|
||||
"name": "api-created-svc",
|
||||
"service_template": service_template.pk,
|
||||
"tenant": tenant.pk,
|
||||
"status": "planned",
|
||||
}, format="json")
|
||||
assert resp.status_code == 201
|
||||
|
||||
def test_resolve_dry_run(self, api_client, db, tenant):
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
|
||||
|
||||
site = Site.objects.create(name="api-res-site", slug="api-res-site")
|
||||
mfr = Manufacturer.objects.create(name="api-res-mfr", slug="api-res-mfr")
|
||||
dt = DeviceType.objects.create(manufacturer=mfr, model="api-model", slug="api-model")
|
||||
role = DeviceRole.objects.create(name="api-role", slug="api-role")
|
||||
Device.objects.create(name="api-dev", site=site, device_type=dt, role=role)
|
||||
|
||||
tmpl = ServiceTemplate.objects.create(
|
||||
name="api-res-tmpl",
|
||||
status="active",
|
||||
tree=[{
|
||||
"name": "device",
|
||||
"object_type": "dcim.device",
|
||||
"quantity": 1,
|
||||
"selector": {"type": "netbox_filter", "filters": {"role": ["api-role"]}},
|
||||
"scope": "service",
|
||||
"children": [],
|
||||
}],
|
||||
)
|
||||
svc = Service.objects.create(name="api-res-svc", service_template=tmpl, tenant=tenant)
|
||||
|
||||
resp = api_client.post(f"/api/plugins/services/services/{svc.pk}/resolve/", {
|
||||
"mode": "dry_run",
|
||||
}, format="json")
|
||||
assert resp.status_code == 200
|
||||
assert resp.data["mode"] == "dry_run"
|
||||
assert resp.data["total_planned"] == 1
|
||||
|
||||
def test_resolve_commit(self, api_client, db, tenant):
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
|
||||
from netbox_services.models import ServiceResourceBinding
|
||||
|
||||
site = Site.objects.create(name="api-commit-site", slug="api-commit-site")
|
||||
mfr = Manufacturer.objects.create(name="api-commit-mfr", slug="api-commit-mfr")
|
||||
dt = DeviceType.objects.create(manufacturer=mfr, model="api-commit-model", slug="api-commit-model")
|
||||
role = DeviceRole.objects.create(name="api-commit-role", slug="api-commit-role")
|
||||
Device.objects.create(name="api-commit-dev", site=site, device_type=dt, role=role)
|
||||
|
||||
tmpl = ServiceTemplate.objects.create(
|
||||
name="api-commit-tmpl",
|
||||
status="active",
|
||||
tree=[{
|
||||
"name": "device",
|
||||
"object_type": "dcim.device",
|
||||
"quantity": 1,
|
||||
"selector": {"type": "netbox_filter", "filters": {"role": ["api-commit-role"]}},
|
||||
"scope": "service",
|
||||
"children": [],
|
||||
}],
|
||||
)
|
||||
svc = Service.objects.create(name="api-commit-svc", service_template=tmpl, tenant=tenant)
|
||||
|
||||
resp = api_client.post(f"/api/plugins/services/services/{svc.pk}/resolve/", {
|
||||
"mode": "commit",
|
||||
}, format="json")
|
||||
assert resp.status_code == 200
|
||||
assert resp.data["mode"] == "commit"
|
||||
assert ServiceResourceBinding.objects.filter(service=svc).count() == 1
|
||||
@@ -0,0 +1,46 @@
|
||||
import pytest
|
||||
|
||||
from netbox_services.models import Service, ServiceTemplate
|
||||
from netbox_services.signals import TemplateLockedError
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def template_with_service(db, tenant):
|
||||
st = ServiceTemplate.objects.create(name="locked-tmpl", status="active", tree=[])
|
||||
Service.objects.create(name="locked-svc", service_template=st, tenant=tenant)
|
||||
return st
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tenant(db):
|
||||
from tenancy.models import Tenant
|
||||
return Tenant.objects.create(name="lock-tenant", slug="lock-tenant")
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestTemplateLock:
|
||||
def test_cannot_edit_name_when_locked(self, template_with_service):
|
||||
template_with_service.name = "renamed"
|
||||
with pytest.raises(TemplateLockedError):
|
||||
template_with_service.save()
|
||||
|
||||
def test_cannot_edit_tree_when_locked(self, template_with_service):
|
||||
template_with_service.tree = [{"name": "x", "object_type": "dcim.device", "quantity": 1, "selector": {"type": "netbox_filter", "filters": {}}}]
|
||||
with pytest.raises(TemplateLockedError):
|
||||
template_with_service.save()
|
||||
|
||||
def test_cannot_delete_when_locked(self, template_with_service):
|
||||
from django.db.models import ProtectedError
|
||||
with pytest.raises((ProtectedError, TemplateLockedError)):
|
||||
template_with_service.delete()
|
||||
|
||||
def test_can_edit_when_unlocked(self, db):
|
||||
st = ServiceTemplate.objects.create(name="unlocked", status="draft", tree=[])
|
||||
st.name = "renamed-unlocked"
|
||||
st.save()
|
||||
assert ServiceTemplate.objects.get(pk=st.pk).name == "renamed-unlocked"
|
||||
|
||||
def test_can_delete_when_unlocked(self, db):
|
||||
st = ServiceTemplate.objects.create(name="deletable", tree=[])
|
||||
st.delete()
|
||||
assert not ServiceTemplate.objects.filter(pk=st.pk).exists()
|
||||
@@ -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
|
||||
@@ -0,0 +1,76 @@
|
||||
import pytest
|
||||
|
||||
from netbox_services.engine.context import Context
|
||||
from netbox_services.engine.selectors import NetBoxFilterSelector, ReferenceSelector
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestContext:
|
||||
def test_resolve_service(self, service):
|
||||
ctx = Context(service=service)
|
||||
assert ctx.resolve_path("service") is service
|
||||
|
||||
def test_resolve_parent_none(self, service):
|
||||
ctx = Context(service=service)
|
||||
assert ctx.resolve_path("parent") is None
|
||||
|
||||
def test_resolve_dotted_path(self, service, service_template):
|
||||
ctx = Context(service=service)
|
||||
assert ctx.resolve_path("service.service_template") is service_template
|
||||
|
||||
def test_resolve_deep_path(self, service, service_template):
|
||||
ctx = Context(service=service)
|
||||
assert ctx.resolve_path("service.service_template.name") == service_template.name
|
||||
|
||||
def test_resolve_missing_attr(self, service):
|
||||
ctx = Context(service=service)
|
||||
assert ctx.resolve_path("service.nonexistent") is None
|
||||
|
||||
def test_child_context(self, service):
|
||||
ctx = Context(service=service)
|
||||
parent_obj = object()
|
||||
child = ctx.child(parent_obj)
|
||||
assert child.service is service
|
||||
assert child.parent_object is parent_obj
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestNetBoxFilterSelector:
|
||||
def test_filter_devices(self, db):
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
|
||||
|
||||
site = Site.objects.create(name="sel-site", slug="sel-site")
|
||||
mfr = Manufacturer.objects.create(name="sel-mfr", slug="sel-mfr")
|
||||
dt = DeviceType.objects.create(manufacturer=mfr, model="sel-model", slug="sel-model")
|
||||
role = DeviceRole.objects.create(name="sel-router", slug="sel-router")
|
||||
Device.objects.create(name="r1", site=site, device_type=dt, role=role)
|
||||
Device.objects.create(name="r2", site=site, device_type=dt, role=role)
|
||||
|
||||
sel = NetBoxFilterSelector({"type": "netbox_filter", "filters": {"role": ["sel-router"]}})
|
||||
qs = sel.resolve(Context(service=None), Device)
|
||||
assert qs.count() == 2
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestReferenceSelector:
|
||||
def test_resolve_reference(self, db):
|
||||
from dcim.models import Site
|
||||
site = Site.objects.create(name="ref-site", slug="ref-site")
|
||||
ctx = Context(service=None, parent_object=site)
|
||||
sel = ReferenceSelector({"type": "reference", "path": "parent"})
|
||||
qs = sel.resolve(ctx, Site)
|
||||
assert qs.first() == site
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestPrefixSelector:
|
||||
def test_prefix_by_filter(self, db):
|
||||
from ipam.models import Prefix
|
||||
from netbox_services.engine.selectors import PrefixSelector
|
||||
|
||||
Prefix.objects.create(prefix="10.0.0.0/24")
|
||||
Prefix.objects.create(prefix="10.1.0.0/24")
|
||||
|
||||
sel = PrefixSelector({"type": "prefix", "filters": {}})
|
||||
qs = sel.resolve(Context(service=None), Prefix)
|
||||
assert qs.count() == 2
|
||||
@@ -0,0 +1,33 @@
|
||||
import pytest
|
||||
|
||||
from netbox_services.models import Service, ServiceTemplate
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestViews:
|
||||
def test_template_list(self, admin_client):
|
||||
ServiceTemplate.objects.create(name="view-tmpl")
|
||||
resp = admin_client.get("/plugins/services/service-templates/")
|
||||
assert resp.status_code == 200
|
||||
|
||||
def test_template_detail(self, admin_client):
|
||||
st = ServiceTemplate.objects.create(name="view-detail-tmpl", tree=[])
|
||||
resp = admin_client.get(f"/plugins/services/service-templates/{st.pk}/")
|
||||
assert resp.status_code == 200
|
||||
|
||||
def test_template_add(self, admin_client):
|
||||
resp = admin_client.get("/plugins/services/service-templates/add/")
|
||||
assert resp.status_code == 200
|
||||
|
||||
def test_service_list(self, admin_client, service_template, tenant):
|
||||
Service.objects.create(name="view-svc", service_template=service_template, tenant=tenant)
|
||||
resp = admin_client.get("/plugins/services/services/")
|
||||
assert resp.status_code == 200
|
||||
|
||||
def test_service_detail(self, admin_client, service):
|
||||
resp = admin_client.get(f"/plugins/services/services/{service.pk}/")
|
||||
assert resp.status_code == 200
|
||||
|
||||
def test_service_resolve_get(self, admin_client, service):
|
||||
resp = admin_client.get(f"/plugins/services/services/{service.pk}/resolve/")
|
||||
assert resp.status_code == 200
|
||||
Reference in new issue
Block a user