kakkotetsu

VyOS と Arista で VXLAN 相互接続 (original : 2014/12/24)

この記事は某所で 2014/12/24 に書いた記事のコピーです。
そのため 2017/05/11 時点ではやや古い情報も含まれています。

概要

また Arista ですか? いえ、これは VyOS Advent Calendar 2014 の記事です。

本項でやること

VyOS と Arista で VXLAN 相互接続します。
とはいえ、interoperability で色々痛い目にあってきた人が思うような苦難は全く無いです。
Arista と VyOS では VXLAN で使おうとする dstPort 番号が異なるのですが、VyOS の記事なので敢えて VyOS 側でこれを変えます。
# デファクトスタンダードな openvswitch ではなく、何でニッチなアレ同士で…というツッコミは不可。

前段

VXLAN とは?

VXLAN 相互接続で気にすべき点

事前の鍵交換 phase やらの面倒ごとが無いシンプルなプロトコルなので、2 台で相互接続する分には左程気をつける点は無いです。

今年の Shownet では、各メーカの箱モノで相互接続実験とかやっていて、(以下のスライド 68-75)

これを見ても気にする点は以下くらいだと分かります。

  • BUM トラフィック転送方法 (Unicast or Multicast)
  • UDP dstport 番号 (4789 or 8472)
  • VXLAN カプセリング時の 802.1q tag 取扱い (除去 or そのまま)

1 つ目は製品によってマチマチです。RFC では Multicast 実装が示されています。

2 つ目は、本項でも気にしています。draft で「IANA から UDP ポート番号割り当ててもらわないと~」と言っている時代が長かったからなのか、Linux VXLAN では 8472 が今も使われています。その内どうにかしないと、ってスタンスではあるようです。( Linux/drivers/net/vxlan.c 参照)
現在は IANA から 4789 を割り当てられているので、最近はそれを使っている・過去の経緯を考慮して dstport 変更可能な製品が多いです。

3 つ目は RFC で取り除けと言ってます↓。本項では、access VLAN しか使っていないので、見ていません。

a VTEP SHOULD NOT include an inner VLAN tag on tunnel packets

本項で取り扱う VTEP 実装

VyOS

VyOS なので基本的に Linux ベースです。
VyOS に VXLAN 取り込んだよ、って話は作者の以下資料を参照して下さい。

VXLAN の dstport に関して(の悩みどころ)はスライド 23 に書かれています。
VyOS 1.1.1 現在は普通に動かすと、dstport 8472 で動きます。

Arista

VXLAN が動かない機器群を収容して、自分が VTEP をやる Hardware VTEP です。(本項で使うのは Software 版ですが…)
デフォルトの dstport は 4789 ですが、設定で変更して既存の 8472 に歩み寄ることができます。本項ではやりませんが。
とりあえず、メーカが出している分かり易い資料は以下です。

Arista EOS 自体については、以下の本をオススメしときますね。

Arista Warrior: A Real-World Guide to Understanding Arista Switches and EOS

Arista Warrior: A Real-World Guide to Understanding Arista Switches and EOS

環境情報など

構成図/環境

以下の構成でやります。全て箱庭の仮想環境です。
赤いテナントと青いテナントを Isolation します、的な。

f:id:kakkotetsu:20170511220335j:plain

ルータは multicast routing 出来れば、何でも良いです(VyOS 1.1.1 では不可だけど実装予定には入っているみたい)。 ノードは何でも良いです。

ホスト環境

VyOS 環境 (VTEP)

  • OS:VyOS 1.1.1
  • CPU:1Core
  • Memory:256MB

Arista 環境 (VTEP)

  • Aboot-veos:Aboot-veos-2.1.0
  • OS:vEOS-4.14.2F
  • CPU:1core
  • Memory:1024MB

Firefly 環境 (ルータ)

  • OS:12.1X47-D10.4
  • CPU:2core
  • Memory:2048MB

ノード環境 (4台共)

事前準備

VyOS のデプロイ

以下を参考に作っておきます。2014/12/24 現在の最新版である 1.1.1 を使います。

Arista(vEOS) のデプロイ

以下リンクを参考に作っておきます。公式に、各種ハイパーバイザごとの導入手順詳細とかもあります。

Firefly のデプロイ~ルータモードに変更

以下リンクを参考に作っておきます。(これは VMPlayer 版ですが、各種ハイパーバイザ使えます。)

あと、デフォルトだと firewall 的な動作をして鬱陶しいので、以下を参考にルータモード(俗称)に切り替えておきます。

Ubuntu のデプロイと設定

ノードの OS は別に問わないので、好きに作って IP アドレス設定だけすれば OK です。 VLAN tag とかも食いません。

設定

VyOS の VXLAN 設定

modprobe vxlan に udp_port オプション追記

事前に Linux Kernel と VyOS バージョン確認しておきます。

kotetsu@vtep-vyos01:~$ uname -r
3.13.11-1-amd64-vyos

kotetsu@vtep-vyos01:~$ show version
Version:      VyOS 1.1.1
Description:  VyOS 1.1.1 (helium)
Copyright:    2014 VyOS maintainers and contributors
Built by:     maintainers@vyos.net
Built on:     Sun Dec  7 21:41:28 UTC 2014
Build ID:     1412072141-129950d
System type:  x86 64-bit
Boot via:     disk
Hypervisor:   VirtualBox
HW model:     VirtualBox
HW S/N:       0
HW UUID:      EF4B6725-D02A-47D3-8194-BFADF71F9987
Uptime:       23:17:43 up 22:39,  1 user,  load average: 0.01, 0.02, 0.05

VXLAN の Kernel module を modprobe する時点で dstport をデフォルトの 8472 から 4789 に変更されるように、以下ファイルを書き換えます。

kotetsu@vyos# ls -alh /opt/vyatta/share/vyatta-cfg/templates/interfaces/vxlan
total 20K
drwxr-xr-x  3 root root 4.0K Dec 23 00:53 .
drwxr-xr-x 16 root root 4.0K Dec  8 06:44 ..
-rw-r--r--  1 root root 1.1K Dec 23 00:53 node.def
-rw-r--r--  1 root root 1.1K Oct 30 17:43 node.def.orig
drwxr-xr-x 13 root root 4.0K Dec  8 06:44 node.tag

kotetsu@vyos:~$ sudo diff /opt/vyatta/share/vyatta-cfg/templates/interfaces/vxlan/node.def /opt/vyatta/share/vyatta-cfg/templates/interfaces/vxlan/node.def.orig
14c14
<   [ -d /sys/module/vxlan ] || sudo modprobe vxlan udp_port=4789
---
>   [ -d /sys/module/vxlan ] || sudo modprobe vxlan

CLI で設定

では構成図のように順次設定していきます。

set interfaces ethernet eth2 address '172.16.2.1/24'

set interfaces bridge 'br100'
set interfaces bridge 'br200'

set interfaces ethernet eth3 bridge-group bridge 'br100'
set interfaces ethernet eth4 bridge-group bridge 'br200'
set interfaces vxlan vxlan0 bridge-group bridge 'br100'
set interfaces vxlan vxlan0 group '239.0.0.1'
set interfaces vxlan vxlan0 link 'eth2'
set interfaces vxlan vxlan0 vni '10'

set interfaces vxlan vxlan1 bridge-group bridge 'br200'
set interfaces vxlan vxlan1 group '239.0.0.1'
set interfaces vxlan vxlan1 link 'eth2'
set interfaces vxlan vxlan1 vni '20'

VyOS が認識しないといけない Arista 側の VTEP IP アドレスは、Arista の Loopback アドレスになるので、StaticRoute を追加しておきます。 #これだと拡張が面倒なので、実環境では DefaultRoute か動的ルーティングを使うことが多い気がします。

set protocols static route 172.16.3.1/32 next-hop '172.16.2.254'

commitsave して設定確認しておきます。

設定確認

kotetsu@vtep-vyos01:~$ show bridge
bridge name     bridge id               STP enabled     interfaces
br100           0000.0800272ede56       no              eth3
                                                        vxlan0
br200           0000.0800276532b1       no              eth4
                                                        vxlan1
kotetsu@vtep-vyos01:~$ show interfaces vxlan
Codes: S - State, L - Link, u - Up, D - Down, A - Admin Down
Interface        IP Address                        S/L  Description
---------        ----------                        ---  -----------
vxlan0           -                                 u/u
vxlan1           -                                 u/u

kotetsu@vtep-vyos01:~$ ip -d link show vxlan0
10: vxlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master br100 state UNKNOWN mode DEFAULT group default
    link/ether 96:3e:2b:62:6a:57 brd ff:ff:ff:ff:ff:ff promiscuity 1
    vxlan id 10 group 239.0.0.1 dev eth2 port 32768 61000 ttl 16 ageing 300

kotetsu@vtep-vyos01:~$ ip -d link show vxlan1
15: vxlan1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master br200 state UNKNOWN mode DEFAULT group default
    link/ether 8e:02:68:1e:d2:69 brd ff:ff:ff:ff:ff:ff promiscuity 1
    vxlan id 20 group 239.0.0.1 dev eth2 port 32768 61000 ttl 16 ageing 300

Arista(vEOS) の VXLAN 設定

設定

vlan 100,200

interface Ethernet1
   description DEV=fw01 IF=ge-0/0/2
   no switchport
   ip address 172.16.1.1/24
!
interface Ethernet2
   description DEV=node001 IF=eth2
   switchport access vlan 100
!
interface Ethernet3
   description DEV=node101 IF=eth2
   switchport access vlan 200

以下のように VXLAN インターフェースの source-interface には、Loopback Interface しか適用できません。VyOS などの dev と同じノリで物理IFを指定できるかと思いきや…。

interface Loopback0
   ip address 172.16.3.1/32
!
interface Vxlan1
   vxlan multicast-group 239.0.0.1
   vxlan source-interface Loopback0
   vxlan udp-port 4789
   vxlan vlan 100 vni 10
   vxlan vlan 200 vni 20

このままだと VTEP は 239.0.0.1 宛の通信をどこに投げればいいのか分からないので、Multicast Routing の設定をします。

ip routing
ip multicast-routing

interface Ethernet1
   ip pim sparse-mode

ip pim rp-address 172.16.1.254

ip route 172.16.2.0/24 172.16.1.254

設定確認

vtep-arista01#show interfaces vxlan 1
Vxlan1 is up, line protocol is up (connected)
  Hardware is Vxlan
  Source interface is Loopback0 and is active with 172.16.3.1
  Replication/Flood Mode is multicast
  Static vlan to vni mapping is
    [100, 10]         [200, 20]
  Multicast group address is 239.0.0.1


vtep-arista01#show vxlan vtep
Remote vteps for Vxlan1:
Total number of remote vteps:  0


vtep-arista01#show vxlan counters software
Rx bytes for encapsulation                      :  0
Rx pkts for encapsulation                       :  0
Encaped bytes                                   :  0
Encaped packets                                 :  0
Tx bytes after encapsulation                    :  0
Tx pkts after encapsulation                     :  0
Rx dot1Q Tunnel pkts for encapsulation          :  0
Rx bytes after decapsulation                    :  0
Rx pkts after decapsulation                     :  0
Decaped bytes                                   :  0
Decaped packets                                 :  0
ARP bytes                                       :  0
ARP packets                                     :  0

Read error for pkts coming in for encapsulation :  0
Discarded runt pkts ( encap side )              :  0
Discard vlan range ( encap eide )               :  0
Discard vlan map ( encap side )                 :  0
Discard mlag peer link ( encap side )           :  0
Discarded pkts due to empty flood list          :  0
Encap send error                                :  0
Encap timeout                                   :  0
Discarded runt pkts ( decap side )              :  0
Discard pkts not for vtep ( decap side )        :  0
Discard bytes not for vtep ( decap side )       :  0
Discard pkts with invalid vxlan header          :  0
Discard vlan map ( decap side )                 :  0
Discard VNI range ( decap side )                :  0
Decap timeout                                   :  0
Decap socket error                              :  0
ARP send error                                  :  0


vtep-arista01#show vxlan address-table
          Vxlan Mac Address Table
----------------------------------------------------------------------

Vlan  Mac Address     Type     Prt  Vtep             Moves   Last Move
----  -----------     ----     ---  ----             -----   ---------
Total Remote Mac Addresses for this criterion: 0

vtep-arista01#show vxlan counters varp
arp cache installed:                    0
vxlan encapsulated arps:                0
arp reply to vArp mac and vArp vtep:    0
arp reply RX:                           0
arp reply head-end-replicated:          0
arp reply TX:                           0
arp request decaped and sent:           0

arp cache install err                   0
arp request decap and send TX err:      0
unknown cpu port id:                    0
packet too short:                       0
unexpected ether type:                  0
unexpected non IP packet                0
unexpected IP prot                      0
unexpected UPD port                     0
unexpected inner packet                 0
unexpected arp reply                    0
invalid IP checksum                     0
invalid Vxlan header                    0
invalid arp packet                      0
unknown vlan to vni mapping             0
unknown source IP                       0
no BUM flood list TX err                0
arp reply to Arp agent TX err           0

Firefly の multicast routing 設定

設定

VXLAN の RFC には dense より bidirectional PIM が効果的かもね、と書いてあります。 ガン無視して、ここは簡単な dense 設定でいきます。

set interfaces ge-0/0/2 unit 0 family inet address 172.16.1.254/24
set interfaces ge-0/0/3 unit 0 family inet address 172.16.2.254/24

set routing-options static route 172.16.3.1/32 next-hop 172.16.1.1

set protocols pim interface ge-0/0/2.0 mode dense
set protocols pim interface ge-0/0/3.0 mode dense

動作確認

疎通

赤 node 同士、青 node 同士が疎通可能なことを確認します。 この時、VyOS や Arista で tcpdump かけておき後で確認します。 # 疎通したは良いが、異様に RTT がでかい…。firefly と Arista 間もそんな感じで、ショボいマシンでArista 動かしている所為かも。

$ ping 10.0.0.2
PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data.
64 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=72.9 ms
64 bytes from 10.0.0.2: icmp_seq=2 ttl=64 time=67.2 ms
64 bytes from 10.0.0.2: icmp_seq=3 ttl=64 time=63.2 ms
64 bytes from 10.0.0.2: icmp_seq=4 ttl=64 time=163 ms
64 bytes from 10.0.0.2: icmp_seq=5 ttl=64 time=75.7 ms
64 bytes from 10.0.0.2: icmp_seq=6 ttl=64 time=67.2 ms
64 bytes from 10.0.0.2: icmp_seq=7 ttl=64 time=68.8 ms
64 bytes from 10.0.0.2: icmp_seq=8 ttl=64 time=73.7 ms
64 bytes from 10.0.0.2: icmp_seq=9 ttl=64 time=55.3 ms
64 bytes from 10.0.0.2: icmp_seq=10 ttl=64 time=102 ms
64 bytes from 10.0.0.2: icmp_seq=11 ttl=64 time=38.5 ms
64 bytes from 10.0.0.2: icmp_seq=12 ttl=64 time=33.0 ms
64 bytes from 10.0.0.2: icmp_seq=13 ttl=64 time=35.1 ms
64 bytes from 10.0.0.2: icmp_seq=14 ttl=64 time=31.7 ms
~$ ping 10.0.0.2
PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data.
64 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=70.6 ms
64 bytes from 10.0.0.2: icmp_seq=2 ttl=64 time=62.5 ms
64 bytes from 10.0.0.2: icmp_seq=3 ttl=64 time=100 ms
64 bytes from 10.0.0.2: icmp_seq=4 ttl=64 time=38.6 ms
64 bytes from 10.0.0.2: icmp_seq=5 ttl=64 time=35.3 ms
64 bytes from 10.0.0.2: icmp_seq=6 ttl=64 time=30.0 ms
64 bytes from 10.0.0.2: icmp_seq=7 ttl=64 time=34.4 ms
64 bytes from 10.0.0.2: icmp_seq=8 ttl=64 time=26.1 ms
64 bytes from 10.0.0.2: icmp_seq=9 ttl=64 time=29.9 ms
64 bytes from 10.0.0.2: icmp_seq=10 ttl=64 time=28.7 ms
64 bytes from 10.0.0.2: icmp_seq=11 ttl=64 time=25.9 ms
64 bytes from 10.0.0.2: icmp_seq=12 ttl=64 time=24.8 ms
64 bytes from 10.0.0.2: icmp_seq=13 ttl=64 time=24.4 ms
$ ping 10.0.0.1
PING 10.0.0.1 (10.0.0.1) 56(84) bytes of data.
64 bytes from 10.0.0.1: icmp_seq=1 ttl=64 time=24.3 ms
64 bytes from 10.0.0.1: icmp_seq=2 ttl=64 time=25.9 ms
64 bytes from 10.0.0.1: icmp_seq=3 ttl=64 time=28.5 ms
64 bytes from 10.0.0.1: icmp_seq=4 ttl=64 time=24.7 ms
64 bytes from 10.0.0.1: icmp_seq=5 ttl=64 time=26.2 ms
64 bytes from 10.0.0.1: icmp_seq=6 ttl=64 time=59.4 ms
64 bytes from 10.0.0.1: icmp_seq=7 ttl=64 time=29.0 ms
64 bytes from 10.0.0.1: icmp_seq=8 ttl=64 time=26.9 ms
64 bytes from 10.0.0.1: icmp_seq=9 ttl=64 time=72.5 ms
64 bytes from 10.0.0.1: icmp_seq=10 ttl=64 time=55.1 ms
64 bytes from 10.0.0.1: icmp_seq=11 ttl=64 time=49.7 ms
64 bytes from 10.0.0.1: icmp_seq=12 ttl=64 time=92.5 ms
64 bytes from 10.0.0.1: icmp_seq=13 ttl=64 time=45.3 ms
64 bytes from 10.0.0.1: icmp_seq=14 ttl=64 time=41.3 ms
64 bytes from 10.0.0.1: icmp_seq=15 ttl=64 time=37.7 ms
64 bytes from 10.0.0.1: icmp_seq=16 ttl=64 time=36.7 ms
64 bytes from 10.0.0.1: icmp_seq=17 ttl=64 time=35.3 ms
$ ping 10.0.0.1
PING 10.0.0.1 (10.0.0.1) 56(84) bytes of data.
64 bytes from 10.0.0.1: icmp_seq=1 ttl=64 time=26.2 ms
64 bytes from 10.0.0.1: icmp_seq=2 ttl=64 time=23.4 ms
64 bytes from 10.0.0.1: icmp_seq=3 ttl=64 time=28.3 ms
64 bytes from 10.0.0.1: icmp_seq=4 ttl=64 time=73.0 ms
64 bytes from 10.0.0.1: icmp_seq=5 ttl=64 time=65.0 ms
64 bytes from 10.0.0.1: icmp_seq=6 ttl=64 time=62.3 ms
64 bytes from 10.0.0.1: icmp_seq=7 ttl=64 time=112 ms
64 bytes from 10.0.0.1: icmp_seq=8 ttl=64 time=45.6 ms
64 bytes from 10.0.0.1: icmp_seq=9 ttl=64 time=38.3 ms
64 bytes from 10.0.0.1: icmp_seq=10 ttl=64 time=35.6 ms
64 bytes from 10.0.0.1: icmp_seq=11 ttl=64 time=34.9 ms
64 bytes from 10.0.0.1: icmp_seq=12 ttl=64 time=40.1 ms
64 bytes from 10.0.0.1: icmp_seq=13 ttl=64 time=38.3 ms

