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
1 change: 1 addition & 0 deletions .github/workflows/smoke-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ jobs:
- "20*.robot"
- "21*.robot"
- "22*.robot"
- "23*.robot"

# allow podman job to fail, since it started to fail on github actions
continue-on-error: ${{ matrix.runtime == 'podman' }}
Expand Down
81 changes: 81 additions & 0 deletions clab/clab.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
depMgr "github.com/srl-labs/containerlab/clab/dependency_manager"
"github.com/srl-labs/containerlab/clab/exec"
errs "github.com/srl-labs/containerlab/errors"
clabels "github.com/srl-labs/containerlab/labels"
"github.com/srl-labs/containerlab/links"
"github.com/srl-labs/containerlab/nodes"
"github.com/srl-labs/containerlab/runtime"
Expand Down Expand Up @@ -221,6 +222,54 @@ func WithTopoPath(path, varsFile string) ClabOption {
}
}

// WithTopoFromLab loads the topology file path based on a running lab name.
// The lab name is used to look up the container labels of a running lab and
// derive the topology file location. It falls back to WithTopoPath once the
// topology path is discovered.
func WithTopoFromLab(labName string) ClabOption {
return func(c *CLab) error {
if labName == "" {
return fmt.Errorf("lab name is required to derive topology path")
}

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

filter := []*types.GenericFilter{
{
FilterType: "label",
Field: clabels.Containerlab,
Operator: "=",
Match: labName,
},
}

containers, err := c.globalRuntime().ListContainers(ctx, filter)
if err != nil {
return fmt.Errorf("failed to list containers for lab '%s': %w", labName, err)
}

if len(containers) == 0 {
return fmt.Errorf("lab '%s' not found - no running containers", labName)
}

topoFile := containers[0].Labels[clabels.TopoFile]
if topoFile == "" {
return fmt.Errorf("could not determine topology file from container labels")
}

// Verify topology file exists and is accessible
if !utils.FileOrDirExists(topoFile) {
return fmt.Errorf("topology file '%s' referenced by lab '%s' does not exist or is not accessible",
topoFile, labName)
}

log.Debugf("found topology file for lab %s: %s", labName, topoFile)

return WithTopoPath(topoFile, "")(c)
}
}

// ProcessTopoPath takes a topology path, which might be the path to a directory or a file
// or stdin or a URL and returns the topology file name if found.
func (c *CLab) ProcessTopoPath(path string) (string, error) {
Expand Down Expand Up @@ -324,6 +373,38 @@ func downloadTopoFile(url, tempDir string) (string, error) {
return tmpFile.Name(), err
}

// NewContainerlabFromTopologyFileOrLabName creates a containerlab instance using either a topology file path
// or a lab name. It returns the initialized CLab structure with the
// topology loaded.
func NewContainerlabFromTopologyFileOrLabName(ctx context.Context, topoPath, labName, varsFile, runtimeName string, debug bool, timeout time.Duration, graceful bool) (*CLab, error) {
if topoPath == "" && labName == "" {
cwd, err := os.Getwd()
if err != nil {
return nil, fmt.Errorf("failed to get current working directory and no topology path or lab name provided: %w", err)
}
topoPath = cwd
}

opts := []ClabOption{
WithTimeout(timeout),
WithRuntime(runtimeName, &runtime.RuntimeConfig{
Debug: debug,
Timeout: timeout,
GracefulShutdown: graceful,
}),
WithDebug(debug),
}

switch {
case topoPath != "":
opts = append(opts, WithTopoPath(topoPath, varsFile))
case labName != "":
opts = append(opts, WithTopoFromLab(labName))
}

return NewContainerLab(opts...)
}

// WithNodeFilter option sets a filter for nodes to be deployed.
// A filter is a list of node names to be deployed,
// names are provided exactly as they are listed in the topology file.
Expand Down
44 changes: 44 additions & 0 deletions cmd/common/tools.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package common

import (
"path/filepath"
"strings"

clabels "github.com/srl-labs/containerlab/labels"
)

// CreateLabelsMap creates container labels map for additional containers launched after the lab is up. Such as sshx, gotty, etc.
func CreateLabelsMap(labName, containerName, owner, toolType string) map[string]string {
shortName := strings.Replace(containerName, "clab-"+labName+"-", "", 1)

labels := map[string]string{
clabels.Containerlab: labName,
clabels.NodeName: shortName,
clabels.LongName: containerName,
clabels.NodeKind: "linux",
clabels.NodeGroup: "",
clabels.NodeType: "tool",
clabels.ToolType: toolType,
}

// Add topology file path
if Topo != "" {
absPath, err := filepath.Abs(Topo)
if err == nil {
labels[clabels.TopoFile] = absPath
} else {
labels[clabels.TopoFile] = Topo
}

// Set node lab directory
baseDir := filepath.Dir(Topo)
labels[clabels.NodeLabDir] = filepath.Join(baseDir, "clab-"+labName, shortName)
}

// Add owner label if available
if owner != "" {
labels[clabels.Owner] = owner
}

return labels
}
80 changes: 45 additions & 35 deletions cmd/destroy.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,15 +167,11 @@ func destroyFn(_ *cobra.Command, _ []string) error {
}
}

