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
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,28 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and

## [Unreleased]

### Added

- **Dual-homed RNEs (`--rne-dual-home-pct`)** — that percentage of each GNE's remote NEs is
drawn from a fleet-wide shared pool instead of a private one, so the same RNE is reachable
through several gateways, as operators deploy them for resilience. A shared RNE's shelf is
derived from its TID, so every gateway fronting it renders byte-identical equipment and a
client can recognise the paths as one device; a private RNE now carries its gateway's index
(`RNE-LAX0007`) so two gateways cannot collide on a city name and serve unrelated equipment
under one TID. Gateways are additionally chained — device N always shares an `RNE-LINK<N>`
element with device N+1, as optical networks dual-home an RNE between neighbouring gateways —
so any two adjacent ports demonstrate dual-homing on every seed instead of only when a random
draw collides. Links count towards a gateway's documented 2–5 remote NEs. Defaults to 0, which
takes no extra draw from the generator stream and leaves output byte-identical to a build
without the option.

### Fixed

- **TL1 `ACT-USER` now accepts a quoted password** — a real 6500 requires a complex password to be
wrapped in double quotes, which rConfig now always sends; the parser compared the literal `"pw"`
against the configured password and denied every login, and truncated a password containing `:`.
The bare form is still accepted, and `;` inside a quoted string no longer terminates the command.

## [0.0.4] — 2026-06-07

### Added
Expand Down
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1237,7 +1237,7 @@ The generator is driven by a **model registry**, of which the nine Cisco size bu
| Model | Vendor | Driver | Protocol | Payload |
|---|---|---|---|---|
| `ciena-6500-tl1` | Ciena | `ciena_tl1` | TL1 over SSH | `RTRV-EQPT::ALL` shelf inventory (7-slot 6500), mmap-streamed |
| `ciena-6500-tl1-gne` | Ciena | `ciena_tl1` | TL1 over SSH | Gateway NE fronting 2–5 Remote NEs; GNE + per-RNE inventories, mmap-streamed |
| `ciena-6500-tl1-gne` | Ciena | `ciena_tl1` | TL1 over SSH | Gateway NE fronting 2–5 Remote NEs; GNE + per-RNE inventories, mmap-streamed. `--rne-dual-home-pct` shares RNEs between gateways |

Mix them into any run, e.g. `--distribution "sm:50,ciena-6500-tl1:50"`. Ciena rows in the manifest carry `vendor=Ciena, template=ciena_tl1`; Cisco rows are unchanged.

Expand Down Expand Up @@ -1282,6 +1282,28 @@ After login, `RTRV-NBR` lists the RNEs, and you address one by putting its TID i

A TID that empty/`ALL`/the GNE's own SID is treated as local; an unknown or unreachable RNE TID returns `DENY`/`IIAC`. The TID-addressed short form `VERB:TID:CTAG;` and the strict `VERB::AID:CTAG;` form are both accepted. The standalone `ciena-6500-tl1` model is a GNE with no RNEs — `RTRV-NBR` returns an empty list.

#### Dual-homed RNEs (`--rne-dual-home-pct`)

Operators dual-home an RNE — reach it through two or more GNEs — so that losing one gateway does not lose the element. `--rne-dual-home-pct` draws that share of each GNE's remote NEs from a fleet-wide shared pool instead of a private one, so the same RNE turns up behind several gateways:

```
rcfg-sim-gen --distribution ciena-6500-tl1-gne:100 --rne-dual-home-pct 30
```

A shared RNE's inventory is derived from its TID, so **every gateway fronting it renders byte-identical equipment** — which is what lets a client recognise the two paths as one device rather than two. Its TID keeps the bare `RNE-<CITY>` form; a private RNE carries its gateway's index (`RNE-LAX0007`) so two gateways cannot collide on a city name by chance and present unrelated equipment under one name. Both forms stay matchable as `RNE-[A-Z0-9]+`.

Gateways are also chained: device *N* always shares an `RNE-LINK<N>` element with device *N+1*, mirroring how optical networks dual-home an RNE between neighbouring gateways around a ring. That makes **any two adjacent ports** demonstrate dual-homing on every seed, rather than only when a random draw happens to collide:

