サカトラ

自動化したり統計したり

AnsibleのNetwork Resource Moduleが冪等性の確認として利用しているコマンドを探る

この記事は エーピーコミュニケーションズ Advent Calendar 2023 の11日目の投稿です。

Network Resoruce Moduleとは

公式はこちら ⇒  Network Resource Modules — Ansible Documentation

端的に言うとネットワーク系のコレクションにおいて~_command~_configではない、~_interface~_ospfなどのより細かい単位での設定を行うモジュールのこと。

今回はその冪等性をいかに確認しているかについてcisco.ios.ios_interfacesモジュールをベースに探っていきます。

冪等性について

大体のconfigモジュール

Configの文字列のダイジェストを比較しています。

cisco.ios.ios_configモジュールより抜粋

        running_config = NetworkConfig(indent=1, contents=output[0], ignore_lines=diff_ignore_lines)
        startup_config = NetworkConfig(indent=1, contents=output[1], ignore_lines=diff_ignore_lines)
        if running_config.sha1 != startup_config.sha1:
            save_config(module, result)

Resource Module

Factを作ってそれを比較しています。

Fact

Ansibleはgather_facttrueにすると対象の機器情報を収集します。
これはネットワーク機器を対象とした場合はそのネットワークOSに合わせたFactsが収集されます。

例えば下記のような形

"GigabitEthernet1": {
        "bandwidth": 1000000, 
        "description": null, 
        "duplex": "Full", 
        "ipv4": {
            "address": "192.168.1.1", 
            "masklen": 24
        }, 
        "lineprotocol": "up ", 
        "macaddress": "", 
        "mediatype": "RJ45", 
        "mtu": 1500, 
        "operstatus": "up", 
        "type": "CSR vNIC"
    }, 

ConfigとPlaybookに記載された内容をこのFactの形に起こして、それらを対合することで冪等性を担保しています。

ではそのFactはどのように集めているのか、それは当然ですがResource Moduleごとに異なります。

cisco.ios.ios_interfacesの場合はcisco.iosplugins/module_utils/network/ios/facts/interfaces/interfaces.pyに答えがあります。

plugins/module_utils/network/ios/facts/interfaces/interfaces.py から一部抜粋

class InterfacesFacts(object):
    """The ios interfaces facts class"""

    def __init__(self, module):
        self._module = module
        self.argument_spec = InterfacesArgs.argument_spec

    def get_interfaces_data(self, connection):
        return connection.get("show running-config | section ^interface")

    def populate_facts(self, connection, ansible_facts, data=None):
        """Populate the facts for Interfaces network resource

        :param connection: the device connection
        :param ansible_facts: Facts dictionary
        :param data: previously collected conf

        :rtype: dictionary
        :returns: facts
        """
        if not data:
            data = self.get_interfaces_data(connection)

ここではget_interfaces_dataにてshow running-config | section ^interfaceをコマンド実行していることがわかります。

まとめ

ということで、cisco.ios.ios_interfacesでは冪等性の確認としてshow running-config | section ^interfaceのコマンドを使用していることがわかりました。

その他のResource Moduleもだいたいはmodule_utils/network/<network_os>/facts/<module_name>/<module_name>.pyのファイルに答えがあるので気になった方は是非ここを探るとよいでしょう。

Ansible Action Pluginの探索

備忘録

AnsibleのAction Pluginは実行時にどのように探索されているのか

Action Pluginについてどのようにコールされているのかがわからず開発で見落としたため、下記を残す。

TaskExecutorのクラス内でメソッドを下記の順で呼んでいる。 run_run_loop_execute_get_action_handler_get_action_handler_with_module_context

そして_get_action_handler_with_module_contextの内容は下記

def _get_action_handler_with_module_context(self, templar):
        '''
        Returns the correct action plugin to handle the requestion task action and the module context
        '''
        module_collection, separator, module_name = self._task.action.rpartition(".")
        module_prefix = module_name.split('_')[0]
        if module_collection:
            # For network modules, which look for one action plugin per platform, look for the
            # action plugin in the same collection as the module by prefixing the action plugin
            # with the same collection.
            network_action = "{0}.{1}".format(module_collection, module_prefix)
        else:
            network_action = module_prefix

        collections = self._task.collections

        # Check if the module has specified an action handler
        module = self._shared_loader_obj.module_loader.find_plugin_with_context(
            self._task.action, collection_list=collections
        )
        if not module.resolved or not module.action_plugin:
            module = None
        if module is not None:
            handler_name = module.action_plugin
        # let action plugin override module, fallback to 'normal' action plugin otherwise
        elif self._shared_loader_obj.action_loader.has_plugin(self._task.action, collection_list=collections):
            handler_name = self._task.action
        elif all((module_prefix in C.NETWORK_GROUP_MODULES, self._shared_loader_obj.action_loader.has_plugin(network_action, collection_list=collections))):
            handler_name = network_action
            display.vvvv("Using network group action {handler} for {action}".format(handler=handler_name,
                                                                                    action=self._task.action),
                         host=self._play_context.remote_addr)
        else:
            # use ansible.legacy.normal to allow (historic) local action_plugins/ override without collections search
            handler_name = 'ansible.legacy.normal'
            collections = None  # until then, we don't want the task's collection list to be consulted; use the builtin

        # networking/psersistent connections handling
        if any(((self._connection.supports_persistence and C.USE_PERSISTENT_CONNECTIONS), self._connection.force_persistence)):

            # check handler in case we dont need to do all the work to setup persistent connection
            handler_class = self._shared_loader_obj.action_loader.get(handler_name, class_only=True)
            if getattr(handler_class, '_requires_connection', True):
                # for persistent connections, initialize socket path and start connection manager
                self._play_context.timeout = self._connection.get_option('persistent_command_timeout')
                display.vvvv('attempting to start connection', host=self._play_context.remote_addr)
                display.vvvv('using connection plugin %s' % self._connection.transport, host=self._play_context.remote_addr)

                options = self._connection.get_options()
                socket_path = start_connection(self._play_context, options, self._task._uuid)
                display.vvvv('local domain socket path is %s' % socket_path, host=self._play_context.remote_addr)
                setattr(self._connection, '_socket_path', socket_path)
            else:
                # TODO: set self._connection to dummy/noop connection, using local for now
                self._connection = self._get_connection({}, templar, 'local')

        handler = self._shared_loader_obj.action_loader.get(
            handler_name,
            task=self._task,
            connection=self._connection,
            play_context=self._play_context,
            loader=self._loader,
            templar=templar,
            shared_loader_obj=self._shared_loader_obj,
            collection_list=collections
        )

        if not handler:
            raise AnsibleError("the handler '%s' was not found" % handler_name)

        return handler, module

コードからわかること FQCNで指定があれば

network_action = "{0}.{1}".format(module_collection, module_prefix)

でモジュールを呼ぶAction Pluginへのパスが通ることがわかる。そうでない場合はすべてのモジュールを探索することになる(これはFQCN指定をするべきであるというインセンティブにつながるかもしれない) なので開発する際は<collection_name>.<os_name>Action Pluginが最低限必要であることがわかる。

Pulumiに入門する

最近目にしたこの記事の最後にPulumiなるものが出てきていて、恥ずかしながら私はまったくこの存在を知りませんでした。
いい機会なのでPulumiについて公式の手順を参考に入門していきます。

zenn.dev

What is Pulumi?

Pulumi - Infrastructure as Code in Any Programming Language

Pulumiとは何か、
トップページ曰く

Infrastructure as code in any programming language Build infrastructure intuitively on any cloud using familiar languages.

これがPulumiを使うとできるみたいです。

Get Start

環境はWSL(Ubuntu22.04)

Pulumiをインストール

curl -fsSL https://get.pulumi.com | sh

インストールできたらpulumi versionで確認

$ pulumi version
v3.78.1

今回はAWS上にリソースを構築する予定なので、環境変数としてAWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYを設定しておきます。 ここはaws cliをインストールしている場合はその設定を利用することが可能なようです。

プロジェクトを作成する

Pulumiがインストールできたら次にプロジェクトを作成します。

冒頭にあった通り、Pulumiでは様々な言語で構成を記述することができます。
現状、TypeScript, JavaScript, Python, Go, C#,Java, YAMLから選択できます。

個人的な好みでYAMLで構成を採用します。

pulumi newのコマンドでひな形を作ることができます。
この時にどのプロバイダーを利用するのかとどの言語で書くかを指定します。

プロジェクトを置くディレクトリを作ってそこにプロジェクトを作成していきます。

mkdir pulumi_aws && cd pulumi_aws
pulumi new aws-yaml

このpulumi newのコマンドは実行するとPulumi Cloudへのログインを求められます。
Pulumi Cloudについてパッと見の感想になりますが、その立ち位置はTerraform Cloudに近いなと思っています。 Pulumi Cloudは個人利用がタダなので、サクッとアカウントも作ってログインしましたが、Pulumi Cloudを利用したくない場合は事前にpulumi login --localを実行してローカル環境をおくと良いそうです。

Pulumi CLI & Pulumi Cloud FAQ | Pulumi Docs

その他、プロジェクト名やプロジェクトのDescription、利用するAWSのロケーション(例: ap-northeast-1)などをインタラクティブに設定します。
Pulumi Cloudを利用するかどうか、利用するプロバイダーや言語によっても変わるようですが大体下記のことを聞かれます。

project name: (pulumi_aws) 
project description: (A minimal AWS Pulumi YAML program) 
stack name: (dev) 
aws:region: The AWS region to deploy into: (us-east-1) 

これで下記のようにファイルが作られました。

.
├── Pulumi.dev.yaml
└── Pulumi.yaml

stack名をdevで作ってしまったのでややこしいですが、Pulumi.dev.yamldevの部分はstack名によって変わるようです。

Pulumi.yamlがリソースなどを記述するメインのファイルで、Pulumi.dev.yamlは構成の設定が入ってくるようです。

pulumi new aws-yamlした直後が下記。

Pulumi.yaml

name: aws-test
runtime: yaml
description: A minimal AWS Pulumi YAML program
outputs:
  # Export the name of the bucket
  bucketName: ${my-bucket.id}
resources:
  # Create an AWS resource (S3 Bucket)
  my-bucket:
    type: aws:s3:Bucket

Pulumi.dev.yaml

config:
  aws:region: ap-northeast-1

なるほど、構成の設定とはクラウドのロケーションなどの情報を指すみたいです。

AWSのConfiguration Optionはこちらから

AWS Classic Installation & Configuration | Pulumi Registry

pulumi new直後の状態でもS3についてのリソースは記述されているのでこの状態でpulumi upすることでAWSにS3バケットを作ることができます。

このあたりのコマンドはTerraformほとんど同じ感覚で使えます。
若干文言が違うので対応表にしました。

Terraform Pulumi
plan preview
apply up
destroy destory

ただこれだとPulumi.yamlがS3バケットのみなのでもうちょっとリソース色々組んでみたいと思います。

