Skip to content

yang schema

Thomas Mangin edited this page Jul 25, 2026 · 2 revisions

Pre-Alpha. This page describes behavior that may change.

Every plugin that has its own configuration ships a YANG schema. Ze parses the schema at plugin registration, uses it to drive the CLI prompts, the web UI forms, the REST API, and the MCP tools, and runs the validators on every commit. The plugin gets the parsed config delivered during stage 2 of startup, as a list of JSON-encoded sections.

This page covers how to author a schema for a plugin. The deeper reference material lives in the in-tree YANG schema guide.

Registering a schema

You declare the schema in the Schema field of sdk.Registration, the struct you pass to p.Run().

p := sdk.NewWithConn("my-plugin", conn)

p.Run(ctx, sdk.Registration{
    Schema: &sdk.SchemaDecl{
        Module:    "ze-my-plugin",
        Namespace: "urn:ze:my-plugin",
        YANGText:  myPluginYANG,
        Handlers:  []string{"my-plugin"},
    },
    WantsConfig: []string{"my-plugin"},
})
Field Purpose
Module YANG module name.
Namespace URI unique to this module.
YANGText The full text of the YANG module.
Handlers Config path prefixes this plugin handles.

Internal plugins embed the YANG file at compile time with //go:embed. External plugins can load it from any source: a file, a constant, a network fetch.

import _ "embed"

//go:embed my-plugin.yang
var myPluginYANG string

A minimal schema

module my-plugin {
    namespace "urn:my-company:my-plugin";
    prefix mp;

    container my-plugin {
        leaf enabled {
            type boolean;
            default true;
        }
    }
}

That is enough for Ze to accept a my-plugin { enabled true; } block in a config file, validate it, and deliver the parsed JSON to the plugin in stage 2.

Types you will actually use

YANG type Go type Notes
string string Plain text, optionally with length or pattern constraints.
boolean bool true or false.
uint8, uint16, uint32, uint64 unsigned integers range restricts valid values.
int8, int16, int32, int64 signed integers Same.
enumeration string (enum member name) Fixed set of values.
leafref the referenced type Validated against another YANG leaf in the tree.

Constraints

Ze runs the schema validators on every commit. Use them. Do not defer validation to your handler code when the schema can catch the problem at commit time.

leaf port {
    type uint16 {
        range "1..65535";
    }
}

leaf hold-time {
    type uint16 {
        range "0 | 3..65535";
    }
}

leaf name {
    type string {
        length "1..64";
    }
}

leaf ipv4-address {
    type string {
        pattern '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+';
    }
}

leaf action {
    type enumeration {
        enum allow;
        enum deny;
        enum log;
    }
}

Containers and lists

A container is a single named block with sub-fields. A list is a named list keyed by one or more leaves.

container settings {
    leaf timeout { type uint32; }
    leaf retries { type uint8;  }
}

list endpoint {
    key "name";
    leaf name    { type string;  }
    leaf url     { type string;  }
    leaf enabled { type boolean; default true; }
}

In the config file:

settings {
    timeout 30;
    retries 3;
}

endpoint api {
    url "https://api.example.com";
}
endpoint backup {
    url     "https://backup.example.com";
    enabled false;
}

Mandatory, default, required

YANG has a mandatory true flag for leaves that must be present. Ze also has a custom extension, ze:required, for leaves that must be present after inheritance resolves (the BGP subtree uses it for the remote IP, remote AS, and local AS of every peer). Use mandatory for leaves that must be in the literal config; use ze:required for leaves that must exist somewhere in the inheritance chain.

leaf required-field {
    type string;
    mandatory true;
}

leaf optional-field {
    type uint32;
    default 100;
}

Leafrefs

leafref ties one leaf's value to another leaf in the tree. Ze validates the reference at commit time and rejects configs where the referenced value does not exist.

list peer-group {
    key "name";
    leaf name { type string; }
}

leaf group {
    type leafref {
        path "/bgp/peer-group/name";
    }
}

The five rules worth knowing

  1. Use meaningful namespaces. Include your organisation or project prefix. Collisions are painful.
  2. Write descriptions on every container and leaf. They appear in the CLI help, the web UI tooltips, and ze schema show.
  3. Set sensible defaults. Reduce the amount of config a user has to write to get something working.
  4. Validate in YANG first. Ranges, patterns, enums, lengths, leafrefs. Do not duplicate validation in handler code.
  5. List your handler prefixes. The Handlers field in SchemaDecl tells Ze which config paths your plugin handles. Without it, the plugin does not see the config sections.

A complete example

module acme-monitor {
    namespace "urn:acme:monitor";
    prefix   acme;

    description "ACME endpoint monitoring plugin";

    container acme-monitor {
        description "Monitor configuration";

        leaf endpoint {
            type string;
            mandatory true;
            description "HTTPS endpoint to monitor";
        }

        leaf interval {
            type uint32 {
                range "10..3600";
            }
            default 60;
            description "Check interval in seconds";
        }

        list alert {
            key "name";
            description "Alert destinations";

            leaf name {
                type string { length "1..64"; }
            }

            leaf email {
                type string;
            }
        }
    }
}

And the plugin main:

package main

import (
    "context"
    _ "embed"
    "log"
    "os"
    "os/signal"

    "github.com/ze-software/ze/pkg/plugin/sdk"
)

//go:embed acme-monitor.yang
var acmeMonitorYANG string

func main() {
    p, err := sdk.NewFromEnv("acme-monitor")
    if err != nil {
        log.Fatal(err)
    }

    p.OnConfigure(func(sections []sdk.ConfigSection) error {
        for _, s := range sections {
            log.Printf("config root=%s data=%s", s.Root, s.Data)
        }
        return nil
    })

    ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
    defer cancel()

    err = p.Run(ctx, sdk.Registration{
        Schema: &sdk.SchemaDecl{
            Module:    "acme-monitor",
            Namespace: "urn:acme:monitor",
            YANGText:  acmeMonitorYANG,
            Handlers:  []string{"acme-monitor"},
        },
        WantsConfig: []string{"acme-monitor"},
    })
    if err != nil {
        log.Fatal(err)
    }
}

See also

Adapted from main/docs/plugin-development/schema.md.

Home

About

First Steps

Configuration

Operation

Interfaces

Plugins

Plugin Development

Chaos Testing

Blueprints

Development

Reference

Clone this wiki locally