Skip to content

Commit

Permalink
Updates
Browse files Browse the repository at this point in the history
  • Loading branch information
abates committed Apr 29, 2024
1 parent c2eeb34 commit 406d0eb
Show file tree
Hide file tree
Showing 15 changed files with 165 additions and 133 deletions.
29 changes: 2 additions & 27 deletions examples/custom_design/designs/l3vpn/context/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
from django.core.exceptions import ObjectDoesNotExist
import ipaddress
from functools import lru_cache

from nautobot.dcim.models import Device, Interface
from nautobot.ipam.models import VRF, Prefix
from nautobot.dcim.models import Device
from nautobot.ipam.models import VRF

from nautobot_design_builder.context import Context, context_file

Expand All @@ -19,20 +18,6 @@ class L3VPNContext(Context):
def __hash__(self):
return hash((self.pe.name, self.ce.name, self.customer_name))

@lru_cache
def get_l3vpn_prefix(self, parent_prefix, prefix_length):
tag = self.design_instance_tag
if tag:
existing_prefix = Prefix.objects.filter(tags__in=[tag], prefix_length=30).first()
if existing_prefix:
return str(existing_prefix)

for new_prefix in ipaddress.ip_network(parent_prefix).subnets(new_prefix=prefix_length):
try:
Prefix.objects.get(prefix=str(new_prefix))
except ObjectDoesNotExist:
return new_prefix

def get_customer_id(self, customer_name, l3vpn_asn):
try:
vrf = VRF.objects.get(description=f"VRF for customer {customer_name}")
Expand All @@ -44,16 +29,6 @@ def get_customer_id(self, customer_name, l3vpn_asn):
new_id = int(last_vrf.name.split(":")[-1]) + 1
return str(new_id)

def get_interface_name(self, device):
root_interface_name = "GigabitEthernet"
interfaces = Interface.objects.filter(name__contains=root_interface_name, device=device)
tag = self.design_instance_tag
if tag:
existing_interface = interfaces.filter(tags__in=[tag]).first()
if existing_interface:
return existing_interface.name
return f"{root_interface_name}1/{len(interfaces) + 1}"

def get_ip_address(self, prefix, offset):
net_prefix = ipaddress.ip_network(prefix)
for count, host in enumerate(net_prefix):
Expand Down
27 changes: 19 additions & 8 deletions examples/custom_design/designs/l3vpn/designs/0001_ipam.yaml.j2
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
---

vrfs:
- "!create_or_update:name": "{{ l3vpn_asn }}:{{ get_customer_id(customer_name, l3vpn_asn) }}"
description: "VRF for customer {{ customer_name }}"
"!ref": "my_vrf"

tags:
- "!create_or_update:name": "VRF Prefix"
"slug": "vrf_prefix"
- "!create_or_update:name": "VRF Interface"
"slug": "vrf_interface"

prefixes:
- "!create_or_update:prefix": "{{ l3vpn_prefix }}"
status__name: "Reserved"
- "!create_or_update:prefix": "{{ get_l3vpn_prefix(l3vpn_prefix, l3vpn_prefix_length) }}"
status__name: "Reserved"
vrf: "!ref:my_vrf"

vrfs:
- "!create_or_update:name": "{{ l3vpn_asn }}:{{ get_customer_id(customer_name, l3vpn_asn) }}"
description: "VRF for customer {{ customer_name }}"
prefixes:
- "!next_prefix":
identified_by:
tags__name: "VRF Prefix"
prefix: "{{ l3vpn_prefix }}"
length: 30
status__name: "Reserved"
tags:
- {"!get:name": "VRF Prefix"}
"!ref": "l3vpn_p2p_prefix"
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,22 @@
"mpls_router": true,
}
interfaces:
- "!create_or_update:name": "{{ get_interface_name(device) }}"
- "!next_interface": {}
status__name: "Planned"
type: "other"
{% if offset == 2 %}
"!connect_cable":
status__name: "Planned"
to:
device__name: "{{ other_device.name }}"
name: "{{ get_interface_name(other_device) }}"
to: "!ref:other_interface"
{% else %}
"!ref": "other_interface"
{% endif %}
tags:
- {"!get:name": "VRF Interface"}
ip_addresses:
- "!create_or_update:address": "{{ get_ip_address(get_l3vpn_prefix(l3vpn_prefix, l3vpn_prefix_length), offset) }}"
- "!child_prefix:address":
parent: "!ref:l3vpn_p2p_prefix"
offset: "0.0.0.{{ offset }}/30"
status__name: "Reserved"

{% endmacro %}
Expand Down
22 changes: 21 additions & 1 deletion examples/custom_design/designs/l3vpn/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,26 @@
from nautobot.extras.jobs import ObjectVar, StringVar

