Skip to content
Open
2 changes: 1 addition & 1 deletion cmd/tools_netem.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ import (
"github.com/spf13/cobra"
clabconstants "github.com/srl-labs/containerlab/constants"
clabcore "github.com/srl-labs/containerlab/core"
clabnetem "github.com/srl-labs/containerlab/netem"
clablinks "github.com/srl-labs/containerlab/links"
clabnetem "github.com/srl-labs/containerlab/netem"
clabruntime "github.com/srl-labs/containerlab/runtime"
clabtypes "github.com/srl-labs/containerlab/types"
clabutils "github.com/srl-labs/containerlab/utils"
Expand Down
36 changes: 23 additions & 13 deletions core/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const (
nodeDirVar = "__clabNodeDir__"
nodeNameVar = "__clabNodeName__"

// clab name specific variables
// clab name specific variables.
gitBranchVar = "__gitBranch__"
gitHashVar = "__gitHash__"
)
Expand All @@ -62,11 +62,16 @@ type Config struct {
func (c *CLab) parseTopology() error {
log.Info("Parsing & checking topology", "file", c.TopoPaths.TopologyFilenameBase())

if strings.Contains(c.Config.Name, gitBranchVar) || strings.Contains(c.Config.Name, gitHashVar) {
if strings.Contains(c.Config.Name, gitBranchVar) ||
strings.Contains(c.Config.Name, gitHashVar) {
r := c.magicTopoNameReplacer()
oldName := c.Config.Name
c.Config.Name = r.Replace(c.Config.Name)
log.Debugf("Topology name contains Git variables, substituted topology name: %q -> %q", oldName, c.Config.Name)
log.Debugf(
"Topology name contains Git variables, substituted topology name: %q -> %q",
oldName,
c.Config.Name,
)
}

err := c.TopoPaths.SetLabDirByPrefix(c.Config.Name)
Expand Down Expand Up @@ -585,12 +590,15 @@ func (c *CLab) verifyContainersUniqueness(ctx context.Context) error {
// check that none of the existing containers has a label that matches
// the lab name of a currently deploying lab
// this ensures lab uniqueness
for idx := range containers {
if containers[idx].Labels[clabconstants.Containerlab] == c.Config.Name {
return fmt.Errorf(
"the '%s' lab has already been deployed. Destroy the lab before deploying a "+
"lab with the same name", c.Config.Name,
)
// Skip this check when node-filter is used for a subset deployment
if len(c.nodeFilter) == 0 {
for idx := range containers {
if containers[idx].Labels[clabconstants.Containerlab] == c.Config.Name {
return fmt.Errorf(
"the '%s' lab has already been deployed. Destroy the lab before deploying a "+
"lab with the same name", c.Config.Name,
)
}
}
}

Expand Down Expand Up @@ -688,7 +696,6 @@ func (c *CLab) addDefaultLabels(cfg *clabtypes.NodeConfig) {
cfg.Labels[clabconstants.GitHash] = gitHash
}
}

}

// labelsToEnvVars adds labels to env vars with CLAB_LABEL_ prefix added
Expand Down Expand Up @@ -802,13 +809,16 @@ func (c *CLab) magicVarReplacer(nodeName string) *strings.Replacer {
)
}

// magicTopoNameReplacer returns a string replacer that replaces all git branch variables in the topology name.
// magicTopoNameReplacer returns a string replacer that replaces all git branch variables in the
// topology name.
func (c *CLab) magicTopoNameReplacer() *strings.Replacer {

gitBranch, gitHash := c.getGitInfo()

if gitHash == "none" && gitBranch == "none" {
log.Warnf("topology name uses git variables, but no Git repository found at %q - variables will be replaced with 'none'", c.TopoPaths.TopologyFileDir())
log.Warnf(
"topology name uses git variables, but no Git repository found at %q - variables will be replaced with 'none'",
c.TopoPaths.TopologyFileDir(),
)
}

// Replace illegal characters in branch name
Expand Down
8 changes: 5 additions & 3 deletions core/config_git_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (
"github.com/stretchr/testify/require"
)

// setupTestGitRepo creates a temporary Git repository for testing
// setupTestGitRepo creates a temporary Git repository for testing.
func setupTestGitRepo(t *testing.T, branchName string) (string, string, func()) {
t.Helper()

Expand Down Expand Up @@ -294,7 +294,8 @@ func TestParseTopology_GitVariableSubstitution(t *testing.T) {
require.NoError(t, err)

// Simulate the check in parseTopology() that triggers substitution
if strings.Contains(c.Config.Name, gitBranchVar) || strings.Contains(c.Config.Name, gitHashVar) {
if strings.Contains(c.Config.Name, gitBranchVar) ||
strings.Contains(c.Config.Name, gitHashVar) {
r := c.magicTopoNameReplacer()
oldName := c.Config.Name
c.Config.Name = r.Replace(c.Config.Name)
Expand Down Expand Up @@ -500,7 +501,8 @@ func TestParseTopology_GitVariableInName(t *testing.T) {
require.NoError(t, err)

// Simulate the check in parseTopology
if strings.Contains(c.Config.Name, gitBranchVar) || strings.Contains(c.Config.Name, gitHashVar) {
if strings.Contains(c.Config.Name, gitBranchVar) ||
strings.Contains(c.Config.Name, gitHashVar) {
r := c.magicTopoNameReplacer()
oldName := c.Config.Name
c.Config.Name = r.Replace(c.Config.Name)
Expand Down
19 changes: 16 additions & 3 deletions core/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,23 @@ func (c *CLab) Deploy( //nolint: funlen

if options.reconfigure {
_ = c.destroy(ctx, uint(len(c.Nodes)), true)
log.Info("Removing directory", "path", c.TopoPaths.TopologyLabDir())

if err := os.RemoveAll(c.TopoPaths.TopologyLabDir()); err != nil {
return nil, err
if len(c.nodeFilter) > 0 {
// When node-filter is used, only remove the filtered nodes' directories
for _, node := range c.Nodes {
nodeDir := node.Config().LabDir
log.Info("Removing node directory", "path", nodeDir)

if err := os.RemoveAll(nodeDir); err != nil {
return nil, err
}
}
} else {
log.Info("Removing directory", "path", c.TopoPaths.TopologyLabDir())

if err := os.RemoveAll(c.TopoPaths.TopologyLabDir()); err != nil {
return nil, err
}
}
}

Expand Down
40 changes: 31 additions & 9 deletions core/destroy.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,17 @@ func (c *CLab) Destroy(ctx context.Context, options ...DestroyOption) (err error
}
}()

// If no containers found but we have a topology file provided via CLI,
// use that topology file directly. This handles cases where:
// - Containers were already removed
// - Containers don't have containerlab labels (e.g. node, kind)
// - Using --node-filter for nodes that never got deployed
if len(topos) == 0 && c.TopoPaths.TopologyFilenameAbsPath() != "" {
log.Debug("No containers with topology labels found, using topology file from CLI",
"path", c.TopoPaths.TopologyFilenameAbsPath())
topos[c.TopoPaths.TopologyFilenameAbsPath()] = c.TopoPaths.TopologyLabDir()
}

if len(topos) == 0 {
return nil
}
Expand Down Expand Up @@ -174,6 +185,11 @@ func (c *CLab) makeCopyForDestroy(
}

func (c *CLab) destroyLabDirs(topos map[string]string, all bool) error {
// When node-filter is active, never remove the entire lab directory
if len(c.nodeFilter) > 0 {
return nil
}

if len(topos) == 0 {
log.Info("no containerlab containers found")

Expand Down Expand Up @@ -251,6 +267,20 @@ func (c *CLab) destroy(ctx context.Context, maxWorkers uint, keepMgmtNet bool) e

c.deleteNodes(ctx, maxWorkers, serialNodes)

for _, node := range c.Nodes {
err = node.DeleteNetnsSymlink()
if err != nil {
return fmt.Errorf("error while deleting netns symlinks: %w", err)
}
}

// When node-filter is used, skip lab-wide cleanup operations
// because other nodes from the same lab are still running and
// depend on these shared resources.
if len(c.nodeFilter) > 0 {
return nil
}

c.deleteToolContainers(ctx)

log.Info("Removing host entries", "path", "/etc/hosts")
Expand All @@ -267,14 +297,6 @@ func (c *CLab) destroy(ctx context.Context, maxWorkers uint, keepMgmtNet bool) e
log.Errorf("failed to remove ssh config file: %v", err)
}

// delete container network namespaces symlinks
for _, node := range c.Nodes {
err = node.DeleteNetnsSymlink()
if err != nil {
return fmt.Errorf("error while deleting netns symlinks: %w", err)
}
}

// delete lab management network
if c.Config.Mgmt.Network != "bridge" && !keepMgmtNet {
log.Debugf("Calling DeleteNet method. *CLab.Config.Mgmt value is: %+v", c.Config.Mgmt)
Expand Down Expand Up @@ -419,7 +441,7 @@ func cliPromptToDestroyAll(topos map[string]string) error {
idx := 1

for topo, labDir := range topos {
sb.WriteString(fmt.Sprintf(" %d. Topology: %s\n Lab Dir: %s\n", idx, topo, labDir))
fmt.Fprintf(&sb, " %d. Topology: %s\n Lab Dir: %s\n", idx, topo, labDir)
idx++
}

Expand Down
7 changes: 6 additions & 1 deletion core/events/netlink.go
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,12 @@ func interfaceAttributes(
attributes["netem_rate"] = strconv.Itoa(snapshot.Rate) + "kbit"
}
if snapshot.Corruption != 0 {
attributes["netem_corruption"] = strconv.FormatFloat(snapshot.Corruption, 'f', 2, 64) + "%"
attributes["netem_corruption"] = strconv.FormatFloat(
snapshot.Corruption,
'f',
2,
64,
) + "%"
}
}

Expand Down
2 changes: 1 addition & 1 deletion nodes/cisco_vios/cisco-vios.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ func (n *vios) genBootConfig() error {
return nil
}

// Stores the vars exposed in the config template
// Stores the vars exposed in the config template.
type ViosTemplateData struct {
Hostname string
Username string
Expand Down
46 changes: 31 additions & 15 deletions nodes/sros/sros.go
Original file line number Diff line number Diff line change
Expand Up @@ -1342,45 +1342,61 @@ func (n *sros) GetContainers(ctx context.Context) ([]clabruntime.GenericContaine
return n.DefaultNode.GetContainers(ctx)
}

// For distributed base nodes, find all component containers (CPM + line cards)
cpmSlot, err := n.cpmSlot()
if err != nil {
return nil, err
}
containerName := n.calcComponentName(n.GetContainerName(), cpmSlot)

cnts, err := n.Runtime.ListContainers(ctx, []*clabtypes.GenericFilter{
{
FilterType: "name",
Match: containerName,
},
})
if err != nil {
return nil, err
var allContainers []clabruntime.GenericContainer
cpmIdx := -1

for _, comp := range n.Cfg.Components {
containerName := n.calcComponentName(n.GetContainerName(), comp.Slot)
cnts, err := n.Runtime.ListContainers(ctx, []*clabtypes.GenericFilter{
{
FilterType: "name",
Match: containerName,
},
})
if err != nil {
return nil, err
}
if comp.Slot == cpmSlot && len(cnts) > 0 {
cpmIdx = len(allContainers)
}
allContainers = append(allContainers, cnts...)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@sacckth @FloSch62 can you check this out? Thanks

}

// check that we retrieved some container information
// otherwise throw ErrContainersNotFound error
if len(cnts) == 0 {
if len(allContainers) == 0 {
return nil, fmt.Errorf("node: %s. %w", n.GetContainerName(),
clabnodes.ErrContainersNotFound)
}

// Put CPM container first — callers expect the primary container at index 0.
if cpmIdx > 0 {
allContainers[0], allContainers[cpmIdx] = allContainers[cpmIdx], allContainers[0]
}

// Forge the IP address to be the actual IP of mgmt
// because the CPM A might not own the netns & mgmt IP
if len(n.Cfg.Components) > 0 {
ips, err := n.distNodeMgmtIPs()
if err == nil {
if ips.IPv4 != "" {
cnts[0].NetworkSettings.IPv4addr = ips.IPv4
cnts[0].NetworkSettings.IPv4pLen = ips.IPv4pLen
allContainers[0].NetworkSettings.IPv4addr = ips.IPv4
allContainers[0].NetworkSettings.IPv4pLen = ips.IPv4pLen
}
if ips.IPv6 != "" {
cnts[0].NetworkSettings.IPv6addr = ips.IPv6
cnts[0].NetworkSettings.IPv6pLen = ips.IPv6pLen
allContainers[0].NetworkSettings.IPv6addr = ips.IPv6
allContainers[0].NetworkSettings.IPv6pLen = ips.IPv6pLen
}
}
}

return cnts, err
return allContainers, nil
}

// populateHosts adds container hostnames for other nodes of a lab to SR Linux /etc/hosts file
Expand Down
8 changes: 7 additions & 1 deletion runtime/docker/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -1417,7 +1417,13 @@ func (d *DockerRuntime) CopyToContainer(
log.Debugf("copying path %v -> %v to container %v", srcPath, dstPath, cID)
err = d.Client.CopyToContainer(ctx, cID, filepath.Dir(dstPath), tarBuf, opts)
if err != nil {
return fmt.Errorf("error copying path %v -> %v to container (%v): %w", srcPath, dstPath, cID, err)
return fmt.Errorf(
"error copying path %v -> %v to container (%v): %w",
srcPath,
dstPath,
cID,
err,
)
}

return nil
Expand Down
8 changes: 7 additions & 1 deletion runtime/podman/podman.go
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,13 @@ func (p *PodmanRuntime) CopyToContainer(
log.Debugf("copying path %v -> %v to container %v", srcPath, dstPath, cID)
_, err = containers.CopyFromArchiveWithOptions(ctx, cID, filepath.Dir(dstPath), tarBuf, opts)
if err != nil {
return fmt.Errorf("error copying path %v -> %v to container (%v): %w", srcPath, dstPath, cID, err)
return fmt.Errorf(
"error copying path %v -> %v to container (%v): %w",
srcPath,
dstPath,
cID,
err,
)
}

return nil
Expand Down
3 changes: 2 additions & 1 deletion runtime/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ type ContainerRuntime interface {
) (<-chan ContainerEvent, <-chan error, error)
// InspectImage returns detailed information about a container image
InspectImage(ctx context.Context, imageName string) (*ImageInspect, error)
// CopyToContainer copies the contents of the given host path into the named container's destination path.
// CopyToContainer copies the contents of the given host path into the named container's
// destination path.
// The path must be a file, and the the destination directory must exist inside the container
CopyToContainer(ctx context.Context, cID string, dstPath string, srcPath string) error
}
Expand Down
3 changes: 2 additions & 1 deletion utils/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -659,7 +659,8 @@ func GetOSRelease() string {
return osRelease
}

// IsPartialConfigFile returns true if the config file name contains .partial substring (case insensitive).
// IsPartialConfigFile returns true if the config file name contains .partial substring (case
// insensitive).
func IsPartialConfigFile(configPath string) bool {
return strings.Contains(strings.ToUpper(configPath), ".PARTIAL")
}
Expand Down
Loading