Skip to content
Open
51 changes: 49 additions & 2 deletions core/clab.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,6 @@ func (c *CLab) filterClabNodes(nodeFilter []string) error {
return nil
}

c.nodeFilter = nodeFilter

// ensure that the node filter is a subset of the nodes in the topology
for _, n := range nodeFilter {
if _, ok := c.Config.Topology.Nodes[n]; !ok {
Expand All @@ -192,6 +190,13 @@ func (c *CLab) filterClabNodes(nodeFilter []string) error {
}
}

// Auto-expand the filter to include nodes that share the network namespace
// of a filtered node. Without this, destroying a namespace-owning node (e.g. a CPM)
// would orphan its dependents (e.g. line cards) that use network-mode: container:<node>.
nodeFilter = c.expandFilterWithNSDependents(nodeFilter)

c.nodeFilter = nodeFilter

log.Infof("Applying node filter: %q", nodeFilter)

// filter nodes
Expand All @@ -205,6 +210,48 @@ func (c *CLab) filterClabNodes(nodeFilter []string) error {
return nil
}

// expandFilterWithNSDependents expands the node filter to include nodes that
// depend on a filtered node's network namespace via network-mode: container:<node>.
// This prevents orphaning containers when their namespace provider is destroyed.
func (c *CLab) expandFilterWithNSDependents(nodeFilter []string) []string {
// Build a map of namespace provider -> dependent nodes from the full topology.
nsDependents := map[string][]string{}

for name := range c.Config.Topology.Nodes {
netMode := c.Config.Topology.GetNodeNetworkMode(name)

parts := strings.SplitN(netMode, ":", 2) //nolint: mnd
if parts[0] != "container" || len(parts) < 2 {
continue
}

provider := parts[1]
nsDependents[provider] = append(nsDependents[provider], name)
}

// For each filtered node, pull in its namespace dependents (transitively).
expanded := make([]string, len(nodeFilter))
copy(expanded, nodeFilter)

queue := make([]string, len(nodeFilter))
copy(queue, nodeFilter)

for len(queue) > 0 {
node := queue[0]
queue = queue[1:]

for _, dep := range nsDependents[node] {
if !slices.Contains(expanded, dep) {
log.Infof("Auto-including node %q in filter (shares network namespace of %q)", dep, node)
expanded = append(expanded, dep)
queue = append(queue, dep)
}
}
}

return expanded
}

// initMgmtNetwork sets management network config.
func (c *CLab) initMgmtNetwork() error {
log.Debugf("method initMgmtNetwork was called mgmt params %+v", c.Config.Mgmt)
Expand Down
15 changes: 9 additions & 6 deletions core/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -588,12 +588,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
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 @@ -70,6 +70,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 @@ -173,6 +184,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 @@ -234,6 +250,20 @@ func (c *CLab) destroy(ctx context.Context, maxWorkers uint, keepMgmtNet bool) e

c.deleteNodes(ctx, maxWorkers)

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 @@ -250,14 +280,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 @@ -388,7 +410,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
44 changes: 23 additions & 21 deletions nodes/sros/sros.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,17 +146,17 @@ var (
`^(?:e(?P<card>\d+)-(?:x(?P<xiom>\d+)-)?(?P<mda>\d+)(?:-c(?P<connector>\d+))?-(?P<port>\d+)|eth(?P<mgmtPort>\d+))$`,
)
InterfaceHelp = `The format of the interface name need to be one of:
Regular SR OS interface names, that is:
1/2/3 -> card 1, mda 2, port 3
1/2/c3/4 -> card 1, mda 2, connector 3, port 4
1/x2/3/4 -> card 1, xiom 2, mda 3, port 4
1/x2/3/c4/5 -> card 1, xiom 2, mda 3, connector 4, port 5
The mapped Linux interface names, that is:
e1-2-3 -> card 1, mda 2, port 3
e1-2-c3-4 -> card 1, mda 2, connector 3, port 4
e1-x2-3-4 -> card 1, xiom 2, mda 3, port 4
e1-x2-3-c4-5 -> card 1, xiom 2, mda 3, connector 4, port 5
eth[0-9], for management interfaces of CPM-A/CPM-B or for fabric interfaces`
Regular SR OS interface names, that is:
1/2/3 -> card 1, mda 2, port 3
1/2/c3/4 -> card 1, mda 2, connector 3, port 4
1/x2/3/4 -> card 1, xiom 2, mda 3, port 4
1/x2/3/c4/5 -> card 1, xiom 2, mda 3, connector 4, port 5
The mapped Linux interface names, that is:
e1-2-3 -> card 1, mda 2, port 3
e1-2-c3-4 -> card 1, mda 2, connector 3, port 4
e1-x2-3-4 -> card 1, xiom 2, mda 3, port 4
e1-x2-3-c4-5 -> card 1, xiom 2, mda 3, connector 4, port 5
eth[0-9], for management interfaces of CPM-A/CPM-B or for fabric interfaces`
// Auxiliary regexps for IXR/SAR detection.
sarRegexp = regexp.MustCompile(`(?i)\bsar-`)
sarHmRegexp = regexp.MustCompile(`(?i)\b(sar-hm|sar-hmc)\b`)
Expand Down Expand Up @@ -547,6 +547,7 @@ func (n *sros) DeleteNetnsSymlink() error {

// sortComponents ensure components are in order of
// LCs first, then CPMs (cpm b comes first if present).
// LCs own the network namespace so CPMs can be cycled independently.
func (n *sros) sortComponents() {
slices.SortFunc(n.Cfg.Components, func(a, b *clabtypes.Component) int {
s1 := strings.ToUpper(strings.TrimSpace(a.Slot))
Expand Down Expand Up @@ -1326,13 +1327,14 @@ 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{
counts, err := n.Runtime.ListContainers(ctx, []*clabtypes.GenericFilter{
{
FilterType: "name",
Match: containerName,
Expand All @@ -1343,7 +1345,7 @@ func (n *sros) GetContainers(ctx context.Context) ([]clabruntime.GenericContaine
}
// check that we retrieved some container information
// otherwise throw ErrContainersNotFound error
if len(cnts) == 0 {
if len(counts) == 0 {
return nil, fmt.Errorf("node: %s. %w", n.GetContainerName(),
clabnodes.ErrContainersNotFound)
}
Expand All @@ -1354,17 +1356,17 @@ func (n *sros) GetContainers(ctx context.Context) ([]clabruntime.GenericContaine
ips, err := n.distNodeMgmtIPs()
if err == nil {
if ips.IPv4 != "" {
cnts[0].NetworkSettings.IPv4addr = ips.IPv4
cnts[0].NetworkSettings.IPv4pLen = ips.IPv4pLen
counts[0].NetworkSettings.IPv4addr = ips.IPv4
counts[0].NetworkSettings.IPv4pLen = ips.IPv4pLen
}
if ips.IPv6 != "" {
cnts[0].NetworkSettings.IPv6addr = ips.IPv6
cnts[0].NetworkSettings.IPv6pLen = ips.IPv6pLen
counts[0].NetworkSettings.IPv6addr = ips.IPv6
counts[0].NetworkSettings.IPv6pLen = ips.IPv6pLen
}
}
}

return cnts, err
return counts, err
}

// populateHosts adds container hostnames for other nodes of a lab to SR Linux /etc/hosts file
Expand Down Expand Up @@ -1643,8 +1645,8 @@ func buildTLSProfileXML() string {
// TLS bootstrap via NETCONF to enable secure gRPC.
func (n *sros) tlsCertBootstrap(ctx context.Context, addr string) error {
// Always import PKI key and cert:
// import "cf3:\node.key" in PEM format as "cf3:\system-pki\node.key" (encrypted DER)
// import "cf3:\node.crt" in PEM format as "cf3:\system-pki\node.crt" (encrypted DER)
// import "cf3:\node.key" in PEM format as "cf3:\system-pki\node.key" (encrypted DER)
// import "cf3:\node.crt" in PEM format as "cf3:\system-pki\node.crt" (encrypted DER)
operations := []clabnetconf.Operation{
func(d *netconf.Driver) (*response.NetconfResponse, error) {
return d.RPC(opoptions.WithFilter(buildPKIImportXML(
Expand All @@ -1657,7 +1659,7 @@ func (n *sros) tlsCertBootstrap(ctx context.Context, addr string) error {
}

// Activate cert-profile in MD is via NETCONF, in Classic mode is via SSH
// enable enables cert-profile "clab-grpc-certs" administratively
// enable enables cert-profile "clab-grpc-certs" administratively
cmd := []string{}
if n.isConfigClassic() {
cmd = append(
Expand Down
12 changes: 12 additions & 0 deletions tests/01-smoke/12-node-filter-srl.clab.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name: nf-srl
topology:
nodes:
srl1:
kind: nokia_srlinux
image: ghcr.io/nokia/srlinux
srl2:
kind: nokia_srlinux
image: ghcr.io/nokia/srlinux

links:
- endpoints: ["srl1:e1-1", "srl2:e1-1"]
Loading