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
5 changes: 5 additions & 0 deletions api/core/v1alpha1/prefix_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ func (p *IPPrefix) UnmarshalJSON(data []byte) error {
return nil
}

// Is6 reports whether the prefix contains an IPv6 address.
func (p IPPrefix) Is6() bool {
return p.Addr().Is6()
}

// IsPointToPoint reports whether the prefix indicates a point-to-point link.
// For IPv4, this means a /31 subnet mask as defined in [RFC 3021].
// For IPv6, this means a /127 subnet mask as defined in [RFC 6164].
Expand Down
6 changes: 6 additions & 0 deletions api/core/v1alpha1/prefixset_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package v1alpha1

import (
"fmt"
"sync"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -73,6 +74,11 @@ type MaskLengthRange struct {
Max int8 `json:"max"`
}

// String returns the mask length range in OpenConfig dot-dot notation, e.g. "16..24".
func (m MaskLengthRange) String() string {
return fmt.Sprintf("%d..%d", m.Min, m.Max)
}

// PrefixSetStatus defines the observed state of PrefixSet.
type PrefixSetStatus struct {
// EntriesSummary provides a human-readable summary of the number of prefix entries.
Expand Down
223 changes: 223 additions & 0 deletions internal/provider/openconfig/aaa.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
// SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and IronCore contributors
// SPDX-License-Identifier: Apache-2.0

package openconfig

import (
"context"
"encoding/json"
"fmt"

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

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

func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest) error {
spec := req.AAA.Spec

sb := new(gnmiext.SetBuilder)

for _, sg := range spec.ServerGroups {
if sg.Type == v1alpha1.AAAServerGroupTypeRADIUS {
return apistatus.NewUnsupportedFieldError(apistatus.FieldViolation{
Field: "spec.serverGroups[].type",
Description: "openconfig provider does not support RADIUS server groups on SRLinux",
})
}

group := &AAAServerGroup{
Name: sg.Name,
Config: &AAAServerGroupConfig{
Name: sg.Name,
Type: AAAServerGroupTypeTACACS,
},
Servers: &AAAServers{},
}

for _, srv := range sg.Servers {
s := &AAAServer{
Address: srv.Address,
Config: &AAAServerConfig{
Address: srv.Address,
},
}
if srv.Timeout != nil {
s.Config.Timeout = uint16(srv.Timeout.Seconds())
}
if srv.TACACS != nil {
key := req.TACACSServerKeys[srv.Address]
s.TACACS = &AAAServerTACACS{
Config: &AAAServerTACACSConfig{
Port: uint16(srv.TACACS.Port), //nolint:gosec
SecretKey: key,
},
}
}
group.Servers.Server.Set(s)
}
sb.Update(group)
}

if spec.Authentication != nil {
methods := make([]AAAMethodType, 0, len(spec.Authentication.Methods))
for _, m := range spec.Authentication.Methods {
methods = append(methods, toAAAMethod(m))
}
sb.Update(&AAAAuthenticationConfig{Methods: methods})
}

if spec.Accounting != nil {
methods := make([]AAAMethodType, 0, len(spec.Accounting.Methods))
for _, m := range spec.Accounting.Methods {
methods = append(methods, toAAAMethod(m))
}
sb.Update(&AAAAccountingConfig{Methods: methods})
}

// Authorization — not supported on SRLinux OC (empty config options).
if spec.Authorization != nil {
return apistatus.NewUnsupportedFieldError(apistatus.FieldViolation{
Field: "spec.authorization",
Description: "openconfig provider does not support AAA authorization on SRLinux",
})
}

return p.client.Do(ctx, sb)
}

func (p *Provider) DeleteAAA(ctx context.Context, req *provider.DeleteAAARequest) error {
sb := new(gnmiext.SetBuilder)
for _, sg := range req.AAA.Spec.ServerGroups {
sb.Delete(&AAAServerGroup{Name: sg.Name})
}
if len(req.AAA.Spec.ServerGroups) == 0 {
sb.Delete(&AAAContainer{})
}
return p.client.Do(ctx, sb)
}

func toAAAMethod(m v1alpha1.AAAMethod) AAAMethodType {
switch m.Type {
case v1alpha1.AAAMethodTypeLocal:
return AAAMethodTypeLocal
case v1alpha1.AAAMethodTypeNone:
return AAAMethodTypeNone
case v1alpha1.AAAMethodTypeGroup:
return AAAMethodType(m.GroupName)
default:
return AAAMethodTypeLocal
}
}

// AAAServerGroupType represents the OpenConfig AAA server group type identity.
type AAAServerGroupType string

const (
AAAServerGroupTypeTACACS AAAServerGroupType = "openconfig-aaa:TACACS"
)

// AAAMethodType represents the AAA authentication/accounting method string.
type AAAMethodType string

const (
AAAMethodTypeLocal AAAMethodType = "local"
AAAMethodTypeNone AAAMethodType = "none"
)

// Compile-time assertions.
var (
_ gnmiext.DataElement = (*AAAServerGroup)(nil)
_ gnmiext.DataElement = (*AAAAuthenticationConfig)(nil)
_ gnmiext.DataElement = (*AAAAccountingConfig)(nil)
_ gnmiext.DataElement = (*AAAContainer)(nil)
)

// AAAContainer targets the full AAA container for deletion.
type AAAContainer struct{}

func (*AAAContainer) XPath() string { return "openconfig-system:system/aaa" }

// AAAServerGroup targets a server-group entry.
type AAAServerGroup struct {
Name string `json:"-"`
Config *AAAServerGroupConfig `json:"config,omitempty"`
Servers *AAAServers `json:"servers,omitempty"`
}

func (g *AAAServerGroup) XPath() string {
return fmt.Sprintf("openconfig-system:system/aaa/server-groups/server-group[name=%s]", g.Name)
}

// AAAServerGroupConfig holds the server-group config.
type AAAServerGroupConfig struct {
Name string `json:"name"`
Type AAAServerGroupType `json:"type"`
}

// AAAServers holds the server list.
type AAAServers struct {
Server gnmiext.List[string, *AAAServer] `json:"server,omitempty"`
}

// AAAServer represents a single server entry.
type AAAServer struct {
Address string `json:"address"`
Config *AAAServerConfig `json:"config,omitempty"`
TACACS *AAAServerTACACS `json:"tacacs,omitempty"`
}

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

// AAAServerConfig holds the server config.
type AAAServerConfig struct {
Address string `json:"address"`
Timeout uint16 `json:"timeout,omitempty"`
}

// AAAServerTACACS holds the tacacs container.
type AAAServerTACACS struct {
Config *AAAServerTACACSConfig `json:"config,omitempty"`
}

// AAAServerTACACSConfig holds tacacs config.
// SecretKey is write-only — the device returns an encrypted form that
// would never match the plaintext, so we exclude it from unmarshal to
// avoid perpetual diffs.
type AAAServerTACACSConfig struct {
Port uint16 `json:"port,omitempty"`
SecretKey string `json:"secret-key,omitempty"`
}

func (c *AAAServerTACACSConfig) UnmarshalJSON(data []byte) error {
type alias struct {
Port uint16 `json:"port,omitempty"`
}
var a alias
if err := json.Unmarshal(data, &a); err != nil {
return err
}
c.Port = a.Port
return nil
}

// AAAAuthenticationConfig targets aaa/authentication/config.
type AAAAuthenticationConfig struct {
Methods []AAAMethodType `json:"authentication-method"`
}

func (*AAAAuthenticationConfig) XPath() string {
return "openconfig-system:system/aaa/authentication/config"
}

// AAAAccountingConfig targets aaa/accounting/config.
type AAAAccountingConfig struct {
Methods []AAAMethodType `json:"accounting-method"`
}

func (*AAAAccountingConfig) XPath() string {
return "openconfig-system:system/aaa/accounting/config"
}
Loading
Loading