pyATS/Genie: ネットワーク機器(IOSXE)を RESTCONF で操作する (rest.connector)

(この記事はネットワーク自動化 Advent Calendar 2019 24日目として書いています)

ネットワーク自動化のカレンダーページです。

今回は前回の記事(NETCONF)に続き、pyATS/Genie を使った RESTCONF でネットワーク機器を操作する方法を紹介します。

RESTCONF とは?

詳細については他の文献にお願いするとして割愛しますが、ざっくり言うとNETCONF に似ており YANG モデルを使う点では同じですが、プロトコルとして HTTP/HTTPS を用い、データも JSON フォーマットで扱うことができます。

XML よりも JSON の方が可読性が高いことや、HTTP/HTTPS というサーバでは使われている手法を使うことにより、サーバの REST 操作と同じ要領でネットワーク機器も操作することができるというメリットがあるかと思います。

IOSXE で RESTCONF 設定

NETCONF の場合と同じく、RESTCONF を使うためにネットワーク機器へ設定が必要になります。RESTCONF では HTTP/HTTPS を使うため、restconf 設定に加えて、HTTPS サーバ設定、ユーザに privilege 15 を設定するなどが必要になります。

conf t
username cisco password 0 Cisc0123
username rest privilege 15 password cisco
!
aaa new-model
aaa authentication login default local
aaa authorization exec default local 
aaa session-id common
!
line con 0
 login authentication default
!
restconf
no ip http server
ip http secure-server
end

pyATS/Genie とrest.connector のインストール

python の virtualenv を有効にしたあとで、pyATS/Genie と REST コネクタの rest.connector をインストールします。

(genie)$ pip install genie rest.connector

RESTCONF を使う testbed.yaml

Telnet/SSH/NETCONF とほぼ同様です。コネクション配下の class に RESTCONF で使う rest.connector を指定します。RESTCONF ではデフォルトポートは 443 になります。

devices:
  R1_xe:
    alias: R1_xe
    connections:
      defaults:
        via: rest
      rest:
        class: rest.connector.Rest
        ip: 172.16.1.228
        port: 443
        credentials:
          rest:
            username: rest
            password: cisco
      ssh:
        ip: 172.16.1.228
        protocol: ssh
    credentials:
      default:
        password: Cisc0123
        username: cisco
      enable:
        password: Cisc0123
    os: iosxe
    platform: iosxe
    type: CSR1000v

RESTCONF でネットワーク機器へ接続

それではネットワーク機器(IOSXE)へ RESTCONF で接続します。
下記は genie shell から試した例です。Telnet/SSH などと同じように connect() で接続できます。

$ genie shell --testbed-file tb.yaml
Welcome to Genie Interactive Shell
==================================
Python 3.6.5 (default, Dec 10 2019, 14:15:25)
[GCC 5.4.0 20160609]

>>> from genie.testbed import load
>>> testbed = load('tb.yaml')
-------------------------------------------------------------------------------
>>> dev = testbed.devices['R1_xe']
>>> dev.connect(via='rest', alias='rest')
Connecting to 'R1_xe' with alias 'cli'
Connected successfully to 'R1_xe'
<Response [200]>
>>>

確認用に SSH でのコネクションも確立しておきます。

>>> dev.connect(via='ssh', alias='ssh')
[2019-12-23 11:04:37,539] +++ R1_xe logfile /tmp/R1_xe-ssh-20191223T110437539.log +++
[2019-12-23 11:04:37,540] +++ Unicon plugin iosxe +++
Password:
[2019-12-23 11:04:38,299] +++ connection to spawn: ssh -l cisco 172.16.1.228, id: 139901769082024 +++
[2019-12-23 11:04:38,299] connection to R1_xe




