はじめに
この記事はシスコの有志による Cisco Systems Japan Advent Calendar 2020 (2枚目) の 2 日目として投稿しています。
- 2017年版: https://qiita.com/advent-calendar/2017/cisco
- 2018年版: https://qiita.com/advent-calendar/2018/cisco
- 2019年版: https://qiita.com/advent-calendar/2019/cisco
- 2020年版: https://qiita.com/advent-calendar/2020/cisco
- 2020年版(2枚目): https://qiita.com/advent-calendar/2020/cisco2 (これ)
この記事では、CML(Cisco Modeling Labs)上のエミューレータ機器に対して、Ansibleを利用して設定を自動化する手順について紹介します。
CML(Cisco Modeling Labs)とは
CMLは以前はVIRLとよばれていたCisco製品用のネットワークシミュレータ製品の後継製品です。企業向けは元々CMLと呼ばれていて、個人向けのほうの名前がVIRLだったのですが、CML Version 2になって同じ名前で統一されました。VIRLと同じ機能はCMLのPersonal, Personal+というライセンスで購入することが可能です。
CMLはVersion2になって内部アーキテクチャとUIが一新され、かなり使いやすい製品になっています。
CMLの具体的な利用方法に関しては以下が参考になると思うので興味がある人はどうぞ。
(資料を見るにはCisco LearningNetworkへの登録が必要です)
Cisco Modeling Labs (CML)を使ってネットワークを学ぼう!(基礎編)
Ansible Modules for CML
CMLではREST APIがサポートされていて、そのREST APIを使ったPython SDK(virl2-client)が以下のリポジトリにあります。
このライブラリを使うことでCML2上でラボを作ったりVMを起動する操作をプログラミングすることが可能です。
GitHub - CiscoDevNet/virl2-client: Client library for the Cisco VIRL 2 Network Simulation Platform
ドキュメントはこちら。
VIRL 2 API Client Documentation - VIRL2 Client - Document - Cisco DevNet
更に、virl2-clientを内部で使ったAnsible Module(ansible-virl)が公開されています。今回はこのAnsible Moduleを使ってCML2上で簡単なラボを作ってみます。
GitHub - CiscoDevNet/ansible-virl
ansible-virlのインストール
Mac(Catalina)上で検証していますが、普通にLinuxでも動作するんじゃないかと思います。WindowsはAnsibleがサポートしていないのでWSLなどを使ってもらえればと思います。
利用するPython環境に、ansible, ansible-virlをインストールします。
(virl2-clientはansible-virlをインストールすると自動で追加されます。)
netaddrはこの後Playbookを書く際にipaddrフィルタを使っているのであわせてインストールしていますが、ansible-virlには必須ではないです。
pip install ansible ansible-virl netaddr
これで準備は完了です。
Ansible Modules
ansible-virlには以下の3つのAnsible Moduleと、Inventory Pluginが含まれています。
モジュール
- virl_lab ... CML上のラボを管理するモジュール
- virl_node ... ラボ内のノードを管理するモジュール
- virl_lab_facts ... ラボのノードに関する情報を取得するモジュール
- virl_interface ... リポジトリにはあるが未実装
virl_lab
新しくラボを作成するにはvirl_labを利用します。以下のようなTaskになります。
- name: Create the lab
virl_lab:
host: "{{ virl_host }}"
user: "{{ virl_username }}"
password: "{{ virl_password }}"
lab: "{{ virl_lab }}"
state: present
file: "{{ virl_lab_file }}"
register: results
host
, user
, password
はそれぞれCMLのホストサーバ、ユーザ名、パスワードです。
パラメータとして設定しない場合は、環境変数(VIRL_USERNAME, VIRL_PASSWORD, VIRL_HOST)を参照します。
lab
はCMLのラボ名です。なければ作ります。このパラメータも設定しない場合は環境変数 VIRL_LABを参照します。
file
はCMLラボの構成を定義したYAMLファイルを設定できます。何も設定しないと空のラボを作成します。
CMLでラボを作成していてそれを雛形にしたい場合は、CMLのWorkbenchでのメニュー(Download Lab)からYAMLファイルをダウンロードして設定可能です。
virl_node
ラボにノードを追加・削除したり、ノードを起動・停止することができるモジュールです。
- name: Start Node
virl_node:
name: "{{ inventory_hostname }}"
host: "{{ virl_host }}"
user: "{{ virl_username }}"
password: "{{ virl_password }}"
lab: "{{ virl_lab }}"
state: started # present, absent, started, stoped, wiped
image_definition: "{{ virl_image_definition | default(omit) }}"
node_definition: "{{ virl_node_definition | default(omit) }}"
config: "{{ day0_config | default(omit) }}"
node_definition
はノードの種類(IOSv, NM-OSvなど)を設定するためのパラメータ(Node ID)です。
image_definition
はノードのイメージ(iosv-15.3.8など)を設定するためのパラメータ(Image ID)です。
node_definition
, image_definition
はノードを作成するときに必要なパラメータになります。
各IDはCMLのLab Managerから、Tools > Node and Image Definitionから確認できます。
config
は、ノードに設定するコンフィグを設定できます。stateが、startedのみのときに有効なようです。
このAnsible Moduleはいまいち実装が足りなくて、例えば作成したノードのトポロジのX,Y座標を設定したり、インタフェースを設定したりすることができないようです。リポジトリ上にはvirl_interface
というモジュールもあり、ノード間のインタフェースの設定ができそうな感じですが、残念ながらまだ実装されていないようです。
このため、virl_node
は現状ノードを作成するには現状では十分ではないので、ノードの起動・停止や設定をいれるのにとどめて、ノードの作成はvirl_lab
のfile
パラメータを使って作成した方が良さそうです。
virl_lab_facts
CMLラボに含まれるノードの情報が返ってきます。
- name: Collect Facts
virl_lab_facts:
host: "{{ virl_host }}"
user: "{{ virl_username }}"
password: "{{ virl_password }}"
lab: "{{ virl_lab }}"
register: result
以下のような結果が返ってきます。
ok: [localhost] => {
"result": {
"changed": false,
"failed": false,
"virl_facts": {
"details": {
"created": "2020-11-30 03:26:46",
"id": "1651bc",
"lab_description": "",
"lab_title": "lab-test",
"link_count": 9,
"node_count": 8,
"state": "STARTED"
},
"nodes": {
"desktop-0": {
"ansible_host": null,
"config": "# this is a shell script which will be sourced at boot\n# if you change the hostname then you need to add a\n# /etc/hosts entry as well to make X11 happy\n# hostname inserthostname_here\n# like this:\n# echo \"127.0.0.1 inserthostname_here\" >>/etc/hosts",
"cpus": null,
"data_volume": null,
"image_definition": "desktop",
"interfaces": {
"eth0": {
"ipv4_addresses": [],
"ipv6_addresses": [],
"mac_address": "52:54:00:15:34:6b",
"state": "STARTED"
}
},
"node_definition": "desktop",
"ram": null,
"state": "BOOTED",
"tags": []
},
...
Inventory Plugin
CML向けのInventory Pluginが含まれています。virl.yaml(or virl.yml)という名前のインベントリファイルを作ってAnsibleを実行するとCML内のノードにたいしてインベントリとして利用可能です。
plugin: virl
host: cmlhost # 設定しなければVIRL_HOSTを使う
user: cmluser # 設定しなければVIRL_USERNAMEを使う
password: password # 設定しなければVIRL_PASSWORDを使う
lab: labname # 設定しなければVIRL_LABを使う
以下のようなインベントリが作成されます。
> ansible-inventory -i virl.yaml --list
SSL Verification disabled
Please ensure the client version is compatible with the server version. client 2.1.0, server 2.0.0
{
"_meta": {
"hostvars": {
"iosv1": {
"virl_facts": {
"config": "interface GigabitEthernet 0/1\n ip address 192.168.16.1 255.255.255.0\n no shut \n",
"cpus": null,
"data_volume": null,
"image_definition": "iosv-158-3",
"interfaces": [
{
"ipv4_addresses": null,
"ipv6_addresses": null,
"mac_address": null,
"name": "Loopback0",
"state": "STARTED"
},
...
CML上にラボを作成するPlaybook
実際にPlaybookを作成してみます。今回は以下のようなシンプルなラボを作成します。
[IOSv1]--------[Unmanaged Switch]---------[IOSv2]
Gi0/0 Port0 Port1 Gi0/0
- IOSv1はGi0/0に192.168.16.1/24を設定
- IOSv2はGi0/0に192.168.16.2/24を設定
以下のようなPlaybookになります。CMLにしか接続しないので、インベントリは作成していません。
(長くなるので、host, user, passwordは環境変数からとってきている前提です)
- hosts: localhost
connection: local
gather_facts: no
vars:
lab: mylab
tasks:
- name: Create Lab
virl_lab:
lab: '{{ lab }}'
file: lab_template.yaml
- name: Create IOSv
virl_node:
name: '{{ item.name }}'
lab: '{{ lab }}'
state: started
config: |
interface GigabitEthernet 0/0
ip address {{ item.addr }} 255.255.255.0
no shut
loop:
- name: iosv1
addr: 192.168.16.1
- name: iosv2
addr: 192.168.16.2
- name: Create Unmanaged Switch
virl_node:
name: unmanaged-switch
lab: '{{ lab }}'
state: started
lab_template.yamlは以下のような感じです。
(事前にCMLで作ってエクスポートしています)
lab:
description: ''
notes: ''
timestamp: 1606720359.0204537
title: lab_template
version: 0.0.3
nodes:
- id: n0
label: iosv1
node_definition: iosv
x: 100
y: -100
configuration: ''
image_definition: iosv-158-3
tags: []
interfaces:
- id: i0
label: Loopback0
type: loopback
- id: i1
slot: 0
label: GigabitEthernet0/0
type: physical
- id: n1
label: iosv2
node_definition: iosv
x: -100
y: -100
configuration: ''
image_definition: iosv-158-3
tags: []
interfaces:
- id: i0
label: Loopback0
type: loopback
- id: i1
slot: 0
label: GigabitEthernet0/0
type: physical
- id: n2
label: unmanaged-switch
node_definition: unmanaged_switch
x: 0
y: 0
configuration: ''
tags: []
interfaces:
- id: i0
slot: 0
label: port0
type: physical
- id: i1
slot: 1
label: port1
type: physical
links:
- id: l0
i1: i0
n1: n2
i2: i1
n2: n1
- id: l1
i1: i1
n1: n2
i2: i1
n2: n0
実行結果です。
> ansible-playbook playbook.yaml
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
PLAY [localhost] *******************************************************************************************************************************
TASK [Create Lab] ******************************************************************************************************************************
[WARNING]: Module did not set no_log for password
changed: [localhost]
TASK [Create IOSv] *****************************************************************************************************************************
changed: [localhost] => (item={'name': 'iosv1', 'addr': '192.168.16.1'})
changed: [localhost] => (item={'name': 'iosv2', 'addr': '192.168.16.2'})
TASK [Create Unmanaged Switch] *****************************************************************************************************************
changed: [localhost]
PLAY RECAP *************************************************************************************************************************************
localhost : ok=3 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
以下のようにCML上にラボが作成されて、すぐに利用できます。
CML上のノードに対してAnsibleを実行する
上記までは、CMLに対してAnsibleを実行してきましたが、ここからは、CML上のノードに対して直接Ansibleを実行する方法を考えたいと思います。
CMLだけの環境を考えると、virl_node
で設定を追加できるので大抵の場合は必要ないんですが、実環境へのPlaybookの検証のためにCMLを使っている場合や、状況によって設定を変更したい場合は、ios_command
, ios_config
などのAnsible Moduleを直接ノードに対して使えると便利です。
これを実現するためには、Ansibleホストから直接CML上のノードに対してSSHで接続できるようにする必要があります。
考えられる方法がいくつかあります。
- コンソールサーバー機能を利用して、Ansibelホストからノードのコンソールにアクセス
CMLのコンソールサーバー機能を利用してAnsibleのホストからノード上のコンソールにアクセスします。設定が面倒ではないですが、本来のSSHではなくコンソール経由のアクセスになるため、利用できるAnsible Moduleが限定されてしまいます - ブレイクアウトツールを利用して、Ansibelホストからノードのコンソールにアクセス
ブレイクアウトツールをAnsibleホストにインストールしてCML内のノードにコンソール接続します。これも設定は面倒ではないですが、本来のSSHではなくコンソール経由のアクセスになるため、利用できるAnsible Moduleが限定されてしまいます - CML内にAnsibleホストとなるマシンを立てる
CML内にLinuxサーバを立ててAnsibleホストとして利用します。直感的で良いですが、CMLラボを作成するAnsibleホストと、各ノードに接続するAnsibleホストが別々になってしまいます - External Connectorを利用
External Connectorを利用してCMLの外側からラボ内のノードに直接接続します。External Connectorの設定の手間や各ノードにSSHの設定をする手間がありますが、CMLの外側にあるAnsibleホストから直接CML内のノードに接続できるので、物理機器に対して設定するのとほぼ同じ手順でAnsibleでの自動化が可能です。
というわけで、ここでは、External Connectorを利用した方法で実行してみます。
External Connectionの設定にはBridge, NAT, Customがあります。CML内の複数のノードにアクセスする場合はNATにしてPort Forwardingとかできればアドレスも節約できてよいのですが、現状ではExternal NodeのNATでは細かい設定ができず、SSH Port Forwarding的な動作を設定できないので、今回はBridgeを利用し、個々のノードに外部とアクセス可能なIPを設定する方法でいきます。
ノードを直接Ansibleで操作するPlaybook
以下のようなシンプルなラボを構築します。
[External Connector]--------[IOSv1]
Port0 Gi0/1 Loop1000
以下はAnsible接続用に、virl_node
で設定
- IOSv1で、Gi0/1にIPアドレス(外部接続用のIPアドレス)と、SSHを有効にする設定諸々
以下は、ノードに直接Ansibleで接続して、ios_config
で設定
- Loopback1000 Interfaceを作成して、192.168.16.1/24を設定
以下のようなPlaybookになります。今回はインベントリ変数として、外部IPやLoopback IFのIPを設定したかったので、インベントリファイルも作成しています。Inventory Plugin(plugin: virl)によるインベントリファイル(virl.yaml)ですが、個別にインベントリ変数を設定できないようだったので、今回は普通のインベントリファイルにしました。
また、virl_lab
, virl_node
に設定するhost, user, password, labは全て環境変数から取得する設定にしています。
- hosts: localhost
connection: local
gather_facts: no
tasks:
- name: Create Lab
virl_lab:
file: template_ec.yaml
- name: Start External Connection
virl_node:
name: ext-conn
state: started
- hosts: ios
connection: local
gather_facts: no
tasks:
- name: Start IOSv
virl_node:
name: '{{ inventory_hostname }}'
state: started
config: |
interface GigabitEthernet 0/0
ip address {{ ansible_host }} 255.255.255.0
no shut
ip route 0.0.0.0 0.0.0.0 {{ default_gateway }}
hostname {{ inventory_hostname }}
ip domain-name cml.lab
crypto key generate rsa modulus 2048
ip ssh version 2
line vty 0 4
transport input ssh
login local
username {{ ansible_user }} password {{ ansible_password }}
enable password {{ ansible_become_pass }}
- hosts: ios
connection: network_cli
gather_facts: no
tasks:
- name: Wait for connection
wait_for_connection:
- name: Config Loop1000
ios_config:
lines: ip address {{ loopif_ip|ipaddr('address') }} {{ loopif_ip|ipaddr('netmask') }}
parents: interface Loopback1000
become: yes
[ios]
iosv1 ansible_host=10.1.1.1 default_gateway=10.1.1.254 loopif_ip=192.168.16.1/24
[ios:vars]
ansible_network_os=ios
ansible_user=cisco
ansible_password=cisco
ansible_become_pass=cisco
lab:
description: ''
notes: ''
timestamp: 1606726982.048025
title: template
version: 0.0.3
nodes:
- id: n0
label: ext-conn
node_definition: external_connector
x: -500
y: 0
configuration: bridge0
tags: []
interfaces:
- id: i0
slot: 0
label: port
type: physical
- id: n1
label: iosv1
node_definition: iosv
x: -350
y: 0
configuration: ''
image_definition: iosv-158-3
tags: []
interfaces:
- id: i0
label: Loopback0
type: loopback
- id: i1
slot: 0
label: GigabitEthernet0/0
type: physical
- id: i2
slot: 1
label: GigabitEthernet0/1
type: physical
- id: i3
slot: 2
label: GigabitEthernet0/2
type: physical
- id: i4
slot: 3
label: GigabitEthernet0/3
type: physical
links:
- id: l0
i1: i0
n1: n0
i2: i1
n2: n1
実行結果です。
❯ ansible-playbook -i inventory playbook.yaml
PLAY [localhost] *******************************************************************************************************************************
TASK [Create Lab] ******************************************************************************************************************************
[WARNING]: Module did not set no_log for password
changed: [localhost]
TASK [Start External Connection] ***************************************************************************************************************
changed: [localhost]
PLAY [ios] *************************************************************************************************************************************
TASK [Start IOSv] ******************************************************************************************************************************
changed: [iosv1]
PLAY [ios] *************************************************************************************************************************************
TASK [Wait for connection] *********************************************************************************************************************
ok: [iosv1]
TASK [Config Loop1000] *************************************************************************************************************************
changed: [iosv1]
PLAY RECAP *************************************************************************************************************************************
iosv1 : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
localhost : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
以下のようなラボがCMLで作成されます。
まとめ
virl-ansibleですが、実装が不十分なところがありますが、使い方を工夫すれば十分に利用できそうです。また、CMLが実装しているREST APIやPython SDK(virl2-client)では必要な機能は実装されているようなので、首を長くして待っていればそのうち実装を追加してくれるかもしれません。
実装してくれなくてもある程度PythonやAnsibleのモジュール作成の知識があれば自分でモジュールを作ることも可能そうです。
CML自体は検証環境を作ったり勉強用の環境を作るのに非常に便利なので、CMLでの環境構築もAnsibleで自動化することで、更なるハッピーネットワークエンジニアライフがおくれることでしょう???