Cisco CML 2.8 Free Tier上にSWを自動配置してAnsibleで設定したい！

<iframe width="560" height="315" src="https://www.youtube.com/embed/NKq8Pdtnu9E?si=T94ihAFFIJdilflj" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

これを実現します。コードは次を参照してください。

https://github.com/recuraki/ciscoCmlLabAutomation/


CML2ってノードの初期設定面倒ですよね。以下を自動化します。

- 1 pythonのvirl2_clientを使ってCML上に外部からアクセス可能なNWノードを自動作成する
    - 1-1 ノードとExternalコネクタ(Bridge)を自動デプロイする
    - 1-2 各ノード間を自動配線する
    - 1-3 Mgmt側I/Fに関する設定を起動時に読み込ませる
- 2 デプロイしたノードのmgmtアドレスを取得する
- 3 AnsibleのCisco.iosで簡単なI/F設定とOSPF設定を投入する

この記事ではポイントを絞って説明します。

# 作りたいネットワーク
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30938/608e0399-93a0-c255-f68f-68de260389c6.png)

- rt1 <-> rt2 でOSPF。PC1, PC2のセグメントの経路を交換。
- 各mgmtはexternal bridge経由で外部(CMLの外)から叩く
- PC1 -> PC2でpingできればOK

# Cisco CMLのインストール

いろいろな人が書いているので割愛します。私が過去に書いた記事は以下です。

https://qiita.com/recuraki/items/3f5d792f2fc704052fd8

# lib2_clientを使ったCML上へのノードの自動デプロイ
lib2_clientはCiscoCMLのWebUI上で行えるすべての作業が行えるPythonのライブラリです。

https://developer.cisco.com/docs/virl2-client/

## 基本的な使い方
```Python
# CMLに接続しlabnwを開く
client = ClientLibrary(f"https://192.168.0.100/", "admin", "cisco", ssl_verify=False)
lab = client.create_lab("labnw")
# labにnode1, node2を作り、（新しいI/Fを作って) ノードを接続する
node1 = lab.create_node("RT1", "iol-xe")
node2 = lab.create_node("RT2", "ioll2-xe")
pc1 = lab.create_node("PC1", "alpine") # alpine(linux)を作成
node1.config = "...(node1のconfig)"
node2.config = "...(node2のconfig)"
pc1.config = "..."
lab.connect_two_nodes(node1, node2 ) # この場合、eth0/0.node1 <-> eth0/0.node2が接続される(intfは勝手に作成される)
intf1 = pc1.create_interface() # eth0.pc1が作成される
intf2 = node1.create_interface() # eth0/1.node1が作成される
lab.connect_two_nodes(intf1, intf2) # eth0.pc1 <-> eth0/1.node1が接続される
lab.start()
# ノードが起動しきったら終了する
```

