Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Query hosts dns servers and populate nodes config with it #1650

Merged
merged 7 commits into from
Oct 18, 2023
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
41 changes: 41 additions & 0 deletions clab/clab.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"context"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
Expand All @@ -25,6 +26,7 @@ import (
"github.com/srl-labs/containerlab/runtime/docker"
"github.com/srl-labs/containerlab/runtime/ignite"
"github.com/srl-labs/containerlab/types"
"github.com/srl-labs/containerlab/utils"
"golang.org/x/crypto/ssh"
"golang.org/x/exp/slices"
)
Expand Down Expand Up @@ -283,6 +285,14 @@ func NewContainerLab(opts ...ClabOption) (*CLab, error) {
if c.TopoPaths.TopologyFileIsSet() {
err = c.parseTopology()
}

// Extract the host systems DNS servers and populate the
// Nodes DNS Config with these if not specifically provided
fileSystem := os.DirFS("/")
if err := c.ExtractDNSServers(fileSystem); err != nil {
return nil, err
}

return c, err
}

Expand Down Expand Up @@ -760,3 +770,34 @@ func (c *CLab) ResolveLinks() error {

return nil
}

// ExtractDNSServers extracts DNS servers from the resolv.conf files
// and populates the Nodes DNS Config with these if not specifically provided.
func (c *CLab) ExtractDNSServers(filesys fs.FS) error {
// extract DNS servers from the relevant resolv.conf files
DNSServers, err := utils.ExtractDNSServersFromResolvConf(filesys,
[]string{"etc/resolv.conf", "run/systemd/resolve/resolv.conf"})
if err != nil {
return err
}

// no DNS Servers found, return
if len(DNSServers) == 0 {
return nil
}

// if no dns servers are explicitly configured,
// we set the DNS servers that we've extracted.
for _, n := range c.Nodes {
config := n.Config()
if config.DNS == nil {
config.DNS = &types.DNSConfig{}
}

if n.Config().DNS.Servers == nil {
n.Config().DNS.Servers = DNSServers
}
}

return nil
}
31 changes: 30 additions & 1 deletion docs/manual/kinds/srl.md
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ banner cli config.json devices tls ztp

The topology file that defines the emulated hardware type is driven by the value of the kinds `type` parameter. Depending on a specified `type`, the appropriate content will be populated into the `topology.yml` file that will get mounted to `/tmp/topology.yml` directory inside the container in `ro` mode.

#### authorized keys
#### Authorized keys

Additionally, containerlab will mount the `authorized_keys` file that will have contents of every public key found in `~/.ssh` directory as well as the contents of a `~/.ssh/authorized_keys` file if it exists[^2]. This file will be mounted to `~/.ssh/authorized_keys` path for the following users:

Expand All @@ -359,6 +359,35 @@ Additionally, containerlab will mount the `authorized_keys` file that will have

This will enable passwordless access for the users above if any public key is found in the user's directory.

#### DNS configuration

SR Linux's management stack lives in a separate network namespace `srbase-mgmt`. Due to this fact, the DNS resolver provided by Docker in the root network namespace is not available to the SR Linux management stack.

To enable DNS resolution for SR Linux, containerlab will extract the DNS servers configured on the host system from

* `/etc/resolv.conf`
* `run/systemd/resolve/resolv.conf`

files and configure IP addresses found there as DNS servers in the management network instance of SR Linux:

```srl
--{ running }--[ ]--
A:srl# info system dns
system {
dns {
network-instance mgmt
server-list [
# these servers were extracted from the host
# and provisioned by containerlab
10.171.10.1
10.171.10.2
]
}
}
```

