diff --git a/netbox_diode_plugin/api/applier.py b/netbox_diode_plugin/api/applier.py index 0267302..aab9a2b 100644 --- a/netbox_diode_plugin/api/applier.py +++ b/netbox_diode_plugin/api/applier.py @@ -9,6 +9,7 @@ from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist from django.db import models +from django.db.utils import IntegrityError from rest_framework.exceptions import ValidationError as ValidationError from .common import NON_FIELD_ERRORS, Change, ChangeSet, ChangeSetException, ChangeSetResult, ChangeType, error_from_validation_error @@ -41,10 +42,14 @@ def apply_changeset(change_set: ChangeSet, request) -> ChangeSetResult: except TypeError as e: # this indicates a problem in model validation (should raise ValidationError) # but raised non-validation error (TypeError) -- we don't know which field trigged it. - logger.error(f"invalid data type for unspecified field (validation raised non-validation error): {data}: {e}") - raise _err("invalid data type for field", object_type, "__all__") - # ConstraintViolationError ? - # ... + import traceback + traceback.print_exc() + logger.error(f"validation raised TypeError error on unspecified field of {object_type}: {data}: {e}") + logger.error(traceback.format_exc()) + raise _err("invalid data type for field (TypeError)", object_type, "__all__") + except IntegrityError as e: + logger.error(f"Integrity error {object_type}: {e} {data}") + raise _err(f"created a conflict with an existing {object_type}", object_type, "__all__") return ChangeSetResult( id=change_set.id, diff --git a/netbox_diode_plugin/api/common.py b/netbox_diode_plugin/api/common.py index 8ea7b1e..8a7d754 100644 --- a/netbox_diode_plugin/api/common.py +++ b/netbox_diode_plugin/api/common.py @@ -2,23 +2,30 @@ # Copyright 2025 NetBox Labs Inc """Diode NetBox Plugin - API - Common types and utilities.""" +import datetime +import decimal import logging import uuid from collections import defaultdict from dataclasses import dataclass, field from enum import Enum +import netaddr from django.apps import apps from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError from django.db import models +from django.db.backends.postgresql.psycopg_any import NumericRange from extras.models import CustomField +from netaddr.eui import EUI from rest_framework import status +from zoneinfo import ZoneInfo logger = logging.getLogger("netbox.diode_data") NON_FIELD_ERRORS = "__all__" +_TRACE = False @dataclass class UnresolvedReference: @@ -43,6 +50,8 @@ def __hash__(self): def __lt__(self, other): """Less than operator.""" + if not isinstance(other, UnresolvedReference): + return False return self.object_type < other.object_type or (self.object_type == other.object_type and self.uuid < other.uuid) @@ -238,7 +247,7 @@ class AutoSlug: def error_from_validation_error(e, object_name): - """Convert a from rest_framework.exceptions.ValidationError to a ChangeSetException.""" + """Convert a from DRF ValidationError to a ChangeSetException.""" errors = {} if e.detail: if isinstance(e.detail, dict): @@ -252,3 +261,26 @@ def error_from_validation_error(e, object_name): NON_FIELD_ERRORS: [e.detail] } return ChangeSetException("validation error", errors=errors) + +def harmonize_formats(data): + """Puts all data in a format that can be serialized and compared.""" + match data: + case None: + return None + case str() | int() | float() | bool() | decimal.Decimal() | UnresolvedReference(): + return data + case dict(): + return {k: harmonize_formats(v) if not k.startswith("_") else v for k, v in data.items()} + case list() | tuple(): + return [harmonize_formats(v) for v in data] + case datetime.datetime(): + return data.strftime("%Y-%m-%dT%H:%M:%SZ") + case datetime.date(): + return data.strftime("%Y-%m-%d") + case NumericRange(): + return (data.lower, data.upper-1) + case netaddr.IPNetwork() | EUI() | ZoneInfo(): + return str(data) + case _: + logger.warning(f"Unknown type in harmonize_formats: {type(data)}") + return data diff --git a/netbox_diode_plugin/api/differ.py b/netbox_diode_plugin/api/differ.py index 7522464..07ee4d2 100644 --- a/netbox_diode_plugin/api/differ.py +++ b/netbox_diode_plugin/api/differ.py @@ -9,12 +9,21 @@ import netaddr from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import ValidationError from django.db.backends.postgresql.psycopg_any import NumericRange from netaddr.eui import EUI +from rest_framework import serializers from utilities.data import shallow_compare_dict -from .common import Change, ChangeSet, ChangeSetException, ChangeSetResult, ChangeType, error_from_validation_error +from .common import ( + NON_FIELD_ERRORS, + Change, + ChangeSet, + ChangeSetException, + ChangeSetResult, + ChangeType, + error_from_validation_error, + harmonize_formats, +) from .plugin_utils import get_primary_value, legal_fields from .supported_models import extract_supported_models from .transformer import cleanup_unresolved_references, set_custom_field_defaults, transform_proto_json @@ -36,11 +45,15 @@ def prechange_data_from_instance(instance) -> dict: # noqa: C901 model = SUPPORTED_MODELS.get(object_type) if not model: - raise ValidationError(f"Model {model_class.__name__} is not supported") + raise serializers.ValidationError({ + NON_FIELD_ERRORS: [f"Model {model_class.__name__} is not supported"] + }) fields = model.get("fields", {}) if not fields: - raise ValidationError(f"Model {model_class.__name__} has no fields") + raise serializers.ValidationError({ + NON_FIELD_ERRORS: [f"Model {model_class.__name__} has no fields"] + }) diode_fields = legal_fields(model_class) @@ -52,9 +65,6 @@ def prechange_data_from_instance(instance) -> dict: # noqa: C901 if not hasattr(instance, field_name): continue - if field_info["type"] == "ForeignKey" and field_info.get("is_many_to_one_rel", False): - continue - value = getattr(instance, field_name) if hasattr(value, "all"): # Handle many-to-many and many-to-one relationships # For any relationship that has an 'all' method, get all related objects' primary keys @@ -82,33 +92,11 @@ def prechange_data_from_instance(instance) -> dict: # noqa: C901 else: cfmap[cf.name] = cf.serialize(value) prechange_data["custom_fields"] = cfmap - prechange_data = _harmonize_formats(prechange_data) - return prechange_data - + prechange_data = harmonize_formats(prechange_data) -def _harmonize_formats(prechange_data): - if prechange_data is None: - return None - if isinstance(prechange_data, (str, int, float, bool, decimal.Decimal)): - return prechange_data - if isinstance(prechange_data, dict): - return {k: _harmonize_formats(v) for k, v in prechange_data.items()} - if isinstance(prechange_data, (list, tuple)): - return [_harmonize_formats(v) for v in prechange_data] - if isinstance(prechange_data, datetime.datetime): - return prechange_data.strftime("%Y-%m-%dT%H:%M:%SZ") - if isinstance(prechange_data, datetime.date): - return prechange_data.strftime("%Y-%m-%d") - if isinstance(prechange_data, NumericRange): - return (prechange_data.lower, prechange_data.upper-1) - if isinstance(prechange_data, netaddr.IPNetwork): - return str(prechange_data) - if isinstance(prechange_data, EUI): - return str(prechange_data) - - logger.warning(f"Unknown type in prechange_data: {type(prechange_data)}") return prechange_data + def clean_diff_data(data: dict, exclude_empty_values: bool = True) -> dict: """Clean diff data by removing null values.""" result = {} @@ -139,7 +127,6 @@ def diff_to_change( change_type = ChangeType.UPDATE if len(prechange_data) > 0 else ChangeType.CREATE if change_type == ChangeType.UPDATE and not len(changed_attrs) > 0: change_type = ChangeType.NOOP - primary_value = str(get_primary_value(prechange_data | postchange_data, object_type)) if primary_value is None: primary_value = "(unnamed)" @@ -173,8 +160,7 @@ def sort_dict_recursively(d): if isinstance(d, dict): return {k: sort_dict_recursively(v) for k, v in sorted(d.items())} if isinstance(d, list): - # Convert all items to strings for comparison - return sorted([sort_dict_recursively(item) for item in d], key=str) + return [sort_dict_recursively(item) for item in d] return d def generate_changeset(entity: dict, object_type: str) -> ChangeSetResult: @@ -183,7 +169,7 @@ def generate_changeset(entity: dict, object_type: str) -> ChangeSetResult: return _generate_changeset(entity, object_type) except ChangeSetException: raise - except ValidationError as e: + except serializers.ValidationError as e: raise error_from_validation_error(e, object_type) except Exception as e: logger.error(f"Unexpected error generating changeset: {e}") diff --git a/netbox_diode_plugin/api/matcher.py b/netbox_diode_plugin/api/matcher.py index c0ee706..f9ebea5 100644 --- a/netbox_diode_plugin/api/matcher.py +++ b/netbox_diode_plugin/api/matcher.py @@ -20,7 +20,7 @@ from django.db.models.query_utils import Q from extras.models.customfields import CustomField -from .common import AutoSlug, UnresolvedReference +from .common import _TRACE, AutoSlug, UnresolvedReference from .plugin_utils import content_type_id, get_object_type, get_object_type_model logger = logging.getLogger(__name__) @@ -46,20 +46,48 @@ condition=Q(assigned_object_id__isnull=True), ), ], + "ipam.aggregate": lambda: [ + ObjectMatchCriteria( + fields=("prefix",), + name="logical_aggregate_prefix_no_rir", + model_class=get_object_type_model("ipam.aggregate"), + condition=Q(rir__isnull=True), + ), + ObjectMatchCriteria( + fields=("prefix", "rir"), + name="logical_aggregate_prefix_within_rir", + model_class=get_object_type_model("ipam.aggregate"), + condition=Q(rir__isnull=False), + ), + ], "ipam.ipaddress": lambda: [ GlobalIPNetworkIPMatcher( - ip_field="address", + ip_fields=("address",), vrf_field="vrf", model_class=get_object_type_model("ipam.ipaddress"), name="logical_ip_address_global_no_vrf", ), VRFIPNetworkIPMatcher( - ip_field="address", + ip_fields=("address",), vrf_field="vrf", model_class=get_object_type_model("ipam.ipaddress"), name="logical_ip_address_within_vrf", ), ], + "ipam.iprange": lambda: [ + GlobalIPNetworkIPMatcher( + ip_fields=("start_address", "end_address"), + vrf_field="vrf", + model_class=get_object_type_model("ipam.iprange"), + name="logical_ip_range_start_end_global_no_vrf", + ), + VRFIPNetworkIPMatcher( + ip_fields=("start_address", "end_address"), + vrf_field="vrf", + model_class=get_object_type_model("ipam.iprange"), + name="logical_ip_range_start_end_within_vrf", + ), + ], "ipam.prefix": lambda: [ ObjectMatchCriteria( fields=("prefix",), @@ -68,12 +96,115 @@ condition=Q(vrf__isnull=True), ), ObjectMatchCriteria( - fields=("prefix", "vrf_id"), + fields=("prefix", "vrf"), name="logical_prefix_within_vrf", model_class=get_object_type_model("ipam.prefix"), condition=Q(vrf__isnull=False), ), ], + "virtualization.cluster": lambda: [ + ObjectMatchCriteria( + fields=("name", "scope_type", "scope_id"), + name="logical_cluster_within_scope", + model_class=get_object_type_model("virtualization.cluster"), + condition=Q(scope_type__isnull=False), + ), + ObjectMatchCriteria( + fields=("name",), + name="logical_cluster_with_no_scope_or_group", + model_class=get_object_type_model("virtualization.cluster"), + condition=Q(scope_type__isnull=True, group__isnull=True), + ), + ], + "ipam.vlan": lambda: [ + ObjectMatchCriteria( + fields=("vid",), + name="logical_vlan_vid_no_group_or_svlan", + model_class=get_object_type_model("ipam.vlan"), + condition=Q(group__isnull=True, qinq_svlan__isnull=True), + ), + ], + "ipam.vlangroup": lambda: [ + ObjectMatchCriteria( + fields=("name",), + name="logical_vlan_group_name_no_scope", + model_class=get_object_type_model("ipam.vlangroup"), + condition=Q(scope_type__isnull=True), + ), + ], + "wireless.wirelesslan": lambda: [ + ObjectMatchCriteria( + fields=("ssid",), + name="logical_wireless_lan_ssid_no_group_or_vlan", + model_class=get_object_type_model("wireless.wirelesslan"), + condition=Q(group__isnull=True, vlan__isnull=True), + ), + ObjectMatchCriteria( + fields=("ssid", "group"), + name="logical_wireless_lan_ssid_in_group", + model_class=get_object_type_model("wireless.wirelesslan"), + condition=Q(group__isnull=False), + ), + ObjectMatchCriteria( + fields=("ssid", "vlan"), + name="logical_wireless_lan_ssid_in_vlan", + model_class=get_object_type_model("wireless.wirelesslan"), + condition=Q(vlan__isnull=False), + ), + ], + "virtualization.virtualmachine": lambda: [ + ObjectMatchCriteria( + fields=("name",), + name="logical_virtual_machine_name_no_cluster", + model_class=get_object_type_model("virtualization.virtualmachine"), + condition=Q(cluster__isnull=True), + ), + ], + "ipam.service": lambda: [ + ObjectMatchCriteria( + fields=("name",), + name="logical_service_name_no_device_or_vm", + model_class=get_object_type_model("ipam.service"), + condition=Q(device__isnull=True, virtual_machine__isnull=True), + ), + ObjectMatchCriteria( + fields=("name", "device"), + name="logical_service_name_on_device", + model_class=get_object_type_model("ipam.service"), + condition=Q(device__isnull=False), + ), + ObjectMatchCriteria( + fields=("name", "virtual_machine"), + name="logical_service_name_on_vm", + model_class=get_object_type_model("ipam.service"), + condition=Q(virtual_machine__isnull=False), + ), + ], + "dcim.modulebay": lambda: [ + ObjectMatchCriteria( + fields=("name", "device"), + name="logical_module_bay_name_on_device", + model_class=get_object_type_model("dcim.modulebay"), + ) + ], + "dcim.inventoryitem": lambda: [ + # TODO: this may be handleable by the existing constraints. + # we ignore it due to null values for parent but could have + # better coverage of this case perhaps. + ObjectMatchCriteria( + fields=("name", "device"), + name="logical_inventory_item_name_on_device_no_parent", + model_class=get_object_type_model("dcim.inventoryitem"), + condition=Q(parent__isnull=True), + ) + ], + "ipam.fhrpgroup": lambda: [ + ObjectMatchCriteria( + fields=("group_id",), + name="logical_fhrp_group_id", + model_class=get_object_type_model("ipam.fhrpgroup"), + ) + ], } @dataclass @@ -166,33 +297,45 @@ def fingerprint(self, data: dict) -> str|None: return hash((self.model_class.__name__, self.name, tuple(values))) def _check_condition(self, data) -> bool: - if self.condition is None: - return True - # TODO: handle evaluating complex conditions, - # there are only simple ones currently - if self.condition.connector != Q.AND: - logger.warning(f"Unhandled condition {self.condition}") - return False + return self._check_condition_1(data, self.condition) - if len(self.condition.children) != 1: - logger.warning(f"Unhandled condition {self.condition}") - return False + def _check_condition_1(self, data, condition) -> bool: + if condition is None: + return True + if _TRACE: logger.debug(f"checking condition {condition}") # noqa: E701 + if isinstance(condition, tuple): + return self._check_simple_condition(data, condition) + + if hasattr(condition, "connector") and condition.connector == Q.AND: + result = True + for child in condition.children: + if not self._check_condition_1(data, child): + result = False + break + if condition.negated: + if _TRACE: logger.debug(f"negated condition {condition} => {not result}") # noqa: E701 + return not result + return result + # TODO handle OR ? + logger.warning(f"Unhandled condition {condition}") + return False - if len(self.condition.children[0]) != 2: - logger.warning(f"Unhandled condition {self.condition}") - return False + def _check_simple_condition(self, data, condition) -> bool: + if condition is None: + return True - k, v = self.condition.children[0] + k, v = condition + if _TRACE: logger.debug(f"checking simple condition {k} => {v}") # noqa: E701 result = False if k.endswith("__isnull"): k = k[:-8] - result = k not in data or data[k] is None + is_null = k not in data or data[k] is None + if _TRACE: logger.debug(f"checking isnull {k}? ({is_null}) want {v}") # noqa: E701 + result = is_null == v else: + if _TRACE: logger.debug(f"checking equality {k} => {data.get(k)} == {v}") # noqa: E701 result = k in data and data[k] == v - if self.condition.negated: - result = not result - return result def build_queryset(self, data) -> models.QuerySet: @@ -203,21 +346,25 @@ def build_queryset(self, data) -> models.QuerySet: return self._build_expressions_queryset(data) raise ValueError("No fields or expressions to build queryset from") - def _build_fields_queryset(self, data) -> models.QuerySet: + def _build_fields_queryset(self, data) -> models.QuerySet: # noqa: C901 """Builds a queryset for a simple set-of-fields constraint.""" + if not self._check_condition(data): + if _TRACE: logger.debug(f" * cannot build fields queryset for {self.name} (condition not met)") # noqa: E701 + return None + data = self._prepare_data(data) lookup_kwargs = {} for field_name in self.fields: field = self.model_class._meta.get_field(field_name) if field_name not in data: - logger.debug(f" * cannot build fields queryset for {self.name} (missing field {field_name})") + if _TRACE: logger.debug(f" * cannot build fields queryset for {self.name} (missing field {field_name})") # noqa: E701 return None # cannot match, missing field data lookup_value = data.get(field_name) if isinstance(lookup_value, UnresolvedReference): - logger.debug(f" * cannot build fields queryset for {self.name} ({field_name} is unresolved reference)") + if _TRACE: logger.debug(f" * cannot build fields queryset for {self.name} ({field_name} is unresolved reference)") # noqa: E701 return None # cannot match, missing field data if isinstance(lookup_value, dict): - logger.debug(f" * cannot build fields queryset for {self.name} ({field_name} is dict)") + if _TRACE: logger.debug(f" * cannot build fields queryset for {self.name} ({field_name} is dict)") # noqa: E701 return None # cannot match, missing field data lookup_kwargs[field.name] = lookup_value @@ -242,10 +389,10 @@ def _build_expressions_queryset(self, data) -> models.QuerySet: refs = [F(ref) for ref in _get_refs(expr)] for ref in refs: if ref not in replacements: - logger.debug(f" * cannot build expr queryset for {self.name} (missing field {ref})") + if _TRACE: logger.debug(f" * cannot build expr queryset for {self.name} (missing field {ref})") # noqa: E701 return None # cannot match, missing field data if isinstance(replacements[ref], UnresolvedReference): - logger.debug(f" * cannot build expr queryset for {self.name} ({ref} is unresolved reference)") + if _TRACE: logger.debug(f" * cannot build expr queryset for {self.name} ({ref} is unresolved reference)") # noqa: E701 return None # cannot match, missing field data rhs = expr.replace_expressions(replacements) @@ -313,7 +460,7 @@ def has_required_fields(self, data: dict) -> bool: class GlobalIPNetworkIPMatcher: """A matcher that ignores the mask.""" - ip_field: str + ip_fields: tuple[str] vrf_field: str model_class: Type[models.Model] name: str @@ -330,19 +477,22 @@ def fingerprint(self, data: dict) -> str|None: if not self._check_condition(data): return None - value = self.ip_value(data) - if value is None: - return None + values = [] + for field in self.ip_fields: + value = self.ip_value(data, field) + if value is None: + return None + values.append(value) - return hash((self.model_class.__name__, self.name, value)) + return hash((self.model_class.__name__, self.name, tuple(values))) def has_required_fields(self, data: dict) -> bool: """Returns True if the data given contains a value for all fields referenced by the constraint.""" - return self.ip_field in data + return all(field in data for field in self.ip_fields) - def ip_value(self, data: dict) -> str|None: + def ip_value(self, data: dict, field: str) -> str|None: """Get the IP value from the data.""" - value = data.get(self.ip_field) + value = data.get(field) if value is None: return None return _ip_only(value) @@ -350,29 +500,37 @@ def ip_value(self, data: dict) -> str|None: def build_queryset(self, data: dict) -> models.QuerySet: """Build a queryset for the custom field.""" if not self.has_required_fields(data): + if _TRACE: logger.debug(f" * cannot build expr queryset for {self.name} (missing field {self.ip_field})") # noqa: E701 return None if not self._check_condition(data): + if _TRACE: logger.debug(f" * cannot build expr queryset for {self.name} (condition not met)") # noqa: E701 return None - value = self.ip_value(data) - if value is None: - return None + filter = { + f'{self.vrf_field}__isnull': True, + } + for field in self.ip_fields: + value = self.ip_value(data, field) + if value is None: + if _TRACE: logger.debug(f" * cannot build expr queryset for {self.name} (ip value is None)") # noqa: E701 + return None + filter[f'{field}__net_host'] = value - return self.model_class.objects.filter(**{f'{self.ip_field}__net_host': value, f'{self.vrf_field}__isnull': True}) + return self.model_class.objects.filter(**filter) @dataclass class VRFIPNetworkIPMatcher: """Matches ip in a vrf, ignores mask.""" - ip_field: str + ip_fields: tuple[str] vrf_field: str model_class: Type[models.Model] name: str def _check_condition(self, data: dict) -> bool: """Check the condition for the custom field.""" - return data.get('vrf_id', None) is not None + return data.get(self.vrf_field, None) is not None def fingerprint(self, data: dict) -> str|None: """Fingerprint the custom field value.""" @@ -382,21 +540,24 @@ def fingerprint(self, data: dict) -> str|None: if not self._check_condition(data): return None - value = self.ip_value(data) - if value is None: - return None + values = [] + for field in self.ip_fields: + value = self.ip_value(data, field) + if value is None: + return None + values.append(value) vrf_id = data[self.vrf_field] - return hash((self.model_class.__name__, self.name, value, vrf_id)) + return hash((self.model_class.__name__, self.name, tuple(values), vrf_id)) def has_required_fields(self, data: dict) -> bool: """Returns True if the data given contains a value for all fields referenced by the constraint.""" - return self.ip_field in data and self.vrf_field in data + return all(field in data for field in self.ip_fields) and self.vrf_field in data - def ip_value(self, data: dict) -> str|None: + def ip_value(self, data: dict, field: str) -> str|None: """Get the IP value from the data.""" - value = data.get(self.ip_field) + value = data.get(field) if value is None: return None return _ip_only(value) @@ -404,17 +565,28 @@ def ip_value(self, data: dict) -> str|None: def build_queryset(self, data: dict) -> models.QuerySet: """Build a queryset for the custom field.""" if not self.has_required_fields(data): + if _TRACE: logger.debug(f" * cannot build expr queryset for {self.name} (missing field {self.ip_field})") # noqa: E701 return None if not self._check_condition(data): + if _TRACE: logger.debug(f" * cannot build expr queryset for {self.name} (condition not met)") # noqa: E701 return None - value = self.ip_value(data) - if value is None: - return None + filter = {} + for field in self.ip_fields: + value = self.ip_value(data, field) + if value is None: + if _TRACE: logger.debug(f" * cannot build expr queryset for {self.name} (ip value is None)") # noqa: E701 + return None + filter[f'{field}__net_host'] = value vrf_id = data[self.vrf_field] - return self.model_class.objects.filter(**{f'{self.ip_field}__net_host': value, f'{self.vrf_field}': vrf_id}) + if isinstance(vrf_id, UnresolvedReference): + if _TRACE: logger.debug(f" * cannot build expr queryset for {self.name} ({self.vrf_field} is unresolved reference)") # noqa: E701 + return None + filter[f'{self.vrf_field}'] = vrf_id + + return self.model_class.objects.filter(**filter) def _ip_only(value: str) -> str|None: @@ -608,31 +780,32 @@ def _fingerprint_all(data: dict) -> str: return hash(tuple(values)) -def fingerprint(data: dict, object_type: str) -> str: +def fingerprints(data: dict, object_type: str) -> list[str]: """ - Fingerprint a data structure. + Get fingerprints for a data structure. - This uses the first matcher that has all - required fields or else uses all fields. - - TODO: This means there are pathological? cases where - the same object is being referenced but by - different unique constraints in the same diff... - this could lead to some unexpected behavior. + This returns all fingerprints for the given data that + have required fields. """ if data is None: return None model_class = get_object_type_model(object_type) # check any known match criteria + fps = [] for matcher in get_model_matchers(model_class): fp = matcher.fingerprint(data) if fp is not None: - return fp - # fall back to fingerprinting all the data - return _fingerprint_all(data) + fps.append(fp) + if _TRACE: logger.debug(f" ** matcher {matcher.name} gave fingerprint {fp}") # noqa: E701 + else: + if _TRACE: logger.debug(f" ** skipped matcher {matcher.name}") # noqa: E701 + fp = _fingerprint_all(data) + if _TRACE: logger.debug(f" ** matcher _fingerprint_all gave fingerprint {fp}") # noqa: E701 + fps.append(fp) + return fps -def find_existing_object(data: dict, object_type: str): +def find_existing_object(data: dict, object_type: str): # noqa: C901 """ Find an existing object that matches the given data. @@ -641,21 +814,21 @@ def find_existing_object(data: dict, object_type: str): Returns the object if found, otherwise None. """ - logger.debug(f"resolving {data}") + if _TRACE: logger.debug(f"resolving {data}") # noqa: E701 model_class = get_object_type_model(object_type) for matcher in get_model_matchers(model_class): if not matcher.has_required_fields(data): - logger.debug(f" * skipped matcher {matcher.name} (missing fields)") + if _TRACE: logger.debug(f" * skipped matcher {matcher.name} (missing fields)") # noqa: E701 continue q = matcher.build_queryset(data) if q is None: - logger.debug(f" * skipped matcher {matcher.name} (no queryset)") + if _TRACE: logger.debug(f" * skipped matcher {matcher.name} (no queryset)") # noqa: E701 continue - logger.debug(f" * trying query {q.query}") + if _TRACE: logger.debug(f" * trying query {q.query}") # noqa: E701 existing = q.order_by('pk').first() if existing is not None: - logger.debug(f" -> Found object {existing} via {matcher.name}") + if _TRACE: logger.debug(f" -> Found object {existing} via {matcher.name}") # noqa: E701 return existing - logger.debug(f" -> No object found for matcher {matcher.name}") - logger.debug(" * No matchers found an existing object") + if _TRACE: logger.debug(f" -> No object found for matcher {matcher.name}") # noqa: E701 + if _TRACE: logger.debug(" * No matchers found an existing object") # noqa: E701 return None diff --git a/netbox_diode_plugin/api/transformer.py b/netbox_diode_plugin/api/transformer.py index f95ec52..07f99fd 100644 --- a/netbox_diode_plugin/api/transformer.py +++ b/netbox_diode_plugin/api/transformer.py @@ -12,12 +12,12 @@ from uuid import uuid4 import graphlib -from django.core.exceptions import ValidationError from django.utils.text import slugify from extras.models.customfields import CustomField +from rest_framework import serializers -from .common import AutoSlug, ChangeSetException, UnresolvedReference -from .matcher import find_existing_object, fingerprint +from .common import _TRACE, NON_FIELD_ERRORS, AutoSlug, ChangeSetException, UnresolvedReference, harmonize_formats +from .matcher import find_existing_object, fingerprints from .plugin_utils import ( CUSTOM_FIELD_OBJECT_REFERENCE_TYPE, apply_format_transformations, @@ -50,6 +50,22 @@ def _camel_to_snake_case(name): "assigned_object_id": UnresolvedReference(object_type=object_type, uuid=uuid), }, }, + "virtualization.virtualmachine": { + "primary_ip4": lambda object_type, uuid: { + "__force_after": UnresolvedReference(object_type=object_type, uuid=uuid), + }, + "primary_ip6": lambda object_type, uuid: { + "__force_after": UnresolvedReference(object_type=object_type, uuid=uuid), + }, + }, + "dcim.virtualdevicecontext": { + "primary_ip4": lambda object_type, uuid: { + "__force_after": UnresolvedReference(object_type=object_type, uuid=uuid), + }, + "primary_ip6": lambda object_type, uuid: { + "__force_after": UnresolvedReference(object_type=object_type, uuid=uuid), + }, + }, } def _no_context(object_type, uuid): @@ -61,15 +77,17 @@ def _nested_context(object_type, uuid, field_name): _IS_CIRCULAR_REFERENCE = { "dcim.interface": frozenset(["primary_mac_address"]), "virtualization.vminterface": frozenset(["primary_mac_address"]), - "dcim.device": frozenset(["primary_ip4", "primary_ip6"]), + "dcim.device": frozenset(["primary_ip4", "primary_ip6", "oob_ip"]), "dcim.virtualdevicecontext": frozenset(["primary_ip4", "primary_ip6"]), "virtualization.virtualmachine": frozenset(["primary_ip4", "primary_ip6"]), + "circuits.provider": frozenset(["accounts"]), + "dcim.modulebay": frozenset(["module"]), # this isn't allowed to be circular, but gives a better error } def _is_circular_reference(object_type, field_name): return field_name in _IS_CIRCULAR_REFERENCE.get(object_type, frozenset()) -def transform_proto_json(proto_json: dict, object_type: str, supported_models: dict) -> list[dict]: +def transform_proto_json(proto_json: dict, object_type: str, supported_models: dict) -> list[dict]: # noqa: C901 """ Transform keys of proto json dict to flattened dictionaries with model field keys. @@ -77,24 +95,28 @@ def transform_proto_json(proto_json: dict, object_type: str, supported_models: d a certain form of deduplication and resolution of existing objects. """ entities = _transform_proto_json_1(proto_json, object_type) - logger.debug(f"_transform_proto_json_1 entities: {json.dumps(entities, default=lambda o: str(o), indent=4)}") + if _TRACE: logger.debug(f"_transform_proto_json_1 entities: {json.dumps(entities, default=lambda o: str(o), indent=4)}") # noqa: E701 entities = _topo_sort(entities) - logger.debug(f"_topo_sort: {json.dumps(entities, default=lambda o: str(o), indent=4)}") + if _TRACE: logger.debug(f"_topo_sort: {json.dumps(entities, default=lambda o: str(o), indent=4)}") # noqa: E701 deduplicated = _fingerprint_dedupe(entities) - logger.debug(f"_fingerprint_dedupe: {json.dumps(deduplicated, default=lambda o: str(o), indent=4)}") + if _TRACE: logger.debug(f"_fingerprint_dedupe: {json.dumps(deduplicated, default=lambda o: str(o), indent=4)}") # noqa: E701 deduplicated = _topo_sort(deduplicated) - logger.debug(f"_topo_sort: {json.dumps(deduplicated, default=lambda o: str(o), indent=4)}") + if _TRACE: logger.debug(f"_topo_sort: {json.dumps(deduplicated, default=lambda o: str(o), indent=4)}") # noqa: E701 _set_auto_slugs(deduplicated, supported_models) - logger.debug(f"_set_auto_slugs: {json.dumps(deduplicated, default=lambda o: str(o), indent=4)}") + if _TRACE: logger.debug(f"_set_auto_slugs: {json.dumps(deduplicated, default=lambda o: str(o), indent=4)}") # noqa: E701 + _handle_cached_scope(deduplicated, supported_models) + if _TRACE: logger.debug(f"_handle_cached_scope: {json.dumps(deduplicated, default=lambda o: str(o), indent=4)}") # noqa: E701 resolved = _resolve_existing_references(deduplicated) - logger.debug(f"_resolve_references: {json.dumps(resolved, default=lambda o: str(o), indent=4)}") - _set_defaults(resolved, supported_models) - logger.debug(f"_set_defaults: {json.dumps(resolved, default=lambda o: str(o), indent=4)}") + if _TRACE: logger.debug(f"_resolve_references: {json.dumps(resolved, default=lambda o: str(o), indent=4)}") # noqa: E701 + _strip_cached_scope(resolved) + if _TRACE: logger.debug(f"_strip_cached_scope: {json.dumps(resolved, default=lambda o: str(o), indent=4)}") # noqa: E701 + defaulted = _set_defaults(resolved, supported_models) + if _TRACE: logger.debug(f"_set_defaults: {json.dumps(defaulted, default=lambda o: str(o), indent=4)}") # noqa: E701 # handle post-create steps - output = _handle_post_creates(resolved) - logger.debug(f"_handle_post_creates: {json.dumps(output, default=lambda o: str(o), indent=4)}") + output = _handle_post_creates(defaulted) + if _TRACE: logger.debug(f"_handle_post_creates: {json.dumps(output, default=lambda o: str(o), indent=4)}") # noqa: E701 _check_unresolved_refs(output) for entity in output: @@ -117,7 +139,8 @@ def _transform_proto_json_1(proto_json: dict, object_type: str, context=None) -> # context pushed down from parent nodes if context is not None: for k, v in context.items(): - node[k] = v + if not k.startswith("_"): + node[k] = v if isinstance(v, UnresolvedReference): node['_refs'].add(v.uuid) @@ -221,31 +244,123 @@ def _topo_sort(entities: list[dict]) -> list[dict]: except graphlib.CycleError as e: # TODO the cycle error references the cycle here ... raise ChangeSetException(f"Circular reference in entities: {e}", errors={ - "__all__": { - "message": "Unable to resolve circular reference in entities", + NON_FIELD_ERRORS: { + NON_FIELD_ERRORS: "Unable to resolve circular reference in entities", } }) def _set_defaults(entities: list[dict], supported_models: dict): + out = [] for entity in entities: + entity = copy.deepcopy(entity) model_fields = supported_models.get(entity['_object_type']) if model_fields is None: - raise ValidationError(f"Model for object type {entity['_object_type']} is not supported") + raise serializers.ValidationError({ + NON_FIELD_ERRORS: [f"Model for object type {entity['_object_type']} is not supported"] + }) auto_slug = entity.pop("_auto_slug", None) if entity.get("_instance"): + out.append(entity) continue if auto_slug: if auto_slug.field_name not in entity: entity[auto_slug.field_name] = auto_slug.value + legal = legal_fields(entity['_object_type']) for field_name, field_info in model_fields.get('fields', {}).items(): + if field_name not in legal: + continue if entity.get(field_name) is None and field_info.get("default") is not None: - entity[field_name] = field_info["default"] + default = field_info["default"] + if callable(default): + default = default() + entity[field_name] = default set_custom_field_defaults(entity, model_fields['model']) + out.append(harmonize_formats(entity)) + return out +def _handle_cached_scope(entities: list[dict], supported_models: dict): + by_type_id = { + (entity['_object_type'], entity['_uuid']): entity + for entity in entities + } + for entity in entities: + model = supported_models.get(entity['_object_type'], {}).get("model") + if _has_cached_scope(model): + _handle_cached_scope_1(entity, by_type_id) + +def _strip_cached_scope(entities: list[dict]): + for entity in entities: + entity.pop("_region", None) + entity.pop("_site_group", None) + entity.pop("_site", None) + entity.pop("_location", None) + +@lru_cache(maxsize=256) +def _has_cached_scope(model): + return hasattr(model, "cache_related_objects") and hasattr(model, "scope") + +def _handle_cached_scope_1(entity: dict, by_type_id: dict): + # these are some auto-set fields that cache scope information, + # some indexes rely on them. Here we attempt to emulate that behavior + # for the purpose of matching. These generally only exist after save. + scope_type = entity.get("scope_type") + scope_id = entity.get("scope_id") + + if scope_type and scope_id: + scope = by_type_id.get((scope_type, scope_id.uuid)) + if scope_type == "dcim.region": + _cache_region_ref(entity, scope_id) + elif scope_type == "dcim.sitegroup": + _cache_site_group_ref(entity, scope_id) + elif scope_type == "dcim.site": + _cache_site_ref(entity, scope_id) + _cache_region_ref(entity, scope.get("region")) + _cache_site_group_ref(entity, scope.get("group")) + elif scope_type == "dcim.location": + _cache_location_ref(entity, scope_id) + site_ref = scope.get("site") + if site_ref is not None and isinstance(site_ref, UnresolvedReference): + _cache_site_ref(entity, site_ref) + site_obj = by_type_id.get((site_ref.object_type, site_ref.uuid)) + if site_obj is not None: + _cache_region_ref(entity, site_obj.get("region")) + _cache_site_group_ref(entity, site_obj.get("group")) + +def _cache_region_ref(entity: dict, ref: UnresolvedReference|None): + if ref is None: + return + entity["_region"] = UnresolvedReference( + object_type=ref.object_type, + uuid=ref.uuid, + ) + +def _cache_site_group_ref(entity: dict, ref: UnresolvedReference|None): + if ref is None: + return + entity["_site_group"] = UnresolvedReference( + object_type=ref.object_type, + uuid=ref.uuid, + ) + +def _cache_site_ref(entity: dict, ref: UnresolvedReference|None): + if ref is None: + return + entity["_site"] = UnresolvedReference( + object_type=ref.object_type, + uuid=ref.uuid, + ) + +def _cache_location_ref(entity: dict, ref: UnresolvedReference|None): + if ref is None: + return + entity["_location"] = UnresolvedReference( + object_type=ref.object_type, + uuid=ref.uuid, + ) def set_custom_field_defaults(entity: dict, model): """Set default values for custom fields in an entity.""" @@ -263,7 +378,9 @@ def _set_auto_slugs(entities: list[dict], supported_models: dict): for entity in entities: model_fields = supported_models.get(entity['_object_type']) if model_fields is None: - raise ValidationError(f"Model for object type {entity['_object_type']} is not supported") + raise serializers.ValidationError({ + NON_FIELD_ERRORS: [f"Model for object type {entity['_object_type']} is not supported"] + }) for field_name, field_info in model_fields.get('fields', {}).items(): if field_info["type"] == "SlugField" and entity.get(field_name) is None: @@ -280,38 +397,52 @@ def _generate_slug(object_type, data): return slugify(str(source_value)) return None -def _fingerprint_dedupe(entities: list[dict]) -> list[dict]: +def _fingerprint_dedupe(entities: list[dict]) -> list[dict]: # noqa: C901 """ Deduplicates/merges entities by fingerprint. *list must be in topo order by reference already* """ + by_uuid = {} by_fp = {} deduplicated = [] new_refs = {} # uuid -> uuid for entity in entities: + if _TRACE: logger.debug(f"fingerprint_dedupe: {entity}") # noqa: E701 if entity.get('_is_post_create'): fp = entity['_uuid'] - existing = None + existing_uuid = None else: - fp = fingerprint(entity, entity['_object_type']) - existing = by_fp.get(fp) - - if existing is None: - logger.debug(" * entity is new.") + _update_unresolved_refs(entity, new_refs) + fps = fingerprints(entity, entity['_object_type']) + if _TRACE: logger.debug(f" ==> {fps}") # noqa: E701 + for fp in fps: + existing_uuid = by_fp.get(fp) + if existing_uuid is not None: + break + + if existing_uuid is None: + if _TRACE: logger.debug(" * entity is new.") # noqa: E701 new_entity = copy.deepcopy(entity) _update_unresolved_refs(new_entity, new_refs) - by_fp[fp] = new_entity - deduplicated.append(fp) + primary_uuid = new_entity['_uuid'] + for fp in fps: + by_fp[fp] = primary_uuid + by_uuid[primary_uuid] = new_entity + deduplicated.append(primary_uuid) else: - logger.debug(" * entity already exists.") + if _TRACE: logger.debug(" * entity already exists.") # noqa: E701 + existing = by_uuid[existing_uuid] new_refs[entity['_uuid']] = existing['_uuid'] merged = _merge_nodes(existing, entity) _update_unresolved_refs(merged, new_refs) - by_fp[fp] = merged + for fp in fps: + by_fp[fp] = existing_uuid + by_uuid[existing_uuid] = merged + deduplicated.append(existing_uuid) - return [by_fp[fp] for fp in deduplicated] + return [by_uuid[u] for u in deduplicated] def _merge_nodes(a: dict, b: dict) -> dict: """ @@ -328,7 +459,15 @@ def _merge_nodes(a: dict, b: dict) -> dict: if k.startswith("_"): continue if k in merged and merged[k] != v: - raise ValueError(f"Conflict merging {a} and {b} on {k}: {merged[k]} and {v}") + ov = { + ok: v for ok, v in a.items() + if ok != k and not ok.startswith("_") + } + raise serializers.ValidationError({ + NON_FIELD_ERRORS: [ + f"Conflicting values for '{k}' merging duplicate {a.get('_object_type')}," + f" `{merged[k]}` != `{v}` other values : {ov}"] + }) merged[k] = v return merged @@ -370,7 +509,7 @@ def _resolve_existing_references(entities: list[dict]) -> list[dict]: existing = find_existing_object(data, object_type) if existing is not None: - logger.debug(f"existing {data} -> {existing}") + if _TRACE: logger.debug(f"existing {data} -> {existing}") # noqa: E701 fp = (object_type, existing.id) if fp in seen: logger.warning(f"objects resolved to the same existing id after deduplication: {seen[fp]} and {data}") @@ -397,7 +536,7 @@ def _update_resolved_refs(data, new_refs): new_items.append(new_refs[item.uuid]) else: new_items.append(item) - data[k] = new_items + data[k] = sorted(new_items) elif isinstance(v, dict): _update_resolved_refs(v, new_refs) @@ -513,7 +652,9 @@ def _prepare_custom_fields(object_type: str, custom_fields: dict) -> tuple[dict, )) out[key] = vals else: - raise ValueError(f"Custom field {keyname} has unknown type: {value_type}") + raise serializers.ValidationError({ + keyname: [f"Custom field {keyname} has unknown type: {value_type}"] + }) except ValueError as e: raise ChangeSetException( f"Custom field {keyname} is invalid: {value}", diff --git a/netbox_diode_plugin/tests/test_api_diff_and_apply.py b/netbox_diode_plugin/tests/test_api_diff_and_apply.py index f53b1b1..651bfe4 100644 --- a/netbox_diode_plugin/tests/test_api_diff_and_apply.py +++ b/netbox_diode_plugin/tests/test_api_diff_and_apply.py @@ -9,9 +9,9 @@ from uuid import uuid4 import netaddr -from circuits.models import Circuit +from circuits.models import Circuit, Provider from core.models import ObjectType -from dcim.models import Device, Interface, Site +from dcim.models import Device, Interface, ModuleBay, Site from django.contrib.auth import get_user_model from extras.models import CustomField from extras.models.customfields import CustomFieldTypeChoices @@ -19,12 +19,14 @@ from rest_framework import status from users.models import Token from utilities.testing import APITestCase -from virtualization.models import VMInterface +from virtualization.models import Cluster, VMInterface logger = logging.getLogger(__name__) User = get_user_model() +def _get_error(response, object_name, field): + return response.json().get("errors", {}).get(object_name, {}).get(field, []) class GenerateDiffAndApplyTestCase(APITestCase): """GenerateDiff -> ApplyChangeSet test cases.""" @@ -416,6 +418,92 @@ def test_generate_diff_and_apply_create_device_with_primary_ip4(self): device = Device.objects.get(name=f"Device {device_uuid}") self.assertEqual(device.primary_ip4.pk, new_ipaddress.pk) + def test_generate_diff_and_apply_create_device_with_primary_ip6(self): + """Test generate diff and apply create device with primary ip6.""" + device_uuid = str(uuid4()) + interface_uuid = str(uuid4()) + addr = "2001:db8::1" + payload = { + "timestamp": 1, + "object_type": "ipam.ipaddress", + "entity": { + "ip_address": { + "address": addr, + "assigned_object_interface": { + "name": f"Interface {interface_uuid}", + "type": "1000base-t", + "device": { + "name": f"Device {device_uuid}", + "role": { + "name": f"Role {uuid4()}", + }, + "site": { + "name": f"Site {uuid4()}", + }, + "device_type": { + "manufacturer": { + "name": f"Manufacturer {uuid4()}", + }, + "model": f"Device Type {uuid4()}", + }, + "primary_ip6": { + "address": addr, + }, + }, + }, + }, + }, + } + + _, response = self.diff_and_apply(payload) + new_ipaddress = IPAddress.objects.get(address=addr) + self.assertEqual(new_ipaddress.assigned_object.name, f"Interface {interface_uuid}") + device = Device.objects.get(name=f"Device {device_uuid}") + self.assertEqual(device.primary_ip6.pk, new_ipaddress.pk) + + def test_generate_diff_and_apply_create_device_with_oob_ip(self): + """Test generate diff and apply create device with oob ip.""" + device_uuid = str(uuid4()) + interface_uuid = str(uuid4()) + addr = "192.168.1.1/24" + payload = { + "timestamp": 1, + "object_type": "ipam.ipaddress", + "entity": { + "ip_address": { + "address": addr, + "assigned_object_interface": { + "name": f"Interface {interface_uuid}", + "type": "1000base-t", + "device": { + "name": f"Device {device_uuid}", + "role": { + "name": f"Role {uuid4()}", + }, + "site": { + "name": f"Site {uuid4()}", + }, + "device_type": { + "manufacturer": { + "name": f"Manufacturer {uuid4()}", + }, + "model": f"Device Type {uuid4()}", + }, + "oob_ip": { + "address": addr, + }, + }, + }, + }, + }, + } + + _, response = self.diff_and_apply(payload) + new_ipaddress = IPAddress.objects.get(address=addr) + self.assertEqual(new_ipaddress.assigned_object.name, f"Interface {interface_uuid}") + device = Device.objects.get(name=f"Device {device_uuid}") + self.assertEqual(device.oob_ip.pk, new_ipaddress.pk) + def test_generate_diff_and_apply_create_and_update_site_with_custom_field(self): """Test generate diff and apply create and update site with custom field.""" site_uuid = str(uuid4()) @@ -913,6 +1001,352 @@ def test_generate_diff_and_apply_complex_vminterface(self): self.assertEqual(vm_interface.mtu, 2000) self.assertEqual(vm_interface.primary_mac_address.mac_address, "00:00:00:00:00:01") + def test_generate_diff_and_apply_dedupe_devicetype(self): + """Test generate diff and apply dedupe devicetype in wireless link.""" + payload = { + "timestamp": "2025-04-16T02:58:20.564615Z", + "object_type": "wireless.wirelesslink", + "entity": { + "wireless_link": { + "interface_a": { + "device": { + "name": "Device 1", + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "role": {"name": "Device Role 1"}, + "site": {"name": "Site 1"} + }, + "name": "Radio0/1", + "type": "ieee802.11ac", + "enabled": True + }, + "interface_b": { + "device": { + "name": "Device 2", + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "role": {"name": "Device Role 1"}, + "site": {"name": "Site 1"} + }, + "name": "Radio0/1", + "type": "ieee802.11ac", + "enabled": True + }, + "ssid": "P2P-Link-1", + "status": "connected", + "tenant": {"name": "Tenant 1"}, + "auth_type": "wpa-personal", + "auth_cipher": "aes", + "auth_psk": "P2PLinkKey123!", + "distance": 1.5, + "distance_unit": "km", + "description": "Point-to-point wireless backhaul link", + "comments": "Building A to Building B wireless bridge", + "tags": [ + { + "name": "Tag 1" + }, + { + "name": "Tag 2" + } + ] + } + } + } + + _ = self.diff_and_apply(payload) + + def test_generate_diff_and_apply_provider_with_accounts(self): + """Test generate diff and apply provider with accounts.""" + payload = { + "timestamp": "2025-04-16T02:58:20.564615Z", + "object_type": "circuits.provider", + "entity": { + "provider": { + "name": "Level 3 Communications", + "slug": "level3", + "description": "Global Tier 1 Internet Service Provider", + "comments": "Primary transit provider for data center connectivity", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}], + "accounts": [ + { + "provider": {"name": "Level 3 Communications"}, + "name": "East Coast Account", + "account": "L3-12345", + "description": "East Coast regional services account", + "comments": "Managed through regional NOC" + }, + { + "provider": {"name": "Level 3 Communications"}, + "name": "West Coast Account", + "account": "L3-67890", + "description": "West Coast regional services account", + "comments": "Managed through regional NOC" + } + ], + "asns": [ + { + "asn": "3356", + "rir": {"name": "ARIN"}, + "tenant": {"name": "Tenant 1"}, + "description": "Level 3 Global ASN", + "comments": "Primary transit ASN" + } + ] + } + } + } + + _ = self.diff_and_apply(payload) + provider = Provider.objects.get(name="Level 3 Communications") + self.assertEqual(provider.accounts.count(), 2) + self.assertEqual(provider.asns.count(), 1) + + def test_generate_diff_and_apply_module_bay_with_module(self): + """Test generate diff and apply module bay with module.""" + payload = { + "timestamp": "2025-04-16T02:58:20.564615Z", + "object_type": "dcim.modulebay", + "entity": { + "module_bay": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "name": "Stack Module Bay 2", + "module": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "module_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S-STACK" + }, + "module_bay": { + "name": "Module Bay 1", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + } + } + + }, + "label": "STACK-2", + "position": "Rear", + "description": "Secondary stacking module bay", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + } + } + _ = self.diff_and_apply(payload) + module_bay = ModuleBay.objects.get(name="Stack Module Bay 2") + self.assertEqual(module_bay.module.device.name, "Device 1") + self.assertEqual(module_bay.module.module_type.manufacturer.name, "Cisco") + self.assertEqual(module_bay.module.module_type.model, "C2960S-STACK") + self.assertEqual(module_bay.module.module_bay.name, "Module Bay 1") + + def test_generate_diff_and_apply_module_bay_circular_ref_fails(self): + """Test generate diff and apply module bay.""" + payload = { + "timestamp": "2025-04-16T02:58:20.564615Z", + "object_type": "dcim.modulebay", + "entity": { + "module_bay": { + "name": "Module Bay 1", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "module": { + "asset_tag": "1234567890", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "module_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S-STACK" + }, + "module_bay": { + "name": "Module Bay 1", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "module": { + "asset_tag": "1234567890", + } + } + }, + "label": "STACK-2", + "position": "Rear", + "description": "Secondary stacking module bay", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + } + } + response1 = self.client.post( + self.diff_url, data=payload, format="json", **self.user_header + ) + self.assertEqual(response1.status_code, status.HTTP_200_OK) + diff = response1.json().get("change_set", {}) + + response2 = self.client.post( + self.apply_url, data=diff, format="json", **self.user_header + ) + self.assertEqual(response2.status_code, status.HTTP_400_BAD_REQUEST) + + self.assertIn( + "A module bay cannot belong to a module installed within it.", + _get_error(response2, "dcim.modulebay", "__all__") + ) + + def test_generate_diff_and_apply_virtual_machine_with_primary_ip_4_ok(self): + """Test generate diff and apply virtual machine with primary ip 4 assigned.""" + payload = { + "timestamp": "2025-04-16T02:58:20.564615Z", + "object_type": "virtualization.virtualmachine", + "entity": { + "timestamp": "2025-04-16T13:45:02.045208Z", + "virtual_machine": { + "name": "app-server-01", + "status": "active", + "site": {"name": "Site 1"}, + "cluster": { + "name": "Cluster 1", + "type": {"name": "Cluster Type 1"} + }, + "device": { + "name": "Device 1", + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "role": {"name": "Device Role 1"}, + "site": {"name": "Site 1"}, + "cluster": { + "name": "Cluster 1", + "type": {"name": "Cluster Type 1"} + } + }, + "serial": "VM-2023-001", + "role": {"name": "Application Server"}, + "tenant": {"name": "Tenant 1"}, + "platform": {"name": "Ubuntu 22.04"}, + "primary_ip4": { + "address": "192.168.2.10", + "assigned_object_vm_interface": { + "virtual_machine": { + "name": "app-server-01", + "cluster": { + "name": "Cluster 1", + "type": {"name": "Cluster Type 1"} + }, + "tenant": {"name": "Tenant 1"}, + }, + "name": "eth0", + "enabled": True, + "mtu": "1500", + } + }, + "vcpus": 4.0, + "memory": "214748364", + "disk": "147483647", + "description": "Primary application server instance", + "comments": "Hosts critical business applications", + "tags": [ + { + "name": "Tag 1" + }, + { + "name": "Tag 2" + } + ] + } + } + } + _ = self.diff_and_apply(payload) + + def test_generate_diff_and_apply_update_cluster_location(self): + """Test generate diff and apply update cluster location, same site.""" + payload = { + "timestamp": "2025-04-16T02:58:20.564615Z", + "object_type": "virtualization.cluster", + "entity": { + "cluster": { + "name": "Cluster A", + "type": {"name": "Cluster Type 1"}, + "group": {"name": "Cluster Group 1"}, + "status": "active", + "tenant": {"name": "Tenant 1"}, + "scope_site": {"name": "Site 1"}, + "description": "Cluster 1 Description", + "comments": "Cluster 1 Comments", + "tags": [{"name": "Tag 1"}] + } + }, + } + _ = self.diff_and_apply(payload) + + cluster = Cluster.objects.get(name="Cluster A") + self.assertEqual(cluster.scope.name, "Site 1") + + payload = { + "timestamp": "2025-04-16T02:58:20.564615Z", + "object_type": "virtualization.cluster", + "entity": { + "cluster": { + "name": "Cluster A", + "type": {"name": "Cluster Type 1"}, + "group": {"name": "Cluster Group 1"}, + "status": "active", + "tenant": {"name": "Tenant 1"}, + "scope_location": {"name": "Location 1", "site": {"name": "Site 1"}}, + "description": "Cluster 1 Description", + "comments": "Cluster 1 Comments", + "tags": [{"name": "Tag 1"}] + } + }, + } + _ = self.diff_and_apply(payload) + cluster = Cluster.objects.get(name="Cluster A") + self.assertEqual(cluster.scope.name, "Location 1") + def diff_and_apply(self, payload): """Diff and apply the payload.""" response1 = self.client.post( diff --git a/netbox_diode_plugin/tests/test_api_generate_diff.py b/netbox_diode_plugin/tests/test_api_generate_diff.py index 0c84fd9..d55ec35 100644 --- a/netbox_diode_plugin/tests/test_api_generate_diff.py +++ b/netbox_diode_plugin/tests/test_api_generate_diff.py @@ -2,6 +2,7 @@ # Copyright 2024 NetBox Labs Inc """Diode NetBox Plugin - Tests.""" +import logging from uuid import uuid4 from core.models import ObjectType @@ -15,6 +16,12 @@ User = get_user_model() +logger = logging.getLogger(__name__) + +def _get_error(response, object_name, field): + return response.json().get("errors", {}).get(object_name, {}).get(field, []) + + class GenerateDiffTestCase(APITestCase): """GenerateDiff test cases.""" @@ -279,6 +286,83 @@ def test_generate_diff_update_rack_type_camel_case(self): before = change.get("before", {}) self.assertEqual(before.get("model"), "Rack Type 1") + def test_merge_states_failed(self): + """Test merge states failed.""" + payload = { + "timestamp": 1, + "object_type": "ipam.vrf", + "entity": { + "vrf": { + "name": "Customer-A-VRF", + "rd": "65000:100", + "tenant": {"name": "Tenant 1"}, + "enforce_unique": True, + "description": "Isolated routing domain for Customer A", + "comments": "Used for customer's private network services", + "tags": [ + { + "name": "Tag 1" + }, + { + "name": "Tag 2" + } + ], + "import_targets": [ + { + "name": "65000:100", + "description": "Primary import route target" + }, + { + "name": "65000:101", + "description": "Backup import route target" + } + ], + "export_targets": [ + { + "name": "65000:100", + "description": "Primary export route target" + } + ] + } + } + } + + response = self.send_request(payload, status.HTTP_400_BAD_REQUEST) + logger.error(response.json()) + errs = _get_error(response, "ipam.vrf", "__all__") + self.assertEqual(len(errs), 1) + err = errs[0] + self.assertTrue(err.startswith("Conflicting values for 'description' merging duplicate ipam.routetarget")) + + def test_vlangroup_error(self): + """Test vlangroup error.""" + payload = { + "timestamp": 1, + "object_type": "ipam.vlangroup", + "entity": { + "vlan_group": { + "name": "Data Center Core", + "slug": "dc-core", + "scope_site": { + "name": "Data Center West", + "slug": "dc-west", + "status": "active" + }, + "description": "Core network VLANs for data center infrastructure", + "tags": [ + { + "name": "Tag 1" + }, + { + "name": "Tag 2" + } + ] + } + } + } + _ = self.send_request(payload) + + def send_request(self, payload, status_code=status.HTTP_200_OK): """Post the payload to the url and return the response.""" response = self.client.post( diff --git a/netbox_diode_plugin/tests/test_updates.py b/netbox_diode_plugin/tests/test_updates.py new file mode 100644 index 0000000..d2c9fc5 --- /dev/null +++ b/netbox_diode_plugin/tests/test_updates.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python +# Copyright 2024 NetBox Labs Inc +"""Diode NetBox Plugin - Tests.""" + +import copy +import datetime +import decimal +import inspect +import json +import logging +import os +from functools import wraps + +from django.contrib.auth import get_user_model +from django.db import models +from django.db.models import QuerySet +from rest_framework import status +from users.models import Token +from utilities.testing import APITestCase + +from netbox_diode_plugin.api.common import harmonize_formats +from netbox_diode_plugin.api.plugin_utils import get_object_type_model + +logger = logging.getLogger(__name__) + +User = get_user_model() + +def _get_error(response, object_name, field): + return response.json().get("errors", {}).get(object_name, {}).get(field, []) + +def load_test_cases(cls): + """Class decorator to load test cases and create test methods.""" + logger.error("**** Loading test cases") + current_dir = os.path.dirname(os.path.abspath(__file__)) + test_data_path = os.path.join(current_dir, "test_updates_cases.json") + logger.error(f"**** Looking for test data at {test_data_path}") + + if not os.path.exists(test_data_path): + logger.error(f"**** Test data file not found at {test_data_path}") + raise FileNotFoundError(f"Test data file not found at {test_data_path}") + + def _create_and_update_test_case(case): + object_type = case["object_type"] + + def test_func(self): + model = get_object_type_model(object_type) + + payload = { + "timestamp": 1, + "object_type": object_type, + "entity": case["create"], + } + res = self.send_request(self.diff_url, payload) + self.assertEqual(res.status_code, status.HTTP_200_OK) + diff = res.json().get("change_set", {}) + res = self.client.post( + self.apply_url, data=diff, format="json", **self.user_header + ) + self.assertEqual(res.status_code, status.HTTP_200_OK) + # lookup the object and check fields + obj = model.objects.get(**case["lookup"]) + self._check_expect(obj, case["create_expect"]) + + # resending the same payload should not change anything + payload = { + "timestamp": 2, + "object_type": object_type, + "entity": case["create"], + } + res = self.send_request(self.diff_url, payload) + self.assertEqual(res.status_code, status.HTTP_200_OK) + + change_set = res.json().get("change_set", {}) + if change_set.get("changes", []) != []: + logger.error(f"Unexpected change set {json.dumps(change_set, indent=4)}") + + self.assertEqual(res.json().get("change_set", {}).get("changes", []), []) + + # updating the object + payload = { + "timestamp": 3, + "object_type": object_type, + "entity": case["update"], + } + res = self.send_request(self.diff_url, payload) + self.assertEqual(res.status_code, status.HTTP_200_OK) + + diff = res.json().get("change_set", {}) + res = self.client.post( + self.apply_url, data=diff, format="json", **self.user_header + ) + self.assertEqual(res.status_code, status.HTTP_200_OK) + obj = model.objects.get(**case["lookup"]) + self._check_expect(obj, case["update_expect"]) + + test_func.__name__ = f"test_updates_{case['name']}" + return test_func + + with open(test_data_path) as f: + test_cases = json.load(f) + for case in test_cases: + t = _create_and_update_test_case(case) + logger.error(f"**** Creating test case {t.__name__}") + setattr(cls, t.__name__, t) + + return cls + +@load_test_cases +class ApplyUpdatesTestCase(APITestCase): + """diff/create/update test cases.""" + + @classmethod + def setUpClass(cls): + """Set up the test cases.""" + super().setUpClass() + + def setUp(self): + """Set up the test case.""" + self.diff_url = "/netbox/api/plugins/diode/generate-diff/" + self.apply_url = "/netbox/api/plugins/diode/apply-change-set/" + self.user = User.objects.create_user(username="testcommonuser") + self.user_token = Token.objects.create(user=self.user) + self.user_header = {"HTTP_AUTHORIZATION": f"Token {self.user_token.key}"} + + self.add_permissions("netbox_diode_plugin.add_diode") + + def _follow_path(self, obj, path): + cur = obj + for i, p in enumerate(path): + if p.isdigit(): + p = int(p) + cur = cur[p] + else: + cur = getattr(cur, p) + if i != len(path) - 1: + self.assertIsNotNone(cur) + if callable(cur): + try: + signature = inspect.signature(cur) + if len(signature.parameters) == 0: + cur = cur() + except ValueError: + pass + return harmonize_formats(cur) + + def _check_set_by(self, obj, path, value): + key = path[-1][len("__by_"):] + path = path[:-1] + cur = self._follow_path(obj, path) + + if isinstance(value, (list, tuple)): + vals = set(value) + else: + vals = {value} + + cvals = {harmonize_formats(getattr(c, key)) for c in cur} + self.assertEqual(cvals, vals) + + def _check_equals(self, obj, path, value): + cur = self._follow_path(obj, path) + self.assertEqual(cur, value) + + def _check_expect(self, obj, expect): + for field, value in expect.items(): + path = field.strip().split(".") + if path[-1].startswith("__by_"): + self._check_set_by(obj, path, value) + else: + self._check_equals(obj, path, value) + + def send_request(self, url, payload, status_code=status.HTTP_200_OK): + """Post the payload to the url and return the response.""" + response = self.client.post( + url, data=payload, format="json", **self.user_header + ) + self.assertEqual(response.status_code, status_code) + return response diff --git a/netbox_diode_plugin/tests/test_updates_cases.json b/netbox_diode_plugin/tests/test_updates_cases.json new file mode 100644 index 0000000..8d94c65 --- /dev/null +++ b/netbox_diode_plugin/tests/test_updates_cases.json @@ -0,0 +1,5902 @@ +[ + { + "name": "ipam_asn_1", + "object_type": "ipam.asn", + "lookup": {"asn": 555}, + "create_expect": { + "asn": 555, + "description": "ASN 555 Description", + "rir.name": "RIR 1" + }, + "create": { + "asn": { + "asn": "555", + "rir": {"name": "RIR 1"}, + "tenant": {"name": "Tenant 1"}, + "description": "ASN 555 Description", + "comments": "ASN 555 Comments", + "tags": [{"name": "Tag 1"}] + } + }, + "update": { + "asn": { + "asn": "555", + "rir": {"name": "RIR 1"}, + "tenant": {"name": "Tenant 1"}, + "description": "ASN 555 Description Updated", + "comments": "ASN 555 Comments", + "tags": [{"name": "Tag 1"}] + } + }, + "update_expect": { + "description": "ASN 555 Description Updated" + } + }, + { + "name": "ipam_asnrange_1", + "object_type": "ipam.asnrange", + "lookup": {"name": "ASN Range 1"}, + "create_expect": { + "name": "ASN Range 1", + "start": 1, + "end": 2, + "rir.name": "RIR 1" + }, + "create": { + "asn_range": { + "name": "ASN Range 1", + "slug": "asn-range-1", + "rir": {"name": "RIR 1"}, + "start": "1", + "end": "2", + "tenant": {"name": "Tenant 1"}, + "description": "ASN Range 1 Description", + "tags": [{"name": "Tag 1"}] + } + }, + "update": { + "asn_range": { + "name": "ASN Range 1", + "slug": "asn-range-1", + "rir": {"name": "RIR 1"}, + "start": "1", + "end": "2", + "tenant": {"name": "Tenant 1"}, + "description": "ASN Range 1 Description Updated", + "tags": [{"name": "Tag 1"}] + } + }, + "update_expect": { + "description": "ASN Range 1 Description Updated" + } + }, + { + "name": "ipam_aggregate_1", + "object_type": "ipam.aggregate", + "lookup": {"prefix": "182.82.82.0/24"}, + "create_expect": { + "prefix": "182.82.82.0/24", + "rir.name": "RIR 1", + "description": "Aggregate Description" + }, + "create": { + "aggregate": { + "prefix": "182.82.82.0/24", + "rir": {"name": "RIR 1"}, + "tenant": {"name": "Tenant 1"}, + "date_added": "2025-04-14T08:08:55Z", + "description": "Aggregate Description", + "comments": "Aggregate Comments", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "aggregate": { + "prefix": "182.82.82.0/24", + "rir": {"name": "RIR 1"}, + "tenant": {"name": "Tenant 1"}, + "date_added": "2025-04-14T08:08:55Z", + "description": "Aggregate Description Updated", + "comments": "Aggregate Comments", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Aggregate Description Updated" + } + }, + { + "name": "circuits_circuit_1", + "object_type": "circuits.circuit", + "lookup": {"cid": "Circuit 1"}, + "create_expect": { + "cid": "Circuit 1", + "provider.name": "Provider 1", + "type.name": "Circuit Type 1", + "description": "Circuit 1 Description" + }, + "create": { + "circuit": { + "cid": "Circuit 1", + "provider": {"name": "Provider 1"}, + "provider_account": { + "provider": {"name": "Provider 1"}, + "account": "account1" + }, + "type": {"name": "Circuit Type 1"}, + "status": "offline", + "tenant": {"name": "Tenant 1"}, + "install_date": "2025-04-14T00:00:00Z", + "termination_date": "2025-04-14T00:00:00Z", + "commit_rate": "10", + "description": "Circuit 1 Description", + "distance": 12.4, + "distance_unit": "ft", + "comments": "Circuit 1 Comments", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "circuit": { + "cid": "Circuit 1", + "provider": {"name": "Provider 1"}, + "provider_account": { + "provider": {"name": "Provider 1"}, + "account": "account1" + }, + "type": {"name": "Circuit Type 1"}, + "status": "offline", + "tenant": {"name": "Tenant 1"}, + "install_date": "2025-04-14T00:00:00Z", + "termination_date": "2025-04-14T00:00:00Z", + "commit_rate": "10", + "description": "Circuit 1 Description Updated", + "distance": 12.4, + "distance_unit": "ft", + "comments": "Circuit 1 Comments", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Circuit 1 Description Updated" + } + }, + { + "name": "circuits_circuitgroup_1", + "object_type": "circuits.circuitgroup", + "lookup": {"name": "Circuit Group 1"}, + "create_expect": { + "name": "Circuit Group 1", + "description": "Circuit Group 1 Description" + }, + "create": { + "circuit_group": { + "name": "Circuit Group 1", + "description": "Circuit Group 1 Description", + "tenant": {"name": "Tenant 1"}, + "tags": [{"name": "Tag 1"}] + } + }, + "update": { + "circuit_group": { + "name": "Circuit Group 1", + "description": "Circuit Group 1 Description Updated", + "tenant": {"name": "Tenant 1"}, + "tags": [{"name": "Tag 1"}] + } + }, + "update_expect": { + "description": "Circuit Group 1 Description Updated" + } + }, + { + "name": "circuits_circuitgroupassignment_1", + "object_type": "circuits.circuitgroupassignment", + "lookup": { + "group__name": "Circuit Group 1" + }, + "create_expect": { + "group.name": "Circuit Group 1", + "member.cid": "Circuit 1", + "priority": "tertiary" + }, + "create": { + "circuit_group_assignment": { + "group": {"name": "Circuit Group 1"}, + "member_circuit": { + "cid": "Circuit 1", + "type": {"name": "Circuit Type 1"}, + "provider": {"name": "Provider 1"} + }, + "priority": "tertiary" + } + }, + "update": { + "circuit_group_assignment": { + "group": {"name": "Circuit Group 1"}, + "member_circuit": { + "cid": "Circuit 1", + "type": {"name": "Circuit Type 1"}, + "provider": {"name": "Provider 1"} + }, + "priority": "secondary" + } + }, + "update_expect": { + "priority": "secondary" + } + }, + { + "name": "circuits_circuitgroupassignment_2", + "object_type": "circuits.circuitgroupassignment", + "lookup": { + "group__name": "Circuit Group 1" + }, + "create_expect": { + "group.name": "Circuit Group 1", + "member.cid": "Virtual Circuit 1", + "priority": "tertiary" + }, + "create": { + "circuit_group_assignment": { + "group": {"name": "Circuit Group 1"}, + "member_virtual_circuit": { + "cid": "Virtual Circuit 1", + "type": {"name": "Virtual Circuit Type 1"}, + "provider_network": { + "name": "Provider Network 1", + "provider": {"name": "Provider 1"} + } + }, + "priority": "tertiary" + } + }, + "update": { + "circuit_group_assignment": { + "group": {"name": "Circuit Group 1"}, + "member_virtual_circuit": { + "cid": "Virtual Circuit 1", + "type": {"name": "Virtual Circuit Type 1"}, + "provider_network": { + "name": "Provider Network 1", + "provider": {"name": "Provider 1"} + } + }, + "priority": "secondary" + } + }, + "update_expect": { + "priority": "secondary" + } + }, + { + "name": "circuits_circuittermination_1", + "object_type": "circuits.circuittermination", + "lookup": { + "circuit__cid": "Circuit 1", + "term_side": "A" + }, + "create_expect": { + "circuit.cid": "Circuit 1", + "term_side": "A", + "port_speed": 9600, + "description": "description" + }, + "create": { + "circuit_termination": { + "circuit": { + "cid": "Circuit 1", + "type": {"name": "Circuit Type 1"}, + "provider": {"name": "Provider 1"} + }, + "term_side": "A", + "termination_location": { + "name": "attic", + "site": {"name": "Site 1"} + }, + "port_speed": "9600", + "upstream_speed": "14400", + "xconnect_id": "xconnect.1", + "pp_info": "pp info", + "description": "description", + "mark_connected": true, + "tags": [{"name": "Tag 1"}] + } + }, + "update": { + "circuit_termination": { + "circuit": { + "cid": "Circuit 1", + "type": {"name": "Circuit Type 1"}, + "provider": {"name": "Provider 1"} + }, + "term_side": "A", + "termination_location": { + "name": "attic", + "site": {"name": "Site 1"} + }, + "port_speed": "9600", + "upstream_speed": "14400", + "xconnect_id": "xconnect.1", + "pp_info": "pp info", + "description": "description Updated", + "mark_connected": true, + "tags": [{"name": "Tag 1"}] + } + }, + "update_expect": { + "description": "description Updated" + } + }, + { + "name": "circuits_circuittermination_2", + "object_type": "circuits.circuittermination", + "lookup": { + "circuit__cid": "Circuit 1", + "term_side": "A" + }, + "create_expect": { + "circuit.cid": "Circuit 1", + "term_side": "A", + "port_speed": 9600, + "description": "description" + }, + "create": { + "circuit_termination": { + "circuit": { + "cid": "Circuit 1", + "type": {"name": "Circuit Type 1"}, + "provider": {"name": "Provider 1"} + }, + "term_side": "A", + "termination_provider_network": { + "provider": {"name": "Provider 1"}, + "name": "Provider Network 1", + "service_id": "service.1" + }, + "port_speed": "9600", + "upstream_speed": "14400", + "xconnect_id": "xconnect.1", + "pp_info": "pp info", + "description": "description", + "mark_connected": true, + "tags": [{"name": "Tag 1"}] + } + }, + "update": { + "circuit_termination": { + "circuit": { + "cid": "Circuit 1", + "type": {"name": "Circuit Type 1"}, + "provider": {"name": "Provider 1"} + }, + "term_side": "A", + "termination_provider_network": { + "provider": {"name": "Provider 1"}, + "name": "Provider Network 1", + "service_id": "service.1" + }, + "port_speed": "9600", + "upstream_speed": "14400", + "xconnect_id": "xconnect.1", + "pp_info": "pp info", + "description": "description Updated", + "mark_connected": true, + "tags": [{"name": "Tag 1"}] + } + }, + "update_expect": { + "description": "description Updated" + } + }, + { + "name": "circuits_circuittermination_3", + "object_type": "circuits.circuittermination", + "lookup": { + "circuit__cid": "Circuit 1", + "term_side": "A" + }, + "create_expect": { + "circuit.cid": "Circuit 1", + "term_side": "A", + "port_speed": 9600, + "description": "description" + }, + "create": { + "circuit_termination": { + "circuit": { + "cid": "Circuit 1", + "type": {"name": "Circuit Type 1"}, + "provider": {"name": "Provider 1"} + }, + "term_side": "A", + "termination_site": {"name": "Site 1"}, + "port_speed": "9600", + "upstream_speed": "14400", + "xconnect_id": "xconnect.1", + "pp_info": "pp info", + "description": "description", + "mark_connected": true, + "tags": [{"name": "Tag 1"}] + } + }, + "update": { + "circuit_termination": { + "circuit": { + "cid": "Circuit 1", + "type": {"name": "Circuit Type 1"}, + "provider": {"name": "Provider 1"} + }, + "term_side": "A", + "termination_site": {"name": "Site 1"}, + "port_speed": "9600", + "upstream_speed": "14400", + "xconnect_id": "xconnect.1", + "pp_info": "pp info", + "description": "description Updated", + "mark_connected": true, + "tags": [{"name": "Tag 1"}] + } + }, + "update_expect": { + "description": "description Updated" + } + }, + { + "name": "circuits_circuittype_1", + "object_type": "circuits.circuittype", + "lookup": {"name": "Circuit Type 1"}, + "create_expect": { + "name": "Circuit Type 1", + "description": "Circuit Type 1 Description" + }, + "create": { + "circuit_type": { + "name": "Circuit Type 1", + "slug": "circuit-type-1", + "color": "0000ff", + "description": "Circuit Type 1 Description", + "tags": [{"name": "Tag 1"}] + } + }, + "update": { + "circuit_type": { + "name": "Circuit Type 1", + "slug": "circuit-type-1", + "color": "0000ff", + "description": "Circuit Type 1 Description Updated", + "tags": [{"name": "Tag 1"}] + } + }, + "update_expect": { + "description": "Circuit Type 1 Description Updated" + } + }, + { + "name": "virtualization_cluster_1", + "object_type": "virtualization.cluster", + "lookup": {"name": "Cluster A"}, + "create_expect": { + "name": "Cluster A", + "type.name": "Cluster Type 1", + "description": "Cluster 1 Description" + }, + "create": { + "cluster": { + "name": "Cluster A", + "type": {"name": "Cluster Type 1"}, + "group": {"name": "Cluster Group 1"}, + "status": "active", + "tenant": {"name": "Tenant 1"}, + "scope_location": { + "name": "Location 1", + "site": {"name": "Site 1"} + }, + "description": "Cluster 1 Description", + "comments": "Cluster 1 Comments", + "tags": [{"name": "Tag 1"}] + } + }, + "update": { + "cluster": { + "name": "Cluster A", + "type": {"name": "Cluster Type 1"}, + "group": {"name": "Cluster Group 1"}, + "status": "active", + "tenant": {"name": "Tenant 1"}, + "scope_location": { + "name": "Location 1", + "site": {"name": "Site 1"} + }, + "description": "Cluster 1 Description Updated", + "comments": "Cluster 1 Comments", + "tags": [{"name": "Tag 1"}] + } + }, + "update_expect": { + "description": "Cluster 1 Description Updated" + } + }, + { + "name": "virtualization_cluster_2", + "object_type": "virtualization.cluster", + "lookup": {"name": "Cluster 2"}, + "create_expect": { + "name": "Cluster 2", + "type.name": "Cluster Type 1", + "description": "Cluster 1 Description" + }, + "create": { + "cluster": { + "name": "Cluster 2", + "type": {"name": "Cluster Type 1"}, + "group": {"name": "Cluster Group 1"}, + "status": "active", + "tenant": {"name": "Tenant 1"}, + "scope_region": {"name": "Region 1"}, + "description": "Cluster 1 Description", + "comments": "Cluster 1 Comments", + "tags": [{"name": "Tag 1"}] + } + }, + "update": { + "cluster": { + "name": "Cluster 2", + "type": {"name": "Cluster Type 1"}, + "group": {"name": "Cluster Group 1"}, + "status": "active", + "tenant": {"name": "Tenant 1"}, + "scope_region": {"name": "Region 1"}, + "description": "Cluster 1 Description Updated", + "comments": "Cluster 1 Comments", + "tags": [{"name": "Tag 1"}] + } + }, + "update_expect": { + "description": "Cluster 1 Description Updated" + } + }, + { + "name": "virtualization_cluster_3", + "object_type": "virtualization.cluster", + "lookup": {"name": "Cluster 3"}, + "create_expect": { + "name": "Cluster 3", + "type.name": "Cluster Type 1", + "description": "Cluster 1 Description" + }, + "create": { + "cluster": { + "name": "Cluster 3", + "type": {"name": "Cluster Type 1"}, + "group": {"name": "Cluster Group 1"}, + "status": "active", + "tenant": {"name": "Tenant 1"}, + "scope_site": {"name": "Site 1"}, + "description": "Cluster 1 Description", + "comments": "Cluster 1 Comments", + "tags": [{"name": "Tag 1"}] + } + }, + "update": { + "cluster": { + "name": "Cluster 3", + "type": {"name": "Cluster Type 1"}, + "group": {"name": "Cluster Group 1"}, + "status": "active", + "tenant": {"name": "Tenant 1"}, + "scope_site": {"name": "Site 1"}, + "description": "Cluster 1 Description Updated", + "comments": "Cluster 1 Comments", + "tags": [{"name": "Tag 1"}] + } + }, + "update_expect": { + "description": "Cluster 1 Description Updated" + } + }, + { + "name": "virtualization_cluster_4", + "object_type": "virtualization.cluster", + "lookup": {"name": "Cluster 4"}, + "create_expect": { + "name": "Cluster 4", + "type.name": "Cluster Type 1", + "description": "Cluster 1 Description" + }, + "create": { + "cluster": { + "name": "Cluster 4", + "type": {"name": "Cluster Type 1"}, + "group": {"name": "Cluster Group 1"}, + "status": "active", + "tenant": {"name": "Tenant 1"}, + "scope_site_group": {"name": "Site Group 1"}, + "description": "Cluster 1 Description", + "comments": "Cluster 1 Comments", + "tags": [{"name": "Tag 1"}] + } + }, + "update": { + "cluster": { + "name": "Cluster 4", + "type": {"name": "Cluster Type 1"}, + "group": {"name": "Cluster Group 1"}, + "status": "active", + "tenant": {"name": "Tenant 1"}, + "scope_site_group": {"name": "Site Group 1"}, + "description": "Cluster 1 Description Updated", + "comments": "Cluster 1 Comments", + "tags": [{"name": "Tag 1"}] + } + }, + "update_expect": { + "description": "Cluster 1 Description Updated" + } + }, + { + "name": "virtualization_clustergroup_1", + "object_type": "virtualization.clustergroup", + "lookup": {"name": "Cluster Group 1"}, + "create_expect": { + "name": "Cluster Group 1", + "description": "Cluster Group 1 Description" + }, + "create": { + "cluster_group": { + "name": "Cluster Group 1", + "description": "Cluster Group 1 Description", + "tags": [{"name": "Tag 1"}] + } + }, + "update": { + "cluster_group": { + "name": "Cluster Group 1", + "description": "Cluster Group 1 Description Updated", + "tags": [{"name": "Tag 1"}] + } + }, + "update_expect": { + "description": "Cluster Group 1 Description Updated" + } + }, + { + "name": "virtualization_clustertype_1", + "object_type": "virtualization.clustertype", + "lookup": {"name": "Cluster Type 1"}, + "create_expect": { + "name": "Cluster Type 1", + "description": "Cluster Type 1 Description" + }, + "create": { + "cluster_type": { + "name": "Cluster Type 1", + "description": "Cluster Type 1 Description", + "tags": [{"name": "Tag 1"}] + } + }, + "update": { + "cluster_type": { + "name": "Cluster Type 1", + "description": "Cluster Type 1 Description Updated", + "tags": [{"name": "Tag 1"}] + } + }, + "update_expect": { + "description": "Cluster Type 1 Description Updated" + } + }, + { + "name": "dcim_consoleport_1", + "object_type": "dcim.consoleport", + "lookup": {"name": "Console Port 1"}, + "create_expect": { + "name": "Console Port 1", + "description": "Console Port 1 Description" + }, + "create": { + "console_port": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "module": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "module_bay": { + "name": "Module Bay 1", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + } + }, + "module_type": { + "manufacturer": { + "name": "Manufacturer 1" + }, + "model": "Module Type 1" + } + }, + "name": "Console Port 1", + "label": "Console Port 1 Label", + "type": "db-25", + "speed": "1200", + "description": "Console Port 1 Description", + "mark_connected": true, + "tags": [{"name": "Tag 1"}] + } + }, + "update": { + "console_port": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "module": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "module_bay": { + "name": "Module Bay 1", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + } + }, + "module_type": { + "manufacturer": { + "name": "Manufacturer 1" + }, + "model": "Module Type 1" + } + }, + "name": "Console Port 1", + "label": "Console Port 1 Label", + "type": "db-25", + "speed": "1200", + "description": "Console Port 1 Description Updated", + "mark_connected": true, + "tags": [{"name": "Tag 1"}] + } + }, + "update_expect": { + "description": "Console Port 1 Description Updated" + } + }, + { + "name": "dcim_consoleserverport_1", + "object_type": "dcim.consoleserverport", + "lookup": {"name": "Console Server Port 1"}, + "create_expect": { + "name": "Console Server Port 1", + "description": "Console Server Port 1 Description" + }, + "create": { + "console_server_port": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "module": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "module_bay": { + "name": "Module Bay 1", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + } + }, + "module_type": { + "manufacturer": { + "name": "Manufacturer 1" + }, + "model": "Module Type 1" + } + }, + "name": "Console Server Port 1", + "label": "Console Server Port 1 Label", + "type": "db-25", + "speed": "1200", + "description": "Console Server Port 1 Description", + "mark_connected": true, + "tags": [{"name": "Tag 1"}] + } + }, + "update": { + "console_server_port": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "module": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "module_bay": { + "name": "Module Bay 1", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + } + }, + "module_type": { + "manufacturer": { + "name": "Manufacturer 1" + }, + "model": "Module Type 1" + } + }, + "name": "Console Server Port 1", + "label": "Console Server Port 1 Label", + "type": "db-25", + "speed": "1200", + "description": "Console Server Port 1 Description Updated", + "mark_connected": true, + "tags": [{"name": "Tag 1"}] + } + }, + "update_expect": { + "description": "Console Server Port 1 Description Updated" + } + }, + { + "name": "tenancy_contact_1", + "object_type": "tenancy.contact", + "lookup": {"name": "Contact 1"}, + "create_expect": { + "name": "Contact 1", + "group.name": "Contact Group 1", + "description": "Contact 1 Description" + }, + "create": { + "contact": { + "group": {"name": "Contact Group 1"}, + "name": "Contact 1", + "title": "Contact 1 Title", + "phone": "1234567890", + "email": "contact1@example.com", + "address": "1234 Main St, Anytown, USA", + "link": "https://example.com", + "description": "Contact 1 Description", + "comments": "Contact 1 Comments", + "tags": [{"name": "Tag 1"}] + } + }, + "update": { + "contact": { + "group": {"name": "Contact Group 1"}, + "name": "Contact 1", + "title": "Contact 1 Title", + "phone": "1234567890", + "email": "contact1@example.com", + "address": "1234 Main St, Anytown, USA", + "link": "https://example.com", + "description": "Contact 1 Description Updated", + "comments": "Contact 1 Comments", + "tags": [{"name": "Tag 1"}] + } + }, + "update_expect": { + "description": "Contact 1 Description Updated" + } + }, + { + "name": "tenancy_contactassignment_1", + "object_type": "tenancy.contactassignment", + "lookup": { + "contact__name": "Contact 1", + "role__name": "Contact Role 1" + }, + "create_expect": { + "contact.name": "Contact 1", + "role.name": "Contact Role 1", + "priority": "primary" + }, + "create": { + "contact_assignment": { + "contact": { + "name": "Contact 1", + "group": {"name": "Contact Group 1"}, + "title": "Contact 1 Title" + }, + "role": {"name": "Contact Role 1"}, + "priority": "primary", + "tags": [{"name": "Tag 1"}], + "object_site": {"name": "Site 1"} + } + }, + "update": { + "contact_assignment": { + "contact": { + "name": "Contact 1", + "group": {"name": "Contact Group 1"}, + "title": "Contact 1 Title" + }, + "role": {"name": "Contact Role 1"}, + "priority": "secondary", + "tags": [{"name": "Tag 1"}], + "object_site": {"name": "Site 1"} + } + }, + "update_expect": { + "priority": "secondary" + } + }, + { + "name": "tenancy_contactgroup_1", + "object_type": "tenancy.contactgroup", + "lookup": {"name": "Contact Group 1"}, + "create_expect": { + "name": "Contact Group 1", + "description": "Contact Group 1 Description" + }, + "create": { + "contact_group": { + "name": "Contact Group 1", + "parent": {"name": "Contact Group 2"}, + "description": "Contact Group 1 Description", + "tags": [{"name": "Tag 1"}] + } + }, + "update": { + "contact_group": { + "name": "Contact Group 1", + "parent": {"name": "Contact Group 2"}, + "description": "Contact Group 1 Description Updated", + "tags": [{"name": "Tag 1"}] + } + }, + "update_expect": { + "description": "Contact Group 1 Description Updated" + } + }, + { + "name": "tenancy_contactrole_1", + "object_type": "tenancy.contactrole", + "lookup": {"name": "Contact Role 1"}, + "create_expect": { + "name": "Contact Role 1", + "description": "Contact Role 1 Description" + }, + "create": { + "contact_role": { + "name": "Contact Role 1", + "description": "Contact Role 1 Description", + "tags": [{"name": "Tag 1"}] + } + }, + "update": { + "contact_role": { + "name": "Contact Role 1", + "description": "Contact Role 1 Description Updated", + "tags": [{"name": "Tag 1"}] + } + }, + "update_expect": { + "description": "Contact Role 1 Description Updated" + } + }, + { + "name": "dcim_device_1", + "object_type": "dcim.device", + "lookup": {"name": "Device ABC"}, + "create_expect": { + "name": "Device ABC", + "device_type.manufacturer.name": "Cisco", + "device_type.model": "C2960S", + "role.name": "Device Role 1", + "description": "Device 1 Description" + }, + "create": { + "device": { + "name": "Device ABC", + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "role": {"name": "Device Role 1"}, + "tenant": {"name": "Tenant 1"}, + "platform": {"name": "Platform 1"}, + "serial": "1234567890", + "asset_tag": "asset.1", + "site": {"name": "Site 1"}, + "location": { + "name": "Location 1", + "site": {"name": "Site 1"} + }, + "rack": { + "name": "Rack 1", + "site": {"name": "Site 1"}, + "location": { + "name": "Location 1", + "site": {"name": "Site 1"} + } + }, + "position": 1.0, + "face": "front", + "status": "active", + "airflow": "bottom-to-top", + "description": "Device 1 Description", + "comments": "Device 1 Comments", + "tags": [{"name": "Tag 1"}] + } + }, + "update": { + "device": { + "name": "Device ABC", + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "role": {"name": "Device Role 1"}, + "tenant": {"name": "Tenant 1"}, + "platform": {"name": "Platform 1"}, + "serial": "1234567890", + "asset_tag": "asset.1", + "site": {"name": "Site 1"}, + "location": { + "name": "Location 1", + "site": {"name": "Site 1"} + }, + "rack": { + "name": "Rack 1", + "site": {"name": "Site 1"}, + "location": { + "name": "Location 1", + "site": {"name": "Site 1"} + } + }, + "position": 1.0, + "face": "front", + "status": "active", + "airflow": "bottom-to-top", + "description": "Device 1 Description Updated", + "comments": "Device 1 Comments", + "tags": [{"name": "Tag 1"}] + } + }, + "update_expect": { + "description": "Device 1 Description Updated" + } + }, + { + "name": "dcim_devicebay_1", + "object_type": "dcim.devicebay", + "lookup": { + "device__name": "Device 1", + "name": "Device Bay 1" + }, + "create_expect": { + "device.name": "Device 1", + "name": "Device Bay 1", + "description": "Device Bay 1 Description" + }, + "create": { + "device_bay": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C3P0", + "subdevice_role": "parent" + }, + "site": {"name": "Site 1"} + }, + "name": "Device Bay 1", + "label": "Device Bay 1 Label", + "description": "Device Bay 1 Description", + "installed_device": { + "name": "Device 2", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "tags": [{"name": "Tag 1"}] + } + }, + "update": { + "device_bay": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C3P0", + "subdevice_role": "parent" + }, + "site": {"name": "Site 1"} + }, + "name": "Device Bay 1", + "label": "Device Bay 1 Label", + "description": "Device Bay 1 Description Updated", + "installed_device": { + "name": "Device 2", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "tags": [{"name": "Tag 1"}] + } + }, + "update_expect": { + "description": "Device Bay 1 Description Updated" + } + }, + { + "name": "dcim_devicerole_1", + "object_type": "dcim.devicerole", + "lookup": {"name": "Core Router"}, + "create_expect": { + "name": "Core Router", + "description": "Primary network routing device" + }, + "create": { + "device_role": { + "name": "Core Router", + "slug": "core-router", + "color": "ff0000", + "vm_role": true, + "description": "Primary network routing device", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "device_role": { + "name": "Core Router", + "slug": "core-router", + "color": "ff0000", + "vm_role": true, + "description": "Primary network routing device Updated", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Primary network routing device Updated" + } + }, + { + "name": "dcim_devicetype_1", + "object_type": "dcim.devicetype", + "lookup": { + "manufacturer__name": "Cisco", + "model": "Catalyst 9300" + }, + "create_expect": { + "manufacturer.name": "Cisco", + "model": "Catalyst 9300", + "description": "Enterprise Series Switch" + }, + "create": { + "device_type": { + "manufacturer": {"name": "Cisco"}, + "default_platform": {"name": "IOS-XE"}, + "model": "Catalyst 9300", + "slug": "catalyst-9300", + "part_number": "C9300-48P-E", + "u_height": 1.0, + "exclude_from_utilization": false, + "is_full_depth": true, + "subdevice_role": "parent", + "airflow": "front-to-rear", + "weight": 14.5, + "weight_unit": "lb", + "description": "Enterprise Series Switch", + "comments": "High-performance access switch", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "device_type": { + "manufacturer": {"name": "Cisco"}, + "default_platform": {"name": "IOS-XE"}, + "model": "Catalyst 9300", + "slug": "catalyst-9300", + "part_number": "C9300-48P-E", + "u_height": 1.0, + "exclude_from_utilization": false, + "is_full_depth": true, + "subdevice_role": "parent", + "airflow": "front-to-rear", + "weight": 14.5, + "weight_unit": "lb", + "description": "Enterprise Series Switch Updated", + "comments": "High-performance access switch", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Enterprise Series Switch Updated" + } + }, + { + "name": "ipam_fhrpgroup_1", + "object_type": "ipam.fhrpgroup", + "lookup": {"name": "HSRP Group 10"}, + "create_expect": { + "name": "HSRP Group 10", + "protocol": "hsrp", + "group_id": 10, + "description": "Core Router HSRP Group" + }, + "create": { + "fhrp_group": { + "name": "HSRP Group 10", + "protocol": "hsrp", + "group_id": "10", + "auth_type": "md5", + "auth_key": "secretkey123", + "description": "Core Router HSRP Group", + "comments": "Primary gateway redundancy group", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "fhrp_group": { + "name": "HSRP Group 10", + "protocol": "hsrp", + "group_id": "10", + "auth_type": "md5", + "auth_key": "secretkey123", + "description": "Core Router HSRP Group Updated", + "comments": "Primary gateway redundancy group", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Core Router HSRP Group Updated" + } + }, + { + "name": "ipam_fhrpgroupassignment_1", + "object_type": "ipam.fhrpgroupassignment", + "lookup": { + "group__name": "HSRP Group 10" + }, + "create_expect": { + "group.name": "HSRP Group 10", + "priority": 100 + }, + "create": { + "fhrp_group_assignment": { + "group": { + "name": "HSRP Group 10", + "protocol": "hsrp", + "group_id": "10" + }, + "interface_interface": { + "device": { + "name": "Device 1", + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "role": {"name": "Device Role 1"}, + "site": {"name": "Site 1"} + }, + "name": "GigabitEthernet1/0/1", + "type": "1000base-t", + "enabled": true + }, + "priority": 100 + } + }, + "update": { + "fhrp_group_assignment": { + "group": { + "name": "HSRP Group 10", + "protocol": "hsrp", + "group_id": "10" + }, + "interface_interface": { + "device": { + "name": "Device 1", + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "role": {"name": "Device Role 1"}, + "site": {"name": "Site 1"} + }, + "name": "GigabitEthernet1/0/1", + "type": "1000base-t", + "enabled": true + }, + "priority": 200 + } + }, + "update_expect": { + "priority": 200 + } + }, + { + "name": "dcim_frontport_1", + "object_type": "dcim.frontport", + "lookup": { + "device__name": "Device 1", + "name": "Front Port 1" + }, + "create_expect": { + "device.name": "Device 1", + "name": "Front Port 1", + "description": "Front fiber port" + }, + "create": { + "front_port": { + "device": { + "name": "Device 1", + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "role": {"name": "Device Role 1"}, + "site": {"name": "Site 1"} + }, + "module": { + "device": { + "name": "Device 1", + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "role": {"name": "Device Role 1"}, + "site": {"name": "Site 1"} + }, + "module_bay": { + "name": "Module Bay 1", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + } + }, + "module_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S-MODULE" + } + }, + "name": "Front Port 1", + "label": "FP1", + "type": "lc-apc", + "color": "0000ff", + "rear_port": { + "device": { + "name": "Device 1", + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "role": {"name": "Device Role 1"}, + "site": {"name": "Site 1"} + }, + "name": "Rear Port 1", + "type": "lc-apc" + }, + "rear_port_position": "1", + "description": "Front fiber port", + "mark_connected": true, + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "front_port": { + "device": { + "name": "Device 1", + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "role": {"name": "Device Role 1"}, + "site": {"name": "Site 1"} + }, + "module": { + "device": { + "name": "Device 1", + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "role": {"name": "Device Role 1"}, + "site": {"name": "Site 1"} + }, + "module_bay": { + "name": "Module Bay 1", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + } + }, + "module_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S-MODULE" + } + }, + "name": "Front Port 1", + "label": "FP1", + "type": "lc-apc", + "color": "0000ff", + "rear_port": { + "device": { + "name": "Device 1", + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "role": {"name": "Device Role 1"}, + "site": {"name": "Site 1"} + }, + "name": "Rear Port 1", + "type": "lc-apc" + }, + "rear_port_position": "1", + "description": "Front fiber port Updated", + "mark_connected": true, + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Front fiber port Updated" + } + }, + { + "name": "vpn_ikepolicy_1", + "object_type": "vpn.ikepolicy", + "lookup": {"name": "IKE-POLICY-1"}, + "create_expect": { + "name": "IKE-POLICY-1", + "version": 2, + "description": "Main IPSec IKE Policy" + }, + "create": { + "ike_policy": { + "name": "IKE-POLICY-1", + "description": "Main IPSec IKE Policy", + "version": "2", + "preshared_key": "secretPSK123!", + "comments": "Primary IKE policy for VPN tunnels", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}], + "proposals": [ + { + "name": "IKE-PROPOSAL-1", + "description": "AES-256 with SHA-256", + "authentication_method": "preshared-keys", + "encryption_algorithm": "aes-256-cbc", + "authentication_algorithm": "hmac-sha256", + "group": "14", + "sa_lifetime": "28800" + } + ] + } + }, + "update": { + "ike_policy": { + "name": "IKE-POLICY-1", + "description": "Main IPSec IKE Policy Updated", + "version": "2", + "preshared_key": "secretPSK123!", + "comments": "Primary IKE policy for VPN tunnels", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}], + "proposals": [ + { + "name": "IKE-PROPOSAL-1", + "description": "AES-256 with SHA-256", + "authentication_method": "preshared-keys", + "encryption_algorithm": "aes-256-cbc", + "authentication_algorithm": "hmac-sha256", + "group": "14", + "sa_lifetime": "28800" + } + ] + } + }, + "update_expect": { + "description": "Main IPSec IKE Policy Updated" + } + }, + { + "name": "vpn_ikeproposal_1", + "object_type": "vpn.ikeproposal", + "lookup": {"name": "IKE-PROPOSAL-2"}, + "create_expect": { + "name": "IKE-PROPOSAL-2", + "description": "High Security IKE Proposal" + }, + "create": { + "ike_proposal": { + "name": "IKE-PROPOSAL-2", + "description": "High Security IKE Proposal", + "authentication_method": "certificates", + "encryption_algorithm": "aes-256-gcm", + "authentication_algorithm": "hmac-sha512", + "group": "21", + "sa_lifetime": "86400", + "comments": "Enhanced security proposal for critical VPNs", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "ike_proposal": { + "name": "IKE-PROPOSAL-2", + "description": "High Security IKE Proposal Updated", + "authentication_method": "certificates", + "encryption_algorithm": "aes-256-gcm", + "authentication_algorithm": "hmac-sha512", + "group": "21", + "sa_lifetime": "86400", + "comments": "Enhanced security proposal for critical VPNs", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "High Security IKE Proposal Updated" + } + }, + { + "name": "ipam_ipaddress_1", + "object_type": "ipam.ipaddress", + "lookup": {"address": "192.168.100.1/24"}, + "create_expect": { + "address": "192.168.100.1/24", + "vrf.name": "PROD-VRF", + "description": "Production VIP Address" + }, + "create": { + "ip_address": { + "address": "192.168.100.1/24", + "vrf": { + "name": "PROD-VRF", + "rd": "65000:1" + }, + "tenant": {"name": "Tenant 1"}, + "status": "active", + "role": "vip", + "assigned_object_interface": { + "device": { + "name": "Device 1", + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "role": {"name": "Device Role 1"}, + "site": {"name": "Site 1"} + }, + "name": "GigabitEthernet1/0/1", + "type": "1000base-t" + }, + "nat_inside": { + "address": "10.0.0.1/24" + }, + "dns_name": "prod-vip.example.com", + "description": "Production VIP Address", + "comments": "Primary virtual IP for load balancing", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "ip_address": { + "address": "192.168.100.1/24", + "vrf": { + "name": "PROD-VRF", + "rd": "65000:1" + }, + "tenant": {"name": "Tenant 1"}, + "status": "active", + "role": "vip", + "assigned_object_interface": { + "device": { + "name": "Device 1", + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "role": {"name": "Device Role 1"}, + "site": {"name": "Site 1"} + }, + "name": "GigabitEthernet1/0/1", + "type": "1000base-t" + }, + "nat_inside": { + "address": "10.0.0.1/24" + }, + "dns_name": "prod-vip.example.com", + "description": "Production VIP Address Updated", + "comments": "Primary virtual IP for load balancing", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Production VIP Address Updated" + } + }, + { + "name": "ipam_iprange_1", + "object_type": "ipam.iprange", + "lookup": { + "start_address": "10.100.0.1", + "end_address": "10.100.0.254" + }, + "create_expect": { + "start_address": "10.100.0.1/32", + "end_address": "10.100.0.254/32", + "description": "Production Server IP Range" + }, + "create": { + "ip_range": { + "start_address": "10.100.0.1", + "end_address": "10.100.0.254", + "vrf": { + "name": "PROD-VRF", + "rd": "65000:1" + }, + "tenant": {"name": "Tenant 1"}, + "status": "active", + "role": { + "name": "Server Pool", + "slug": "server-pool" + }, + "description": "Production Server IP Range", + "comments": "Allocated for production server deployments", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}], + "mark_utilized": true + } + }, + "update": { + "ip_range": { + "start_address": "10.100.0.1", + "end_address": "10.100.0.254", + "vrf": { + "name": "PROD-VRF", + "rd": "65000:1" + }, + "tenant": {"name": "Tenant 1"}, + "status": "active", + "role": { + "name": "Server Pool", + "slug": "server-pool" + }, + "description": "Production Server IP Range Updated", + "comments": "Allocated for production server deployments", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}], + "mark_utilized": true + } + }, + "update_expect": { + "description": "Production Server IP Range Updated" + } + }, + { + "name": "vpn_ipsecpolicy_1", + "object_type": "vpn.ipsecpolicy", + "lookup": {"name": "IPSEC-POLICY-1"}, + "create_expect": { + "name": "IPSEC-POLICY-1", + "description": "Site-to-Site VPN Policy" + }, + "create": { + "ip_sec_policy": { + "name": "IPSEC-POLICY-1", + "description": "Site-to-Site VPN Policy", + "pfs_group": "14", + "comments": "High-security IPSec policy for site-to-site VPN", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}], + "proposals": [ + { + "name": "IPSEC-PROPOSAL-1", + "description": "AES-256-GCM with ESP", + "encryption_algorithm": "aes-256-gcm", + "sa_lifetime_seconds": "28800", + "sa_lifetime_data": "28800", + "comments": "Strong encryption proposal for VPN tunnels" + } + ] + } + }, + "update": { + "ip_sec_policy": { + "name": "IPSEC-POLICY-1", + "description": "Site-to-Site VPN Policy Updated", + "pfs_group": "14", + "comments": "High-security IPSec policy for site-to-site VPN", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}], + "proposals": [ + { + "name": "IPSEC-PROPOSAL-1", + "description": "AES-256-GCM with ESP", + "encryption_algorithm": "aes-256-gcm", + "sa_lifetime_seconds": "28800", + "sa_lifetime_data": "28800", + "comments": "Strong encryption proposal for VPN tunnels" + } + ] + } + }, + "update_expect": { + "description": "Site-to-Site VPN Policy Updated" + } + }, + { + "name": "vpn_ipsecprofile_1", + "object_type": "vpn.ipsecprofile", + "lookup": {"name": "IPSEC-PROFILE-1"}, + "create_expect": { + "name": "IPSEC-PROFILE-1", + "description": "Remote Access VPN Profile" + }, + "create": { + "ip_sec_profile": { + "name": "IPSEC-PROFILE-1", + "description": "Remote Access VPN Profile", + "mode": "esp", + "ike_policy": { + "name": "IKE-POLICY-1", + "version": "2", + "preshared_key": "secretkey123" + }, + "ipsec_policy": { + "name": "IPSEC-POLICY-1", + "description": "Strong encryption policy", + "pfs_group": "14" + }, + "comments": "Standard IPSec profile for remote access VPN tunnels", + "tags": [{"name": "VPN"}, {"name": "Remote-Access"}] + } + }, + "update": { + "ip_sec_profile": { + "name": "IPSEC-PROFILE-1", + "description": "Remote Access VPN Profile Updated", + "mode": "esp", + "ike_policy": { + "name": "IKE-POLICY-1", + "version": "2", + "preshared_key": "secretkey123" + }, + "ipsec_policy": { + "name": "IPSEC-POLICY-1", + "description": "Strong encryption policy", + "pfs_group": "14" + }, + "comments": "Standard IPSec profile for remote access VPN tunnels", + "tags": [{"name": "VPN"}, {"name": "Remote-Access"}] + } + }, + "update_expect": { + "description": "Remote Access VPN Profile Updated" + } + }, + { + "name": "vpn_ipsecproposal_1", + "object_type": "vpn.ipsecproposal", + "lookup": {"name": "IPSec-Proposal-AES256"}, + "create_expect": { + "name": "IPSec-Proposal-AES256", + "description": "High security IPSec proposal using AES-256-GCM" + }, + "create": { + "ip_sec_proposal": { + "name": "IPSec-Proposal-AES256", + "description": "High security IPSec proposal using AES-256-GCM", + "encryption_algorithm": "aes-256-gcm", + "authentication_algorithm": "hmac-sha512", + "sa_lifetime_seconds": "28800", + "sa_lifetime_data": "42949", + "comments": "Used for critical infrastructure VPNs", + "tags": [ + { + "name": "high-security", + "slug": "high-security", + "color": "0000ff" + }, + { + "name": "production", + "slug": "production", + "color": "0000ff" + } + ] + } + }, + "update": { + "ip_sec_proposal": { + "name": "IPSec-Proposal-AES256", + "description": "High security IPSec proposal using AES-256-GCM Updated", + "encryption_algorithm": "aes-256-gcm", + "authentication_algorithm": "hmac-sha512", + "sa_lifetime_seconds": "28800", + "sa_lifetime_data": "42949", + "comments": "Used for critical infrastructure VPNs", + "tags": [ + { + "name": "high-security", + "slug": "high-security", + "color": "0000ff" + }, + { + "name": "production", + "slug": "production", + "color": "0000ff" + } + ] + } + }, + "update_expect": { + "description": "High security IPSec proposal using AES-256-GCM Updated" + } + }, + { + "name": "dcim_interface_1", + "object_type": "dcim.interface", + "lookup": {"name": "GigabitEthernet1/0/1"}, + "create_expect": { + "name": "GigabitEthernet1/0/1", + "label": "Core Link 1", + "tagged_vlans.all.0.vid": 101, + "tagged_vlans.all.1.vid": 102 + }, + "create": { + "interface": { + "name": "GigabitEthernet1/0/1", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "module": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "module_bay": { + "name": "Module Bay 1", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + } + }, + "module_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S-MODULE" + } + }, + "label": "Core Link 1", + "type": "1000base-t", + "enabled": true, + "bridge": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "name": "Bridge1", + "type": "bridge" + }, + "lag": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "name": "Port-Channel2", + "type": "lag" + }, + "mtu": "9000", + "primary_mac_address": { + "mac_address": "00:11:22:33:44:55" + }, + "speed": "1000000000", + "duplex": "full", + "wwn": "50:01:43:80:00:00:00:00", + "vrf": { + "name": "PROD-VRF", + "rd": "65000:1" + }, + "description": "Core network interface", + "mode": "tagged", + "mark_connected": true, + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}], + "mgmt_only": false, + "poe_mode": "pse", + "poe_type": "type3-ieee802.3bt", + "untagged_vlan": { + "vid": 100, + "name": "Data VLAN", + "status": "active" + }, + "vlan_translation_policy": { + "name": "Customer Translation Policy", + "description": "VLAN translation for customer traffic" + }, + "vdcs": [ + { + "name": "VDC1", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "identifier": 1, + "status": "active", + "description": "Primary VDC" + } + ], + "tagged_vlans": [ + { + "vid": 101, + "name": "Voice VLAN", + "status": "active" + }, + { + "vid": 102, + "name": "Data VLAN", + "status": "active" + } + ] + } + }, + "update": { + "interface": { + "name": "GigabitEthernet1/0/1", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "module": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "module_bay": { + "name": "Module Bay 1", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + } + }, + "module_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S-MODULE" + } + }, + "label": "Core Link 1", + "type": "1000base-t", + "enabled": true, + "bridge": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "name": "Bridge1", + "type": "bridge" + }, + "lag": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "name": "Port-Channel2", + "type": "lag" + }, + "mtu": "9000", + "primary_mac_address": { + "mac_address": "00:11:22:33:44:55" + }, + "speed": "1000000000", + "duplex": "full", + "wwn": "50:01:43:80:00:00:00:00", + "vrf": { + "name": "PROD-VRF", + "rd": "65000:1" + }, + "description": "Core network interface", + "mode": "q-in-q", + "mark_connected": true, + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}, {"name": "Tag 3"}], + "mgmt_only": false, + "poe_mode": "pse", + "poe_type": "type3-ieee802.3bt", + "untagged_vlan": { + "vid": 100, + "name": "Data VLAN", + "status": "active" + }, + "vlan_translation_policy": { + "name": "Customer Translation Policy", + "description": "VLAN translation for customer traffic" + }, + "vdcs": [ + { + "name": "VDC1", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "identifier": 1, + "status": "active", + "description": "Primary VDC" + } + ], + "tagged_vlans": [ + { + "vid": 101, + "name": "Voice VLAN", + "status": "active" + }, + { + "vid": 102, + "name": "Data VLAN", + "status": "active" + } + ] + } + }, + "update_expect": { + "tags.all.__by_name": ["Tag 1", "Tag 2", "Tag 3"] + } + }, + { + "name": "dcim_interface_2", + "object_type": "dcim.interface", + "lookup": {"name": "WirelessGigabitEthernet1/0/1"}, + "create_expect": { + "name": "WirelessGigabitEthernet1/0/1", + "label": "Core Link 1" + }, + "create": { + "interface": { + "name": "WirelessGigabitEthernet1/0/1", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "module": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "module_bay": { + "name": "Module Bay 1", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + } + }, + "module_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S-MODULE" + } + }, + "label": "Core Link 1", + "type": "other-wireless", + "enabled": true, + "bridge": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "name": "Bridge1", + "type": "bridge" + }, + "lag": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "name": "Port-Channel2", + "type": "lag" + }, + "mtu": "9000", + "primary_mac_address": { + "mac_address": "00:11:22:33:44:55" + }, + "speed": "1000000000", + "duplex": "full", + "wwn": "50:01:43:80:00:00:00:00", + "rf_role": "ap", + "rf_channel": "2.4g-1-2412-22", + "tx_power": "20", + "wireless_lans": [ + { + "ssid": "Corp-Secure", + "description": "Corporate secure wireless network", + "group": { + "name": "Corporate Networks", + "slug": "corporate-networks" + }, + "status": "active", + "vlan": { + "vid": 800, + "name": "Production Servers" + }, + "tenant": {"name": "Tenant 1"} + } + ], + "vrf": { + "name": "PROD-VRF", + "rd": "65000:1" + }, + "description": "Core network interface", + "mode": "access", + "mark_connected": true, + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}], + "mgmt_only": false, + "untagged_vlan": { + "vid": 900, + "name": "Data VLAN", + "status": "active" + }, + "vlan_translation_policy": { + "name": "Customer Translation Policy", + "description": "VLAN translation for customer traffic" + }, + "vdcs": [ + { + "name": "VDC1", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "identifier": 1, + "status": "active", + "description": "Primary VDC" + } + ] + } + }, + "update": { + "interface": { + "name": "WirelessGigabitEthernet1/0/1", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "module": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "module_bay": { + "name": "Module Bay 1", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + } + }, + "module_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S-MODULE" + } + }, + "label": "Core Link 1", + "type": "other-wireless", + "enabled": true, + "bridge": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "name": "Bridge1", + "type": "bridge" + }, + "lag": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "name": "Port-Channel2", + "type": "lag" + }, + "mtu": "9000", + "primary_mac_address": { + "mac_address": "00:11:22:33:44:55" + }, + "speed": "1000000000", + "duplex": "full", + "wwn": "50:01:43:80:00:00:00:00", + "rf_role": "ap", + "rf_channel": "2.4g-1-2412-22", + "tx_power": "20", + "wireless_lans": [ + { + "ssid": "Corp-Secure", + "description": "Corporate secure wireless network", + "group": { + "name": "Corporate Networks", + "slug": "corporate-networks" + }, + "status": "active", + "vlan": { + "vid": 800, + "name": "Production Servers" + }, + "tenant": {"name": "Tenant 1"} + } + ], + "vrf": { + "name": "PROD-VRF", + "rd": "65000:1" + }, + "description": "Core network interface", + "mode": "access", + "mark_connected": true, + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}, {"name": "Tag 3"}], + "mgmt_only": false, + "untagged_vlan": { + "vid": 900, + "name": "Data VLAN", + "status": "active" + }, + "vlan_translation_policy": { + "name": "Customer Translation Policy", + "description": "VLAN translation for customer traffic" + }, + "vdcs": [ + { + "name": "VDC1", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "identifier": 1, + "status": "active", + "description": "Primary VDC" + } + ] + } + }, + "update_expect": { + "tags.all.__by_name": ["Tag 1", "Tag 2", "Tag 3"] + } + }, + { + "name": "dcim_interface_3", + "object_type": "dcim.interface", + "lookup": {"name": "VirtualGigabitEthernet1/0/1"}, + "create_expect": { + "name": "VirtualGigabitEthernet1/0/1", + "label": "Core Link 1" + }, + "create": { + "interface": { + "name": "VirtualGigabitEthernet1/0/1", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "parent": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "name": "Port-Channel1", + "type": "1000base-t" + }, + "module": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "module_bay": { + "name": "Module Bay 1", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + } + }, + "module_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S-MODULE" + } + }, + "label": "Core Link 1", + "type": "virtual", + "enabled": true, + "bridge": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "name": "Bridge1", + "type": "bridge" + }, + "mtu": "9000", + "primary_mac_address": { + "mac_address": "00:11:22:33:44:55" + }, + "speed": "1000000000", + "duplex": "full", + "wwn": "50:01:43:80:00:00:00:00", + "vrf": { + "name": "PROD-VRF", + "rd": "65000:1" + }, + "description": "Core network interface", + "mode": "q-in-q", + "mark_connected": false, + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}], + "mgmt_only": false, + "untagged_vlan": { + "vid": 444, + "name": "Data VLAN", + "status": "active" + }, + "qinq_svlan": { + "vid": 2000, + "name": "Service VLAN", + "status": "active" + }, + "vlan_translation_policy": { + "name": "Customer Translation Policy", + "description": "VLAN translation for customer traffic" + }, + "vdcs": [ + { + "name": "VDC1", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "identifier": 1, + "status": "active", + "description": "Primary VDC" + } + ] + } + }, + "update": { + "interface": { + "name": "VirtualGigabitEthernet1/0/1", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "parent": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "name": "Port-Channel1", + "type": "1000base-t" + }, + "module": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "module_bay": { + "name": "Module Bay 1", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + } + }, + "module_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S-MODULE" + } + }, + "label": "Core Link 1", + "type": "virtual", + "enabled": true, + "bridge": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "name": "Bridge1", + "type": "bridge" + }, + "mtu": "9000", + "primary_mac_address": { + "mac_address": "00:11:22:33:44:55" + }, + "speed": "1000000000", + "duplex": "full", + "wwn": "50:01:43:80:00:00:00:00", + "vrf": { + "name": "PROD-VRF", + "rd": "65000:1" + }, + "description": "Core network interface", + "mode": "q-in-q", + "mark_connected": false, + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}, {"name": "Tag 3"}], + "mgmt_only": false, + "untagged_vlan": { + "vid": 444, + "name": "Data VLAN", + "status": "active" + }, + "qinq_svlan": { + "vid": 2000, + "name": "Service VLAN", + "status": "active" + }, + "vlan_translation_policy": { + "name": "Customer Translation Policy", + "description": "VLAN translation for customer traffic" + }, + "vdcs": [ + { + "name": "VDC1", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "identifier": 1, + "status": "active", + "description": "Primary VDC" + } + ] + } + }, + "update_expect": { + "tags.all.__by_name": ["Tag 1", "Tag 2", "Tag 3"] + } + }, + { + "name": "vpn_l2vpn_1", + "object_type": "vpn.l2vpn", + "lookup": {"name": "Customer-VPLS-1"}, + "create_expect": { + "name": "Customer-VPLS-1", + "type": "vpls", + "description": "Customer VPLS service for multi-site connectivity" + }, + "create": { + "l2vpn": { + "name": "Customer-VPLS-1", + "slug": "customer-vpls-1", + "type": "vpls", + "identifier": "65000", + "import_targets": [ + { + "name": "65000:1001", + "description": "Primary import target" + }, + { + "name": "65000:1002", + "description": "Secondary import target" + } + ], + "export_targets": [ + { + "name": "65000:1003", + "description": "Primary export target" + } + ], + "description": "Customer VPLS service for multi-site connectivity", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "l2vpn": { + "name": "Customer-VPLS-1", + "slug": "customer-vpls-1", + "type": "vpls", + "identifier": "65000", + "import_targets": [ + { + "name": "65000:1001", + "description": "Primary import target" + }, + { + "name": "65000:1002", + "description": "Secondary import target" + } + ], + "export_targets": [ + { + "name": "65000:1003", + "description": "Primary export target" + } + ], + "description": "Customer VPLS service for multi-site connectivity Updated", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Customer VPLS service for multi-site connectivity Updated" + } + }, + { + "name": "dcim_inventoryitem_1", + "object_type": "dcim.inventoryitem", + "lookup": {"name": "Power Supply 1"}, + "create_expect": { + "name": "Power Supply 1", + "description": "715W AC Power Supply" + }, + "create": { + "inventory_item": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "parent": { + "name": "Chassis 1", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + } + }, + "name": "Power Supply 1", + "label": "PSU1", + "role": { + "name": "Power Supply", + "color": "00ff00" + }, + "manufacturer": {"name": "Cisco"}, + "part_id": "PWR-C1-715WAC", + "serial": "ABC123XYZ", + "asset_tag": "ASSET-001", + "discovered": true, + "description": "715W AC Power Supply", + "status": "active", + "component_power_port": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "name": "PSU1 Power Port", + "type": "iec-60320-c14", + "maximum_draw": 715, + "allocated_draw": 500, + "description": "Power input port for PSU1" + }, + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "inventory_item": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "parent": { + "name": "Chassis 1", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + } + }, + "name": "Power Supply 1", + "label": "PSU1", + "role": { + "name": "Power Supply", + "color": "00ff00" + }, + "manufacturer": {"name": "Cisco"}, + "part_id": "PWR-C1-715WAC", + "serial": "ABC123XYZ", + "asset_tag": "ASSET-001", + "discovered": true, + "description": "715W AC Power Supply Updated", + "status": "active", + "component_power_port": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "name": "PSU1 Power Port", + "type": "iec-60320-c14", + "maximum_draw": 715, + "allocated_draw": 500, + "description": "Power input port for PSU1" + }, + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "715W AC Power Supply Updated" + } + }, + { + "name": "dcim_inventoryitemrole_1", + "object_type": "dcim.inventoryitemrole", + "lookup": {"name": "Line Card"}, + "create_expect": { + "name": "Line Card", + "description": "Network switch line card module" + }, + "create": { + "inventory_item_role": { + "name": "Line Card", + "slug": "line-card", + "color": "0000ff", + "description": "Network switch line card module", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "inventory_item_role": { + "name": "Line Card", + "slug": "line-card", + "color": "0000ff", + "description": "Network switch line card module Updated", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Network switch line card module Updated" + } + }, + { + "name": "vpn_l2vpn_1", + "object_type": "vpn.l2vpn", + "lookup": {"name": "Customer-VPLS-1"}, + "create_expect": { + "name": "Customer-VPLS-1", + "type": "vpls", + "identifier": 65000, + "description": "Customer VPLS service for multi-site connectivity" + }, + "create": { + "l2vpn": { + "name": "Customer-VPLS-1", + "slug": "customer-vpls-1", + "type": "vpls", + "identifier": "65000", + "import_targets": [ + { + "name": "65000:1001", + "description": "Primary import target" + }, + { + "name": "65000:1002", + "description": "Secondary import target" + } + ], + "export_targets": [ + { + "name": "65000:1003", + "description": "Primary export target" + } + ], + "description": "Customer VPLS service for multi-site connectivity", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "l2vpn": { + "name": "Customer-VPLS-1", + "slug": "customer-vpls-1", + "type": "vpls", + "identifier": "65000", + "import_targets": [ + { + "name": "65000:1001", + "description": "Primary import target" + }, + { + "name": "65000:1002", + "description": "Secondary import target" + } + ], + "export_targets": [ + { + "name": "65000:1003", + "description": "Primary export target" + } + ], + "description": "Customer VPLS service for multi-site connectivity Updated", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Customer VPLS service for multi-site connectivity Updated" + } + }, + { + "name": "vpn_l2vpntermination_1", + "object_type": "vpn.l2vpntermination", + "lookup": { + "l2vpn__name": "Customer-VPLS-1" + }, + "create_expect": { + "l2vpn.name": "Customer-VPLS-1", + "l2vpn.type": "vpls" + }, + "create": { + "l2vpn_termination": { + "l2vpn": { + "name": "Customer-VPLS-1", + "type": "vpls", + "identifier": "65000" + }, + "assigned_object_interface": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "name": "GigabitEthernet1/0/1", + "type": "1000base-t", + "enabled": true + }, + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "l2vpn_termination": { + "l2vpn": { + "name": "Customer-VPLS-1", + "type": "vpls", + "identifier": "65000" + }, + "assigned_object_interface": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "name": "GigabitEthernet1/0/1", + "type": "1000base-t", + "enabled": true + }, + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}, {"name": "Tag 3"}] + } + }, + "update_expect": { + "tags.all.__by_name": ["Tag 1", "Tag 2", "Tag 3"] + } + }, + { + "name": "dcim_location_1", + "object_type": "dcim.location", + "lookup": {"name": "Data Center East Wing"}, + "create_expect": { + "name": "Data Center East Wing", + "description": "East wing of the main data center facility" + }, + "create": { + "location": { + "name": "Data Center East Wing", + "slug": "dc-east-wing", + "site": {"name": "Site 1"}, + "parent": { + "name": "Main Data Center", + "slug": "main-dc", + "site": {"name": "Site 1"} + }, + "status": "active", + "tenant": {"name": "Tenant 1"}, + "facility": "Building A, Floor 3", + "description": "East wing of the main data center facility", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "location": { + "name": "Data Center East Wing", + "slug": "dc-east-wing", + "site": {"name": "Site 1"}, + "parent": { + "name": "Main Data Center", + "slug": "main-dc", + "site": {"name": "Site 1"} + }, + "status": "active", + "tenant": {"name": "Tenant 1"}, + "facility": "Building A, Floor 3", + "description": "East wing of the main data center facility Updated", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "East wing of the main data center facility Updated" + } + }, + { + "name": "dcim_macaddress_1", + "object_type": "dcim.macaddress", + "lookup": {"mac_address": "00:1A:2B:3C:4D:5E"}, + "create_expect": { + "mac_address": "00:1A:2B:3C:4D:5E", + "description": "Primary management interface MAC" + }, + "create": { + "mac_address": { + "mac_address": "00:1A:2B:3C:4D:5E", + "assigned_object_interface": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "name": "GigabitEthernet1/0/1", + "type": "1000base-t", + "enabled": true + }, + "description": "Primary management interface MAC", + "comments": "Reserved for network management access", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "mac_address": { + "mac_address": "00:1A:2B:3C:4D:5E", + "assigned_object_interface": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "name": "GigabitEthernet1/0/1", + "type": "1000base-t", + "enabled": true + }, + "description": "Primary management interface MAC Updated", + "comments": "Reserved for network management access", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Primary management interface MAC Updated" + } + }, + { + "name": "dcim_manufacturer_1", + "object_type": "dcim.manufacturer", + "lookup": {"name": "Arista Networks"}, + "create_expect": { + "name": "Arista Networks", + "slug": "arista-networks", + "description": "Leading provider of cloud networking solutions" + }, + "create": { + "manufacturer": { + "name": "Arista Networks", + "slug": "arista-networks", + "description": "Leading provider of cloud networking solutions", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + + } + }, + "update": { + "manufacturer": { + "name": "Arista Networks", + "slug": "arista-networks", + "description": "Leading provider of cloud networking solutions Updated", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Leading provider of cloud networking solutions Updated" + } + }, + { + "name": "dcim_module_1", + "object_type": "dcim.module", + "lookup": {"asset_tag": "MOD-001"}, + "create_expect": { + "status": "active", + "serial": "MOD123XYZ", + "asset_tag": "MOD-001", + "description": "Stacking module for switch interconnect" + }, + "create": { + "module": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "module_bay": { + "name": "Module Bay 1", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + } + }, + "module_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S-STACK" + }, + "status": "active", + "serial": "MOD123XYZ", + "asset_tag": "MOD-001", + "description": "Stacking module for switch interconnect", + "comments": "Primary stack member module", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "module": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "module_bay": { + "name": "Module Bay 1", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + } + }, + "module_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S-STACK" + }, + "status": "active", + "serial": "MOD123XYZ", + "asset_tag": "MOD-001", + "description": "Stacking module for switch interconnect Updated", + "comments": "Primary stack member module", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Stacking module for switch interconnect Updated" + } + }, + { + "name": "dcim_modulebay_1", + "object_type": "dcim.modulebay", + "lookup": {"name": "Stack Module Bay 2"}, + "create_expect": { + "name": "Stack Module Bay 2", + "description": "Secondary stacking module bay" + }, + "create": { + "module_bay": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "name": "Stack Module Bay 2", + "module": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "module_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S-STACK" + }, + "module_bay": { + "name": "Module Bay 1", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + } + } + + }, + "label": "STACK-2", + "position": "Rear", + "description": "Secondary stacking module bay", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "module_bay": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "name": "Stack Module Bay 2", + "module": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "module_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S-STACK" + }, + "module_bay": { + "name": "Module Bay 1", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + } + } + + }, + "label": "STACK-2", + "position": "Rear", + "description": "Secondary stacking module bay Updated", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Secondary stacking module bay Updated" + } + }, + { + "name": "dcim_moduletype_1", + "object_type": "dcim.moduletype", + "lookup": { + "manufacturer__name": "Cisco", + "model": "C9300-NM-8X" + }, + "create_expect": { + "manufacturer.name": "Cisco", + "model": "C9300-NM-8X", + "description": "Catalyst 9300 8 x 10GE Network Module" + }, + "create": { + "module_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C9300-NM-8X", + "part_number": "C9300-NM-8X=", + "airflow": "front-to-rear", + "weight": 0.7, + "weight_unit": "kg", + "description": "Catalyst 9300 8 x 10GE Network Module", + "comments": "Hot-swappable uplink module for C9300 series switches", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "module_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C9300-NM-8X", + "part_number": "C9300-NM-8X=", + "airflow": "front-to-rear", + "weight": 0.7, + "weight_unit": "kg", + "description": "Catalyst 9300 8 x 10GE Network Module Updated", + "comments": "Hot-swappable uplink module for C9300 series switches", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Catalyst 9300 8 x 10GE Network Module Updated" + } + }, + { + "name": "dcim_platform_1", + "object_type": "dcim.platform", + "lookup": {"name": "Cisco IOS-XE"}, + "create_expect": { + "name": "Cisco IOS-XE", + "manufacturer.name": "Cisco", + "description": "Enterprise-class IOS operating system for Catalyst switches and ISR routers" + }, + "create": { + "platform": { + "name": "Cisco IOS-XE", + "slug": "cisco-ios-xe", + "manufacturer": {"name": "Cisco"}, + "description": "Enterprise-class IOS operating system for Catalyst switches and ISR routers", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "platform": { + "name": "Cisco IOS-XE", + "slug": "cisco-ios-xe", + "manufacturer": {"name": "Cisco"}, + "description": "Enterprise-class IOS operating system for Catalyst switches and ISR routers Updated", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Enterprise-class IOS operating system for Catalyst switches and ISR routers Updated" + } + }, + { + "name": "dcim_powerfeed_1", + "object_type": "dcim.powerfeed", + "lookup": {"name": "Power Feed A1"}, + "create_expect": { + "name": "Power Feed A1", + "power_panel.name": "Panel A", + "description": "Primary power feed for network equipment rack" + }, + "create": { + "power_feed": { + "power_panel": { + "site": {"name": "Site 1"}, + "name": "Panel A" + }, + "rack": { + "name": "Rack 1", + "site": {"name": "Site 1"}, + "location": { + "name": "Location 1", + "site": {"name": "Site 1"} + } + }, + "name": "Power Feed A1", + "status": "active", + "type": "primary", + "supply": "ac", + "phase": "three-phase", + "voltage": "208", + "amperage": "30", + "max_utilization": "80", + "mark_connected": true, + "description": "Primary power feed for network equipment rack", + "tenant": {"name": "Tenant 1"}, + "comments": "Connected to UPS system A with redundant backup", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "power_feed": { + "power_panel": { + "site": {"name": "Site 1"}, + "name": "Panel A" + }, + "rack": { + "name": "Rack 1", + "site": {"name": "Site 1"}, + "location": { + "name": "Location 1", + "site": {"name": "Site 1"} + } + }, + "name": "Power Feed A1", + "status": "active", + "type": "primary", + "supply": "ac", + "phase": "three-phase", + "voltage": "208", + "amperage": "30", + "max_utilization": "80", + "mark_connected": true, + "description": "Primary power feed for network equipment rack Updated", + "tenant": {"name": "Tenant 1"}, + "comments": "Connected to UPS system A with redundant backup", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Primary power feed for network equipment rack Updated" + } + }, + { + "name": "dcim_poweroutlet_1", + "object_type": "dcim.poweroutlet", + "lookup": { + "device__name": "Device 1", + "name": "PSU1-Outlet1" + }, + "create_expect": { + "device.name": "Device 1", + "name": "PSU1-Outlet1", + "description": "Power outlet for network switch PSU" + }, + "create": { + "power_outlet": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "module": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "module_type": { + "manufacturer": {"name": "Cisco"}, + "model": "PWR-C1-715WAC" + }, + "module_bay": { + "name": "Module Bay 1", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + } + } + }, + "name": "PSU1-Outlet1", + "label": "OUT-1", + "type": "iec-60320-c13", + "color": "0000ff", + "power_port": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "name": "PSU1" + }, + "feed_leg": "A", + "description": "Power outlet for network switch PSU", + "mark_connected": true, + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "power_outlet": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "module": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "module_type": { + "manufacturer": {"name": "Cisco"}, + "model": "PWR-C1-715WAC" + }, + "module_bay": { + "name": "Module Bay 1", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + } + } + }, + "name": "PSU1-Outlet1", + "label": "OUT-1", + "type": "iec-60320-c13", + "color": "0000ff", + "power_port": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "name": "PSU1" + }, + "feed_leg": "A", + "description": "Power outlet for network switch PSU Updated", + "mark_connected": true, + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Power outlet for network switch PSU Updated" + } + }, + { + "name": "dcim_powerpanel_1", + "object_type": "dcim.powerpanel", + "lookup": { + "site__name": "Site 1", + "name": "Panel A" + }, + "create_expect": { + "site.name": "Site 1", + "name": "Panel A", + "description": "Main power distribution panel" + }, + "create": { + "power_panel": { + "site": {"name": "Site 1"}, + "location": { + "name": "Location 1", + "site": {"name": "Site 1"} + }, + "name": "Panel A", + "description": "Main power distribution panel", + "comments": "Primary power distribution for data center", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "power_panel": { + "site": {"name": "Site 1"}, + "location": { + "name": "Location 1", + "site": {"name": "Site 1"} + }, + "name": "Panel A", + "description": "Main power distribution panel Updated", + "comments": "Primary power distribution for data center", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Main power distribution panel Updated" + } + }, + { + "name": "dcim_powerport_1", + "object_type": "dcim.powerport", + "lookup": { + "device__name": "Device 1", + "name": "PSU1" + }, + "create_expect": { + "device.name": "Device 1", + "name": "PSU1", + "description": "Primary power supply unit" + }, + "create": { + "power_port": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "module": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "module_type": { + "manufacturer": {"name": "Cisco"}, + "model": "PWR-C1-715WAC" + }, + "module_bay": { + "name": "Module Bay 1", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + } + } + }, + "name": "PSU1", + "label": "PSU-1", + "type": "iec-60320-c14", + "maximum_draw": 715, + "allocated_draw": 650, + "description": "Primary power supply unit", + "mark_connected": true, + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "power_port": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "module": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "module_type": { + "manufacturer": {"name": "Cisco"}, + "model": "PWR-C1-715WAC" + }, + "module_bay": { + "name": "Module Bay 1", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + } + } + }, + "name": "PSU1", + "label": "PSU-1", + "type": "iec-60320-c14", + "maximum_draw": 715, + "allocated_draw": 650, + "description": "Primary power supply unit Updated", + "mark_connected": true, + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Primary power supply unit Updated" + } + }, + { + "name": "ipam_prefix_1", + "object_type": "ipam.prefix", + "lookup": {"prefix": "10.100.0.0/16"}, + "create_expect": { + "prefix": "10.100.0.0/16", + "description": "Production network address space" + }, + "create": { + "prefix": { + "prefix": "10.100.0.0/16", + "vrf": { + "name": "PROD-VRF", + "rd": "65000:1" + }, + "scope_site": {"name": "Site 1"}, + "tenant": {"name": "Tenant 1"}, + "vlan": { + "name": "Production VLAN", + "vid": "112" + }, + "status": "active", + "role": { + "name": "Production", + "slug": "production" + }, + "is_pool": true, + "mark_utilized": true, + "description": "Production network address space", + "comments": "Primary address allocation for production services", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "prefix": { + "prefix": "10.100.0.0/16", + "vrf": { + "name": "PROD-VRF", + "rd": "65000:1" + }, + "scope_site": {"name": "Site 1"}, + "tenant": {"name": "Tenant 1"}, + "vlan": { + "name": "Production VLAN", + "vid": "112" + }, + "status": "active", + "role": { + "name": "Production", + "slug": "production" + }, + "is_pool": true, + "mark_utilized": true, + "description": "Production network address space Updated", + "comments": "Primary address allocation for production services", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Production network address space Updated" + } + }, + { + "name": "circuits_provider_1", + "object_type": "circuits.provider", + "lookup": {"name": "Level 3 Communications"}, + "create_expect": { + "name": "Level 3 Communications", + "slug": "level3", + "description": "Global Tier 1 Internet Service Provider" + }, + "create": { + "provider": { + "name": "Level 3 Communications", + "slug": "level3", + "description": "Global Tier 1 Internet Service Provider", + "comments": "Primary transit provider for data center connectivity", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}], + "accounts": [ + { + "provider": {"name": "Level 3 Communications"}, + "name": "East Coast Account", + "account": "L3-12345", + "description": "East Coast regional services account", + "comments": "Managed through regional NOC" + }, + { + "provider": {"name": "Level 3 Communications"}, + "name": "West Coast Account", + "account": "L3-67890", + "description": "West Coast regional services account", + "comments": "Managed through regional NOC" + } + ], + "asns": [ + { + "asn": "3356", + "rir": {"name": "ARIN"}, + "tenant": {"name": "Tenant 1"}, + "description": "Level 3 Global ASN", + "comments": "Primary transit ASN" + } + ] + } + }, + "update": { + "provider": { + "name": "Level 3 Communications", + "slug": "level3", + "description": "Global Tier 1 Internet Service Provider Updated", + "comments": "Primary transit provider for data center connectivity", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}], + "accounts": [ + { + "provider": {"name": "Level 3 Communications"}, + "name": "East Coast Account", + "account": "L3-12345", + "description": "East Coast regional services account", + "comments": "Managed through regional NOC" + }, + { + "provider": {"name": "Level 3 Communications"}, + "name": "West Coast Account", + "account": "L3-67890", + "description": "West Coast regional services account", + "comments": "Managed through regional NOC" + } + ], + "asns": [ + { + "asn": "3356", + "rir": {"name": "ARIN"}, + "tenant": {"name": "Tenant 1"}, + "description": "Level 3 Global ASN", + "comments": "Primary transit ASN" + } + ] + } + }, + "update_expect": { + "description": "Global Tier 1 Internet Service Provider Updated" + } + }, + { + "name": "circuits_provideraccount_1", + "object_type": "circuits.provideraccount", + "lookup": { + "provider__name": "Level 3 Communications", + "account": "ACCT-12345" + }, + "create_expect": { + "provider.name": "Level 3 Communications", + "account": "ACCT-12345", + "description": "Primary enterprise account" + }, + "create": { + "provider_account": { + "provider": {"name": "Level 3 Communications"}, + "account": "ACCT-12345", + "name": "L3 Enterprise", + "description": "Primary enterprise account", + "comments": "Global services contract", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "provider_account": { + "provider": {"name": "Level 3 Communications"}, + "account": "ACCT-12345", + "name": "L3 Enterprise", + "description": "Primary enterprise account Updated", + "comments": "Global services contract", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Primary enterprise account Updated" + } + }, + { + "name": "circuits_providernetwork_1", + "object_type": "circuits.providernetwork", + "lookup": {"name": "Global MPLS Network"}, + "create_expect": { + "name": "Global MPLS Network", + "provider.name": "Level 3 Communications", + "description": "Global MPLS backbone network" + }, + "create": { + "provider_network": { + "provider": {"name": "Level 3 Communications"}, + "name": "Global MPLS Network", + "service_id": "L3-MPLS-001", + "description": "Global MPLS backbone network", + "comments": "Primary enterprise MPLS network infrastructure", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "provider_network": { + "provider": {"name": "Level 3 Communications"}, + "name": "Global MPLS Network", + "service_id": "L3-MPLS-001", + "description": "Global MPLS backbone network Updated", + "comments": "Primary enterprise MPLS network infrastructure", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Global MPLS backbone network Updated" + } + }, + { + "name": "ipam_rir_1", + "object_type": "ipam.rir", + "lookup": {"name": "ARIN"}, + "create_expect": { + "name": "ARIN", + "description": "American Registry for Internet Numbers" + }, + "create": { + "rir": { + "name": "ARIN", + "slug": "arin", + "is_private": false, + "description": "American Registry for Internet Numbers", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "rir": { + "name": "ARIN", + "slug": "arin", + "is_private": false, + "description": "American Registry for Internet Numbers Updated", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "American Registry for Internet Numbers Updated" + } + }, + { + "name": "dcim_rack_1", + "object_type": "dcim.rack", + "lookup": {"asset_tag": "RACK-009"}, + "create_expect": { + "name": "Rack ZZ", + "site.name": "Site 1", + "description": "Standard 42U server rack" + }, + "create": { + "rack": { + "name": "Rack ZZ", + "facility_id": "FAC-001", + "site": {"name": "Site 1"}, + "location": {"name": "Data Center East Wing", "site": {"name": "Site 1"}}, + "tenant": {"name": "Tenant 1"}, + "status": "active", + "role": { + "name": "Server Rack", + "slug": "server-rack", + "color": "0000ff", + "description": "Primary server rack role" + }, + "serial": "RACK123XYZ", + "asset_tag": "RACK-009", + "rack_type": { + "manufacturer": {"name": "Manufacturer 1"}, + "model": "R2000", + "slug": "r2000", + "form_factor": "4-post-cabinet" + }, + "form_factor": "4-post-cabinet", + "width": "19", + "u_height": "42", + "starting_unit": "1", + "desc_units": false, + "airflow": "front-to-rear", + "description": "Standard 42U server rack", + "comments": "Located in primary data center", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "rack": { + "name": "Rack ZZ", + "facility_id": "FAC-001", + "site": {"name": "Site 1"}, + "location": {"name": "Data Center East Wing", "site": {"name": "Site 1"}}, + "tenant": {"name": "Tenant 1"}, + "status": "active", + "role": { + "name": "Server Rack", + "slug": "server-rack", + "color": "0000ff", + "description": "Primary server rack role" + }, + "serial": "RACK123XYZ", + "asset_tag": "RACK-009", + "rack_type": { + "manufacturer": {"name": "Manufacturer 1"}, + "model": "R2000", + "slug": "r2000", + "form_factor": "4-post-cabinet" + }, + "form_factor": "4-post-cabinet", + "width": "19", + "u_height": "42", + "starting_unit": "1", + "desc_units": false, + "mounting_depth": "30", + "airflow": "front-to-rear", + "description": "Standard 42U server rack Updated", + "comments": "Located in primary data center", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Standard 42U server rack Updated" + } + }, + { + "name": "dcim_racktype_1", + "object_type": "dcim.racktype", + "lookup": { + "manufacturer__name": "Manufacturer 1", + "model": "R2000" + }, + "create_expect": { + "manufacturer.name": "Manufacturer 1", + "model": "R2000", + "description": "Standard 42U server rack" + }, + "create": { + "rack_type": { + "manufacturer": {"name": "Manufacturer 1"}, + "model": "R2000", + "slug": "r2000", + "description": "Standard 42U server rack", + "form_factor": "4-post-cabinet", + "width": "19", + "u_height": "42", + "starting_unit": "1", + "desc_units": false, + "outer_width": "24", + "outer_depth": "36", + "outer_unit": "in", + "weight": "350.5", + "max_weight": "1000", + "weight_unit": "lb", + "mounting_depth": "30", + "comments": "Standard enterprise rack configuration", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "rack_type": { + "manufacturer": {"name": "Manufacturer 1"}, + "model": "R2000", + "slug": "r2000", + "description": "Standard 42U server rack Updated", + "form_factor": "4-post-cabinet", + "width": "19", + "u_height": "42", + "starting_unit": "1", + "desc_units": false, + "outer_width": "24", + "outer_depth": "36", + "outer_unit": "in", + "weight": "350.5", + "max_weight": "1000", + "weight_unit": "lb", + "mounting_depth": "30", + "comments": "Standard enterprise rack configuration", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Standard 42U server rack Updated" + } + }, + { + "name": "dcim_rearport_1", + "object_type": "dcim.rearport", + "lookup": { + "device__name": "Device 1", + "name": "Rear Port 1" + }, + "create_expect": { + "device.name": "Device 1", + "name": "Rear Port 1", + "description": "Rear fiber port" + }, + "create": { + "rear_port": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "module": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "module_bay": { + "name": "Module Bay 1", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + } + }, + "module_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S-MODULE" + } + }, + "name": "Rear Port 1", + "label": "RP1", + "type": "lc-apc", + "color": "0000ff", + "positions": "1", + "description": "Rear fiber port", + "mark_connected": true, + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "rear_port": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "module": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "module_bay": { + "name": "Module Bay 1", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + } + }, + "module_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S-MODULE" + } + }, + "name": "Rear Port 1", + "label": "RP1", + "type": "lc-apc", + "color": "0000ff", + "positions": "1", + "description": "Rear fiber port Updated", + "mark_connected": true, + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Rear fiber port Updated" + } + }, + { + "name": "dcim_region_1", + "object_type": "dcim.region", + "lookup": {"name": "North America"}, + "create_expect": { + "name": "North America", + "parent.name": "Global", + "description": "North American Region" + }, + "create": { + "region": { + "name": "North America", + "slug": "north-america", + "parent": { + "name": "Global", + "slug": "global", + "description": "Global Region", + "tags": [{"name": "Tag 1"}] + }, + "description": "North American Region", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "region": { + "name": "North America", + "slug": "north-america", + "parent": { + "name": "Global", + "slug": "global", + "description": "Global Region", + "tags": [{"name": "Tag 1"}] + }, + "description": "North American Region Updated", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "North American Region Updated" + } + }, + { + "name": "ipam_role_1", + "object_type": "ipam.role", + "lookup": {"name": "Network Administrator"}, + "create_expect": { + "name": "Network Administrator", + "weight": 1000, + "description": "Primary network administration role" + }, + "create": { + "role": { + "name": "Network Administrator", + "slug": "network-admin", + "weight": "1000", + "description": "Primary network administration role", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "role": { + "name": "Network Administrator", + "slug": "network-admin", + "weight": "1000", + "description": "Primary network administration role Updated", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Primary network administration role Updated" + } + }, + { + "name": "ipam_routetarget_1", + "object_type": "ipam.routetarget", + "lookup": {"name": "65000:1001"}, + "create_expect": { + "name": "65000:1001", + "tenant.name": "Tenant 1", + "description": "Primary route target for MPLS VPN" + }, + "create": { + "route_target": { + "name": "65000:1001", + "tenant": {"name": "Tenant 1"}, + "description": "Primary route target for MPLS VPN", + "comments": "Used for customer VPN service", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "route_target": { + "name": "65000:1001", + "tenant": {"name": "Tenant 1"}, + "description": "Primary route target for MPLS VPN Updated", + "comments": "Used for customer VPN service", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Primary route target for MPLS VPN Updated" + } + }, + { + "name": "ipam_service_1", + "object_type": "ipam.service", + "lookup": {"name": "Web Server"}, + "create_expect": { + "name": "Web Server", + "protocol": "tcp", + "ports": [80, 443], + "description": "Primary web server service" + }, + "create": { + "service": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "name": "Web Server", + "protocol": "tcp", + "ports": ["80", "443"], + "description": "Primary web server service", + "comments": "Handles HTTPS traffic for main website", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}], + "ipaddresses": [ + { + "address": "192.168.1.100/24", + "status": "active", + "dns_name": "web.example.com" + } + ] + } + }, + "update": { + "service": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "name": "Web Server", + "protocol": "tcp", + "ports": ["80", "443"], + "description": "Primary web server service Updated", + "comments": "Handles HTTPS traffic for main website", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}], + "ipaddresses": [ + { + "address": "192.168.1.100/24", + "status": "active", + "dns_name": "web.example.com" + } + ] + } + }, + "update_expect": { + "description": "Primary web server service Updated" + } + }, + { + "name": "dcim_site_1", + "object_type": "dcim.site", + "lookup": {"name": "Data Center West"}, + "create_expect": { + "name": "Data Center West", + "region.name": "North America", + "group.name": "Primary Data Centers", + "description": "Primary West Coast Data Center" + }, + "create": { + "site": { + "name": "Data Center West", + "slug": "dc-west", + "status": "active", + "region": { + "name": "North America", + "slug": "north-america" + }, + "group": { + "name": "Primary Data Centers", + "slug": "primary-dcs" + }, + "tenant": {"name": "Tenant 1"}, + "facility": "Building 7", + "time_zone": "America/Los_Angeles", + "description": "Primary West Coast Data Center", + "physical_address": "123 Tech Drive, San Jose, CA 95134", + "shipping_address": "Receiving Dock 3, 123 Tech Drive, San Jose, CA 95134", + "latitude": 37.3382, + "longitude": -121.8863, + "comments": "24x7 access requires security clearance", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}], + "asns": [ + { + "asn": "555", + "rir": {"name": "RIR 1"}, + "tenant": {"name": "Tenant 1"}, + "description": "ASN 555 Description", + "comments": "ASN 555 Comments", + "tags": [{"name": "Tag 1"}] + } + ] + } + }, + "update": { + "site": { + "name": "Data Center West", + "slug": "dc-west", + "status": "active", + "region": { + "name": "North America", + "slug": "north-america" + }, + "group": { + "name": "Primary Data Centers", + "slug": "primary-dcs" + }, + "tenant": {"name": "Tenant 1"}, + "facility": "Building 7", + "time_zone": "America/Los_Angeles", + "description": "Primary West Coast Data Center Updated", + "physical_address": "123 Tech Drive, San Jose, CA 95134", + "shipping_address": "Receiving Dock 3, 123 Tech Drive, San Jose, CA 95134", + "latitude": 37.3382, + "longitude": -121.8863, + "comments": "24x7 access requires security clearance", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}], + "asns": [ + { + "asn": "555", + "rir": {"name": "RIR 1"}, + "tenant": {"name": "Tenant 1"}, + "description": "ASN 555 Description", + "comments": "ASN 555 Comments", + "tags": [{"name": "Tag 1"}] + } + ] + } + }, + "update_expect": { + "description": "Primary West Coast Data Center Updated" + } + }, + { + "name": "dcim_sitegroup_1", + "object_type": "dcim.sitegroup", + "lookup": {"name": "Global Data Centers"}, + "create_expect": { + "name": "Global Data Centers", + "parent.name": "Infrastructure", + "description": "Worldwide data center facilities" + }, + "create": { + "site_group": { + "name": "Global Data Centers", + "slug": "global-dcs", + "parent": { + "name": "Infrastructure", + "slug": "infrastructure" + }, + "description": "Worldwide data center facilities", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "site_group": { + "name": "Global Data Centers", + "slug": "global-dcs", + "parent": { + "name": "Infrastructure", + "slug": "infrastructure" + }, + "description": "Worldwide data center facilities Updated", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Worldwide data center facilities Updated" + } + }, + { + "name": "extras_tag_1", + "object_type": "extras.tag", + "lookup": {"name": "Production"}, + "create_expect": { + "name": "Production", + "slug": "production", + "color": "ff0000" + }, + "create": { + "tag": { + "name": "Production", + "slug": "production", + "color": "ff0000" + } + }, + "update": { + "tag": { + "name": "Production", + "slug": "production", + "color": "00ff00" + } + }, + "update_expect": { + "color": "00ff00" + } + }, + { + "name": "tenancy_tenant_1", + "object_type": "tenancy.tenant", + "lookup": {"name": "Acme Corporation"}, + "create_expect": { + "name": "Acme Corporation", + "slug": "acme-corp", + "description": "Global technology solutions provider" + }, + "create": { + "tenant": { + "name": "Acme Corporation", + "slug": "acme-corp", + "group": { + "name": "Enterprise Customers", + "slug": "enterprise-customers" + }, + "description": "Global technology solutions provider", + "comments": "Fortune 500 company with worldwide operations", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + + } + }, + "update": { + "tenant": { + "name": "Acme Corporation", + "slug": "acme-corp", + "group": { + "name": "Enterprise Customers", + "slug": "enterprise-customers" + }, + "description": "Global technology solutions provider Updated", + "comments": "Fortune 500 company with worldwide operations", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Global technology solutions provider Updated" + } + }, + { + "name": "tenancy_tenantgroup_1", + "object_type": "tenancy.tenantgroup", + "lookup": {"name": "Financial Services"}, + "create_expect": { + "name": "Financial Services", + "description": "Banking and financial industry customers" + }, + "create": { + "tenant_group": { + "name": "Financial Services", + "slug": "financial-services", + "parent": { + "name": "Enterprise Sectors", + "slug": "enterprise-sectors" + }, + "description": "Banking and financial industry customers", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "tenant_group": { + "name": "Financial Services", + "slug": "financial-services", + "parent": { + "name": "Enterprise Sectors", + "slug": "enterprise-sectors" + }, + "description": "Banking and financial industry customers Updated", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Banking and financial industry customers Updated" + } + }, + { + "name": "vpn_tunnel_1", + "object_type": "vpn.tunnel", + "lookup": {"name": "DC-West-to-East-Primary"}, + "create_expect": { + "name": "DC-West-to-East-Primary", + "status": "active" + }, + "create": { + "tunnel": { + "name": "DC-West-to-East-Primary", + "status": "active", + "group": { + "name": "Inter-DC Tunnels", + "slug": "inter-dc-tunnels" + }, + "encapsulation": "ipsec-tunnel", + "ipsec_profile": { + "name": "IPSEC-PROFILE-1", + "mode": "esp", + "ike_policy": { + "name": "IKE-POLICY-TUN-1", + "version": "2", + "preshared_key": "1234567890", + "comments": "Using AES-256-GCM encryption with PFS" + }, + "ipsec_policy": { + "name": "IPSEC-POLICY-1", + "pfs_group": "2" + } + }, + "tenant": {"name": "Tenant 1"}, + "tunnel_id": "1001", + "description": "Primary IPSec tunnel between West and East data centers", + "comments": "Using AES-256-GCM encryption with PFS", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "tunnel": { + "name": "DC-West-to-East-Primary", + "status": "active", + "group": { + "name": "Inter-DC Tunnels", + "slug": "inter-dc-tunnels" + }, + "encapsulation": "ipsec-tunnel", + "ipsec_profile": { + "name": "IPSEC-PROFILE-1", + "mode": "esp", + "ike_policy": { + "name": "IKE-POLICY-TUN-1", + "version": "2", + "preshared_key": "1234567890", + "comments": "Using AES-256-GCM encryption with PFS" + }, + "ipsec_policy": { + "name": "IPSEC-POLICY-1", + "pfs_group": "2" + } + }, + "tenant": {"name": "Tenant 1"}, + "tunnel_id": "1001", + "description": "Primary IPSec tunnel between West and East data centers Updated", + "comments": "Using AES-256-GCM encryption with PFS", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Primary IPSec tunnel between West and East data centers Updated" + } + }, + { + "name": "vpn_tunnel_group_1", + "object_type": "vpn.tunnelgroup", + "lookup": {"name": "Regional Backbones"}, + "create_expect": { + "name": "Regional Backbones", + "description": "High-capacity encrypted tunnels between regional data centers" + }, + "create": { + "tunnel_group": { + "name": "Regional Backbones", + "slug": "regional-backbones", + "description": "High-capacity encrypted tunnels between regional data centers", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "tunnel_group": { + "name": "Regional Backbones", + "slug": "regional-backbones", + "description": "High-capacity encrypted tunnels between regional data centers Updated", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "High-capacity encrypted tunnels between regional data centers Updated" + } + }, + { + "name": "vpn_tunneltermination_1", + "object_type": "vpn.tunneltermination", + "lookup": {"tunnel__name": "DC-West-to-East-Primary"}, + "create_expect": { + "tunnel.name": "DC-West-to-East-Primary", + "role": "hub" + }, + "create": { + "tunnel_termination": { + "tunnel": { + "name": "DC-West-to-East-Primary", + "status": "active", + "encapsulation": "ipsec-tunnel" + }, + "role": "hub", + "termination_device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "outside_ip": { + "address": "203.0.113.1/24", + "status": "active", + "dns_name": "vpn1.example.com" + }, + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "tunnel_termination": { + "tunnel": { + "name": "DC-West-to-East-Primary", + "status": "active", + "encapsulation": "ipsec-tunnel" + }, + "role": "hub", + "termination_device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "outside_ip": { + "address": "203.0.113.1/24", + "status": "active", + "dns_name": "vpn1.example.com" + }, + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}, {"name": "Tag 3"}] + } + }, + "update_expect": { + "tags.all.__by_name": ["Tag 1", "Tag 2", "Tag 3"] + } + }, + { + "name": "ipam_vlan_1", + "object_type": "ipam.vlan", + "lookup": {"vid": 807}, + "create_expect": { + "vid": 807, + "name": "Production Servers" + }, + "create": { + "vlan": { + "group": { + "name": "Production VLANs", + "slug": "production-vlans" + }, + "vid": "807", + "name": "Production Servers", + "tenant": {"name": "Tenant 1"}, + "status": "active", + "role": { + "name": "Production", + "slug": "production" + }, + "description": "Primary production server network", + "qinq_role": "cvlan", + "qinq_svlan": { + "vid": "1909", + "name": "Service Provider VLAN" + }, + "comments": "Used for customer-facing production workloads", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "vlan": { + "group": { + "name": "Production VLANs", + "slug": "production-vlans" + }, + "vid": "807", + "name": "Production Servers", + "tenant": {"name": "Tenant 1"}, + "status": "active", + "role": { + "name": "Production", + "slug": "production" + }, + "description": "Primary production server network Updated", + "qinq_role": "cvlan", + "qinq_svlan": { + "vid": "1909", + "name": "Service Provider VLAN" + }, + "comments": "Used for customer-facing production workloads", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Primary production server network Updated" + } + }, + { + "name": "ipam_vlan_group_1", + "object_type": "ipam.vlangroup", + "lookup": {"name": "Data Center Core"}, + "create_expect": { + "name": "Data Center Core", + "slug": "dc-core", + "description": "Core network VLANs for data center infrastructure" + }, + "create": { + "vlan_group": { + "name": "Data Center Core", + "slug": "dc-core", + "scope_site": { + "name": "Data Center West", + "slug": "dc-west", + "status": "active" + }, + "description": "Core network VLANs for data center infrastructure", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "vlan_group": { + "name": "Data Center Core", + "slug": "dc-core", + "scope_site": { + "name": "Data Center West", + "slug": "dc-west", + "status": "active" + }, + "description": "Core network VLANs for data center infrastructure Updated", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Core network VLANs for data center infrastructure Updated" + } + }, + { + "name": "ipam_vlan_translation_policy_1", + "object_type": "ipam.vlantranslationpolicy", + "lookup": {"name": "Customer Edge Translation"}, + "create_expect": { + "name": "Customer Edge Translation", + "description": "VLAN translation policy for customer edge interfaces" + }, + "create": { + "vlan_translation_policy": { + "name": "Customer Edge Translation", + "description": "VLAN translation policy for customer edge interfaces" + } + }, + "update": { + "vlan_translation_policy": { + "name": "Customer Edge Translation", + "description": "VLAN translation policy for customer edge interfaces Updated" + } + }, + "update_expect": { + "description": "VLAN translation policy for customer edge interfaces Updated" + } + }, + { + "name": "ipam_vlan_translation_rule_1", + "object_type": "ipam.vlantranslationrule", + "lookup": {"policy__name": "Customer Edge Translation", "local_vid": "100"}, + "create_expect": { + "policy.name": "Customer Edge Translation", + "local_vid": 100, + "remote_vid": 1100, + "description": "Map customer VLAN 100 to provider VLAN 1100" + }, + "create": { + "vlan_translation_rule": { + "policy": { + "name": "Customer Edge Translation", + "description": "VLAN translation policy for customer edge interfaces" + }, + "local_vid": "100", + "remote_vid": "1100", + "description": "Map customer VLAN 100 to provider VLAN 1100" + } + }, + "update": { + "vlan_translation_rule": { + "policy": { + "name": "Customer Edge Translation", + "description": "VLAN translation policy for customer edge interfaces" + }, + "local_vid": "100", + "remote_vid": "1100", + "description": "Map customer VLAN 100 to provider VLAN 1100 Updated" + } + }, + "update_expect": { + "description": "Map customer VLAN 100 to provider VLAN 1100 Updated" + } + }, + { + "name": "virtualization_vminterface_1", + "object_type": "virtualization.vminterface", + "lookup": {"name": "eth0"}, + "create_expect": { + "name": "eth0", + "description": "Primary network interface" + }, + "create": { + "vm_interface": { + "virtual_machine": { + "name": "web-server-01", + "status": "active", + "role": {"name": "Web Server"}, + "site": {"name": "Site 1"} + }, + "name": "eth0", + "enabled": true, + "parent": { + "virtual_machine": { + "name": "web-server-01", + "status": "active", + "role": {"name": "Web Server"}, + "site": {"name": "Site 1"} + }, + "name": "bond0" + }, + "bridge": { + "virtual_machine": { + "name": "web-server-01", + "status": "active", + "role": {"name": "Web Server"}, + "site": {"name": "Site 1"} + }, + "name": "br0" + }, + "mtu": "9000", + "primary_mac_address": { + "mac_address": "00:1A:2B:3C:4D:5E" + }, + "description": "Primary network interface", + "mode": "q-in-q", + "untagged_vlan": { + "vid": "1101", + "name": "Production Servers" + }, + "qinq_svlan": { + "vid": "1000", + "name": "Service Provider VLAN" + }, + "vlan_translation_policy": { + "name": "Customer Edge Translation" + }, + "vrf": { + "name": "PROD-VRF", + "rd": "65000:1" + }, + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "vm_interface": { + "virtual_machine": { + "name": "web-server-01", + "status": "active", + "role": {"name": "Web Server"}, + "site": {"name": "Site 1"} + }, + "name": "eth0", + "enabled": true, + "parent": { + "virtual_machine": { + "name": "web-server-01", + "status": "active", + "role": {"name": "Web Server"}, + "site": {"name": "Site 1"} + }, + "name": "bond0" + }, + "bridge": { + "virtual_machine": { + "name": "web-server-01", + "status": "active", + "role": {"name": "Web Server"}, + "site": {"name": "Site 1"} + }, + "name": "br0" + }, + "mtu": "9000", + "primary_mac_address": { + "mac_address": "00:1A:2B:3C:4D:5E" + }, + "description": "Primary network interface Updated", + "mode": "q-in-q", + "untagged_vlan": { + "vid": "1101", + "name": "Production Servers" + }, + "qinq_svlan": { + "vid": "1000", + "name": "Service Provider VLAN" + }, + "vlan_translation_policy": { + "name": "Customer Edge Translation" + }, + "vrf": { + "name": "PROD-VRF", + "rd": "65000:1" + }, + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Primary network interface Updated" + } + }, + { + "name": "ipam_vrf_1", + "object_type": "ipam.vrf", + "lookup": {"name": "Customer-A-VRF"}, + "create_expect": { + "name": "Customer-A-VRF", + "rd": "65000:100", + "tenant.name": "Tenant 1", + "enforce_unique": true, + "description": "Isolated routing domain for Customer A", + "comments": "Used for customer's private network services" + }, + "create": { + "vrf": { + "name": "Customer-A-VRF", + "rd": "65000:100", + "tenant": {"name": "Tenant 1"}, + "enforce_unique": true, + "description": "Isolated routing domain for Customer A", + "comments": "Used for customer's private network services", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}], + "import_targets": [ + { + "name": "65000:100" + }, + { + "name": "65000:101" + } + ], + "export_targets": [ + { + "name": "65000:103" + } + ] + } + }, + "update": { + "vrf": { + "name": "Customer-A-VRF", + "rd": "65000:100", + "tenant": {"name": "Tenant 1"}, + "enforce_unique": true, + "description": "Isolated routing domain for Customer A Updated", + "comments": "Used for customer's private network services", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}], + "import_targets": [ + { + "name": "65000:100" + }, + { + "name": "65000:101" + } + ], + "export_targets": [ + { + "name": "65000:103" + } + ] + } + }, + "update_expect": { + "description": "Isolated routing domain for Customer A Updated" + } + }, + { + "name": "dcim_virtualchassis_1", + "object_type": "dcim.virtualchassis", + "lookup": {"name": "Stack-DC1-Core"}, + "create_expect": { + "name": "Stack-DC1-Core", + "domain": "dc1-core.example.com" + }, + "create": { + "virtual_chassis": { + "name": "Stack-DC1-Core", + "domain": "dc1-core.example.com", + "master": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "description": "Core switch stack in DC1", + "comments": "Primary switching infrastructure for data center 1", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "virtual_chassis": { + "name": "Stack-DC1-Core", + "domain": "dc1-core.example.com", + "master": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "description": "Core switch stack in DC1 Updated", + "comments": "Primary switching infrastructure for data center 1", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Core switch stack in DC1 Updated" + } + }, + { + "name": "circuits_virtualcircuit_1", + "object_type": "circuits.virtualcircuit", + "lookup": {"cid": "VC-001-LAX-NYC"}, + "create_expect": { + "cid": "VC-001-LAX-NYC" + }, + "create": { + "virtual_circuit": { + "cid": "VC-001-LAX-NYC", + "provider_network": { + "provider": {"name": "Level 3 Communications"}, + "name": "Global MPLS Network", + "service_id": "L3-MPLS-001" + }, + "provider_account": { + "provider": {"name": "Level 3 Communications"}, + "name": "East Coast Account", + "account": "L3-12345" + }, + "type": { + "name": "MPLS L3VPN", + "slug": "mpls-l3vpn" + }, + "status": "active", + "tenant": {"name": "Tenant 1"}, + "description": "LAX to NYC MPLS circuit", + "comments": "Primary east-west connectivity", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "virtual_circuit": { + "cid": "VC-001-LAX-NYC", + "provider_network": { + "provider": {"name": "Level 3 Communications"}, + "name": "Global MPLS Network", + "service_id": "L3-MPLS-001" + }, + "provider_account": { + "provider": {"name": "Level 3 Communications"}, + "name": "East Coast Account", + "account": "L3-12345" + }, + "type": { + "name": "MPLS L3VPN", + "slug": "mpls-l3vpn" + }, + "status": "active", + "tenant": {"name": "Tenant 1"}, + "description": "LAX to NYC MPLS circuit Updated", + "comments": "Primary east-west connectivity", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "LAX to NYC MPLS circuit Updated" + } + }, + { + "name": "circuits_virtualcircuittermination_1", + "object_type": "circuits.virtualcircuittermination", + "lookup": {"virtual_circuit__cid": "VC-001-LAX-NYC"}, + "create_expect": { + "virtual_circuit.cid": "VC-001-LAX-NYC", + "role": "hub", + "interface.device.name": "Device 1" + }, + "create": { + "virtual_circuit_termination": { + "virtual_circuit": { + "cid": "VC-001-LAX-NYC", + "provider_network": { + "provider": {"name": "Level 3 Communications"}, + "name": "Global MPLS Network" + }, + "type": { + "name": "MPLS L3VPN", + "slug": "mpls-l3vpn" + } + }, + "role": "hub", + "interface": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "name": "MegabitEthernet1/0/1", + "type": "virtual", + "enabled": true + }, + "description": "LAX hub termination for east-west MPLS circuit", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "virtual_circuit_termination": { + "virtual_circuit": { + "cid": "VC-001-LAX-NYC", + "provider_network": { + "provider": {"name": "Level 3 Communications"}, + "name": "Global MPLS Network" + }, + "type": { + "name": "MPLS L3VPN", + "slug": "mpls-l3vpn" + } + }, + "role": "hub", + "interface": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "name": "MegabitEthernet1/0/1", + "type": "virtual", + "enabled": true + }, + "description": "LAX hub termination for east-west MPLS circuit Updated", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "LAX hub termination for east-west MPLS circuit Updated" + } + }, + { + "name": "circuits_virtualcircuittype_1", + "object_type": "circuits.virtualcircuittype", + "lookup": {"name": "EVPN-VXLAN"}, + "create_expect": { + "name": "EVPN-VXLAN", + "description": "Data center interconnect using EVPN-VXLAN overlay" + }, + "create": { + "virtual_circuit_type": { + "name": "EVPN-VXLAN", + "slug": "evpn-vxlan", + "color": "0000ff", + "description": "Data center interconnect using EVPN-VXLAN overlay", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "virtual_circuit_type": { + "name": "EVPN-VXLAN", + "slug": "evpn-vxlan", + "color": "0000ff", + "description": "Data center interconnect using EVPN-VXLAN overlay Updated", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Data center interconnect using EVPN-VXLAN overlay Updated" + } + }, + { + "name": "dcim_virtualdevicecontext_1", + "object_type": "dcim.virtualdevicecontext", + "lookup": {"name": "VDC-Production"}, + "create_expect": { + "name": "VDC-Production", + "description": "Production virtual device context", + "comments": "Isolated network context for production services", + "identifier": 1, + "device.name": "Device 1", + "primary_ip4.address": "192.168.1.1/32", + "primary_ip6.address": "2001:db8::1/128" + }, + "create": { + "virtual_device_context": { + "name": "VDC-Production", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "identifier": "1", + "tenant": {"name": "Tenant 1"}, + "primary_ip4": { + "address": "192.168.1.1", + "assigned_object_interface": { + "type": "1000base-t", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "name": "eth0" + } + }, + "primary_ip6": { + "address": "2001:db8::1", + "assigned_object_interface": { + "type": "1000base-t", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "name": "eth0" + } + }, + "status": "active", + "description": "Production virtual device context", + "comments": "Isolated network context for production services", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + + }, + "update": { + "virtual_device_context": { + "name": "VDC-Production", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "identifier": "1", + "tenant": {"name": "Tenant 1"}, + "primary_ip4": { + "address": "192.168.1.1", + "assigned_object_interface": { + "type": "1000base-t", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "name": "eth0" + } + }, + "primary_ip6": { + "address": "2001:db8::1", + "assigned_object_interface": { + "type": "1000base-t", + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "name": "eth0" + } + }, + "status": "active", + "description": "Production virtual device context Updated", + "comments": "Isolated network context for production services", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Production virtual device context Updated" + } + }, + { + "name": "virtualization_virtualdisk_1", + "object_type": "virtualization.virtualdisk", + "lookup": {"name": "root-volume"}, + "create_expect": { + "name": "root-volume", + "description": "Primary system disk" + }, + "create": { + "virtual_disk": { + "virtual_machine": { + "name": "web-server-01", + "status": "active", + "role": {"name": "Web Server"}, + "site": {"name": "Site 1"} + }, + "name": "root-volume", + "description": "Primary system disk", + "size": "182400", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "virtual_disk": { + "virtual_machine": { + "name": "web-server-01", + "status": "active", + "role": {"name": "Web Server"}, + "site": {"name": "Site 1"} + }, + "name": "root-volume", + "description": "Primary system disk Updated", + "size": "182400", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Primary system disk Updated" + } + }, + { + "name": "virtualization_virtualmachine_1", + "object_type": "virtualization.virtualmachine", + "lookup": {"name": "app-server-01"}, + "create_expect": { + "name": "app-server-01", + "description": "Primary application server instance", + "comments": "Hosts critical business applications" + }, + "create": { + "virtual_machine": { + "name": "app-server-01", + "status": "active", + "site": {"name": "Site 1"}, + "cluster": { + "name": "Cluster 1", + "type": {"name": "Cluster Type 1"}, + "scope_site": {"name": "Site 1"} + }, + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"}, + "cluster": { + "name": "Cluster 1", + "type": {"name": "Cluster Type 1"}, + "scope_site": {"name": "Site 1"} + } + }, + "serial": "VM-2023-001", + "role": {"name": "Application Server"}, + "tenant": {"name": "Tenant 1"}, + "platform": {"name": "Ubuntu 22.04"}, + "primary_ip4": { + "address": "192.168.2.99", + "assigned_object_vm_interface": { + "virtual_machine": { + "name": "app-server-01", + "cluster": { + "name": "Cluster 1", + "type": {"name": "Cluster Type 1"}, + "scope_site": {"name": "Site 1"} + }, + "tenant": {"name": "Tenant 1"} + }, + "name": "eth0", + "enabled": true, + "mtu": "1500" + } + }, + "primary_ip6": { + "address": "2001:db8::99", + "assigned_object_vm_interface": { + "virtual_machine": { + "name": "app-server-01", + "cluster": { + "name": "Cluster 1", + "type": {"name": "Cluster Type 1"}, + "scope_site": {"name": "Site 1"} + }, + "tenant": {"name": "Tenant 1"} + }, + "name": "eth0", + "enabled": true, + "mtu": "1500" + } + }, + "vcpus": 4.0, + "memory": "214748364", + "disk": "147483647", + "description": "Primary application server instance", + "comments": "Hosts critical business applications", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "virtual_machine": { + "name": "app-server-01", + "status": "active", + "site": {"name": "Site 1"}, + "cluster": { + "name": "Cluster 1", + "type": {"name": "Cluster Type 1"}, + "scope_site": {"name": "Site 1"} + }, + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"}, + "cluster": { + "name": "Cluster 1", + "type": {"name": "Cluster Type 1"}, + "scope_site": {"name": "Site 1"} + } + }, + "serial": "VM-2023-001", + "role": {"name": "Application Server"}, + "tenant": {"name": "Tenant 1"}, + "platform": {"name": "Ubuntu 22.04"}, + "primary_ip4": { + "address": "192.168.2.99", + "assigned_object_vm_interface": { + "virtual_machine": { + "name": "app-server-01", + "cluster": { + "name": "Cluster 1", + "type": {"name": "Cluster Type 1"}, + "scope_site": {"name": "Site 1"} + }, + "tenant": {"name": "Tenant 1"} + }, + "name": "eth0", + "enabled": true, + "mtu": "1500" + } + }, + "primary_ip6": { + "address": "2001:db8::99", + "assigned_object_vm_interface": { + "virtual_machine": { + "name": "app-server-01", + "cluster": { + "name": "Cluster 1", + "type": {"name": "Cluster Type 1"}, + "scope_site": {"name": "Site 1"} + }, + "tenant": {"name": "Tenant 1"} + }, + "name": "eth0", + "enabled": true, + "mtu": "1500" + } + }, + "vcpus": 4.0, + "memory": "214748364", + "disk": "147483647", + "description": "Primary application server instance Updated", + "comments": "Hosts critical business applications", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Primary application server instance Updated" + } + }, + { + "name": "wireless_wirelesslan_1", + "object_type": "wireless.wirelesslan", + "lookup": {"ssid": "Corp-Secure"}, + "create_expect": { + "ssid": "Corp-Secure", + "group.name": "Corporate Networks", + "description": "Corporate secure wireless network" + }, + "create": { + "wireless_lan": { + "ssid": "Corp-Secure", + "description": "Corporate secure wireless network", + "group": { + "name": "Corporate Networks", + "slug": "corporate-networks" + }, + "status": "active", + "vlan": { + "vid": 100, + "name": "Production Servers" + }, + "scope_site": {"name": "Site 1"}, + "tenant": {"name": "Tenant 1"}, + "auth_type": "wpa-enterprise", + "auth_cipher": "aes", + "auth_psk": "SecureWiFiKey123!", + "comments": "Primary corporate wireless network with 802.1X authentication", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "wireless_lan": { + "ssid": "Corp-Secure", + "description": "Corporate secure wireless network Updated", + "group": { + "name": "Corporate Networks", + "slug": "corporate-networks" + }, + "status": "active", + "vlan": { + "vid": 100, + "name": "Production Servers" + }, + "scope_site": {"name": "Site 1"}, + "tenant": {"name": "Tenant 1"}, + "auth_type": "wpa-enterprise", + "auth_cipher": "aes", + "auth_psk": "SecureWiFiKey123!", + "comments": "Primary corporate wireless network with 802.1X authentication", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Corporate secure wireless network Updated" + } + }, + { + "name": "wireless_wirelesslangroup_1", + "object_type": "wireless.wirelesslangroup", + "lookup": {"name": "Corporate Networks"}, + "create_expect": { + "name": "Corporate Networks", + "parent.name": "All Networks", + "description": "Enterprise corporate wireless networks" + }, + "create": { + "wireless_lan_group": { + "name": "Corporate Networks", + "slug": "corporate-networks", + "parent": { + "name": "All Networks", + "slug": "all-networks" + }, + "description": "Enterprise corporate wireless networks", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "wireless_lan_group": { + "name": "Corporate Networks", + "slug": "corporate-networks", + "parent": { + "name": "All Networks", + "slug": "all-networks" + }, + "description": "Enterprise corporate wireless networks Updated", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Enterprise corporate wireless networks Updated" + } + }, + { + "name": "wireless_wirelesslink_1", + "object_type": "wireless.wirelesslink", + "lookup": {"ssid": "P2P-Link-1"}, + "create_expect": { + "interface_a.device.name": "Device 1", + "interface_b.device.name": "Device 2", + "description": "Point-to-point wireless backhaul link" + }, + "create": { + "wireless_link": { + "interface_a": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "name": "Radio0/1", + "type": "ieee802.11ac", + "enabled": true + }, + "interface_b": { + "device": { + "name": "Device 2", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "name": "Radio0/1", + "type": "ieee802.11ac", + "enabled": true + }, + "ssid": "P2P-Link-1", + "status": "connected", + "tenant": {"name": "Tenant 1"}, + "auth_type": "wpa-personal", + "auth_cipher": "aes", + "auth_psk": "P2PLinkKey123!", + "distance": 1.5, + "distance_unit": "km", + "description": "Point-to-point wireless backhaul link", + "comments": "Building A to Building B wireless bridge", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update": { + "wireless_link": { + "interface_a": { + "device": { + "name": "Device 1", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "name": "Radio0/1", + "type": "ieee802.11ac", + "enabled": true + }, + "interface_b": { + "device": { + "name": "Device 2", + "role": {"name": "Device Role 1"}, + "device_type": { + "manufacturer": {"name": "Cisco"}, + "model": "C2960S" + }, + "site": {"name": "Site 1"} + }, + "name": "Radio0/1", + "type": "ieee802.11ac", + "enabled": true + }, + "ssid": "P2P-Link-1", + "status": "connected", + "tenant": {"name": "Tenant 1"}, + "auth_type": "wpa-personal", + "auth_cipher": "aes", + "auth_psk": "P2PLinkKey123!", + "distance": 1.5, + "distance_unit": "km", + "description": "Point-to-point wireless backhaul link Updated", + "comments": "Building A to Building B wireless bridge", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}] + } + }, + "update_expect": { + "description": "Point-to-point wireless backhaul link Updated" + } + } +] \ No newline at end of file