後述のようにnode.configにbootstrapのscriptを設定しておくことでmgmtアドレスを設定可能です。このアドレスは[この](https://github.com/recuraki/ciscoCmlLabAutomation/blob/main/simpleLab/10_createLab.py#L120)ように取得可能です。iosxeでも、alpineでも取得できます。

```Python
for node in lab.nodes():
    print(node)
    for interface in node.interfaces():
        print(" ", interface, interface.discovered_ipv4)
```

## 特殊なノード
```Python: Bridge
# Bridge (external_connectorのconfigにテキストで"System Bridge"と書く)
bridgeNode = lab.create_node("bridge", "external_connector", 0, 0)
bridgeNode.config = "System Bridge"
```

```Python: アンマネージスイッチ
mgmtsw = lab.create_node("mgmtsw", "unmanaged_switch", 0, 500)
```



## Configのポイント
`node.config`に設定する設定のポイントです。

### iol-xeあるいはioll2-xeの場合

[ここ](https://github.com/recuraki/ciscoCmlLabAutomation/blob/main/simpleLab/10_createLab.py#L11)を参考してください。
sshのkey作成は`mod 2048`などを与えるとキーボード入力を求められることがありません。
```cisco_ios
ip domain name cisco
transport input telnet ssh
ip ssh server algorithm authentication password
crypt key gen rsa mod 2048
do write memory
```

DHCPによるアドレス取得としたい場合は以下の通りとなります。
```
interface Ethernet 0/0
 ip address dhcp
```

### Alpine設定のポイント

[ここ](https://github.com/recuraki/ciscoCmlLabAutomation/blob/main/simpleLab/10_createLab.py#L33)を参考にしてください。注意書きに書いた通り、staticにアドレスを指定してもdhcpcが起動します。DHCPのみの付与でよい場合はipコマンド群は不要です。

```bash
hostname {hostname}
# ここで設定したUSER, PASSでユーザが作成されます。sudo可能です。
USERNAME={loginUsername}
PASSWORD={loginPassword}
# 以下のようにstaticアドレスを付与できますが、この有無にかかわらず"udhcpcが起動する"ことを注意してください
ip address add {mgmtaddr}/{mgmtsubnet} dev eth0
ip route add default via {mgmtgw}
```

### Nexus設定のポイント
今回のスクリプトには含めていませんがNexusの場合、一番最初の`create_interface()`がmgmtインタフェースです。

# インベントリの作成
次の工程でansibleによる自動設定を行いたいので`hostname <-> mgmt ipaddr`のマッピングを作ります。

mgmtアドレスをDHCPで設定した場合、デプロイしたノードのmgmtアドレスを知るのは困難に思えますが上記で見た通りCML2ではnodeのinterface.discovered_ipv4に設定されたIPアドレスが格納されています。これを利用して以下のようなアプローチでmgmtを取得できます。
- アプローチ1: 各nodeのipv4アドレスを見て、あらかじめ設定したmgmtのアドレス空間に含まれていればそれをそのnodeのmgmtアドレスとして扱う
- アプローチ2: 各ノードの最初のインタフェースがmgmtになるように生成を行いそれをそのnodeのmgmtアドレスとして扱う

今回は[この](https://github.com/recuraki/ciscoCmlLabAutomation/blob/main/simpleLab/20_inventory.py#L42)ようにアプローチ2を採用しています。(今回の実装ではeth0あるいはfastether0/0かで判定しています)

```Python
for node in lab.nodes():
    nodeName = node._label
    for interface in node.interfaces():
        if "Interface: Ethernet0/0" in str(interface): 
           # この場合、iosxe
        elif "Interface: eth0" in str(interface):
           # この場合、alpine
```

ansibleのinventoryのテンプレートを[この](https://github.com/recuraki/ciscoCmlLabAutomation/blob/main/simpleLab/20_inventory.py#L14)ように作って起き、loadした後にhosts部分を埋めて再度dumpします。
```Python
baseYaml = """
all: ...
  children:
    iosxe:
      hosts:
    pc:
      hosts:
...
"""
dat = yaml.safe_load(baseYaml)
dat["all"]["children"]["iosxe"]["hosts"] = {}
dat["all"]["children"]["pc"]["hosts"] = {}
...
         dat["all"]["children"]["pc"]["hosts"][nodeName] = {
              "ansible_host": str(mgmtAddr),
          } # など
...
with open('inventory.yaml','w')as fw:
    yaml.dump(dat, fw, default_flow_style=False, allow_unicode=True)
```

これにより、[この](https://github.com/recuraki/ciscoCmlLabAutomation/blob/main/simpleLab/inventory.yaml.sampleoutput)ようなinventoryが生成されます。
```yaml
all:
  children:
    iosxe:
      hosts:
        rt1:
          ansible_host: 192.168.101.222
          ansible_network_os: ios
        rt2:
          ansible_host: 192.168.101.223
          ansible_network_os: ios
    pc:
      hosts:
        pc1:
          ansible_host: 192.168.101.224          
```

# 設定する
ここまでくればansibleのcisco.iosモジュールで設定できます。[この](https://github.com/recuraki/ciscoCmlLabAutomation/blob/main/simpleLab/30_setconfig.yaml)ように各種設定をして以下のように実行しましょう。

> ansible-playbook -i inventory.yaml 30_setconfig.yaml

```yaml:playbookの例
- name: router 1
  hosts: rt1
  gather_facts: no
  tasks:
    - name: rt1-interface-addr
      cisco.ios.ios_l3_interfaces:
        config:
          - name: Ethernet0/1
            ipv4:
              - address: 10.0.0.1/24
        state: merged
    - name: OSPF V2 configuration
      cisco.ios.ios_ospfv2:
        config:
          processes:
            - process_id: 1
              router_id: 10.255.0.1
              log_adjacency_changes:
                set: true
              network:
                - address: 10.0.0.0
                  wildcard_bits: 0.0.0.255
                  area: 0
```

## alpine設定のコツ
alpineにはPythonが入っていないので`builtin.raw`で流します。しかし、`sudo ip`で実行すると戻り値がtrueにならないようなので以下のように`|| true`を使って強制的にtrueを戻すようにしています。
```
- hosts: pc1
  gather_facts: no
  tasks:
    - name: link up ip address
      ansible.builtin.raw: sudo ip link set eth1 up || true
    - name : set ip address
      ansible.builtin.raw: sudo ip addr add 10.101.0.2/24 dev eth1 || true
    - name: set route
      ansible.builtin.raw: sudo ip route add 10.0.0.0/8 via 10.101.0.1 || true
```


# 動作確認
設定変更と変わりません。