Skip to content

Implement Openconfig NTP provider - #517

Open
adamtrizuljak-sap wants to merge 6 commits into
mainfrom
feat/openconfig-ntp
Open

Implement Openconfig NTP provider#517
adamtrizuljak-sap wants to merge 6 commits into
mainfrom
feat/openconfig-ntp

Conversation

@adamtrizuljak-sap

@adamtrizuljak-sap adamtrizuljak-sap commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Work in progress

  • Add OpenConfig provider implementation for the NTP resource (internal/provider/openconfig/ntp.go) and a gnmi testdata file (test/gnmi/testdata/openconfig/ntp.txt).
  • Add optional field NTPSpec.SourceAddress and validation logic (see below)
  • Add a simple Containerlab setup with one SRLinux node + the associated network-operator Device and NTP resources
  • Lint required renaming SrcIpItems, SrcIp to SrcIPItems,SrcIP in internal/provider/cisco/nxos/ntp.go
  • Applying examples/openconfig-containerlab/kubernetes/01-devices/v1alpha1-leaf1-clab-ntp.yaml produces the following config in SRLinux:
--{ + running }--[  ]--
A:admin@srl# info system ntp | as json
{
  "admin-state": "enable",
  "server": [
    {
      "address": "de.pool.ntp.org",
      "prefer": true,
      "network-instance": "mgmt",
      "source-address": "192.168.65.254"
    },
    {
      "address": "pool.ntp.org",
      "network-instance": "mgmt",
      "source-address": "192.168.65.254"
    }
  ]
}

Cisco vs Openconfig compatibility issue (solved)

NTPSpec defines the SourceInterfaceName field https://github.com/ironcore-dev/network-operator/blob/main/api/core/v1alpha1/ntp_types.go#L36. Openconfig has something similar source-address https://openconfig.net/projects/models/schemadocs/yangdoc/openconfig-system.html#system-ntp-servers-server-config-source-address

A way to handle it would be to look up the interface given by SourceInterfaceName , get its IPv4 address and put it into source-address. Does it make sense? Do you usually do such operations, or do you keep the provider code clean from reading any additional k8s resources?

Other way would be to just ignore SourceInterfaceName and not pass it into the Openconfig struct, or even throw a apistatus.FieldViolation saying that it's unsupported

Solution

Discussed with @nikatza and @rgildein

Since both fields are optional, the change will be backwards-compatible with the previous NTPSpec and the generated custom resources.

@hardikdr hardikdr added the area/switch-automation Automation processes for network switch management and operations. label Aug 22, 2026
@hardikdr hardikdr added this to Roadmap Aug 22, 2026
Signed-off-by: Adam Trizuljak <adam.trizuljak@sap.com>
Add optional field NTPSpec.SourceAddress
Make SourceInterfaceName also optional
Don't support SourceInterfaceName in Openconfig provider
In NXOS provider ensure that at least one of these fields is defined

Signed-off-by: Adam Trizuljak <adam.trizuljak@sap.com>
Signed-off-by: Adam Trizuljak <adam.trizuljak@sap.com>
Signed-off-by: Adam Trizuljak <adam.trizuljak@sap.com>
Signed-off-by: Adam Trizuljak <adam.trizuljak@sap.com>
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown

Merging this branch will decrease overall coverage

Impacted Packages Coverage Δ 🤖
github.com/ironcore-dev/network-operator/api/core/v1alpha1 2.66% (ø)
github.com/ironcore-dev/network-operator/internal/provider/cisco/nxos 9.82% (-0.02%) 👎
github.com/ironcore-dev/network-operator/internal/provider/openconfig 7.69% (-0.28%) 👎

Coverage by file

Changed files (no unit tests)

Changed File Coverage Δ Total Covered Missed 🤖
github.com/ironcore-dev/network-operator/api/core/v1alpha1/ntp_types.go 12.50% (ø) 8 1 7
github.com/ironcore-dev/network-operator/internal/provider/cisco/nxos/ntp.go 50.00% (ø) 4 2 2
github.com/ironcore-dev/network-operator/internal/provider/cisco/nxos/provider.go 0.37% (-0.00%) 2167 (+6) 8 2159 (+6) 👎
github.com/ironcore-dev/network-operator/internal/provider/openconfig/ntp.go 0.00% (ø) 21 (+21) 0 21 (+21)