from nautobot_design_builder.design_job import DesignJob
from nautobot_design_builder.design import ModelInstance
from nautobot_design_builder.ext import AttributeExtension
from nautobot_design_builder.contrib import ext

from .context import L3VPNContext


class NextInterfaceExtension(AttributeExtension):
tag = "next_interface"

def attribute(self, *args, value: ext.Any, model_instance: ModelInstance) -> None:
root_interface_name = "GigabitEthernet"
interfaces = model_instance.relationship_manager.filter(name__startswith="GigabitEthernet")
existing_interface = interfaces.filter(tags__name="VRF Interface").first()
if existing_interface:
model_instance.instance = existing_interface
return {"!create_or_update:name": existing_interface.name}
return {"!create_or_update:name": f"{root_interface_name}1/{len(interfaces) + 1}"}


class L3vpnDesign(DesignJob):
"""Create a l3vpn connection."""

Expand Down Expand Up @@ -38,7 +53,12 @@ class Meta:
"designs/0002_devices.yaml.j2",
]
context_class = L3VPNContext
extensions = [ext.CableConnectionExtension]
extensions = [
ext.CableConnectionExtension,
ext.NextPrefixExtension,
NextInterfaceExtension,
ext.ChildPrefixExtension,
]

@staticmethod
def validate_data_logic(data):
Expand Down
11 changes: 1 addition & 10 deletions nautobot_design_builder/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@
from functools import cached_property
from collections import UserList, UserDict, UserString
import inspect
from typing import Any, Union
from typing import Any
import yaml

from jinja2.nativetypes import NativeEnvironment

from nautobot.extras.models import JobResult
from nautobot.extras.models import Tag

from nautobot_design_builder.errors import DesignValidationError
from nautobot_design_builder.jinja2 import new_template_environment
Expand Down Expand Up @@ -371,14 +370,6 @@ def validate(self):
if len(errors) > 0:
raise DesignValidationError("\n".join(errors))

@property
def design_instance_tag(self) -> Union[Tag, None]:
"""Returns the `Tag` of the design instance if exists."""
try:
return Tag.objects.get(name__contains=self._instance_name)
except Tag.DoesNotExist:
return None

@property
def _instance_name(self):
if nautobot_version < "2.0.0":
Expand Down
33 changes: 27 additions & 6 deletions nautobot_design_builder/contrib/ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,12 @@ def lookup(self, queryset, query, parent: ModelInstance = None):
Any: The object matching the query.
"""
query = self.environment.resolve_values(query)
# it's possible an extension actually returned the instance we need, in
# that case, no need to look it up. This is especially true for the
# !ref extension used as a value.
if isinstance(query, ModelInstance):
return query

query = self.flatten_query(query)
try:
model_class = self.environment.model_class_index[queryset.model]
Expand All @@ -143,6 +149,7 @@ def attribute(self, *args, value, model_instance) -> None: # pylint:disable=arg
assign it to an attribute of another object.
Args:
*args: Any additional arguments following the tag name. These are `:` delimited.
value: A filter describing the object to get. Keys should map to lookup
parameters equivalent to Django's `filter()` syntax for the given model.
The special `type` parameter will override the relationship's model class
Expand Down Expand Up @@ -230,10 +237,12 @@ def get_query_managers(endpoint_type):

return query_managers

def attribute(self, value, model_instance) -> None:
def attribute(self, *args, value=None, model_instance: ModelInstance = None) -> None:
"""Connect a cable termination to another cable termination.
Args:
*args: Any additional arguments following the tag name. These are `:` delimited.
value: Dictionary with details about the cable. At a minimum
the dictionary must have a `to` key which includes a query
dictionary that will return exactly one object to be added to the
Expand Down Expand Up @@ -313,10 +322,12 @@ class NextPrefixExtension(AttributeExtension):

tag = "next_prefix"

def attribute(self, value: dict, model_instance) -> None:
def attribute(self, *args, value: dict = None, model_instance: ModelInstance = None) -> None:
"""Provides the `!next_prefix` attribute that will calculate the next available prefix.
Args:
*args: Any additional arguments following the tag name. These are `:` delimited.
value: A filter describing the parent prefix to provision from. If `prefix`
is one of the query keys then the network and prefix length will be
split and used as query arguments for the underlying Prefix object. The
Expand All @@ -339,12 +350,20 @@ def attribute(self, value: dict, model_instance) -> None:
- "10.0.0.0/23"
- "10.0.2.0/23"
length: 24
identified_by:
tag__name: "some tag name"
status__name: "Active"
```
"""
if not isinstance(value, dict):
raise DesignImplementationError("the next_prefix tag requires a dictionary of arguments")