Pulumiを書く(YAML

Pulumi.yaml

これはどの言語で書く際にも実は生成されています。 トップレベルのキーruntimeで使用言語を決めてプロジェクトの名前や説明も使用言語にかかわらずこのファイルに載ってきます。

Pulumiの公式examplesより

examples/aws-apigateway-py-routes at master · pulumi/examples · GitHub

name: aws-apigateway-py-routes
runtime:
  name: python
  options:
    virtualenv: venv
description: Demonstration of API Gateway routes
template:
    config:
      aws:region:
        description: The AWS region to deploy into
        default: us-east-2

runtime: yamlの場合はこのPulumi.yamlにさらにresourcesoutputsを書いていきます。

このresourcesoutputsの書き方は感覚的にTerraformと近かったです。   ただ、これはなんとなく近いわけではなくPulumiのパッケージはTerraformのものをベースに作られているものが少なからずあるため意図的なもののようです。

PulumiのexampleがあるのでここからYAMLベースのやつを参考にして簡単なものを自分でも書いてみます。

github.com

まずは簡単にVPC, Subnet, SecurityGroup, EC2を作るやつを書いてみました。

name: aws-test
runtime: yaml
variables:
  myIp: myGrobalIp
  ubuntu:
    fn::invoke:
      function: aws:ec2:getAmi
      arguments:
        mostRecent: true
        filters:
          - name: name
            values:
              - ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*
          - name: virtualization-type
            values:
              - hvm
        owners:
          - '000000000000'
  keyPair: myKeyPair
resources:
  myVpc:
    type: aws:ec2:Vpc
    properties:
      cidrBlock: 10.0.0.0/16
      enableDnsHostnames: true
      enableDnsSupport: true
      tags:
        Name: tkd-pulumi-vpc
  
  mySubnet:
    type: aws:ec2:Subnet
    properties:
      vpcId: ${myVpc.id}
      cidrBlock: 10.0.1.0/24
      tags:
        Name: tkd-pulumi-subnet

  mySecurityGroup:
    type: aws:ec2:SecurityGroup
    properties:
      vpcId: ${myVpc.id}
      ingress:
        - description: for HTTP
          fromPort: 80
          toPort: 80
          protocol: tcp
          cidrBlocks:
            - 0.0.0.0/0
        - description: for SSH
          fromPort: 22
          toPort: 22
          protocol: tcp
          cidrBlocks:
            - ${myIp}
      egress:
        - protocol: "-1"
          fromPort: 0
          toPort: 0
          cidrBlocks:
            - 0.0.0.0/0

  myInternetGateway:
    type: aws:ec2:InternetGateway
    properties:
      vpcId: ${myVpc.id}
      tags:
        Name: tkd-pulumi-igw

  myRouteTable:
    type: aws:ec2:RouteTable
    properties:
      vpcId: ${myVpc.id}
      routes:
        - cidrBlock: 0.0.0.0/0
          gatewayId: ${myInternetGateway.id}
      tags:
        Name: tkd-pulumi-route-table

  myRouteAssociation:
    type: aws:ec2:RouteTableAssociation
    properties:
      routeTableId: ${myRouteTable.id}
      subnetId: ${mySubnet.id}

  myEc2:
    type: aws:ec2:Instance
    properties:
      subnetId: ${mySubnet.id}
      ami: ${ubuntu.id}
      instanceType: t2.micro
      vpcSecurityGroupIds:
        - ${mySecurityGroup.id}
      associatePublicIpAddress: true
      keyName: ${keyPair}
      tags:
        Name: tkd-pulumi-Ubuntu

outputs:
  instanceIp: ${myEc2.publicIp}

Pulumiを実行

pulumi upコマンドで実行できます。

実行するとTerraformと同じでリソースの見積をしてから確認を経てプロビジョニングします。

View in Browser (Ctrl+O): https://app.pulumi.com/<username>/aws-test/dev/previews/a0333beb-4e14-4691-b569-ad4c3d3c52ac

     Type                              Name                Plan       
 +   pulumi:pulumi:Stack               aws-test-dev        create     
 +   ├─ aws:ec2:Vpc                    myVpc               create     
 +   ├─ aws:ec2:InternetGateway        myInternetGateway   create     
 +   ├─ aws:ec2:SecurityGroup          mySecurityGroup     create     
 +   ├─ aws:ec2:Subnet                 mySubnet            create     
 +   ├─ aws:ec2:RouteTable             myRouteTable        create     
 +   ├─ aws:ec2:RouteTableAssociation  myRouteAssociation  create     
 +   └─ aws:ec2:Instance               myEc2               create     


Outputs:
    instanceIp: output<string>

Resources:
    + 8 to create

Do you want to perform this update?

今回はPulumi Cloudを利用しているので、ブラウザからも同じ内容を見ることができます。

そしてプロビジョニング

Updating (dev)

View in Browser (Ctrl+O): https://app.pulumi.com/<username>/aws-test/dev/updates/8

     Type                              Name                Status              
 +   pulumi:pulumi:Stack               aws-test-dev        created (48s)       
 +   ├─ aws:ec2:Vpc                    myVpc               created (11s)       
 +   ├─ aws:ec2:Subnet                 mySubnet            created (1s)        
 +   ├─ aws:ec2:InternetGateway        myInternetGateway   created (0.93s)     
 +   ├─ aws:ec2:SecurityGroup          mySecurityGroup     created (3s)        
 +   ├─ aws:ec2:RouteTable             myRouteTable        created (1s)        
 +   ├─ aws:ec2:RouteTableAssociation  myRouteAssociation  created (0.67s)     
 +   └─ aws:ec2:Instance               myEc2               created (32s)       


Outputs:
    instanceIp: "---.---.---.---"

Resources:
    + 8 created

Duration: 49s

これもPulumi Cloudから見るとこんな感じ

これでいい感じにプロビジョニングができていました。

使用感

Terraformと近いけど、それが自分の使いたい言語でできるのでそこは中々良いと感じました。 ただYAMLだと関数などが使いづらいので使用感はあんまりよくない(現時点では)。

Pulumi Cloudはなんとなく触った感じ、Terraform Cloudと近いながらもプロジェクトの追加に関してはTerraform Cloudよりも触りやすい印象。

ちなみ金額がPulumi Cloudの方が安く見えるので、そういう意味ではPulumiは選択肢になるかもなと思いました。 今後PulumiとTerraformはどうなっていくのか注視していきたいところです。

Pulumi Pricing

Terraform Cloud Pricing

Terraform ↔ Ansible

TerraformとAnsible

TerraformとAnsible、どちらもIaCツールでありインフラ構築の自動化を行うことができます。

TerraformとAnsible、この両者はどちらも一方から他方を呼ぶことができます。
ということでTerraform → AnsibleとAnsible → Terraformで両方のパターンの橋渡しの部分についてを書いていきます。

Terraform → Ansible

Terraformが主体となりAnsible Playbookを実行するやり方で大きくは2つのパターンがあるのでそれぞれ紹介します。

  • outputinventory.iniファイルを作成してlocal-execで実行
  • ansible/ansibleproviderを利用する方法

outputinventory.iniファイルを作成してlocal-execで実行

Terraformのoutputを利用し、Ansibleで利用するためのinventory.iniを作成し、local-execansible-playbookコマンドを実行する方法です。

Ansibleで使用するinventoryファイルはtemplatefileメソッドで作成します。
Jinja2っぽいテンプレートファイル(微妙にJinja2とは違う)を利用してあとは利用している変数のマッピングtemplatefileメソッドの引数で指定すればOKです。

output.tf

resource "local_file" "inventory" {
  depends_on = [aws_instance.myServer]
  content = templatefile("./inventory.tftpl",
    {
      server = aws_instance.myServer
    }
  )
  filename = "./inventory.ini"
}

inventory.tftpl

[server]
${server.tags.Name} ansible_host=${server.public_ip}

[server:vars]
ansible_user="ec2-user"

[all:vars]
ansible_ssh_private_key_file="./private_key.pem"

これでinventory.iniのファイルが生成されるのでそれを利用してansible-playbookコマンドを実行します。

Ansibleを実行するための.tfファイルは下記になります。

ansible.tf

resource "null_resource" "provisioning" {
  depends_on = [local_file.inventory]
  provisioner "remote-exec" {
    connection {
      host        = aws_instance.myServer.public_ip
      user        = "ec2-user"
      private_key = file("./ssh_key.pem")
    }
    inline = [ "echo 'ready to do ansible!'" ]
  }
  provisioner "local-exec" {
    command = "ansible-playbook -i inventory.ini setup_server.yaml"
  }

}

この時、対象のEC2が起動してくるのを待つためにremote-execでのコマンド実行を挟んでおくのがポイントです。
これがないと起動しきっていないインスタンスにPlaybookを実行してしまいます。

ansible/ansibleproviderを利用する

ansible/ansibleプロバイダーが公式より提供されています。 https://registry.terraform.io/providers/ansible/ansible/latest

このansible/ansibleプロバイダーではansible_playbookresourceansible-playbookコマンドを実行することができます。

また、ansible_hostリソースを使用すると、Ansibleのcloud.terraform.terraform_pluginで参照するためのインベントリ情報を作成することもできます。
これは後述のAnsible → Terraformにて使用します。

話を戻してTerraform → Ansibleについて。
このansible_playbookresourceですが、少々癖があり、リソースの設定ではinventoryファイルの指定ができない他、プロバイダーに同梱のansible_hostを全く参照してくれません。
そのため、リソースのnameか、extra_varsの部分でansible_hostansible_userを渡してあげる必要があります。

GitHubのIssueでは外部変数でinventory_fileを指定すると良いというような記述がありますが、inventory_fileはマジック変数でAnsibleの公式ドキュメントではマジック変数はユーザー側から指定できないとあるので、この解決方法はちょっと眉唾です。(ちなみに私はこれでは解決しませんでした。Issueにもそれでうまくいかなかった人のコメントがあります。)

ansible.tf

resource "ansible_playbook" "setup_server" {
  playbook   = "./setup_server.yaml"
  name       = "myServer"
  replayable              = false
  ignore_playbook_failure = true # ここをtrueにしないとproviderが呼べませんでした
  extra_vars = {
    ansible_host                 = aws_instance.myServer.public_ip
    ansible_user                 = "ec2-user"
    ansible_ssh_private_key_file = "./ssh_key.pem"
  }
}

output "playbook_stdout" {
  value = ansible_playbook.setup_server.ansible_playbook_stdout
}

outputansible_playbook_stdoutを指定してあげるとAnsibleの実行結果をoutputとして返してくれます。

ただ、ここでもEC2インスタンスが作成される前にAnsible Playbookが実行されてしまう問題があったので、最終的に下記の形になりました。

ansible.tf

resource "null_resource" "wait_instance" {
  depends_on = [aws_instance.myServer]
  provisioner "remote-exec" {
    connection {
      host        = aws_instance.myServer.public_ip
      user        = "ec2-user"
      private_key = file("./ssh_key.pem")
    }
    inline = ["echo 'ready to do ansible!'"]
  }
}

resource "ansible_playbook" "setup_server" {
  depends_on              = [null_resource.wait_instance]
  playbook                = "./setup_server.yaml"
  name                    = "myServer"
  replayable              = false
  ignore_playbook_failure = true
  extra_vars = {
    ansible_host                 = aws_instance.myServer.public_ip
    ansible_user                 = "ec2-user"
    ansible_ssh_private_key_file = "./ssh_key.pem"
  }
}
output "playbook_stdout" {
  value = ansible_playbook.setup_server.ansible_playbook_stdout
}
output "playbook_stderr" {
  value = ansible_playbook.setup_server.ansible_playbook_stderr
}

null_resourcewait_instanceで対象のインスタンスremote-execをし、それをansible_playbookからdepends_onで依存関係に設定することでインスタンスの立ち上がりを完全に待つことができました。

Ansible → Terraform

AnsibleからTerraformを呼ぶ方法は大きく下記の2つがあります。

  • ansible.builtin.shellモジュールでコマンドから実行する方法
  • cloud.terraformコレクションを利用する方法

ansible.builtin.shellモジュールでコマンドから実行

terraform initterraform applyansible.builtin.shellモジュールで実行します。

インベントリについてはプラットフォームのinventory pluginを用いてみます。
後述のcloud.terraform.terraform_pluginをつかっても良いのですが、差別化のためにここはあえて。

---
plugin: amazon.aws.aws_ec2
regions:
  - ap-northeast-1
filters:
  instance-state-name: running
keyed_groups:
  - key: tags.Name
hostnames:
  - ip-address

inventory pluginを使う場合はansible.cfgで有効化が必要です。

ansible.cfg

[inventory]
enable_pluginsf = amazon.aws.aws_ec2

これでPlaybook側で呼び出せばOKです。

---
# AnsibleからTerraformを呼ぶ
- name: Provisioning
  hosts: localhost
  gather_facts: false
  tasks:
    - name: Call Terraform
      ansible.builtin.shell:
        cmd: terraform init && terraform apply -auto-approve

# Terraformで立ち上げたEC2に設定を行う
- name: Setting
  hosts: myServer
  gather_facts: true
  become: true
  tasks:
    - name: Install package
      ansible.builtin.yum:
        name:
          - httpd
...

cloud.terraformコレクションを利用する

cloud.terraformコレクションのcloud.terraform.terraformモジュールでTerraformのプロジェクトを呼び出します。
Ansibleによる設定投入を実行する際に必要となるインベントリについてはansible/ansibleprovidercloud.terraform.terraform_pluginを今回は使います。

inventory pluginを使う場合はTerraformとAnsibleのそれぞれでファイルに手を入れる必要があります。

Terraform側では、ansible_groupまたはansible_hostresourceを作成します。

inventory.tf

resource "ansible_host" "myServer" {
  depends_on = [aws_instance.myServer]
  name       = aws_instance.myServer.tags.Name
  groups     = ["server"]
  variables = {
    ansible_host                 = aws_instance.myServer.public_ip
    ansible_user                 = "ec2-user"
    ansible_ssh_private_key_file = "ssh_key.pem"
  }
}

Ansible側ではcloud.terraform.terraform_pluginを利用するinventoryファイルを作成します。

inventory.yaml

---
plugin: cloud.terraform.terraform_provider

ansible.cfgファイルにcloud.terraform.terraform_providerを有効化する設定を記述します。

[inventory]
enable_plugin = ini, cloud.terraform.terraform_provider

これでTerraformで作成したインスタンスをAnsibleのインベントリにつなげることができました。

call_terraform.yaml

---
# AnsibleからTerraformを呼ぶ
- name: Provisioning
  hosts: localhost
  gather_facts: false
  tasks:
    - name: Call Terraform
      cloud.terraform.terraform:
        project_path: terraform/
        state: present
        force_init: true

# Terraformで立ち上げたEC2に設定を行う
- name: Setting
  hosts: myServer
  gather_facts: true
  become: true
  tasks:
    - name: Install package
      ansible.builtin.yum:
        name:
          - httpd
...

このcloud.terraform.terraformのドキュメントはこちら
community.general.terraformでもほとんど同じことができます。

ほとんど差がないですが、inventory pluginがある分cloud.terraformの方がお得な気がします。

参考

GitHub - ansible/terraform-provider-ansible: community terraform provider for ansible Inventory plugins — Ansible Documentation templatefile - Functions - Configuration Language | Terraform | HashiCorp Developer Ansible vs. Terraform, clarified Providing Terraform with that Ansible Magic

...

Ansible → Terraformの方が順序性がわかりやすくて個人的に好き。 ansible/ansibleansible_playbookのリソースが使い勝手悪いせいもあるので、今後アップデートがかかることに期待。

【Ansible】NEC IX向けのコレクションをAnsible Galaxyにアップしました。

Ansible Galaxyへコレクションをアップする。

以前作成した、Ansibleコレクションをこの度Ansible Galaxyにアップしました。 URLはこちら

https://galaxy.ansible.com/rucdev/ix

気になる方は是非お試しください。

現在のコレクションが抱える課題

ということでAnsible Galaxyにコレクションをアップしてみたのですが、このコレクション結構課題山積みなのです。 その課題を今わかっているものだけでもアップしておきます。 使ってみたい人向けの諸注意といいつつ、自分のための備忘録です。

課題

  • NEC IX 2105のみでしか動作検証をしていない
    作成者の財力のNASA🚀
  • IXのバージョンは10.2.39のみでしか動作検証をしていない
    作成者がフリーメールのアドレスしか持っていないためNetMeisterに登録してOSのバージョンを上げるが出来ませんでした。
  • 他のユーザーがグローバルコンフィグモードに入っていると接続に失敗する
    これはグローバルコンフィグモードへの移行をconfigureで行っているためです。
    この部分についてはsvintr-configに切り替えるか、それらを選べるようにするのかを決めかねて結局何もしていない状況です。
  • ドキュメントが未整備
    ひとえに作成者の怠惰です。
    英語難しい...
  • まだモジュールがix_commandix_configしかない
    今後色々作る予定です...

とまあ自分で思いつくだけでこのくらいあるので、もし使う方がいましたらこのあたりを把握していただけますと幸いです。

Juniper vLabsでAnsibleを使う BGP - Multi - AS編

この記事は 【アットホームな現場です】🎄★☆ネットワーク系エンジニア★☆アレコレアウトプット★☆🎄のカレンダー | Advent Calendar 2022 - Qiita20日目の記事になります。

今回はJuniper vLabsのBGP - Multi - ASの構成でAnsibleを使った設定投入にチャレンジしていきます。

Juniper vLabsを立ち上げる

Juniper vLabsはJuniper Networks社が提供する、無料の(登録必要)のラボ環境です。(Ciscoの提供するDevNetSandboxとほぼ同じ感じ) 1回3時間(最大6時間)の制約付きで用意されている様々な構成のJuniper製品をお試しできます。 (お得!)

色々な構成

そしてこのJuniper vLabsでは特定のIPからvLabs内の機器に通信を許可することが出来ます。
ということは、自分の手元のAnsibleから設定を流すこともできるということ!

立ち上げ方

まずは今回利用する BGP - Multi - ASの環境を起動します。

  1. Juniper vLabsにアクセス
  2. BGP - Multi - ASのLaunchをクリック
  3. 構成の画面からRESERVEボタンをクリック
  4. あとは環境が立ち上がるのを待ちます。
    ここは構成によっては10分~15分かかるものもあるので気長待ちましょう。
  5. 起動が完了した通知メールがアカウントに紐づいているアドレスに来ればOKです。
  6. Sandboxのページ(RESERVEをした時のページ、閉じた場合はメールのリンクから再度開けます。)でApache Guacamoleを使用したSSHでの接続ができます。
  7. Apache Guacamoleを使用してブラウザ上でSSH接続ができていれば準備完了です。

vLabsの構成

BGP - Multi - ASの構成は下記のようになっています。

BGP - Multi - ASの構成

vMXルータ6台で構成されており、それぞれのAS間はeBGPのネイバー関係が確立されており、AS内はIGPとしてOSPFが設定されているほか、iBGPが張られています。
また、それぞれloopbackインターフェース(10.100.100.1 ~ 6)を持っており、これらのアドレスがルーティングテーブルに乗ってくるかどうかでBGPの動作確認ができそうです。

各vMXルータはBGPのネイバー関係こそ確立しているもののアドバタイズの設定が全く入っていないので、構築初期の段階では他ASのルータが持つloopbackインターフェースは見えません。 起動時のvMX2のルーティングテーブルはこんな感じ

jcluser@vMX2> show route 

inet.0: 16 destinations, 16 routes (16 active, 0 holddown, 0 hidden)
+ = Active Route, - = Last Active, * = Both

0.0.0.0/0          *[Static/5] 00:46:44
                    >  to 100.123.0.1 via fxp0.0
10.100.12.0/24     *[Direct/0] 00:41:52
                    >  via ge-0/0/3.0
10.100.12.2/32     *[Local/0] 00:41:52
                       Local via ge-0/0/3.0
10.100.13.0/24     *[OSPF/10] 00:40:51, metric 2
                    >  to 10.100.23.2 via ge-0/0/2.0
                       to 10.100.12.1 via ge-0/0/3.0
10.100.23.0/24     *[Direct/0] 00:41:52
                    >  via ge-0/0/2.0
10.100.23.1/32     *[Local/0] 00:41:52
                       Local via ge-0/0/2.0
10.100.24.0/24     *[Direct/0] 00:41:52
                    >  via ge-0/0/0.0
10.100.24.1/32     *[Local/0] 00:41:52
                       Local via ge-0/0/0.0
10.100.25.0/24     *[Direct/0] 00:41:52
                    >  via ge-0/0/1.0
10.100.25.1/32     *[Local/0] 00:41:52
                       Local via ge-0/0/1.0
10.100.100.1/32    *[OSPF/10] 00:40:51, metric 1
                    >  to 10.100.12.1 via ge-0/0/3.0
10.100.100.2/32    *[Direct/0] 00:41:52
                    >  via lo0.0
10.100.100.3/32    *[OSPF/10] 00:40:56, metric 1
                    >  to 10.100.23.2 via ge-0/0/2.0
100.123.0.0/16     *[Direct/0] 00:46:44
                    >  via fxp0.0
100.123.1.1/32     *[Local/0] 00:46:44
                       Local via fxp0.0
224.0.0.5/32       *[OSPF/10] 00:41:52, metric 1
                       MultiRecv

inet6.0: 1 destinations, 1 routes (1 active, 0 holddown, 0 hidden)
+ = Active Route, - = Last Active, * = Both

ff02::2/128        *[INET6/0] 00:47:43
                       MultiRecv

ということで、BGP経由でルートを受け取っていないことがわかります。これでは他のASにあるvMXのloopbackインターフェースへたどり着けません。

なのでBGPで自身のloopbackインターフェースのアドレスをアドバタイズするようにしたいと思います。

BGPで経路をアドバタイズする

Juniper製品でBGPの経路広報を行うには下記に習えばできそうです。

JUNOS - BGPの設定(EBGPネイバーの設定)

なので今回は下記のコンフィグで行けそうです。(vMX2の場合)

set policy-options prefix-list vMX2Loopback 10.100.100.2/32
set policy-options policy-statement ADV-Loopback term 1 from prefix-list vMX2Loopback
set policy-options policy-statement ADV-Loopback term 1 then accept
set protocols bgp group to-AS64533 export ADV-Loopback
set protocols bgp group to-AS64544 export ADV-Loopback

ちなみにvMX2の起動時のコンフィグは下記

vMX2のconfig

set version 21.1R3.11
set system host-name vMX2
set system root-authentication encrypted-password "$6$w0uV/Veg$MxUKS00aYKDRZKuI13guEQ3yhv0XjZ5vDD/xBSVatXwzxvgMZCjERUu5kEpMaRzFDhrcyf8NLW8lQiM.KpUCE1"
set system scripts language python
set system login user jcladmin uid 2000
set system login user jcladmin class super-user
set system login user jcladmin authentication encrypted-password "$6$COH4QgW/$uFzZAk1fYdnuwVl5WUjhb/4JdtSWIq7y/eCqB3qEFLFK/QBeG1C686NzW0XL0sz8qX4bzyYW0uMIBNXK47Kw7."
set system login user jcluser uid 2001
set system login user jcluser class super-user
set system login user jcluser authentication encrypted-password "$6$G44rGtvQ$I3jMwJk.0/CbTlhEoZzoDGv9dcFuZYdKvNFHiZwZ6s5Lktf/vMHipZxDwEXxgtid.dmN5K27fMBYwKnSijiQ/."
set system services ssh root-login allow
set system services netconf ssh
set system services rest http port 3000
set system services rest enable-explorer
set system syslog user * any emergency
set system syslog file messages any notice
set system syslog file messages authorization info
set system syslog file interactive-commands interactive-commands any
set system processes dhcp-service traceoptions file dhcp_logfile
set system processes dhcp-service traceoptions file size 10m
set system processes dhcp-service traceoptions level all
set system processes dhcp-service traceoptions flag all
set chassis fpc 0 pic 0 number-of-ports 8
set chassis fpc 0 lite-mode
set interfaces ge-0/0/0 unit 0 family inet address 10.100.24.1/24
set interfaces ge-0/0/1 unit 0 family inet address 10.100.25.1/24
set interfaces ge-0/0/2 unit 0 family inet address 10.100.23.1/24
set interfaces ge-0/0/3 unit 0 family inet address 10.100.12.2/24
set interfaces fxp0 unit 0 family inet address 100.123.1.1/16
set interfaces lo0 unit 0 family inet address 10.100.100.2/32
set protocols bgp group IBGP type internal
set protocols bgp group IBGP local-address 10.100.100.2
set protocols bgp group IBGP neighbor 10.100.100.1
set protocols bgp group IBGP neighbor 10.100.100.3
set protocols bgp group to-AS64533 type external
set protocols bgp group to-AS64533 peer-as 64533
set protocols bgp group to-AS64533 neighbor 10.100.24.2
set protocols bgp group to-AS64544 type external
set protocols bgp group to-AS64544 peer-as 64544
set protocols bgp group to-AS64544 neighbor 10.100.25.2
set protocols ospf area 0.0.0.0 interface ge-0/0/3.0
set protocols ospf area 0.0.0.0 interface ge-0/0/2.0
set protocols ospf area 0.0.0.0 interface lo0.0
set routing-options autonomous-system 64522
set routing-options static route 0.0.0.0/0 next-hop 100.123.0.1

試しに先ほどのconfigをvMX2(AS64522)に入れてみました。

するとAS64533のvMX4で10.100.100.2のアドレスがルーティングテーブルに載ってきました。 なのでconfigとしてはこれでよさそうです。

jcluser@vMX4> show route 

inet.0: 14 destinations, 16 routes (14 active, 0 holddown, 0 hidden)
+ = Active Route, - = Last Active, * = Both

0.0.0.0/0          *[Static/5] 01:14:43
                    >  to 100.123.0.1 via fxp0.0
10.100.24.0/24     *[Direct/0] 01:10:00
                    >  via ge-0/0/0.0
10.100.24.2/32     *[Local/0] 01:10:00
                       Local via ge-0/0/0.0
10.100.34.0/24     *[Direct/0] 01:10:00
                    >  via ge-0/0/1.0
10.100.34.2/32     *[Local/0] 01:10:00
                       Local via ge-0/0/1.0
10.100.45.0/24     *[Direct/0] 01:10:00
                    >  via ge-0/0/2.0
10.100.45.1/32     *[Local/0] 01:10:00
                       Local via ge-0/0/2.0
10.100.46.0/24     *[Direct/0] 01:10:00
                    >  via ge-0/0/3.0
10.100.46.1/32     *[Local/0] 01:10:00
                       Local via ge-0/0/3.0
10.100.100.2/32    *[BGP/170] 00:00:32, localpref 100
                      AS path: 64522 I, validation-state: unverified
                    >  to 10.100.24.1 via ge-0/0/0.0
                    [BGP/170] 00:00:32, localpref 100
                      AS path: 64544 64522 I, validation-state: unverified
                    >  to 10.100.45.2 via ge-0/0/2.0
                    [BGP/170] 00:00:31, localpref 100
                      AS path: 64544 64522 I, validation-state: unverified
                    >  to 10.100.46.2 via ge-0/0/3.0
10.100.100.4/32    *[Direct/0] 01:10:00
                    >  via lo0.0
100.123.0.0/16     *[Direct/0] 01:14:43
                    >  via fxp0.0
100.123.1.3/32     *[Local/0] 01:14:43
                       Local via fxp0.0
224.0.0.5/32       *[OSPF/10] 01:10:00, metric 1
                       MultiRecv

inet6.0: 1 destinations, 1 routes (1 active, 0 holddown, 0 hidden)
+ = Active Route, - = Last Active, * = Both

ff02::2/128        *[INET6/0] 01:15:42
                       MultiRecv

ですが、これを全部のvMXに行うのは少ししんどいですね。

なのでこれをAnsibleで自動化しようと思います。

自動化では下記項目を達成します。

  • 各vMXは自身のLoopbackアドレスを指すprefix-listを作る
  • 各vMXは上記で作成したprefix-listを使ってpolicy-statementを作成する
  • eBGPでそのpolicy-statementexportする

ということでまずは手元の環境からAnsibleへ接続できるようにします。

SandboxのページのCOMMANDS -> Add Allowed Network Prefix から自分の接続したい環境のグローバルIPを入れます。 自宅環境ならここから調べられます。

アクセス情報【使用中のIPアドレス確認】

IPの許可設定ができたらvLabs側のグローバルIPとポートの一覧がメールで送られてきます。

この情報を使ってinventoryファイルを作ります。 ansible_host, ansible_port, ansible_user, ansible_passwordはメールに書いてあるものを入れてください。

[junos]
vMX1 ansible_host=<ip_address> ansible_port=<port>
vMX2 ansible_host=<ip_address> ansible_port=<port>
vMX3 ansible_host=<ip_address> ansible_port=<port>
vMX4 ansible_host=<ip_address> ansible_port=<port>
vMX5 ansible_host=<ip_address> ansible_port=<port>
vMX6 ansible_host=<ip_address> ansible_port=<port>

[junos:vars]
ansible_network_os=junos
ansible_connection=netconf
ansible_user=<user_name>
ansible_password=<password>
ansible_become=yes

続いてPlaybookを書いていきましょう。
juniperneteworks.junosコレクションのモジュールを使っていきます。

具体的な手順は下記で作ります。

  1. Loopbackインターフェースの情報を取得
  2. BGPグループの情報を取得
  3. Loopbackインターフェースのアドレスを/32でかけるprefix-listを作る
  4. 作ったprefix-listを使ったpolicy-statementを作る
  5. 作ったpolicy-statementをeBGPでexportする

この手順でPlaybookを組みました。

---
- hosts: junos
  name: Advertise loopback
  gather_facts: false
  tasks:
    # Loopbackインターフェースの情報を取得
    - name: Get Loopback info
      junipernetworks.junos.junos_command:
        commands:
          - show interfaces lo0.0
        display: json
      register: lo_info

    # BGP グループの情報を取得
    - name: Get BGP info
      junipernetworks.junos.junos_command:
        commands:
          - show bgp group
        display: json
      register: bgp_info

    # prefix-listとpolicy-optionを作成
    - name: Make Policy Option
      junipernetworks.junos.junos_config:
        lines:
          - set policy-options prefix-list {{ inventory_hostname }}Loopback {{ loopback_address }}/32
          - set policy-options policy-statement ADV-Loopback term 1 from prefix-list {{ inventory_hostname }}Loopback
          - set policy-options policy-statement ADV-Loopback term 1 then accept
      vars: # 長いので行分割
        loopback: "{{ lo_info.stdout[0]['interface-information'][0]['logical-interface'][0]['address-family'][0] }}" 
        loopback_address: "{{ loopback['interface-address'][0]['ifa-local'][0]['data'] }}" # loopbackインターフェースのアドレスを引く

    # eBGPのグループでpolicy-statementをexport
    - name: Set Export
      junipernetworks.junos.junos_bgp_global:
        config:
          groups:
            - name: "{{ item.name[0].data }}"
              type: external
              export: ADV-Loopback
      vars:
        bgp_group: "{{ bgp_info.stdout[0]['bgp-group-information'][0]['bgp-group'] }}" # BGPグループのリストを取得
      loop: "{{ bgp_group }}" # BGPグループで繰り返す
      when: item.type[0].data == "External" # eBGPかどうかを判断

これでPlaybookを実行!

vlab-test$ ansible-playbook -i inventory/inventory.ini advertise_ebgp.yaml 

PLAY [junos] *****************************************************************************************************************************************

TASK [Get Loopback info] *****************************************************************************************************************************
ok: [vMX4]
ok: [vMX5]
ok: [vMX1]
ok: [vMX3]
ok: [vMX2]
ok: [vMX6]

TASK [Get BGP info] **********************************************************************************************************************************
ok: [vMX2]
ok: [vMX3]
ok: [vMX5]
ok: [vMX4]
ok: [vMX1]
ok: [vMX6]

TASK [Make Policy Option] ****************************************************************************************************************************
changed: [vMX4]
changed: [vMX5]
changed: [vMX1]
changed: [vMX3]
changed: [vMX2]
changed: [vMX6]

TASK [Set Export] ************************************************************************************************************************************
skipping: [vMX1] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'Internal'}], 'peer-as': [{'data': '64522'}], 'local-as': [{'data': '64522'}], 'name': [{'data': 'IBGP'}], 'group-index': [{'data': '0'}], 'group-flags': [{}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '2'}], 'established-count': [{'data': '2'}], 'peer-address': [{'data': '10.100.100.2+57396'}, {'data': '10.100.100.3+52090'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '0'}], 'received-prefix-count': [{'data': '0'}], 'accepted-prefix-count': [{'data': '0'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '0'}]}]}) 
skipping: [vMX1]
skipping: [vMX2] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'Internal'}], 'peer-as': [{'data': '64522'}], 'local-as': [{'data': '64522'}], 'name': [{'data': 'IBGP'}], 'group-index': [{'data': '0'}], 'group-flags': [{}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '2'}], 'established-count': [{'data': '2'}], 'peer-address': [{'data': '10.100.100.1+179'}, {'data': '10.100.100.3+179'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '0'}], 'received-prefix-count': [{'data': '0'}], 'accepted-prefix-count': [{'data': '0'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '0'}]}]}) 
skipping: [vMX3] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'Internal'}], 'peer-as': [{'data': '64522'}], 'local-as': [{'data': '64522'}], 'name': [{'data': 'IBGP'}], 'group-index': [{'data': '0'}], 'group-flags': [{}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '2'}], 'established-count': [{'data': '2'}], 'peer-address': [{'data': '10.100.100.1+179'}, {'data': '10.100.100.2+49178'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '0'}], 'received-prefix-count': [{'data': '0'}], 'accepted-prefix-count': [{'data': '0'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '0'}]}]}) 
skipping: [vMX5] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'Internal'}], 'peer-as': [{'data': '64544'}], 'local-as': [{'data': '64544'}], 'name': [{'data': 'IBGP'}], 'group-index': [{'data': '0'}], 'group-flags': [{}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '1'}], 'established-count': [{'data': '1'}], 'peer-address': [{'data': '10.100.100.6+63217'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '0'}], 'received-prefix-count': [{'data': '0'}], 'accepted-prefix-count': [{'data': '0'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '0'}]}]}) 
skipping: [vMX6] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'Internal'}], 'peer-as': [{'data': '64544'}], 'local-as': [{'data': '64544'}], 'name': [{'data': 'IBGP'}], 'group-index': [{'data': '0'}], 'group-flags': [{}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '1'}], 'established-count': [{'data': '1'}], 'peer-address': [{'data': '10.100.100.5+179'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '0'}], 'received-prefix-count': [{'data': '0'}], 'accepted-prefix-count': [{'data': '0'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '0'}]}]}) 
changed: [vMX2] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'External'}], 'local-as': [{'data': '64522'}], 'name': [{'data': 'to-AS64533'}], 'group-index': [{'data': '1'}], 'group-flags': [{}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '1'}], 'established-count': [{'data': '1'}], 'peer-address': [{'data': '10.100.24.2+179'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '0'}], 'received-prefix-count': [{'data': '0'}], 'accepted-prefix-count': [{'data': '0'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '0'}]}]})
changed: [vMX6] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'External'}], 'local-as': [{'data': '64544'}], 'name': [{'data': 'to-AS64533'}], 'group-index': [{'data': '1'}], 'group-flags': [{}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '1'}], 'established-count': [{'data': '1'}], 'peer-address': [{'data': '10.100.46.1+179'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '0'}], 'received-prefix-count': [{'data': '0'}], 'accepted-prefix-count': [{'data': '0'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '0'}]}]})
changed: [vMX5] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'External'}], 'local-as': [{'data': '64544'}], 'name': [{'data': 'to-AS64533'}], 'group-index': [{'data': '1'}], 'group-flags': [{}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '1'}], 'established-count': [{'data': '1'}], 'peer-address': [{'data': '10.100.45.1+59768'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '0'}], 'received-prefix-count': [{'data': '0'}], 'accepted-prefix-count': [{'data': '0'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '0'}]}]})
changed: [vMX4] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'External'}], 'local-as': [{'data': '64533'}], 'name': [{'data': 'to-AS64544'}], 'group-index': [{'data': '0'}], 'group-flags': [{}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '2'}], 'established-count': [{'data': '2'}], 'peer-address': [{'data': '10.100.46.2+56067'}, {'data': '10.100.45.2+179'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '0'}], 'received-prefix-count': [{'data': '0'}], 'accepted-prefix-count': [{'data': '0'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '0'}]}]})
changed: [vMX3] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'External'}], 'local-as': [{'data': '64522'}], 'name': [{'data': 'to-AS64533'}], 'group-index': [{'data': '1'}], 'group-flags': [{}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '1'}], 'established-count': [{'data': '1'}], 'peer-address': [{'data': '10.100.34.2+55602'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '0'}], 'received-prefix-count': [{'data': '0'}], 'accepted-prefix-count': [{'data': '0'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '0'}]}]})
changed: [vMX2] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'External'}], 'local-as': [{'data': '64522'}], 'name': [{'data': 'to-AS64544'}], 'group-index': [{'data': '2'}], 'group-flags': [{}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '1'}], 'established-count': [{'data': '1'}], 'peer-address': [{'data': '10.100.25.2+179'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '0'}], 'received-prefix-count': [{'data': '0'}], 'accepted-prefix-count': [{'data': '0'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '0'}]}]})
changed: [vMX4] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'External'}], 'local-as': [{'data': '64533'}], 'name': [{'data': 'to-AS64522'}], 'group-index': [{'data': '1'}], 'group-flags': [{}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '2'}], 'established-count': [{'data': '2'}], 'peer-address': [{'data': '10.100.34.1+179'}, {'data': '10.100.24.1+51261'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '0'}], 'received-prefix-count': [{'data': '0'}], 'accepted-prefix-count': [{'data': '0'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '0'}]}]})
changed: [vMX5] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'External'}], 'local-as': [{'data': '64544'}], 'name': [{'data': 'to-AS64522'}], 'group-index': [{'data': '2'}], 'group-flags': [{}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '2'}], 'established-count': [{'data': '2'}], 'peer-address': [{'data': '10.100.25.1+63829'}, {'data': '10.100.35.1+64405'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '0'}], 'received-prefix-count': [{'data': '0'}], 'accepted-prefix-count': [{'data': '0'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '0'}]}]})
changed: [vMX3] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'External'}], 'local-as': [{'data': '64522'}], 'name': [{'data': 'to-AS64544'}], 'group-index': [{'data': '2'}], 'group-flags': [{}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '1'}], 'established-count': [{'data': '1'}], 'peer-address': [{'data': '10.100.35.2+179'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '0'}], 'received-prefix-count': [{'data': '0'}], 'accepted-prefix-count': [{'data': '0'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '0'}]}]})