R1_xe>
[2019-12-23 11:04:38,441] +++ initializing handle +++
enable
Password:
R1_xe#
[2019-12-23 11:04:38,559] +++ R1_xe: executing command 'term length 0' +++
term length 0
R1_xe#
[2019-12-23 11:04:38,779] +++ R1_xe: executing command 'term width 0' +++
term width 0
R1_xe#
[2019-12-23 11:04:38,952] +++ R1_xe: executing command 'show version' +++
show version
Cisco IOS XE Software, Version 16.09.01
Cisco IOS Software [Fuji], Virtual XE Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.9.1, RELEASE SOFTWARE (fc2)
Technical Support: http://www.cisco.com/techsupport
Copyright (c) 1986-2018 by Cisco Systems, Inc.
Compiled Tue 17-Jul-18 16:57 by mcpre


Cisco IOS-XE software, Copyright (c) 2005-2018 by cisco Systems, Inc.
All rights reserved.  Certain components of Cisco IOS-XE software are
licensed under the GNU General Public License ("GPL") Version 2.0.  The
software code licensed under GPL Version 2.0 is free software that comes
with ABSOLUTELY NO WARRANTY.  You can redistribute and/or modify such
GPL code under the terms of GPL Version 2.0.  For more details, see the
documentation or "License Notice" file accompanying the IOS-XE software,
or the applicable URL provided on the flyer accompanying the IOS-XE
software.


ROM: IOS-XE ROMMON

R1_xe uptime is 2 weeks, 1 day, 20 hours, 37 minutes
Uptime for this control processor is 2 weeks, 1 day, 20 hours, 39 minutes
System returned to ROM by reload
System restarted at 14:27:16 UTC Sat Dec 7 2019
System image file is "bootflash:packages.conf"
Last reload reason: Reload Command



This product contains cryptographic features and is subject to United
States and local country laws governing import, export, transfer and
use. Delivery of Cisco cryptographic products does not imply
third-party authority to import, export, distribute or use encryption.
Importers, exporters, distributors and users are responsible for
compliance with U.S. and local country laws. By using this product you
agree to comply with applicable laws and regulations. If you are unable
to comply with U.S. and local laws, return this product immediately.

A summary of U.S. laws governing Cisco cryptographic products may be found at:


If you require further assistance please contact us by sending email to
export@cisco.com.

License Level: ax
License Type: Default. No valid license found.
Next reload license Level: ax

cisco CSR1000V (VXE) processor (revision VXE) with 1217428K/3075K bytes of memory.
Processor board ID 9ZFZVAFKW4L
2 Gigabit Ethernet interfaces
32768K bytes of non-volatile configuration memory.
3018864K bytes of physical memory.
7774207K bytes of virtual hard disk at bootflash:.
0K bytes of WebUI ODM Files at webui:.

Configuration register is 0x2102

R1_xe#
[2019-12-23 11:04:39,127] +++ R1_xe: config +++
config term
Enter configuration commands, one per line.  End with CNTL/Z.
R1_xe(config)#no logging console
R1_xe(config)#line console 0
R1_xe(config-line)#exec-timeout 0
R1_xe(config-line)#end
R1_xe#
'Password: \r\n\r\n\r\n\r\nR1_xe>'
>>> dev.ssh.connected
True

get()

ネットワーク機器が対応している YANG モデルを確認する

RESTCONF でアクセスするネットワーク機器が対応している YANG モデルを確認する方法です。get() で RESTCONF URI/restconf/data/ietf-yang-library:modules-state/ を指定することでリストを取得できます。

