Skip to content
Merged
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
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -735,14 +735,16 @@ Pass the topology scheme and file on the CLI, or set the same values under
l8k generate --user-config cluster-config.yaml \
--spectrum-x RA2.3 \
--topology-scheme 2-tier \
--ip-version ipv4 \
--ip-version ipv6 \
--topology-file ./topology.json
```

`profile.spectrumX.hostFirstOctet` is config-only. When omitted, l8k uses `172`
for 2-tier IPv4 allocation and `10` for 3-tier IPv4 allocation. `ipVersion:
ipv6` is accepted in the config and CLI, but Spectrum-X CIDRPool rendering
currently supports IPv4 static allocations only.
for 2-tier IPv4 allocation and `10` for 3-tier IPv4 allocation. It has no effect
on IPv6 allocation. IPv6 uses the standard `fd02:00PP:RRDD:SSHH::peer` layout,
with a `/64` per node, host candidate `::1`, leaf gateway `::2`, and a `/40`
CIDRPool per rail or rail-plane. Generated IPv6 routes use `/32` for a single
plane and `/24` for dual- or quad-plane deployments.

CIDRPool allocation requires exact, case-sensitive equality between selected
`clusterConfig.workerNodes` values and topology host endpoint `node` values.
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ Discovery also accepts the profile and Spectrum-X flags below. Explicit flags ov
| `--multiplane-mode` | `none`, `swplb`, or `hwplb`. |
| `--number-of-planes` | Plane count for Spectrum-X. |
| `--topology-scheme` | `2-tier` or `3-tier` for topology-driven CIDRPool allocation. |
| `--ip-version` | `ipv4` or `ipv6`. CIDRPool rendering currently supports IPv4. |
| `--ip-version` | `ipv4` for per-node `/31` allocation or `ipv6` for per-node `/64` allocation. |
| `--topology-file` | Path to spcx-gen/reference-generator or contract-compliant NVIDIA AIR topology JSON. The format is detected from its structure. |
| `--spectrum-x-config` | Full ConfigMap YAML or raw `data.profile` YAML. Required for RA2.3. |
| `--spectrum-x-configmap-name` | ConfigMap name when `--spectrum-x-config` is raw profile YAML. |
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ profile:
| `spectrumX.multiplaneMode` | `none`, `swplb`, or `hwplb`. |
| `spectrumX.numberOfPlanes` | `1`, `2`, or `4`. |
| `spectrumX.topologyType` | `2-tier` or `3-tier`. |
| `spectrumX.ipVersion` | `ipv4` or `ipv6`; CIDRPool rendering currently supports IPv4. |
| `spectrumX.ipVersion` | `ipv4` for per-node `/31` allocation or `ipv6` for per-node `/64` allocation. |
| `spectrumX.hostFirstOctet` | Config-only first octet for generated IPv4 topology addressing. |
| `spectrumX.topologyFile` | Path to spcx-gen/reference-generator or contract-compliant NVIDIA AIR topology JSON. The format is detected from its structure; relative paths resolve from the config file. |
| `spectrumX.configMapName` / `profile` | RA2.3 ConfigMap name and embedded profile data. |
Expand Down
23 changes: 21 additions & 2 deletions docs/user/spectrum-x.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,11 +166,30 @@ l8k generate \
--spectrum-x RA2.3 \
--spectrum-x-config ./spectrum-x-profile-configmap.yaml \
--topology-scheme 2-tier \
--ip-version ipv4 \
--ip-version ipv6 \
--topology-file ./topology.json
```

IPv4 CIDRPool rendering is supported. IPv6 is accepted in config for forward compatibility but is not rendered into CIDRPools yet.
Both `ipv4` and `ipv6` generate complete nv-ipam CIDRPools. IPv4 preserves the
existing per-node `/31` allocation. IPv6 uses the standard Spectrum-X layout:

```text
fd02:00PP:RRDD:SSHH::peer/64
```

`PP`, `RR`, `DD`, `SS`, and `HH` are the zero-based plane, rail, pod, SU, and
host indices encoded as one byte each. Two-tier topologies require `DD=00`.
The host candidate is `::1`, the connected leaf and gateway are `::2`, and the
static allocation prefix is the canonical `/64` network. Each pool covers a
rail or rail-plane with a `/40`. The route is `/32` for a single-plane
deployment and `/24` for a dual- or quad-plane deployment.

In `swplb`, the plane is encoded and l8k emits one pool per rail-plane. In
`none` and `hwplb`, the address plane is zero and l8k emits one pool per rail.
`profile.spectrumX.hostFirstOctet` affects IPv4 only. The current topology
contract provides the fields required by this standard layout; alternative
platform-specific layouts that require additional topology fields are not
generated.