PLAY RECAP *******************************************************************************************************************************************
vMX1                       : ok=3    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0   
vMX2                       : ok=4    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
vMX3                       : ok=4    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
vMX4                       : ok=4    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
vMX5                       : ok=4    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
vMX6                       : ok=4    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0  

タスク内のloopで流れてくる情報が多いので見づらくなっていますが、いい感じにできたようです。

ということで確認してみましょう。

vMX2のルーティングテーブル(BGPのみ)

jcluser@vMX2> show route protocol bgp 

inet.0: 19 destinations, 24 routes (19 active, 0 holddown, 0 hidden)
+ = Active Route, - = Last Active, * = Both

10.100.100.4/32    *[BGP/170] 00:46:15, localpref 100
                      AS path: 64533 I, validation-state: unverified
                    >  to 10.100.24.2 via ge-0/0/0.0
                    [BGP/170] 00:46:14, localpref 100, from 10.100.100.3
                      AS path: 64533 I, validation-state: unverified
                    >  to 100.123.0.1 via fxp0.0
                    [BGP/170] 00:46:38, localpref 100
                      AS path: 64544 64533 I, validation-state: unverified
                    >  to 10.100.25.2 via ge-0/0/1.0
10.100.100.5/32    *[BGP/170] 00:46:16, localpref 100
                      AS path: 64544 I, validation-state: unverified
                    >  to 10.100.25.2 via ge-0/0/1.0
                    [BGP/170] 00:46:15, localpref 100, from 10.100.100.3
                      AS path: 64544 I, validation-state: unverified
                    >  to 100.123.0.1 via fxp0.0
                    [BGP/170] 00:46:38, localpref 100
                      AS path: 64533 64544 I, validation-state: unverified
                    >  to 10.100.24.2 via ge-0/0/0.0