VTEP状態確認

kotetsu@vtep-vyos01:~$ show interfaces vxlan detail
vxlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master br100 state UNKNOWN group default
    link/ether fe:61:bc:ca:cd:1d brd ff:ff:ff:ff:ff:ff
    inet6 fe80::fc61:bcff:feca:cd1d/64 scope link
       valid_lft forever preferred_lft forever

    RX:  bytes    packets     errors    dropped    overrun      mcast
        117556       1480          0          0          0          0
    TX:  bytes    packets     errors    dropped    carrier collisions
        383710       4382          0          0          0          0
vxlan1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master br200 state UNKNOWN group default
    link/ether b2:02:ef:4e:26:d4 brd ff:ff:ff:ff:ff:ff
    inet6 fe80::b002:efff:fe4e:26d4/64 scope link
       valid_lft forever preferred_lft forever

    RX:  bytes    packets     errors    dropped    overrun      mcast
        232330       3109          0          0          0          0
    TX:  bytes    packets     errors    dropped    carrier collisions
        490658       5270          0          0          0          0
vtep-arista01#show vxlan vtep
Remote vteps for Vxlan1:
172.16.2.1
Total number of remote vteps:  1



vtep-arista01#show mac address-table
          Mac Address Table
------------------------------------------------------------------

Vlan    Mac Address       Type        Ports      Moves   Last Move
----    -----------       ----        -----      -----   ---------
 100    0800.272d.daa2    DYNAMIC     Vx1        1       0:14:19 ago
 100    0800.27a6.f13d    DYNAMIC     Et2        1       0:14:22 ago
 200    0800.272c.080f    DYNAMIC     Et3        1       0:11:05 ago
 200    0800.27a9.6b58    DYNAMIC     Vx1        1       0:10:33 ago
Total Mac Addresses for this criterion: 4

          Multicast Mac Address Table
------------------------------------------------------------------

Vlan    Mac Address       Type        Ports
----    -----------       ----        -----
Total Mac Addresses for this criterion: 0


vtep-arista01#show vxlan address-table
          Vxlan Mac Address Table
----------------------------------------------------------------------

Vlan  Mac Address     Type     Prt  Vtep             Moves   Last Move
----  -----------     ----     ---  ----             -----   ---------
 100  0800.272d.daa2  DYNAMIC  Vx1  172.16.2.1       1       0:14:15 ago
 200  0800.27a9.6b58  DYNAMIC  Vx1  172.16.2.1       1       0:10:28 ago
Total Remote Mac Addresses for this criterion: 2

vtep-arista01#show vxlan counters software
Rx bytes for encapsulation                      :  249372
Rx pkts for encapsulation                       :  2390
Encaped bytes                                   :  249372
Encaped packets                                 :  2390
Tx bytes after encapsulation                    :  306732
Tx pkts after encapsulation                     :  2390
Rx dot1Q Tunnel pkts for encapsulation          :  0
Rx bytes after decapsulation                    :  79320
Rx pkts after decapsulation                     :  733
Decaped bytes                                   :  23664
Decaped packets                                 :  348
ARP bytes                                       :  0
ARP packets                                     :  0

Read error for pkts coming in for encapsulation :  0
Discarded runt pkts ( encap side )              :  0
Discard vlan range ( encap eide )               :  0
Discard vlan map ( encap side )                 :  0
Discard mlag peer link ( encap side )           :  0
Discarded pkts due to empty flood list          :  0
Encap send error                                :  0
Encap timeout                                   :  0
Discarded runt pkts ( decap side )              :  1
Discard pkts not for vtep ( decap side )        :  384
Discard bytes not for vtep ( decap side )       :  40980
Discard pkts with invalid vxlan header          :  0
Discard vlan map ( decap side )                 :  0
Discard VNI range ( decap side )                :  0
Decap timeout                                   :  0
Decap socket error                              :  0
ARP send error                                  :  0
vtep-arista01#


vtep-arista01#show vxlan counters varp
arp cache installed:                    0
vxlan encapsulated arps:                2
arp reply to vArp mac and vArp vtep:    0
arp reply RX:                           0
arp reply head-end-replicated:          0
arp reply TX:                           0
arp request decaped and sent:           2

arp cache install err                   0
arp request decap and send TX err:      0
unknown cpu port id:                    0
packet too short:                       0
unexpected ether type:                  0
unexpected non IP packet                0
unexpected IP prot                      0
unexpected UPD port                     0
unexpected inner packet                 0
unexpected arp reply                    0
invalid IP checksum                     0
invalid Vxlan header                    0
invalid arp packet                      0
unknown vlan to vni mapping             0
unknown source IP                       0
no BUM flood list TX err                0
arp reply to Arp agent TX err           0

パケットキャプチャ

特徴的なものをいくつか画面キャプチャで貼っていきます。

↓ node からの BUM トラフィック(ping 実行時最初の ARP)は、設定したマルチキャストアドレスで。

f:id:kakkotetsu:20170511220409j:plain

↓ VNI10 側。VTEP 間のユニキャストにカプセリングされているのがよく分かる。また、指定どおり UDP dstport 4789 で動作している。

f:id:kakkotetsu:20170511220421j:plain

f:id:kakkotetsu:20170511220432j:plain

↓VNI20 側も一緒。

f:id:kakkotetsu:20170511220443j:plain

おしまい

雑感

  • このように利用する UDP dstport が異なるような VTEP 間でも、それさえ揃えてしまえば相互接続もチョロいものです。
  • まあ、普通は設定一発で UDP dstport を変更できるやつ(今回のケースでいうと Arista 側)で変更すると思います。今回 VyOS でやったのはただの興味本位です。
  • VyOS を VTEP として使うユースケースがまだ見えてきません。ハイパーバイザ1台に1インスタンスバラまくとか?
  • マルチキャストルーティングを勉強しよう。

Arista の REST(っぽい) API を ruby や Ansible で突いてみよう (original : 2014/12/14)

この記事は某所で 2014/12/14 に書いたもののコピーです。
そのため 2017/05/13 時点ではやや古い情報も含まれています。(以下一例)

  • Ansible のバージョン 1.8 系、今となっては古い
  • 今は Arista 公式が Ruby 用のライブラリを公開している(当時はなかった)
  • 本記事でとりあげた Arista 公式の Ansible モジュールは、今は「Ansible の Core Module に入っている方を使ってね」と言っている

.

概要

もう expect して正規表現でシコシコしないで良いんですか!?やった~!!

本項でやること

Arista の REST API である eAPI を触ります。
http or https で REST 叩けてリクエストやレスポンスの JSON を弄れれば、クライアント実装は何でも良いですが以下を試します。

  • arista-eapi (ruby の gem)
  • arista.eos (Arista 公式が出している Ansible モジュール)

いずれも「Arista EOS の REST API(eAPI) を操作することに特化」しており、これを使うと Arista の操作が楽々だ~というのを見ます。
両方で「現在の設定情報を取得して、それを基に設定投入」という操作を試します。
vEOS や ruby や ansible 本体のセットアップについては、記載しません。

Arista EOS?

この本をオススメしときますね。

Arista Warrior: A Real-World Guide to Understanding Arista Switches and EOS

Arista Warrior: A Real-World Guide to Understanding Arista Switches and EOS

eAPI とは?

公式 "Introduction to Managing EOS Devices – Setting up Management" の記載から抜粋します。

Arista EOS provides multiple APIs, of which Extensible API (eAPI) is a RESTFUL programmatic interface based on the JSON structured format via HTTP or HTTPS, to simplify interactions with any Arista EOS.

情報取得や設定投入が出来ます。
リクエスト内に CLI コマンドをそのまま埋め込んで投げると、JSON で構造化されたレスポンスを得られます。

環境情報など

構成図・環境

以下の構成でやります。
Arista(vEOS) 含め全て仮想 OS です。
物理?構成は一般的なデータセンタネットワークのそれで、Arista の Multi-Chassis Link Aggregation (MLAG)を使っています。
本項では MLAG の詳細は説明しません & 設定内容もベストプラクティスではないです。

f:id:kakkotetsu:20170513210542j:plain

ruby 環境は nwman というノードで、Ansible 環境は ansible というノードで動かしますが、分けているのは単に元々あった環境の都合です。

ホスト環境

ruby 環境

  • OS:CentOS release 6.5
  • rubyruby 2.1.2p95 (2014-05-08 revision 45877) [x86_64-linux]
  • gem:arista-eapi (0.11.1)
  • gem:json (1.8.1)
  • gem:rest-client (1.7.2)

Ansible 環境

  • OS:Ubuntu-14.04.01-server-amd64
  • ansible:1.8.2
  • ansible-eos:v0.1.2

Arista 環境

  • vEOS Aboot-veos:Aboot-veos-2.1.0
  • vEOS OS:vEOS-4.14.2F
  • vEOS CPU:1core
  • vEOS Memory:1024MB

eAPI 操作内容、利用 API

今回は上記構成にて「VLAN 30 と同じトポロジで VLAN 31 を作る」という操作をします。つまり

  1. 対象機の VLAN 状態を取得して「VLAN 30 が存在するか」・「存在する場合、VLAN 30 をどこの物理 IF に trunk で通しているか」を判断
  2. 1 で VLAN 30 が存在する場合、VLAN 31 を作る
  3. 1 で VLAN 30 が存在する場合、VLAN 30 を通している全物理 IF に VLAN 31 を追加で通す
  4. 2-3 を実施した場合、メモリに設定保存

てな流れです。
同じことを ruby と Ansible 両方で試します。

1 では "show vlan" をリクエストに埋め込んで状態取得します。
レスポンスの例は以下の感じです。(後述の "Exploler" で実行したコピペ)

{
   "jsonrpc": "2.0",
   "result": [
      {
         "sourceDetail": "",
         "_meta": {
            "execDuration": 0.04,
            "execStartTime": 1417437611.14
         },
         "vlans": {
            "1": {
               "status": "active",
               "name": "default",
               "interfaces": {
                  "Ethernet2": {
                     "annotation": "port channel configuration",
                     "privatePromoted": false
                  },
                  "Port-Channel1": {
                     "privatePromoted": false
                  },
                  "Ethernet1": {
                     "annotation": "port channel configuration",
                     "privatePromoted": false
                  }
               },
               "dynamic": false
            }
         }
      }
   ],
   "id": "CapiExplorer-123"
}

事前準備

vEOS x4 (spine001/002, leaf001/002)のデプロイ

以下リンクを参考に作っておきます。公式に、各種ハイパーバイザごとの導入手順詳細とかもあります。

vEOS x4 で eAPI 有効化

有効化設定

公式 "Introduction to Managing EOS Devices – Setting up Management" の "1.10 eAPI" 通りで。
本項ではプロトコルに HTTP を使います。(HTTPS で vEOS 自己証明書の対応するのが面倒ってだけの理由です。)
デフォルトは HTTPS で、以下は HTTP で動かす例です。

leaf001#conf t
leaf001(config)#management api http-commands 
leaf001(config-mgmt-api-http-cmds)#no protocol https
leaf001(config-mgmt-api-http-cmds)#protocol http
leaf001(config-mgmt-api-http-cmds)#no shutdown

http が動いていることを確認します。

leaf002#show management api http-commands
Enabled:        Yes
HTTPS server:   shutdown, set to use port 443
HTTP server:    running, set to use port 80
VRF:            default
Hits:           0
Last hit:       never
Bytes in:       0
Bytes out:      0
Requests:       0
Commands:       0
Duration:       0.000 seconds
URLs
--------------------------------------
Management1 : http://192.168.101.53:80

ブラウザで eAPI 簡易動作確認

vEOS の Management に疎通可能な端末のブラウザで http[s]://<ip_address_switch>/ にアクセスします。
認証は vEOS に作ってあるアカウント/パスワードで。

"Command Documentation" では参照(show)系 API の仕様が見られます。設定系はなかったです。

f:id:kakkotetsu:20170513212924j:plain

"Explorer" ではリクエストのパラメータを少し入力するだけで簡易に試せます。(ブラウザの RestClient アドオン的な)

f:id:kakkotetsu:20170513212956j:plain

"Overview" ではエラーコードの説明などが見られます。スタートアップガイド的な。

f:id:kakkotetsu:20170513213009j:plain

2014/12/14 現在、eAPI ドキュメントは Web 上やマニュアルとしては整備されていなくて、ここで見られる情報が一番豊富だと思います。
しかし公式の QA (Supported Commands for eAPI? ) を見ると、今後マニュアルが整備されるのかも知れません。

vEOS セットアップ(その他)~実行前 config

詳細は省略しますが、VLAN や物理IF などの設定を構成図のようにセットアップしました。
ruby 版も Ansible 版も、これが実行前 config になります。

  • spine001
! Command: show running-config
! device: spine001 (vEOS, EOS-4.14.2F)
!
! boot system flash:/vEOS.swi
!
alias ztpprep bash sudo /mnt/flash/ztpprep
!
transceiver qsfp default-mode 4x10G
!
lldp timer 5
!
hostname spine001
!
spanning-tree mode mstp
no spanning-tree vlan 4094
!
no aaa root
!
username kotetsu secret 5 $1$H02U1Fnf$JeNAkt5krejSEFwYo7ymu1
!
clock timezone Japan
!
vlan 30,4094
!
interface Port-Channel1
   description DEV=leaf001 IF=Po1
   switchport trunk allowed vlan 30
   switchport mode trunk
   mlag 1
!
interface Port-Channel2
   description DEV=leaf002 IF=Po1
   mlag 2
!
interface Port-Channel100
   description DEV=spine002 IF=Po100
   switchport trunk allowed vlan 30,4094
   switchport mode trunk
!
interface Ethernet1
   description DEV=leaf001 IF=Eth1
   channel-group 1 mode active
!
interface Ethernet2
   description DEV=leaf002 IF=Eth1
   channel-group 2 mode active
!
interface Ethernet3
   description DEV=spine002 IF=Eth3
   channel-group 100 mode active
!
interface Ethernet4
   description DEV=spine002 IF=Eth4
   channel-group 100 mode active
!
interface Management1
   ip address 192.168.101.50/24
!
interface Vlan4094
   description MC-LAG dedicated PeerLink
   no autostate
   ip address 192.0.2.1/30
!
no ip routing
!
mlag configuration
   domain-id DOMAIN_MLAG
   heartbeat-interval 2500
   local-interface Vlan4094
   peer-address 192.0.2.2
   peer-link Port-Channel100
   reload-delay 150
!
management api http-commands
   no protocol https
   protocol http
   no shutdown
!
!
end
  • spine002
! Command: show running-config
! device: spine002 (vEOS, EOS-4.14.2F)
!
! boot system flash:/vEOS.swi
!
transceiver qsfp default-mode 4x10G
!
lldp timer 5
!
hostname spine002
!
spanning-tree mode mstp
no spanning-tree vlan 4094
!
no aaa root
!
username kotetsu secret 5 $1$HDrZK8m8$A13QQaIqjLdrvik2.3cm9.
!
clock timezone Japan
!
vlan 30,4094
!
interface Port-Channel1
   description DEV=leaf001 IF=Po1
   switchport trunk allowed vlan 30
   switchport mode trunk
   mlag 1
!
interface Port-Channel2
   description DEV=leaf002 IF=Po1
   mlag 2
!
interface Port-Channel100
   description DEV=spine001 IF=Po100
   switchport trunk allowed vlan 30,4094
   switchport mode trunk
!
interface Ethernet1
   description DEV=leaf001 IF=Eth2
   channel-group 1 mode active
!
interface Ethernet2
   description DEV=leaf002 IF=Eth2
   channel-group 2 mode active
!
interface Ethernet3
   description DEV=spine001 IF=Eth3
   channel-group 100 mode active
!
interface Ethernet4
   description DEV=spine001 IF=Eth4
   channel-group 100 mode active
!
interface Management1
   ip address 192.168.101.51/24
!
interface Vlan4094
   description MC-LAG dedicated PeerLink
   no autostate
   ip address 192.0.2.2/30
!
no ip routing
!
mlag configuration
   domain-id DOMAIN_MLAG
   heartbeat-interval 2500
   local-interface Vlan4094
   peer-address 192.0.2.1
   peer-link Port-Channel100
   reload-delay 150
!
management api http-commands
   no protocol https
   protocol http
   no shutdown
!
!
end
  • leaf001
! Command: show running-config
! device: leaf001 (vEOS, EOS-4.14.2F)
!
! boot system flash:/vEOS.swi
!
alias ztpprep bash sudo /mnt/flash/ztpprep
!
transceiver qsfp default-mode 4x10G
!
lldp timer 5
!
hostname leaf001
!
spanning-tree mode mstp
!
no aaa root
!
username kotetsu secret 5 $1$HDrZK8m8$A13QQaIqjLdrvik2.3cm9.
!
clock timezone Japan
!
vlan 30
!
interface Port-Channel1
   description DEV=spine001_002 IF=mlag1
   switchport trunk allowed vlan 30
   switchport mode trunk
!
interface Ethernet1
   description DEV=spine001 IF=Eth1
   channel-group 1 mode active
!
interface Ethernet2
   description DEV=spine002 IF=Eth1
   channel-group 1 mode active
!
interface Ethernet3
!
interface Ethernet4
   switchport access vlan 30
   switchport trunk allowed vlan 30
   switchport mode trunk
