Skip to content

Commit b8abdae

Browse files
FloSch62hellt
andauthored
Tool gotty (#2583)
* Move common tools functions to common * Add GoTTY command support * refactor: move GetOwner to utils and rename lab config param (#1) * remove GetOwner function and migrate to utils package * Add gotty tool tests and docs * Add smoke tests for tool gotty * Add WithTopoFromLab function to derive topology path from running lab name * changed lab configuration retrieval to use GetTopology function * refactor getowner * rename to NewContainerlabFromTopologyFileOrLabName for consistency * check for missing topo file * use label constants * remove duplicate import * added doc entry --------- Co-authored-by: hellt <[email protected]>
1 parent a06c526 commit b8abdae

File tree

17 files changed

+1145
-212
lines changed

17 files changed

+1145
-212
lines changed

.github/workflows/smoke-tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ jobs:
4040
- "20*.robot"
4141
- "21*.robot"
4242
- "22*.robot"
43+
- "23*.robot"
4344

4445
# allow podman job to fail, since it started to fail on github actions
4546
continue-on-error: ${{ matrix.runtime == 'podman' }}

clab/clab.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
depMgr "github.com/srl-labs/containerlab/clab/dependency_manager"
2222
"github.com/srl-labs/containerlab/clab/exec"
2323
errs "github.com/srl-labs/containerlab/errors"
24+
clabels "github.com/srl-labs/containerlab/labels"
2425
"github.com/srl-labs/containerlab/links"
2526
"github.com/srl-labs/containerlab/nodes"
2627
"github.com/srl-labs/containerlab/runtime"
@@ -221,6 +222,54 @@ func WithTopoPath(path, varsFile string) ClabOption {
221222
}
222223
}
223224

225+
// WithTopoFromLab loads the topology file path based on a running lab name.
226+
// The lab name is used to look up the container labels of a running lab and
227+
// derive the topology file location. It falls back to WithTopoPath once the
228+
// topology path is discovered.
229+
func WithTopoFromLab(labName string) ClabOption {
230+
return func(c *CLab) error {
231+
if labName == "" {
232+
return fmt.Errorf("lab name is required to derive topology path")
233+
}
234+
235+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
236+
defer cancel()
237+
238+
filter := []*types.GenericFilter{
239+
{
240+
FilterType: "label",
241+
Field: clabels.Containerlab,
242+
Operator: "=",
243+
Match: labName,
244+
},
245+
}
246+
247+
containers, err := c.globalRuntime().ListContainers(ctx, filter)
248+
if err != nil {
249+
return fmt.Errorf("failed to list containers for lab '%s': %w", labName, err)
250+
}
251+
252+
if len(containers) == 0 {
253+
return fmt.Errorf("lab '%s' not found - no running containers", labName)
254+
}
255+
256+
topoFile := containers[0].Labels[clabels.TopoFile]
257+
if topoFile == "" {
258+
return fmt.Errorf("could not determine topology file from container labels")
259+
}
260+
261+
// Verify topology file exists and is accessible
262+
if !utils.FileOrDirExists(topoFile) {
263+
return fmt.Errorf("topology file '%s' referenced by lab '%s' does not exist or is not accessible",
264+
topoFile, labName)
265+
}
266+
267+
log.Debugf("found topology file for lab %s: %s", labName, topoFile)
268+
269+
return WithTopoPath(topoFile, "")(c)
270+
}
271+
}
272+
224273
// ProcessTopoPath takes a topology path, which might be the path to a directory or a file
225274
// or stdin or a URL and returns the topology file name if found.
226275
func (c *CLab) ProcessTopoPath(path string) (string, error) {
@@ -324,6 +373,38 @@ func downloadTopoFile(url, tempDir string) (string, error) {
324373
return tmpFile.Name(), err
325374
}
326375

376+
// NewContainerlabFromTopologyFileOrLabName creates a containerlab instance using either a topology file path
377+
// or a lab name. It returns the initialized CLab structure with the
378+
// topology loaded.
379+
func NewContainerlabFromTopologyFileOrLabName(ctx context.Context, topoPath, labName, varsFile, runtimeName string, debug bool, timeout time.Duration, graceful bool) (*CLab, error) {
380+
if topoPath == "" && labName == "" {
381+
cwd, err := os.Getwd()
382+
if err != nil {
383+
return nil, fmt.Errorf("failed to get current working directory and no topology path or lab name provided: %w", err)
384+
}
385+
topoPath = cwd
386+
}
387+
388+
opts := []ClabOption{
389+
WithTimeout(timeout),
390+
WithRuntime(runtimeName, &runtime.RuntimeConfig{
391+
Debug: debug,
392+
Timeout: timeout,
393+
GracefulShutdown: graceful,
394+
}),
395+
WithDebug(debug),
396+
}
397+
398+
switch {
399+
case topoPath != "":
400+
opts = append(opts, WithTopoPath(topoPath, varsFile))
401+
case labName != "":
402+
opts = append(opts, WithTopoFromLab(labName))
403+
}
404+
405+
return NewContainerLab(opts...)
406+
}
407+
327408
// WithNodeFilter option sets a filter for nodes to be deployed.
328409
// A filter is a list of node names to be deployed,
329410
// names are provided exactly as they are listed in the topology file.

cmd/common/tools.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package common
2+
3+
import (
4+
"path/filepath"
5+
"strings"
6+
7+
clabels "github.com/srl-labs/containerlab/labels"
8+
)
9+
10+
// CreateLabelsMap creates container labels map for additional containers launched after the lab is up. Such as sshx, gotty, etc.
11+
func CreateLabelsMap(labName, containerName, owner, toolType string) map[string]string {
12+
shortName := strings.Replace(containerName, "clab-"+labName+"-", "", 1)
13+
14+
labels := map[string]string{
15+
clabels.Containerlab: labName,
16+
clabels.NodeName: shortName,
17+
clabels.LongName: containerName,
18+
clabels.NodeKind: "linux",
19+
clabels.NodeGroup: "",
20+
clabels.NodeType: "tool",
21+
clabels.ToolType: toolType,
22+
}
23+
24+
// Add topology file path
25+
if Topo != "" {
26+
absPath, err := filepath.Abs(Topo)
27+
if err == nil {
28+
labels[clabels.TopoFile] = absPath
29+
} else {
30+
labels[clabels.TopoFile] = Topo
31+
}
32+
33+
// Set node lab directory
34+
baseDir := filepath.Dir(Topo)
35+
labels[clabels.NodeLabDir] = filepath.Join(baseDir, "clab-"+labName, shortName)
36+
}
37+
38+
// Add owner label if available
39+
if owner != "" {
40+
labels[clabels.Owner] = owner
41+
}
42+
43+
return labels
44+
}

cmd/destroy.go

Lines changed: 45 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -167,15 +167,11 @@ func destroyFn(_ *cobra.Command, _ []string) error {
167167
}
168168
}
169169

170-
if len(errs) != 0 {
171-
return fmt.Errorf("error(s) occurred during the deletion. Check log messages")
172-
}
173-
174-
// Additional step: Remove any SSHX containers associated with destroyed labs
170+
// Remove any tool containers associated with destroyed labs
175171
if !all && len(labs) > 0 {
176172
// Get the lab name from the first lab
177173
labName := labs[0].Config.Name
178-
log.Debugf("Looking for SSHX containers with lab name: %s", labName)
174+
log.Debugf("Looking for tool containers with lab name: %s", labName)
179175

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

188184
if err == nil {
189-
// Look for SSHX containers with matching lab label
190-
sshxFilter := []*types.GenericFilter{
191-
{
192-
FilterType: "label",
193-
Field: "tool-type",
194-
Operator: "=",
195-
Match: "sshx",
196-
},
197-
{
198-
FilterType: "label",
199-
Field: labels.Containerlab,
200-
Operator: "=",
201-
Match: labName,
202-
},
203-
}
204-
205-
sshxContainers, err := rt.ListContainers(ctx, sshxFilter)
206-
if err == nil && len(sshxContainers) > 0 {
207-
log.Infof("Found %d SSHX containers associated with lab %s", len(sshxContainers), labName)
208-
for _, container := range sshxContainers {
209-
containerName := strings.TrimPrefix(container.Names[0], "/")
210-
log.Infof("Removing SSHX container: %s", containerName)
211-
if err := rt.DeleteContainer(ctx, containerName); err != nil {
212-
log.Warnf("Failed to remove SSHX container %s: %v", containerName, err)
213-
} else {
214-
log.Infof("SSHX container %s removed successfully", containerName)
215-
}
216-
}
217-
}
185+
// Clean up any tool containers (SSHX, GoTTY, etc.)
186+
removeToolContainers(ctx, rt, labName, "sshx")
187+
removeToolContainers(ctx, rt, labName, "gotty")
218188
}
219189
}
220190
}
@@ -226,6 +196,46 @@ func destroyFn(_ *cobra.Command, _ []string) error {
226196
return nil
227197
}
228198