10.100.100.6/32    *[BGP/170] 00:46:40, localpref 100
                      AS path: 64533 64544 I, validation-state: unverified
                    >  to 10.100.24.2 via ge-0/0/0.0
                    [BGP/170] 00:46:40, localpref 100, from 10.100.100.3
                      AS path: 64533 64544 I, validation-state: unverified
                    >  to 100.123.0.1 via fxp0.0

inet6.0: 1 destinations, 1 routes (1 active, 0 holddown, 0 hidden)

vMX2から見て他ASのvMXのLoopbackアドレス(10.100.100.4~6)が受け取れているので成功に見えます。  ではvMX2のLoopbackから各Loopbackへ実際に到達できるのかpingして確かめてみます。

10.100.100.4 → OK!

jcluser@vMX2> ping 10.100.100.4 source 10.100.100.2  
PING 10.100.100.4 (10.100.100.4): 56 data bytes
64 bytes from 10.100.100.4: icmp_seq=0 ttl=64 time=1.599 ms
64 bytes from 10.100.100.4: icmp_seq=1 ttl=64 time=1.308 ms
64 bytes from 10.100.100.4: icmp_seq=2 ttl=64 time=1.454 ms
64 bytes from 10.100.100.4: icmp_seq=3 ttl=64 time=1.499 ms
64 bytes from 10.100.100.4: icmp_seq=4 ttl=64 time=2.057 ms
^C
--- 10.100.100.4 ping statistics ---
5 packets transmitted, 5 packets received, 0% packet loss
round-trip min/avg/max/stddev = 1.308/1.583/2.057/0.255 ms