identified_by = value.pop("identified_by", None)
if identified_by:
try:
model_instance.instance = model_instance.relationship_manager.get(**identified_by)
return
except ObjectDoesNotExist:
pass
length = value.pop("length", None)
if length is None:
raise DesignImplementationError("the next_prefix tag requires a prefix length")
Expand Down Expand Up @@ -400,7 +419,7 @@ class ChildPrefixExtension(AttributeExtension):

tag = "child_prefix"

def attribute(self, value: dict, model_instance) -> None:
def attribute(self, attr: str = "prefix", value: dict = {}, model_instance=None) -> None:
"""Provides the `!child_prefix` attribute.
!child_prefix calculates a child prefix using a parent prefix
Expand Down Expand Up @@ -457,7 +476,7 @@ def attribute(self, value: dict, model_instance) -> None:
if not isinstance(offset, str):
raise DesignImplementationError("offset must be string")

return "prefix", network_offset(parent, offset)
return attr, network_offset(parent, offset)


class BGPPeeringExtension(AttributeExtension):
Expand Down Expand Up @@ -486,14 +505,16 @@ def __init__(self, environment: Environment):
"the `bgp_peering` tag can only be used when the bgp models app is installed."
)

def attribute(self, value, model_instance) -> None:
def attribute(self, *args, value=None, model_instance: ModelInstance = None) -> None:
"""This attribute tag creates or updates a BGP peering for two endpoints.
!bgp_peering will take an `endpoint_a` and `endpoint_z` argument to correctly
create or update a BGP peering. Both endpoints can be specified using typical
Design Builder syntax.
Args:
*args: Any additional arguments following the tag name. These are `:` delimited.
value (dict): dictionary containing the keys `endpoint_a`
and `endpoint_z`. Both of these keys must be dictionaries
specifying a way to either lookup or create the appropriate
Expand Down
6 changes: 6 additions & 0 deletions nautobot_design_builder/design.py
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,12 @@ def _send(self, signal: str):
self.metadata.send(signal)

def _load_instance(self): # pylint: disable=too-many-branches
# Short circuit if the instance was loaded earlier in
# the initialization process
if self.instance is not None:
self._initial_state = serialize_object_v2(self.instance)
return

query_filter = self.metadata.query_filter
field_values = self.metadata.query_filter_values
if self.metadata.action == ModelMetadata.GET:
Expand Down
1 change: 0 additions & 1 deletion nautobot_design_builder/design_job.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,6 @@ def _run_in_transaction(self, **kwargs): # pylint: disable=too-many-branches, t
if previous_journal:
deleted_object_ids = previous_journal - journal
if deleted_object_ids:
self.log_debug(f"Deleting {list(deleted_object_ids)}")
journal.design_instance.decommission(*deleted_object_ids, local_logger=self.logger)
self.post_implementation(context, self.environment)

Expand Down
16 changes: 12 additions & 4 deletions nautobot_design_builder/ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,18 @@ class AttributeExtension(Extension, ABC):
"""An `AttributeExtension` will be evaluated when the design key matches the `tag`."""

@abstractmethod
def attribute(self, value: Any, model_instance: "ModelInstance") -> None:
def attribute(self, *args, value: Any = None, model_instance: "ModelInstance" = None) -> None:
"""This method is called when the `attribute_tag` is encountered.
Note: The method signature must match the above for the extension to work. The
extension name is parsed by splitting on `:` symbols and the result is passed as the
varargs. For instance, if the attribute tag is `mytagg` and it is called with `!mytagg:arg1`: {} then
`*args` will be ['arg1'] and `value` will be the empty dictionary.
Args:
*args: Any additional arguments following the tag name. These are `:` delimited.
value (Any): The value of the data structure at this key's point in the design YAML. This could be a scalar, a dict or a list.
model_instance (CreatorObject): Object is the CreatorObject that would ultimately contain the values.
model_instance (ModelInstance): Object is the ModelInstance that would ultimately contain the values.
"""


Expand Down Expand Up @@ -151,10 +157,12 @@ def __init__(self, environment: "Environment"): # noqa: D107
super().__init__(environment)
self._env = {}

def attribute(self, value, model_instance):
def attribute(self, *args, value, model_instance):
"""This method is called when the `!ref` tag is encountered.
Args:
*args: Any additional arguments following the tag name. These are `:` delimited.
value (Any): Value should be a string name (the reference) to refer to the object
model_instance (CreatorObject): The object that will be later referenced
Expand Down Expand Up @@ -243,7 +251,7 @@ def _reset(self):
"directories": [],
}

def attribute(self, value, model_instance):
def attribute(self, *args, value=None, model_instance: "ModelInstance" = None):
"""Provide the attribute tag functionality for git_context.
Args:
Expand Down
Loading

0 comments on commit 406d0eb

Please sign in to comment.