Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Tiltfile
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,9 @@ k8s_resource(new_name='mac-entry', objects=['mac-entry:probe'], trigger_mode=TRI
k8s_resource(new_name='route-prefix', objects=['route-prefix:probe'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False, labels=['samples'])
k8s_resource(new_name='vtep-peers', objects=['vtep-peers:probe'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False, labels=['samples'])

k8s_yaml('./examples/openconfig-containerlab/kubernetes/01-devices/v1alpha1-leaf1-clab-ntp.yaml')
k8s_resource(new_name='ntp-clab', objects=['ntp-clab:ntp'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False, labels=['containerlab'])

print('🚀 network-operator development environment')
print('👉 Edit the code inside the api/, cmd/, or internal/ directories')
print('👉 Tilt will automatically rebuild and redeploy when changes are detected')
Expand Down
1 change: 1 addition & 0 deletions api/core/v1alpha1/ntp_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ type NTPServer struct {
Prefer bool `json:"prefer,omitempty"`

// The name of the vrf used to communicate with the NTP server.
// Maps to network-instance in Openconfig
// +optional
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=63
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions config/crd/bases/networking.metal.ironcore.dev_ntp.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docs/api-reference/index.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions examples/openconfig-containerlab/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
clab-nav-srl
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
apiVersion: networking.metal.ironcore.dev/v1alpha1
kind: NTP
metadata:
labels:
app.kubernetes.io/name: network-operator
app.kubernetes.io/managed-by: kustomize
networking.metal.ironcore.dev/device-name: leaf1-clab
name: ntp-clab
spec:
deviceRef:
name: leaf1
sourceInterfaceName: mgmt0
servers:
- address: de.pool.ntp.org
prefer: true
vrfName: mgmt
- address: pool.ntp.org
prefer: false
vrfName: mgmt
14 changes: 14 additions & 0 deletions examples/openconfig-containerlab/topology.clab.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: nav-srl

topology:
nodes:
leaf1:
kind: nokia_srlinux
image: ghcr.io/nokia/srlinux:26.7.1
startup-config: |-
system name host-name srl
system aaa authentication admin-user password admin
system grpc-server mgmt yang-models openconfig
ports:
- 8022:22
- 9339:57400
25 changes: 25 additions & 0 deletions internal/provider/openconfig/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package openconfig
import (
"context"
"encoding/json"
"errors"
"fmt"

"github.com/ironcore-dev/network-operator/api/core/v1alpha1"
Expand Down Expand Up @@ -664,3 +665,27 @@ type SubinterfaceVlanDoubleTaggedConfig struct {
InnerVlanID uint16 `json:"inner-vlan-id,omitempty"`
OuterVlanID uint16 `json:"outer-vlan-id,omitempty"`
}

type interfaceAddrs struct {
ifName string
Address gnmiext.List[string, *IPv4Address] `json:"address"`
}

func (a *interfaceAddrs) XPath() string {
return fmt.Sprintf("openconfig-interfaces:interfaces/interface[name=%s]/subinterfaces/subinterface[index=0]/openconfig-if-ip:ipv4/addresses", a.ifName)
}

// interfaceIPAddr retrieves the first IPv4 address from the state of the named interface.
func (p *Provider) interfaceIPAddr(ctx context.Context, name string) (string, error) {
addrs := &interfaceAddrs{ifName: name}
if err := p.client.GetState(ctx, addrs); err != nil {
if errors.Is(err, gnmiext.ErrNil) {
return "", apistatus.NewFailedPreconditionError(fmt.Sprintf("interface %q has no IPv4 address A", name))
}
return "", fmt.Errorf("failed to get IPv4 address for interface %q: %w", name, err)
}
for _, a := range addrs.Address {
return a.IP, nil
}
return "", apistatus.NewFailedPreconditionError(fmt.Sprintf("interface %q has no IPv4 address B", name))
}
91 changes: 91 additions & 0 deletions internal/provider/openconfig/ntp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and IronCore contributors
// SPDX-License-Identifier: Apache-2.0

package openconfig

import (
"context"

"github.com/ironcore-dev/network-operator/api/core/v1alpha1"
"github.com/ironcore-dev/network-operator/internal/provider"
"github.com/ironcore-dev/network-operator/internal/transport/gnmiext"
)

var _ provider.NTPProvider = (*Provider)(nil)

func (p *Provider) EnsureNTP(ctx context.Context, req *provider.EnsureNTPRequest) error {
spec := req.NTP.Spec

n := &NTP{
Config: &NTPConfig{
Enabled: spec.AdminState == v1alpha1.AdminStateUp,
},
}

var sourceAddress string
if spec.SourceInterfaceName != "" {
var err error
sourceAddress, err = p.interfaceIPAddr(ctx, spec.SourceInterfaceName)
if err != nil {
return err
}
}

if len(spec.Servers) > 0 {
n.Servers = &NTPServers{}
for _, s := range spec.Servers {
n.Servers.Server.Set(&NTPServer{
Address: s.Address,
Config: &NTPServerConfig{
Address: s.Address,
Prefer: s.Prefer,
NetworkInstance: s.VrfName,
SourceAddress: sourceAddress,
},
})
}
}

return p.client.Update(ctx, n)
}

func (p *Provider) DeleteNTP(ctx context.Context) error {
return p.client.Delete(ctx, &NTP{})
}

// Compile-time assertions.
var _ gnmiext.DataElement = (*NTP)(nil)

// DNS represents the OpenConfig /system/ntp container.
type NTP struct {
Config *NTPConfig `json:"config"`
Servers *NTPServers `json:"servers"`
}

func (*NTP) XPath() string { return "openconfig-system:system/ntp" }

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

// NTPServers holds the servers container for NTP.
type NTPServers struct {
Server gnmiext.List[string, *NTPServer] `json:"server"`
}

// NTPServer represents a single NTP server entry.
type NTPServer struct {
Address string `json:"address"`
Config *NTPServerConfig `json:"config"`
}

func (s *NTPServer) Key() string { return s.Address }

// NTPServerConfig holds the config container for a NTP server.
type NTPServerConfig struct {
Address string `json:"address"`
Prefer bool `json:"prefer,omitempty"`
NetworkInstance string `json:"network-instance,omitempty"` // Maps to VrfName
SourceAddress string `json:"source-address,omitempty"`
}
Loading
Loading