10.100.100.5 → OK!

jcluser@vMX2> ping 10.100.100.5 source 10.100.100.2    
PING 10.100.100.5 (10.100.100.5): 56 data bytes
64 bytes from 10.100.100.5: icmp_seq=0 ttl=64 time=1.575 ms
64 bytes from 10.100.100.5: icmp_seq=1 ttl=64 time=1.189 ms
64 bytes from 10.100.100.5: icmp_seq=2 ttl=64 time=1.388 ms
64 bytes from 10.100.100.5: icmp_seq=3 ttl=64 time=1.456 ms
64 bytes from 10.100.100.5: icmp_seq=4 ttl=64 time=1.502 ms
^C
--- 10.100.100.5 ping statistics ---
5 packets transmitted, 5 packets received, 0% packet loss
round-trip min/avg/max/stddev = 1.189/1.422/1.575/0.131 ms

10.100.100.6 → ダメそう...

jcluser@vMX2> ping 10.100.100.6 source 10.100.100.2   
PING 10.100.100.6 (10.100.100.6): 56 data bytes
^C
--- 10.100.100.6 ping statistics ---
15 packets transmitted, 0 packets received, 100% packet loss

ルーティングテーブル的には良さそうだったのでこれはvMX6に問題がありそうです。
vMX6のルーティングテーブルを見てみます。

jcluser@vMX6> show route protocol bgp 

inet.0: 13 destinations, 16 routes (13 active, 0 holddown, 0 hidden)
+ = Active Route, - = Last Active, * = Both

10.100.100.2/32    *[BGP/170] 01:06:56, localpref 100, from 10.100.100.5
                      AS path: 64522 I, validation-state: unverified
                    >  to 100.123.0.1 via fxp0.0
                    [BGP/170] 01:07:17, localpref 100
                      AS path: 64533 64522 I, validation-state: unverified
                    >  to 10.100.46.1 via ge-0/0/3.0
10.100.100.3/32    *[BGP/170] 01:06:52, localpref 100, from 10.100.100.5
                      AS path: 64522 I, validation-state: unverified
                    >  to 100.123.0.1 via fxp0.0
                    [BGP/170] 01:07:13, localpref 100
                      AS path: 64533 64522 I, validation-state: unverified
                    >  to 10.100.46.1 via ge-0/0/3.0
10.100.100.4/32    *[BGP/170] 01:07:14, localpref 100
                      AS path: 64533 I, validation-state: unverified
                    >  to 10.100.46.1 via ge-0/0/3.0
                    [BGP/170] 01:07:13, localpref 100, from 10.100.100.5
                      AS path: 64533 I, validation-state: unverified
                    >  to 100.123.0.1 via fxp0.0

inet6.0: 1 destinations, 1 routes (1 active, 0 holddown, 0 hidden)

10.100.100.2のルートは2つ受信しており、選ばれているのはAS path: 64522 Iとなっているものです。
もう一つはAS path: 64533 64522 Iとなっており、AS pathの長さでAS path: 64522 Iの方が選ばれていると考えられます。
これらはどの道筋から来たものかというと、下記の赤と青のラインで来ています。

そして選ばれているAS path: 64522 Iのルートは赤のラインになります。
ただこのルートはネクストホップは10.123.0.1になっています。

この10.123.0.1はvMX6の全プロトコルのルーティングテーブルを見るとデフォルトルートに設定されていることがわかります。

jcluser@vMX6> show route 

inet.0: 13 destinations, 16 routes (13 active, 0 holddown, 0 hidden)
+ = Active Route, - = Last Active, * = Both

0.0.0.0/0          *[Static/5] 02:22:02
                    >  to 100.123.0.1 via fxp0.0
10.100.46.0/24     *[Direct/0] 02:17:33
                    >  via ge-0/0/3.0
10.100.46.2/32     *[Local/0] 02:17:33
                       Local via ge-0/0/3.0
10.100.56.0/24     *[Direct/0] 02:17:33
                    >  via ge-0/0/4.0
10.100.56.2/32     *[Local/0] 02:17:33
                       Local via ge-0/0/4.0
10.100.100.2/32    *[BGP/170] 01:27:26, localpref 100, from 10.100.100.5
                      AS path: 64522 I, validation-state: unverified
                    >  to 100.123.0.1 via fxp0.0
                    [BGP/170] 01:27:47, localpref 100
                      AS path: 64533 64522 I, validation-state: unverified
                    >  to 10.100.46.1 via ge-0/0/3.0
10.100.100.3/32    *[BGP/170] 01:27:22, localpref 100, from 10.100.100.5
                      AS path: 64522 I, validation-state: unverified
                    >  to 100.123.0.1 via fxp0.0
                    [BGP/170] 01:27:43, localpref 100
                      AS path: 64533 64522 I, validation-state: unverified
                    >  to 10.100.46.1 via ge-0/0/3.0
10.100.100.4/32    *[BGP/170] 01:27:44, localpref 100
                      AS path: 64533 I, validation-state: unverified
                    >  to 10.100.46.1 via ge-0/0/3.0
                    [BGP/170] 01:27:43, localpref 100, from 10.100.100.5
                      AS path: 64533 I, validation-state: unverified
                    >  to 100.123.0.1 via fxp0.0
10.100.100.5/32    *[OSPF/10] 02:16:34, metric 1
                    >  to 10.100.56.1 via ge-0/0/4.0
10.100.100.6/32    *[Direct/0] 02:17:33
                    >  via lo0.0
100.123.0.0/16     *[Direct/0] 02:22:02
                    >  via fxp0.0       
100.123.1.5/32     *[Local/0] 02:22:02
                       Local via fxp0.0
224.0.0.5/32       *[OSPF/10] 02:17:33, metric 1
                       MultiRecv

inet6.0: 1 destinations, 1 routes (1 active, 0 holddown, 0 hidden)
+ = Active Route, - = Last Active, * = Both

ff02::2/128        *[INET6/0] 02:23:03
                       MultiRecv

ということはvMX6から10.100.100.2向けのルートのネクストホップが見えていないってことですね。

なぜそうなるのか?それはBGPのネクストホップの扱い方に原因がありました。

参考: BGPパスアトリビュート - NEXT_HOP

これを読むまでiBGPはネクストホップを書き換えないということを知りませんでした。
なるほどな、これが原因か!

これを解消するconfigは下記になります。

set policy-options policy-statement NEXT_HOP term 1 from protocol bgp
set policy-options policy-statement NEXT_HOP term 1 then next-hop self
set protocols bgp group IBGP export NEXT_HOP

ということでこれを流すタスクをPlaybookに追加します。

---
- hosts: junos
  name: Advertise loopback
  gather_facts: false
  tasks:
    - name: Get Loopback info
      junipernetworks.junos.junos_command:
        commands:
          - show interfaces lo0.0
        display: json
      register: lo_info

    - name: Get BGP info
      junipernetworks.junos.junos_command:
        commands:
          - show bgp group
        display: json
      register: bgp_info

    - name: Make Policy Option
      junipernetworks.junos.junos_config:
        lines:
          - set policy-options prefix-list {{ inventory_hostname }}Loopback {{ loopback_address }}/32
          - set policy-options policy-statement ADV-Loopback term 1 from prefix-list {{ inventory_hostname }}Loopback
          - set policy-options policy-statement ADV-Loopback term 1 then accept
      vars:
        loopback: "{{ lo_info.stdout[0]['interface-information'][0]['logical-interface'][0]['address-family'][0] }}"
        loopback_address: "{{ loopback['interface-address'][0]['ifa-local'][0]['data'] }}"

    - name: Set Export
      junipernetworks.junos.junos_bgp_global:
        config:
          groups:
            - name: "{{ item.name[0].data }}"
              type: external
              export: ADV-Loopback
      vars:
        bgp_group: "{{ bgp_info.stdout[0]['bgp-group-information'][0]['bgp-group'] }}"
      loop: "{{ bgp_group }}"
      when: item.type[0].data == "External"

    # このタスクを追加
    - name: Set Next hop attribute
      junipernetworks.junos.junos_config:
        lines:
          - set policy-options policy-statement NEXT_HOP term 1 from protocol bgp
          - set policy-options policy-statement NEXT_HOP term 1 then next-hop self
          - set protocols bgp group IBGP export NEXT_HOP

これで再度実行。
既に実行されたタスクについてはOKで帰ってきます。 今回追加したタスクのみchangedで無事終了。

vlabs-test$ ansible-playbook -i inventory/inventory.ini advertise_ebgp.yaml 

PLAY [Advertise loopback] *******************************************************************************************************************************************

TASK [Get Loopback info] ********************************************************************************************************************************************
ok: [vMX1]
ok: [vMX4]
ok: [vMX3]
ok: [vMX5]
ok: [vMX2]
ok: [vMX6]

