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

Download License + scp and ftp (also e.g. for startup-config) #1414

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
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
7 changes: 7 additions & 0 deletions .github/workflows/cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,13 @@ jobs:
with:
py_ver: ${{ needs.file-changes.outputs.py_ver }}

download-tests:
uses: ./.github/workflows/download-tests.yml
needs:
- unit-test
- staticcheck
- build-containerlab

# a job that downloads coverage artifact and uses codecov to upload it
coverage:
runs-on: ubuntu-22.04
Expand Down
63 changes: 63 additions & 0 deletions .github/workflows/download-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: download-test

"on":
workflow_call:

jobs:
download-tests:
runs-on: ubuntu-22.04
strategy:
matrix:
runtime:
- "docker"
test-suite:
- "*.robot"
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: actions/download-artifact@v3
with:
name: containerlab

- name: Move containerlab to usr/bin
run: sudo mv ./containerlab /usr/bin/containerlab && sudo chmod a+x /usr/bin/containerlab

- uses: actions/setup-python@v4
with:
python-version: "3.10"
cache: pip
cache-dependency-path: "tests/requirements.txt"

- name: Install robotframework
run: |
pip install -r tests/requirements.txt

- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Run tests
run: |
bash ./tests/rf-run.sh ${{ matrix.runtime }} ./tests/09-download/${{ matrix.test-suite }}