!
interface Management1
   ip address 192.168.101.52/24
!
no ip routing
!
management api http-commands
   no protocol https
   protocol http
   no shutdown
!
!
end
  • leaf002
! Command: show running-config
! device: leaf002 (vEOS, EOS-4.14.2F)
!
! boot system flash:/vEOS.swi
!
alias ztpprep bash sudo /mnt/flash/ztpprep
!
transceiver qsfp default-mode 4x10G
!
lldp timer 5
!
hostname leaf002
!
spanning-tree mode mstp
!
no aaa root
!
username kotetsu secret 5 $1$HDrZK8m8$A13QQaIqjLdrvik2.3cm9.
!
clock timezone Japan
!
interface Port-Channel1
   description DEV=spine001_002 IF=mlag2
!
interface Ethernet1
   description DEV=spine001 IF=Eth2
   channel-group 1 mode active
!
interface Ethernet2
   description DEV=spine002 IF=Eth2
   channel-group 1 mode active
!
interface Management1
   ip address 192.168.101.53/24
!
no ip routing
!
management api http-commands
   no protocol https
   protocol http
   no shutdown
!
!
end

ruby (arista-eapi gem) 版

どんな gem?

Arista 公式が出しているものではないです。

特徴は以下の感じです。

  • REST 周りの処理、リクエスト生成、JSON レスポンスのパースをしてくれる (ので、ユーザがやりたいことだけコーディングできる)
  • リクエストのコマンド群やレスポンスは Array で表現される
  • レスポンスの JSON は symbolize された Hash で取得できる
  • 任意のコマンドを実行できる run メソッドと、コマンドを書かずに簡易に情報取得可能なメソッド(2014/12 現在は version だけ)がある

run メソッドさえあれば困ることはないので、本項ではそれでやります。

インストール

普通に gem install で入れました。

# gem install arista-eapi

# gem list

*** LOCAL GEMS ***

arista-eapi (0.11.1)
json (1.8.1, 1.7.7)
mime-types (2.4.3)
netrc (0.8.0)
rest-client (1.7.2)

スクリプト

  • sample_eapi.rb
#!/usr/bin/env ruby
require 'arista/eapi'
require 'stringio'
require 'io/console'


device_list = Array.new()
File.open(ARGV[0]) do |f|
  f.each_line do |line|
    device_list << line
  end
end


print ("username :")
username = $stdin.gets.to_s.chomp!

print ("password :")
password = STDIN.noecho(&:gets).to_s.chomp!


device_list.flatten.each do |target|
  target.chomp!
  
  puts "\n--------------------- Start #{target} ---------------------\n"
  array_interfaces = Array.new()
  device = Arista::EAPI::Switch.new(target, username, password, protocol = 'http')
  results = device.run(["show vlan"])
  
  unless (results.nil? || results[0].nil? || results[0][:vlans].nil? || results[0][:vlans][:"30"].nil?) then
    results[0][:vlans][:"30"][:interfaces].each_key do |interface_name|
      array_interfaces << interface_name.to_s
    end
    
    # create vlan & set interface switchport
    unless array_interfaces.nil? then
      results << device.run(["enable", "configure", "vlan 31"])
      puts "Created vlan 31..."
      array_interfaces.each do |interface|
        results << device.run(["enable", "configure", "interface #{interface}", "switchport trunk allowed vlan add 31"])
        puts "Added trunk vlan 31 to #{interface}"
      end
      results << device.run(["enable", "write memory"])
      puts "wrote memory!!"
    end
  end
  puts "\n--------------------- End #{target} ---------------------\n"
end

device_list.list はこんな感じで。

192.168.101.50
192.168.101.51
192.168.101.52
192.168.101.53

内容補足

  • require 'arista/eapi' で arista-eapi をインポート
  • 実行対象は device_list ファイルに外出し、スクリプト実行時に引数で渡す (処理はシーケンシャル)
  • eAPI 認証用の username と password は標準入力で interactive に
  • 結果は標準出力のみ
  • 設定投入時など、エラーハンドリングは割と適当 (results という Array に格納しているだけ)

実行~状態確認

$ ruby eapi_sample.rb device_list_arista.list 
username :kotetsu
password :
--------------------- Start 192.168.101.50 ---------------------
Created vlan 31...
Added trunk vlan 31 to port-channel1
Added trunk vlan 31 to port-channel100
wrote memory!!

--------------------- End 192.168.101.50 ---------------------

--------------------- Start 192.168.101.51 ---------------------
Created vlan 31...
Added trunk vlan 31 to port-channel1
Added trunk vlan 31 to port-channel100
wrote memory!!

--------------------- End 192.168.101.51 ---------------------

--------------------- Start 192.168.101.52 ---------------------
Created vlan 31...
Added trunk vlan 31 to port-channel1
Added trunk vlan 31 to ethernet4
wrote memory!!

--------------------- End 192.168.101.52 ---------------------

--------------------- Start 192.168.101.53 ---------------------

--------------------- End 192.168.101.53 ---------------------

vEOS で VLAN 状態や log から、想定通り設定されていることを確認します。

spine001#show vlan 
VLAN  Name                             Status    Ports
----- -------------------------------- --------- -------------------------------
1     default                          active    Po2
30    VLAN0030                         active    Po1, Po100
31    VLAN0031                         active    Po1, Po100
4094  VLAN4094                         active    Cpu, Po100


pine002#show vlan 
VLAN  Name                             Status    Ports
----- -------------------------------- --------- -------------------------------
1     default                          active    Po2
30    VLAN0030                         active    Po1, Po100
31    VLAN0031                         active    Po1, Po100
4094  VLAN4094                         active    Cpu, Po100


leaf001#show vlan 
VLAN  Name                             Status    Ports
----- -------------------------------- --------- -------------------------------
1     default                          active    Et3
30    VLAN0030                         active    Et4, Po1
31    VLAN0031                         active    Et4, Po1


leaf002#show vlan
VLAN  Name                             Status    Ports
----- -------------------------------- --------- -------------------------------
1     default                          active
spine001#show logging last 4 minutes 
Dec  9 00:00:37 spine001 Capi: %SYS-5-CONFIG_E: Enter configuration mode from console by kotetsu on command-api (192.168.101.20)
Dec  9 00:00:38 spine001 Capi: %SYS-5-CONFIG_I: Configured from console by kotetsu on command-api (192.168.101.20)
Dec  9 00:00:38 spine001 Capi: %SYS-5-CONFIG_E: Enter configuration mode from console by kotetsu on command-api (192.168.101.20)
Dec  9 00:00:38 spine001 Capi: %SYS-5-CONFIG_I: Configured from console by kotetsu on command-api (192.168.101.20)
Dec  9 00:00:38 spine001 Capi: %SYS-5-CONFIG_E: Enter configuration mode from console by kotetsu on command-api (192.168.101.20)
Dec  9 00:00:38 spine001 Capi: %SYS-5-CONFIG_I: Configured from console by kotetsu on command-api (192.168.101.20)
Dec  9 00:00:39 spine001 Capi: %SYS-5-CONFIG_STARTUP: Startup config saved from system:/running-config by kotetsu on command-api (192.168.101.20).

Ansible (arista.eos Role) 版

Ansible 初心者なので、Ansibler の皆様からの突っ込み大歓迎です。

どんな Role?

Arista が公式で出している EOS(vEOS でも良い)用の Ansible role です。

github の README を見ると手っ取り早いですが、特徴は以下。

  • eos_command という任意のコマンドを実行できるモジュールと、eos_<機能名> という抽象化されてコマンドを直接書く必要ないモジュール(限定的ではある)が含まれている
  • Ansible リモート実行仕組み(Ansible quickstart のスライド16が分かり易い)を前提としていて、EOS 側で python スクリプトが実行されるので eAPI のクライアントも EOS 側になる(127.0.0.1 にリクエスト)

本項では汎用性が高い eos_command のみを使います。

セットアップ

vEOS に ssh 公開鍵をバラまく

公式 README に従います。
Ansible では interactive にパスワードを入力させる仕組みもあるようなので、それを使う場合には本手順は不要だと思います。

実行対象全台に実行します。
今回は手動で実行しましたが、Ansible で鍵をバラまく方法もあったり(未試行)、Arista の場合 ZTP でやってしまう手もあるかと。

まずは、vEOS で SCP 転送用のユーザ(ansible)とディレクトリを作ります。

spine001>en
spine001#bash

Arista Networks EOS shell

[kotetsu@spine001 ~]$
[kotetsu@spine001 ~]$ sudo useradd -d /persist/local/ansible -G eosadmin ansible
[kotetsu@spine001 ~]$ echo password | sudo passwd --stdin ansible
Changing password for user ansible.
passwd: all authentication tokens updated successfully.
[kotetsu@spine001 ~]$ sudo mkdir /persist/local/ansible/.ssh
[kotetsu@spine001 ~]$ sudo chmod 700 /persist/local/ansible/.ssh
[kotetsu@spine001 ~]$ sudo chown ansible:eosadmin /persist/local/ansible/.ssh
[kotetsu@spine001 ~]$ sudo ls -lah /persist/local/ansible
total 16K
drwx------ 3 ansible ansible  160 Dec 13 00:24 .
drwxrwxrwx 3 root    root      60 Dec 13 00:24 ..
-rw-r--r-- 1 ansible ansible   17 Sep 24 02:58 .bash_logout
-rw-r--r-- 1 ansible ansible   17 Sep 24 02:58 .bash_logout.Eos
-rw-r--r-- 1 ansible ansible  176 Jun 22  2011 .bash_profile
-rw-r--r-- 1 ansible ansible  124 Jun 22  2011 .bashrc
-rw-r--r-- 1 ansible ansible    0 Sep 24 02:58 .dircolors
drwx------ 2 ansible eosadmin  40 Dec 13 00:24 .ssh
[kotetsu@spine001 ~]$
$ logout
spine001#
spine001#
spine001#exit

ansible 側から、ansible 実行時に使う公開鍵を SCP で vEOS 全台に転送します。
再起動時に ansible ユーザが作られる(& SCP 転送で使った一時的なパスワードはなくなる)ように、各 vEOS の /mnt/flash/rc.eos を作成します。

$ scp /home/kotetsu/.ssh/id_rsa.pub ansible@192.168.101.50:.ssh/authorized_keys
Password:


$ ssh ansible@192.168.101.50

Arista Networks EOS shell

[ansible@spine001 ~]$ vi /mnt/flash/rc.eos
#!/bin/sh
useradd -d /persist/local/ansible -G eosadmin ansible

[ansible@spine001 ~]$ sudo reboot
[ansible@spine001 ~]$
Broadcast message from ansible@spine001
        (/dev/pts/3) at 0:28 ...

The system is going down for reboot NOW!

$ ssh ansible@192.168.101.50

Arista Networks EOS shell

[ansible@spine001 ~]$

ansible に Role インストール

Ansible Galaxy に登録されているので、そこから取ってこれます。

$ sudo ansible-galaxy install arista.eos
- downloading role 'eos', owned by arista
- downloading role from https://github.com/arista-eosplus/ansible-eos/archive/v0.1.2.tar.gz
- extracting arista.eos to /etc/ansible/roles/arista.eos
- arista.eos was installed successfully

/etc/ansible/roles/arista.eos/ 配下に落ちてきています。

$ ls -alh /etc/ansible/roles/arista.eos/
total 60K
drwxr-xr-x 9 root root 4.0K Dec 11 00:43 .
drwxr-xr-x 3 root root 4.0K Dec 11 00:43 ..
-rw-rw-r-- 1 root root  687 Aug 28 10:18 CHANGELOG.md
drwxr-xr-x 2 root root 4.0K Dec 11 00:43 defaults
drwxr-xr-x 2 root root 4.0K Dec 11 00:43 files
-rw-rw-r-- 1 root root  544 Aug 28 10:18 .gitignore
drwxr-xr-x 2 root root 4.0K Dec 11 00:43 handlers
drwxr-xr-x 2 root root 4.0K Dec 11 00:43 library
-rw-rw-r-- 1 root root 1.5K Aug 28 10:18 LICENSE
drwxr-xr-x 2 root root 4.0K Dec 11 00:43 meta
-rw-rw-r-- 1 root root  11K Aug 28 10:18 README.md
drwxr-xr-x 2 root root 4.0K Dec 11 00:43 tasks
drwxr-xr-x 2 root root 4.0K Dec 11 00:43 vars

ansible 用 hosts ファイル作成

ベストプラクティス はガン無視して、適当な work ディレクトリに作りました。
なんか Ansible オレオレベストプラクティス とかもあるし、使い方によってディレクトリ構成のベストプラクティスも変りそうですね?

[spine]
192.168.101.[50:51]

[leaf]
192.168.101.[52:53]

[arista_all]
spine
leaf

ping pong

ansible から sftp で vEOS に疎通確認します。

$ ansible -i hosts 192.168.101.50 -u ansible -m ping
192.168.101.50 | success >> {
    "changed": false,
    "ping": "pong"
}

Playbook

hosts ファイルと同じディレクトリに playbook_eapi_vlan.yml として突っ込みました。

- name: eos nodes
  hosts: arista_all
  gather_facts: yes
  sudo: true

  vars:
    eapi_username: kotetsu
    eapi_password: kotetsu
    eapi_protocol: http

  roles:
    - role: arista.eos

  tasks:
    - name: get vlans
      action: eos_command
      args: {
         commands: [
           "show vlan"
         ],
         eapi_username: "{{ eapi_username }}",
         eapi_password: "{{ eapi_password }}",
         eapi_protocol: "{{ eapi_protocol }}"
      }
      register: vlans

    - name: vlan30.exist?
      set_fact:
         interfaces: "{{ vlans.output[0].response.vlans[\"30\"].interfaces }}"
      ignore_errors: True

    - name: create vlan 31
      action: eos_command
      args: {
         commands: [
           "enable",
           "configure",
           "vlan 31",
         ],
         eapi_username: "{{ eapi_username }}",
         eapi_password: "{{ eapi_password }}",
         eapi_protocol: "{{ eapi_protocol }}"
      }

    - name: allowed vlan add 31 to interfaces
      action: eos_command
      args: {
         commands: [
           "enable",
           "configure",
           "interface {{ item.key }}",
           "switchport trunk allowed vlan add 31"
         ],
         eapi_username: "{{ eapi_username }}",
         eapi_password: "{{ eapi_password }}",
         eapi_protocol: "{{ eapi_protocol }}"
      }
      with_dict: interfaces
      register: showint

    - name: save config
      action: eos_command
      args: {
         commands: [
           "enable",
           "write memory"
         ],
         eapi_username: "{{ eapi_username }}",
         eapi_password: "{{ eapi_password }}",
         eapi_protocol: "{{ eapi_protocol }}"
      }

内容補足

  • eAPI 認証用の username, password は何と vars: にベタ書き!!
  • roles: で今回セットアップした arista.eos を指定
  • hosts: で実行対象を hosts に書いた arista_all つまり全台指定
  • vlan30.exist? という task で leaf002 は Failure になり、それ以降の設定系 task は実行されない。途中で止まらないように、当該 task で ignore_errors: True にしている。(when とか使って頑張ろうとしたがモゴモゴ)

Playbook 実行~状態確認

$ ansible-playbook playbook_eapi_vlan.yml -f 10 -u ansible -i hosts

PLAY [eos nodes] **************************************************************

GATHERING FACTS ***************************************************************
ok: [192.168.101.50]
ok: [192.168.101.53]
ok: [192.168.101.51]
ok: [192.168.101.52]

TASK: [arista.eos | check if running on eos node] *****************************
ok: [192.168.101.53]
ok: [192.168.101.52]
ok: [192.168.101.51]
ok: [192.168.101.50]

TASK: [arista.eos | collect eos facts] ****************************************
ok: [192.168.101.52]
ok: [192.168.101.51]
ok: [192.168.101.53]
ok: [192.168.101.50]

TASK: [arista.eos | include eos variables] ************************************
ok: [192.168.101.50]
ok: [192.168.101.51]
ok: [192.168.101.53]
ok: [192.168.101.52]

TASK: [arista.eos | check for working directory] ******************************
ok: [192.168.101.52]
ok: [192.168.101.50]
ok: [192.168.101.51]
ok: [192.168.101.53]

TASK: [arista.eos | create source] ********************************************
skipping: [192.168.101.50]
skipping: [192.168.101.51]
skipping: [192.168.101.52]
skipping: [192.168.101.53]

TASK: [arista.eos | check if pip is installed] ********************************
ok: [192.168.101.50]
ok: [192.168.101.53]
ok: [192.168.101.52]
ok: [192.168.101.51]

TASK: [arista.eos | copy pip extension to node] *******************************
skipping: [192.168.101.50]
skipping: [192.168.101.51]
skipping: [192.168.101.53]
skipping: [192.168.101.52]

TASK: [arista.eos | create tmp config file to load pip] ***********************
skipping: [192.168.101.50]
skipping: [192.168.101.51]
skipping: [192.168.101.52]
skipping: [192.168.101.53]

TASK: [arista.eos | load pip eos extension] ***********************************
skipping: [192.168.101.51]
skipping: [192.168.101.50]
skipping: [192.168.101.53]
skipping: [192.168.101.52]

TASK: [arista.eos | copy required libraries to node] **************************
ok: [192.168.101.50] => (item=eapilib-0.1.0.tar.gz)
ok: [192.168.101.51] => (item=eapilib-0.1.0.tar.gz)
ok: [192.168.101.53] => (item=eapilib-0.1.0.tar.gz)
ok: [192.168.101.52] => (item=eapilib-0.1.0.tar.gz)

TASK: [arista.eos | install required libraries] *******************************
ok: [192.168.101.53] => (item=eapilib-0.1.0.tar.gz)
ok: [192.168.101.52] => (item=eapilib-0.1.0.tar.gz)
ok: [192.168.101.51] => (item=eapilib-0.1.0.tar.gz)
ok: [192.168.101.50] => (item=eapilib-0.1.0.tar.gz)

TASK: [arista.eos | install jsonrpclib] ***************************************
skipping: [192.168.101.50]
skipping: [192.168.101.53]
skipping: [192.168.101.51]
skipping: [192.168.101.52]

