import json import yaml from django import forms from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ from netbox.forms import ( NetBoxModelBulkEditForm, NetBoxModelFilterSetForm, NetBoxModelForm, NetBoxModelImportForm, PrimaryModelForm, ) from tenancy.models import Tenant from utilities.forms.fields import ( DynamicModelChoiceField, DynamicModelMultipleChoiceField, TagFilterField, ) from utilities.forms.rendering import FieldSet from .models import ( Service, ServiceStatusChoices, ServiceTemplate, ServiceTemplateStatusChoices, validate_tree, ) # ── Service Template ───────────────────────────────────────────────── class ServiceTemplateForm(PrimaryModelForm): tree_raw = forms.CharField( label=_("Requirement tree"), required=False, widget=forms.Textarea( attrs={ "rows": 20, "class": "font-monospace", "placeholder": ( '- name: routers\n' ' object_type: dcim.device\n' ' quantity: 2\n' ' selector:\n' ' type: netbox_filter\n' ' filters:\n' ' role: router\n' ' children:\n' ' - name: mgmt-ip\n' ' object_type: ipam.ipaddress\n' ' allocation:\n' ' strategy: first_free\n' ' prefix:\n' ' type: prefix\n' ' filters: {}\n' ), }, ), help_text=_("Enter the requirement tree as YAML or JSON."), ) fieldsets = ( FieldSet("name", "status", "tree_raw", name=_("Service Template")), FieldSet("description", "tags", name=_("Metadata")), ) class Meta: model = ServiceTemplate fields = ("name", "status", "description", "comments") def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if self.instance and self.instance.pk and self.instance.tree: self.fields["tree_raw"].initial = yaml.dump( self.instance.tree, default_flow_style=False, sort_keys=False, ) def clean(self): super().clean() raw = self.cleaned_data.get("tree_raw", "").strip() if not raw: self.instance.tree = [] return self.cleaned_data tree = None try: tree = json.loads(raw) except (json.JSONDecodeError, ValueError): pass if tree is None: try: tree = yaml.safe_load(raw) except yaml.YAMLError as exc: self.add_error("tree_raw", _(f"Invalid YAML or JSON: {exc}")) return self.cleaned_data if tree is None: tree = [] self.instance.tree = tree self.instance._tree_validated_by_form = True try: validate_tree(tree) except ValidationError as exc: self.add_error("tree_raw", ValidationError(exc.messages)) return self.cleaned_data class ServiceTemplateFilterSetForm(NetBoxModelFilterSetForm): model = ServiceTemplate name = forms.CharField(required=False) status = forms.MultipleChoiceField( choices=ServiceTemplateStatusChoices, required=False, ) tag = TagFilterField(model) class ServiceTemplateBulkEditForm(NetBoxModelBulkEditForm): model = ServiceTemplate status = forms.ChoiceField( choices=ServiceTemplateStatusChoices, required=False, ) description = forms.CharField(required=False) nullable_fields = ("description",) class ServiceTemplateImportForm(NetBoxModelImportForm): class Meta: model = ServiceTemplate fields = ("name", "status", "description", "comments", "tags") # ── Service ────────────────────────────────────────────────────────── class ServiceForm(PrimaryModelForm): service_template = DynamicModelChoiceField( queryset=ServiceTemplate.objects.all(), ) tenant = DynamicModelChoiceField( queryset=Tenant.objects.all(), ) fieldsets = ( FieldSet("name", "service_template", "tenant", "status", "description", "tags", name=_("Service")), ) class Meta: model = Service fields = ("name", "service_template", "tenant", "status", "description", "comments") class ServiceFilterSetForm(NetBoxModelFilterSetForm): model = Service name = forms.CharField(required=False) service_template = DynamicModelMultipleChoiceField( queryset=ServiceTemplate.objects.all(), required=False, ) tenant = DynamicModelMultipleChoiceField( queryset=Tenant.objects.all(), required=False, ) status = forms.MultipleChoiceField( choices=ServiceStatusChoices, required=False, ) tag = TagFilterField(model) class ServiceBulkEditForm(NetBoxModelBulkEditForm): model = Service status = forms.ChoiceField( choices=ServiceStatusChoices, required=False, ) description = forms.CharField(required=False) nullable_fields = ("description",) # ── Resolve form ───────────────────────────────────────────────────── class ResolveForm(forms.Form): mode = forms.ChoiceField( choices=( ("dry_run", _("Dry Run")), ("commit", _("Commit")), ), initial="dry_run", widget=forms.RadioSelect, )