# upload test reports as a zip file
- uses: actions/upload-artifact@v3
if: always()
with:
name: 08-${{ matrix.runtime }}-vxlan-log
path: ./tests/out/*.html

# upload coverage report from unit tests, as they are then
# merged with e2e tests coverage
- uses: actions/upload-artifact@v3
if: always()
with:
name: coverage
path: /tmp/clab-tests/coverage/*
retention-days: 7
16 changes: 10 additions & 6 deletions clab/clab.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,13 @@ func (c *CLab) ProcessTopoPath(path string) (string, error) {

switch {
case path == "-" || path == "stdin":
file, err = readFromStdin(c.TopoPaths.ClabTmpDir())
file, err = c.readFromStdin()
if err != nil {
return "", err
}
// if the path is not a local file and a URL, download the file and store it in the tmp dir
case !utils.FileOrDirExists(path) && utils.IsHttpURL(path, true):
file, err = downloadTopoFile(path, c.TopoPaths.ClabTmpDir())
file, err = c.downloadTopoFile(path)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -215,8 +215,10 @@ func FindTopoFileByPath(path string) (string, error) {
// readFromStdin reads the topology file from stdin
// creates a temp file with topology contents
// and returns a path to the temp file.
func readFromStdin(tempDir string) (string, error) {
tmpFile, err := os.CreateTemp(tempDir, "topo-*.clab.yml")
func (c *CLab) readFromStdin() (string, error) {
utils.CreateDirectory(c.TopoPaths.ClabTmpDir(), 0755)

tmpFile, err := os.CreateTemp(c.TopoPaths.ClabTmpDir(), "topo-*.clab.yml")
if err != nil {
return "", err
}
Expand All @@ -229,8 +231,10 @@ func readFromStdin(tempDir string) (string, error) {
return tmpFile.Name(), nil
}

func downloadTopoFile(url, tempDir string) (string, error) {
tmpFile, err := os.CreateTemp(tempDir, "topo-*.clab.yml")
func (c *CLab) downloadTopoFile(url string) (string, error) {
utils.CreateDirectory(c.TopoPaths.ClabTmpDir(), 0755)

tmpFile, err := os.CreateTemp(c.TopoPaths.ClabTmpDir(), "topo-*.clab.yml")
if err != nil {
return "", err
}
Expand Down
42 changes: 38 additions & 4 deletions clab/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,10 +226,11 @@ func (c *CLab) createNodeCfg(nodeName string, nodeDef *types.NodeDefinition, idx
nodeCfg.EnforceStartupConfig = c.Config.Topology.GetNodeEnforceStartupConfig(nodeCfg.ShortName)
nodeCfg.SuppressStartupConfig = c.Config.Topology.GetNodeSuppressStartupConfig(nodeCfg.ShortName)

// initialize license field
p := c.Config.Topology.GetNodeLicense(nodeCfg.ShortName)
// resolve the lic path to an abs path
nodeCfg.License = utils.ResolvePath(p, c.TopoPaths.TopologyFileDir())
// process NodeLicense
err = c.processNodeLicense(nodeCfg)
if err != nil {
return nil, err
}

// initialize bind mounts
binds, err := c.Config.Topology.GetNodeBinds(nodeName)
Expand Down Expand Up @@ -257,9 +258,14 @@ func (c *CLab) createNodeCfg(nodeName string, nodeDef *types.NodeDefinition, idx
// It handles remote files, local files and embedded configs.
// Returns an absolute path to the startup-config file.
func (c *CLab) processStartupConfig(nodeCfg *types.NodeConfig) error {
var err error
// process startup-config
p := c.Config.Topology.GetNodeStartupConfig(nodeCfg.ShortName)

if p == "" {
return nil
}

// embedded config is a config that is defined as a multi-line string in the topology file
// it contains at least one newline
isEmbeddedConfig := strings.Count(p, "\n") >= 1
Expand Down Expand Up @@ -301,12 +307,40 @@ func (c *CLab) processStartupConfig(nodeCfg *types.NodeConfig) error {
p = absDestFile
}
}

p, err = utils.ProcessDownloadableAndEmbeddedFile(nodeCfg.ShortName, p, "embedded.partial.cfg", c.TopoPaths)
if err != nil {
return err
}

// resolve the startup config path to an abs path
nodeCfg.StartupConfig = utils.ResolvePath(p, c.TopoPaths.TopologyFileDir())

return nil
}

// processStartupConfig processes the raw path of the startup-config as it is defined in the topology file.
// It handles remote files, local files and embedded configs.
// Returns an absolute path to the startup-config file.
func (c *CLab) processNodeLicense(nodeCfg *types.NodeConfig) error {
var err error
// process startup-config
p := c.Config.Topology.GetNodeLicense(nodeCfg.ShortName)
if p == "" {
return nil
}

p, err = utils.ProcessDownloadableAndEmbeddedFile(nodeCfg.ShortName, p, "embedded.lic", c.TopoPaths)
if err != nil {
return err
}

// resolve the startup config path to an abs path
nodeCfg.License = utils.ResolvePath(p, c.TopoPaths.TopologyFileDir())

return nil
}

// CheckTopologyDefinition runs topology checks and returns any errors found.
// This function runs after topology file is parsed and all nodes/links are initialized.
func (c *CLab) CheckTopologyDefinition(ctx context.Context) error {
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.20
require (
github.com/a8m/envsubst v1.4.2
github.com/awalterschulze/gographviz v2.0.3+incompatible
github.com/bramvdbogaerde/go-scp v1.2.1
Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

I tried the current PR from @steiler rebased with main. So far scp works, but with scp I get the warnings:

WARN[0000] error performing host key validation based on "/root/.ssh/known_hosts" for hostname "flosch1:22" (knownhosts: key is unknown). continuing anyways

Also not sure if we should have this in the config file and not only as env var: CLAB_SSH_KEY

I also tried sftp, but I do not get it to work. I remains in:
DEBU[0000] Fetching "ftp://user:pass@localhost:2222/license" for node "leaf1" storing at "/tmp/.clab/clos02-leaf1-license"

I can debug a bit more...

Choose a reason for hiding this comment

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

I tried the current PR from @steiler rebased with main. So far scp works, but with scp I get the warnings:

WARN[0000] error performing host key validation based on "/root/.ssh/known_hosts" for hostname "flosch1:22" (knownhosts: key is unknown). continuing anyways

Also not sure if we should have this in the config file and not only as env var: CLAB_SSH_KEY

I also tried sftp, but I do not get it to work. I remains in: DEBU[0000] Fetching "ftp://user:pass@localhost:2222/license" for node "leaf1" storing at "/tmp/.clab/clos02-leaf1-license"

I can debug a bit more...

could be HostKeyCallback setting this code

clientConfig := ssh.ClientConfig{
		User:            u.User.Username(),
		Auth:            []ssh.AuthMethod{},
		HostKeyCallback: getCustomHostKeyCallback(knownHostsPath),

HostKeyCallback: getCustomHostKeyCallback(knownHostsPath), suggest to use ssh.InsecureIgnoreHostKey() to allow any host.

Choose a reason for hiding this comment

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

also, would suggest to keep SCP as a fallback in SFTP service does not available in the router.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

this is a custom Hostkey callback that is being used.
What happens is, that it informs you that it acted like InsecureIgnoreHostKey() in case no known key was found.
Thats the reason for the warning plus the "continuing anyways".

github.com/cenkalti/backoff v2.2.1+incompatible
github.com/containernetworking/plugins v1.4.0
github.com/containers/common v0.57.2
Expand All @@ -24,6 +25,7 @@ require (
github.com/h2non/gock v1.2.0
github.com/hairyhenderson/gomplate/v3 v3.11.6
github.com/hashicorp/go-version v1.6.0
github.com/jlaffaye/ftp v0.2.0
github.com/joho/godotenv v1.5.1
github.com/jsimonetti/rtnetlink v1.4.0
github.com/kellerza/template v0.0.6
Expand Down
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -919,6 +919,8 @@ github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dR
github.com/bombsimon/wsl v1.2.5/go.mod h1:43lEF/i0kpXbLCeDXL9LMT8c92HyBywXb0AsgMHYngM=
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bramvdbogaerde/go-scp v1.2.1 h1:BKTqrqXiQYovrDlfuVFaEGz0r4Ou6EED8L7jCXw6Buw=
github.com/bramvdbogaerde/go-scp v1.2.1/go.mod h1:s4ZldBoRAOgUg8IrRP2Urmq5qqd2yPXQTPshACY8vQ0=
github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
Expand Down Expand Up @@ -2039,6 +2041,8 @@ github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJS
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/jlaffaye/ftp v0.2.0 h1:lXNvW7cBu7R/68bknOX3MrRIIqZ61zELs1P2RAiA3lg=
github.com/jlaffaye/ftp v0.2.0/go.mod h1:is2Ds5qkhceAPy2xD6RLI6hmp/qysSoymZ+Z2uTnspI=
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
Expand Down Expand Up @@ -3033,6 +3037,7 @@ golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWP
golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
Expand Down
83 changes: 83 additions & 0 deletions tests/09-download/01-download.robot
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
*** Settings ***
Library OperatingSystem
Library String
Library Process
Resource ../common.robot
Suite Setup Setup
Suite Teardown Teardown


*** Variables ***
${lab-file} 01-downloads.clab.yaml
${lab-name} 01-downloads
${idrsa} ${CURDIR}/id_rsa
${scpservername} scp_server
${ftpservername} ftp_server
${httpservername} http_server
${lic_text} this is the fake license
# runtime command to execute tasks in a container
# defaults to docker exec. Will be rewritten to containerd `ctr` if needed in "Define runtime exec" test
${runtime-cli-exec-cmd} sudo docker exec


*** Test Cases ***
Deploy helper container - SCP
${rc} ${sshpubkey} = Run And Return Rc And Output sudo cat ${idrsa}.pub
Run sudo docker run -d --name=${scpservername} -e USER_NAME=user -e PUBLIC_KEY=\"${sshpubkey}\" --restart unless-stopped lscr.io/linuxserver/openssh-server:latest
#Run cat ${idrsa}.pub | sudo docker exec -i ${scpservername} tee /root/.ssh/authorized_keys -
Run echo "${lic_text} scp" | sudo docker exec -i ${scpservername} tee /config/lic.txt -
${rc} ${scp_server_ip} = Run And Return Rc And Output
... sudo docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' ${scpservername}
Log ${scp_server_ip}
Set Suite Variable ${scp_server_ip} ${scp_server_ip}
Set Environment Variable CLAB_SSH_KEY ${idrsa}
Set Environment Variable SCP_SERVER_IP ${scp_server_ip}

Deploy helper container - FTP
Run docker run -d --name ${ftpservername} lhauspie/vsftpd-alpine
${rc} ${ftp_server_ip} = Run And Return Rc And Output
... sudo docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' ${ftpservername}
Log ${ftp_server_ip}
Set Suite Variable ${ftp_server_ip} ${ftp_server_ip}
Set Environment Variable FTP_SERVER_IP ${ftp_server_ip}
Run sudo docker exec -i ${ftpservername} mkdir -p /home/vsftpd/user/
Run echo "${lic_text} ftp" | sudo docker exec -i ${ftpservername} tee /home/vsftpd/user/lic.txt -

Deploy helper container - HTTP
Run docker run -d --name ${httpservername} httpd:2.4
Run echo "${lic_text} http" | sudo docker exec -i ${httpservername} tee /usr/local/apache2/htdocs/lic.txt -
${rc} ${http_server_ip} = Run And Return Rc And Output
... sudo docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' ${httpservername}
Log ${http_server_ip}
Set Suite Variable ${http_server_ip} ${http_server_ip}
Set Environment Variable HTTP_SERVER_IP ${http_server_ip}


Deploy ${lab-name} lab
Log ${CURDIR}
${rc} ${output} = Run And Return Rc And Output
... cat ${CURDIR}/${lab-file}| envsubst | tee ${CURDIR}/rendered.clab.yml | sudo -E ${CLAB_BIN} --runtime ${runtime} deploy -t -
Log ${output}
Should Be Equal As Integers ${rc} 0
# save output to be used in next steps
Set Suite Variable ${deploy-output} ${output}


Check licenses
${rc} ${output} = Run And Return Rc And Output cat /tmp/.clab/${lab-name}-l1-lic.txt
Should Contain ${output} ${lic_text} scp
${rc} ${output} = Run And Return Rc And Output cat /tmp/.clab/${lab-name}-l2-lic.txt
Should Contain ${output} ${lic_text} ftp
${rc} ${output} = Run And Return Rc And Output cat /tmp/.clab/${lab-name}-l3-lic.txt
Should Contain ${output} ${lic_text} http

*** Keywords ***
Teardown
Run sudo -E ${CLAB_BIN} --runtime ${runtime} destroy -t ${CURDIR}/01-downloads.clab.yaml --cleanup
Run rm ${CURDIR}/id_rsa*
Run sudo docker rm -f ${scpservername} ${ftpservername} ${httpservername}
Run ssh-keygen -f "~/.ssh/known_hosts" -R ${scp_server_ip}
Run sudo rm -rf /tmp/.clab/${lab-name}-*

Setup
Run ssh-keygen -t ssh-rsa -f ${idrsa} -N ""
19 changes: 19 additions & 0 deletions tests/09-download/01-downloads.clab.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: 01-downloads

topology:
nodes:
l1:
kind: linux
image: alpine:edge
license: scp://user@${SCP_SERVER_IP}:2222/config/lic.txt
cmd: sleep infinity
l2:
kind: linux
image: alpine:edge
license: ftp://user:pass@${FTP_SERVER_IP}/lic.txt
cmd: sleep infinity
l3:
kind: linux
image: alpine:edge
license: http://${HTTP_SERVER_IP}/lic.txt
cmd: sleep infinity
6 changes: 6 additions & 0 deletions types/topo_paths.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,12 @@ func (t *TopoPaths) StartupConfigDownloadFileAbsPath(node, postfix string) strin
return filepath.Join(t.ClabTmpDir(), fmt.Sprintf("%s-%s-%s", t.topoName, node, postfix))
}

// DownloadFileTmpAbsPath returns the absolute path to a file
// when it is downloaded from a remote location to the clab temp directory.
func (t *TopoPaths) DownloadFileTmpAbsPath(node string, postfix string) string {
return filepath.Join(t.ClabTmpDir(), fmt.Sprintf("%s-%s-%s", t.topoName, node, postfix))
}

// TopologyFilenameBase returns the full filename of the topology file
// without any additional paths.
func (t *TopoPaths) TopologyFilenameBase() string {
Expand Down
Loading
Loading