If you wish to turn off the automatic DNS provisioning, set the `servers` list to an empty value in the [node configuration](../nodes.md#dns).

## Host Requirements

SR Linux is a containerized NOS, therefore it depends on the host's kernel and CPU. It is recommended to run a kernel v4 and newer, though it might also run on the older kernels.
Expand Down
18 changes: 12 additions & 6 deletions nodes/srl/srl.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ set / system gnmi-server admin-state enable network-instance mgmt admin-state en
set / system gnmi-server rate-limit 65000
set / system gnmi-server trace-options [ request response common ]
set / system gnmi-server unix-socket admin-state enable
{{- if .DNSServers }}
set / system dns network-instance mgmt
set / system dns server-list [ {{ range $dnsserver := .DNSServers}}{{$dnsserver}} {{ end }}]
{{- end }}
set / system json-rpc-server admin-state enable network-instance mgmt http admin-state enable
set / system json-rpc-server admin-state enable network-instance mgmt https admin-state enable tls-profile clab-profile
set / system snmp community public
Expand Down Expand Up @@ -553,6 +557,7 @@ type srlTemplateData struct {
IFaces map[string]tplIFace
SSHPubKeys string
MgmtMTU int
DNSServers []string
}

// tplIFace template interface struct.
Expand All @@ -573,12 +578,13 @@ func (n *srl) addDefaultConfig(ctx context.Context) error {
// struct that holds data used in templating of the default config snippet

tplData := srlTemplateData{
TLSKey: n.Cfg.TLSKey,
TLSCert: n.Cfg.TLSCert,
TLSAnchor: n.Cfg.TLSAnchor,
Banner: b,
IFaces: map[string]tplIFace{},
MgmtMTU: 0,
TLSKey: n.Cfg.TLSKey,
TLSCert: n.Cfg.TLSCert,
TLSAnchor: n.Cfg.TLSAnchor,
Banner: b,
IFaces: map[string]tplIFace{},
MgmtMTU: 0,
DNSServers: n.Config().DNS.Servers,
}

n.filterSSHPubKeys()
Expand Down
58 changes: 58 additions & 0 deletions utils/resolve.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package utils

import (
"bufio"
"io/fs"
"net"
"strings"

log "github.com/sirupsen/logrus"
)

// ExtractDNSServersFromResolvConf extracts IP addresses
// of the DNS servers from the resolv.conf-formatted files passed in filenames list.
// Returns a list of IP addresses of the DNS servers.
func ExtractDNSServersFromResolvConf(filesys fs.FS, filenames []string) ([]string, error) {
DNSServersMap := map[string]struct{}{}

for _, filename := range filenames {
readFile, err := filesys.Open(filename)
if err != nil {
log.Debugf("Error opening host DNS config %s: %v", filename, err)
continue
}

fileScanner := bufio.NewScanner(readFile)
fileScanner.Split(bufio.ScanLines)

// check line by line for a match
for fileScanner.Scan() {
line := strings.TrimSpace(fileScanner.Text())
if strings.HasPrefix(line, "nameserver") {
fields := strings.Fields(line)
if len(fields) != 2 {
continue
}

ip := net.ParseIP(fields[1])
if ip == nil || ip.IsLoopback() {
continue
}

DNSServersMap[ip.String()] = struct{}{}
}
}

readFile.Close()
}

if len(DNSServersMap) == 0 {
return nil, nil
}

DNSServers := make([]string, 0, len(DNSServersMap))
for k := range DNSServersMap {
DNSServers = append(DNSServers, k)
}
return DNSServers, nil
}
149 changes: 149 additions & 0 deletions utils/resolve_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package utils

import (
"io/fs"
"strings"
"testing"
"testing/fstest"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
)

func TestExtractDNSServersFromResolvConf(t *testing.T) {
type args struct {
filesys fs.FS
filenames []string
}
tests := []struct {
name string
args args
want []string
wantErr bool
}{
{
name: "One file local dns empty result",
args: args{
filesys: fstest.MapFS{
"etc/resolv.conf": &fstest.MapFile{
Data: []byte(
`
# This is /run/systemd/resolve/stub-resolv.conf managed by man:systemd-resolved(8).
# Do not edit.
#
# This file might be symlinked as /etc/resolv.conf. If you're looking at
# /etc/resolv.conf and seeing this text, you have followed the symlink.

nameserver 127.0.0.53
options edns0 trust-ad
search .
`,
),
},
},
filenames: []string{"etc/resolv.conf"},
},
want: nil,
wantErr: false,
},
{
name: "Two files local dns and two remote, two results",
args: args{
filesys: fstest.MapFS{
"etc/resolv.conf": &fstest.MapFile{
Data: []byte(
`
# This is /run/systemd/resolve/stub-resolv.conf managed by man:systemd-resolved(8).
# Do not edit.
#
# This file might be symlinked as /etc/resolv.conf. If you're looking at
# /etc/resolv.conf and seeing this text, you have followed the symlink.

nameserver 1.1.1.1
options edns0 trust-ad
search .
`,
),
},
"etc/someother/resolv.conf": &fstest.MapFile{
Data: []byte(
`
# This is /run/systemd/resolve/stub-resolv.conf managed by man:systemd-resolved(8).
# Do not edit.

nameserver 127.0.0.53
nameserver 8.8.8.8
options edns0 trust-ad
search .
`,
),
},
},
filenames: []string{"etc/resolv.conf", "etc/someother/resolv.conf"},
},
want: []string{"1.1.1.1", "8.8.8.8"},
wantErr: false,
},
{
name: "Duplicat 8.8.8.8",
args: args{
filesys: fstest.MapFS{
"etc/resolv.conf": &fstest.MapFile{
Data: []byte(
`
# Do not edit.
nameserver 1.1.1.1
nameserver 8.8.8.8

options edns0 trust-ad
search .
`,
),
},
"etc/someother/resolv.conf": &fstest.MapFile{
Data: []byte(
`
nameserver 8.8.8.8
options edns0 trust-ad
search .
`,
),
},
},
filenames: []string{"etc/resolv.conf", "etc/someother/resolv.conf"},
},
want: []string{"1.1.1.1", "8.8.8.8"},
wantErr: false,
},
{
name: "Files do not exist",
args: args{
filesys: fstest.MapFS{},
filenames: []string{"etc/resolv.conf", "etc/someother/resolv.conf"},
},
want: nil,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ExtractDNSServersFromResolvConf(tt.args.filesys, tt.args.filenames)
if (err != nil) != tt.wantErr {
t.Errorf("ExtractDNSServerFromResolvConf() error = %v, wantErr %v", err, tt.wantErr)
return
}
if diff := cmp.Diff(tt.want, got, cmpopts.SortSlices(func(s1, s2 string) bool {
switch strings.Compare(s1, s2) {
case -1:
return false
case 0:
return true
}
return true
},
)); diff != "" {
t.Errorf(diff)
}
})
}
}