TASK: [arista.eos | install required libraries and dependencies] **************
skipping: [192.168.101.50] => (item=eapilib-0.1.0.tar.gz)
skipping: [192.168.101.52] => (item=eapilib-0.1.0.tar.gz)
skipping: [192.168.101.51] => (item=eapilib-0.1.0.tar.gz)
skipping: [192.168.101.53] => (item=eapilib-0.1.0.tar.gz)

TASK: [get vlans] *************************************************************
ok: [192.168.101.52]
ok: [192.168.101.50]
ok: [192.168.101.51]
ok: [192.168.101.53]

TASK: [vlan30.exist?] *********************************************************
ok: [192.168.101.50]
ok: [192.168.101.52]
ok: [192.168.101.51]
fatal: [192.168.101.53] => One or more undefined variables: 'dict object' has no attribute '30'

TASK: [create vlan 31] ********************************************************
ok: [192.168.101.52]
ok: [192.168.101.50]
ok: [192.168.101.51]

TASK: [allowed vlan add 31 to interfaces] *************************************
ok: [192.168.101.50] => (item={'key': u'Port-Channel1', 'value': {u'privatePromoted': False}})
ok: [192.168.101.52] => (item={'key': u'Port-Channel1', 'value': {u'privatePromoted': False}})
ok: [192.168.101.51] => (item={'key': u'Port-Channel1', 'value': {u'privatePromoted': False}})
ok: [192.168.101.50] => (item={'key': u'Port-Channel100', 'value': {u'privatePromoted': False}})
ok: [192.168.101.52] => (item={'key': u'Ethernet4', 'value': {u'privatePromoted': False}})
ok: [192.168.101.51] => (item={'key': u'Port-Channel100', 'value': {u'privatePromoted': False}})

TASK: [save config] ***********************************************************
ok: [192.168.101.52]
ok: [192.168.101.50]
ok: [192.168.101.51]

PLAY RECAP ********************************************************************
           to retry, use: --limit @/home/kotetsu/playbook_eapi_vlan.retry

192.168.101.50             : ok=14   changed=0    unreachable=0    failed=0
192.168.101.51             : ok=14   changed=0    unreachable=0    failed=0
192.168.101.52             : ok=14   changed=0    unreachable=0    failed=0
192.168.101.53             : ok=10   changed=0    unreachable=1    failed=0

上記には残っていませんが、初めて ansible で vEOS に playbook を流した時には library のインストールとかの task も動いて changed もカウントされます。

vEOS 実機を見ると以下の感じです。ログの設定次第で、もう少し細かい logging 情報が見られるかもです。

leaf001#show vlan
VLAN  Name                             Status    Ports
----- -------------------------------- --------- -------------------------------
1     default                          active    Et3
30    VLAN0030                         active    Et4, Po1
31    VLAN0031                         active    Et4, Po1

leaf001#show logging last 5 minutes
Dec 14 00:44:03 leaf001 Capi: %SYS-5-CONFIG_E: Enter configuration mode from console by kotetsu on command-api (127.0.0.1)
Dec 14 00:44:03 leaf001 Capi: %SYS-5-CONFIG_I: Configured from console by kotetsu on command-api (127.0.0.1)
Dec 14 00:44:04 leaf001 Capi: %SYS-5-CONFIG_E: Enter configuration mode from console by kotetsu on command-api (127.0.0.1)
Dec 14 00:44:04 leaf001 Capi: %SYS-5-CONFIG_I: Configured from console by kotetsu on command-api (127.0.0.1)
Dec 14 00:44:05 leaf001 Capi: %SYS-5-CONFIG_E: Enter configuration mode from console by kotetsu on command-api (127.0.0.1)
Dec 14 00:44:05 leaf001 Capi: %SYS-5-CONFIG_I: Configured from console by kotetsu on command-api (127.0.0.1)
Dec 14 00:44:06 leaf001 Capi: %SYS-5-CONFIG_STARTUP: Startup config saved from system:/running-config by kotetsu on command-api (127.0.0.1).

おしまい

REST と netconf のメーカ実装動向(2014/12/14 時点)

最近はようやく構造化された情報取得が出来るアレコレを実装した NW 機器が増えてきて、利用者としては嬉しいことです。
自分が抑えている範囲では、以下の感じです。(SNMP や OpenFlow は除外)

  • Juniper JUNOS:netconf と最近は一部ハイエンド機器で REST
  • Brocade NOS:netconf と最近は REST
  • Cisco NX-OS:netconf
  • Arista EOS:REST
  • F5 BIG-IP:SOAP(iControl) と最近は REST
  • 日立電線 Apresia Aeos:最近は netconf (ただし、parse しにくい CLI 出力がそのまま埋め込まれているレスポンスなので、正規表現で頑張れ)
  • Vyos:REST 実装予定らしい
  • 故 Vyatta Core:有償版では REST 対応していたらしい

何かしら統一されるとベターですけどねー。

それと、今回紹介した 2 つはいずれも ShowNet2014 活動レポート (pdf) のスライド 62 で言うところの py-junos-eznc と同じ位置づけ(特定メーカのNetwork OS に特化して便利な機能を提供するライブラリ)ですね。

