【pyATS】testbed.yaml生成をJinja2で自動化してみた【Python3】

# ~~ぶっちゃけやりたい事によってはcsv＋正規表現が楽~~

pyATSを使っていて必須となるのが自動化対象ノードを記述したyamlデータですが(以下testbed.yamlといいます)、全体共通は一行分でOK！というものではなく各ノード丁寧に記述する必要があるので結構長くなります。

pyATSのcsvコンバートって実はそんなに強いものではなくて、ipやらosやらprotocol等自動化に最低限必要なカラムの定義しかないです。↓以下はソースコード(抜粋)です。
```python3
# -> pyats.contrib/src/pyats/contrib/creators/creator.py
  def _construct_yaml(self, devices):
        """ Construct list of dicts containing device data into nested yaml 
            structure.

        Args:
            devices ('list'): List of dict containing device attributes.

        Returns:
             dict: Testbed dictionary that's ready to be dumped into yaml.
    
        """
        yaml_dict = {
            'devices': {}
        }
 
                
                # get port from ip
                ad_port = row['ip'].strip().rsplit(':', 1)
                address, port = ad_port[0], ad_port[1] if len(ad_port) > 1 else None
                os = row.pop('os')

                # build the connection dict
                if port:
                    connections = {
                        'cli': {
                            'ip': address,
                            'port': int(port),
                            'protocol': row.pop('protocol')
                        }
                    }
                else:
                    connections = {
                        'cli': {
                            'ip': row.pop('ip'),
                            'protocol': row.pop('protocol')}}
                
                if 'proxy' in row:
                    connections['cli'].update({'proxy': row.pop('proxy')})

               credentials = {
                    'default': {
                        'username': row.pop('username'),
                        'password': password},
                    'enable': {
                        'password':  enable_password
                    }}


            dev = yaml_dict['devices'].setdefault(name, {})
            dev['os'] = os
            dev['connections'] = connections
            dev['credentials'] = credentials
            type = row.get('type')
            dev['type'] = type if type else os
            if row and len(row) > 0:
                for key, value in row.items():
                    if 'custom:' in key:
                        dev.setdefault('custom', {}).setdefault(
                            key.replace('custom:', ''), value)
                    else:
                        dev.setdefault(key, value)

        return yaml_dict

```

https://github.com/CiscoTestAutomation/pyats.contrib/blob/main/src/pyats/contrib/creators/creator.py

定義が無い情報を盛り込む場合は別途'custom'というカラムを作って{'wan':'Te1/0/8'}と自分で辞書っぽく書いておくしかないです。しかしながら、customカラムに定義した情報はcustom属性配下に入ってしまうので融通が利きません。

connection属性の配下arguments属性配下に置きたい'init_config_commands: []'なんかは良く使うのにデータ構造がpyATS側でガッツリ定義されちゃってるのでこれもcsvで定義できずです。


一旦csvで基礎部分を出力=>正規表現でinit_config_commandsを差し込むという何ともテンポの悪い感じになっちゃってます。カスタム属性でもっと色々定義したい方なんかは特にcsv上での可読性の悪さから結局手作業要素が増えているのではないでしょうか？

これ、Python3のJinja2でワンストップ自動化してしまいましょう！というのが今回の話題です！


## Jinja2での自動化
各ホストの接続情報が以下のような資料で定義されている想定です。今回は記事用に単一資料で作成しましたが、実際に参照するのは案件で展開されているor展開したIPアドレス一覧表やアカウント一覧表といった複数資料です。pyATSに必要な情報を各資料からPythonで少しずつ集めてマージするイメージです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3899867/c279b343-e7e7-c927-10f2-a6d7fed047d1.png)
↑Python3で上記資料を読んでいきます！