TASK [Get BGP info] *************************************************************************************************************************************************
ok: [vMX5]
ok: [vMX3]
ok: [vMX4]
ok: [vMX2]
ok: [vMX1]
ok: [vMX6]

TASK [Make Policy Option] *******************************************************************************************************************************************
ok: [vMX4]
ok: [vMX2]
ok: [vMX5]
ok: [vMX3]
ok: [vMX1]
ok: [vMX6]

TASK [Set Export] ***************************************************************************************************************************************************
skipping: [vMX1] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'Internal'}], 'peer-as': [{'data': '64522'}], 'local-as': [{'data': '64522'}], 'name': [{'data': 'IBGP'}], 'group-index': [{'data': '0'}], 'group-flags': [{}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '2'}], 'established-count': [{'data': '2'}], 'peer-address': [{'data': '10.100.100.2+57396'}, {'data': '10.100.100.3+52090'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '3'}], 'received-prefix-count': [{'data': '6'}], 'accepted-prefix-count': [{'data': '6'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '0'}]}]}) 
skipping: [vMX1]
skipping: [vMX2] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'Internal'}], 'peer-as': [{'data': '64522'}], 'local-as': [{'data': '64522'}], 'name': [{'data': 'IBGP'}], 'group-index': [{'data': '0'}], 'group-flags': [{}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '2'}], 'established-count': [{'data': '2'}], 'peer-address': [{'data': '10.100.100.1+179'}, {'data': '10.100.100.3+179'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '0'}], 'received-prefix-count': [{'data': '3'}], 'accepted-prefix-count': [{'data': '3'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '3'}]}]}) 
skipping: [vMX3] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'Internal'}], 'peer-as': [{'data': '64522'}], 'local-as': [{'data': '64522'}], 'name': [{'data': 'IBGP'}], 'group-index': [{'data': '0'}], 'group-flags': [{}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '2'}], 'established-count': [{'data': '2'}], 'peer-address': [{'data': '10.100.100.1+179'}, {'data': '10.100.100.2+49178'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '0'}], 'received-prefix-count': [{'data': '3'}], 'accepted-prefix-count': [{'data': '3'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '3'}]}]}) 
skipping: [vMX5] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'Internal'}], 'peer-as': [{'data': '64544'}], 'local-as': [{'data': '64544'}], 'name': [{'data': 'IBGP'}], 'group-index': [{'data': '0'}], 'group-flags': [{}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '1'}], 'established-count': [{'data': '1'}], 'peer-address': [{'data': '10.100.100.6+63217'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '0'}], 'received-prefix-count': [{'data': '1'}], 'accepted-prefix-count': [{'data': '1'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '3'}]}]}) 
skipping: [vMX6] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'Internal'}], 'peer-as': [{'data': '64544'}], 'local-as': [{'data': '64544'}], 'name': [{'data': 'IBGP'}], 'group-index': [{'data': '0'}], 'group-flags': [{}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '1'}], 'established-count': [{'data': '1'}], 'peer-address': [{'data': '10.100.100.5+179'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '2'}], 'received-prefix-count': [{'data': '3'}], 'accepted-prefix-count': [{'data': '3'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '1'}]}]}) 
ok: [vMX4] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'External'}], 'local-as': [{'data': '64533'}], 'name': [{'data': 'to-AS64544'}], 'group-index': [{'data': '0'}], 'group-flags': [{}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'export-policy': [{'data': 'ADV-Loopback'}], 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '2'}], 'established-count': [{'data': '2'}], 'peer-address': [{'data': '10.100.46.2+56067'}, {'data': '10.100.45.2+179'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '2'}], 'received-prefix-count': [{'data': '6'}], 'accepted-prefix-count': [{'data': '6'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '3'}]}]})
ok: [vMX3] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'External'}], 'local-as': [{'data': '64522'}], 'name': [{'data': 'to-AS64533'}], 'group-index': [{'data': '1'}], 'group-flags': [{}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'export-policy': [{'data': 'ADV-Loopback'}], 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '1'}], 'established-count': [{'data': '1'}], 'peer-address': [{'data': '10.100.34.2+55602'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '2'}], 'received-prefix-count': [{'data': '3'}], 'accepted-prefix-count': [{'data': '3'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '2'}]}]})
ok: [vMX5] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'External'}], 'local-as': [{'data': '64544'}], 'name': [{'data': 'to-AS64533'}], 'group-index': [{'data': '1'}], 'group-flags': [{}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'export-policy': [{'data': 'ADV-Loopback'}], 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '1'}], 'established-count': [{'data': '1'}], 'peer-address': [{'data': '10.100.45.1+59768'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '1'}], 'received-prefix-count': [{'data': '3'}], 'accepted-prefix-count': [{'data': '3'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '3'}]}]})
ok: [vMX6] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'External'}], 'local-as': [{'data': '64544'}], 'name': [{'data': 'to-AS64533'}], 'group-index': [{'data': '1'}], 'group-flags': [{'data': 'Export Eval'}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'export-policy': [{'data': 'ADV-Loopback'}], 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '1'}], 'established-count': [{'data': '1'}], 'peer-address': [{'data': '10.100.46.1+179'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '1'}], 'received-prefix-count': [{'data': '3'}], 'accepted-prefix-count': [{'data': '3'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '3'}]}]})
ok: [vMX2] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'External'}], 'local-as': [{'data': '64522'}], 'name': [{'data': 'to-AS64533'}], 'group-index': [{'data': '1'}], 'group-flags': [{}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'export-policy': [{'data': 'ADV-Loopback'}], 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '1'}], 'established-count': [{'data': '1'}], 'peer-address': [{'data': '10.100.24.2+179'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '2'}], 'received-prefix-count': [{'data': '3'}], 'accepted-prefix-count': [{'data': '3'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '2'}]}]})
ok: [vMX4] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'External'}], 'local-as': [{'data': '64533'}], 'name': [{'data': 'to-AS64522'}], 'group-index': [{'data': '1'}], 'group-flags': [{'data': 'Export Eval'}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'export-policy': [{'data': 'ADV-Loopback'}], 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '2'}], 'established-count': [{'data': '2'}], 'peer-address': [{'data': '10.100.34.1+179'}, {'data': '10.100.24.1+51261'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '2'}], 'received-prefix-count': [{'data': '4'}], 'accepted-prefix-count': [{'data': '4'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '3'}]}]})
ok: [vMX3] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'External'}], 'local-as': [{'data': '64522'}], 'name': [{'data': 'to-AS64544'}], 'group-index': [{'data': '2'}], 'group-flags': [{'data': 'Export Eval'}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'export-policy': [{'data': 'ADV-Loopback'}], 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '1'}], 'established-count': [{'data': '1'}], 'peer-address': [{'data': '10.100.35.2+179'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '1'}], 'received-prefix-count': [{'data': '2'}], 'accepted-prefix-count': [{'data': '2'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '3'}]}]})
ok: [vMX5] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'External'}], 'local-as': [{'data': '64544'}], 'name': [{'data': 'to-AS64522'}], 'group-index': [{'data': '2'}], 'group-flags': [{'data': 'Export Eval'}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'export-policy': [{'data': 'ADV-Loopback'}], 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '2'}], 'established-count': [{'data': '2'}], 'peer-address': [{'data': '10.100.25.1+63829'}, {'data': '10.100.35.1+64405'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '2'}], 'received-prefix-count': [{'data': '4'}], 'accepted-prefix-count': [{'data': '4'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '2'}]}]})
ok: [vMX2] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'External'}], 'local-as': [{'data': '64522'}], 'name': [{'data': 'to-AS64544'}], 'group-index': [{'data': '2'}], 'group-flags': [{'data': 'Export Eval'}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'export-policy': [{'data': 'ADV-Loopback'}], 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '1'}], 'established-count': [{'data': '1'}], 'peer-address': [{'data': '10.100.25.2+179'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '1'}], 'received-prefix-count': [{'data': '2'}], 'accepted-prefix-count': [{'data': '2'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '3'}]}]})

TASK [Set Next hop attribute] ***************************************************************************************************************************************
changed: [vMX3]
changed: [vMX4]
changed: [vMX5]
changed: [vMX2]
changed: [vMX1]
changed: [vMX6]

PLAY RECAP **********************************************************************************************************************************************************
vMX1                       : ok=4    changed=1    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0   
vMX2                       : ok=5    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
vMX3                       : ok=5    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
vMX4                       : ok=5    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
vMX5                       : ok=5    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
vMX6                       : ok=5    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

vMX6のルーティングテーブル(BGP)をもう一度見てみます。

jcluser@vMX6> show route protocol bgp 

inet.0: 13 destinations, 16 routes (13 active, 0 holddown, 0 hidden)
+ = Active Route, - = Last Active, * = Both

10.100.100.2/32    *[BGP/170] 00:11:48, localpref 100, from 10.100.100.5
                      AS path: 64522 I, validation-state: unverified
                    >  to 10.100.56.1 via ge-0/0/4.0
                    [BGP/170] 02:27:19, localpref 100
                      AS path: 64533 64522 I, validation-state: unverified
                    >  to 10.100.46.1 via ge-0/0/3.0
10.100.100.3/32    *[BGP/170] 00:11:48, localpref 100, from 10.100.100.5
                      AS path: 64522 I, validation-state: unverified
                    >  to 10.100.56.1 via ge-0/0/4.0
                    [BGP/170] 02:27:15, localpref 100
                      AS path: 64533 64522 I, validation-state: unverified
                    >  to 10.100.46.1 via ge-0/0/3.0
10.100.100.4/32    *[BGP/170] 02:27:16, localpref 100
                      AS path: 64533 I, validation-state: unverified
                    >  to 10.100.46.1 via ge-0/0/3.0
                    [BGP/170] 00:11:48, localpref 100, from 10.100.100.5
                      AS path: 64533 I, validation-state: unverified
                    >  to 10.100.56.1 via ge-0/0/4.0

inet6.0: 1 destinations, 1 routes (1 active, 0 holddown, 0 hidden)

10.100.100.2向けルートのAS path: 64522 Iネクストホップが10.123.0.1から10.100.56.1になっています。 これで良さそうですね。

これでもう一度vMX2のLoopbackアドレスからvMX6のLoopbackアドレスに向けてpingを打ってみます。

jcluser@vMX2> ping 10.100.100.6 source 10.100.100.2 
PING 10.100.100.6 (10.100.100.6): 56 data bytes
64 bytes from 10.100.100.6: icmp_seq=0 ttl=63 time=1.798 ms
64 bytes from 10.100.100.6: icmp_seq=1 ttl=63 time=2.021 ms
64 bytes from 10.100.100.6: icmp_seq=2 ttl=63 time=2.316 ms
64 bytes from 10.100.100.6: icmp_seq=3 ttl=63 time=2.684 ms
64 bytes from 10.100.100.6: icmp_seq=4 ttl=63 time=1.942 ms
^C
--- 10.100.100.6 ping statistics ---
5 packets transmitted, 5 packets received, 0% packet loss
round-trip min/avg/max/stddev = 1.798/2.152/2.684/0.315 ms

できました!

そしてもう一つ
ここまで触れていませんでしたが、実はvMX1のLoopbackアドレス10.100.100.1がAS64533とAS64544から見えない問題が残っています。

これはAnsibleでやるとなると下記のconfigをvMX1と同じASに属するvMX2とvMX3に入れる形ですかね

set policy-options prefix-list vMX1Loopback 10.100.100.1/32
set policy-options policy-statement ADV-Loopback term 2 from prefix-list vMX1Loopback
set policy-options policy-statement ADV-Loopback term 2 then accept

これをPlaybookのタスクに付け加えます。

    - name: Advertise vMX1 Loopback
      junipernetworks.junos.junos_config:
        lines:
          - set policy-options prefix-list vMX1Loopback 10.100.100.1/32
          - set policy-options policy-statement ADV-Loopback term 2 from prefix-list vMX1Loopback
          - set policy-options policy-statement ADV-Loopback term 2 then accept
      when:
        - inventory_hostname in ['vMX2', 'vMX3']

そして実行!

(vlab-junos-py3.11) yu_takeda@IT-PC-2012-0531:~/vlab_junos$ ansible-playbook -i inventory/inventory.ini advertise_ebgp.yaml 

PLAY [Advertise loopback] *******************************************************************************************************************************************

TASK [Get Loopback info] ********************************************************************************************************************************************
ok: [vMX4]
ok: [vMX3]
ok: [vMX1]
ok: [vMX2]
ok: [vMX5]
ok: [vMX6]