>>> res = dev.rest.get('/restconf/data/ietf-yang-library:modules-state/')
>>> import json
>>> print(json.dumps(res.json()['ietf-yang-library:modules-state'], indent=2))
{
  "module-set-id": "69baa0408f1bed9aed4f037b9f69b5d1",
  "module": [
(snip)
   {
      "name": "Cisco-IOS-XE-device-tracking",
      "revision": "2017-06-07",
      "schema": "https://172.16.1.228:443/restconf/tailf/modules/Cisco-IOS-XE-device-tracking/2017-06-07",
      "namespace": "http://cisco.com/ns/yang/Cisco-IOS-XE-device-tracking",
      "conformance-type": "implement"
    },
    {
      "name": "Cisco-IOS-XE-dhcp",
      "revision": "2018-06-16",
      "schema": "https://172.16.1.228:443/restconf/tailf/modules/Cisco-IOS-XE-dhcp/2018-06-16",
      "namespace": "http://cisco.com/ns/yang/Cisco-IOS-XE-dhcp",
      "conformance-type": "implement"
    },
    {
      "name": "Cisco-IOS-XE-dhcp-oper",
      "revision": "2018-02-13",
      "schema": "https://172.16.1.228:443/restconf/tailf/modules/Cisco-IOS-XE-dhcp-oper/2018-02-13",
      "namespace": "http://cisco.com/ns/yang/Cisco-IOS-XE-dhcp-oper",
      "conformance-type": "implement"
    },
(snip)

インタフェース情報の取得

先ほどと同じく get() を使って interface GigabitEthernet2 の情報を取得してみます。

>>> res = dev.rest.get('/restconf/data/ietf-interfaces:interfaces/interface=GigabitEthernet2')
>>> import json
>>> print(json.dumps(res.json(), indent=2))
{
  "ietf-interfaces:interface": {
    "name": "GigabitEthernet2",
    "type": "iana-if-type:ethernetCsmacd",
    "enabled": true,
    "ietf-ip:ipv4": {
      "address": [
        {
          "ip": "172.16.12.1",
          "netmask": "255.255.255.0"
        }
      ]
    },
    "ietf-ip:ipv6": {}
  }
}
>>>

RESTCONF URI の interface=GigabitEthernet2 を指定しない場合は、全インタフェースの情報が取得できます。

>>> res = dev.rest.get('/restconf/data/ietf-interfaces:interfaces')
>>> print(json.dumps(res.json(), indent=2))
{
  "ietf-interfaces:interfaces": {
    "interface": [
      {
        "name": "GigabitEthernet1",
        "type": "iana-if-type:ethernetCsmacd",
        "enabled": true,
        "ietf-ip:ipv4": {},
        "ietf-ip:ipv6": {}
      },
      {
        "name": "GigabitEthernet2",
        "type": "iana-if-type:ethernetCsmacd",
        "enabled": true,
        "ietf-ip:ipv4": {
          "address": [
            {
              "ip": "172.16.12.1",
              "netmask": "255.255.255.0"
            }
          ]
        },
        "ietf-ip:ipv6": {}
      },
      {
        "name": "Loopback0",
        "type": "iana-if-type:softwareLoopback",
        "enabled": true,
        "ietf-ip:ipv4": {
          "address": [
            {
              "ip": "10.1.1.1",
              "netmask": "255.255.255.255"
            }
          ]
        },
        "ietf-ip:ipv6": {}
      }
    ]
  }
}

RESTCONF URI とは?

今まで上の get() の中に入れていたものが RESTCONF URI と言われるものです。

>>> res = dev.rest.get('/restconf/data/ietf-interfaces:interfaces/interface=GigabitEthernet2')

例えば、上記の RESTCONF が何故そのような値になるかと言うと、YANG モデルによるものとなります。先頭の /restconf/data/ の data 部分は resource-type と言われ、data または operations(RPC) などがあります。

その後の ietf-interfaces:interfaces/interface=GigabitEthernet2 がどこからくるかと言うと、YANG モデルになり、下記 <<< で示したように YANG モデルの階層構造に沿って RESTCONF URI が構成されていることが分かるかと思います。

(genie)$ pip install pyang
(genie)$ pyang -f tree ietf-interfaces.yang
module: ietf-interfaces <<<
  +--rw interfaces      <<<
  |  +--rw interface* [name] <<<
  |     +--rw name                        string
  |     +--rw description?                string
  |     +--rw type                        identityref
  |     +--rw enabled?                    boolean
  |     +--rw link-up-down-trap-enable?   enumeration {if-mib}?

例えば、さらに深い階層になる特定のインタフェースの type のみを取得したい場合には、下記のように指定し取得します。

>>> res = dev.rest.get('/restconf/data/ietf-interfaces:interfaces/interface=GigabitEthernet2/type')
>>> print(json.dumps(res.json(), indent=2))
{
  "ietf-interfaces:type": "iana-if-type:ethernetCsmacd"
}
>>>

YANGモデルを読み解けば RESTCONF URI が作成できることが理解いただけたかと思います。

post()

post() は新規設定追加の場合に使います。下記の例では 設定変更したい箇所を RESTCONF URI で指定し、payload に設定変更したい内容を送信することで設定追加ができます。今回は ip domain name 設定を変更してみます。

現在の状態は ip domain が設定されていないことを確認します。

>>> dev.ssh.execute('show run | i domain')
[2019-12-23 16:27:16,884] +++ R1_xe: executing command 'show run | i domain' +++
show run | i domain
R1_xe#
''

上記を ip domain name ccieojisan.net へ変更する場合には、RESTCONF URI で設定変更したい箇所を指定し、payload で変更内容を送ります。

>>> payload = """
... {
...     "name": "ccieojisan.net"
... }
... """
>>> dev.rest.post('/restconf/data/Cisco-IOS-XE-native:native/ip/domain', payload=payload)
<Response [201]>

Response 201 は正常に設定追加が行われたことを示しています。そのため、設定が存在する状態で post() すると、下記のように設定対象が既に存在するため、409(Conflict) コードが返ってきてエラーになります。

>>> payload = """
... {
...     "name": "ccieojisan.net"
... }
... """
>>> dev.rest.post('/restconf/data/Cisco-IOS-XE-native:native/ip/domain', payload=payload)
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "src/pyats/async_/synchronize.py", line 117, in pyats.async_.synchronize.Lockable.locked._wrapped
  File "/home/virl/.pyenv/versions/3.6.5/envs/genie_rest/pypi/rest/connector/src/rest/connector/libs/iosxe/implementation.py", line 285, in post
    t=response.text))
