「体験しながら学ぶネットワーク管理技術入門」のテストネットワークをContainerlabで試す方法

# はじめに

「体験しながら学ぶネットワーク管理技術入門」という本

https://www.sbcr.jp/product/4815618599/

のテストネットワークは、tinet


https://github.com/tinynetwork/tinet

で試すようになっています。

これをContainerlab

https://containerlab.dev/

で試すための変換ツールを開発しました。

そのツールをここで紹介します。

# tinetとcontainerlabのネットワーク定義

tinetのネットワーク定義は、

```yaml:spec.yaml
---
nodes:
- name: R1
  image: tinynetwork/pmacctd:develop
  interfaces:
  - { name: net0, type: direct, args: R2#net0 }
- name: R2
  image: tinynetwork/pmacctd:develop
  interfaces:
  - { name: net0, type: direct, args: R1#net0 }

node_configs:
- name: R1
  cmds:
  - cmd: ip addr add 10.2.0.1/24 dev net0
- name: R2
  cmds:
  - cmd: ip addr add 10.2.0.2/24 dev net0
  - cmd: ip route add default via 10.2.0.1

```

のようにnodeとnodeで実行する設定コマンドで構成されます。

この定義をcontainerlabの

```yaml:topology.yaml
name: test
topology:
  nodes:
    R1:
      kind: linux
      image: tinynetwork/pmacctd:develop
      network-mode: none
      exec:
        - ip addr add 10.2.0.1/24 dev net0
    R2:
      kind: linux
      image: tinynetwork/pmacctd:develop
      network-mode: none
      exec:
        - ip addr add 10.2.0.2/24 dev net0
        - ip route add default via 10.2.0.1
  links:
    - endpoints: ["R1:net0","R2:net0"]
```

のように変更します。

# 変換のポイント

変換処理のポイントは

- nameにネットワーク名を設定
- topologyの配下にnodesとlinksを配置
- nodesは名前をキーに設定
- kindをlinuxに設定
- imageは同じ値を設定
- network-modeをnoneに設定
- node_configsの設定コマンドはノード単位のexecに設定
- nodeのinterfacesから接続関係のlinksを作成
- mountはbindに変更
- dnsの設定も以降

です。

# 変換ツール

変換処理のポイントを元に変換ツールをGo言語で作成しました。

```terminal
$go build -o tinet2clab .
$cat spec.yaml | tinet2clab > topology.yaml
```
のように変換します。

「体験しながら学ぶネットワーク管理技術入門」のspec_01.yamlも変換できました。


変換ツールのソースコードは、以下の通りです。