TASK [Get BGP info] *************************************************************************************************************************************************
ok: [vMX2]
ok: [vMX4]
ok: [vMX3]
ok: [vMX5]
ok: [vMX1]
ok: [vMX6]

TASK [Make Policy Option] *******************************************************************************************************************************************
ok: [vMX3]
ok: [vMX4]
ok: [vMX1]
ok: [vMX5]
ok: [vMX2]
ok: [vMX6]

TASK [Set Export] ***************************************************************************************************************************************************
skipping: [vMX1] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'Internal'}], 'peer-as': [{'data': '64522'}], 'local-as': [{'data': '64522'}], 'name': [{'data': 'IBGP'}], 'group-index': [{'data': '0'}], 'group-flags': [{'data': 'Export Eval'}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'export-policy': [{'data': 'NEXT_HOP'}], 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '2'}], 'established-count': [{'data': '2'}], 'peer-address': [{'data': '10.100.100.2+57396'}, {'data': '10.100.100.3+52090'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '3'}], 'received-prefix-count': [{'data': '6'}], 'accepted-prefix-count': [{'data': '6'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '0'}]}]}) 
skipping: [vMX1]
skipping: [vMX2] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'Internal'}], 'peer-as': [{'data': '64522'}], 'local-as': [{'data': '64522'}], 'name': [{'data': 'IBGP'}], 'group-index': [{'data': '0'}], 'group-flags': [{}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'export-policy': [{'data': 'NEXT_HOP'}], 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '2'}], 'established-count': [{'data': '2'}], 'peer-address': [{'data': '10.100.100.1+179'}, {'data': '10.100.100.3+179'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '0'}], 'received-prefix-count': [{'data': '3'}], 'accepted-prefix-count': [{'data': '3'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '3'}]}]}) 
skipping: [vMX3] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'Internal'}], 'peer-as': [{'data': '64522'}], 'local-as': [{'data': '64522'}], 'name': [{'data': 'IBGP'}], 'group-index': [{'data': '0'}], 'group-flags': [{'data': 'Export Eval'}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'export-policy': [{'data': 'NEXT_HOP'}], 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '2'}], 'established-count': [{'data': '2'}], 'peer-address': [{'data': '10.100.100.1+179'}, {'data': '10.100.100.2+49178'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '0'}], 'received-prefix-count': [{'data': '3'}], 'accepted-prefix-count': [{'data': '3'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '3'}]}]}) 
skipping: [vMX5] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'Internal'}], 'peer-as': [{'data': '64544'}], 'local-as': [{'data': '64544'}], 'name': [{'data': 'IBGP'}], 'group-index': [{'data': '0'}], 'group-flags': [{'data': 'Export Eval'}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'export-policy': [{'data': 'NEXT_HOP'}], 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '1'}], 'established-count': [{'data': '1'}], 'peer-address': [{'data': '10.100.100.6+63217'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '0'}], 'received-prefix-count': [{'data': '1'}], 'accepted-prefix-count': [{'data': '1'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '4'}]}]}) 
skipping: [vMX6] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'Internal'}], 'peer-as': [{'data': '64544'}], 'local-as': [{'data': '64544'}], 'name': [{'data': 'IBGP'}], 'group-index': [{'data': '0'}], 'group-flags': [{'data': 'Export Eval'}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'export-policy': [{'data': 'NEXT_HOP'}], 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '1'}], 'established-count': [{'data': '1'}], 'peer-address': [{'data': '10.100.100.5+179'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '3'}], 'received-prefix-count': [{'data': '4'}], 'accepted-prefix-count': [{'data': '4'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '1'}]}]}) 
ok: [vMX6] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'External'}], 'local-as': [{'data': '64544'}], 'name': [{'data': 'to-AS64533'}], 'group-index': [{'data': '1'}], 'group-flags': [{}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'export-policy': [{'data': 'ADV-Loopback'}], 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '1'}], 'established-count': [{'data': '1'}], 'peer-address': [{'data': '10.100.46.1+179'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '1'}], 'received-prefix-count': [{'data': '4'}], 'accepted-prefix-count': [{'data': '4'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '4'}]}]})
ok: [vMX2] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'External'}], 'local-as': [{'data': '64522'}], 'name': [{'data': 'to-AS64533'}], 'group-index': [{'data': '1'}], 'group-flags': [{'data': 'Export Eval'}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'export-policy': [{'data': 'ADV-Loopback'}], 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '1'}], 'established-count': [{'data': '1'}], 'peer-address': [{'data': '10.100.24.2+179'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '2'}], 'received-prefix-count': [{'data': '3'}], 'accepted-prefix-count': [{'data': '3'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '3'}]}]})
ok: [vMX4] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'External'}], 'local-as': [{'data': '64533'}], 'name': [{'data': 'to-AS64544'}], 'group-index': [{'data': '0'}], 'group-flags': [{}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'export-policy': [{'data': 'ADV-Loopback'}], 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '2'}], 'established-count': [{'data': '2'}], 'peer-address': [{'data': '10.100.46.2+56067'}, {'data': '10.100.45.2+179'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '2'}], 'received-prefix-count': [{'data': '8'}], 'accepted-prefix-count': [{'data': '8'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '4'}]}]})
ok: [vMX3] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'External'}], 'local-as': [{'data': '64522'}], 'name': [{'data': 'to-AS64533'}], 'group-index': [{'data': '1'}], 'group-flags': [{}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'export-policy': [{'data': 'ADV-Loopback'}], 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '1'}], 'established-count': [{'data': '1'}], 'peer-address': [{'data': '10.100.34.2+55602'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '2'}], 'received-prefix-count': [{'data': '3'}], 'accepted-prefix-count': [{'data': '3'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '2'}]}]})
ok: [vMX5] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'External'}], 'local-as': [{'data': '64544'}], 'name': [{'data': 'to-AS64533'}], 'group-index': [{'data': '1'}], 'group-flags': [{}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'export-policy': [{'data': 'ADV-Loopback'}], 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '1'}], 'established-count': [{'data': '1'}], 'peer-address': [{'data': '10.100.45.1+59768'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '1'}], 'received-prefix-count': [{'data': '4'}], 'accepted-prefix-count': [{'data': '4'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '4'}]}]})
ok: [vMX3] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'External'}], 'local-as': [{'data': '64522'}], 'name': [{'data': 'to-AS64544'}], 'group-index': [{'data': '2'}], 'group-flags': [{}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'export-policy': [{'data': 'ADV-Loopback'}], 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '1'}], 'established-count': [{'data': '1'}], 'peer-address': [{'data': '10.100.35.2+179'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '1'}], 'received-prefix-count': [{'data': '2'}], 'accepted-prefix-count': [{'data': '2'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '3'}]}]})
ok: [vMX4] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'External'}], 'local-as': [{'data': '64533'}], 'name': [{'data': 'to-AS64522'}], 'group-index': [{'data': '1'}], 'group-flags': [{}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'export-policy': [{'data': 'ADV-Loopback'}], 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '2'}], 'established-count': [{'data': '2'}], 'peer-address': [{'data': '10.100.34.1+179'}, {'data': '10.100.24.1+51261'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '3'}], 'received-prefix-count': [{'data': '5'}], 'accepted-prefix-count': [{'data': '5'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '3'}]}]})
ok: [vMX2] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'External'}], 'local-as': [{'data': '64522'}], 'name': [{'data': 'to-AS64544'}], 'group-index': [{'data': '2'}], 'group-flags': [{'data': 'Export Eval'}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'export-policy': [{'data': 'ADV-Loopback'}], 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '1'}], 'established-count': [{'data': '1'}], 'peer-address': [{'data': '10.100.25.2+179'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '1'}], 'received-prefix-count': [{'data': '2'}], 'accepted-prefix-count': [{'data': '2'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '4'}]}]})
ok: [vMX5] => (item={'attributes': {'junos:style': 'brief'}, 'type': [{'data': 'External'}], 'local-as': [{'data': '64544'}], 'name': [{'data': 'to-AS64522'}], 'group-index': [{'data': '2'}], 'group-flags': [{}], 'bgp-option-information': [{'attributes': {'xmlns': 'http://xml.juniper.net/junos/21.1R0/junos-routing'}, 'export-policy': [{'data': 'ADV-Loopback'}], 'bgp-options': [{}], 'bgp-options2': [{}], 'bgp-options-extended': [{'data': 'GracefulShutdownRcv'}], 'holdtime': [{'data': '0'}], 'preference': [{'data': '0'}], 'gshut-recv-local-preference': [{'data': '0'}]}], 'peer-count': [{'data': '2'}], 'established-count': [{'data': '2'}], 'peer-address': [{'data': '10.100.25.1+63829'}, {'data': '10.100.35.1+64405'}], 'bgp-rib': [{'attributes': {'junos:style': 'terse'}, 'name': [{'data': 'inet.0'}], 'active-prefix-count': [{'data': '3'}], 'received-prefix-count': [{'data': '5'}], 'accepted-prefix-count': [{'data': '5'}], 'suppressed-prefix-count': [{'data': '0'}], 'advertised-prefix-count': [{'data': '2'}]}]})

TASK [Set Next hop attribute] ***************************************************************************************************************************************
ok: [vMX4]
ok: [vMX3]
ok: [vMX5]
ok: [vMX2]
ok: [vMX1]
ok: [vMX6]

TASK [Advertise vMX1 Loopback] **************************************************************************************************************************************
skipping: [vMX1]
skipping: [vMX4]
skipping: [vMX5]
skipping: [vMX6]
changed: [vMX3]
changed: [vMX2]

PLAY RECAP **********************************************************************************************************************************************************
vMX1                       : ok=4    changed=0    unreachable=0    failed=0    skipped=2    rescued=0    ignored=0   
vMX2                       : ok=6    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
vMX3                       : ok=6    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
vMX4                       : ok=5    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0   
vMX5                       : ok=5    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0   
vMX6                       : ok=5    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0   

ちゃんとvMX2とvMX3にのみ設定が入りましたね。

これでvMX1からもLoopbackアドレスでpingができるようになりました!

jcluser@vMX1> ping 10.100.100.5 source 10.100.100.1    
PING 10.100.100.5 (10.100.100.5): 56 data bytes
64 bytes from 10.100.100.5: icmp_seq=0 ttl=63 time=2.144 ms
64 bytes from 10.100.100.5: icmp_seq=1 ttl=63 time=2.704 ms
64 bytes from 10.100.100.5: icmp_seq=2 ttl=63 time=43.369 ms
64 bytes from 10.100.100.5: icmp_seq=3 ttl=63 time=2.631 ms
64 bytes from 10.100.100.5: icmp_seq=4 ttl=63 time=2.518 ms
^C
--- 10.100.100.5 ping statistics ---
5 packets transmitted, 5 packets received, 0% packet loss
round-trip min/avg/max/stddev = 2.144/10.673/43.369/16.349 ms

これでJuniper vLabsのBGP - Multi - ASで各vMXのLoopback同士が通信できるようになりました!
やった~!

これを足掛かりに次はMPLSとかやりたいですね。

NEC IX対応のAnsible Collectionを作ってみた

Ansible Collection

NECのUniverge IXシリーズ対応コレクションを作成しました。 ただしモジュールは簡単なcommandとconfigの変更ができるだけで課題は山積みです。

環境作成

まずはPythonの仮想環境を作成します。 インストールしたパッケージは下記になります。 パッケージ管理ツールでpoetryを使っているのでtomlからの抜粋になります。

[tool.poetry.dependencies]
python = "^3.10"
ansible = "^6.4.0"
paramiko = "^2.11.0"
ansible-pylibssh = "^1.0.0"

コレクションの雛形を作成する。

まずは ansible-galaxyコマンドでコレクションの雛形を作ります。
ansible_collectionsという名前のディレクトリを作った後、ansible-galaxy colleciton initコマンドで作成します。
また、いずれはAnsible Galaxyに挙げられたらいいなと思うのでnamespaceは自分のGitHubの名前にします。

参考
Creating collections
[Ansible] 自作のコレクションを作ってGalaxyで公開するまで

# ディレクトリを作成
mkdir ansible_collections
# 作成したディレクトリへ移動
cd ansible_collections
# コレクションの雛形を作成
ansible-galaxy init rucdev.ix

これで ansible_collections/rucdev/ix/の中身は下記のようになります。

.
├── README.md
├── docs
├── galaxy.yml
├── plugins
│   └── README.md
└── roles

