Genie ConfによるCisco機器のConfig自動生成

# はじめに
Ciscoが公開しているPythonベースのテストフレームワーク/フィーチャーライブラリー「pyATS/Genie」の内、Genie Confを使ってみた結果をメモしておきます。

# Genie Confとは
インターフェース、スパニングツリー、ACL、BGP、OSPF、VLAN、HSRPなどの設定項目に対し、アトリビュート（パラメーター）を指定すると、Configを自動で生成してくれるPythonオブジェクトです。
IOS、IOS-XE、IOS-XR、NX-OS等に対応しており、OSによるコマンドの違いを意識することなく、設定変更が可能です。

参考URL:
[Genie Documentation - Getting Started - Genie.Conf](https://pubhub.devnetcloud.com/media/pyats-packages/docs/genie/cookbooks/conf.html)
[GitHub - genielibs - conf](https://github.com/CiscoTestAutomation/genielibs/tree/master/pkgs/conf-pkg/src/genie/libs/conf)

# セットアップ

#### pyATS/Genieインストール
CCIEおじさんのブログ記事を参考にさせて頂きました。
[pyATS/Genie : ネットワークテスト自動化ツール pyATS & Genie についてとインストール方法](https://ccieojisan.net/post-1697/)

#### 検証環境  
Cisco DevNet SandboxのCatalyst9300(16.6.5)×1台を使用しました。  


# Testbed
対象機器のホスト名、OSタイプ、ログイン情報などをYAML形式で記載します。
(Ansibleで言うところのInventoryファイル)

```yaml:testbed.yaml
testbed: 
  name: 'testbed'
devices:
  leaf1:   # ホスト名と同じにする必要あり
      type: catalyst9300
      os: "iosxe"   # プラットフォーム名　ios、iosxe、iosxr、nxos等を指定
      alias: 'helper'
      tacacs:
          login_prompt: 'login:'
          password_prompt: 'Password:'
          username: admin   # ログインユーザ名
      passwords:
          tacacs: cisco   # ログインパスワード　認証方式に関係なくtacacsをキーとして指定
          enable: cisco   # enableパスワード
          line: cisco   # lineパスワード
      connections:
          ssh:
              protocol: ssh   # telnet、ポート番号も指定可能
              ip: "10.10.20.81"   # ログインIPアドレス
```

今回の対象機器は、ホスト名`leaf1`の1台ですが、例えば`leaf2`も変更する場合は、下に`leaf2:`を追記し、同様にOSタイプ、ログイン情報を記載します。aliasは同じ値が使えないので注意が必要です。


# Pythonコードと出力結果

## 設定例1: L3インターフェースの設定
Catalyst9300のマネジメントIFにDescriptionとIPアドレス設定を行い、no shutdownした例です。

```python:genie6-1.py
from genie.conf import Genie
from genie.conf.base import Interface

testbed = Genie.init('testbed.yaml')
device = testbed.devices['leaf1']

# create an interface object
intf = Interface(device=device, name='GigabitEthernet0/0')

# configuration the interface by setting various object level attributes
intf.description = '< For Test >' 
intf.ipv4 = '10.1.1.1'
intf.ipv4.netmask = '255.255.255.0'
intf.shutdown = False

# view the generated configuration
print(intf.build_config(apply=False))

# apply configuration on device
device.connect()
intf.build_config()
```

#### 出力結果
testbed.yamlと同じディレクトリにPythonコードを格納し、実行した結果です。 
冒頭の`interface GigabitEthernet0/0`コマンドは、設定Configをprint文で出力した結果です。

```:output6-1-1
$ python genie6-1.py
interface GigabitEthernet0/0
 description < For Test >
 ip address 10.1.1.1 255.255.255.0
 no shutdown
 exit
```

その後、ログイン処理と`show version`の実行が行われています。

```:output6-1-2
[2019-07-06 09:16:13,088] +++ leaf1 logfile /tmp/leaf1-cli-20190706T091613087.log +++
[2019-07-06 09:16:13,089] +++ Unicon plugin iosxe +++
[2019-07-06 09:16:13,095] +++ connection to spawn: ssh -l admin 10.10.20.81, id: 140124079080168 +++
[2019-07-06 09:16:13,096] connection to leaf1
[2019-07-06 09:16:15,117] Timeout occurred : Timeout value : 2
 Target: leaf1
 Command sent: ssh -l admin 10.10.20.81
 Pattern: '.+$'
 Got: ''
[2019-07-06 09:16:15,117] Timeout occured, sending '\r'


Password: 



leaf1#
[2019-07-06 09:16:16,704] +++ initializing handle +++
[2019-07-06 09:16:16,705] +++ leaf1: executing command 'term length 0' +++
term length 0
leaf1#
[2019-07-06 09:16:17,096] +++ leaf1: executing command 'term width 0' +++
term width 0
leaf1#
[2019-07-06 09:16:17,455] +++ leaf1: executing command 'show version' +++
show version
Cisco IOS XE Software, Version 16.06.05
Cisco IOS Software [Everest], Catalyst L3 Switch Software (CAT9K_IOSXE), Version 16.6.5, RELEASE SOFTWARE (fc3)

(省略: show versionの出力結果)

```

続いて、`no logging console`、`line console 0`、`exec-timeout 0`が実行されていますが、これは本設定が自動化の妨げになるためです。

```:output6-1-3
[2019-07-06 09:16:17,813] +++ leaf1: config +++
config term
Enter configuration commands, one per line.  End with CNTL/Z.
leaf1(config)#no logging console
leaf1(config)#line console 0
leaf1(config-line)#exec-timeout 0
leaf1(config-line)#end
leaf1#
```

無効化したい場合は、Pythonコードを以下の通り書き換えます。

```python:
# 変更前
device.connect()
# 変更後
device.connect(init_config_commands=[])
```

最後に設定変更が行われ、期待通りのコマンドが実行されました。

```:output6-1-4
[2019-07-06 09:16:19,643] +++ leaf1: config +++
config term
Enter configuration commands, one per line.  End with CNTL/Z.
leaf1(config)#interface GigabitEthernet0/0
leaf1(config-if)# description < For Test >
leaf1(config-if)# ip address 10.1.1.1 255.255.255.0
leaf1(config-if)# no shutdown
leaf1(config-if)# exit
leaf1(config)#end
leaf1#
```

## 設定例2: L3インターフェースの初期化
設定例1で設定した内容を削除（初期化）します。設定時は`intf.build_config()`を指定していましたが、削除時は`intf.build_unconfig()`を用います。

```python:genie6-2.py
from genie.conf import Genie
from genie.conf.base import Interface

testbed = Genie.init('testbed.yaml')
device = testbed.devices['leaf1']

# create an interface object
intf = Interface(device=device, name='GigabitEthernet0/0')

# remove configuration from device
device.connect()
intf.build_unconfig()
```

#### 出力結果
`default interface GigabitEthernet0/0`コマンドで設定が初期化され、最後に閉塞が行われているのが分かります。

```
[2019-07-06 09:28:08,824] +++ leaf1: config +++
config term
Enter configuration commands, one per line.  End with CNTL/Z.
leaf1(config)#default interface GigabitEthernet0/0
% Management interface VRF can not be changed.

% Error(s) seen setting interface GigabitEthernet0/0 to it's default config
leaf1(config)#interface GigabitEthernet0/0
leaf1(config-if)#shutdown
leaf1(config-if)#end
leaf1#
```

## 設定例3: L3インターフェースの一部設定削除
`intf.build_unconfig()`の引数で、削除したいアトリビュートを指定できます。一つだけの場合は、`attributes='ipv4'`(IPアドレス)のように指定し、それ以上の場合は、辞書形式で指定します。

```python:genie6-3.py
from genie.conf import Genie
from genie.conf.base import Interface

testbed = Genie.init('testbed.yaml')
device = testbed.devices['leaf1']

# create an interface object
intf = Interface(device=device, name='GigabitEthernet0/0')

# configuration the interface by setting various object level attributes
intf.description = '< For Test >' 
intf.ipv4 = '10.1.1.1'
intf.ipv4.netmask = '255.255.255.0'
intf.shutdown = False

# remove configuration from device
device.connect()
intf.build_unconfig(attributes='ipv4')
intf.build_unconfig(attributes={'description': None, 'shutdown': None})
```

#### 出力結果
2つに分かれていますが、削除できています。

```
[2019-07-06 09:49:31,747] +++ leaf1: config +++
config term
Enter configuration commands, one per line.  End with CNTL/Z.
leaf1(config)#interface GigabitEthernet0/0
leaf1(config-if)# no ip address 10.1.1.1 255.255.255.0
leaf1(config-if)# exit
leaf1(config)#end
leaf1#
[2019-07-06 09:49:34,314] +++ leaf1: config +++
config term
Enter configuration commands, one per line.  End with CNTL/Z.
leaf1(config)#interface GigabitEthernet0/0
leaf1(config-if)# no description < For Test >
leaf1(config-if)# shutdown
leaf1(config-if)# exit
leaf1(config)#end
leaf1#
```

## 設定例4: スイッチポートの設定
Trunkポートの例です。allowed vlanの追加・削除も行っています。

```python:genie6-4.py
from genie.conf import Genie
from genie.conf.base import Interface

testbed = Genie.init('testbed.yaml')
device = testbed.devices['leaf1']

# create an interface object
intf = Interface(device=device, name='TenGigabitEthernet1/0/24')

# configuration the interface by setting various object level attributes
intf.description = '< Test Switchport >' 
intf.switchport_enable = True
intf.switchport_mode = "trunk"
intf.trunk_vlans = "100-200,250"
intf.trunk_add_vlans = "300"
intf.trunk_remove_vlans = "250,300"
intf.native_vlan = "1"
intf.shutdown = False

# apply configuration on device
device.connect()
intf.build_config()
```

#### 出力結果

```
[2019-07-06 10:02:17,725] +++ leaf1: config +++
config term
Enter configuration commands, one per line.  End with CNTL/Z.
leaf1(config)#interface TenGigabitEthernet1/0/24
leaf1(config-if)# description < Test Switchport >
leaf1(config-if)# no shutdown
leaf1(config-if)# switchport
leaf1(config-if)# switchport mode trunk
leaf1(config-if)# switchport trunk allowed vlan 100-200,250
leaf1(config-if)# switchport trunk native vlan 1
leaf1(config-if)# switchport trunk allowed vlan add 300
leaf1(config-if)# switchport trunk allowed vlan remove 250,300
leaf1(config-if)# exit
leaf1(config)#end
leaf1#
```

## 設定例5: Loopbackインターフェースの設定

```python:genie6-5.py
from genie.conf import Genie
from genie.conf.base import Interface

testbed = Genie.init('testbed.yaml')
device = testbed.devices['leaf1']

# create an interface object
intf = Interface(device = device, name = 'Loopback100')

# configuration the interface by setting various object level attributes
intf.description = '< Test Loopback IF >' 
intf.ipv4 = '10.2.2.2'
intf.ipv4.netmask = '255.255.255.0'

# apply configuration on device
device.connect()
intf.build_config()
```

#### 出力結果

```
[2019-07-06 10:38:21,797] +++ leaf1: config +++
config term
Enter configuration commands, one per line.  End with CNTL/Z.
leaf1(config)#interface Loopback100
leaf1(config-if)# description < Test Loopback IF >
leaf1(config-if)# ip address 10.2.2.2 255.255.255.0
leaf1(config-if)# exit
leaf1(config)#end
leaf1#
```

## 設定例6: Loopbackインターフェースの初期化

```python:genie6-6.py
from genie.conf import Genie
from genie.conf.base import Interface

testbed = Genie.init('testbed.yaml')
device = testbed.devices['leaf1']

# create an interface object
intf = Interface(device = device, name = 'Loopback100')

# remove configuration from device
device.connect()
intf.build_unconfig()
```

#### 出力結果
物理IFの場合、default～コマンドで初期化していましたが、Loopbackのような論理IFは、IFごと削除してしまえばいいです。
Genie Confはこの違いもロジックに組み込まれているようです。

```
[2019-07-06 10:42:45,002] +++ leaf1: config +++
config term
Enter configuration commands, one per line.  End with CNTL/Z.
leaf1(config)#no interface Loopback100
leaf1(config)#end
leaf1#
```

## 設定例7: スパニングツリーの設定
今回はPVST+の例です。Rapid PVST+やMSTPにも対応しているようです。

```python:genie6-7.py
from genie.conf import Genie
from genie.libs.conf.stp import Stp

testbed = Genie.init('testbed.yaml')
device = testbed.devices['leaf1']

# create an STP instance
stp = Stp()
device.add_feature(stp)

# bridge assurance configuration
stp.device_attr[device].etherchannel_misconfig_guard = True
stp.device_attr[device].loop_guard = True
stp.device_attr[device].bpdu_guard = True

# pvst global configuration
stp.device_attr[device].mode_attr['pvst'].pvst_attr['default'].\
    vlan_attr['100'].v_bridge_priority = 4096
stp.device_attr[device].mode_attr['pvst'].pvst_attr['default'].\
    vlan_attr['100'].v_hello_time = 5

# pvst interface configuration
stp.device_attr[device].mode_attr['pvst'].pvst_attr['default'].\
    vlan_attr['100'].interface_attr['TenGigabitEthernet1/0/24'].v_if_cost = '123'
stp.device_attr[device].mode_attr['pvst'].pvst_attr['default'].\
    vlan_attr['100'].interface_attr['TenGigabitEthernet1/0/24'].v_if_port_priority = 16

stp.device_attr[device].mode_attr['pvst'].pvst_attr['default'].\
    interface_attr['TenGigabitEthernet1/0/23'].p_if_edge_port = 'edge_enable'
stp.device_attr[device].mode_attr['pvst'].pvst_attr['default'].\
    interface_attr['TenGigabitEthernet1/0/23'].p_if_link_type = 'shared'
stp.device_attr[device].mode_attr['pvst'].pvst_attr['default'].\
    interface_attr['TenGigabitEthernet1/0/23'].p_if_guard = 'root'
stp.device_attr[device].mode_attr['pvst'].pvst_attr['default'].\
    interface_attr['TenGigabitEthernet1/0/23'].p_if_bpdu_guard = True
stp.device_attr[device].mode_attr['pvst'].pvst_attr['default'].\
    interface_attr['TenGigabitEthernet1/0/23'].p_if_bpdu_filter = True

# apply configuration on device
device.connect()
stp.build_config()
```

#### 出力結果

```
[2019-07-06 11:09:54,835] +++ leaf1: config +++
config term
Enter configuration commands, one per line.  End with CNTL/Z.
leaf1(config)#spanning-tree etherchannel guard misconfig
leaf1(config)#spanning-tree loopguard default
leaf1(config)#spanning-tree portfast bpduguard default
leaf1(config)#spanning-tree mode pvst
leaf1(config)#spanning-tree vlan 100 hello-time 5
leaf1(config)#spanning-tree vlan 100 priority 4096
leaf1(config)#interface TenGigabitEthernet1/0/24
leaf1(config-if)# spanning-tree vlan 100 cost 123
leaf1(config-if)# spanning-tree vlan 100 port-priority 16
leaf1(config-if)# exit
leaf1(config)#interface TenGigabitEthernet1/0/23
leaf1(config-if)# spanning-tree portfast
%Warning: portfast should only be enabled on ports connected to a single
 host. Connecting hubs, concentrators, switches, bridges, etc... to this
 interface  when portfast is enabled, can cause temporary bridging loops.
 Use with CAUTION

%Portfast has been configured on TenGigabitEthernet1/0/23 but will only
 have effect when the interface is in a non-trunking mode.
leaf1(config-if)# spanning-tree link-type shared
leaf1(config-if)# spanning-tree guard root
leaf1(config-if)# spanning-tree bpduguard enable
leaf1(config-if)# spanning-tree bpdufilter enable
leaf1(config-if)# exit
leaf1(config)#end
leaf1#
```

## 設定例8: スパニングツリーの削除

```python:genie6-8.py
# genie6-7.pyと異なる点
stp.build_unconfig()
```

#### 出力結果

```
[2019-07-06 11:16:57,490] +++ leaf1: config +++
config term
Enter configuration commands, one per line.  End with CNTL/Z.
leaf1(config)#no spanning-tree etherchannel guard misconfig
leaf1(config)#no spanning-tree loopguard default
leaf1(config)#no spanning-tree portfast bpduguard default
leaf1(config)#no spanning-tree mode pvst
leaf1(config)#no spanning-tree vlan 100 hello-time 5
leaf1(config)#no spanning-tree vlan 100 priority 4096
leaf1(config)#interface TenGigabitEthernet1/0/24
leaf1(config-if)# no spanning-tree vlan 100 cost 123
leaf1(config-if)# no spanning-tree vlan 100 port-priority 16
leaf1(config-if)# exit
leaf1(config)#interface TenGigabitEthernet1/0/23
leaf1(config-if)# no spanning-tree portfast
leaf1(config-if)# no spanning-tree link-type shared
leaf1(config-if)# no spanning-tree guard root
leaf1(config-if)# no spanning-tree bpduguard enable
leaf1(config-if)# no spanning-tree bpdufilter enable
leaf1(config-if)# exit
leaf1(config)#end
leaf1#
```

## 設定例9: ACLの設定
名前付き拡張ACLの設定例です。ACLのエントリ追加だけでなく、IF適用のためのConfig生成も可能です。

```python:genie6-9.py
from genie.conf import Genie
from genie.libs.conf.acl import Acl

testbed = Genie.init('testbed.yaml')
device = testbed.devices['leaf1']

# create an acl object
acl = Acl()
device.add_feature(acl)

# extended acl configuration
acl.device_attr[device].acl_attr['ipv4_acl'].acl_type = 'ipv4-acl-type'
acl.device_attr[device].acl_attr['ipv4_acl'].ace_attr['10'].actions_forwarding = 'permit'
acl.device_attr[device].acl_attr['ipv4_acl'].ace_attr['10'].protocol = 'ip'
acl.device_attr[device].acl_attr['ipv4_acl'].ace_attr['10'].src = 'host 10.1.1.1'
acl.device_attr[device].acl_attr['ipv4_acl'].ace_attr['10'].dst = 'any'

# apply acl to vlan interface
acl.device_attr[device].acl_attr['ipv4_acl'].interface_attr['Vlan100'].if_in = True

# apply configuration on device
device.connect()
acl.build_config()
```

#### 出力結果

```
[2019-07-06 11:23:07,742] +++ leaf1: config +++
config term
Enter configuration commands, one per line.  End with CNTL/Z.
leaf1(config)#ip access-list extended ipv4_acl
leaf1(config-ext-nacl)# 10 permit ip host 10.1.1.1 any
leaf1(config-ext-nacl)# exit
leaf1(config)#interface Vlan100
leaf1(config-if)# ip access-group ipv4_acl in
leaf1(config-if)# exit
leaf1(config)#end
leaf1#
```

## 設定例10: ACLの削除

```python:genie6-9.py
# genie6-9.pyと異なる点
acl.build_unconfig()
```

#### 出力結果

```
[2019-07-06 11:25:31,697] +++ leaf1: config +++
config term
Enter configuration commands, one per line.  End with CNTL/Z.
leaf1(config)#no ip access-list extended ipv4_acl
leaf1(config)#interface Vlan100
leaf1(config-if)# no ip access-group ipv4_acl in
leaf1(config-if)# exit
leaf1(config)#end
leaf1#
```

# 所感
今回ご紹介した方法以外に、Jinja2テンプレートを使ってConfig自動生成する方法もあります。Genie Diffのメリットは、テンプレートファイルを自作しなくて良い点と、OSやIFタイプの違いを意識せずに済む点かと思います。
すべての設定項目には対応していないようですが、サポートしているものは今後も使っていきたいと思います。
行く行くはAnsibleのモジュールとして組み込まれたら、より多くの人がHappyになれるかも知れませんね。