AnsibleのJUNOS設定モジュール書いてみた(VLAN編)

Ansibleのお勉強のためにJUNOS向けにNETCONFでVLANを作成・削除するモジュールを書いてみた。
動いてるけど、VC環境だとcommit syncするときにタイムアウトするかも。
Ex3300とEx2200で動作確認した。意外にちゃんとうごいて冪等性も担保できてる。

引数

  • state: 作成する時(present),削除する時(absent)
  • node: 操作対象ネットワーク機器のノード名またはIPアドレス
  • port: 操作対象ネットワーク機器がNETCONF用に口をあけているポート(デフォルト:830)
  • user: 操作対象ネットワーク機器のログインアカウント
  • password: 操作対象ネットワーク機器のパスワード
  • vlan_name: VLAN名
  • vlan_id: VLANID
  • vlan_desc: VLANのDescription

テストしてみる

VLANを作成する
$ ~/ansible/hacking/test-module -m ./junos_vlan -a "node='switch00' port=830 user='foo' password='bar' state='present' vlan_name='VLAN3000' vlan_id=3000 vlan_desc='test vlan'"
VLANを削除する

もちろん対象VLANが存在しなければ何もしない。

$ ~/ansible/hacking/test-module -m ./junos_vlan -a "node='switch00' port=830 user='foo' password='bar' state='absent' vlan_name='VLAN3000' vlan_id=3000 vlan_desc='test vlan'"

モジュール

#!/usr/bin/env python
# -*- coding: utf-8 -*-

DOCUMENTATION = '''
---
module: junos_vlan
author: Hideki Saito
short_description: Manage VLAN resources
requirements:
    - ncclient v0.3.2
description:
    - Manage VLAN resources on JUNOS devices.  This module requires the
      ncclient.
      [link]
      http://ncclient.grnet.gr/
options:
    node:
        desctiption
            - the hostname or IP address to connect
        requred: true
    port:
        description
            - the port number to connect
    user:
        description
            - junos user id
        required: true
    password:
        description
            - junos user password
        requre: true
    vlan_name:
        description:
            - name of vlan
        required: false
    vlan_id:
        description:
            - the vlan id
        required: true
    vlan_desc:
        description:
            - a descriptive name for the vlan
        required: false
    state:
        description:
            - describe the desired state of the vlan related to the config
        required: false
        default: 'present'
        choices: [ 'present', 'absent' ]
    logging:
        description:
            - enables or disables the syslog facility for this module
        required: false
        choices: [ 'true', 'false', 'yes', 'no' ]
notes:
    - The ncclient module must be installed.
    - See http://ncclient.grnet.gr/ for details
'''

import json
from jinja2 import Template
from ncclient import manager
from ncclient.operations.errors import TimeoutExpiredError
from xml.etree import ElementTree


create_vlan = """
<config>
  <configuration>
    <vlans>
      <vlan operation="create">
        <name>{{ vlan_name }}</name>
        <description>{{ vlan_desc }}</description>
        <vlan-id>{{ vlan_id }}</vlan-id>
      </vlan>
    </vlans>
  </configuration>
</config>
"""

delete_vlan = """
<config>
  <configuration>
    <vlans>
      <vlan operation="delete">
        <name>{{ vlan_name }}</name>
        <vlan-id>{{ vlan_id }}</vlan-id>
      </vlan>
    </vlans>
  </configuration>
</config>
"""

set_description = """
<config>
  <configuration>
    <vlans>
      <vlan>
        <name>{{ vlan_name }}</name>
        <description>{{ vlan_desc }}</description>
      </vlan>
    </vlans>
  </configuration>
</config>
"""


class JunosVlan(object):
    def __init__(self, module):
        self.module = module
        self.node = module.params['node']
        self.port = module.params['port']
        self.user = module.params['user']
        self.password = module.params['password']
        self.vlan_name = module.params['vlan_name']
        self.vlan_id = module.params['vlan_id']
        self.vlan_desc = module.params['vlan_desc']
        self.state = module.params['state']
        with manager.connect(host=self.node,
                             port=self.port,
                             username=self.user,
                             password=self.password,
                             hostkey_verify=False) as m:
            self.config = m.get_config(source='candidate').data_xml

    def push(self, config):
        try:
            with manager.connect(host=self.node,
                                 port=self.port,
                                 username=self.user,
                                 password=self.password,
                                 unknown_host_cb=_always_unknown_true) as mgr:
                mgr.edit_config(target="candidate",
                                config=config,
                                test_option="test-then-set")
                mgr.commit()
                rc = 0
                out = 'commit succeeded'
                err = ''
        except TimeoutExpiredError as e:
            rc = 1
            out = 'commit failure'
            err = 'operation timeout'
        return (rc, out, err)

    def vlans(self):
        elm = ElementTree.fromstring(self.config)
        elm_vlans = elm.find('.//vlans')
        vlan_info = dict()
        for elm_vlan in elm_vlans.findall('.//vlan'):
            vlan_id = elm_vlan.findtext('.//vlan-id')
            name = elm_vlan.findtext('.//name')
            description = elm_vlan.findtext('.//description')
            vlan_info[name] = dict(vlan_id=vlan_id, description=description)
        return vlan_info.keys()

def _always_unknown_true(host, fingerprint):
    return True


def main():

    module = AnsibleModule(
        argument_spec = dict(
            node=dict(default=None, requred=True, type='str'),
            port=dict(default=830, requred=True, type='int'),
            user=dict(default=None, requred=True, type='str'),
            password=dict(default=None, requred=True, type='str'),
            vlan_name=dict(default=None, required=True, type='str'),
            vlan_id=dict(default=None, type='int'),
            vlan_desc=dict(default=None, type='str'),
            state=dict(default='present',
                       choices=['present', 'absent']),
        ),
        supports_check_mode = True
    )

    obj = JunosVlan(module)
    rc = None
    result = dict()
    changed = False

    if obj.state == 'absent':
        if obj.vlan_name in obj.vlans():
            template = Template(delete_vlan)
            config = template.render(vlan_name=obj.vlan_name,
                                     vlan_id=obj.vlan_id)
            if module.check_mode:
                module.exit_json(changed=True)
            (rc, out, err) = obj.push(config)
            result['results'] = out

    elif obj.state == 'present':
        if obj.vlan_name in obj.vlans():
            template = Template(set_description)
            config = template.render(vlan_name=obj.vlan_name,
                                     vlan_desc=obj.vlan_desc)
        else:
            template = Template(create_vlan)
            config = template.render(vlan_name=obj.vlan_name,
                                     vlan_id=obj.vlan_id,
                                     vlan_desc=obj.vlan_desc)
        if module.check_mode:
            module.exit_json(changed=True)
        (rc, out, err) = obj.push(config)
        result['results'] = out

        if rc is not None and rc != 0:
            module.fail_json(msg=err, rc=rc)

    if rc is None:
        result['changed'] = False
    else:
        result['changed'] = True

    module.exit_json(**result)


# include magic from lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
main()

##
## [EOF]
##