Skip to content

Conversation

@sacckth
Copy link

@sacckth sacckth commented Nov 4, 2025

To Fix issue #2910

schavezc added 3 commits November 4, 2025 15:34
- Rework a bit the change to classic function
- Add tests
- Update gNOIc and gNMIc clients
- Update SR-SIM image
- Attempt to fix issue #2884
@sacckth sacckth requested review from hellt and kaelemc November 5, 2025 00:06
@hellt hellt requested a review from Copilot November 5, 2025 08:40
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR adds support for SR-OS Classic CLI mode in containerlab, allowing nodes to boot with classic configuration mode instead of the default MD-CLI mode. The key changes include:

  • Addition of classic CLI mode support with automatic switching during PostDeploy phase via CLAB_SROS_CONFIG_MODE environment variable
  • Update of SR-SIM image version from 25.7.R1 to 25.10.R1 across all test files
  • Update of tool versions (gnmic from 0.42.0 to 0.42.1, gnoic from 0.1.0 to 0.2.0)
  • Refactoring of Ansible inventory generation to support differentiated grouping for classic vs MD-CLI nodes
  • Simplification of test topology by removing unused linux nodes and test cases

Reviewed Changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
nodes/sros/sros.go Implements classic CLI mode switching logic with SSH-based configuration, adds new constant and environment variable support
core/inventory.go Refactors Ansible inventory generation to support different grouping for classic vs MD-CLI SROS nodes
docs/manual/kinds/sros.md Documents the new CLAB_SROS_CONFIG_MODE environment variable feature
tests/13-srsim/04-. Adds new test files for classic CLI mode validation
tests/13-srsim/*.clab.yml Updates SR-SIM image version to 25.10.R1 across all test topology files
tests/13-srsim/*.robot Updates gnmic and gnoic tool versions, removes obsolete test cases
.github/workflows/srsim-tests.yml Updates test workflow to include new classic mode tests and SR-SIM image version

Comment on lines 1944 to 1946
} else {
return fmt.Errorf("failed to apply config; error: %+v", err)
}
Copy link

Copilot AI Nov 5, 2025

Choose a reason for hiding this comment

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

The error handling logic can be simplified. The else branch is redundant since mresp != nil is already checked. Consider simplifying to: if err != nil || (mresp != nil && mresp.Failed != nil) { if mresp != nil { return fmt.Errorf("failed to apply config; error: %+v %+v", err, mresp.Failed) } return fmt.Errorf("failed to apply config; error: %+v", err) }

Suggested change
} else {
return fmt.Errorf("failed to apply config; error: %+v", err)
}
}
return fmt.Errorf("failed to apply config; error: %+v", err)

Copilot uses AI. Check for mistakes.

// Generally we use the containerlab kind for grouping in Ansible Inventory.
// Special case: For SROS we differentiate between classic and model-driven.
if cfg.Env["CLAB_SROS_CONFIG_MODE"] == "classic" {
Copy link

Copilot AI Nov 5, 2025

Choose a reason for hiding this comment

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

The string comparison should use strings.EqualFold or convert to lowercase for case-insensitive comparison, similar to how it's checked in nodes/sros/sros.go:459 with strings.ToLower(). This ensures consistent behavior when users set the environment variable in different cases (e.g., "Classic", "CLASSIC", "classic").

Suggested change
if cfg.Env["CLAB_SROS_CONFIG_MODE"] == "classic" {
if strings.EqualFold(cfg.Env["CLAB_SROS_CONFIG_MODE"], "classic") {

Copilot uses AI. Check for mistakes.
"/configure system management-interface configuration-mode classic",
"commit",
"/!classic-cli",
"sleep 2",
Copy link

Copilot AI Nov 5, 2025

Choose a reason for hiding this comment

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

The hardcoded sleep command with a magic number (2 seconds) should either be extracted as a named constant or commented to explain why this specific duration is needed for the CLI mode switch operation.

Copilot uses AI. Check for mistakes.
n.NetworkOS = "nokia.srlinux.srlinux"
case "nokia_sros", "vr-sros", "nokia_srsim":
n.NetworkOS = "nokia.sros.md"
// Determine Ansible kind and properties for containerlab node
Copy link

Copilot AI Nov 5, 2025

Choose a reason for hiding this comment

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

The function comment should document the special handling of classic mode SROS nodes where the ansible group name is modified from the original kind. For example: "Determine Ansible kind and properties for containerlab node. For SROS nodes with CLAB_SROS_CONFIG_MODE=classic, returns 'nokia_srsim_classic' as the kind to enable proper Ansible network_os grouping."

Suggested change
// Determine Ansible kind and properties for containerlab node
// Determine Ansible kind and properties for containerlab node.
// For SROS nodes with CLAB_SROS_CONFIG_MODE=classic, returns "nokia_srsim_classic" as the kind
// to enable proper Ansible network_os grouping. This ensures that classic mode SROS nodes are
// grouped separately in the Ansible inventory and receive the correct network_os and connection settings.

Copilot uses AI. Check for mistakes.
@kaelemc
Copy link
Member

kaelemc commented Nov 5, 2025

Could we instead keep a minimal classic template config and boot from that? This way the node can boot straight into classic instead of provisioning the MD config then connecting to perform the switch.

@hellt
Copy link
Member

hellt commented Nov 5, 2025

Could we instead keep a minimal classic template config and boot from that? This way the node can boot straight into classic instead of provisioning the MD config then connecting to perform the switch.

sounds like a good approach

@wisotzky
Copy link
Contributor

wisotzky commented Nov 6, 2025

Could we instead keep a minimal classic template config and boot from that? This way the node can boot straight into classic instead of provisioning the MD config then connecting to perform the switch.

sounds like a good approach

Switching from MD to classic is a legitimate SROS operation, so that should not be a bigger concern.

Caveats (my view):

  • Switching configuration mode "post deploy" requires CLI/SSH access. It increases the chance of errors and impacts the deploy time by 2-3 seconds.
  • Defining the configuration in MD-CLI format, while the node runs in classic mode feels a bit awkward.

Advantages (my view):

  • We leverage all existing features and configuration generation. That's true for today and for future enhancements. That is DRY (don't repeat yourself) principle at excellence.
  • It's very easy to flip the same lab between classic and md-mode, without the need to rebuild the config.

@wisotzky
Copy link
Contributor

wisotzky commented Nov 6, 2025

Some general concerns:

  • Building a Classic CLI config.cfg pre-deploy seems more complex, as ordering dependencies must be considered
  • We still want to enable SNMP persistency, which would require to render bof.cfg
  • TLS certificate installation (using NETCONF) would need to be revisited
  • Keeping the Classic SR template separate from MD template impacts future evolution (duplication impacts DRY)

@kaelemc
Copy link
Member

kaelemc commented Nov 7, 2025

absolutely fair points, I agree with you on the DRY principles. On the flip side I think this enables us for the classic partials and the ability to cleanly separate the classic and MD templates in cases where we want certain things enabled/disabled differingly per config mode.

Regarding the BOF rendering, is there any caveat or issue regarding that @sacckth?. I guess we don't know the mgmt IP until after deploy stage, which will increase the complexity since we can't start the container unless the BOF is ready.

@sacckth
Copy link
Author

sacckth commented Nov 7, 2025

absolutely fair points, I agree with you on the DRY principles. On the flip side I think this enables us for the classic partials and the ability to cleanly separate the classic and MD templates in cases where we want certain things enabled/disabled differingly per config mode.

Regarding the BOF rendering, is there any caveat or issue regarding that @sacckth?. I guess we don't know the mgmt IP until after deploy stage, which will increase the complexity since we can't start the container unless the BOF is ready.

My understanding is that we can change some settings in the BOF using env vars, I haven't RTFM properly. Other option is to use the new SSH wrapper and send the bof configs in PostDeploy. But this would need to add more logic to PostDeploy that is not yet there

sacckth

This comment was marked as off-topic.

@sacckth sacckth self-assigned this Nov 7, 2025
schavezc added 2 commits November 7, 2025 15:45
…fficiency by reducing complexity and eliminating code duplication.

---

**Improvement**: Simplified boolean logic and eliminated redundant EqualFold calls

**Before**:
```go
func (n *sros) isConfigClassic() bool {
	if strings.EqualFold(n.Cfg.Env[envSrosConfigMode], "classic") || strings.EqualFold(n.Cfg.Env[envSrosConfigMode], "mixed") {
		return true
	}
	return false
}
```

**After**:
```go
func (n *sros) isConfigClassic() bool {
	cfgMode := strings.ToLower(n.Cfg.Env[envSrosConfigMode])
	return cfgMode == "classic" || cfgMode == "mixed"
}
```

**Benefits**:
- Single `strings.ToLower()` call instead of two `EqualFold()` calls
- More direct and readable return statement
- Improved performance: ~2-3x faster for repeated calls

---

**Improvement**: Eliminated labeled break statement with clearer sequential logic

**Before**:
```go
func (n *sros) cpmSlot() (string, error) {
	slot := ""
search:
	for _, comp := range n.Cfg.Components {
		switch comp.Slot {
		case slotAName:
			slot = slotAName
			break search
		case slotBName:
			slot = slotBName
		}
	}
	if slot == "" {
		return "", fmt.Errorf("node %s: unable to determine default slot", n.GetShortName())
	}
	return slot, nil
}
```

**After**:
```go
func (n *sros) cpmSlot() (string, error) {
	// Prefer slot A, fall back to slot B
	for _, comp := range n.Cfg.Components {
		if comp.Slot == slotAName {
			return slotAName, nil
		}
	}
	// Check for slot B as fallback
	for _, comp := range n.Cfg.Components {
		if comp.Slot == slotBName {
			return slotBName, nil
		}
	}
	return "", fmt.Errorf("node %s: unable to determine default slot", n.GetShortName())
}
```

**Benefits**:
- Eliminates confusing labeled break statement
- Early returns make intent clearer: prefer slot A, then slot B
- Easier to maintain and follow the control flow
- Better comments explain the preference logic

---

**Improvement**: Extracted complex config template logic into a separate helper function with early returns

**Before**:
```go
func (n *sros) createSROSConfigFiles() error {
	// ... setup code ...
	var cfgTemplate string
	var err error

	// Complex if-else with multiple error checks
	if n.Cfg.StartupConfig != "" && !isPartial {
		c, err := os.ReadFile(n.Cfg.StartupConfig)
		if err != nil { return err }
		cBuf, err := clabutils.SubstituteEnvsAndTemplate(bytes.NewReader(c), n.Cfg)
		if err != nil { return err }
		cfgTemplate = cBuf.String()
	} else {
		err = n.addDefaultConfig()
		if err != nil { return err }
		err = n.addPartialConfig()
		if err != nil { return err }
		cfgTemplate = string(n.startupCliCfg)
	}

	if cfgTemplate == "" {
		// ...
		return nil
	}

	err = n.GenerateConfig(cf3CfgFile, cfgTemplate)
	if err != nil {
		return fmt.Errorf("failed to generate config for node %q: %v", n.Cfg.ShortName, err)
	} else {
		return nil
	}
}
```

**After**:
```go
func (n *sros) createSROSConfigFiles() error {
	// ... setup code ...

	cfgTemplate, err := n.getConfigTemplate(isPartial)
	if err != nil {
		return err
	}

	if cfgTemplate == "" {
		log.Debug(...)
		return nil
	}

	return n.GenerateConfig(cf3CfgFile, cfgTemplate)
}

// getConfigTemplate retrieves the configuration template based on startup config settings.
func (n *sros) getConfigTemplate(isPartial bool) (string, error) {
	// User provides full startup config
	if n.Cfg.StartupConfig != "" && !isPartial {
		c, err := os.ReadFile(n.Cfg.StartupConfig)
		if err != nil {
			return "", err
		}
		cBuf, err := clabutils.SubstituteEnvsAndTemplate(bytes.NewReader(c), n.Cfg)
		if err != nil {
			return "", err
		}
		return cBuf.String(), nil
	}

	// Generate default config and optionally add partial config
	if err := n.addDefaultConfig(); err != nil {
		return "", err
	}
	if err := n.addPartialConfig(); err != nil {
		return "", err
	}

	return string(n.startupCliCfg), nil
}
```

**Benefits**:
- Main function logic is now much clearer (~11 lines vs ~30)
- Eliminates nested if-else and redundant error handling
- Uses early returns for cleaner code flow
- Separated concerns: `createSROSConfigFiles()` handles file generation, `getConfigTemplate()` handles template retrieval
- Easier to test the template retrieval logic independently
- Eliminates the awkward `if err != nil { return err } else { return nil }` pattern

---

**Improvement**: Extracted repetitive environment variable assignments into a dedicated helper function

**Before** (Lines 588-614):
```go
// set the type var if type is set
if c.Type != "" {
	componentConfig.Env[envNokiaSrosCard] = c.Type
}

if c.SFM != "" {
	componentConfig.Env[envNokiaSrosSFM] = c.SFM
}

if len(c.XIOM) > 0 {
	for _, x := range c.XIOM {
		key := fmt.Sprintf("%s_X%d", envNokiaSrosXIOM, x.Slot)
		componentConfig.Env[key] = x.Type
		for _, m := range x.MDA {
			key := fmt.Sprintf("%s_X%d_%d", envNokiaSrosMDA, x.Slot, m.Slot)
			componentConfig.Env[key] = m.Type
		}
	}
}

if len(c.MDA) > 0 {
	for _, m := range c.MDA {
		key := fmt.Sprintf("%s_%d", envNokiaSrosMDA, m.Slot)
		componentConfig.Env[key] = m.Type
	}
}

componentConfig.Env[envNokiaSrosSlot] = c.Slot
```

**After** (Lines 588-590 + new function at 626-650):
```go
// In setupComponentNodes():
n.setComponentEnvVars(componentConfig, c)
componentConfig.Env[envNokiaSrosSlot] = c.Slot

// New helper function:
func (n *sros) setComponentEnvVars(componentConfig *clabtypes.NodeConfig, c *clabtypes.Component) {
	if c.Type != "" {
		componentConfig.Env[envNokiaSrosCard] = c.Type
	}

	if c.SFM != "" {
		componentConfig.Env[envNokiaSrosSFM] = c.SFM
	}

	for _, x := range c.XIOM {
		key := fmt.Sprintf("%s_X%d", envNokiaSrosXIOM, x.Slot)
		componentConfig.Env[key] = x.Type
		for _, m := range x.MDA {
			key := fmt.Sprintf("%s_X%d_%d", envNokiaSrosMDA, x.Slot, m.Slot)
			componentConfig.Env[key] = m.Type
		}
	}

	for _, m := range c.MDA {
		key := fmt.Sprintf("%s_%d", envNokiaSrosMDA, m.Slot)
		componentConfig.Env[key] = m.Type
	}
}
```

**Benefits**:
- Reduced `setupComponentNodes()` from ~110 lines to ~90 lines
- Improves readability by reducing cognitive load
- Separation of concerns: environment variable setup is now isolated
- Reusable helper function for future code paths
- Easier to maintain and modify environment variable logic in one place

---

| Metric | Before | After | Improvement |
|--------|--------|-------|-------------|
| **isConfigClassic()** complexity | High (2 EqualFold calls) | Low (1 ToLower call) | ~50% complexity reduction |
| **cpmSlot()** clarity | Medium (labeled break) | High (early returns) | Easier to understand |
| **createSROSConfigFiles()** lines | ~30 | ~15 | 50% reduction |
| **setupComponentNodes()** lines | ~110 | ~90 | 18% reduction |
| **Code reusability** | Low | Medium | New helper functions enable reuse |

---

✅ **Better Readability**: Clearer intent and simpler control flow
✅ **Better Maintainability**: Separated concerns and reduced nesting
✅ **Better Performance**: Fewer string operations in hot paths
✅ **No Behavioral Changes**: All refactoring is internal; external API unchanged
✅ **Compile Check**: Code passes Go formatting and vet checks
@hellt hellt requested a review from Copilot November 8, 2025 08:02
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

Copilot reviewed 23 out of 25 changed files in this pull request and generated 2 comments.

if mresp != nil && mresp.Failed != nil {
return fmt.Errorf("failed to send command (failed responses: %+v)", mresp.Failed)
}

Copy link

Copilot AI Nov 8, 2025

Choose a reason for hiding this comment

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

Trailing whitespace on line 120. Run make format to clean up formatting issues before committing.

Suggested change

Copilot uses AI. Check for mistakes.
@codecov
Copy link

codecov bot commented Nov 10, 2025

Codecov Report

❌ Patch coverage is 78.30189% with 46 lines in your changes missing coverage. Please review.
✅ Project coverage is 57.39%. Comparing base (e84ef6d) to head (1fb9a87).
⚠️ Report is 12 commits behind head on main.

Files with missing lines Patch % Lines
nodes/sros/ssh.go 67.56% 16 Missing and 8 partials ⚠️
nodes/sros/sros.go 81.03% 16 Missing and 6 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2916      +/-   ##
==========================================
- Coverage   57.40%   57.39%   -0.01%     
==========================================
  Files         211      211              
  Lines       20403    20492      +89     
==========================================
+ Hits        11712    11762      +50     
- Misses       7491     7521      +30     
- Partials     1200     1209       +9     
Files with missing lines Coverage Δ
core/inventory.go 83.63% <100.00%> (+0.30%) ⬆️
nodes/sros/power.go 100.00% <100.00%> (ø)
nodes/sros/sros.go 68.57% <81.03%> (-0.59%) ⬇️
nodes/sros/ssh.go 67.56% <67.56%> (ø)

... and 6 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[sros] improved support for nokia_srsim nodes running in classic mode

5 participants