ここから主に pluginsディレクトリ以下にコレクションを構成するプラグインとモジュールを書いていきます。

モジュールなどを呼び出す際のFQCNnamespace.コレクション名となるので rucdev.ixになります。

なので、ix_configなるモジュールを作ったときにプレイブックからは rucdev.ix.ix_configで呼び出します。

ここから下の内容は既存モジュールのソースコードを読みながら、各機器とIXシリーズの機器の差異を埋められるように見様見真似で作ったものなります。ただ、私自身オブジェクト指向メタプログラミングについての知識が浅いのでややコピペになっている部分があることは否めないです。

yamahaのモジュールが比較的コンパクトな実装になっているので、それを見て最低限必要なものを揃えて、足りなそうなところはciscoとjuniperのモジュールを見ながら作っていました。

Pluginを作る

他のネットワーク機器と同じように inventoryファイルで ansible_network_osへの指定をできるようにします。その際の network_osrucdev.ix.ixで設定できるように下記の2つのPluginを作っていきます。

  • Terminal Plugin
  • Cliconf Plugin

Terminal Pluginを作る

Terminal Pluginは機器のプロンプトやエラーメッセージについてをキャッチするための正規表現を設定したり、becomeで特権に入るための手順と機器から抜けるための手順などを設定します。
まずはTerminal Pluginを書くためのディレクトリとファイルを用意します。
pluginディレクトリの下に terminalというディレクトリを作り、その中にTerminal Pluginを書くファイルを置きます。
ansible_network_os=rucdev.ix.ixで呼べるようにしたいので、ファイル名は ix.pyとします。

ansible_network_osの指定の部分は<namespace>.<collection>.<plugin>の並びになっています。

# ansible_collections/rucdev/ix/
.
├── README.md
├── docs
├── galaxy.yml
├── plugins
│   ├── terminal - 追加
│   │   ├── __init__.py - 追加
│   │   └── ix.py - 追加
│   └── README.md
└── roles

Terminal Pluginは netcommonモジュールの TerminalBaseクラスを拡張した TerminalModuleという名前のクラスを作ってその中に機器に合わせた形の挙動を記述します。
※ちなみに TerminalBaseを継承した TerminalModuleというクラス名でないときちんと呼び出しが出来ません。

from ansible_collections.ansible.netcommon.plugins.plugin_utils.terminal_base import (
    TerminalBase,
)

class TerminalModule(TerminalBase):
    # 以下に設定を記述します。
    pass

こんな感じになりました。
terminal/ix.py

Cliconf Pluginを作る

次にCliconf Pluginを作っていきます。 Cliconf Pluginは機器との接続する部分のラッパーを提供します。 公式曰くCLIインターフェースを抽象化したもののようです。 手順としてはTerminal Pluginの時と同じです。

まずはディレクトリとファイルを用意します。

# ansible_collections/rucdev/ix/
.
├── README.md
├── docs
├── galaxy.yml
├── plugins
│   ├── cliconf - 追加
│   │   ├── __init__.py - 追加
│   │   └── ix.py - 追加
│   ├── terminal
│   │   ├── __init__.py
│   │   └── ix.py
│   └── README.md
└── roles

この cliconf/ix.pynetcommonモジュールの CliconfBaseクラスを継承した Cliconfクラスを作成します。

from ansible_collections.ansible.netcommon.plugins.plugin_utils.cliconf_base import (
    CliconfBase,
)

class Cliconf(CliconfBase):
    # 以下に設定を記述します。
    pass

詳細は下記
cliconf/ix.py

moduleを作る

module_utilsを作る

Cliconf PluginとTerminal Pluginが出来たら、モジュール作成に移る前に、module_utilsにモジュールから参照するコマンド実行用ラッパーを作成します。

# ansible_collections/rucdev/ix/
.
├── README.md
├── docs
├── galaxy.yml
├── plugins
│   ├── cliconf 
│   │   ├── __init__.py
│   │   └── ix.py
│   ├── terminal
│   │   ├── __init__.py
│   │   └── ix.py
│   ├── module_utils - 追加
│   │   ├── __init__.py - 追加
│   │   └── network - 追加
│   │       ├── __init__.py - 追加
│   │       └── ix - 追加
│   │           ├── __init__.py - 追加
│   │           └── ix.py - 追加
│   └── README.md
└── roles

この module_utils/network/ix/ix.pyにはCliconf Pluginで書いたCLIインターフェースを抽象化したメソッドを AnsibleModuleクラスと合わせた形でモジュール側で呼び出せるように作ります。

ここの部分はこれまでのPlugin以上によくわからず書いているので、特筆することがないです。
ほぼほぼコピペ。

module_utils/network/ix/ix.py

moduleを書く

ここまで来てようやくモジュールを書き始められます。

まずはshowコマンド系を打つモジュールとCLIコマンドでの設定変更をするモジュールの2つをつくりたいとおもいます。
モジュール名は cisco.iosに習って ix_commandix_configにします。

まずはモジュールの本体となるファイルを用意します。

# ansible_collections/rucdev/ix/
.
├── README.md
├── docs
├── galaxy.yml
├── plugins
│   ├── cliconf 
│   │   ├── __init__.py
│   │   └── ix.py
│   ├── terminal
│   │   ├── __init__.py
│   │   └── ix.py
│   ├── module_utils
│   │   ├── __init__.py
│   │   └── network
│   │       ├── __init__.py
│   │       └── ix
│   │           ├── __init__.py
│   │           └── ix.py
│   ├── modules - 追加
│   │   ├── __init__.py - 追加
│   │   ├── ix_command.py - 追加
│   │   └── ix_config.py - 追加
│   └── README.md
└── roles

どちらのモジュールも流れとしては下記になります。

graph LR
    A(モジュールが持てるパラメータを定義) --> A2(AnsibleModuleクラスを呼び出す)
    A2 --> B(モジュールの持つオプションなどに対応する処理)
    B --> C(冪等性に関する処理)
    C --> D(コマンドの送信)
    D --> E(結果の解釈)

この上記の流れをそれぞれの、Pythonファイルの main関数内で行います。

そんなこんなで出来上がったのが下記。

modules/ix_command.py
modules/ix_config.py

実際にプレイブック内で使ってみる

ではここからは実際に作ったコレクションを用いてNEC Univerge IXシリーズに接続してみたいと思います。

ansible.cfgへ設定

プレイブックを動かす前に、先に今回作成したコレクションを参照するための設定を入れます。

# IXコレクションを開発しているディレクトリ
.
├── ansible_collections
│   └── rucdev
│       └── ix - 先程まで作成していたコレクション
└── ansible.cfg - 追加

ansible.cfgファイルに下記を記述。

[defaults]
collections_path = /home/user/.ansible/collections:/usr/share/ansible/collections:./

これで今回作成したコレクションも呼び出せるようになりました。 ホームディレクトリと /usr/share/配下のコレクションもパスに入れているのはIXへの接続時に使用する network_cliを提供する netcommonを呼べるようにするためです。

プレイブックを書いてみる

準備も出来たところで簡単なプレイブックを書いてみます。

.
├── ansible_collections
├── ansible.cfg
└── ix_demo.yaml - 追加

ix_demo.yamlの中身は下記のようにしてみました。

---
- hosts: ix
  gather_facts: false
  tasks:
    - name: show ip interface Loopback0.0
      rucdev.ix.ix_command:
        commands:
          - show ip interface Loopback0.0
      register: pre_status

    - name: pre check
      ansible.builtin.debug:
        msg: "{{ pre_status.stdout_lines[0] }}"
        
    - name: set ip address
      rucdev.ix.ix_config:
        lines:
          - ip address 10.0.0.1/24
        parents: interface Loopback0.0
    
    - name: show ip interface Loopback0.0
      rucdev.ix.ix_command:
        commands:
          - show ip interface Loopback0.0
      register: post_status

    - name: post check
      ansible.builtin.debug:
        msg: "{{ post_status.stdout_lines[0] }}"

インターフェースLoopback0.0へのIPアドレスの設定とその事前事後確認を行うプレイブックです。

そしてinventoryファイルはこんな感じ。

[ix]
ix_test ansible_host=<ip address>
[ix:vars]
ansible_network_os=rucdev.ix.ix
ansible_connection=network_cli
ansible_user=<username>
ansible_password=<password>

これを動かすとこんな感じでできました。

PLAY [ix] *********************************************************************************************************************************************************

TASK [show ip interface Loopback0.0] ******************************************************************************************************************************
ok: [ix_test]

TASK [pre check] **************************************************************************************************************************************************
ok: [ix_test] => {
    "msg": [
        "Interface Loopback0.0 is dormant, line protocol is down",
        "  Internet protocol processing disabled"
    ]
}

TASK [change description] *****************************************************************************************************************************************
changed: [ix_test]

TASK [show ip interface Loopback0.0] ******************************************************************************************************************************
ok: [ix_test]

TASK [post check] *************************************************************************************************************************************************
ok: [ix_test] => {
    "msg": [
        "Interface Loopback0.0 is up, line protocol is up",
        "  Internet address is 1.1.1.1/24",
        "  Broadcast address is 255.255.255.255",
        "  Address determined by config",
        "  MTU is 1500 octets",
        "  Directed broadcast forwarding is disabled",
        "  Proxy ARP is disabled",
        "  Local proxy ARP is disabled",
        "  ICMP redirects are never sent",
        "  TCP MSS adjustment is disabled"
    ]
}

PLAY RECAP ********************************************************************************************************************************************************
ix_test                    : ok=5    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

事前事後のshowコマンドも取れていて設定もできている。良さそうですね。

冪等性のチェックとして同じプレイブックをもう一度流すと、、、

PLAY [ix] *********************************************************************************************************************************************************

TASK [show ip interface Loopback0.0] ******************************************************************************************************************************
ok: [ix_test]

TASK [pre check] **************************************************************************************************************************************************
ok: [ix_test] => {
    "msg": [
        "Interface Loopback0.0 is up, line protocol is up",
        "  Internet address is 1.1.1.1/24",
        "  Broadcast address is 255.255.255.255",
        "  Address determined by config",
        "  MTU is 1500 octets",
        "  Directed broadcast forwarding is disabled",
        "  Proxy ARP is disabled",
        "  Local proxy ARP is disabled",
        "  ICMP redirects are never sent",
        "  TCP MSS adjustment is disabled"
    ]
}

TASK [change description] *****************************************************************************************************************************************
ok: [ix_test]

TASK [show ip interface Loopback0.0] ******************************************************************************************************************************
ok: [ix_test]

TASK [post check] *************************************************************************************************************************************************
ok: [ix_test] => {
    "msg": [
        "Interface Loopback0.0 is up, line protocol is up",
        "  Internet address is 1.1.1.1/24",
        "  Broadcast address is 255.255.255.255",
        "  Address determined by config",
        "  MTU is 1500 octets",
        "  Directed broadcast forwarding is disabled",
        "  Proxy ARP is disabled",
        "  Local proxy ARP is disabled",
        "  ICMP redirects are never sent",
        "  TCP MSS adjustment is disabled"
    ]
}

PLAY RECAP ********************************************************************************************************************************************************
ix_test                    : ok=5    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

今度はokになりました。
これで冪等性についても良さそうです。(この一点においては)

課題

なんやかんやで上手くいっているように見えますが、このコレクションには課題が山積しています。

  • IXのconfigureモードは同時に1人しか入れない仕様をクリアしていない
    これはIXの仕様によるものでconfigureモードは1機器1セッションしか許可されていないので、Ansible実行時に誰かが対象のIXでconfigureモードで作業しているとエラーが出てしまいます。
  • ドキュメントが全く書けていない
    ドキュメントの量がなかなか多いので書けていない状況です。自動生成するツールもあるようですがまだ調査不足なので今後対応します。
  • エラーコマンドの収集不足
    Terminal pluginを作るにあたり、IXが出すエラー出力を正規表現で設定する必要がありますが、IXが出すエラーについてはマニュアルにも詳細な記述が見当たらなかったので 自分で調べる必要がありました。ただこれがかなり難しい。どこまでで収集しきったといえるかもわからないのでこれは永遠に続く課題かもしれません。(メーカーの人間になれば話は変わるでしょうが)

そんな感じで使うにはもう一歩な感じはありますが、コレクションを作る中でAnsibleのコアな部分に触れられたのでやってみてよかったなと思います。
今後はぼちぼちでこのモジュールをよりよくしていければと思います。