requests.exceptions.RequestException: '409' result code has been returned instead of the expected status code(s) '(201, 204, 200)' for 'R1_xe'
{
  "errors": {
    "error": [
      {
        "error-message": "object already exists: /ios:native/ios:ip/ios:domain/ios:name",
        "error-path": "/Cisco-IOS-XE-native:native/ip/domain",
        "error-tag": "data-exists",
        "error-type": "application"
      }
    ]
  }
}

設定変更後のコンフィグを ssh 経由で確認します。

>>> dev.ssh.execute('show run | i domain')
[2019-12-23 16:27:43,885] +++ R1_xe: executing command 'show run | i domain' +++
show run | i domain
ip domain name ccieojisan.net
R1_xe#
'ip domain name ccieojisan.net'
>>>

patch()

patch() では設定の部分変更ができます。まずは ssh 経由で設定内容を確認します。(RESTCONF でも設定確認できますが、現時点では CLI の方が理解しやすいと思ったため、CLI を使っています。)

>>> dev.ssh.execute('sh run int gi2')
[2019-12-23 11:13:19,202] +++ R1_xe: executing command 'sh run int gi2' +++
sh run int gi2
Building configuration...

Current configuration : 138 bytes
!
interface GigabitEthernet2
 ip address 172.16.12.1 255.255.255.0
 ip ospf 1 area 0
 negotiation auto
 no mop enabled
 no mop sysid
end

R1_xe#
'Building configuration...\r\n\r\nCurrent configuration : 138 bytes\r\n!\r\ninterface GigabitEthernet2\r\n ip address 172.16.12.1 255.255.255.0\r\n ip ospf 1 area 0\r\n negotiation auto\r\n no mop enabled\r\n no mop sysid\r\nend'
>>>

続いて、設定変更したい箇所の RESTCONF URL と変更内容の payload を送ることで設定変更ができます。

>>> payload = """
... {
...     "primary": {
...         "address": "192.168.1.1",
...         "mask": "255.255.255.0"
...     }
... }
... """
>>> dev.rest.patch("/restconf/data/Cisco-IOS-XE-native:native/interface/GigabitEthernet=2/ip/address/primary", payload=payload)
<Response [204]>

上記のように Response 204 が返ってきたら、正常に設定変更が行われています。もう一度 ssh 経由で確認すると設定変更が行われたことが分かります。