Please note that the "Total", "Covered", and "Missed" counts above refer to code statements instead of lines of code. The value in brackets refers to the test coverage of that file in the old version of the code.

Signed-off-by: Adam Trizuljak <adam.trizuljak@sap.com>
@adamtrizuljak-sap
adamtrizuljak-sap marked this pull request as ready for review September 3, 2026 14:21
@@ -0,0 +1,43 @@
# Banner PreLogin

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Banner PreLogin
# NTP

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need all these files in examples/openconfig-containerlab?

If you just use the following containerlab topology:

name: srlinux
topology:
  nodes:
    srl:
      kind: nokia_srlinux
      image: ghcr.io/nokia/srlinux:26.3.1
      startup-config: |-
        system grpc-server mgmt yang-models native
        system aaa authentication admin-user password admin
      ports:
        - 8022:22
        - 9339:57400

The default device sample in https://github.com/ironcore-dev/network-operator/blob/main/config/samples/v1alpha1_device.yaml connecting to host.docker.internal on port 9339 with admin:admin will work out of the box. This is already setup and integrated with the Tiltfile, so everything is already there.

Prefer bool `json:"prefer,omitempty"`

// The name of the vrf used to communicate with the NTP server.
// Maps to NetworkInstance in Openconfig. If empty, will be set to `mgmt`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We try to avoid such vendor specifics in our core apis, as these are meant to be vendor-agnostic and work across all vendors.

Also note that the management vrf is called differently on different platforms, so it might not always be called "mgmt".

// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=63
SourceInterfaceName string `json:"sourceInterfaceName"`
SourceAddress string `json:"sourceAddress,omitempty"`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we just look up the source address by using the source interface name inside of the provider implementation and then use it as source address without the need to alter the API?


// DNS represents the OpenConfig /system/ntp container.
type NTP struct {
Config *NTPConfig `json:"config,omitempty"`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Config *NTPConfig `json:"config,omitempty"`
Config *NTPConfig `json:"config"`

If a field is always set/present, we shouldn't have a "omitempty" tag on it. Applies also to the other types in this file. See

network-operator/AGENTS.md

Lines 148 to 164 in 7603471

**The `omitempty`/`omitzero` trap:**
Using `omitempty` or `omitzero` JSON struct tags on gNMI payload structs is dangerous for the same reason. An omitted field means "don't set this" in the gNMI payload, which is correct on first write. But on subsequent reconciles:
1. Device returns platform defaults for those fields in the Get response
2. The desired struct has those fields empty/zero (omitted from serialization)
3. Diff compares device state (with defaults) against desired (without) → mismatch
4. Operator performs a Set on every reconcile — breaking idempotency
**`omitempty` guidelines:**
1. **Safe:** The field's Go zero value matches the platform default or "absent" state. Omitting it from the payload is semantically equivalent to the device's default.
2. **Safe:** The field is a pointer or slice representing "not configured" (nil) vs "configured" (non-nil). Mutually exclusive choices (e.g. `accept`/`drop`) fall into this category.
3. **Dangerous:** The platform default is non-zero (e.g. `admin-state` defaults to `"enable"`, `port` defaults to `49`). Omitting the Go zero value would either misrepresent intent or cause a false diff on subsequent GET responses.
4. **Unnecessary:** The field is unconditionally set to a non-zero value by the provider code. The tag never triggers, but removing it documents intent — the field is always present.
**Rule:** After setting configuration once, the provider must produce no gNMI Set calls on subsequent reconciles when no user-facing configuration has changed. Test this explicitly.
for the usage guidelines on "omitempty".


// NTPConfig holds the config container for NTP.
type NTPConfig struct {
Enabled bool `json:"enabled"` // Maps to AdminState

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Enabled bool `json:"enabled"` // Maps to AdminState
Enabled bool `json:"enabled"`

nit: I think the mapping is already clear from the implementation itself, so these comments don't add to much additional value. Would just leave them out.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/switch-automation Automation processes for network switch management and operations. size/L

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

3 participants