if len(errs) != 0 {
return fmt.Errorf("error(s) occurred during the deletion. Check log messages")
}

// Additional step: Remove any SSHX containers associated with destroyed labs
// Remove any tool containers associated with destroyed labs
if !all && len(labs) > 0 {
// Get the lab name from the first lab
labName := labs[0].Config.Name
log.Debugf("Looking for SSHX containers with lab name: %s", labName)
log.Debugf("Looking for tool containers with lab name: %s", labName)

// Initialize runtime
_, rinit, err := clab.RuntimeInitializer(common.Runtime)
Expand All @@ -186,35 +182,9 @@ func destroyFn(_ *cobra.Command, _ []string) error {
}))

if err == nil {
// Look for SSHX containers with matching lab label
sshxFilter := []*types.GenericFilter{
{
FilterType: "label",
Field: "tool-type",
Operator: "=",
Match: "sshx",
},
{
FilterType: "label",
Field: labels.Containerlab,
Operator: "=",
Match: labName,
},
}

sshxContainers, err := rt.ListContainers(ctx, sshxFilter)
if err == nil && len(sshxContainers) > 0 {
log.Infof("Found %d SSHX containers associated with lab %s", len(sshxContainers), labName)
for _, container := range sshxContainers {
containerName := strings.TrimPrefix(container.Names[0], "/")
log.Infof("Removing SSHX container: %s", containerName)
if err := rt.DeleteContainer(ctx, containerName); err != nil {
log.Warnf("Failed to remove SSHX container %s: %v", containerName, err)
} else {
log.Infof("SSHX container %s removed successfully", containerName)
}
}
}
// Clean up any tool containers (SSHX, GoTTY, etc.)
removeToolContainers(ctx, rt, labName, "sshx")
removeToolContainers(ctx, rt, labName, "gotty")
}
}
}
Expand All @@ -226,6 +196,46 @@ func destroyFn(_ *cobra.Command, _ []string) error {
return nil
}

// removeToolContainers removes containers of a specific tool type associated with a lab
func removeToolContainers(ctx context.Context, rt runtime.ContainerRuntime, labName, toolType string) {
toolFilter := []*types.GenericFilter{
{
FilterType: "label",
Field: labels.ToolType,
Operator: "=",
Match: toolType,
},
{
FilterType: "label",
Field: labels.Containerlab,
Operator: "=",
Match: labName,
},
}

containers, err := rt.ListContainers(ctx, toolFilter)
if err != nil {
log.Warnf("Failed to list %s containers: %v", toolType, err)
return
}

if len(containers) == 0 {
log.Debugf("No %s containers found for lab %s", toolType, labName)
return
}

log.Infof("Found %d %s containers associated with lab %s", len(containers), toolType, labName)
for _, container := range containers {
containerName := strings.TrimPrefix(container.Names[0], "/")
log.Infof("Removing %s container: %s", toolType, containerName)
if err := rt.DeleteContainer(ctx, containerName); err != nil {
log.Warnf("Failed to remove %s container %s: %v", toolType, containerName, err)
} else {
log.Infof("%s container %s removed successfully", toolType, containerName)
}
}
}

func destroyLab(ctx context.Context, c *clab.CLab) (err error) {
return c.Destroy(ctx, maxWorkers, keepMgmtNet)
}
Expand Down
Loading
Loading