```
port 22000: RNE-LINK0000 RNE-STL RNE-LAX RNE-MKE RNE-SLC
port 22001: RNE-LINK0000 RNE-LINK0001 RNE-BUF RNE-PHL
port 22002: RNE-LINK0001 RNE-LINK0002 RNE-CMH
```

Links count towards a gateway's 2–5 remote NEs rather than being added on top, and the last device in a fleet claims no forward link.

The default is `0`: every RNE is private to its gateway, no extra draw is taken from the generator's stream, and output is byte-identical to a build without the option.

#### SSH-layer auth vs in-band TL1 auth

Real 6500 deployments differ in whether the SSH transport itself challenges for a password. Both patterns are supported via the server's `--ssh-auth` flag:
Expand Down
3 changes: 2 additions & 1 deletion cmd/rcfg-sim-gen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ func main() {
flag.IntVar(&cfg.PortStart, "port-start", 10000, "first port in range")
flag.IntVar(&cfg.DevicesPerIP, "devices-per-ip", 2500, "devices mapped to each IP")
flag.Int64Var(&cfg.Seed, "seed", 42, "PRNG seed for deterministic output")
flag.StringVar(&cfg.Distribution, "distribution", "sm:40,md:40,lg:15,xl:5", "model weights (percent, sum=100; models: sm, md, lg, xl, 2xl, 3xl, 4xl, 5xl, 6xl, ciena-6500-tl1)")
flag.StringVar(&cfg.Distribution, "distribution", "sm:40,md:40,lg:15,xl:5", "model weights (percent, sum=100; models: sm, md, lg, xl, 2xl, 3xl, 4xl, 5xl, 6xl, ciena-6500-tl1, ciena-6500-tl1-gne)")
flag.IntVar(&cfg.RNEDualHomePct, "rne-dual-home-pct", 0, "percent of GNE remote NEs drawn from a shared pool, so the same RNE is reachable through more than one gateway (0 = none)")
flag.StringVar(&cfg.Username, "username", "admin", "username written into manifest")
flag.StringVar(&cfg.Password, "password", "admin", "password written into manifest")
flag.StringVar(&cfg.EnablePassword, "enable-password", "enable123", "enable password written into manifest")
Expand Down
115 changes: 113 additions & 2 deletions internal/configs/ciena.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package configs

import (
"crypto/sha256"
"encoding/binary"
"fmt"
"math/rand"
"strings"
Expand Down Expand Up @@ -72,19 +74,55 @@ type CienaGNEData struct {
// shape as a standalone node; each RNE is a full shelf re-identified with an
// RNE-<CITY> TID. All randomness flows from deviceRand(seed, index) so output is
// byte-reproducible.
//
// With cfg.RNEDualHomePct > 0 some RNEs are drawn from a fleet-wide shared pool
// instead of being private to this GNE, so the same RNE turns up behind several
// gateways. A shared RNE's shelf is derived from its TID rather than from this
// GNE's stream, which is what makes two gateways emit byte-identical sections for
// it without the generator workers having to coordinate. At the default of 0 no
// draw is taken at all, so output stays byte-identical to a build without this.
//
// Dual-homing also links each gateway to its neighbours by a guaranteed shared
// element (see linkRnes), so any two adjacent devices in the fleet always have a
// dual-homed RNE between them rather than only when the random draw happens to
// collide.
func buildCienaGNE(cfg Config, index int) CienaGNEData {
rng := deviceRand(cfg.Seed, index)
gne := buildCienaShelf(cfg, index, rng)

nRNE := 2 + rng.Intn(4) // 2..5
used := map[string]bool{}
var rnes []CienaEqptData
for i := 0; i < nRNE; i++ {
tid := "RNE-" + strings.ToUpper(citySyllables[rng.Intn(len(citySyllables))])

// Links to the neighbouring gateways come first and count towards this GNE's
// total, so enabling dual-homing re-mixes which elements sit behind a gateway
// rather than growing every gateway past the documented 2-5.
for _, tid := range linkRnes(cfg, index) {
used[tid] = true
rnes = append(rnes, buildSharedRneShelf(cfg, tid))
}

for i := len(rnes); i < nRNE; i++ {
city := strings.ToUpper(citySyllables[rng.Intn(len(citySyllables))])

shared := false
if cfg.RNEDualHomePct > 0 {
shared = rng.Intn(100) < cfg.RNEDualHomePct
}

tid := rneTID(cfg, city, index, shared)
if used[tid] {
continue // dedupe RNE TID collisions within one GNE
}
used[tid] = true

if shared {
// Identical through every gateway that fronts it, so rConfig sees one device.
rnes = append(rnes, buildSharedRneShelf(cfg, tid))

continue
}

r := buildCienaShelf(cfg, index, rng)
r.SID = tid
r.ShelfSerial = serialFor(tid)
Expand All @@ -94,6 +132,79 @@ func buildCienaGNE(cfg Config, index int) CienaGNEData {
return CienaGNEData{CienaEqptData: gne, RNEs: rnes}
}

// rneTID names a remote NE.
//
// A shared RNE keeps the bare "RNE-<CITY>" form so two gateways drawing the same
// city name the same element. A private one carries its gateway's index, which
// stops two gateways colliding on a city by chance and presenting unrelated
// equipment under one name. Without dual-homing enabled the historic bare form is
// kept for every RNE so existing fleets regenerate unchanged.
//
// The suffix stays alphanumeric: TIDs are matched as RNE-[A-Z0-9]+ on the wire.
func rneTID(cfg Config, city string, index int, shared bool) string {
if shared || cfg.RNEDualHomePct == 0 {
return "RNE-" + city
}

return fmt.Sprintf("RNE-%s%04d", city, index%10000)
}

// linkRnes returns the TIDs of the elements this gateway shares with its immediate
// neighbours in the fleet.
//
// Optical networks are built as chains and rings: an RNE sits between two gateways
// and is reachable through either, which is what makes losing one survivable. The
// link between gateway N-1 and N is named for that pair, so both devices derive the
// same TID independently — device N claims the link to N-1 and the link to N+1, and
// its neighbours claim the same two from their side. No coordination between
// generator workers, and every adjacent pair is guaranteed to share an element
// rather than only doing so when a random draw collides.
//
// Empty unless dual-homing is enabled, so default output is untouched.
func linkRnes(cfg Config, index int) []string {
if cfg.RNEDualHomePct <= 0 {
return nil
}

var tids []string
if index > 0 {
tids = append(tids, linkTID(index-1))
}
// The last gateway in the fleet has no successor, so it claims no forward link -
// an element with only one gateway would not be dual-homed at all.
if index+1 < cfg.Count {
tids = append(tids, linkTID(index))
}

return tids
}

// linkTID names the element shared between gateway n and gateway n+1.
func linkTID(n int) string {
return fmt.Sprintf("RNE-LINK%04d", n%10000)
}

// buildSharedRneShelf builds a remote NE entirely from its TID, so every gateway
// that fronts it renders the same bytes. Nothing about the GNE leaks in: the
// shelf contents, serial and management IP are all TID-derived.
func buildSharedRneShelf(cfg Config, tid string) CienaEqptData {
seedIndex := tidSeedIndex(tid)

r := buildCienaShelf(cfg, seedIndex, deviceRand(cfg.Seed, seedIndex))
r.SID = tid
r.ShelfSerial = serialFor(tid)
r.NodeIP = ipPlusOffset(cfg.IPBase, seedIndex%256)

return r
}

// tidSeedIndex turns a TID into the stable index its shelf is generated from.
func tidSeedIndex(tid string) int {
sum := sha256.Sum256([]byte(tid))

return int(binary.BigEndian.Uint32(sum[:4]) & 0x7fffffff)
}

// CienaEqptData is the payload for templates/ciena_tl1_eqpt.tmpl: the equipment
// inventory of a 6500 7-slot shelf. Fully determined by (seed, index) via
// deviceRand, so a fixed seed yields byte-identical output. Disjoint from the
Expand Down
Loading
Loading