Plugins declare their configuration schema using YANG (RFC 7950). This guide covers the basics for plugin authors.
| Plugin kind | Its own YANG module | Config it receives |
|---|---|---|
Internal plugin, compiled into ze (package added to internal/component/plugin/all) |
Registered at build, parsed and validated; the operator writes config under its roots | Its WantsConfig roots |
| External plugin (a separate process, SDK or any language) | Not yet used. Ze stores the YANGText it declares but never parses it, so the config parser does not know the plugin's root |
Only roots that a compiled-in module already defines (for example bgp) |
A config block under an external plugin's own root is refused when the config
is loaded, before any plugin starts: ze config validate reports
unknown top-level keyword: <root>. Compiling an external plugin's module at
startup, so its own root becomes valid config, is planned
(plan/post-release/features/spec-plugin-external-yang-at-startup.md). The acme-monitor example
at the end of this page shows that planned behavior for an external plugin.
Schemas are declared via the Schema field of sdk.Registration, which is passed to
p.Run(). The Schema field is a pointer to sdk.SchemaDecl.
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, // YANG module text (string)
Handlers: []string{"my-plugin"},
},
WantsConfig: []string{"my-plugin"},
})| Field | Type | Purpose |
|---|---|---|
Module |
string |
YANG module name |
Namespace |
string |
YANG namespace URI |
YANGText |
string |
Full YANG module text |
Handlers |
[]string |
Config path prefixes this plugin handles |
Internal plugins use //go:embed to embed YANG files at compile time:
import _ "embed"
//go:embed my-plugin.yang
var myPluginYANG stringThe embedded string is then passed as YANGText in the SchemaDecl.
For external plugins, the YANG text can come from any source (file read, constant, etc.).
module my-plugin {
namespace "urn:my-company:my-plugin";
prefix mp;
container my-plugin {
leaf enabled {
type boolean;
default true;
}
}
}module name {
namespace "urn:..."; // Unique namespace URI
prefix prefix; // Short prefix for references
// Content: containers, lists, leafs
}| YANG Type | Go Type | Example |
|---|---|---|
string |
string |
"hello" |
boolean |
bool |
true/false |
uint8/16/32/64 |
uintN |
42 |
int8/16/32/64 |
intN |
-42 |
leaf port {
type uint16 {
range "1..65535";
}
}
leaf hold-time {
type uint16 {
range "0 | 3..65535"; // 0 or 3+
}
}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;
}
}container settings {
leaf timeout { type uint32; }
leaf retries { type uint8; }
}Config:
settings {
timeout 30;
retries 3;
}
list endpoint {
key "name";
leaf name { type string; }
leaf url { type string; }
leaf enabled { type boolean; default true; }
}Config:
endpoint api {
url "https://api.example.com";
}
endpoint backup {
url "https://backup.example.com";
enabled false;
}
leaf required-field {
type string;
mandatory true; // Must be provided
}
leaf optional-field {
type uint32;
default 100; // Used if not provided
}Ze checks the three YANG reference types. A leafref value must have the
type of the leaf its path names and, unless the type says
require-instance false, equal an instance of that leaf somewhere in the
config, or its default where the default is in use. An identityref value
must name an identity derived from the base, unprefixed or with the defining
module's prefix. An instance-identifier value is a path with every node
prefixed by its module's prefix, and must name a node the config holds. The
loader refuses a leafref whose path names no leaf, a config leafref pointing
at state, and a loop of leafrefs (ErrLeafrefPath).
// In your module: a reference to a class of the same qdisc
leaf default-class {
type leafref { path "../class/name"; }
}A reference no YANG path can express, such as one into state a plugin holds,
stays a string checked by a ze:validate function.
- Use meaningful namespaces -- include your organization
- Add both help texts -- a
ze:helpone-line summary for the completion row, and adescriptionparagraph for the?box - Set sensible defaults -- reduce required config
- Validate early -- use YANG constraints, not just code
- Use the
Handlersfield -- list the config path prefixes your plugin manages
Planned, not yet working for an external plugin. This plugin runs as an
external process and defines its own root, acme-monitor. Today Ze refuses
an acme-monitor { ... } block in the config (see "What works today" above),
so the plugin never receives a section. The example becomes valid when
plan/post-release/features/spec-plugin-external-yang-at-startup.md lands. The SDK calls it shows
(NewConfigTx, Run, SchemaDecl) work today for roots a compiled-in module
defines.
A complete example showing YANG schema registration with the SDK. The plugin
reads leaves of its root, so it registers a config transaction: Stage 2
installs the parsed config, and a reload verifies, applies and, when the commit
fails, rolls back the same value. A plugin that declares WantsConfig and
registers only OnConfigure refuses every reload that changes its root (see
handlers.md, "A plugin with no apply handler").
package main
import (
"context"
_ "embed"
"encoding/json"
"fmt"
"log"
"os"
"os/signal"
"sync"
"github.com/ze-software/ze/pkg/plugin/sdk"
)
//go:embed acme-monitor.yang
var acmeMonitorYANG string
// monitorConfig is the part of the acme-monitor container the plugin acts on.
type monitorConfig struct {
Endpoint string `json:"endpoint"`
Interval json.Number `json:"interval"`
}
// parseMonitor reads the plugin's config from one delivery. It changes no
// state, because a refused verify and a CLI validate run it too.
func parseMonitor(sections []sdk.ConfigSection) (monitorConfig, bool, error) {
for _, s := range sections {
if s.Root != "acme-monitor" {
continue
}
var data struct {
Monitor monitorConfig `json:"acme-monitor"`
}
if err := json.Unmarshal([]byte(s.Data), &data); err != nil {
return monitorConfig{}, true, fmt.Errorf("acme-monitor: %w", err)
}
return data.Monitor, true, nil
}
return monitorConfig{}, false, nil
}
// monitor holds the running config the poller reads.
type monitor struct {
mu sync.Mutex
cfg monitorConfig
}
// install makes cfg the running config. A failed install must leave the
// running config as it was.
func (m *monitor) install(cfg monitorConfig) error {
m.mu.Lock()
defer m.mu.Unlock()
m.cfg = cfg
log.Printf("monitoring %s every %ss", cfg.Endpoint, cfg.Interval)
return nil
}
func main() {
p, err := sdk.NewFromEnv("acme-monitor")
if err != nil {
log.Fatal(err)
}
m := &monitor{}
sdk.NewConfigTx(monitorConfig{}, parseMonitor, m.install).Register(p)
p.OnEvent(func(event string) error {
log.Printf("event: %s", event)
return nil
})
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
if 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"},
}); err != nil {
log.Fatal(err)
}
}module acme-monitor {
namespace "urn:acme:monitor";
prefix acme;
import ze-extensions { prefix ze; }
description "ACME endpoint monitoring plugin";
container acme-monitor {
ze:help "Monitor configuration.";
description "Every endpoint the plugin polls, and where it sends an alert.";
leaf endpoint {
type string;
mandatory true;
ze:help "HTTPS endpoint to monitor.";
description "The plugin issues one GET to this URL at every interval.";
}
leaf interval {
type uint32 {
range "10..3600";
}
default 60;
ze:help "Check interval in seconds.";
description "The pause between two polls of the endpoint, from 10 seconds to one hour.";
}
list alert {
key "name";
ze:help "Alert destinations.";
description "One entry for each address that receives a failure notice.";
leaf name {
type string {
length "1..64";
}
}
leaf email {
type string;
}
}
}
}