199+
// removeToolContainers removes containers of a specific tool type associated with a lab
200+
func removeToolContainers(ctx context.Context, rt runtime.ContainerRuntime, labName, toolType string) {
201+
toolFilter := []*types.GenericFilter{
202+
{
203+
FilterType: "label",
204+
Field: labels.ToolType,
205+
Operator: "=",
206+
Match: toolType,
207+
},
208+
{
209+
FilterType: "label",
210+
Field: labels.Containerlab,
211+
Operator: "=",
212+
Match: labName,
213+
},
214+
}
215+
216+
containers, err := rt.ListContainers(ctx, toolFilter)
217+
if err != nil {
218+
log.Warnf("Failed to list %s containers: %v", toolType, err)
219+
return
220+
}
221+
222+
if len(containers) == 0 {
223+
log.Debugf("No %s containers found for lab %s", toolType, labName)
224+
return
225+
}
226+
227+
log.Infof("Found %d %s containers associated with lab %s", len(containers), toolType, labName)
228+
for _, container := range containers {
229+
containerName := strings.TrimPrefix(container.Names[0], "/")
230+
log.Infof("Removing %s container: %s", toolType, containerName)
231+
if err := rt.DeleteContainer(ctx, containerName); err != nil {
232+
log.Warnf("Failed to remove %s container %s: %v", toolType, containerName, err)
233+
} else {
234+
log.Infof("%s container %s removed successfully", toolType, containerName)
235+
}
236+
}
237+
}
238+
229239
func destroyLab(ctx context.Context, c *clab.CLab) (err error) {
230240
return c.Destroy(ctx, maxWorkers, keepMgmtNet)
231241
}

0 commit comments

Comments
 (0)