VyattaのconfigをREST APIで一括設定

VyattaのREST APIで、作成したconfigをリモートから一括で設定&commitできるクラスを作成してみました。REST APIのrequestを作るのには [Requests](http://docs.python-requests.org/en/latest/#) というPythonのHTTPライブラリを使っています。

sshでログインしてconfigをコピペしても一括でconfigを適用することはできますが、手作業が発生してしまいます。この方法であれば、リモートからコマンドだけで設定が変更できるので作業がかなり楽になりますし、provisioning時のスクリプトから実行するなど処理の自動化もできるようになるでしょう。


## 前提となるシステム要件

- VyattaはSubscription Editionであること
  - Vyatta Community EditionではREST APIがサポートされません
  - SoftLayerの場合はVyatta Gateway Applianceを選択してください
- [Requests](http://docs.python-requests.org/en/latest/#) のインストール  
  - ```$ pip install requests``` でインストール


## 注意点

使用にあたってはいくつか注意点があります。

- configを変更前の状態に元に戻す機能は実装していません。その場合はsshでVyattaにログインして、deleteコマンドで設定を削除してください。
- commitされるので、変更するconfigの内容によってはVyattaにアクセスできなくなってしまう場合もあるかもしれません。実行の際は注意してください。
  - もしアクセスできなくなった場合は、SoftLayerのチケットで修復をお願いしてみてください。きっと優しいSoftLayerのサポートエンジニアが助けてくれると思います。
  - [IPMIを使って](https://www.ibm.com/developerworks/community/blogs/560da618-352c-4903-9e75-2dadfda71076/entry/softlayer%25e3%2581%25ae%25e6%25ad%25a9%25e3%2581%258d%25e6%2596%25b9_ipmi%25e3%2581%25b8%25e3%2581%25ae%25e3%2582%25a2%25e3%2582%25af%25e3%2582%25bb%25e3%2582%25b9%25e6%2596%25b9%25e6%25b3%2595?lang=en)ローカルコンソールからログインして修正することもできるでしょう。
  - または、変更したconfigはcommitしていますがsaveはしていないので、最悪自分でVyattaを再起動すれば元の設定に戻ります。


## モジュール

### VyattaControlクラス

```py:Vyatta.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import re
import urllib
import requests
requests.packages.urllib3.disable_warnings()    # Surpress "InsecureRequestWarning" warning
import Vyatta
import vyuser

class VyattaControl(object):
    """
    Provides methods to show and modify Vyatta status and configurations.
    """

    def __init__(self, urlBase, user, passwd):

        self.urlBase = urlBase
        self.urlConfBase = urlBase + 'rest/conf'
        self.urlOpBase = urlBase + 'rest/op'
        self.user = user
        self.passwd = passwd


    def getOpId(self, urlOpId):
        """
        Get the operation id, which is substring of the Location header in HTTP response.

        :param urlOpPost:
        :return:
        """

        rop = requests.post(urlOpId, auth=(self.user, self.passwd), verify=False)   # Request to get operation id
        return rop.headers['Location'].split('/')[2]    # Get Location header


    def getConfId(self):
        """
        Get the configuration id, which is substring of the Location header in HTTP response.

        :return:
        """

        rconf = requests.post(self.urlConfBase, auth=(self.user, self.passwd), verify=False)
        return rconf.headers['Location'].split('/')[2]


    def deleteConfId(self, confId):
        """
        Delete existing Vyatta configuration session

        :param confId: Configuration ID to be deleted
        :return: HTTP status code for DELETE request
        """

        urlConfDelete = self.urlConfBase + '/' + confId
        rdel = requests.delete(urlConfDelete, auth=(self.user, self.passwd), verify=False)
        return rdel.status_code


    def commandOperational(self, opCommandFileName):
        """
        Call Vyatta operational mode commands from opCommandFileName file.

        :param opCommandFileName: Input file for Vyatta operational mode commands
        :return:
        """

        with open(opCommandFileName, encoding='utf-8') as opCommandFile:
            for line in opCommandFile:

                urlOpCommand = self.urlOpBase + '/' + '/'.join(line.split(None))
                ropResult = requests.get(self.urlOpBase + '/' + self.getOpId(urlOpCommand),
                                         auth=(self.user, self.passwd),
                                         verify=False)    # Request to get the results
                print('$ ' + line)
                print(ropResult.text)


    def createEncodedUrl(self, confId, string):
        """
        URLencode every configuration words and form proper URL for REST API requests.

        :param confId: Configuration session ID
        :param string: One line Vyatta configuration commands and parameters
        :return: Encoded URL for Vyatta REST API

        TODO: Fix wrong encoding when spaces in description "" in Vyatta configuration is used.
              ex. description "IPSEC to HQ". In the meantime, avoid using "" in Vyatta configuration.
        """

        encodedWord = []
        for word in string.split():
            encodedWord.append(urllib.parse.quote(word, safe=""))   # Encode each words, then make a list of words

        encodedUrl = self.urlConfBase + '/' + confId + '/' \
                     + '/'.join(' '.join(encodedWord).split(None))

        return encodedUrl


    def editConfig(self, confFileName):
        """
        Read configurations from a file and send requests to Vyatta via REST API,
        then actually modify Vyatta configuration and commit configuration changes.

        :param confFileName: A file which has the configurations to apply
        :return: HTTP status code for deleteConfId()
        """

        # Set configurations
        with open(confFileName, encoding='utf-8') as confFile:

            confId = self.getConfId()   # Get configuration ID

            for line in confFile:
                if not (re.compile("^#").match(line)
                        or re.compile("^$").match(line)):      # Skip line matches with "^#" or "^$"
                    urlConfPut = self.createEncodedUrl(confId, line)

                    rconf = requests.put(urlConfPut,
                                         auth=(self.user, self.passwd),
                                         verify=False)   # Request for configuration commands

                    print("%s : %s" % (urlConfPut, rconf.status_code))

        # Commit configurations
        self.commitConfig(confId)

        # Delete conf-id and return HTTP status code
        return self.deleteConfId(confId)


    def commitConfig(self, confId):
        """
        Commit configuration changes

        :param confId: Configuration session ID
        :return: HTTP status code
        """

        urlConfCommit = self.urlConfBase + '/' + confId + '/commit'
        rconf = requests.post(urlConfCommit, auth=(self.user, self.passwd), verify=False)   # Request for commit
        print("%s : %s" % (urlConfCommit, rconf.status_code))
        return rconf.status_code


    def saveConfig(self, confId):
        """
        To be implemented

        :param confId:
        :return:
        """

        pass


    def revertConfig(self, confId):
        """
        To be implemented

        :param confId:
        :return:
        """

        pass


if __name__ == "__main__":
    # Test code
    pass
```


## 実行スクリプトの例

### 実行スクリプト

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

import Vyatta
import vyuser

user = vyuser.VYUSER
passwd = vyuser.VYPASSWD
urlBase = vyuser.URLBASE

vy = Vyatta.VyattaControl(urlBase, user, passwd)
vy.editConfig('vy_configuration.conf')
vy.commandOperational('vy_op_command.conf')
```

### Vyattaユーザー情報ファイル

```text:vyuser.conf
VYUSER = "vyatta"
VYPASSWD = "<vyatta password>"
URLBASE = "https://10.x.x.x/"
```

### configの定義ファイル

変更するVyattaのconfigが記載されたファイル。
設定ファイルをテンプレート化して、アドレス等のパラメーターを与えると、構成パターンに応じたconfigを自動生成するような仕組みがほしいところですが、今のところは変更するconfigを全て手で個別に作成します。
以下は、BYOIPのためにGRE over IPsecを確立する設定のconfigの例です。

```text:vy_configuration.conf
# Comments start with #

# BYOIP subnet
set interfaces bonding bond0 address 192.168.2.1/24

# GRE settings
set interfaces tunnel tun0 address 192.168.10.1/30
set interfaces tunnel tun0 encapsulation gre
set interfaces tunnel tun0 multicast enable
set interfaces tunnel tun0 local-ip 111.111.111.111
set interfaces tunnel tun0 remote-ip 222.222.222.222

# IPsec ESP policy settings
set vpn ipsec esp-group ESP1 lifetime 3600
set vpn ipsec esp-group ESP1 mode tunnel
set vpn ipsec esp-group ESP1 pfs enable
set vpn ipsec esp-group ESP1 proposal 1 encryption aes128
set vpn ipsec esp-group ESP1 proposal 1 hash sha1

# IPsec IKE policy settings
set vpn ipsec ike-group IKE1 lifetime 86400
set vpn ipsec ike-group IKE1 proposal 1 dh-group 5
set vpn ipsec ike-group IKE1 proposal 1 encryption aes128
set vpn ipsec ike-group IKE1 proposal 1 hash sha1

# IPsec interface settings
set vpn ipsec ipsec-interfaces interface bond1

# IPsec VPN settings
set vpn ipsec site-to-site peer 222.222.222.222 authentication mode pre-shared-secret
set vpn ipsec site-to-site peer 222.222.222.222 authentication pre-shared-secret test_key_1
set vpn ipsec site-to-site peer 222.222.222.222 default-esp-group ESP1
set vpn ipsec site-to-site peer 222.222.222.222 ike-group IKE1
set vpn ipsec site-to-site peer 222.222.222.222 local-address 111.111.111.111
set vpn ipsec site-to-site peer 222.222.222.222 tunnel 1 protocol gre

# Static Route
set protocols static route 192.168.1.0/24 next-hop 192.168.10.2
```

### 実行するOperational Modeコマンドの定義ファイル

configを確認したりステータスを確認するために、Operational Modeのコマンドもファイルから入力して実行できます。

```text:vy_op_command.conf
show configuration
show interfaces
show vpn ipsec sa
```