>>> dev.ssh.execute('sh run int gi2')
[2019-12-23 11:21:38,698] +++ R1_xe: executing command 'sh run int gi2' +++
sh run int gi2
Building configuration...

Current configuration : 138 bytes
!
interface GigabitEthernet2
 ip address 192.168.1.1 255.255.255.0
 ip ospf 1 area 0
 negotiation auto
 no mop enabled
 no mop sysid
end

R1_xe#
'Building configuration...\r\n\r\nCurrent configuration : 138 bytes\r\n!\r\ninterface GigabitEthernet2\r\n ip address 192.168.1.1 255.255.255.0\r\n ip ospf 1 area 0\r\n negotiation auto\r\n no mop enabled\r\n no mop sysid\r\nend'
>>>

put()

put() では指定した箇所を丸ごと更新することができます。今回はホスト名を変更してみます。

まずは事前の hostname を確認します。R1_xe であることが分かります。

>>> dev.ssh.execute('show run | i hostname')
[2019-12-23 12:11:42,642] +++ R1_xe: executing command 'show run | i hostname' +++
show run | i hostname
hostname R1_xe
R1_xe#
'hostname R1_xe'

ホスト名をtest に変えてみます。今までと同じく設定変更した箇所の RESTCONF URI と変更内容を書いた payload を送ります。

>>> payload = """
... {
...     "hostname": "test"
... }
... """
>>> dev.rest.put('/restconf/data/Cisco-IOS-XE-native:native/hostname', payload=payload)
<Response [204]>

上記ではホスト名を変更しました。pyATS/Genie では接続時に testbed.yaml のデバイス名とホスト名のマッチングをチェックしているため、dev.ssh.hostname でホスト名を上書きして接続して確認することができます。

>>> dev.ssh.hostname = 'test'
>>> dev.ssh.execute('show run | i hostname')
[2019-12-23 12:28:53,218] +++ test: executing command 'show run | i hostname' +++
show run | i hostname
hostname test
test#
'hostname test'
>>>

put() vs patch() の違いは?

今までの説明で put() は 指定した箇所を丸ごと更新 と説明し、patch() は 設定の部分変更 と説明しましたが、少しわかりにくいかと思ったので補足しておきます。

patch() については前述の項目を参照ください。再度見てもらうと分かりますが、interface GigabitEthernet2 のip address のみ変更になっていることが分かります。

少し payload 変更して put で同じ操作をやってみましょう。まずは事前の設定を ssh で確認しておきます。

>>> dev.ssh.execute('show run int gi2')
[2019-12-23 13:30:24,790] +++ R1_xe: executing command 'show run int gi2' +++
show run int gi2
Building configuration...

Current configuration : 138 bytes
!
interface GigabitEthernet2
 ip address 172.16.12.1 255.255.255.0
 ip ospf 1 area 0
 negotiation auto
 no mop enabled
 no mop sysid
end

R1_xe#
'Building configuration...\r\n\r\nCurrent configuration : 138 bytes\r\n!\r\ninterface GigabitEthernet2\r\n ip address 172.16.12.1 255.255.255.0\r\n ip ospf 1 area 0\r\n negotiation auto\r\n no mop enabled\r\n no mop sysid\r\nend'
>>>

次に put() で GigabitEthernet2 に対して少し上の階層から作成したIPアドレス変更を送ってみます。

>>> payload = """
... {
...   "GigabitEthernet": {
...     "name": "2",
...     "ip": {
...      "address": {
...        "primary": {
...          "address": "192.168.1.1",
...          "mask": "255.255.255.0"
...         }
...       }
...     }
...   }
... }
... """
>>> dev.rest.put("/restconf/data/Cisco-IOS-XE-native:native/interface/GigabitEthernet=2", payload=payload)
<Response [204]>

この場合に、ip address のみ変わって欲しいところですが、put だと他のキーがデフォルトで上書きされてしまいます。

>>> dev.ssh.execute('show run int gi2')
[2019-12-23 13:54:28,726] +++ R1_xe: executing command 'show run int gi2' +++
show run int gi2
Building configuration...