### Troubleshooting CIDRPool allocation errors

Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ var schemaCmd = &cobra.Command{
"--ip-version": {
Type: "string",
Default: config.SpectrumXIPVersionIPv4,
Description: "Spectrum-X host-to-leaf IP version for guide-based allocation: ipv4 or ipv6. IPv6 is accepted in config but CIDRPool rendering currently supports IPv4 only.",
Description: "Spectrum-X host-to-leaf IP version for guide-based CIDRPool allocation: ipv4 or ipv6.",
},
"--topology-file": {
Type: "string",
Expand Down
2 changes: 1 addition & 1 deletion pkg/config/default-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ profile:
# multiplaneMode: swplb # CLI parameter (overrides this value): --multiplane-mode (none, swplb, hwplb)
# numberOfPlanes: 4 # CLI parameter (overrides this value): --number-of-planes, also used as pfsPerNic
# topologyType: 2-tier # CLI parameter (overrides this value): --topology-scheme (2-tier, 3-tier)
# ipVersion: ipv4 # CLI parameter (overrides this value): --ip-version (ipv4, ipv6); CIDRPool rendering currently supports ipv4
# ipVersion: ipv4 # CLI parameter (overrides this value): --ip-version (ipv4 /31 or ipv6 /64 CIDRPool allocation)
# hostFirstOctet: 172 # Config-only IPv4 first octet override; defaults to 172 for 2-tier, 10 for 3-tier
# topologyFile: ./topology.json # CLI parameter (overrides this value): --topology-file; spcx-gen/reference-generator or contract-compliant NVIDIA AIR JSON
# configMapName: "site-ra23-profile" # Required for RA2.3 when profile is raw data.profile YAML; CLI: --spectrum-x-configmap-name
Expand Down
169 changes: 133 additions & 36 deletions pkg/networkoperatorplugin/spectrumx/addressing.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ package spectrumx
import (
"encoding/json"
"fmt"
"net"
"net/netip"
"os"
"sort"
"strings"
Expand All @@ -28,10 +28,18 @@ import (
)

type CIDRPool struct {
Name string
CIDR string
Routes []string
StaticAllocations []StaticAllocation
Name string
CIDR string
GatewayIndex int
PerNodeNetworkPrefix int
PerNodeExclusions []PerNodeExclusion
Routes []string
StaticAllocations []StaticAllocation
}

type PerNodeExclusion struct {
StartIndex int
EndIndex int
}

type StaticAllocation struct {
Expand Down Expand Up @@ -137,8 +145,8 @@ type hostLink struct {
su int
rail int
hostIndex int
hostIP net.IP
leafIP net.IP
hostIP netip.Addr
leafIP netip.Addr
}

type poolKey struct {
Expand All @@ -163,10 +171,6 @@ func BuildCIDRPools(cfg *config.LaunchKitConfig, group config.ClusterConfig) ([]
return nil, nil
}
spcx := cfg.Profile.SpectrumX
if spcx.IPVersion == config.SpectrumXIPVersionIPv6 {
return nil, fmt.Errorf("Spectrum-X CIDRPool generation currently supports ipVersion=%s only; got %s",
config.SpectrumXIPVersionIPv4, spcx.IPVersion)
}
if spcx.TopologyFile == "" {
return nil, fmt.Errorf("profile.spectrumX.topologyFile or --topology-file is required for Spectrum-X CIDRPool generation")
}
Expand Down Expand Up @@ -203,16 +207,15 @@ func BuildCIDRPools(cfg *config.LaunchKitConfig, group config.ClusterConfig) ([]
}
}
cidr := poolCIDR(staticAllocations[0].Prefix, spcx)
planeCIDR := planeCIDR(staticAllocations[0].Prefix, spcx)
routes := []string{cidr}
if planeCIDR != "" && planeCIDR != cidr {
routes = append(routes, planeCIDR)
}
settings := poolSettings(spcx)
pools = append(pools, CIDRPool{
Name: poolName(key, group.MergedIdentifier, spcx),
CIDR: cidr,
Routes: routes,
StaticAllocations: staticAllocations,
Name: poolName(key, group.MergedIdentifier, spcx),
CIDR: cidr,
GatewayIndex: settings.gatewayIndex,
PerNodeNetworkPrefix: settings.perNodeNetworkPrefix,
PerNodeExclusions: settings.perNodeExclusions,
Routes: poolRoutes(cidr, spcx),
StaticAllocations: staticAllocations,
})
}
return pools, nil
Expand Down Expand Up @@ -284,10 +287,10 @@ func hostLinks(topology *topologyFile, spcx *config.ProfileSpectrumX) ([]hostLin
hostIndexes[key+"|"+host.Node] = index
}
addressPlane := leaf.Attrs.Plane
if spcx.MultiplaneMode == "hwplb" {
if spcx.MultiplaneMode != "swplb" {
addressPlane = 0
}
hostIP, leafIP, err := allocateIPv4HostLeaf(spcx, addressPlane, host.Attrs.Rail, host.Attrs.Pod, host.Attrs.SU, index)
hostIP, leafIP, err := allocateHostLeaf(spcx, addressPlane, host.Attrs.Rail, host.Attrs.Pod, host.Attrs.SU, index)
if err != nil {
return nil, fmt.Errorf("topology host link %d endpoint %s/%s: %w", linkIdx, host.Node, host.Interface, err)
}
Expand Down Expand Up @@ -335,29 +338,80 @@ func countHostIndex(indexes map[string]int, key string) int {
return count
}

func allocateIPv4HostLeaf(spcx *config.ProfileSpectrumX, plane, rail, pod, su, hostIndex int) (net.IP, net.IP, error) {
func allocateHostLeaf(spcx *config.ProfileSpectrumX, plane, rail, pod, su, hostIndex int) (netip.Addr, netip.Addr, error) {
switch spcx.IPVersion {
case "", config.SpectrumXIPVersionIPv4:
return allocateIPv4HostLeaf(spcx, plane, rail, pod, su, hostIndex)
case config.SpectrumXIPVersionIPv6:
return allocateIPv6HostLeaf(spcx, plane, rail, pod, su, hostIndex)
default:
return netip.Addr{}, netip.Addr{}, fmt.Errorf("unsupported Spectrum-X ipVersion %q", spcx.IPVersion)
}
}

func allocateIPv4HostLeaf(spcx *config.ProfileSpectrumX, plane, rail, pod, su, hostIndex int) (netip.Addr, netip.Addr, error) {
if rail < 0 || rail > 7 {
return nil, nil, fmt.Errorf("rail %d exceeds the supported 3-bit rail field", rail)
return netip.Addr{}, netip.Addr{}, fmt.Errorf("rail %d exceeds the supported 3-bit rail field", rail)
}
if hostIndex < 0 || hostIndex > 127 {
return nil, nil, fmt.Errorf("host index %d exceeds the supported 7-bit host field", hostIndex)
return netip.Addr{}, netip.Addr{}, fmt.Errorf("host index %d exceeds the supported 7-bit host field", hostIndex)
}
firstOctet := spcx.HostFirstOctet
if firstOctet == 0 {
firstOctet = config.SpectrumXDefaultHostFirstOctet(spcx.TopologyType)
}
if firstOctet < 1 || firstOctet > 255 {
return nil, nil, fmt.Errorf("hostFirstOctet %d must fit in one IPv4 octet", firstOctet)
return netip.Addr{}, netip.Addr{}, fmt.Errorf("hostFirstOctet %d must fit in one IPv4 octet", firstOctet)
}
second, third, err := ipv4HostLeafMiddleOctets(spcx, plane, rail, pod, su)
if err != nil {
return nil, nil, err
return netip.Addr{}, netip.Addr{}, err
}
host := net.IPv4(byte(firstOctet), byte(second), byte(third), byte(hostIndex<<1))
leaf := net.IPv4(byte(firstOctet), byte(second), byte(third), byte(hostIndex<<1|1))
host := netip.AddrFrom4([4]byte{byte(firstOctet), byte(second), byte(third), byte(hostIndex << 1)})
leaf := netip.AddrFrom4([4]byte{byte(firstOctet), byte(second), byte(third), byte(hostIndex<<1 | 1)})
return host, leaf, nil
}

func allocateIPv6HostLeaf(spcx *config.ProfileSpectrumX, plane, rail, pod, su, hostIndex int) (netip.Addr, netip.Addr, error) {
fields := []struct {
name string
value int
}{
{name: "plane", value: plane},
{name: "rail", value: rail},
{name: "pod", value: pod},
{name: "su", value: su},
{name: "host index", value: hostIndex},
}
for _, field := range fields {
if field.value < 0 || field.value > 255 {
return netip.Addr{}, netip.Addr{}, fmt.Errorf("%s %d exceeds the supported 8-bit field", field.name, field.value)
}
}

switch spcx.TopologyType {
case config.SpectrumXTopology2Tier:
if pod != 0 {
return netip.Addr{}, netip.Addr{}, fmt.Errorf("pod %d must be zero for 2-tier IPv6 allocation", pod)
}
case config.SpectrumXTopology3Tier:
default:
return netip.Addr{}, netip.Addr{}, fmt.Errorf("unsupported Spectrum-X topologyType %q", spcx.TopologyType)
}

address := [16]byte{
0xfd, 0x02,
0x00, byte(plane),
byte(rail), byte(pod),
byte(su), byte(hostIndex),
}
hostAddress := address
hostAddress[15] = 1
leafAddress := address
leafAddress[15] = 2
return netip.AddrFrom16(hostAddress), netip.AddrFrom16(leafAddress), nil
}

func ipv4HostLeafMiddleOctets(spcx *config.ProfileSpectrumX, plane, rail, pod, su int) (int, int, error) {
planeAware := planeAwareAddressing(spcx)
switch spcx.TopologyType {
Expand Down Expand Up @@ -405,6 +459,7 @@ func planeAwareAddressing(spcx *config.ProfileSpectrumX) bool {
func allocationsByPool(links []hostLink, allowedNodes map[string]struct{}, spcx *config.ProfileSpectrumX) map[poolKey][]StaticAllocation {
result := map[poolKey][]StaticAllocation{}
seen := map[poolKey]map[string]struct{}{}
settings := poolSettings(spcx)
for _, link := range links {
if len(allowedNodes) > 0 {
if _, ok := allowedNodes[link.node]; !ok {
Expand All @@ -422,7 +477,7 @@ func allocationsByPool(links []hostLink, allowedNodes map[string]struct{}, spcx
result[key] = append(result[key], StaticAllocation{
Gateway: link.leafIP.String(),
NodeName: link.node,
Prefix: link.hostIP.String() + "/31",
Prefix: netip.PrefixFrom(link.hostIP, settings.perNodeNetworkPrefix).Masked().String(),
})
}
return result
Expand Down Expand Up @@ -465,11 +520,27 @@ func poolCIDR(prefix string, spcx *config.ProfileSpectrumX) string {
return supernet(prefix, railPrefixLength(spcx))
}

func planeCIDR(prefix string, spcx *config.ProfileSpectrumX) string {
return supernet(prefix, planePrefixLength(spcx))
func poolRoutes(cidr string, spcx *config.ProfileSpectrumX) []string {
if isIPv6(spcx) {
prefixLength := 32
if spcx.NumberOfPlanes > 1 {
prefixLength = 24
}
return []string{supernet(cidr, prefixLength)}
}

planeCIDR := supernet(cidr, planePrefixLength(spcx))
routes := []string{cidr}
if planeCIDR != "" && planeCIDR != cidr {
routes = append(routes, planeCIDR)
}
return routes
}

func railPrefixLength(spcx *config.ProfileSpectrumX) int {
if isIPv6(spcx) {
return 40
}
if spcx.TopologyType == config.SpectrumXTopology3Tier {
if planeAwareAddressing(spcx) {
return 13
Expand All @@ -496,13 +567,39 @@ func planePrefixLength(spcx *config.ProfileSpectrumX) int {
}

func supernet(prefix string, prefixLength int) string {
ip, _, err := net.ParseCIDR(prefix)
parsed, err := netip.ParsePrefix(prefix)
if err != nil {
return ""
}
mask := net.CIDRMask(prefixLength, 32)
network := ip.Mask(mask)
return (&net.IPNet{IP: network, Mask: mask}).String()
if prefixLength < 0 || prefixLength > parsed.Addr().BitLen() {
return ""
}
return netip.PrefixFrom(parsed.Addr(), prefixLength).Masked().String()
}

type cidrPoolSettings struct {
gatewayIndex int
perNodeNetworkPrefix int
perNodeExclusions []PerNodeExclusion
}

func poolSettings(spcx *config.ProfileSpectrumX) cidrPoolSettings {
if isIPv6(spcx) {
return cidrPoolSettings{
gatewayIndex: 2,
perNodeNetworkPrefix: 64,
perNodeExclusions: []PerNodeExclusion{{StartIndex: 2, EndIndex: 2}},
}
}
return cidrPoolSettings{
gatewayIndex: 0,
perNodeNetworkPrefix: 31,
perNodeExclusions: []PerNodeExclusion{{StartIndex: 1, EndIndex: 1}},
}
}

func isIPv6(spcx *config.ProfileSpectrumX) bool {
return spcx.IPVersion == config.SpectrumXIPVersionIPv6
}

func nodeSet(nodes []string) map[string]struct{} {
Expand Down
Loading