diff --git a/invoke.nautobot_2.yml b/invoke.nautobot_2.yml new file mode 100644 index 00000000..95f027f5 --- /dev/null +++ b/invoke.nautobot_2.yml @@ -0,0 +1,5 @@ +--- +nautobot_design_builder: + project_name: "nautobot_design_builder_v2" + python_ver: "3.9" + nautobot_ver: "2.1" diff --git a/nautobot_design_builder/design.py b/nautobot_design_builder/design.py index 1df2e447..1ae187cc 100644 --- a/nautobot_design_builder/design.py +++ b/nautobot_design_builder/design.py @@ -140,6 +140,7 @@ def __init__( self.filter = {} self.action = None self.instance_fields = {} + self._kwargs = {} for direction in Relationship.objects.get_for_model(model_class): for relationship in direction: self.instance_fields[relationship.slug] = field_factory(self, relationship) @@ -225,7 +226,11 @@ def _parse_attributes(self): # pylint: disable=too-many-branches raise errors.DesignImplementationError(f"{fieldname} is not a property", self.model_class) self.attributes[fieldname] = {search: self.attributes.pop(key)} elif not hasattr(self.model_class, key) and key not in self.instance_fields: - raise errors.DesignImplementationError(f"{key} is not a property", self.model_class) + value = self.creator.resolve_values(self.attributes.pop(key)) + if isinstance(value, ModelInstance): + value = value.instance + self._kwargs[key] = value + # raise errors.DesignImplementationError(f"{key} is not a property", self.model_class) if self.action is None: self.action = self.CREATE @@ -280,7 +285,10 @@ def _load_instance(self): self.attributes.update(query_filter) elif self.action != "create": raise errors.DesignImplementationError(f"Unknown database action {self.action}", self.model_class) - self.instance = self.model_class() + try: + self.instance = self.model_class(**self._kwargs) + except TypeError as ex: + raise errors.DesignImplementationError(str(ex), self.model_class) def _update_fields(self): # pylint: disable=too-many-branches if self.action == self.GET and self.attributes: diff --git a/nautobot_design_builder/errors.py b/nautobot_design_builder/errors.py index 0b1e3c1c..c2cd9e2c 100644 --- a/nautobot_design_builder/errors.py +++ b/nautobot_design_builder/errors.py @@ -54,7 +54,15 @@ def _model_str(model): if not isinstance(model, Model) and not hasattr(model, "instance"): if isclass(model): return model.__name__ - return str(model) + try: + return str(model) + except Exception: # pylint: disable=broad-exception-caught + # Sometimes when converting a model to a string the __str__ + # method itself produces an exceptions, like when an attribute + # hasn't been set or something. Whatever it is commonly is + # the cause of the original exception, we don't want to + # cause *another* exception because of that. + return model.__class__.__name__ model_class = model.__class__ # if it looks like a duck... @@ -63,7 +71,15 @@ def _model_str(model): model = model.instance if model: - instance_str = str(model) + try: + instance_str = str(model) + except Exception: # pylint: disable=broad-exception-caught + # Sometimes when converting a model to a string the __str__ + # method itself produces an exceptions, like when an attribute + # hasn't been set or something. Whatever it is commonly is + # the cause of the original exception, we don't want to + # cause *another* exception because of that. + instance_str = model.__class__.__name__ model_str = model_class._meta.verbose_name.capitalize() if instance_str: model_str = f"{model_str} {instance_str}" diff --git a/nautobot_design_builder/tests/test_builder.py b/nautobot_design_builder/tests/test_builder.py index ad927bae..8a26cafd 100644 --- a/nautobot_design_builder/tests/test_builder.py +++ b/nautobot_design_builder/tests/test_builder.py @@ -1,799 +1,162 @@ """Test object creator methods.""" +import importlib +from operator import attrgetter +import os from unittest.mock import Mock, patch import yaml -from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import ObjectDoesNotExist + +from django.db.models import Manager, Q from django.test import TestCase -from nautobot.dcim.models import Device, DeviceType, Interface, Manufacturer, Cable -from nautobot.extras.choices import RelationshipTypeChoices -from nautobot.extras.models import ( - ConfigContext, - Relationship, - RelationshipAssociation, - Tag, - Secret, - SecretsGroup, - SecretsGroupAssociation, -) -from nautobot.ipam.models import VLAN, IPAddress, Prefix + +from nautobot.dcim.models import Cable from nautobot_design_builder.design import Builder from nautobot_design_builder.util import nautobot_version -if nautobot_version < "2.0.0": - from nautobot.dcim.models import Region, Site # pylint: disable=no-name-in-module,ungrouped-imports - -INPUT_CREATE_OBJECTS = """ -manufacturers: - - name: "manufacturer1" - - name: "manufacturer2" - -device_types: - - manufacturer__name: "manufacturer1" - model: "model name" - u_height: 1 -""" - -INPUT_UPDATE_OBJECT = """ -device_types: - - "!update:model": "model name" - manufacturer__name: "manufacturer2" -""" - -INPUT_UPDATE_OBJECT_1 = """ -manufacturers: - - name: "manufacturer1" - -device_types: - - manufacturer__name: "manufacturer1" - model: "model name" - u_height: 1 - "!ref": "device" - - - "!update:id": "!ref:device.id" - model: "new model name" -""" - -INPUT_CREATE_NESTED_OBJECTS = """ -manufacturers: - - name: "manufacturer1" - -device_types: - - manufacturer__name: "manufacturer1" - model: "model name" - u_height: 1 - -device_roles: - - name: "device role" - -sites: - - name: "site_1" - status__name: "Active" - -devices: - - name: "device_1" - site__name: "site_1" - status__name: "Active" - device_type__model: "model name" - device_role__name: "device role" - interfaces: - - name: "Ethernet1/1" - type: "virtual" - status__name: "Active" - description: "description for Ethernet1/1" -""" - -INPUT_UPDATE_NESTED_OBJECTS = """ -devices: - - "!update:name": "device_1" - interfaces: - - "!update:name": "Ethernet1/1" - description: "new description for Ethernet1/1" -""" - -INPUT_MANY_TO_MANY_OBJECTS = """ -regions: - - name: "Region 1" - "!ref": "region_1" - -config_contexts: - - name: "My Context" - data: - foo: 123 - regions: - - "!ref:region_1" - - name: "My cool new region" -""" - -INPUT_ONE_TO_ONE_OBJECTS = """ -manufacturers: - - name: "manufacturer1" - -device_types: - - manufacturer__name: "manufacturer1" - model: "chassis" - u_height: 1 - subdevice_role: "parent" - - - manufacturer__name: "manufacturer1" - model: "card" - u_height: 0 - subdevice_role: "child" - -device_roles: - - name: "device role" - -sites: - - name: "site_1" - status__name: "Active" - -devices: - - name: "device_1" - site__name: "site_1" - status__name: "Active" - device_type__model: "chassis" - device_role__name: "device role" - devicebays: - - name: "Bay 1" - installed_device: - name: "device_2" - site__name: "site_1" - status__name: "Active" - device_type__model: "card" - device_role__name: "device role" -""" - -INPUT_PREFIXES = """ -sites: - - name: "site_1" - status__name: "Active" - -prefixes: - - site__name: "site_1" - status__name: Active - prefix: "192.168.0.0/24" - - "!create_or_update:site__name": "site_1" - "!create_or_update:prefix": "192.168.56.0/24" - status__name: "Active" -""" - -INPUT_INTERFACE_ADDRESSES = """ -manufacturers: - - name: "manufacturer1" - -device_types: - - manufacturer__name: "manufacturer1" - model: "model name" - u_height: 1 - -device_roles: - - name: "device role" - -sites: - - name: "site_1" - status__name: "Active" - -devices: - - name: "device_1" - site__name: "site_1" - status__name: "Active" - device_type__model: "model name" - device_role__name: "device role" - interfaces: - - name: "Ethernet1/1" - type: "virtual" - status__name: "Active" - description: "description for Ethernet1/1" - ip_addresses: - - address: 192.168.56.1/24 - status__name: "Active" - """ - -INPUT_CREATE_TAGS = """ -tags: - - name: Test Tag - slug: test_tag - description: Some Description -""" - -INPUT_ASSIGN_TAGS = """ -tags: - - name: Test Tag - "!ref": test_tag - slug: test_tag - description: Some Description - -sites: - - name: "site_1" - status__name: "Active" - tags: - - "!ref:test_tag" -""" - -INPUT_ASSIGN_TAGS_1 = """ -tags: - - name: "Test Tag" - slug: "test_tag" - description: "Some Description" - -sites: - - name: "site_1" - status__name: "Active" - tags: - - { "!get:name": "Test Tag" } -""" - -INPUT_CREATE_MLAG = """ -manufacturers: - - name: "manufacturer1" - -device_types: - - manufacturer__name: "manufacturer1" - model: "model name" - u_height: 1 - -device_roles: - - name: "device role" - -sites: - - name: "site_1" - status__name: "Active" - -devices: - - name: "device_1" - site__name: "site_1" - status__name: "Active" - device_type__model: "model name" - device_role__name: "device role" - interfaces: - - name: "Ethernet1/1" - type: "1000base-t" - status__name: "Active" - "!ref": "ethernet11" - - name: "Ethernet2/1" - type: "1000base-t" - status__name: "Active" - "!ref": "ethernet21" - - name: "Ethernet3/1" - type: "1000base-t" - status__name: "Active" - "!ref": "ethernet31" - - name: "Ethernet4/1" - type: "1000base-t" - status__name: "Active" - "!ref": "ethernet41" - - name: "Port-Channel1" - type: lag - status__name: "Active" - member_interfaces: - - "!ref:ethernet11" - - "!ref:ethernet21" - - "!ref:ethernet31" - - "!ref:ethernet41" -""" - -INPUT_CUSTOM_RELATION = """ -vlans: - - "!create_or_update:vid": 42 - name: "The Answer" - status__name: "Active" - -devices: - - "!create_or_update:name": "device_1" - "device-to-vlans": - - "!get:vid": 42 - - vid: "43" - name: "Better Answer" - status__name: "Active" -""" - -INPUT_REF_FOR_CREATE_OR_UPDATE = """ -# Secrets & Secrets Groups -secrets: -- "!create_or_update:name": "Device username" - "description": "Username for network devices" - "provider": "environment-variable" - "parameters": {"variable": "NAUTOBOT_NAPALM_USERNAME"} - "!ref": "device_username" -- "!create_or_update:name": "Device password" - "description": "Password for network devices" - "provider": "environment-variable" - "parameters": {"variable": "NAUTOBOT_NAPALM_PASSWORD"} - "!ref": "device_password" - -secrets_groups: -- "!create_or_update:name": "Device credentials" - "!ref": "device_credentials" - -secrets_group_associations: -- "!create_or_update:group": "!ref:device_credentials" - "!create_or_update:secret": "!ref:device_username" - "access_type": "Generic" - "secret_type": "username" -- "!create_or_update:group": "!ref:device_credentials" - "!create_or_update:secret": "!ref:device_password" - "access_type": "Generic" - "secret_type": "password" -""" - -INPUT_REF_FOR_CREATE_OR_UPDATE1 = """ -secrets_groups: -- "!create_or_update:name": "Device credentials" - secrets: - - access_type: "Generic" - secret_type: "username" - secret: - "name": "Device username" - "description": "Username for network devices" - "provider": "environment-variable" - "parameters": {"variable": "NAUTOBOT_NAPALM_USERNAME"} - - access_type: "Generic" - secret_type: "password" - secret: - "name": "Device password" - "description": "Password for network devices" - "provider": "environment-variable" - "parameters": {"variable": "NAUTOBOT_NAPALM_PASSWORD"} -""" - -INPUT_COMPLEX_DESIGN1 = """ -manufacturers: - - "name": "manufacturer" - -device_types: - - "manufacturer__name": "manufacturer" - "model": "model name" - "u_height": 1 - -device_roles: - - "name": "EVPN Leaf" - - "name": "EVPN Spine" - -sites: - - "name": "site" - "status__name": "Active" - -devices: - # Create Spine Switches - - "!create_or_update:name": "spine1" - "status__name": "Active" - "site__name": "site" - "device_role__name": "EVPN Spine" - "device_type__model": "model name" - "interfaces": - - "!create_or_update:name": "Ethernet9/3" - "type": "100gbase-x-qsfp28" - "status__name": "Active" - "!ref": "spine1_to_leaf1" - - "!create_or_update:name": "Ethernet25/3" - "type": "100gbase-x-qsfp28" - "status__name": "Active" - "!ref": "spine1_to_leaf2" - - "!create_or_update:name": "spine2" - "status__name": "Active" - "site__name": "site" - "device_role__name": "EVPN Spine" - "device_type__model": "model name" - "interfaces": - - "!create_or_update:name": "Ethernet9/3" - "type": "100gbase-x-qsfp28" - "status__name": "Active" - "!ref": "spine2_to_leaf1" - - "!create_or_update:name": "Ethernet25/3" - "type": "100gbase-x-qsfp28" - "status__name": "Active" - "!ref": "spine2_to_leaf2" - - "!create_or_update:name": "spine3" - "status__name": "Active" - "site__name": "site" - "device_role__name": "EVPN Spine" - "device_type__model": "model name" - "interfaces": - - "!create_or_update:name": "Ethernet9/3" - "type": "100gbase-x-qsfp28" - "status__name": "Active" - "!ref": "spine3_to_leaf1" - - "!create_or_update:name": "Ethernet25/3" - "type": "100gbase-x-qsfp28" - "status__name": "Active" - "!ref": "spine3_to_leaf2" - - "!create_or_update:name": leaf1 - "status__name": "Active" - "site__name": "site" - "device_role__name": "EVPN Leaf" - "device_type__model": "model name" - "interfaces": - - "!create_or_update:name": "Ethernet33/1" - "type": "100gbase-x-qsfp28" - "!ref": leaf1_to_spine1 - "status__name": "Active" - - "!create_or_update:name": "Ethernet34/1" - "type": "100gbase-x-qsfp28" - "!ref": leaf1_to_spine2 - "status__name": "Active" - - "!create_or_update:name": "Ethernet35/1" - "type": "100gbase-x-qsfp28" - "!ref": leaf1_to_spine3 - "status__name": "Active" - - "!create_or_update:name": leaf2 - "status__name": "Active" - "site__name": "site" - "device_role__name": "EVPN Leaf" - "device_type__model": "model name" - "interfaces": - - "!create_or_update:name": "Ethernet33/1" - "type": "100gbase-x-qsfp28" - "!ref": leaf2_to_spine1 - "status__name": "Active" - - "!create_or_update:name": "Ethernet34/1" - "type": "100gbase-x-qsfp28" - "!ref": leaf2_to_spine2 - "status__name": "Active" - - "!create_or_update:name": "Ethernet35/1" - "type": "100gbase-x-qsfp28" - "!ref": leaf2_to_spine3 - "status__name": "Active" - -cables: - - "!create_or_update:termination_a_id": "!ref:spine1_to_leaf1.id" - "!create_or_update:termination_b_id": "!ref:leaf1_to_spine1.id" - "termination_a": "!ref:spine1_to_leaf1" - "termination_b": "!ref:leaf1_to_spine1" - "status__name": "Planned" - - "!create_or_update:termination_a_id": "!ref:spine2_to_leaf1.id" - "!create_or_update:termination_b_id": "!ref:leaf1_to_spine2.id" - "termination_a": "!ref:spine2_to_leaf1" - "termination_b": "!ref:leaf1_to_spine2" - "status__name": "Planned" - - "!create_or_update:termination_a_id": "!ref:spine3_to_leaf1.id" - "!create_or_update:termination_b_id": "!ref:leaf1_to_spine3.id" - "termination_a": "!ref:spine3_to_leaf1" - "termination_b": "!ref:leaf1_to_spine3" - "status__name": "Planned" - - "!create_or_update:termination_a_id": "!ref:spine1_to_leaf2.id" - "!create_or_update:termination_b_id": "!ref:leaf2_to_spine1.id" - "termination_a": "!ref:spine1_to_leaf2" - "termination_b": "!ref:leaf2_to_spine1" - "status__name": "Planned" - - "!create_or_update:termination_a_id": "!ref:spine2_to_leaf2.id" - "!create_or_update:termination_b_id": "!ref:leaf2_to_spine2.id" - "termination_a": "!ref:spine2_to_leaf2" - "termination_b": "!ref:leaf2_to_spine2" - status__name: "Planned" - - "!create_or_update:termination_a_id": "!ref:spine3_to_leaf2.id" - "!create_or_update:termination_b_id": "!ref:leaf2_to_spine3.id" - "termination_a": "!ref:spine3_to_leaf2" - "termination_b": "!ref:leaf2_to_spine3" - "status__name": "Planned" -""" - -INPUT_COMPLEX_DESIGN2 = """ -manufacturers: - - "name": "manufacturer" - -device_types: - - "manufacturer__name": "manufacturer" - "model": "model name" - "u_height": 1 - -device_roles: - - "name": "EVPN Leaf" - -sites: - - "name": "site" - "status__name": "Active" - -devices: - - "!create_or_update:name": leaf1 - "status__name": "Active" - "site__name": "site" - "device_role__name": "EVPN Leaf" - "device_type__model": "model name" - "interfaces": - - "!create_or_update:name": "Ethernet33/1" - "type": "100gbase-x-qsfp28" - "!ref": "lag1" - "status__name": "Active" - - "!create_or_update:name": "Ethernet34/1" - "type": "100gbase-x-qsfp28" - "!ref": "lag2" - "status__name": "Active" - - "!create_or_update:name": "Ethernet35/1" - "type": "100gbase-x-qsfp28" - "!ref": "lag3" - "status__name": "Active" - - name: "PortChannel1" - type: lag - status__name: "Active" - description: "MLAG" - mtu: 9214 - member_interfaces: - - "!ref:lag1" - - "!ref:lag2" -""" - -INPUT_PRIMARY_INTERFACE_ADDRESSES = """ -manufacturers: - - name: "manufacturer1" - -device_types: - - manufacturer__name: "manufacturer1" - model: "model name" - u_height: 1 - -device_roles: - - name: "device role" - -sites: - - name: "site_1" - status__name: "Active" - -devices: - - name: "device_1" - site__name: "site_1" - status__name: "Active" - device_type__model: "model name" - device_role__name: "device role" - interfaces: - - name: "Ethernet1/1" - type: "virtual" - status__name: "Active" - description: "description for Ethernet1/1" - ip_addresses: - - address: 192.168.56.1/24 - status__name: "Active" - primary_ip4: {"!get:address": "192.168.56.1/24"} - """ - - -class TestProvisioner(TestCase): # pylint:disable=too-many-public-methods - builder = None + +class BuilderChecks: + """Collection of static methods for testing designs.""" + + @staticmethod + def check_connected(test, check, index): + """Check if two endpoints are connected with a cable.""" + value0 = _get_value(check[0])[0] + value1 = _get_value(check[1])[0] + cables = Cable.objects.filter( + Q(termination_a_id=value0.id, termination_b_id=value1.id) + | Q(termination_a_id=value1.id, termination_b_id=value0.id) + ) + test.assertEqual(1, cables.count(), msg=f"Check {index}") + + @staticmethod + def check_count(test, check, index): + """Check the number of items in a collection.""" + values = _get_value(check) + test.assertEqual(check["count"], len(values), msg=f"Check {index}") + + @staticmethod + def check_equal(test, check, index): + """Check that two values are equal.""" + value0 = _get_value(check[0]) + value1 = _get_value(check[1]) + if len(value0) == 1 and len(value1) == 1: + test.assertEqual(value0[0], value1[0], msg=f"Check {index}") + test.assertEqual(value0, value1, msg=f"Check {index}") + + @staticmethod + def check_model_exists(test, check, index): + """Check that a model exists.""" + values = _get_value(check) + test.assertEqual(len(values), 1, msg=f"Check {index}") + + @staticmethod + def check_model_not_exist(test, check, index): + """Check that a model does not exist.""" + values = _get_value(check) + test.assertEqual(len(values), 0, msg=f"Check {index}") + + +def _get_value(check_info): + if "value" in check_info: + value = check_info["value"] + if isinstance(value, list): + return value + return [check_info["value"]] + if "model" in check_info: + model_class = _load_class(check_info["model"]) + queryset = model_class.objects.filter(**check_info.get("query", {})) + if len(queryset) == 0: + return [] + value = [] + for model in queryset: + if "attribute" in check_info: + model = attrgetter(check_info["attribute"])(model) + if isinstance(model, Manager): + value.extend(model.all()) + elif callable(model): + value.append(model()) + else: + value.append(model) + else: + value.append(model) + return value + raise ValueError(f"Can't get value for {check_info}") + + +def _load_class(class_path): + module_name, class_name = class_path.rsplit(".", 1) + module = importlib.import_module(module_name) + return getattr(module, class_name) + + +def _testcases(data_dir): + for filename in os.listdir(data_dir): + if filename.endswith(".yaml"): + with open(os.path.join(data_dir, filename), encoding="utf-8") as file: + yield yaml.safe_load(file), filename + + +def builder_test_case(data_dir): + """Decorator to load tests into a TestCase from a data directory.""" + + def class_wrapper(test_class): + for testcase, filename in _testcases(data_dir): + # Strip the .yaml extension + testcase_name = f"test_{filename[:-5]}" + + # Create a new closure for testcase + def test_wrapper(testcase): + @patch("nautobot_design_builder.design.Builder.roll_back") + def test_runner(self, roll_back: Mock): + if testcase.get("skip", False): + self.skipTest("Skipping due to testcase skip=true") + extensions = [] + for extension in testcase.get("extensions", []): + extensions.append(_load_class(extension)) + + for design in testcase["designs"]: + builder = Builder(extensions=extensions) + commit = design.pop("commit", True) + builder.implement_design(design=design, commit=commit) + if not commit: + roll_back.assert_called() + + for index, check in enumerate(testcase.get("checks", [])): + for check_name, args in check.items(): + _check_name = f"check_{check_name}" + if hasattr(BuilderChecks, _check_name): + getattr(BuilderChecks, _check_name)(self, args, index) + else: + raise ValueError(f"Unknown check {check_name} {check}") + + return test_runner + + setattr(test_class, testcase_name, test_wrapper(testcase)) + return test_class + + return class_wrapper + + +@builder_test_case(os.path.join(os.path.dirname(__file__), "testdata")) +class TestGeneralDesigns(TestCase): # pylint:disable=too-many-public-methods + """Designs that should work with all versions of Nautobot.""" + + +@builder_test_case(os.path.join(os.path.dirname(__file__), "testdata", "nautobot_v1")) +class TestV1Designs(TestCase): # pylint:disable=too-many-public-methods + """Designs that only work in Nautobot 1.x""" def setUp(self): if nautobot_version >= "2.0.0": self.skipTest("These tests are only supported in Nautobot 1.x") super().setUp() - def implement_design(self, design_input, commit=True): - """Convenience function for implementing a design.""" - self.builder = Builder() - self.builder.implement_design(design=yaml.safe_load(design_input), commit=commit) - - def test_create(self): - self.implement_design(INPUT_CREATE_OBJECTS) - - for want in ["manufacturer1", "manufacturer2"]: - got = Manufacturer.objects.get(name=want).name - self.assertEqual(want, got) - - got = DeviceType.objects.first().manufacturer - want = Manufacturer.objects.get(name="manufacturer1") - self.assertEqual(want, got) - - def test_update(self): - self.implement_design(INPUT_CREATE_OBJECTS) - self.implement_design(INPUT_UPDATE_OBJECT) - got = DeviceType.objects.first().manufacturer - want = Manufacturer.objects.get(name="manufacturer2") - self.assertEqual(want, got) - - def test_update_with_ref(self): - self.implement_design(INPUT_UPDATE_OBJECT_1) - want = "new model name" - got = DeviceType.objects.first().model - self.assertEqual(want, got) - - def test_nested_create(self): - self.implement_design(INPUT_CREATE_NESTED_OBJECTS) - - site = Site.objects.get(name="site_1") - device = Device.objects.get(name="device_1") - self.assertEqual(site, device.site) - - interface = Interface.objects.get(name="Ethernet1/1") - self.assertEqual(list(device.interfaces.all()), [interface]) - - def test_nested_update(self): - self.implement_design(INPUT_CREATE_NESTED_OBJECTS) - interface = Interface.objects.get(name="Ethernet1/1") - self.assertEqual("description for Ethernet1/1", interface.description) - - self.implement_design(INPUT_UPDATE_NESTED_OBJECTS) - - interface.refresh_from_db() - self.assertEqual("new description for Ethernet1/1", interface.description) - - def test_many_to_many(self): - self.implement_design(INPUT_MANY_TO_MANY_OBJECTS) - region = Region.objects.first() - context = ConfigContext.objects.first() - - self.assertIn(region, context.regions.all()) - try: - Region.objects.get(name="My cool new region") - except ObjectDoesNotExist: - self.fail("Failed to find newly created region") - - def test_one_to_one(self): - self.implement_design(INPUT_ONE_TO_ONE_OBJECTS) - device = Device.objects.all()[0] - want = Device.objects.all()[1] - self.assertEqual(1, len(device.devicebays.all())) - - got = device.devicebays.first().installed_device - self.assertEqual(want, got) - - def test_prefixes(self): - self.implement_design(INPUT_PREFIXES) - want = "192.168.0.0/24" - got = str(Prefix.objects.all()[0]) - self.assertEqual(want, got) - - want = "192.168.56.0/24" - got = str(Prefix.objects.all()[1]) - self.assertEqual(want, got) - - def test_interface_addresses(self): - self.implement_design(INPUT_INTERFACE_ADDRESSES) - want = "192.168.56.1/24" - address = IPAddress.objects.get(address="192.168.56.1/24") - got = str(address) - self.assertEqual(want, got) - - want = [address] - got = list(Interface.objects.first().ip_addresses.all()) - self.assertEqual(want, got) - - def test_create_tags(self): - self.implement_design(INPUT_CREATE_TAGS) - want = "Some Description" - tag = Tag.objects.get(name="Test Tag") - got = tag.description - self.assertEqual(want, got) - - def test_assign_tags(self): - self.implement_design(INPUT_ASSIGN_TAGS) - tag = Tag.objects.get(name="Test Tag") - site = Site.objects.first() - self.assertIn(tag, list(site.tags.all())) - - def test_assign_tags_by_name(self): - self.implement_design(INPUT_ASSIGN_TAGS_1) - tag = Tag.objects.get(name="Test Tag") - site = Site.objects.first() - self.assertIn(tag, list(site.tags.all())) - - def test_create_mlag(self): - self.implement_design(INPUT_CREATE_MLAG) - device = Device.objects.get(name="device_1") - lag = device.interfaces.get(name="Port-Channel1") - self.assertEqual(4, lag.member_interfaces.count()) - interfaces = [i.name for i in lag.member_interfaces.all()] - for i in range(1, 5): - self.assertIn(f"Ethernet{i}/1", interfaces) - - def test_custom_relation(self): - device_type = ContentType.objects.get_for_model(Device) - relationship, _ = Relationship.objects.get_or_create( - name="Device to VLANS", - defaults={ - "slug": "device-to-vlans", - "type": RelationshipTypeChoices.TYPE_MANY_TO_MANY, - "source_type": device_type, - "destination_type": ContentType.objects.get_for_model(VLAN), - }, - ) - self.implement_design(INPUT_CREATE_NESTED_OBJECTS) - self.implement_design(INPUT_CUSTOM_RELATION) - vlan42 = VLAN.objects.get(vid=42) - vlan43 = VLAN.objects.get(vid=43) - - device = Device.objects.get(name="device_1") - query_params = {"relationship": relationship, "source_id": device.pk, "source_type": device_type} - vlans = [obj.destination for obj in RelationshipAssociation.objects.filter(**query_params)] - self.assertIn(vlan42, vlans) - self.assertIn(vlan43, vlans) - - @patch("nautobot_design_builder.design.Builder.roll_back") - def test_simple_design_roll_back(self, roll_back: Mock): - self.implement_design(INPUT_CREATE_OBJECTS, False) - roll_back.assert_called() - - def test_create_or_update_with_ref(self): - # run it twice to make sure it is idempotent - for _ in range(2): - self.implement_design(INPUT_REF_FOR_CREATE_OR_UPDATE) - self.assertEqual(2, len(Secret.objects.all())) - self.assertEqual(1, len(SecretsGroup.objects.all())) - self.assertEqual(2, len(SecretsGroupAssociation.objects.all())) - - def test_complex_design1(self): - self.implement_design(INPUT_COMPLEX_DESIGN1) - devices = {} - for role in ["leaf", "spine"]: - for i in [1, 2, 3]: - if role == "leaf" and i == 3: - continue - device = Device.objects.get(name=f"{role}{i}") - devices[device.name] = device - - cables = Cable.objects.all().order_by("_termination_a_device__name", "_termination_b_device__name") - self.assertEqual(6, len(cables)) - self.assertEqual(cables[0].termination_a.device, devices["spine1"]) - self.assertEqual(cables[0].termination_b.device, devices["leaf1"]) - self.assertEqual(cables[1].termination_a.device, devices["spine1"]) - self.assertEqual(cables[1].termination_b.device, devices["leaf2"]) - self.assertEqual(cables[2].termination_a.device, devices["spine2"]) - self.assertEqual(cables[2].termination_b.device, devices["leaf1"]) - self.assertEqual(cables[3].termination_a.device, devices["spine2"]) - self.assertEqual(cables[3].termination_b.device, devices["leaf2"]) - self.assertEqual(cables[4].termination_a.device, devices["spine3"]) - self.assertEqual(cables[4].termination_b.device, devices["leaf1"]) - self.assertEqual(cables[5].termination_a.device, devices["spine3"]) - self.assertEqual(cables[5].termination_b.device, devices["leaf2"]) - - def test_complex_design2(self): - self.implement_design(INPUT_COMPLEX_DESIGN2) - device = Device.objects.first() - interfaces = device.interfaces.filter(name__startswith="Ethernet") - mlag = device.interfaces.get(name="PortChannel1") - self.assertEqual(mlag.member_interfaces.all()[0], interfaces[0]) - self.assertEqual(mlag.member_interfaces.all()[1], interfaces[1]) - - def test_create_or_update_relationship(self): - design = """ - manufacturers: - - name: "Vendor" - device_types: - - "!create_or_update:model": "test model" - "!create_or_update:manufacturer__name": "Vendor" - device_roles: - - "name": "role" - sites: - - "name": "Site" - "status__name": "Active" - devices: - - "!create_or_update:name": "test device" - "!create_or_update:device_type__manufacturer__name": "Vendor" - "device_role__name": "role" - "site__name": "Site" - "status__name": "Active" - """ - self.implement_design(design) - device_type = DeviceType.objects.get(model="test model") - self.assertEqual("Vendor", device_type.manufacturer.name) - device = Device.objects.get(name="test device") - self.assertEqual(device_type, device.device_type) - - def test_primary_interface_addresses(self): - self.implement_design(INPUT_PRIMARY_INTERFACE_ADDRESSES) - want = "192.168.56.1/24" - device = Device.objects.get(name="device_1") - self.assertEqual(want, str(device.primary_ip4.address)) - - def test_create_or_update_rack(self): - design = """ - manufacturers: - - name: "Vendor" - device_types: - - "!create_or_update:model": "test model" - "!create_or_update:manufacturer__name": "Vendor" - device_roles: - - "name": "role" - sites: - - "name": "Site" - "status__name": "Active" - devices: - - "!create_or_update:name": "test device" - "!create_or_update:device_type__manufacturer__name": "Vendor" - "device_role__name": "role" - "site__name": "Site" - "status__name": "Active" - "rack": - "!create_or_update:name": "rack-1" - "!create_or_update:site__name": "Site" - "status__name": "Active" - """ - self.implement_design(design) - device = Device.objects.get(name="test device") - self.assertEqual("rack-1", device.rack.name) + +@builder_test_case(os.path.join(os.path.dirname(__file__), "testdata", "nautobot_v2")) +class TestV2Designs(TestCase): # pylint:disable=too-many-public-methods + """Designs that only work in Nautobot 1.x""" + + def setUp(self): + if nautobot_version < "2.0.0": + self.skipTest("These tests are only supported in Nautobot 2.x") + super().setUp() diff --git a/nautobot_design_builder/tests/testdata/many_to_many.yaml b/nautobot_design_builder/tests/testdata/many_to_many.yaml new file mode 100644 index 00000000..4691f14c --- /dev/null +++ b/nautobot_design_builder/tests/testdata/many_to_many.yaml @@ -0,0 +1,28 @@ +--- +designs: + - manufacturers: + - name: "manufacturer1" + + device_types: + - manufacturer__name: "manufacturer1" + model: "model1" + "!ref": "model1" + + config_contexts: + - name: "My Context" + data: + foo: 123 + device_types: + - "!ref:model1" + - model: "model2" + manufacturer__name: "manufacturer1" +checks: + - model_exists: + model: "nautobot.dcim.models.DeviceType" + query: {model: "model2"} + + - equal: + - model: "nautobot.dcim.models.DeviceType" + - model: "nautobot.extras.models.ConfigContext" + query: {name: "My Context"} + attribute: "device_types" diff --git a/nautobot_design_builder/tests/testdata/nautobot_v1/assign_tags_by_name.yaml b/nautobot_design_builder/tests/testdata/nautobot_v1/assign_tags_by_name.yaml new file mode 100644 index 00000000..21faef8d --- /dev/null +++ b/nautobot_design_builder/tests/testdata/nautobot_v1/assign_tags_by_name.yaml @@ -0,0 +1,19 @@ +--- +designs: + - tags: + - name: "Test Tag" + "!ref": "test_tag" + slug: "test_tag" + description: "Some Description" + + sites: + - name: "site_1" + status__name: "Active" + tags: + - {"!get:name": "Test Tag"} +checks: + - equal: + - model: "nautobot.dcim.models.Site" + query: {name: "site_1"} + attribute: "tags" + - model: "nautobot.extras.models.Tag" diff --git a/nautobot_design_builder/tests/testdata/nautobot_v1/assign_tags_by_ref.yaml b/nautobot_design_builder/tests/testdata/nautobot_v1/assign_tags_by_ref.yaml new file mode 100644 index 00000000..ec426a45 --- /dev/null +++ b/nautobot_design_builder/tests/testdata/nautobot_v1/assign_tags_by_ref.yaml @@ -0,0 +1,19 @@ +--- +designs: + - tags: + - name: "Test Tag" + "!ref": "test_tag" + slug: "test_tag" + description: "Some Description" + + sites: + - name: "site_1" + status__name: "Active" + tags: + - "!ref:test_tag" +checks: + - equal: + - model: "nautobot.dcim.models.Site" + query: {name: "site_1"} + attribute: "tags" + - model: "nautobot.extras.models.Tag" diff --git a/nautobot_design_builder/tests/testdata/nautobot_v1/complex_design1.yaml b/nautobot_design_builder/tests/testdata/nautobot_v1/complex_design1.yaml new file mode 100644 index 00000000..14bf76cc --- /dev/null +++ b/nautobot_design_builder/tests/testdata/nautobot_v1/complex_design1.yaml @@ -0,0 +1,172 @@ +--- +checks: + # Spine 1 to Leaf 1 + - connected: + - model: "nautobot.dcim.models.Interface" + query: {device__name: "spine1", name: "Ethernet9/3"} + - model: "nautobot.dcim.models.Interface" + query: {device__name: "leaf1", name: "Ethernet33/1"} + + # Spine 1 to Leaf 2 + - connected: + - model: "nautobot.dcim.models.Interface" + query: {device__name: "spine1", name: "Ethernet25/3"} + - model: "nautobot.dcim.models.Interface" + query: {device__name: "leaf2", name: "Ethernet33/1"} + + # Spine 2 to Leaf 1 + - connected: + - model: "nautobot.dcim.models.Interface" + query: {device__name: "spine2", name: "Ethernet9/3"} + - model: "nautobot.dcim.models.Interface" + query: {device__name: "leaf1", name: "Ethernet34/1"} + + # Spine 2 to Leaf 2 + - connected: + - model: "nautobot.dcim.models.Interface" + query: {device__name: "spine2", name: "Ethernet25/3"} + - model: "nautobot.dcim.models.Interface" + query: {device__name: "leaf2", name: "Ethernet34/1"} + + # Spine 3 to Leaf 1 + - connected: + - model: "nautobot.dcim.models.Interface" + query: {device__name: "spine3", name: "Ethernet9/3"} + - model: "nautobot.dcim.models.Interface" + query: {device__name: "leaf1", name: "Ethernet35/1"} + + # Spine 3 to Leaf 2 + - connected: + - model: "nautobot.dcim.models.Interface" + query: {device__name: "spine3", name: "Ethernet25/3"} + - model: "nautobot.dcim.models.Interface" + query: {device__name: "leaf2", name: "Ethernet35/1"} +designs: + - manufacturers: + - "name": "manufacturer" + + device_types: + - "manufacturer__name": "manufacturer" + "model": "model name" + "u_height": 1 + + device_roles: + - "name": "EVPN Leaf" + - "name": "EVPN Spine" + + sites: + - "name": "site" + "status__name": "Active" + + devices: + # Create Spine Switches + - "!create_or_update:name": "spine1" + "status__name": "Active" + "site__name": "site" + "device_role__name": "EVPN Spine" + "device_type__model": "model name" + "interfaces": + - "!create_or_update:name": "Ethernet9/3" + "type": "100gbase-x-qsfp28" + "status__name": "Active" + "!ref": "spine1_to_leaf1" + - "!create_or_update:name": "Ethernet25/3" + "type": "100gbase-x-qsfp28" + "status__name": "Active" + "!ref": "spine1_to_leaf2" + - "!create_or_update:name": "spine2" + "status__name": "Active" + "site__name": "site" + "device_role__name": "EVPN Spine" + "device_type__model": "model name" + "interfaces": + - "!create_or_update:name": "Ethernet9/3" + "type": "100gbase-x-qsfp28" + "status__name": "Active" + "!ref": "spine2_to_leaf1" + - "!create_or_update:name": "Ethernet25/3" + "type": "100gbase-x-qsfp28" + "status__name": "Active" + "!ref": "spine2_to_leaf2" + - "!create_or_update:name": "spine3" + "status__name": "Active" + "site__name": "site" + "device_role__name": "EVPN Spine" + "device_type__model": "model name" + "interfaces": + - "!create_or_update:name": "Ethernet9/3" + "type": "100gbase-x-qsfp28" + "status__name": "Active" + "!ref": "spine3_to_leaf1" + - "!create_or_update:name": "Ethernet25/3" + "type": "100gbase-x-qsfp28" + "status__name": "Active" + "!ref": "spine3_to_leaf2" + - "!create_or_update:name": "leaf1" + "status__name": "Active" + "site__name": "site" + "device_role__name": "EVPN Leaf" + "device_type__model": "model name" + "interfaces": + - "!create_or_update:name": "Ethernet33/1" + "type": "100gbase-x-qsfp28" + "!ref": "leaf1_to_spine1" + "status__name": "Active" + - "!create_or_update:name": "Ethernet34/1" + "type": "100gbase-x-qsfp28" + "!ref": "leaf1_to_spine2" + "status__name": "Active" + - "!create_or_update:name": "Ethernet35/1" + "type": "100gbase-x-qsfp28" + "!ref": "leaf1_to_spine3" + "status__name": "Active" + - "!create_or_update:name": "leaf2" + "status__name": "Active" + "site__name": "site" + "device_role__name": "EVPN Leaf" + "device_type__model": "model name" + "interfaces": + - "!create_or_update:name": "Ethernet33/1" + "type": "100gbase-x-qsfp28" + "!ref": "leaf2_to_spine1" + "status__name": "Active" + - "!create_or_update:name": "Ethernet34/1" + "type": "100gbase-x-qsfp28" + "!ref": "leaf2_to_spine2" + "status__name": "Active" + - "!create_or_update:name": "Ethernet35/1" + "type": "100gbase-x-qsfp28" + "!ref": "leaf2_to_spine3" + "status__name": "Active" + + cables: + - "!create_or_update:termination_a_id": "!ref:spine1_to_leaf1.id" + "!create_or_update:termination_b_id": "!ref:leaf1_to_spine1.id" + "termination_a": "!ref:spine1_to_leaf1" + "termination_b": "!ref:leaf1_to_spine1" + "status__name": "Planned" + - "!create_or_update:termination_a_id": "!ref:spine2_to_leaf1.id" + "!create_or_update:termination_b_id": "!ref:leaf1_to_spine2.id" + "termination_a": "!ref:spine2_to_leaf1" + "termination_b": "!ref:leaf1_to_spine2" + "status__name": "Planned" + - "!create_or_update:termination_a_id": "!ref:spine3_to_leaf1.id" + "!create_or_update:termination_b_id": "!ref:leaf1_to_spine3.id" + "termination_a": "!ref:spine3_to_leaf1" + "termination_b": "!ref:leaf1_to_spine3" + "status__name": "Planned" + - "!create_or_update:termination_a_id": "!ref:spine1_to_leaf2.id" + "!create_or_update:termination_b_id": "!ref:leaf2_to_spine1.id" + "termination_a": "!ref:spine1_to_leaf2" + "termination_b": "!ref:leaf2_to_spine1" + "status__name": "Planned" + - "!create_or_update:termination_a_id": "!ref:spine2_to_leaf2.id" + "!create_or_update:termination_b_id": "!ref:leaf2_to_spine2.id" + "termination_a": "!ref:spine2_to_leaf2" + "termination_b": "!ref:leaf2_to_spine2" + "status__name": "Planned" + - "!create_or_update:termination_a_id": "!ref:spine3_to_leaf2.id" + "!create_or_update:termination_b_id": "!ref:leaf2_to_spine3.id" + "termination_a": "!ref:spine3_to_leaf2" + "termination_b": "!ref:leaf2_to_spine3" + "status__name": "Planned" diff --git a/nautobot_design_builder/tests/testdata/nautobot_v1/create_or_update_by_ref.yaml b/nautobot_design_builder/tests/testdata/nautobot_v1/create_or_update_by_ref.yaml new file mode 100644 index 00000000..2ec509f1 --- /dev/null +++ b/nautobot_design_builder/tests/testdata/nautobot_v1/create_or_update_by_ref.yaml @@ -0,0 +1,63 @@ +--- +designs: + # Design 1 + - secrets: + - "!create_or_update:name": "Device username" + "description": "Username for network devices" + "provider": "environment-variable" + "parameters": {"variable": "NAUTOBOT_NAPALM_USERNAME"} + "!ref": "device_username" + - "!create_or_update:name": "Device password" + "description": "Password for network devices" + "provider": "environment-variable" + "parameters": {"variable": "NAUTOBOT_NAPALM_PASSWORD"} + "!ref": "device_password" + + secrets_groups: + - "!create_or_update:name": "Device credentials" + "!ref": "device_credentials" + + secrets_group_associations: + - "!create_or_update:group": "!ref:device_credentials" + "!create_or_update:secret": "!ref:device_username" + "access_type": "Generic" + "secret_type": "username" + - "!create_or_update:group": "!ref:device_credentials" + "!create_or_update:secret": "!ref:device_password" + "access_type": "Generic" + "secret_type": "password" + # Design 2 + - secrets: + - "!create_or_update:name": "Device username" + "description": "Username for network devices" + "provider": "environment-variable" + "parameters": {"variable": "NAUTOBOT_NAPALM_USERNAME"} + "!ref": "device_username" + - "!create_or_update:name": "Device password" + "description": "Password for network devices" + "provider": "environment-variable" + "parameters": {"variable": "NAUTOBOT_NAPALM_PASSWORD"} + "!ref": "device_password" + secrets_groups: + - "!create_or_update:name": "Device credentials" + "!ref": "device_credentials" + + secrets_group_associations: + - "!create_or_update:group": "!ref:device_credentials" + "!create_or_update:secret": "!ref:device_username" + "access_type": "Generic" + "secret_type": "username" + - "!create_or_update:group": "!ref:device_credentials" + "!create_or_update:secret": "!ref:device_password" + "access_type": "Generic" + "secret_type": "password" +checks: + - count: + model: "nautobot.extras.models.Secret" + count: 2 + - count: + model: "nautobot.extras.models.SecretsGroup" + count: 1 + - count: + model: "nautobot.extras.models.SecretsGroupAssociation" + count: 2 diff --git a/nautobot_design_builder/tests/testdata/nautobot_v1/create_or_update_mlag.yaml b/nautobot_design_builder/tests/testdata/nautobot_v1/create_or_update_mlag.yaml new file mode 100644 index 00000000..bda2b1a6 --- /dev/null +++ b/nautobot_design_builder/tests/testdata/nautobot_v1/create_or_update_mlag.yaml @@ -0,0 +1,55 @@ +--- +designs: + - manufacturers: + - name: "manufacturer1" + + device_types: + - manufacturer__name: "manufacturer1" + model: "model name" + u_height: 1 + + device_roles: + - name: "device role" + + sites: + - name: "site_1" + status__name: "Active" + + devices: + - name: "device_1" + site__name: "site_1" + status__name: "Active" + device_type__model: "model name" + device_role__name: "device role" + interfaces: + - name: "Ethernet1/1" + type: "1000base-t" + status__name: "Active" + "!ref": "ethernet11" + - name: "Ethernet2/1" + type: "1000base-t" + status__name: "Active" + "!ref": "ethernet21" + - name: "Ethernet3/1" + type: "1000base-t" + status__name: "Active" + "!ref": "ethernet31" + - name: "Ethernet4/1" + type: "1000base-t" + status__name: "Active" + "!ref": "ethernet41" + - name: "Port-Channel1" + type: "lag" + status__name: "Active" + member_interfaces: + - "!ref:ethernet11" + - "!ref:ethernet21" + - "!ref:ethernet31" + - "!ref:ethernet41" +checks: + - equal: + - model: "nautobot.dcim.models.Interface" + query: {name: "Port-Channel1"} + attribute: "member_interfaces" + - model: "nautobot.dcim.models.Interface" + query: {name__startswith: "Ethernet"} diff --git a/nautobot_design_builder/tests/testdata/nautobot_v1/create_or_update_rack.yaml b/nautobot_design_builder/tests/testdata/nautobot_v1/create_or_update_rack.yaml new file mode 100644 index 00000000..599d64ea --- /dev/null +++ b/nautobot_design_builder/tests/testdata/nautobot_v1/create_or_update_rack.yaml @@ -0,0 +1,28 @@ +--- +designs: + - manufacturers: + - name: "Vendor" + device_types: + - "!create_or_update:model": "test model" + "!create_or_update:manufacturer__name": "Vendor" + device_roles: + - name: "role" + sites: + - name: "Site" + status__name: "Active" + devices: + - "!create_or_update:name": "test device" + "!create_or_update:device_type__manufacturer__name": "Vendor" + device_role__name: "role" + site__name: "Site" + status__name: "Active" + rack: + "!create_or_update:name": "rack-1" + "!create_or_update:site__name": "Site" + status__name: "Active" +checks: + - equal: + - model: "nautobot.dcim.models.Device" + query: {name: "test device"} + attribute: "rack.name" + - value: "rack-1" diff --git a/nautobot_design_builder/tests/testdata/nautobot_v1/create_or_update_relationships.yaml b/nautobot_design_builder/tests/testdata/nautobot_v1/create_or_update_relationships.yaml new file mode 100644 index 00000000..b0544ecb --- /dev/null +++ b/nautobot_design_builder/tests/testdata/nautobot_v1/create_or_update_relationships.yaml @@ -0,0 +1,30 @@ +--- +designs: + - manufacturers: + - name: "Vendor" + device_types: + - "!create_or_update:model": "test model" + "!create_or_update:manufacturer__name": "Vendor" + device_roles: + - "name": "role" + sites: + - "name": "Site" + "status__name": "Active" + devices: + - "!create_or_update:name": "test device" + "!create_or_update:device_type__manufacturer__name": "Vendor" + "device_role__name": "role" + "site__name": "Site" + "status__name": "Active" +checks: + - equal: + - model: "nautobot.dcim.models.DeviceType" + query: {model: "test model"} + attribute: "manufacturer.name" + - value: "Vendor" + - equal: + - model: "nautobot.dcim.models.DeviceType" + query: {model: "test model"} + - model: "nautobot.dcim.models.Device" + query: {name: "test device"} + attribute: "device_type" diff --git a/nautobot_design_builder/tests/testdata/nautobot_v1/create_tags.yaml b/nautobot_design_builder/tests/testdata/nautobot_v1/create_tags.yaml new file mode 100644 index 00000000..2312d64d --- /dev/null +++ b/nautobot_design_builder/tests/testdata/nautobot_v1/create_tags.yaml @@ -0,0 +1,10 @@ +--- +designs: + - tags: + - name: "Test Tag" + slug: "test_tag" + description: "Some Description" +checks: + - model_exists: + model: "nautobot.extras.models.Tag" + query: {name: "Test Tag"} diff --git a/nautobot_design_builder/tests/testdata/nautobot_v1/custom_relationship.yaml b/nautobot_design_builder/tests/testdata/nautobot_v1/custom_relationship.yaml new file mode 100644 index 00000000..fae287b7 --- /dev/null +++ b/nautobot_design_builder/tests/testdata/nautobot_v1/custom_relationship.yaml @@ -0,0 +1,55 @@ +--- +extensions: + - "nautobot_design_builder.contrib.ext.LookupExtension" +designs: + - relationships: + - name: "Device to VLANS" + slug: "device-to-vlans" + type: "many-to-many" + "!lookup:source_type": + app_label: "dcim" + model: "device" + "!lookup:destination_type": + app_label: "ipam" + model: "vlan" + manufacturers: + - name: "manufacturer1" + + device_types: + - manufacturer__name: "manufacturer1" + model: "model name" + u_height: 1 + + device_roles: + - name: "device role" + + sites: + - name: "site_1" + status__name: "Active" + + vlans: + - "!create_or_update:vid": 42 + name: "The Answer" + status__name: "Active" + + devices: + - name: "device_1" + site__name: "site_1" + status__name: "Active" + device_type__model: "model name" + device_role__name: "device role" + device-to-vlans: + - "!get:vid": 42 + - vid: "43" + name: "Better Answer" + status__name: "Active" +checks: + - model_exists: + model: "nautobot.ipam.models.VLAN" + query: {vid: "43"} + + - equal: + - model: "nautobot.extras.models.RelationshipAssociation" + query: {relationship__name: "Device to VLANS"} + attribute: "destination" + - model: "nautobot.ipam.models.VLAN" diff --git a/nautobot_design_builder/tests/testdata/nautobot_v1/device_primary_ip.yaml b/nautobot_design_builder/tests/testdata/nautobot_v1/device_primary_ip.yaml new file mode 100644 index 00000000..364b3fdd --- /dev/null +++ b/nautobot_design_builder/tests/testdata/nautobot_v1/device_primary_ip.yaml @@ -0,0 +1,38 @@ +--- +designs: + - manufacturers: + - name: "manufacturer1" + + device_types: + - manufacturer__name: "manufacturer1" + model: "model name" + u_height: 1 + + device_roles: + - name: "device role" + + sites: + - name: "site_1" + status__name: "Active" + + devices: + - name: "device_1" + site__name: "site_1" + status__name: "Active" + device_type__model: "model name" + device_role__name: "device role" + interfaces: + - name: "Ethernet1/1" + type: "virtual" + status__name: "Active" + description: "description for Ethernet1/1" + ip_addresses: + - address: "192.168.56.1/24" + status__name: "Active" + primary_ip4: {"!get:address": "192.168.56.1/24"} +checks: + - equal: + - model: "nautobot.dcim.models.Device" + query: {name: "device_1"} + attribute: "primary_ip4.address.__str__" + - value: "192.168.56.1/24" diff --git a/nautobot_design_builder/tests/testdata/nautobot_v1/interface_addresses.yaml b/nautobot_design_builder/tests/testdata/nautobot_v1/interface_addresses.yaml new file mode 100644 index 00000000..b2ca3fe2 --- /dev/null +++ b/nautobot_design_builder/tests/testdata/nautobot_v1/interface_addresses.yaml @@ -0,0 +1,41 @@ +--- +designs: + - manufacturers: + - name: "manufacturer1" + + device_types: + - manufacturer__name: "manufacturer1" + model: "model name" + u_height: 1 + + device_roles: + - name: "device role" + + sites: + - name: "site_1" + status__name: "Active" + + devices: + - name: "device_1" + site__name: "site_1" + status__name: "Active" + device_type__model: "model name" + device_role__name: "device role" + interfaces: + - name: "Ethernet1/1" + type: "virtual" + status__name: "Active" + description: "description for Ethernet1/1" + ip_addresses: + - address: "192.168.56.1/24" + status__name: "Active" +checks: + - model_exists: + model: "nautobot.ipam.models.IPAddress" + query: {address: "192.168.56.1/24"} + - equal: + - model: "nautobot.dcim.models.Interface" + query: {name: "Ethernet1/1"} + - model: "nautobot.ipam.models.IPAddress" + query: {address: "192.168.56.1/24"} + attribute: "interface" diff --git a/nautobot_design_builder/tests/testdata/nautobot_v1/nested_create.yaml b/nautobot_design_builder/tests/testdata/nautobot_v1/nested_create.yaml new file mode 100644 index 00000000..fde23df6 --- /dev/null +++ b/nautobot_design_builder/tests/testdata/nautobot_v1/nested_create.yaml @@ -0,0 +1,41 @@ +--- +designs: + - manufacturers: + - name: "manufacturer1" + + device_types: + - manufacturer__name: "manufacturer1" + model: "model name" + u_height: 1 + + device_roles: + - name: "device role" + + sites: + - name: "site_1" + status__name: "Active" + + devices: + - name: "device_1" + site__name: "site_1" + status__name: "Active" + device_type__model: "model name" + device_role__name: "device role" + interfaces: + - name: "Ethernet1/1" + type: "virtual" + status__name: "Active" + description: "description for Ethernet1/1" +checks: + - equal: + - model: "nautobot.dcim.models.Interface" + query: {name: "Ethernet1/1"} + attribute: "device" + - model: "nautobot.dcim.models.Device" + query: {name: "device_1"} + - equal: + - model: "nautobot.dcim.models.Device" + query: {name: "device_1"} + attribute: "site" + - model: "nautobot.dcim.models.Site" + query: {name: "site_1"} diff --git a/nautobot_design_builder/tests/testdata/nautobot_v1/nested_update.yaml b/nautobot_design_builder/tests/testdata/nautobot_v1/nested_update.yaml new file mode 100644 index 00000000..dd4a58ab --- /dev/null +++ b/nautobot_design_builder/tests/testdata/nautobot_v1/nested_update.yaml @@ -0,0 +1,47 @@ +--- +designs: + # Design 1 + - manufacturers: + - name: "manufacturer1" + + device_types: + - manufacturer__name: "manufacturer1" + model: "model name" + u_height: 1 + + device_roles: + - name: "device role" + + sites: + - name: "site_1" + status__name: "Active" + + devices: + - name: "device_1" + site__name: "site_1" + status__name: "Active" + device_type__model: "model name" + device_role__name: "device role" + interfaces: + - name: "Ethernet1/1" + type: "virtual" + status__name: "Active" + description: "description for Ethernet1/1" + # Design 2 + - devices: + - "!update:name": "device_1" + interfaces: + - "!update:name": "Ethernet1/1" + description: "new description for Ethernet1/1" +checks: + - equal: + - model: "nautobot.dcim.models.Interface" + query: {name: "Ethernet1/1"} + attribute: "device" + - model: "nautobot.dcim.models.Device" + query: {name: "device_1"} + - equal: + - model: "nautobot.dcim.models.Interface" + query: {name: "Ethernet1/1"} + attribute: "description" + - value: "new description for Ethernet1/1" diff --git a/nautobot_design_builder/tests/testdata/nautobot_v1/one_to_one.yaml b/nautobot_design_builder/tests/testdata/nautobot_v1/one_to_one.yaml new file mode 100644 index 00000000..c6c599d6 --- /dev/null +++ b/nautobot_design_builder/tests/testdata/nautobot_v1/one_to_one.yaml @@ -0,0 +1,50 @@ +--- +designs: + - manufacturers: + - name: "manufacturer1" + + device_types: + - manufacturer__name: "manufacturer1" + model: "chassis" + u_height: 1 + subdevice_role: "parent" + + - manufacturer__name: "manufacturer1" + model: "card" + u_height: 0 + subdevice_role: "child" + + device_roles: + - name: "device role" + + sites: + - name: "site_1" + status__name: "Active" + + devices: + - name: "device_1" + site__name: "site_1" + status__name: "Active" + device_type__model: "chassis" + device_role__name: "device role" + devicebays: + - name: "Bay 1" + installed_device: + name: "device_2" + site__name: "site_1" + status__name: "Active" + device_type__model: "card" + device_role__name: "device role" +checks: + - model_exists: + model: "nautobot.dcim.models.Device" + query: {name: "device_1"} + - model_exists: + model: "nautobot.dcim.models.Device" + query: {name: "device_2"} + - equal: + - model: "nautobot.dcim.models.Device" + query: {name: "device_2"} + attribute: "parent_bay.device" + - model: "nautobot.dcim.models.Device" + query: {name: "device_1"} diff --git a/nautobot_design_builder/tests/testdata/nautobot_v1/prefixes.yaml b/nautobot_design_builder/tests/testdata/nautobot_v1/prefixes.yaml new file mode 100644 index 00000000..58d241d6 --- /dev/null +++ b/nautobot_design_builder/tests/testdata/nautobot_v1/prefixes.yaml @@ -0,0 +1,20 @@ +--- +designs: + - sites: + - name: "site_1" + status__name: "Active" + + prefixes: + - site__name: "site_1" + status__name: "Active" + prefix: "192.168.0.0/24" + - "!create_or_update:site__name": "site_1" + "!create_or_update:prefix": "192.168.56.0/24" + status__name: "Active" + +checks: + - equal: + - model: "nautobot.ipam.models.Prefix" + query: {site__name: "site_1"} + attribute: "__str__" + - value: ["192.168.0.0/24", "192.168.56.0/24"] diff --git a/nautobot_design_builder/tests/testdata/nautobot_v2/interface_addresses.yaml b/nautobot_design_builder/tests/testdata/nautobot_v2/interface_addresses.yaml new file mode 100644 index 00000000..bc13583d --- /dev/null +++ b/nautobot_design_builder/tests/testdata/nautobot_v2/interface_addresses.yaml @@ -0,0 +1,54 @@ +--- +designs: + - manufacturers: + - name: "manufacturer1" + + device_types: + - manufacturer__name: "manufacturer1" + model: "model name" + u_height: 1 + + roles: + - name: "device role" + content_types: + - "!get:app_label": "dcim" + "!get:model": "device" + + location_types: + - name: "Site" + content_types: + - "!get:app_label": "dcim" + "!get:model": "device" + locations: + name: "site_1" + status__name: "Active" + + prefixes: + - prefix: "192.168.56.0/24" + status__name: "Active" + "!ref": "parent_prefix" + + devices: + - name: "device_1" + location__name: "site_1" + status__name: "Active" + device_type__model: "model name" + role__name: "device role" + interfaces: + - name: "Ethernet1/1" + type: "virtual" + status__name: "Active" + ip_address_assignments: + - ip_address: + "!create_or_update:address": "192.168.56.1/24" + "!create_or_update:parent": "!ref:parent_prefix" + status__name: "Active" +checks: + - model_exists: + model: "nautobot.ipam.models.IPAddress" + query: {address: "192.168.56.1/24"} + - equal: + - model: "nautobot.ipam.models.IPAddressToInterface" + query: {interface__name: "Ethernet1/1"} + attribute: "ip_address" + - model: "nautobot.ipam.models.IPAddress" diff --git a/nautobot_design_builder/tests/testdata/nautobot_v2/ip_address_with_namespace.yaml b/nautobot_design_builder/tests/testdata/nautobot_v2/ip_address_with_namespace.yaml new file mode 100644 index 00000000..ec511f8a --- /dev/null +++ b/nautobot_design_builder/tests/testdata/nautobot_v2/ip_address_with_namespace.yaml @@ -0,0 +1,25 @@ +--- +designs: + - namespaces: + - name: "VRF Namespace" + "!ref": "parent_namespace" + + prefixes: + - namespace__name: "VRF Namespace" + status__name: "Active" + prefix: "192.168.56.0/24" + + ip_addresses: + - address: "192.168.56.1/24" + # Note: `namespace` is a keyword argument in the IPAddress constructor, + # therefore, reflection cannot take place. This means that attribute action tags + # (like !lookup:`) and query params (like `namespace__name`) can't be used. + # If an IPAddress needs to be assigned to a namespace it can only be + # done via a !ref. + namespace: "!ref:parent_namespace" + status__name: "Active" + +checks: + - model_exists: + model: "nautobot.ipam.models.IPAddress" + query: {address: "192.168.56.1/24"} diff --git a/nautobot_design_builder/tests/testdata/nautobot_v2/nested_create.yaml b/nautobot_design_builder/tests/testdata/nautobot_v2/nested_create.yaml new file mode 100644 index 00000000..afd5886b --- /dev/null +++ b/nautobot_design_builder/tests/testdata/nautobot_v2/nested_create.yaml @@ -0,0 +1,49 @@ +--- +designs: + - manufacturers: + - name: "manufacturer1" + + device_types: + - manufacturer__name: "manufacturer1" + model: "model name" + u_height: 1 + + roles: + - name: "device role" + content_types: + - "!get:app_label": "dcim" + "!get:model": "device" + + location_types: + - name: "Site" + content_types: + - "!get:app_label": "dcim" + "!get:model": "device" + locations: + name: "site_1" + status__name: "Active" + + devices: + - name: "device_1" + location__name: "site_1" + status__name: "Active" + device_type__model: "model name" + role__name: "device role" + interfaces: + - name: "Ethernet1/1" + type: "virtual" + status__name: "Active" + description: "description for Ethernet1/1" +checks: + - equal: + - model: "nautobot.dcim.models.Interface" + query: {name: "Ethernet1/1"} + attribute: "device" + - model: "nautobot.dcim.models.Device" + query: {name: "device_1"} + - equal: + - model: "nautobot.dcim.models.Device" + query: {name: "device_1"} + attribute: "location" + - model: "nautobot.dcim.models.Location" + query: {name: "site_1"} diff --git a/nautobot_design_builder/tests/testdata/nautobot_v2/prefixes.yaml b/nautobot_design_builder/tests/testdata/nautobot_v2/prefixes.yaml new file mode 100644 index 00000000..9d3469cb --- /dev/null +++ b/nautobot_design_builder/tests/testdata/nautobot_v2/prefixes.yaml @@ -0,0 +1,25 @@ +--- +designs: + - location_types: + - name: "Site" + content_types: + - "!get:app_label": "ipam" + "!get:model": "prefix" + locations: + name: "site_1" + status__name: "Active" + + prefixes: + - location__name: "site_1" + status__name: "Active" + prefix: "192.168.0.0/24" + - "!create_or_update:location__name": "site_1" + "!create_or_update:prefix": "192.168.56.0/24" + status__name: "Active" + +checks: + - equal: + - model: "nautobot.ipam.models.Prefix" + query: {location__name: "site_1"} + attribute: "__str__" + - value: ["192.168.0.0/24", "192.168.56.0/24"] diff --git a/nautobot_design_builder/tests/testdata/roll_back.yaml b/nautobot_design_builder/tests/testdata/roll_back.yaml new file mode 100644 index 00000000..6f2bc05a --- /dev/null +++ b/nautobot_design_builder/tests/testdata/roll_back.yaml @@ -0,0 +1,13 @@ +--- +designs: + - commit: false + manufacturers: + - name: "manufacturer1" + - name: "manufacturer2" + + device_types: + - manufacturer__name: "manufacturer1" + model: "model1" + u_height: 1 +# No checks, commit: false automatically checks that the rollback +# function has been called diff --git a/nautobot_design_builder/tests/testdata/simple_create.yaml b/nautobot_design_builder/tests/testdata/simple_create.yaml new file mode 100644 index 00000000..1ec97fe6 --- /dev/null +++ b/nautobot_design_builder/tests/testdata/simple_create.yaml @@ -0,0 +1,23 @@ +--- +designs: + - manufacturers: + - name: "manufacturer1" + - name: "manufacturer2" + + device_types: + - manufacturer__name: "manufacturer1" + model: "model1" + u_height: 1 +checks: + - model_exists: + model: "nautobot.dcim.models.Manufacturer" + query: {name: "manufacturer1"} + - model_exists: + model: "nautobot.dcim.models.Manufacturer" + query: {name: "manufacturer2"} + - equal: + - model: "nautobot.dcim.models.Manufacturer" + query: {name: "manufacturer1"} + - model: "nautobot.dcim.models.DeviceType" + query: {model: "model1"} + attribute: "manufacturer" diff --git a/nautobot_design_builder/tests/testdata/simple_update.yaml b/nautobot_design_builder/tests/testdata/simple_update.yaml new file mode 100644 index 00000000..31e88ec0 --- /dev/null +++ b/nautobot_design_builder/tests/testdata/simple_update.yaml @@ -0,0 +1,23 @@ +--- +designs: + # Design 1 + - manufacturers: + - name: "manufacturer1" + - name: "manufacturer2" + + device_types: + - manufacturer__name: "manufacturer1" + model: "model1" + u_height: 1 + + # Design 2 + - device_types: + - "!update:model": "model1" + manufacturer__name: "manufacturer2" +checks: + - equal: + - model: "nautobot.dcim.models.Manufacturer" + query: {name: "manufacturer2"} + - model: "nautobot.dcim.models.DeviceType" + query: {model: "model1"} + attribute: "manufacturer" diff --git a/nautobot_design_builder/tests/testdata/update_with_ref.yaml b/nautobot_design_builder/tests/testdata/update_with_ref.yaml new file mode 100644 index 00000000..47201996 --- /dev/null +++ b/nautobot_design_builder/tests/testdata/update_with_ref.yaml @@ -0,0 +1,21 @@ +--- +designs: + - manufacturers: + - name: "manufacturer1" + + device_types: + - manufacturer__name: "manufacturer1" + model: "model1" + u_height: 1 + "!ref": "device" + + - "!update:id": "!ref:device.id" + model: "new model name" + +checks: + - model_exists: + model: "nautobot.dcim.models.DeviceType" + query: {model: "new model name"} + - model_not_exist: + model: "nautobot.dcim.models.DeviceType" + query: {model: "model1"} diff --git a/pyproject.toml b/pyproject.toml index 81fe0df7..659b29c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,8 +36,8 @@ pytest = "*" yamllint = "*" Markdown = "*" toml = "*" -# Pining the version. The newer versions break unittests. -nautobot-bgp-models= "0.7.1" + +nautobot-bgp-models = "*" # Rendering docs to HTML mkdocs = "1.5.2"