Current configuration : 105 bytes
!
interface GigabitEthernet2
 ip address 192.168.1.1 255.255.255.0
 speed 1000
 no negotiation auto
end

R1_xe#
'Building configuration...\r\n\r\nCurrent configuration : 105 bytes\r\n!\r\ninterface GigabitEthernet2\r\n ip address 192.168.1.1 255.255.255.0\r\n speed 1000\r\n no negotiation auto\r\nend'
>>>

上記コンフィグを確認すると、ip addrss だけでなく、ip ospf area 設定が消え、speed 設定が追加などが行われています。

patch() で同じ payload を送って、同じく確認してみます。すると、ip address のみが変更できていることが分かるかと思います。

>>> payload = """
... {
...   "GigabitEthernet": {
...     "name": "2",
...     "ip": {
...      "address": {
...        "primary": {
...          "address": "192.168.1.1",
...          "mask": "255.255.255.0"
...         }
...       }
...     }
...   }
... }
... """
>>> dev.rest.patch("/restconf/data/Cisco-IOS-XE-native:native/interface/GigabitEthernet=2", payload=payload)
<Response [204]>
>>> dev.ssh.execute('show run int gi2')
[2019-12-23 16:11:13,608] +++ R1_xe: executing command 'show run int gi2' +++
show run int gi2
Building configuration...

Current configuration : 138 bytes
!
interface GigabitEthernet2
 ip address 192.168.1.1 255.255.255.0
 ip ospf 1 area 0
 negotiation auto
 no mop enabled
 no mop sysid
end

R1_xe#
'Building configuration...\r\n\r\nCurrent configuration : 138 bytes\r\n!\r\ninterface GigabitEthernet2\r\n ip address 192.168.1.1 255.255.255.0\r\n ip ospf 1 area 0\r\n negotiation auto\r\n no mop enabled\r\n no mop sysid\r\nend'
>>>

delete()

delete() では設定削除を行います。削除したい箇所の RESTCONF URI を送ります。下記は Interface GigabitEthernet2 から ip address を削除する例です。

>>> dev.rest.delete("/restconf/data/Cisco-IOS-XE-native:native/interface/GigabitEthernet=2/ip/address/primary")
<Response [204]>

ssh 経由で設定を確認すると no ip address になっていることが確認できます。

>>> dev.ssh.execute('show run int gi2')
[2019-12-23 11:49:14,993] +++ R1_xe: executing command 'show run int gi2' +++
show run int gi2
Building configuration...

Current configuration : 115 bytes
!
interface GigabitEthernet2
 no ip address
 ip ospf 1 area 0
 negotiation auto
 no mop enabled
 no mop sysid
end

R1_xe#
'Building configuration...\r\n\r\nCurrent configuration : 115 bytes\r\n!\r\ninterface GigabitEthernet2\r\n no ip address\r\n ip ospf 1 area 0\r\n negotiation auto\r\n no mop enabled\r\n no mop sysid\r\nend'
>>>

まとめ

  • pyATS/Genie でも RESTCONF が使える!
  • RESTCONF を使う場合もtestbed.yaml や操作 device.xxx() はほぼ同じ!
  • SSH と RESTCONF 両方のセッションを保持してそれぞれ確認することが可能
  • get() : コンフィグや機器の状態を取得
  • post() : 機器へのコンフィグ新規追加
  • patch() : 機器へのコンフィグ部分変更
  • put() : 機器へのコンフィグ指定箇所丸ごと更新
  • delete() : コンフィグ削除
  • pyATS/Genie ではホスト名が変更になっても、hostname アトリビュートを上書きすることでアクセスが可能

今回は pyATS/Genie を使ってネットワーク機器(IOSXE)を RESTCONF で操作する方法を紹介しました。pyATS/Genie では RESTCONF での操作もでき、各機器へのそれぞれのコネクションを持てるため、例えば RESTCONF で設定変更し、CLI で確認ということも可能になります。また、技術的には RESTCONF をメインで使うことを想定し、問題があれば CLI へフォールバックするということも実現可能です。

スポンサーリンク