```Python:generate_yaml.py

import os
import openpyxl
import jinja2
from   jinja2 import Template, Environment, FileSystemLoader


def setup_j2():
    """
    Jinja2(テンプレートエンジン)の環境セットアップ
    可読性向上+おまじない
    """

    def debug(text) -> None:
        print(text)

    j2_env = Environment(
        trim_blocks             = True,
        keep_trailing_newline   = True,
        extensions              = ['jinja2.ext.do', 'jinja2.ext.loopcontrols'],
        loader                  = FileSystemLoader('./', encoding='utf8')
    )

    j2_env.filters.update({
        'debug'  : debug
    })

    j2_engine = j2_env.get_template('template.j2')

    return j2_engine


def get_hosts_datas():
    """
    自動化対象ホスト群のホスト名/IP/OS/機種情報を取得する
    参照している資料は試験書のチェックリスト
    """

    wb_filename = "Excel.xlsx"

    wb = openpyxl.load_workbook(wb_filename)
    ws = wb["Excel"]

    hostsdatas = {}

    for row in ws:
        column_class = row[2].value
        # もしホスト情報に関する行でなければ後続の処理をスキップする
        if column_class not in ["SW", "WLC", "RT"]:
            continue

        hostname = row[1].value
        classify = row[2].value
        os       = row[3].value
        platform = row[4].value
        ip       = row[5].value
        password = row[7].value

        hostsdatas[hostname] = {
            "platform" : platform,
            "class"    : classify,
            "os"       : os,
            "platform" : platform,
            "ip"       : ip,
            "password" : password
        }

    return hostsdatas


if __name__ == "__main__":
    """
    各関数へdocstringを記載済み。
    コーディング用ソフトウェアを使用している場合
    関数名へカーソルを合わせる事で参照可能。
    """

    os.chdir(os.path.dirname(os.path.abspath(__file__)))

    filename   = 'testbed.yaml'
    j2_engine  = setup_j2()
    hostsdatas = get_hosts_datas()

    with open(filename, "w") as f:
        generated_str = j2_engine.render(hostsdatas = hostsdatas)
        f.write(generated_str)
```
get_host_datas()関数でyaml生成に必要な各ホストの情報を取得しています。
その後、Jinja2で事前に作成したテンプレートを読ませ、ホスト数分テンプレートをループしてデータをyamlに整形しています。最後にtestbed.yamlに書き出しておわりです！


使用したJijna2テンプレートは以下となっています。
```Jinja:template.j2
devices:
{% for hostname, datas in hostsdatas.items() %}
  {{ hostname }}:
    os: {{ datas["os"] }}
{% if datas["platform"] != None %}
    platform: {{ datas["platform"] }}
{% endif %}
    credentials:
      default:
        username: admin
        password: {{ datas["password"] }}
      enable:
        password: {{ datas["password"] }}
    connections:
      cli:
{% if datas["class"] in ["RT", "WLC"] %}
        protocol: ssh
{% else %}
        protocol: telnet 
{% endif %}
        ip: {{ datas["ip"] }}
        arguments:
          init_config_commands: []
{% endfor %}

```
一台分のテンプレートしか記載していませんが、これをfor文で囲っています。ホスト名で回しているのでホストの台数分テンプレートを展開できるという仕組みです。勿論csvコンバートのような制約はなく、init_config_commands: []も簡単に差し込めます。

至る所でLoopしたりIF文を書けるインテリジェンスなテンプレート設計が魅力ですね！

## 出力
```yaml:testbed.yaml
devices:
  SW01:
    os: ios
    platform: cat3k
    credentials:
      default:
        username: admin
        password: cisco
      enable:
        password: cisco
    connections:
      cli:
        protocol: telnet 
        ip: 192.168.0.1
        arguments:
          init_config_commands: []
  RT01:
    os: ios
    credentials:
      default:
        username: admin
        password: hoge
      enable:
        password: hoge
    connections:
      cli:
        protocol: ssh
        ip: 192.168.0.2
        arguments:
          init_config_commands: []
  WLC01:
    os: iosxe
    platform: c9800
    credentials:
      default:
        username: admin
        password: moge
      enable:
        password: moge
    connections:
      cli:
        protocol: ssh
        ip: 192.168.0.3
        arguments:
          init_config_commands: []
```

# 免責事項
本サイトおよび対応するコメントにおいて表明される意見は、投稿者本人の個人的意見であり、所属する組織の意見ではありません。本サイトの内容は、情報の提供のみを目的として掲載されており、投稿者が所属する組織や他の関係者による推奨や表明を目的としたものではありません。各利用者は、本Webサイトへの掲載により、投稿、リンクその他の方法でアップロードした全ての情報の内容に対して全責任を負い、本Web サイトの利用に関するあらゆる責任から投稿者の所属する組織を免責することに同意したものとします。