雑感

  • リクエストに CLI コマンドそのまま埋め込む実装は、敬遠されそうな一方で「API 未実装だからこの機能は使えない…自分で追加実装しないと」っていう面倒ごとは起き難くて(CLI で出来ることは大抵出来る)、悪くないと思います。
  • レスポンスが構造化されていると、走査が楽で良いですねー。ただ、深い階層のデータをスマートに弄るのはムズいので修行しましょう。
  • 構造化されたレスポンスで…ってなると、netconf とかも良いですね。snmp はちょっと…。ただ、XML より JSON の方が人間にとっては可読性も高いし、扱い易いかも。
  • 自動化系ツールでいうと、最近は chef や puppet に対応する NW 機器もポロポロ(Juniper QFX とか Arista とか)ありますね。ただ、エージェント必須型のやつは昔ながらの NW 機器で使えないし、事前準備が大変すぎて、個人的にはちょっと…。Ansible も今回とりあげた範囲では「Python 2.6 以上必須」になる動きですが、やり方次第ではそれも不要になるので許容範囲です。
  • ruby の方はスクリプトでちょちょいと繰り返しや条件指定出来て良いですね。Ansible(& jinja2) はそこの表現が初心者には敷居が高かったです。
  • とはいえ、システム全体を統合管理することを考えると、Ansible の Role,Module 型の作りは良いと思います。最近は何でもプラグインで追加実装できる、ってのが当然のようになってきてますよね。
  • これが俺の SDN だ (きっぱり

Arista の Zero Touch Provisioning (ZTP) を試す (Dynamic Provisioning 編) (original : 2014/11/29)

この記事は某所で 2014/11/29 に書いたもののコピーです。
そのため 2017/05/13 時点ではやや古い情報も含まれています。

概要

本項でやること

ZTP を使って、Arista vEOS x2台の初期設定投入をしてみます。
対象機器(Arista)の System MAC アドレスや シリアル番号を事前登録する必要なく、クライアントの LLDP 状態で条件付けして初期設定の動的生成やパラメータ払い出し管理をできるよ、ってところを見ます。

  • 方式は Dynamic Provisioning で、対象 vEOS 識別に ztpserver の仕組み(neighbordb)を使う
  • 設定ファイルは ztpserver の仕組み(bootstrap と template と definition)を使って動的に生成
  • パラメータの払い出しは ztpserver の仕組み(Resource pools)を使う

Static Provisioning(System MAC アドレスやシリアル番号を事前登録する方式)は Arista の Zero Touch Provisioning (ZTP) を試す (Static Provisioning 編) を参照。

Arista EOS 自体については、以下の本をオススメしときますね。

Arista Warrior: A Real-World Guide to Understanding Arista Switches and EOS (English Edition)

Arista Warrior: A Real-World Guide to Understanding Arista Switches and EOS (English Edition)

Arista Warrior: A Real-World Guide to Understanding Arista Switches and EOS

Arista Warrior: A Real-World Guide to Understanding Arista Switches and EOS

Resource pools とは?

公式の説明 を読むのが手っ取り早いです。
ノード単位で付与するパラメータをプール化しておき、ZTP 実行時に ztpserver が勝手に空いているところを使って利用済みにしてくれる、簡単なリソース管理までやってくれる方式です。

結果概要

ztpserver v1.1.0 で試行した時点では、課題が残りました。いまいちスッキリしてません。詳細は後述します。

  • クライアント(vEOS)識別に neighbordb で LLDP 状態マッチングは便利だけど、Arista 側の機能制約起因で実用に少し工夫が必要
  • パラメータ払い出しの Resource pools 動作は微妙なので、現時点では実用に結構な工夫が必要

構成図・環境情報

以下の構成でやります。
物理構成は一般的なデータセンタネットワークのそれで、Arista の Multi-Chassis Link Aggregation (MLAG)を使っています。
本項では MLAG の詳細は説明しません & 設定内容もベストプラクティスではないです。

f:id:kakkotetsu:20170513164238j:plain

  • ztpserver と Arisra vEOS x2 台(spine001/002)はセットアップ済
  • Arista vEOS x2 台(leaf001/002)は VirtualBox に突っ込んだだけで何も設定していない

本項の環境は以下の通りです。vEOS は 4 台とも同じスペック。

  • VirtualBox と ztpserver v1.1.0 は これ
  • vEOS Aboot-veos:Aboot-veos-2.1.0
  • vEOS OS:vEOS-4.14.2F
  • vEOS CPU:1core
  • vEOS Memory:1024MB

vEOS のネットワークアダプタは、構成図の通りです。vEOS 同士の接続は、全部 VirtualBox の intnet で。

事前準備

この辺は軽く…。

ztpserver のインストール~初期設定

これで。

vEOS x4 (spine001/002, leaf001/002)のデプロイ

以下リンクを参考に作っておきます。公式に、各種ハイパーバイザごとの導入手順詳細とかもあります。

vEOS(spine001/002) の設定

必要に応じてネットワークアダプタの追加

VirtualBox の場合は、本構成のように5つ目以降のネットワークアダプタGUI でできないかも(調べてませんが、バージョン次第?)なので、コマンドラインで足します。

VBoxManage modifyvm vEOS-spine001 --nic5 intnet
VBoxManage modifyvm vEOS-spine001 --nicpromisc5 allow-all
VBoxManage modifyvm vEOS-spine001 --intnet5 intnet_ar_02
VBoxManage showvminfo "vEOS-spine001"

VBoxManage modifyvm vEOS-spine002 --nic5 intnet
VBoxManage modifyvm vEOS-spine002 --nicpromisc5 allow-all
VBoxManage modifyvm vEOS-spine002 --intnet5 intnet_ar_02
VBoxManage showvminfo "vEOS-spine002"

設定

設定を晒しておきます。
Arista の操作詳細は以下を参考に。MC-LAG の設定内容もマニュアルのサンプルコンフィグほぼそのままです。

spine001#show run
! Command: show running-config
! device: spine001 (vEOS, EOS-4.14.2F)
!
! boot system flash:/vEOS.swi
!
alias ztpprep bash sudo /mnt/flash/ztpprep
!
transceiver qsfp default-mode 4x10G
!
lldp timer 5
!
hostname spine001
!
spanning-tree mode mstp
no spanning-tree vlan 4094
!
no aaa root
!
username kotetsu secret 5 $1$H02U1Fnf$JeNAkt5krejSEFwYo7ymu1
!
vlan 4094
!
interface Port-Channel1
   description DEV=leaf001 IF=Po1
   mlag 1
!
interface Port-Channel2
   description DEV=leaf002 IF=Po1
   mlag 2
!
interface Port-Channel100
   description DEV=spine002 IF=Po100
   switchport trunk allowed vlan 4094
   switchport mode trunk
!
interface Ethernet1
   description DEV=leaf001 IF=Eth1
!
interface Ethernet2
   description DEV=leaf002 IF=Eth1
!
interface Ethernet3
   description DEV=spine002 IF=Eth3
   channel-group 100 mode active
!
interface Ethernet4
   description DEV=spine002 IF=Eth4
   channel-group 100 mode active
!
interface Management1
   ip address 192.168.101.50/24
!
interface Vlan4094
   description MC-LAG dedicated PeerLink
   no autostate
   ip address 192.0.2.1/30
!
no ip routing
!
mlag configuration
   domain-id DOMAIN_MLAG
   heartbeat-interval 2500
   local-interface Vlan4094
   peer-address 192.0.2.2
   peer-link Port-Channel100
   reload-delay 150
!
management api http-commands
   no shutdown
!
!
end
  • spine002
spine002#show run
! Command: show running-config
! device: spine002 (vEOS, EOS-4.14.2F)
!
! boot system flash:/vEOS.swi
!
transceiver qsfp default-mode 4x10G
!
lldp timer 5
!
hostname spine002
!
spanning-tree mode mstp
no spanning-tree vlan 4094
!
no aaa root
!
username kotetsu secret 5 $1$HDrZK8m8$A13QQaIqjLdrvik2.3cm9.
!
vlan 4094
!
interface Port-Channel1
   description DEV=leaf001 IF=Po1
   mlag 1
!
interface Port-Channel2
   description DEV=leaf002 IF=Po1
   mlag 2
!
interface Port-Channel100
   description DEV=spine001 IF=Po100
   switchport trunk allowed vlan 4094
   switchport mode trunk
!
interface Ethernet1
   description DEV=leaf001 IF=Eth2
!
interface Ethernet2
   description DEV=leaf002 IF=Eth2
!
interface Ethernet3
   description DEV=spine001 IF=Eth3
   channel-group 100 mode active
!
interface Ethernet4
   description DEV=spine001 IF=Eth4
   channel-group 100 mode active
!
interface Management1
   ip address 192.168.101.51/24
!
interface Vlan4094
   description MC-LAG dedicated PeerLink
   no autostate
   ip address 192.0.2.2/30
!
no ip routing
!
mlag configuration
   domain-id DOMAIN_MLAG
   heartbeat-interval 2500
   local-interface Vlan4094
   peer-address 192.0.2.1
   peer-link Port-Channel100
   reload-delay 150
!
management api http-commands
   no shutdown
!
!
end

それぞれ、interface Ethernet1-2 設定内に channel-group 設定を入れていません。
これは Dynamic Provisioning で leaf001,002 と LLDP 情報の交換をするために、以下の Arista 仕様を回避するためです。
「Po で LLDP 関係の設定は出来ないから、Eth の方でやってね」って意味だと思っていたのですが、実際に試した限りは Port-Channel 組んでいる物理 IF では LLDP 交換できませんでした。

12.2 LLDP Overview 12.2.4 Guidelines and Limitations

・LLDP is supported only on physical interfaces.

vEOS (leaf001,002) のネットワーク接続

ZTP 実行対象となる 2 台のネットワークアダプタ設定は、事前に VirtualBox でやっておきます。
「配線して電源 ON するだけで初期設定されるよ!」的な ZTP の謳い文句で言うところの配線にあたります。
まあ、仮想環境なんですけどね。

そんなわけで、内部的には仮想スイッチ的なものが挟まるので vEOS 間の linkUp/Down 挙動を単純に再現することは難しいです。

ztpserver 設定手順

基本的に 公式の Configuration公式の Examples を参照しつつ、必要に応じて ztpserver の source を参照して進めていきます。

neigbordb ファイルの編集 (LLDP 条件と実行処理の紐付け)

Dynamic Provisioning では、ztpserver がクライアントから受け取った LLDP 情報に合わせた処理を実行します。
ここでは、その条件と処理へのリンクの紐付けを /usr/share/ztpserver/neighbordb に定義します。

patterns:
  - name: leaf_dynamic
    definition: def_leaf
    interfaces:
      - Ethernet1: spine001:any
      - Ethernet2: spine002:any

name: は、本ファイル内で一意であれば何でも良い筈です。
definition: は、この後作成する definition のファイル名を記載します。マッチングしたときの処理です。
interfaces: は、マッチング条件です。leaf の場合、Ethernet1 を spine001 に、Ethernet2 を spine002 に接続するという設計にして、leaf がいくら増えてもこの定義が適用されるように spine 側のポートは any 指定しています。

definition ファイルの作成

Static Provisioning の時とほぼ同様ですが、今回は node 単位ではなくて前述の条件単位なので /usr/share/ztpserver/definitions/ 配下に作ります。ファイル名は neighbordb 内で指定した def_leaf で。

/usr/share/ztpserver/definitions/def_leaf

---
name: def_leaf
actions:
  -
    action: add_config
    attributes:
      url: files/templates/ma1.template
      variables:
        ipaddress: allocate('mgmt_subnet')
    name: "configure ma1"
    onstart: "Starting to configure ma1"
    onsuccess: "SUCCESS: ma1 configure"
  -
    action: add_config
    attributes:
      url: files/templates/system.template
      variables:
        hostname: allocate('leaf_hostname')
    name: "configure global system"
    onstart: "Starting to add basic system config"
    onsuccess: "SUCCESS: basic config added"
  -
    action: add_config
    attributes:
      url: files/templates/login.template
    name: "configure auth"
  -
    action: add_config
    attributes:
      url: files/templates/ztpprep.template
    name: "configure ztpprep alias"
  -
    action: add_config
    attributes:
      url: files/templates/uplink.template
      variables:
        spine_downlink: allocate('spine_downlink')
    name: "configure uplink"
    onstart: "Starting to configure uplink"
    onsuccess: "SUCCESS: uplink configured"
  -
    action: copy_file
    always_execute: true
    attributes:
      dst_url: /mnt/flash/
      mode: 777
      overwrite: if-missing
      src_url: files/automate/ztpprep
    name: "automate reload"

variables: で各 template ファイルに定義されている変数を Resource pools から払い出されるように allocate('<Resource pools ファイル名>') で書いています。
この後定義する Resource pools のファイル名を指定する必要があります。

各種 template ファイル作成

definition ファイル内で呼び出している各種テンプレートや bash スクリプトを作っておきます。
これは Static Provisioning の時 と同様です。Static/Dynamic の方式問わず共用できます。

  • /usr/share/ztpserver/files/automate/ztpprep
#!/bin/bash
#delete system files and reload the system for ztp
rm -rf /mnt/flash/startup-config
rm -rf /mnt/flash/*extensions*
shutdown -r now
  • /usr/share/ztpserver/files/templates/ma1.template
interface Management1
  ip address $ipaddress
  no shutdown
  • /usr/share/ztpserver/files/templates/system.template
hostname $hostname
!
!
management api http-commands
   no shutdown
!
lldp timer 5
!
  • /usr/share/ztpserver/files/templates/system.template

username も password も kotetsu です。admin ユーザはデフォルトであります。

username kotetsu secret 5 $1$HDrZK8m8$A13QQaIqjLdrvik2.3cm9.
  • /usr/share/ztpserver/files/templates/ztpprep.template
alias ztpprep bash sudo /mnt/flash/ztpprep
  • /usr/share/ztpserver/files/templates/system.template

これは spine001,002 と接続する Uplink の設定です。

interface Ethernet1
   description DEV=spine001 IF=Eth$spine_downlink
   channel-group 1 mode active
!
interface Ethernet2
   description DEV=spine002 IF=Eth$spine_downlink
   channel-group 1 mode active
!
interface Port-Channel1
   description DEV=spine001_002 IF=mlag$spine_downlink

# 最低限の設定を入れたつもりだったが timezone 位は入れておけばよかった…。

Resource pools ファイル作成 (払い出すパラメータ定義)

  • /usr/share/ztpserver/resources/mgmt_subnet
192.168.101.52/24: null
192.168.101.53/24: null
192.168.101.54/24: null
192.168.101.55/24: null
  • /usr/share/ztpserver/resources/leaf_hostname
leaf001: null
leaf002: null
leaf003: null
leaf004: null
  • /usr/share/ztpserver/resources/spine_downlink
1: null
2: null
3: null
4: null

ZTP 動作確認

ztpserver 起動

Dynamic Provisioning でも /usr/share/ztpserver/ 配下にノード単位のディレクトリが生成されるので、せやかて sudo します。

$ sudo ztps --debug
INFO: [app:115] Logging started for ztpserver
INFO: [app:116] Using repository /usr/share/ztpserver
DEBUG: [controller:776] server URL: http://192.168.101.16:8080
Starting server on http://192.168.101.16:8080

vEOS 再起動

leaf001,002 のどちらからでもいいですが。

startup-config を消して再起動します。いつものように reload 実行時に色々聞かれるので Save するかは no を、Confirm は そのまま Enter で対応します。

# write erase
# reload

VirtualBox のコンソールで vEOS 停止~起動を見守ります。ログインプロンプトが出たら、ZTP がはじまります。

ZTP 実行

vEOS 起動後、「startup-config が無いから ZTP はじめるよ」的なメッセージが出て ZTP シーケンスが開始します。画面的には Static Provisioning の時 と代わり映えしないので省略。

ztpserver 側のログを見ると以下の感じ(debug レベルにしているので長い…)

192.168.101.62 - - [27/Nov/2014 01:11:25] "GET /bootstrap HTTP/1.1" 200 35386
192.168.101.62 - - [27/Nov/2014 01:11:27] "GET /bootstrap/config HTTP/1.1" 200 200
DEBUG: [controller:252] POST /nodes HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, compress
Content-Length: 339
Content-Type: application/json
Host: 192.168.101.16:8080
User-Agent: python-requests/1.2.3 CPython/2.7.0 Linux/3.4.43.Ar-2083164.4142F

{"neighbors": {"Ethernet2": [{"device": "spine002", "port": "Ethernet1"}], "Management1": [{"device": "spine002", "port": "Management1"}, {"device": "spine001", "port": "Management1"}], "Ethernet1": [{"device": "spine001", "port": "Ethernet1"}]}, "version": "4.14.2F", "systemmac": "08:00:27:e2:d0:f4", "model": "vEOS", "serialnumber": ""}

DEBUG: [topology:278] 080027e2d0f4: creating neighbor spine002:Ethernet1 for interface Ethernet2
DEBUG: [topology:278] 080027e2d0f4: creating neighbor spine002:Management1 for interface Management1
DEBUG: [topology:278] 080027e2d0f4: creating neighbor spine001:Management1 for interface Management1
DEBUG: [topology:278] 080027e2d0f4: creating neighbor spine001:Ethernet1 for interface Ethernet1
DEBUG: [topology:140] 080027e2d0f4: created node object Node(serialnumber=, systemmac=080027e2d0f4, neighbors=OrderedCollection([(u'Ethernet2', [Neighbor(device=u'spine002', interface=u'Ethernet1')]), (u'Management1', [Neighbor(device=u'spine002', interface=u'Management1'), Neighbor(device=u'spine001', interface=u'Management1')]), (u'Ethernet1', [Neighbor(device=u'spine001', interface=u'Ethernet1')])]))
DEBUG: [controller:170] 080027e2d0f4: running node_exists
DEBUG: [controller:170] 080027e2d0f4: running post_config
DEBUG: [controller:170] 080027e2d0f4: running post_node
DEBUG: [validators:80] 080027e2d0f4: running NeighbordbValidator.validate
DEBUG: [validators:96] 080027e2d0f4: running NeighbordbValidator.validate_patterns 
DEBUG: [validators:80] 080027e2d0f4: running PatternValidator.validate
DEBUG: [validators:96] 080027e2d0f4: running PatternValidator.validate_attributes for 'leaf_dynamic'
WARNING: [validators:171] 080027e2d0f4: PatternValidator warning: 'leaf_dynamic' is missing optional attribute (node)
WARNING: [validators:171] 080027e2d0f4: PatternValidator warning: 'leaf_dynamic' is missing optional attribute (variables)
DEBUG: [validators:96] 080027e2d0f4: running PatternValidator.validate_definition for 'leaf_dynamic'
DEBUG: [validators:96] 080027e2d0f4: running PatternValidator.validate_interfaces for 'leaf_dynamic'
DEBUG: [validators:80] 080027e2d0f4: running InterfacePatternValidator.validate
DEBUG: [validators:96] 080027e2d0f4: running InterfacePatternValidator.validate_interface_pattern 
DEBUG: [validators:204] 080027e2d0f4: adding interface pattern '{'Ethernet1': 'spine001:any'}' to valid interface patterns
DEBUG: [validators:80] 080027e2d0f4: running InterfacePatternValidator.validate
DEBUG: [validators:96] 080027e2d0f4: running InterfacePatternValidator.validate_interface_pattern 
DEBUG: [validators:204] 080027e2d0f4: adding interface pattern '{'Ethernet2': 'spine002:any'}' to valid interface patterns
DEBUG: [validators:96] 080027e2d0f4: running PatternValidator.validate_name for 'leaf_dynamic'
DEBUG: [validators:96] 080027e2d0f4: running PatternValidator.validate_node for 'leaf_dynamic'
DEBUG: [validators:96] 080027e2d0f4: running PatternValidator.validate_variables for 'leaf_dynamic'
DEBUG: [validators:140] 080027e2d0f4: adding pattern 'leaf_dynamic' ({'definition': 'def_leaf', 'interfaces': [{'Ethernet1': 'spine001:any'}, {'Ethernet2': 'spine002:any'}], 'name': 'leaf_dynamic'}) to valid patterns
DEBUG: [validators:96] 080027e2d0f4: running NeighbordbValidator.validate_variables 
DEBUG: [validators:315] 080027e2d0f4: NeighbordbValidator validation successful
DEBUG: [topology:472] 080027e2d0f4: checking pattern 'leaf_dynamic' entries for variable substitution
DEBUG: [topology:482] 080027e2d0f4: pattern 'leaf_dynamic' variable substitution complete
DEBUG: [topology:369] 080027e2d0f4: pattern 'Pattern(name='leaf_dynamic')' parsed successfully
DEBUG: [topology:108] 080027e2d0f4: loaded neighbordb: Neighbordb(variables=0, globals=1, nodes=0)
DEBUG: [topology:417] 080027e2d0f4: searching for eligible patterns
DEBUG: [topology:426] 080027e2d0f4: all global patterns are eligible
DEBUG: [topology:437] 080027e2d0f4: attempting to match pattern leaf_dynamic
DEBUG: [topology:559] 080027e2d0f4: pattern 'leaf_dynamic' - attempting to match node ("Node(serialnumber=, systemmac=080027e2d0f4, neighbors=OrderedCollection([(u'Ethernet2', [Neighbor(device=u'spine002', interface=u'Ethernet1')]), (u'Management1', [Neighbor(device=u'spine002', interface=u'Management1'), Neighbor(device=u'spine001', interface=u'Management1')]), (u'Ethernet1', [Neighbor(device=u'spine001', interface=u'Ethernet1')])]))")
DEBUG: [topology:569] 080027e2d0f4: pattern 'leaf_dynamic' - attempting to match interface Ethernet2([Neighbor(device=u'spine002', interface=u'Ethernet1')])
DEBUG: [topology:575] 080027e2d0f4: pattern 'leaf_dynamic' - checking interface pattern for Ethernet2: InterfacePattern(interface=Ethernet1, remote_device=spine001, remote_interface=any)
DEBUG: [topology:675] 080027e2d0f4: attempting to match Ethernet2(Neighbor(device=u'spine002', interface=u'Ethernet1')) against interface pattern InterfacePattern(interface=Ethernet1, remote_device=spine001, remote_interface=any)
DEBUG: [topology:575] 080027e2d0f4: pattern 'leaf_dynamic' - checking interface pattern for Ethernet2: InterfacePattern(interface=Ethernet2, remote_device=spine002, remote_interface=any)
DEBUG: [topology:675] 080027e2d0f4: attempting to match Ethernet2(Neighbor(device=u'spine002', interface=u'Ethernet1')) against interface pattern InterfacePattern(interface=Ethernet2, remote_device=spine002, remote_interface=any)
DEBUG: [topology:582] 080027e2d0f4: pattern 'leaf_dynamic' - interface pattern match for Ethernet2: InterfacePattern(interface=Ethernet2, remote_device=spine002, remote_interface=any)
DEBUG: [topology:569] 080027e2d0f4: pattern 'leaf_dynamic' - attempting to match interface Management1([Neighbor(device=u'spine002', interface=u'Management1'), Neighbor(device=u'spine001', interface=u'Management1')])
DEBUG: [topology:575] 080027e2d0f4: pattern 'leaf_dynamic' - checking interface pattern for Management1: InterfacePattern(interface=Ethernet1, remote_device=spine001, remote_interface=any)
DEBUG: [topology:675] 080027e2d0f4: attempting to match Management1(Neighbor(device=u'spine002', interface=u'Management1')) against interface pattern InterfacePattern(interface=Ethernet1, remote_device=spine001, remote_interface=any)
DEBUG: [topology:675] 080027e2d0f4: attempting to match Management1(Neighbor(device=u'spine001', interface=u'Management1')) against interface pattern InterfacePattern(interface=Ethernet1, remote_device=spine001, remote_interface=any)
DEBUG: [topology:595] 080027e2d0f4: pattern 'leaf_dynamic' - interface Management1 did not match any interface patterns
DEBUG: [topology:569] 080027e2d0f4: pattern 'leaf_dynamic' - attempting to match interface Ethernet1([Neighbor(device=u'spine001', interface=u'Ethernet1')])
DEBUG: [topology:575] 080027e2d0f4: pattern 'leaf_dynamic' - checking interface pattern for Ethernet1: InterfacePattern(interface=Ethernet1, remote_device=spine001, remote_interface=any)
DEBUG: [topology:675] 080027e2d0f4: attempting to match Ethernet1(Neighbor(device=u'spine001', interface=u'Ethernet1')) against interface pattern InterfacePattern(interface=Ethernet1, remote_device=spine001, remote_interface=any)
DEBUG: [topology:582] 080027e2d0f4: pattern 'leaf_dynamic' - interface pattern match for Ethernet1: InterfacePattern(interface=Ethernet1, remote_device=spine001, remote_interface=any)
DEBUG: [topology:440] 080027e2d0f4: pattern leaf_dynamic matched
DEBUG: [controller:373] 080027e2d0f4: 1 pattern(s) in neihgbordb are a good match
INFO: [controller:377] 080027e2d0f4: node matched 'leaf_dynamic' pattern in neighbordb
INFO: [controller:399] 080027e2d0f4: new /nodes/080027e2d0f4 folder created
DEBUG: [controller:170] 080027e2d0f4: running dump_node
DEBUG: [controller:170] 080027e2d0f4: running set_location
DEBUG: [controller:182] 080027e2d0f4: response to set_location: {'status': 201, 'location': 'nodes/080027e2d0f4'}
192.168.101.62 - - [27/Nov/2014 01:11:49] "POST /nodes HTTP/1.1" 201 0
DEBUG: [controller:470] GET /nodes/080027e2d0f4 HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, compress
Content-Length: 4
Content-Type: text/html
Host: 192.168.101.16:8080
User-Agent: python-requests/1.2.3 CPython/2.7.0 Linux/3.4.43.Ar-2083164.4142F
Resource: 080027e2d0f4

DEBUG: [topology:278] 080027e2d0f4: creating neighbor spine002:Ethernet1 for interface Ethernet2
DEBUG: [topology:278] 080027e2d0f4: creating neighbor spine002:Management1 for interface Management1
DEBUG: [topology:278] 080027e2d0f4: creating neighbor spine001:Management1 for interface Management1
DEBUG: [topology:278] 080027e2d0f4: creating neighbor spine001:Ethernet1 for interface Ethernet1
DEBUG: [topology:140] 080027e2d0f4: created node object Node(serialnumber=None, systemmac=080027e2d0f4, neighbors=OrderedCollection([('Ethernet2', [Neighbor(device='spine002', interface='Ethernet1')]), ('Management1', [Neighbor(device='spine002', interface='Management1'), Neighbor(device='spine001', interface='Management1')]), ('Ethernet1', [Neighbor(device='spine001', interface='Ethernet1')])]))
DEBUG: [controller:170] 080027e2d0f4: running get_definition
DEBUG: [controller:498] 080027e2d0f4: defintion is nodes/080027e2d0f4/definition ([{'action': 'add_config', 'attributes': {'url': 'files/templates/ma1.template', 'variables': {'ipaddress': "allocate('mgmt_subnet')"}}, 'name': 'configure ma1', 'onstart': 'Starting to configure ma1', 'onsuccess': 'SUCCESS: ma1 configure'}, {'action': 'add_config', 'attributes': {'url': 'files/templates/system.template', 'variables': {'hostname': "allocate('leaf_hostname')"}}, 'name': 'configure global system', 'onstart': 'Starting to add basic system config', 'onsuccess': 'SUCCESS: basic config added'}, {'action': 'add_config', 'attributes': {'url': 'files/templates/login.template'}, 'name': 'configure auth'}, {'action': 'add_config', 'attributes': {'url': 'files/templates/ztpprep.template'}, 'name': 'configure ztpprep alias'}, {'action': 'add_config', 'attributes': {'url': 'files/templates/uplink.template', 'variables': {'spine_downlink': "allocate('spine_downlink')"}}, 'name': 'configure uplink', 'onstart': 'Starting to configure uplink', 'onsuccess': 'SUCCESS: uplink configured'}, {'action': 'copy_file', 'always_execute': True, 'name': 'automate reload', 'attributes': {'dst_url': '/mnt/flash/', 'src_url': 'files/automate/ztpprep', 'mode': 777, 'overwrite': 'if-missing'}}])
DEBUG: [controller:170] 080027e2d0f4: running do_validation
DEBUG: [validators:80] 080027e2d0f4: running PatternValidator.validate
DEBUG: [validators:96] 080027e2d0f4: running PatternValidator.validate_attributes for 'leaf_dynamic'
DEBUG: [validators:96] 080027e2d0f4: running PatternValidator.validate_definition for 'leaf_dynamic'
DEBUG: [validators:96] 080027e2d0f4: running PatternValidator.validate_interfaces for 'leaf_dynamic'
DEBUG: [validators:80] 080027e2d0f4: running InterfacePatternValidator.validate
DEBUG: [validators:96] 080027e2d0f4: running InterfacePatternValidator.validate_interface_pattern 
DEBUG: [validators:204] 080027e2d0f4: adding interface pattern '{'Ethernet1': 'spine001:any'}' to valid interface patterns
DEBUG: [validators:80] 080027e2d0f4: running InterfacePatternValidator.validate
DEBUG: [validators:96] 080027e2d0f4: running InterfacePatternValidator.validate_interface_pattern 
DEBUG: [validators:204] 080027e2d0f4: adding interface pattern '{'Ethernet2': 'spine002:any'}' to valid interface patterns
DEBUG: [validators:96] 080027e2d0f4: running PatternValidator.validate_name for 'leaf_dynamic'
DEBUG: [validators:96] 080027e2d0f4: running PatternValidator.validate_node for 'leaf_dynamic'
DEBUG: [validators:96] 080027e2d0f4: running PatternValidator.validate_variables for 'leaf_dynamic'
DEBUG: [validators:315] 080027e2d0f4: PatternValidator validation successful
DEBUG: [topology:472] 080027e2d0f4: checking pattern 'leaf_dynamic' entries for variable substitution
DEBUG: [topology:482] 080027e2d0f4: pattern 'leaf_dynamic' variable substitution complete
DEBUG: [topology:559] 080027e2d0f4: pattern 'leaf_dynamic' - attempting to match node ("Node(serialnumber=None, systemmac=080027e2d0f4, neighbors=OrderedCollection([('Ethernet2', [Neighbor(device='spine002', interface='Ethernet1')]), ('Management1', [Neighbor(device='spine002', interface='Management1'), Neighbor(device='spine001', interface='Management1')]), ('Ethernet1', [Neighbor(device='spine001', interface='Ethernet1')])]))")
DEBUG: [topology:569] 080027e2d0f4: pattern 'leaf_dynamic' - attempting to match interface Ethernet2([Neighbor(device='spine002', interface='Ethernet1')])
DEBUG: [topology:575] 080027e2d0f4: pattern 'leaf_dynamic' - checking interface pattern for Ethernet2: InterfacePattern(interface=Ethernet1, remote_device=spine001, remote_interface=any)
DEBUG: [topology:675] 080027e2d0f4: attempting to match Ethernet2(Neighbor(device='spine002', interface='Ethernet1')) against interface pattern InterfacePattern(interface=Ethernet1, remote_device=spine001, remote_interface=any)
DEBUG: [topology:575] 080027e2d0f4: pattern 'leaf_dynamic' - checking interface pattern for Ethernet2: InterfacePattern(interface=Ethernet2, remote_device=spine002, remote_interface=any)
DEBUG: [topology:675] 080027e2d0f4: attempting to match Ethernet2(Neighbor(device='spine002', interface='Ethernet1')) against interface pattern InterfacePattern(interface=Ethernet2, remote_device=spine002, remote_interface=any)
DEBUG: [topology:582] 080027e2d0f4: pattern 'leaf_dynamic' - interface pattern match for Ethernet2: InterfacePattern(interface=Ethernet2, remote_device=spine002, remote_interface=any)
DEBUG: [topology:569] 080027e2d0f4: pattern 'leaf_dynamic' - attempting to match interface Management1([Neighbor(device='spine002', interface='Management1'), Neighbor(device='spine001', interface='Management1')])
DEBUG: [topology:575] 080027e2d0f4: pattern 'leaf_dynamic' - checking interface pattern for Management1: InterfacePattern(interface=Ethernet1, remote_device=spine001, remote_interface=any)
DEBUG: [topology:675] 080027e2d0f4: attempting to match Management1(Neighbor(device='spine002', interface='Management1')) against interface pattern InterfacePattern(interface=Ethernet1, remote_device=spine001, remote_interface=any)
DEBUG: [topology:675] 080027e2d0f4: attempting to match Management1(Neighbor(device='spine001', interface='Management1')) against interface pattern InterfacePattern(interface=Ethernet1, remote_device=spine001, remote_interface=any)
DEBUG: [topology:595] 080027e2d0f4: pattern 'leaf_dynamic' - interface Management1 did not match any interface patterns
DEBUG: [topology:569] 080027e2d0f4: pattern 'leaf_dynamic' - attempting to match interface Ethernet1([Neighbor(device='spine001', interface='Ethernet1')])
DEBUG: [topology:575] 080027e2d0f4: pattern 'leaf_dynamic' - checking interface pattern for Ethernet1: InterfacePattern(interface=Ethernet1, remote_device=spine001, remote_interface=any)
DEBUG: [topology:675] 080027e2d0f4: attempting to match Ethernet1(Neighbor(device='spine001', interface='Ethernet1')) against interface pattern InterfacePattern(interface=Ethernet1, remote_device=spine001, remote_interface=any)
DEBUG: [topology:582] 080027e2d0f4: pattern 'leaf_dynamic' - interface pattern match for Ethernet1: InterfacePattern(interface=Ethernet1, remote_device=spine001, remote_interface=any)
DEBUG: [controller:529] 080027e2d0f4: node passed pattern validation (nodes/080027e2d0f4/pattern)
DEBUG: [controller:170] 080027e2d0f4: running get_startup_config
DEBUG: [controller:549] 080027e2d0f4: no startup-config nodes/080027e2d0f4/startup-config
DEBUG: [controller:170] 080027e2d0f4: running do_actions
DEBUG: [controller:567] 080027e2d0f4: action configure ma1 included in definition
DEBUG: [controller:567] 080027e2d0f4: action configure global system included in definition
DEBUG: [controller:567] 080027e2d0f4: action configure auth included in definition
DEBUG: [controller:567] 080027e2d0f4: action configure ztpprep alias included in definition
DEBUG: [controller:567] 080027e2d0f4: action configure uplink included in definition
DEBUG: [controller:562] 080027e2d0f4: always_execute action automate reload included in definition
DEBUG: [controller:170] 080027e2d0f4: running get_attributes
WARNING: [controller:589] 080027e2d0f4: no node specific attributes file
DEBUG: [controller:170] 080027e2d0f4: running do_substitution
DEBUG: [controller:609] 080027e2d0f4: processing action configure ma1 (variable substitution)
DEBUG: [controller:609] 080027e2d0f4: processing action configure global system (variable substitution)
DEBUG: [controller:609] 080027e2d0f4: processing action configure auth (variable substitution)
DEBUG: [controller:609] 080027e2d0f4: processing action configure ztpprep alias (variable substitution)
DEBUG: [controller:609] 080027e2d0f4: processing action configure uplink (variable substitution)
DEBUG: [controller:609] 080027e2d0f4: processing action automate reload (variable substitution)
DEBUG: [controller:170] 080027e2d0f4: running do_resources
DEBUG: [topology:147] 080027e2d0f4: computing resources (attr={'url': 'files/templates/ma1.template', 'variables': {'ipaddress': "allocate('mgmt_subnet')"}})
DEBUG: [topology:147] 080027e2d0f4: computing resources (attr={'ipaddress': "allocate('mgmt_subnet')"})
DEBUG: [resources:76] 080027e2d0f4: allocating resources
DEBUG: [resources:108] 080027e2d0f4: looking up resource in 'mgmt_subnet'
DEBUG: [resources:90] 080027e2d0f4: allocated 'mgmt_subnet':'192.168.101.54/24'
DEBUG: [topology:171] 080027e2d0f4: resources: {'ipaddress': '192.168.101.54/24'}
DEBUG: [topology:171] 080027e2d0f4: resources: {'url': 'files/templates/ma1.template', 'variables': {'ipaddress': '192.168.101.54/24'}}
DEBUG: [topology:147] 080027e2d0f4: computing resources (attr={'url': 'files/templates/system.template', 'variables': {'hostname': "allocate('leaf_hostname')"}})
DEBUG: [topology:147] 080027e2d0f4: computing resources (attr={'hostname': "allocate('leaf_hostname')"})
DEBUG: [resources:76] 080027e2d0f4: allocating resources
DEBUG: [resources:108] 080027e2d0f4: looking up resource in 'leaf_hostname'
DEBUG: [resources:90] 080027e2d0f4: allocated 'leaf_hostname':'leaf004'
DEBUG: [topology:171] 080027e2d0f4: resources: {'hostname': 'leaf004'}
DEBUG: [topology:171] 080027e2d0f4: resources: {'url': 'files/templates/system.template', 'variables': {'hostname': 'leaf004'}}
DEBUG: [topology:147] 080027e2d0f4: computing resources (attr={'url': 'files/templates/login.template'})
DEBUG: [topology:171] 080027e2d0f4: resources: {'url': 'files/templates/login.template'}
DEBUG: [topology:147] 080027e2d0f4: computing resources (attr={'url': 'files/templates/ztpprep.template'})
DEBUG: [topology:171] 080027e2d0f4: resources: {'url': 'files/templates/ztpprep.template'}
DEBUG: [topology:147] 080027e2d0f4: computing resources (attr={'url': 'files/templates/uplink.template', 'variables': {'spine_downlink': "allocate('spine_downlink')"}})
DEBUG: [topology:147] 080027e2d0f4: computing resources (attr={'spine_downlink': "allocate('spine_downlink')"})
DEBUG: [resources:76] 080027e2d0f4: allocating resources
DEBUG: [resources:108] 080027e2d0f4: looking up resource in 'spine_downlink'
DEBUG: [resources:90] 080027e2d0f4: allocated 'spine_downlink':'Eth4'
DEBUG: [topology:171] 080027e2d0f4: resources: {'spine_downlink': 'Eth4'}
DEBUG: [topology:171] 080027e2d0f4: resources: {'url': 'files/templates/uplink.template', 'variables': {'spine_downlink': 'Eth4'}}
DEBUG: [topology:147] 080027e2d0f4: computing resources (attr={'dst_url': '/mnt/flash/', 'src_url': 'files/automate/ztpprep', 'mode': 777, 'overwrite': 'if-missing'})
DEBUG: [topology:171] 080027e2d0f4: resources: {'dst_url': '/mnt/flash/', 'src_url': 'files/automate/ztpprep', 'mode': 777, 'overwrite': 'if-missing'}
DEBUG: [controller:170] 080027e2d0f4: running finalize_response
DEBUG: [controller:182] 080027e2d0f4: response to finalize_response: {'body': {'name': 'def_leaf', 'actions': [{'action': 'add_config', 'attributes': {'url': 'files/templates/ma1.template', 'variables': {'ipaddress': '192.168.101.54/24'}}, 'name': 'configure ma1', 'onstart': 'Starting to configure ma1', 'onsuccess': 'SUCCESS: ma1 configure'}, {'action': 'add_config', 'attributes': {'url': 'files/templates/system.template', 'variables': {'hostname': 'leaf004'}}, 'name': 'configure global system', 'onstart': 'Starting to add basic system config', 'onsuccess': 'SUCCESS: basic config added'}, {'action': 'add_config', 'attributes': {'url': 'files/templates/login.template'}, 'name': 'configure auth'}, {'action': 'add_config', 'attributes': {'url': 'files/templates/ztpprep.template'}, 'name': 'configure ztpprep alias'}, {'action': 'add_config', 'attributes': {'url': 'files/templates/uplink.template', 'variables': {'spine_downlink': 'Eth4'}}, 'name': 'configure uplink', 'onstart': 'Starting to configure uplink', 'onsuccess': 'SUCCESS: uplink configured'}, {'action': 'copy_file', 'always_execute': True, 'name': 'automate reload', 'attributes': {'dst_url': '/mnt/flash/', 'src_url': 'files/automate/ztpprep', 'mode': 777, 'overwrite': 'if-missing'}}]}, 'status': 200, 'content_type': 'application/json'}
192.168.101.62 - - [27/Nov/2014 01:11:49] "GET /nodes/080027e2d0f4 HTTP/1.1" 200 1181
DEBUG: [controller:143] GET /actions/add_config HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, compress
Content-Length: 4
Content-Type: text/html
Host: 192.168.101.16:8080
User-Agent: python-requests/1.2.3 CPython/2.7.0 Linux/3.4.43.Ar-2083164.4142F
Resource: add_config

192.168.101.62 - - [27/Nov/2014 01:11:49] "GET /actions/add_config HTTP/1.1" 200 3249
DEBUG: [controller:120] GET /files/templates/ma1.template HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, compress
Content-Length: 4
Content-Type: text/html
Host: 192.168.101.16:8080
User-Agent: python-requests/1.2.3 CPython/2.7.0 Linux/3.4.43.Ar-2083164.4142F
Resource: templates/ma1.template

192.168.101.62 - - [27/Nov/2014 01:11:49] "GET /files/templates/ma1.template HTTP/1.1" 200 60
DEBUG: [controller:120] GET /files/templates/system.template HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, compress
Content-Length: 4
Content-Type: text/html
Host: 192.168.101.16:8080
User-Agent: python-requests/1.2.3 CPython/2.7.0 Linux/3.4.43.Ar-2083164.4142F
Resource: templates/system.template

192.168.101.62 - - [27/Nov/2014 01:11:49] "GET /files/templates/system.template HTTP/1.1" 200 84
DEBUG: [controller:120] GET /files/templates/login.template HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, compress
Content-Length: 4
Content-Type: text/html
Host: 192.168.101.16:8080
User-Agent: python-requests/1.2.3 CPython/2.7.0 Linux/3.4.43.Ar-2083164.4142F
Resource: templates/login.template

192.168.101.62 - - [27/Nov/2014 01:11:49] "GET /files/templates/login.template HTTP/1.1" 200 61
DEBUG: [controller:120] GET /files/templates/ztpprep.template HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, compress
Content-Length: 4
Content-Type: text/html
Host: 192.168.101.16:8080
User-Agent: python-requests/1.2.3 CPython/2.7.0 Linux/3.4.43.Ar-2083164.4142F
Resource: templates/ztpprep.template

192.168.101.62 - - [27/Nov/2014 01:11:49] "GET /files/templates/ztpprep.template HTTP/1.1" 200 43
DEBUG: [controller:120] GET /files/templates/uplink.template HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, compress
Content-Length: 4
Content-Type: text/html
Host: 192.168.101.16:8080
User-Agent: python-requests/1.2.3 CPython/2.7.0 Linux/3.4.43.Ar-2083164.4142F
Resource: templates/uplink.template

192.168.101.62 - - [27/Nov/2014 01:11:49] "GET /files/templates/uplink.template HTTP/1.1" 200 279
DEBUG: [controller:143] GET /actions/copy_file HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, compress
Content-Length: 4
Content-Type: text/html
Host: 192.168.101.16:8080
User-Agent: python-requests/1.2.3 CPython/2.7.0 Linux/3.4.43.Ar-2083164.4142F
Resource: copy_file

192.168.101.62 - - [27/Nov/2014 01:11:49] "GET /actions/copy_file HTTP/1.1" 200 5589
DEBUG: [controller:120] GET /files/automate/ztpprep HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, compress
Content-Length: 4
Content-Type: text/html
Host: 192.168.101.16:8080
User-Agent: python-requests/1.2.3 CPython/2.7.0 Linux/3.4.43.Ar-2083164.4142F
Resource: automate/ztpprep

192.168.101.62 - - [27/Nov/2014 01:11:49] "GET /files/automate/ztpprep HTTP/1.1" 200 144
192.168.101.62 - - [27/Nov/2014 01:11:49] "GET /meta/files/automate/ztpprep HTTP/1.1" 200 65

実行後の状態確認

Resource pools

実行を見守っている時点で「え?」と思ったのですが、Resource pools の払い出しを見ると以下のようになっています。

$ cat /usr/share/ztpserver/resources/mgmt_subnet
192.168.101.52/24: null
192.168.101.53/24: null
192.168.101.54/24: 080027e2d0f4
192.168.101.55/24: null

$ cat /usr/share/ztpserver/resources/leaf_hostname
leaf001: null
leaf002: null
leaf003: null
leaf004: 080027e2d0f4

$ cat /usr/share/ztpserver/resources/spine_downlink
1: null
2: null
3: null
4: 080027e2d0f4

別に上から順に使われていくわけではないようです。
これについては後述します。

nodes ディレクト

/usr/share/ztpserver/nodes/ 配下には、Static Provisioning の時には手動生成した「ノード単位のディレクトリ・ファイル」が自動生成されています。
definition ファイルは、先の手順で生成した /usr/share/ztpserver/definitions/def_leaf が、neighbordb マッチングの結果、ほぼそのまま持ってこられています。

$ ls -alh /usr/share/ztpserver/nodes/
total 20K
drwxr-xr-x 5 root root 4.0K Nov 29 17:05 .
drwxr-xr-x 8 root root 4.0K Nov 27 00:20 ..
drwxr-xr-x 2 root root 4.0K Nov 24 02:12 080027316065
drwxr-xr-x 2 root root 4.0K Nov 29 16:35 080027be304d
drwxr-xr-x 2 root root 4.0K Nov 29 17:05 080027e2d0f4

$ ls -alh /usr/share/ztpserver/nodes/080027e2d0f4/
total 20K
drwxr-xr-x 2 root root 4.0K Nov 29 17:05 .
drwxr-xr-x 5 root root 4.0K Nov 29 17:05 ..
-rw-r--r-- 1 root root 1.2K Nov 29 17:05 definition
-rw-r--r-- 1 root root  314 Nov 29 17:05 .node
-rw-r--r-- 1 root root  129 Nov 29 17:05 pattern

$ cat /usr/share/ztpserver/nodes/080027e2d0f4/pattern
definition: def_leaf
interfaces:
- Ethernet1: spine001:any
- Ethernet2: spine002:any
name: leaf_dynamic
node: null
variables: {}

$ cat /usr/share/ztpserver/nodes/080027e2d0f4/.node
{"neighbors": {"Ethernet2": [{"device": "spine002", "port": "Ethernet1"}], "Management1": [{"device": "spine001", "port": "Management1"}, {"device": "spine002", "port": "Management1"}], "Ethernet1": [{"device": "spine001", "port": "Ethernet1"}]}, "model": "vEOS", "version": "4.14.2F", "systemmac": "080027e2d0f4"}

Resource pools 動作調査

Resource pools の払い出し判定ってどうなってるの?っていうところを見てみます。
公式の説明 を見ると、空いている最初の値をとってくるように見えるので…。

開発状況

Static Provisioning の時には既知 issue で若干嵌ったので、とりあえず github issue 見ておきます。

ザッと眺めたところ、この機能はまだテストがちゃんとされていないようです。 2014/11/29 時点では v1.2.0 (2014/12 release 予定)を milestone としているようですが、develop branch を見ても特にテストコードは入ってません。

ソースコード

# python 詳しくないので、間違えたこと言ってたらスミマセン

以下が Resource pools の allocate しているログです。

DEBUG: [topology:147] 080027e2d0f4: computing resources (attr={'url': 'files/templates/ma1.template', 'variables': {'ipaddress': "allocate('mgmt_subnet')"}})
DEBUG: [topology:147] 080027e2d0f4: computing resources (attr={'ipaddress': "allocate('mgmt_subnet')"})
DEBUG: [resources:76] 080027e2d0f4: allocating resources
DEBUG: [resources:108] 080027e2d0f4: looking up resource in 'mgmt_subnet'
DEBUG: [resources:90] 080027e2d0f4: allocated 'mgmt_subnet':'192.168.101.54/24'

ソースコードとしては、端的には ここ で、関係するのは以下あたり。

動作としては…

  1. 対象の Resouce pools ファイルを /usr/share/ztpserver/resources/ から YAML として load (PyYAML)
  2. 上記 YAML の中身を、空の dict 型 object に key, value 格納 (value が null とか空なら None)
  3. 上記の dict を走査、valueNone ならこいつを割り当てるリソースとして key を取り出す

ってことで、明らかにおかしいところは見つからないけど… dict 型は順序が保証されるものではないから…とか?
OrderedDict なんていう順序保証型の辞書型もあるらしいけど。

ちなみに、事前に /usr/share/ztpserver/resources/ 配下の定義ファイルにコメント入れたり、順序をぐちゃぐちゃにしても、コメントは消されて、ソートされて上書きされます。(dict をファイルに吐き出している)

一応 MLAG 構成完成させる

仕方ないので、インチキしてとりあえず想定するパラメータが付与されるように leaf001,002 を作りました…。
具体的には /usr/share/ztpserver/resources/ 配下の Resource pools 設定ファイルを弄って、付与したいパラメータだけを書いた状態で 1 台ずつ ZTP…。

f:id:kakkotetsu:20170513165039j:plain

気持ち悪さを残したまま、 MLAG 設定を作り上げておきます。 以下設定を spine001,002 に追加。

interface Ethernet1
   channel-group 1 mode active

interface Ethernet2
   channel-group 2 mode active

Port-channel を組んだことで、しばし待つと LLDP neighbor から消えてしまいます…。
# Ma1 は VirtualBox 環境なので、同一セグメントの vEOS 間で全部拾ってしまっています。

spine002#show lldp neighbors
Last table change time   : 0:00:07 ago
Number of table inserts  : 27
Number of table deletes  : 22
Number of table drops    : 0
Number of table age-outs : 22

Port       Neighbor Device ID             Neighbor Port ID           TTL
Et1        leaf001                        Ethernet2                  120
Et2        leaf002                        Ethernet2                  120
Ma1        spine001                       Management1                120
Ma1        leaf001                        Management1                120
Ma1        leaf002                        Management1                120


spine002#show lldp neighbors
Last table change time   : 0:29:36 ago
Number of table inserts  : 27
Number of table deletes  : 24
Number of table drops    : 0
Number of table age-outs : 24

Port       Neighbor Device ID             Neighbor Port ID           TTL
Ma1        spine001                       Management1                120
Ma1        leaf001                        Management1                120
Ma1        leaf002                        Management1                120

おわり

課題

以下あたりが課題ですかね。適当にやってるところがあるので、間違えてたら指摘下さい。

  • ZTP 関係なく、Port-Channel のメンバ物理 IF では LLDP が動作しないってのはちょっと…。(その後のバージョンや物理箱ではそういう制約は踏まなかったので、この環境固有の問題)
  • Resource pools 周りは、巧く動かせなかったので…。現時点では明確に何とも言えないので、暇があったらもう少し追ってみようかと。とりあえず直近 v1.2.0 が出るので、見てますか?
  • Resource pools が想定通り動いても、今度は起動順で払い出しを制御しないといけない、とかそもそもどのラックのどのスイッチをどのホスト名にするかを気にしないようにする、とか本番環境で実用する場合には運用を熟考しないといけなさそうです。
  • neighbordb の条件付けで正規表現とか、もっと柔軟な定義が使えるようになるようです。そうなると、definition での variables のところも柔軟にできると結構実用的になりそうです。(e.x. LLDP で送ってきた spine 側のポート番号を使って hostname: leaf&("%03d", spine_port) 的な書き方)。
  • write erase からの reload は必要なので、Zero Touch ではないすね。箱だと出荷時点で startup-config なかったりするのでしょうか。

雑感

  • ZTP なんて昔から Cisco がやってるじゃん、と思ってましたが、結構進化していたみたいです。ごめんなさい。
  • ztpserver の issue を見ていると、enhance も結構計画されていて期待できそうです。
  • Arista は「LLDP 情報を基に port description を更新!」なんてのも出しているし、サーバでもどんどん LLDP 動かそう。
  • こういうのを巧く使いこなすには、統一されたネットワーク設計が必要でしょう。それがあって最大限に使いこなすと、ネットワーク屋さんがいなくてもどんどんサーバ増やしていける世界になるんじゃないでしょうか。ていうか、なれ。

Arista の Zero Touch Provisioning (ZTP) を試す (Static Provisioning 編) (original : 2014/11/24)

この記事は某所で 2014/11/24 に書いたもののコピーです。
そのため 2017/05/13 時点ではやや古い情報も含まれています。

概要

本項でやること

ZTP を使って、Arista vEOS x1台の初期設定投入をしてみます。
Cisco ではちょっとした作りこみが必要だった「設定テンプレートを作成し、対象ノードの MAC アドレスとノードごとの変数(ホスト名や管理IP)の組み合わせを定義して、バラまく startupconfig ファイルを生成」というような動作を、ztpserver の仕組みを使ってより柔軟にできるよ、ってところを見ます。

  • 方式は Static Provisioning で、対象 vEOS の Sysyte MAC アドレスを事前登録
  • 設定ファイルは ztpserver の仕組み(bootstrap と template と definition)を使って動的に生成

Static Provisioning と Dynamic Provisioning とは?

公式の説明 を読むのが手っ取り早いです。
超ザックリ言うと、以下の通り。

  • Static は、事前に ztpserver にクライアント(Arista)の System MAC アドレスかシリアル番号を登録しておく。サーバがクライアントに応じた startup-config か bootstrap スクリプトをバラまくことができる。
  • Dynamic は、ZTP シーケンスの中でクライアントが送ってきた LLDP 情報を基に、サーバ側で条件マッチして bootstrap スクリプトをバラまくことができる。

Dynamic Provisioning こそが ZTP の本領発揮、という感じですが、本項ではまず Static Provisioning を見ておきます。(startup-config 方式は、あまり面白くなさそうなので割愛)

Arista EOS?

Arista EOS については、以下の本をオススメしときますね。

Arista Warrior: A Real-World Guide to Understanding Arista Switches and EOS

Arista Warrior: A Real-World Guide to Understanding Arista Switches and EOS

構成図・事前準備

Arista の ztpserver インストール~初期設定 で作った ztpserver と、突っ込んだだけで何も設定していない ARISTA vEOS x1 を使います。以下の簡単構成で。(作業時に手元で作ってた絵があまりにお粗末だったので、試しに ShowNet のアイコンを入れてみたが、かえってチグハグに…)

f:id:kakkotetsu:20170513162241j:plain

ARISTA vEOS も 1 台必要なので、以下リンクを参考に作っておきます。公式に、各種ハイパーバイザごとの導入手順詳細とかもあります。

本項の環境は以下の通りです。

  • ztpserver v1.1.0 は 前回の通り
  • vEOS Aboot-veos:Aboot-veos-2.1.0
  • vEOS OS:vEOS-4.14.2F
  • vEOS CPU:1core
  • vEOS Memory:1024MB
  • vEOS ネットワークアダプタ1:ホストオンリーネットワーク(ztpserverと同じところ)

設定手順

基本的に 公式の Configuration公式の Examples を参照しつつ、必要に応じて ztpserver の source を参照して進めていきます。

vEOS で System MAC アドレス確認

今回は Static Provisioning を使うので、vEOS 側の System MAC アドレスを拾っておきます。以下の System MAC address に表示されている 12 桁のやつです。仮想機器なので Serial number はなし…。

#show version
Arista vEOS
Hardware version:
Serial number:
System MAC address:  0800.2731.6065

Software image version: 4.14.2F
Architecture:           i386
Internal build version: 4.14.2F-2083164.4142F.1
Internal build ID:      19fe6cb3-1777-40b6-a4e6-53875b30658c

Uptime:                 1 hour and 54 minutes
Total memory:           996152 kB
Free memory:            34112 kB

ztpserver でクライアントノード単位の設定

/usr/share/ztpserver/nodes/ 配下にディレクトリ・ファイル作成していきます。
例によって 公式の Configuration を見ながら。

ディレクトリ作成

以下のように、まずはノードを識別する System MAC アドレスディレクトリを作ります。
この配下に、ノードごとの定義ファイルとか(今回は使いませんが startupconfig ファイルとか)を作ります。

$ sudo mkdir /usr/share/ztpserver/nodes/080027316065

definition ファイル作成

/usr/share/ztpserver/nodes/080027316065/ 配下に definition ファイルを作成します。
見ての通り、クライアント側に実行させたい処理やテンプレート呼び出しと、テンプレートに埋め込むクライアント単位の変数を定義します。

---
name: spine001
actions:
  - name: "configure ma1"
    action: add_config
    attributes:
      url: /files/templates/ma1.template
      variables:
        ipaddress: 192.168.101.50/24
  - name: "configure system"
    action: add_config
    attributes:
      url: /files/templates/system.template
      variables:
        hostname: spine001
  - name: "configure ztpprep alias"
    action: add_config
    attributes:
      url: /files/templates/ztpprep.template
  - name: "automate reload"
    action: copy_file
    always_execute: true
    attributes:
      dst_url: /mnt/flash/
      overwrite: if-missing
      src_url: files/automate/ztpprep
      mode: 777

間抜けな&当然の話ですが、yaml の文法を間違えたりすると巧いこと vEOS に設定が反映されません。自分はそれでミスって実機に "hostname $hostname" なんてのが設定されてしまいました。 ztps 起動時に、文法チェックとかはしないのです。

pattern ファイル作成

/usr/share/ztpserver/nodes/080027316065/ 配下に pattern ファイルを作成します。
これは、ztpserver の Global 設定 /etc/ztpserver/ztpserver.conf 内で disable_topology_validation = True にしている場合には作成する必要ないです。(公式の記載)
ただ、今後 Dynamic Provisioning を使う際に、これを True にしておくと都合が悪いので False のままにしておきます。この場合、pattern ファイルに記入する内容で「クライアントがどんな LLDP 状態を送ってきても、それを無視する」を実現させます。

name: static_node
interfaces:
  - none: none:none

ここで気を付けるのは、公式の記載 そのままに interfaces: 内で any: any:any と書いても、「クライアントの LLDP が動いていない・LLDP で何も情報を得ていない場合」には期待する動作にはならず異常終了してしまうことです。
これについては github の issue に記載されていますが、公式ドキュメントには 2014/11/24 現在反映されていないようです。

また、ソースコードの master / develop ブランチを比較すると、pattern ファイルの必須パラメータなども今後変わっていく気配があります。上記設定はあくまで v1.1.0 版ということで。

ztpserver に各種テンプレートを作成

前述の definition ファイル内で呼び出している各種テンプレートや bash スクリプトを作っておきます。
公式のサンプルを持ってきて、必要に応じてカスタマイズして配置するのが手っ取り早いと思います。

  • /usr/share/ztpserver/files/automate/ztpprep
#!/bin/bash
#delete system files and reload the system for ztp
rm -rf /mnt/flash/startup-config
rm -rf /mnt/flash/*extensions*
shutdown -r now
  • /usr/share/ztpserver/files/templates/ma1.template
interface Management1
  ip address $ipaddress
  no shutdown
  • /usr/share/ztpserver/files/templates/system.template
hostname $hostname
!
!
management api http-commands
   no shutdown
!
lldp timer 5
!
  • /usr/share/ztpserver/files/templates/ztpprep.template
alias ztpprep bash sudo /mnt/flash/ztpprep

# 超最低限の設定を入れたつもりだったが、login.template でユーザ登録くらいはしておくべきだった…。

ZTP 動作確認

実際に動かして、vEOS 1台が設定されることを確認します。

ztpserver 起動

$ sudo ztps --debug
INFO: [app:115] Logging started for ztpserver
INFO: [app:116] Using repository /usr/share/ztpserver
DEBUG: [controller:776] server URL: http://192.168.101.16:8080
Starting server on http://192.168.101.16:8080

vEOS 再起動

startup-config を消して再起動します。いつものように reload 実行時に色々聞かれるので Save するかは no を、Confirm は そのまま Enter で対応します。

# write erase
# reload

VirtualBox のコンソールで vEOS 停止~起動を見守ります。ログインプロンプトが出たら、ZTP がはじまります。

ZTP 実行

vEOS 起動後、以下のようなメッセージが出て ZTP シーケンスが開始します。左の窓が ztpserver で、右の窓が vEOS です。 f:id:kakkotetsu:20170513162524j:plain

公式のシーケンス図と見比べながら、見守ります。 f:id:kakkotetsu:20170513162536j:plain

definition ファイルで定義した通りに動作していることをフムフム言いながら見ていると、vEOS の再起動が始まります。 f:id:kakkotetsu:20170513162548j:plain

vEOS 再起動が終わったら、定義した通りの startup-config で起動していることを確認できます。 f:id:kakkotetsu:20170513162601j:plain

ztpserver 側のログを見ると以下の感じ(debug レベルにしているので長い…)

$ sudo ztps --debug
INFO: [app:115] Logging started for ztpserver
INFO: [app:116] Using repository /usr/share/ztpserver
DEBUG: [controller:776] server URL: http://192.168.101.16:8080
Starting server on http://192.168.101.16:8080
192.168.101.61 - - [24/Nov/2014 02:17:00] "GET /bootstrap HTTP/1.1" 200 35386
192.168.101.61 - - [24/Nov/2014 02:17:01] "GET /bootstrap/config HTTP/1.1" 200 200
DEBUG: [controller:252] POST /nodes HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, compress
Content-Length: 110
Content-Type: application/json
Host: 192.168.101.16:8080
User-Agent: python-requests/1.2.3 CPython/2.7.0 Linux/3.4.43.Ar-2083164.4142F

{"neighbors": {}, "version": "4.14.2F", "systemmac": "08:00:27:31:60:65", "model": "vEOS", "serialnumber": ""}

DEBUG: [topology:140] 080027316065: created node object Node(serialnumber=, systemmac=080027316065, neighbors=OrderedCollection())
DEBUG: [controller:170] 080027316065: running node_exists
DEBUG: [controller:170] 080027316065: running dump_node
DEBUG: [controller:170] 080027316065: running set_location
DEBUG: [controller:182] 080027316065: response to set_location: {'status': 409, 'location': 'nodes/080027316065'}
192.168.101.61 - - [24/Nov/2014 02:17:28] "POST /nodes HTTP/1.1" 409 0
DEBUG: [controller:470] GET /nodes/080027316065 HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, compress
Content-Length: 4
Content-Type: text/html
Host: 192.168.101.16:8080
User-Agent: python-requests/1.2.3 CPython/2.7.0 Linux/3.4.43.Ar-2083164.4142F
Resource: 080027316065

DEBUG: [topology:140] 080027316065: created node object Node(serialnumber=None, systemmac=080027316065, neighbors=OrderedCollection())
DEBUG: [controller:170] 080027316065: running get_definition
DEBUG: [controller:498] 080027316065: defintion is nodes/080027316065/definition ([{'action': 'add_config', 'attributes': {'url': '/files/templates/ma1.template', 'variables': {'ipaddress': '192.168.101.50/24'}}, 'name': 'configure ma1'}, {'action': 'add_config', 'attributes': {'url': '/files/templates/system.template', 'variables': {'hostname': 'spine001'}}, 'name': 'configure system'}, {'action': 'add_config', 'attributes': {'url': '/files/templates/ztpprep.template'}, 'name': 'configure ztpprep alias'}, {'action': 'copy_file', 'always_execute': True, 'name': 'automate reload', 'attributes': {'dst_url': '/mnt/flash/', 'src_url': 'files/automate/ztpprep', 'mode': 777, 'overwrite': 'if-missing'}}])
DEBUG: [controller:170] 080027316065: running do_validation
DEBUG: [validators:80] 080027316065: running PatternValidator.validate
DEBUG: [validators:96] 080027316065: running PatternValidator.validate_attributes for 'static_node'
WARNING: [validators:171] 080027316065: PatternValidator warning: 'static_node' is missing optional attribute (definition)
WARNING: [validators:171] 080027316065: PatternValidator warning: 'static_node' is missing optional attribute (variables)
DEBUG: [validators:96] 080027316065: running PatternValidator.validate_definition for 'static_node'
DEBUG: [validators:96] 080027316065: running PatternValidator.validate_interfaces for 'static_node'
DEBUG: [validators:80] 080027316065: running InterfacePatternValidator.validate
DEBUG: [validators:96] 080027316065: running InterfacePatternValidator.validate_interface_pattern 
DEBUG: [validators:204] 080027316065: adding interface pattern '{'none': 'none:none'}' to valid interface patterns
DEBUG: [validators:96] 080027316065: running PatternValidator.validate_name for 'static_node'
DEBUG: [validators:96] 080027316065: running PatternValidator.validate_node for 'static_node'
DEBUG: [validators:96] 080027316065: running PatternValidator.validate_variables for 'static_node'
DEBUG: [validators:315] 080027316065: PatternValidator validation successful
DEBUG: [topology:472] 080027316065: checking pattern 'static_node' entries for variable substitution
DEBUG: [topology:482] 080027316065: pattern 'static_node' variable substitution complete
DEBUG: [topology:559] 080027316065: pattern 'static_node' - attempting to match node ('Node(serialnumber=None, systemmac=080027316065, neighbors=OrderedCollection())')
DEBUG: [controller:529] 080027316065: node passed pattern validation (nodes/080027316065/pattern)
DEBUG: [controller:170] 080027316065: running get_startup_config
DEBUG: [controller:549] 080027316065: no startup-config nodes/080027316065/startup-config
DEBUG: [controller:170] 080027316065: running do_actions
DEBUG: [controller:567] 080027316065: action configure ma1 included in definition
DEBUG: [controller:567] 080027316065: action configure system included in definition
DEBUG: [controller:567] 080027316065: action configure ztpprep alias included in definition
DEBUG: [controller:562] 080027316065: always_execute action automate reload included in definition
DEBUG: [controller:170] 080027316065: running get_attributes
WARNING: [controller:589] 080027316065: no node specific attributes file
DEBUG: [controller:170] 080027316065: running do_substitution
DEBUG: [controller:609] 080027316065: processing action configure ma1 (variable substitution)
DEBUG: [controller:609] 080027316065: processing action configure system (variable substitution)
DEBUG: [controller:609] 080027316065: processing action configure ztpprep alias (variable substitution)
DEBUG: [controller:609] 080027316065: processing action automate reload (variable substitution)
DEBUG: [controller:170] 080027316065: running do_resources
DEBUG: [topology:147] 080027316065: computing resources (attr={'url': '/files/templates/ma1.template', 'variables': {'ipaddress': '192.168.101.50/24'}})
DEBUG: [topology:147] 080027316065: computing resources (attr={'ipaddress': '192.168.101.50/24'})
DEBUG: [topology:171] 080027316065: resources: {'ipaddress': '192.168.101.50/24'}
DEBUG: [topology:171] 080027316065: resources: {'url': '/files/templates/ma1.template', 'variables': {'ipaddress': '192.168.101.50/24'}}
DEBUG: [topology:147] 080027316065: computing resources (attr={'url': '/files/templates/system.template', 'variables': {'hostname': 'spine001'}})
DEBUG: [topology:147] 080027316065: computing resources (attr={'hostname': 'spine001'})
DEBUG: [topology:171] 080027316065: resources: {'hostname': 'spine001'}
DEBUG: [topology:171] 080027316065: resources: {'url': '/files/templates/system.template', 'variables': {'hostname': 'spine001'}}
DEBUG: [topology:147] 080027316065: computing resources (attr={'url': '/files/templates/ztpprep.template'})
DEBUG: [topology:171] 080027316065: resources: {'url': '/files/templates/ztpprep.template'}
DEBUG: [topology:147] 080027316065: computing resources (attr={'dst_url': '/mnt/flash/', 'src_url': 'files/automate/ztpprep', 'mode': 777, 'overwrite': 'if-missing'})
DEBUG: [topology:171] 080027316065: resources: {'dst_url': '/mnt/flash/', 'src_url': 'files/automate/ztpprep', 'mode': 777, 'overwrite': 'if-missing'}
DEBUG: [controller:170] 080027316065: running finalize_response
DEBUG: [controller:182] 080027316065: response to finalize_response: {'body': {'name': 'spine001', 'actions': [{'action': 'add_config', 'attributes': {'url': '/files/templates/ma1.template', 'variables': {'ipaddress': '192.168.101.50/24'}}, 'name': 'configure ma1'}, {'action': 'add_config', 'attributes': {'url': '/files/templates/system.template', 'variables': {'hostname': 'spine001'}}, 'name': 'configure system'}, {'action': 'add_config', 'attributes': {'url': '/files/templates/ztpprep.template'}, 'name': 'configure ztpprep alias'}, {'action': 'copy_file', 'always_execute': True, 'name': 'automate reload', 'attributes': {'dst_url': '/mnt/flash/', 'src_url': 'files/automate/ztpprep', 'mode': 777, 'overwrite': 'if-missing'}}]}, 'status': 200, 'content_type': 'application/json'}
192.168.101.61 - - [24/Nov/2014 02:17:28] "GET /nodes/080027316065 HTTP/1.1" 200 657
DEBUG: [controller:143] GET /actions/add_config HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, compress
Content-Length: 4
Content-Type: text/html
Host: 192.168.101.16:8080
User-Agent: python-requests/1.2.3 CPython/2.7.0 Linux/3.4.43.Ar-2083164.4142F
Resource: add_config

192.168.101.61 - - [24/Nov/2014 02:17:28] "GET /actions/add_config HTTP/1.1" 200 3249
DEBUG: [controller:120] GET /files/templates/ma1.template HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, compress
Content-Length: 4
Content-Type: text/html
Host: 192.168.101.16:8080
User-Agent: python-requests/1.2.3 CPython/2.7.0 Linux/3.4.43.Ar-2083164.4142F
Resource: templates/ma1.template

192.168.101.61 - - [24/Nov/2014 02:17:28] "GET /files/templates/ma1.template HTTP/1.1" 200 60
DEBUG: [controller:120] GET /files/templates/system.template HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, compress
Content-Length: 4
Content-Type: text/html
Host: 192.168.101.16:8080
User-Agent: python-requests/1.2.3 CPython/2.7.0 Linux/3.4.43.Ar-2083164.4142F
Resource: templates/system.template

192.168.101.61 - - [24/Nov/2014 02:17:28] "GET /files/templates/system.template HTTP/1.1" 200 84
DEBUG: [controller:120] GET /files/templates/ztpprep.template HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, compress
Content-Length: 4
Content-Type: text/html
Host: 192.168.101.16:8080
User-Agent: python-requests/1.2.3 CPython/2.7.0 Linux/3.4.43.Ar-2083164.4142F
Resource: templates/ztpprep.template

192.168.101.61 - - [24/Nov/2014 02:17:28] "GET /files/templates/ztpprep.template HTTP/1.1" 200 43
DEBUG: [controller:143] GET /actions/copy_file HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, compress
Content-Length: 4
Content-Type: text/html
Host: 192.168.101.16:8080
User-Agent: python-requests/1.2.3 CPython/2.7.0 Linux/3.4.43.Ar-2083164.4142F
Resource: copy_file

192.168.101.61 - - [24/Nov/2014 02:17:28] "GET /actions/copy_file HTTP/1.1" 200 5589

あと、nodes ディレクトリ配下に .node ファイルができています。このファイルが残ったまま再実行しても、特にエラーにはならないです。

$ ls -alh /usr/share/ztpserver/nodes/080027316065/
total 20K
drwxr-xr-x 2 root root 4.0K Nov 24 02:12 .
drwxr-xr-x 3 root root 4.0K Nov 23 11:19 ..
-rw-r--r-- 1 root root  665 Nov 24 02:12 definition
-rw-r--r-- 1 root root   85 Nov 24 02:17 .node
-rw-r--r-- 1 root root   69 Nov 24 01:55 pattern

$ cat /usr/share/ztpserver/nodes/080027316065/.node
{"neighbors": {}, "model": "vEOS", "version": "4.14.2F", "systemmac": "080027316065"}

おわり

Cisco でやったことあればフーンという感じではありますが…template や変数埋め込みの仕組みが簡易なのや、bootstrap の仕組みで Arista 側に任意の bash スクリプトを実行させることが出来るのは、嬉しいかもです。

でも個人的に一番嬉しいのは、仮想OSとして各種ハイパーバイザ上で動かせるイメージが配布されていることで、こういうちょっとした機能試験が手元で簡単にできることです。(例えば別メーカで言うと Juniper の Junos を弄る場合 FireFly が使えますが、EX/MX シリーズとは勿論機能が違うし、箱版の SRX よりできることが少ないです。結果、EX を試したい時には EX2200-C とかの小さなファンレス機を探して箱物を買う羽目に…)

まあ、本領発揮は Dynamic Provisioning で!ということで。

Arista の ztpserver インストール~初期設定 (original : 2014/11/24)

この記事は某所で 2014/11/24 に書いたもののコピーです。
そのため 2017/05/13 時点ではやや古い情報も含まれています。

概要

本項の内容

Arista が公開している ztpserver(2014/11/24 時点の release 版である v1.1.0) を Ubuntu に手動でインストールして初期設定します。

ZTP (zero touch provisioning) とは?

ZTP とは PXE ブートみたいなプロビジョニングを Arista のスイッチで実現する方式です。
スイッチを結線して起動すると、DHCP でアドレスを取得して config ファイルや bootstrap を ztpserver から取得して初期設定されます。
スイッチでは旧来 Cisco が "Smart Install" という名前で実現している方法で、Arista の ZTP は旧来の Cisco より出来ることが多いです。
Arista の ZTP 概要(動作フローなど)は公式ドキュメント Overview 章 ZTP Overview を見ると手っ取り早く掴めます。

参考リンク

Arista EOS?

Arista EOS 自体については、以下の本をオススメしときますね。

Arista Warrior: A Real-World Guide to Understanding Arista Switches and EOS

Arista Warrior: A Real-World Guide to Understanding Arista Switches and EOS

Arista ZTP に関する必読系

Packer で楽々インストール (本項では使わず)

Arista 公式で Packer も公開されているので、この辺を使うと手っ取り早く作って試せます。ネットワーク構成なども固まってしまうので、必要に応じてカスタマイズして使うのが良いと思います。本項の手動インストールでは参考資料として使いました。

事前準備

ztpserver をインストールするサーバを準備して、Python 2.7 以上をインストールしておきます。
あとは 公式の Requirement を満たせば、何でも良いと思います。
本項の環境は以下の通りです。(ztpserver は GuestOS にインストールしました。)

インストール~初期設定手順

基本的に 公式のInstallation に従いつつ、必要に応じて 公式の Ubuntu12.04 用 ztpserver Packer レシピ を参考にしながら進めます。

必要パッケージインストール

とりあえず Python が 2.7 以上であることを確認します。

$ python --version
Python 2.7.6

必要なパッケージを apt-get で install していきます。面倒なら適当に -y とかつけて下さい。
ZTP v1.1.0 で bootstrap を使う場合、XMPP サーバと接続できないと異常終了してしまいます。これは ZTP 用のサーバとは別に居ても良いのですが、今回は相乗りさせるので ejabberd も入れておきます。

$ sudo apt-get install python-setuptools python-pip python-dev li bxml2-dev libxslt-dev
$ sudo apt-get install git
$ sudo apt-get install libyaml-dev
$ sudo apt-get install isc-dhcp-server
$ sudo apt-get install ejabberd
$ sudo apt-get install libapache2-mod-wsgi

ztpserver インストール

お手軽に pip install で入れるか git clone してコンパイルするか、お好きな方で。

ztpserver インストール (pip 版)

pip で簡単にインストールできます。これで実行したら、次項の git clone 版は実行不要です。
ファイルが /etc//usr/share/ 配下に展開されるので sudo してます。

$ sudo pip install ztpserver

$ ztps -v
ztps version 1.1.0

ztpserver インストール (git clone 版)

pip install では味気ないと思うなら、手動でやることもできます。とはいえ、git clone して setup 叩くだけの簡単なお仕事です。

github から clone します。

$ git clone https://github.com/arista-eosplus/ztpserver.git

$ cd ztpserver/
$ git show
commit de90d377dd4be7f2fe5eccc70c4f3ca11c8fdc9a
Author: Andrei Dvornic <advornic@aristanetworks.com>
Date:   Fri Nov 21 19:53:00 2014 +0000

    fixes #242

diff --git a/client/bootstrap b/client/bootstrap
index 0692900..5512442 100644
--- a/client/bootstrap
+++ b/client/bootstrap
@@ -358,7 +358,8 @@ class Node(object):
             self._disable_copp()
         except jsonrpclib.jsonrpc.ProtocolError as err:
             log('WARNING: unable to disable COPP: %s '
-                '(can be ignored for EOS-4.11.x and older)' % err)
+                '(platform/EOS version might not support this feature)' % 
+                err)
 
         global SYSTEM_ID                    #pylint: disable=W0603
         SYSTEM_ID = \
@@ -397,8 +398,9 @@ class Node(object):
                 'show management api http-commands | grep running')
 
     def _disable_copp(self):
-        # COPP does not apply to vEOS
-        if self.system()['model'] != 'vEOS':
+        # COPP does not apply to vEOS or EOS-4.11.x and earlier
+        if (self.system()['model'] != 'vEOS' and
+            int(self.system()['version'].split('.')[1]) < 12):
             self.api_config_cmds(['control-plane',
                                   'no service-policy input copp-system-policy'])
 

2014/11/24 時点のリリース版を checkout します。

$ git checkout v1.1.0
Note: checking out 'v1.1.0'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

  git checkout -b new_branch_name

HEAD is now at 269e7c2... Merge pull request #199 from arista-eosplus/rtd-docs

$ git show
commit 269e7c264c459c3af13104d28a9fd140cb3be47d
Merge: 53fb159 4731287
Author: Jere Julian <jerearista@users.noreply.github.com>
Date:   Wed Aug 27 19:25:06 2014 -0400

    Merge pull request #199 from arista-eosplus/rtd-docs
    
    Add capability for sphinx at ReadTheDocs to find bootstrap and actions docstrings

$ ls -alh
total 100K
drwxrwxr-x 11 kotetsu kotetsu 4.0K Nov 23 00:42 .
drwxr-xr-x  4 kotetsu kotetsu 4.0K Nov 23 00:40 ..
drwxrwxr-x  2 kotetsu kotetsu 4.0K Nov 23 00:42 actions
drwxrwxr-x  2 kotetsu kotetsu 4.0K Nov 23 00:42 bin
-rw-rw-r--  1 kotetsu kotetsu   20 Nov 23 00:42 CHANGELOG.md
drwxrwxr-x  3 kotetsu kotetsu 4.0K Nov 23 00:42 client
drwxrwxr-x  2 kotetsu kotetsu 4.0K Nov 23 00:42 conf
-rw-rw-r--  1 kotetsu kotetsu 1.8K Nov 23 00:41 CONTRIBUTING.md
drwxrwxr-x  3 kotetsu kotetsu 4.0K Nov 23 00:42 docs
drwxrwxr-x  8 kotetsu kotetsu 4.0K Nov 23 00:42 .git
-rw-rw-r--  1 kotetsu kotetsu  400 Nov 23 00:42 .gitignore
-rw-rw-r--  1 kotetsu kotetsu  300 Nov 23 00:41 INSTALL.md
-rw-rw-r--  1 kotetsu kotetsu 2.3K Nov 23 00:42 LICENSE
-rw-rw-r--  1 kotetsu kotetsu 2.0K Nov 23 00:42 Makefile
-rw-rw-r--  1 kotetsu kotetsu   96 Nov 23 00:42 MANIFEST.in
-rw-rw-r--  1 kotetsu kotetsu 1.1K Nov 23 00:42 .pylintrc
-rw-rw-r--  1 kotetsu kotetsu 2.5K Nov 23 00:41 README.md
-rw-rw-r--  1 kotetsu kotetsu   55 Nov 23 00:41 requirements.txt
-rw-rw-r--  1 kotetsu kotetsu   37 Nov 23 00:41 setup.cfg
-rw-rw-r--  1 kotetsu kotetsu 3.2K Nov 23 00:42 setup.py
drwxrwxr-x  6 kotetsu kotetsu 4.0K Nov 23 00:41 test
-rw-rw-r--  1 kotetsu kotetsu  353 Nov 23 00:41 .travis.yml
drwxrwxr-x  2 kotetsu kotetsu 4.0K Nov 23 00:41 utils
-rw-rw-r--  1 kotetsu kotetsu    6 Nov 23 00:42 VERSION
drwxrwxr-x  2 kotetsu kotetsu 4.0K Nov 23 00:42 ztpserver

build ~ install します。
ファイルが /etc//usr/share/ 配下に展開されるので sudo してます。

$ python setup.py build

$ sudo python setup.py install

$ ztps -v
ztps version 1.1.0

DHCPサーバ (isc-dhcp-server) の設定

/etc/dhcp/dhcpd.conf に環境に合わせた設定を追加しておきます。
domain-name 関係は今回使っていないので、設定しません。(内部 DNS サーバを使って、XMPPサーバや ZTP サーバをドメインで渡す場合には設定して下さい。本項では面倒なのでやっていないだけで、公式 Packer ではそうしています。)

以下例で range に指定しているのは、ZTP の DHCP シーケンス中で一時的に Arista に付与されるアドレスなので、複数機器を同時に多数 ZTP 処理するような場合には広めにとっておきましょう。
option bootfile-name には ZTP サーバのアドレスを入れておきます。ポート番号や path はこのままで OK です。

/etc/dhcp/dhcpd.conf の追加設定例

#Subnet created for VEOS devices
subnet 192.168.101.0 netmask 255.255.255.0 {
 range 192.168.101.61 192.168.101.69;
 option bootfile-name "http://192.168.101.16:8080/bootstrap";
 default-lease-time 86400;
 max-lease-time 86400;
}

サービス起動

$ sudo service isc-dhcp-server start

runlevel は 2345 になっている筈です。/etc/init/isc-dhcp-server.conf 参照。

XMPP サーバ (ejabberd) の設定

bootstrap を使うために必要なので、設定します。多分、startup-config ファイルを配布する方式だけを使う場合には、XMPP サーバは不要だと思います。

まず /etc/ejabberd/ejaejabberd.cfg を編集。
以下設定例で ztpsadmin の部分は、この後登録する ejabberd の admin ユーザ名です。(何でも良いです。)

%% Admin user
{acl, admin, {user, "ztpsadmin", "localhost"}}.

ejabberd の admin ユーザを登録します。以下例では ztpsadmin が設定ファイルにも書いたユーザ名で eosplus はパスワード文字列です。

$ sudo ejabberdctl register ztpsadmin localhost eosplus
User ztpsadmin@localhost successfully registered

サービスを起動します。

$ sudo service ejabberd restart
Restarting jabber server: ejabberd is not running. Starting ejabberd.

$ ejabberd status とか見ても、正常なのか良く分からなかったので、Web ブラウザで動作確認しました。http://<ejabberd 入れたサーバの IP アドレス>:5280/admin にアクセスしてユーザ名に ztpsadmin@localhost を、パスワードに eosplus を入力して、何かオレンジの画面が出たらきっと動いてます。WebUI で設定変更とか出来るようです。

f:id:kakkotetsu:20170513161309j:plain

ztpserver の設定

ZTP の方式によって色々とカスタマイズできる個別設定は別項として、ここでは共通設定を済ませておきます。
例によって 公式の Configuration公式の Examples に従い、必要に応じて 公式 Packer で配布している設定ファイル群 を参考に進めます。

Global configuration file (/etc/ztpserver/ztpserver.conf) 編集

/etc/ztpserver/ztpserver.conf を編集します。
デフォルトの identifier = serialnumberidentifier = systemmac にしています。自分が試した vEOS (公式で配布されている仮想OS)は、ZTP シーケンス時 serialnumber を空で ZTP サーバに送信していたので、identifier = serialnumber だと異常終了してしまったからです。

[default]

# Location of all ztps boostrap process data files
data_root = /usr/share/ztpserver

# UID used in the /nodes structure (either systemmac or serialnumber)
identifier = systemmac

# Server URL to-be-advertised to clients (via POST replies) during the bootstrap process
server_url = http://192.168.101.16:8080

# Enable local logging
logging = True

# Enable console logging
console_logging = True

# Globally disable topology validation in the bootstrap process
disable_topology_validation = False


[server]

# Note: this section only applies to using the standalone server.  If
# running under a WSGI server, these values are ignored

# Interface to which the server will bind to (0:0:0:0 will bind to
# all available IPv4 addresses on the local machine)
interface = 192.168.101.16

# TCP listening port
port = 8080


[ files]
# Path for the files directory (overriding data_root/files)
folder = files
path_prefix = /usr/share/ztpserver

[actions]
# Path for the actions directory (overriding data_root/actions)
folder = actions
path_prefix = /usr/share/ztpserver

[bootstrap]
# Path for the bootstrap directory (overriding data_root/bootstrap)
folder = bootstrap
path_prefix = /usr/share/ztpserver

# Bootstrap filename
filename = bootstrap


[neighbordb]

# Neighbordb filename (file located in data_root)
filename = neighbordb

bootstrap 設定ファイル (/usr/share/ztpserver/bootstrap/bootstrap.conf) 編集

これは bootstrap 方式で ZTP する場合にだけ使われるので、共通設定とはいえないかもですが。

/usr/share/ztpserver/bootstrap/bootstrap.conf を編集します。xmpp のところには、ejaejabberd で設定した usernamepassword を書きます。domain は本項の環境では使わないので、IP アドレスを。

logging:
   - destination: file:/tmp/ztps-log
     level: DEBUG

xmpp:
   username: ztpsadmin
   password: eosplus
   domain: 192.168.101.16
   msg_type : debug
   rooms:
     - xmpproom

ztpserver 起動

以下のように起動します。ZTP の方式によってはシーケンス中に /usr/share/ztpserver/ 配下にファイル生成するので、sudo してます。

$ sudo ztps --debug
INFO: [app:115] Logging started for ztpserver
INFO: [app:116] Using repository /usr/share/ztpserver
DEBUG: [controller:776] server URL: http://192.168.101.16:8080
Starting server on http://192.168.101.16:8080

おわり

インストールと初期設定だけなので、特筆すべき点はないです。