```go:main.go
package main

import (
	"fmt"
	"log"
	"os"
	"strings"

	"github.com/spf13/viper"
)

func main() {
	c, err := loadTinetSpec()
	if err != nil {
		log.Fatalln(err)
	}
	err = saveClabTopology(c)
	if err != nil {
		log.Fatalln(err)
	}
}

type Tn struct {
	PreCmd      []PreCmd     `yaml:"precmd"`
	PreInit     []PreInit    `yaml:"preinit"`
	PreConf     []PreConf    `yaml:"preconf"`
	PostInit    []PostInit   `yaml:"postinit"`
	PostFini    []PostFini   `yaml:"postfini"`
	Nodes       []Node       `yaml:"nodes" mapstructure:"nodes"`
	Switches    []Switch     `yaml:"switches" mapstructure:"switches"`
	NodeConfigs []NodeConfig `yaml:"node_configs" mapstructure:"node_configs"`
	Test        []Test       `yaml:"test"`
}

// PreCmd
type PreCmd struct {
	// Cmds []Cmd `yaml:"cmds"`
	Cmds []Cmd `yaml:"cmds" mapstructure:"cmds"`
}

// PreInit
type PreInit struct {
	Cmds []Cmd `yaml:"cmds" mapstructure:"cmds"`
}

// PreConf
type PreConf struct {
	Cmds []Cmd `yaml:"cmds" mapstructure:"cmds"`
}

// PostInit
type PostInit struct {
	Cmds []Cmd `yaml:"cmds" mapstructure:"cmds"`
}

// PostFini
type PostFini struct {
	Cmds []Cmd `yaml:"cmds" mapstructure:"cmds"`
}

// Node
type Node struct {
	Name           string                 `yaml:"name" mapstructure:"name"`
	Type           string                 `yaml:"type" mapstructure:"type"`
	NetBase        string                 `yaml:"net_base" mapstructure:"net_base"`
	VolumeBase     string                 `yaml:"volume" mapstructure:"volume"`
	Image          string                 `yaml:"image" mapstructure:"image"`
	BuildFile      string                 `yaml:"buildfile" mapstructure:"buildfile"`
	BuildContext   string                 `yaml:"buildcontext" mapstructure:"buildcontext"`
	Interfaces     []Interface            `yaml:"interfaces" mapstructure:"interfaces"`
	Sysctls        []Sysctl               `yaml:"sysctls" mapstructure:"sysctls"`
	Mounts         []string               `yaml:"mounts,flow" mapstructure:"mounts,flow"`
	DNS            []string               `yaml:"dns,flow" mapstructure:"dns,flow"`
	DNSSearches    []string               `yaml:"dns_search,flow" mapstructure:"dns_search,flow"`
	HostNameIgnore bool                   `yaml:"hostname_ignore" mapstructure:"hostname_ignore"`
	EntryPoint     string                 `yaml:"entrypoint" mapstructure:"entrypoint"`
	ExtraArgs      string                 `yaml:"docker_run_extra_args" mapstructure:"docker_run_extra_args"`
	Vars           map[string]interface{} `yaml:"vars" mapstructure:"vars"`
	Templates      []Template             `yaml:"templates" mapstructure:"templates"`
}

// Interface
type Interface struct {
	Name  string `yaml:"name"`
	Type  string `yaml:"type"`
	Args  string `yaml:"args"`
	Addr  string `yaml:"addr"`
	Label string `yaml:"label"`
}

// Sysctl
type Sysctl struct {
	Sysctl string `yaml:"string"`
}

type Template struct {
	Src     string `yaml:"src"`
	Dst     string `yaml:"dst"`
	Content string `yaml:"content"`
}

// Switch
type Switch struct {
	Name       string      `yaml:"name"`
	Interfaces []Interface `yaml:"interfaces" mapstructure:"interfaces"`
}

// NodeConfig
type NodeConfig struct {
	Name string `yaml:"name"`
	Cmds []Cmd  `yaml:"cmds" mapstructure:"cmds"`
}

// Cmd
type Cmd struct {
	Cmd string `yaml:"cmd"`
}

// Test
type Test struct {
	Name string
	Cmds []Cmd `yaml:"cmds" mapstructure:"cmds"`
}

func loadTinetSpec() (Tn, error) {
	var r Tn
	var err error
	viper.SetConfigType("yaml")

	if err = viper.ReadConfig(os.Stdin); err != nil {
		return r, err
	}

	err = viper.Unmarshal(&r)
	return r, err
}

func saveClabTopology(c Tn) error {
	name := "test"
	if len(os.Args) > 1 {
		name = os.Args[1]
	}
	fmt.Printf("name: %s\n", name)
	fmt.Printf("topology:\n")
	fmt.Printf("  nodes:\n")
	for _, n := range c.Nodes {
		fmt.Printf("    %s:\n", n.Name)
		fmt.Printf("      kind: linux\n")
		fmt.Printf("      image: %s\n", n.Image)
		if n.NetBase == "" {
			n.NetBase = "none"
		}
		fmt.Printf("      network-mode: %s\n", n.NetBase)
		if len(n.DNS) > 0 {
			fmt.Printf("      dns:\n")
			fmt.Printf("        servers:\n")
			for _, d := range n.DNS {
				fmt.Printf("          - %s\n", d)
			}
			if len(n.DNSSearches) > 0 {
				fmt.Printf("        search:\n")
				for _, d := range n.DNSSearches {
					fmt.Printf("          - %s\n", d)
				}
			}
		}
		if len(n.Mounts) > 0 {
			fmt.Printf("      binds:\n")
			for _, m := range n.Mounts {
				fmt.Printf("        - %s\n", m)
			}
		}
		for _, nc := range c.NodeConfigs {
			if nc.Name == n.Name && len(nc.Cmds) > 0 {
				fmt.Printf("      exec:\n")
				for _, cmd := range nc.Cmds {
					fmt.Printf("        - %s\n", strings.ReplaceAll(cmd.Cmd, "\\$", "$$"))
				}
				break
			}
		}
		if n.BuildFile != "" {
			buildContext := "."
			if n.BuildContext != "" {
				buildContext = n.BuildContext
			}
			fmt.Printf("#docker build -t %s -f %s %s\n", n.Image, n.BuildFile, buildContext)
		}
		if n.ExtraArgs != "" {
			a := strings.Fields(n.ExtraArgs)
			ports := []string{}
			for i, p := range a {
				if p == "-p" && i+1 < len(a) {
					ports = append(ports, a[i+1])
				}
			}
			if len(ports) > 0 {
				fmt.Printf("      ports:\n")
				for _, e := range ports {
					fmt.Printf("        - %s\n", e)
				}
			}
		}
	}
	linkMap := make(map[string]bool)
	fmt.Printf("  links:\n")
	for _, n := range c.Nodes {
		for _, i := range n.Interfaces {
			if i.Type == "direct" {
				a := strings.SplitN(i.Args, "#", 2)
				if len(a) == 2 {
					k := fmt.Sprintf("%s:%s:%s:%s", a[0], a[1], n.Name, i.Name)
					if _, ok := linkMap[k]; !ok {
						fmt.Printf("    - endpoints: [\"%s:%s\",\"%s:%s\"]\n", n.Name, i.Name, a[0], a[1])
						linkMap[k] = true
						linkMap[fmt.Sprintf("%s:%s:%s:%s", n.Name, i.Name, a[0], a[1])] = true
					}
				}
			}

		}
	}
	return nil
}

```