modified: netbox_services/engine/__init__.py
modified: netbox_services/engine/allocators.py modified: netbox_services/engine/context.py new file: netbox_services/engine/mappers.py modified: netbox_services/engine/resolver.py new file: netbox_services/engine/templating.py modified: netbox_services/forms.py modified: netbox_services/models.py modified: netbox_services/tests/test_allocators.py new file: netbox_services/tests/test_mappers.py modified: netbox_services/tests/test_resolver.py new file: netbox_services/tests/test_templating.py Simplify layout to template and service instances
This commit is contained in:
1 parent
4ff9eac274
commit
b1c9c7a40b
12 files changed
+1133
-33
No files matched your search
@@ -31,6 +31,127 @@ from .models import (
|
||||
|
||||
# ── Service Template ─────────────────────────────────────────────────
|
||||
|
||||
EXAMPLE_TREE_YAML = """\
|
||||
# Requirement tree — defines what objects a service needs.
|
||||
# Delete this example and write your own. YAML or JSON accepted.
|
||||
#
|
||||
# Node parameters:
|
||||
# name (required) unique identifier within the tree
|
||||
# object_type (required) NetBox model as "app_label.model_name"
|
||||
# quantity (optional, default 1) how many objects to resolve
|
||||
# scope (optional, default "parent")
|
||||
# "parent" = each parent gets its own children;
|
||||
# "service" = children resolved once at service
|
||||
# level (only meaningful when quantity > 1)
|
||||
# selector (mutually exclusive with allocation) find
|
||||
# existing objects
|
||||
# allocation (mutually exclusive with selector) create/acquire
|
||||
# objects
|
||||
# children (optional) list of child requirement nodes
|
||||
#
|
||||
# Selector types:
|
||||
# netbox_filter — filter existing objects via NetBox filter
|
||||
# params
|
||||
# reference — resolve a single object via context path
|
||||
# (parent.site)
|
||||
# prefix — select ipam.Prefix by filter or reference
|
||||
#
|
||||
# Allocation strategies:
|
||||
# create — instantiate a new model from attrs
|
||||
# (returned unsaved, resolver saves after
|
||||
# auto-linking to parent)
|
||||
# first_free — allocate next available IP from a resolved
|
||||
# prefix
|
||||
# specific — use an existing object by address or context
|
||||
# path
|
||||
#
|
||||
# Jinja2 templating in allocation.attrs values:
|
||||
# {{ service.tenant.slug }} — the service's tenant
|
||||
# {{ index + 1 }} — 0-based index within a quantity # loop
|
||||
# {{ ..name }} — immediate parent's name
|
||||
# (.. = parent)
|
||||
# {{ ...site.slug }} — grandparent's site slug
|
||||
# (... = grandparent)
|
||||
# {{ ....region.slug }} — great-grandparent (.... = p3)
|
||||
# FK fields must be PKs (integers), e.g. site: 1 or
|
||||
# site: "{{ ..site.pk }}"
|
||||
#
|
||||
# Auto-linking: children created via create/first_free
|
||||
# are automatically linked to their parent via the central mapper
|
||||
# (engine/mappers.py).
|
||||
# You do NOT specify the link field — the mapper knows.
|
||||
# If a mapping is missing, resolution fails with an error telling
|
||||
# you exactly which register_link() call to add.
|
||||
|
||||
# ── Example 1: Select 2 existing routers, assign each a management IP ──
|
||||
- name: routers
|
||||
object_type: dcim.device
|
||||
quantity: 2
|
||||
selector:
|
||||
type: netbox_filter
|
||||
filters:
|
||||
role: router
|
||||
site: hq
|
||||
scope: service
|
||||
children:
|
||||
- name: mgmt_ip
|
||||
object_type: ipam.ipaddress
|
||||
quantity: 1
|
||||
allocation:
|
||||
strategy: first_free
|
||||
prefix:
|
||||
type: prefix
|
||||
filters:
|
||||
prefix: 10.0.0.0/8
|
||||
scope: parent
|
||||
|
||||
# ── Example 2: Create a new device + interface + IP (all auto-linked) ──
|
||||
- name: edge_router
|
||||
object_type: dcim.device
|
||||
quantity: 1
|
||||
allocation:
|
||||
strategy: create
|
||||
attrs:
|
||||
name: "rtr-{{ service.tenant.slug }}-{{ index + 1 }}"
|
||||
device_type: 1 # PK of a DeviceType
|
||||
role: 1 # PK of a DeviceRole
|
||||
site: "{{ ..site.pk }}" # not valid at top-level (no parent);
|
||||
# use a literal PK here, e.g. site: 1
|
||||
status: active
|
||||
scope: service
|
||||
children:
|
||||
- name: uplink
|
||||
object_type: dcim.interface
|
||||
quantity: 1
|
||||
allocation:
|
||||
strategy: create
|
||||
attrs:
|
||||
name: eth0
|
||||
type: 1000base-t
|
||||
scope: parent
|
||||
children:
|
||||
- name: uplink_ip
|
||||
object_type: ipam.ipaddress
|
||||
quantity: 1
|
||||
allocation:
|
||||
strategy: first_free
|
||||
prefix:
|
||||
type: prefix
|
||||
filters:
|
||||
prefix: 10.0.0.0/8
|
||||
scope: parent
|
||||
|
||||
# ── Example 3: Use a specific existing IP address ──
|
||||
- name: loopback
|
||||
object_type: ipam.ipaddress
|
||||
quantity: 1
|
||||
allocation:
|
||||
strategy: specific
|
||||
address: 10.255.255.1/32
|
||||
scope: service
|
||||
"""
|
||||
|
||||
|
||||
class ServiceTemplateForm(PrimaryModelForm):
|
||||
tree_raw = forms.CharField(
|
||||
label=_("Requirement tree"),
|
||||
@@ -76,6 +197,8 @@ class ServiceTemplateForm(PrimaryModelForm):
|
||||
self.fields["tree_raw"].initial = yaml.dump(
|
||||
self.instance.tree, default_flow_style=False, sort_keys=False,
|
||||
)
|
||||
elif not self.instance.pk:
|
||||
self.fields["tree_raw"].initial = EXAMPLE_TREE_YAML
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
Reference in new issue
Block a user