SoftLayerとNetScalerでAuto Scale

SoftLayerではCitrix NetScalerという高機能な負荷分散装置が利用できます。
ただし現時点では、このNetScalerは、SoftLayerのAuto Scaleと連携した負荷分散には対応しておらず、Auto Scaleで追加された仮想サーバーを、自動的にNetScalerの負荷分散対象にすることができません。

そこで、以下のような機能を実現するPythonスクリプトを作成してみました。このスクリプトを使うと、SoftLayerのAuto ScaleとNetScalerを連携させることができます。

- SoftLayerのAuto Scaleで追加されたサーバーを、自動的にNetScalerの負荷分散クラスタに追加する
- SoftLayerのAuto Scaleで削除されたサーバーを、自動的にNetScalerの負荷分散クラスタから削除する

このスクリプトでは、SoftLayerの特定のAuto Scale Group(nsAutoScaleGroup)のサーバーが、SoftLayer上の仮想サーバーとして存在しているかどうかで、負荷分散対象とするかどうかを判断しています。実サーバーがHTTP等のサービスを提供しているかどうかはチェックしていませんが、負荷分散対象サーバーとして追加されても、NetScalerからのヘルスチェックで確認されない限りパケット転送対象とはならないので、HTTP等のサービスが起動していない状態であっても、ユーザーが利用するサービス自体には問題はありません。

あと一応動きますがかなりやっつけなので、もっと良い方法があるはず。エラー処理などもほとんどしてません。そのあたりは気が向いたらそのうち対応するかも。。

## 変更

- 2015/02/11
 - 存在しないAuto Scale Groupを指定した場合のエラーハンドリングを追加
 - nitroの例外処理を追加

## 前提

### 前提ソフトウェア

- Python 2.7
- SoftLayer API Python Client 3.3.0
- nsnitro 1.0.31

#### Python 2.7

Python 3.3だとnsnitroがエラーになる模様

```bash
$ python3.3 ns_test01.py
Traceback (most recent call last):
  File "ns_test01.py", line 12, in <module>
    from nsnitro import *
  File "/Library/Python/2.7/site-packages/nsnitro/nsnitro.py", line 67
    except urllib2.URLError, ue:
```

#### SoftLayer API

GitHub版を使用

```
$ sudo pip install git+git://github.com/softlayer/softlayer-python.git
```

#### NITRO API

このスクリプトはNetScaler NITRO APIというAPIを使用して、NetScalerを操作しています。
NITROは、RESTベースのインターフェースを提供しているAPIで、リモートからNetScalerを様々なプログラム言語で操作したり監視したりすることができます。
今回はPythonでスクリプトを作成するので、nsnitroというPythonのパッケージをインストールしておきます。

```bash
$ sudo pip install nsnitro
```

- [NITRO API - Citrix eDocs](http://support.citrix.com/proddocs/topic/netscaler-main-api-10-map/ns-nitro-wrapper-con.html)
- [nsnitro 1.0.31](https://pypi.python.org/pypi/nsnitro)
- [GitHub: Citrix Netscaler 9.2+ Python Library (NITRO API)](https://github.com/favoretti/nsnitro)
- [API](http://blogs.citrix.com/2014/09/19/scripting-automating-netscaler-configurations-using-nitro-rest-api-and-powershell-part-1/)


## スクリプト

### 変数の設定

|   変数   |     説明     |
|-------------|--------------|
| nsAutoScaleGroup | NetScalerの負荷分散対象とするSoftLayerのAuto Scale Group名 |
| nsHost | NetScaler's IP address |
| nsApiUser | NetScaler login user |
| nsApiPass | NetScaler login password |
| nsLBVServer | NetScaler Load Balancing Virtual Server |
| nsIpType | SoftLayerの仮想サーバーのPublicとPrivateのどちらのアドレスを負荷分散対象とするかを指定。"public"の場合はPublic Primary IPが、それ以外の場合はPrivate Primary IPが使われる。|


### スクリプト

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

import sys
import traceback
from prettytable import PrettyTable
import SoftLayer
import sluser

# import the necessary libraries for netscaler nitro
import json
import urllib
import httplib2
import time
from nsnitro import *

SL_USERNAME = sluser.SL_USERNAME
SL_API_KEY = sluser.SL_API_KEY

# NetScaler constant
nsAutoScaleGroup = "TK-as-group"    # SoftLayer auto scale group name
nsHost = "192.168.1.141"            # NetScaler's IP address
nsApiUser = "nsroot"                # NetScaler login user
nsApiPass = "nsroot"                # NetScaler login password
nsLBVServer = "vserver01_http"      # NetScaler Load Balancing Virtual Server
nsIpType = "public"                 # IP type used for NetScaler server ip. "public" or "private"

def getASMembers(autoScaleGroups, nsAutoScaleGroup):
    """
    Get an array of dictionaries of all members of nsAutoScaleGroup from SoftLayer API

    :param autoScaleGroups: SoftLayer auto scale groups objects retrieved by getScaleGroups()
    :param nsAutoScaleGroup: SoftLayer auto scale group name which the script looks at to add/delete servers
    :return: Array of dictionaries of all auto scale members
    """
    for asg in autoScaleGroups:
        asMembers = []
        if asg['name'] == nsAutoScaleGroup:
            for a in asg['virtualGuestMembers'][0:]:
                memberId = a['virtualGuest']['id']
                asMember = {
                    "id": a['virtualGuest']['id'],
                    "name": a['virtualGuest']['hostname'],
                    "publicPrimaryIP" : client['Virtual_Guest'].getPrimaryIpAddress(id=memberId),
                    "privatePrimaryIP" : client['Virtual_Guest'].getPrimaryBackendIpAddress(id=memberId),
                }
                asMembers.append(asMember)
            break

    if len(asMembers) == 0:
        sys.exit("No such auto scale group exists in SoftLayer auto scale configurations.")

    return(asMembers)

def addNSServer(nsServerName, nsServerIp, nsServiceName, nsLBVServer):
    """
    Add SoftLayer VSIs to NetScaler servers,
    create new services and bind the services to LB virtual server.
    Must be logged in to NetScaler with API beforehand.

    :param nsServerName: The name of NetScaler server to be created.
    :param nsServerIp: The IP address of NetScaler server to be created.
    :param nsServiceName: The name of NetScaler service to be created.
    :param nsLBVServer: The name of NetScaler LB virtual server to be binded with the service.
    :return:
    """

    # add server test
    addserver = NSServer()
    addserver.set_name(nsServerName)
    addserver.set_ipaddress(nsServerIp)
    NSServer.add(nitro, addserver)

    # get state
    server = NSServer()
    server.set_name(nsServerName)
    server = server.get(nitro, server)
    # print(server.get_name() + ": " + server.get_state())

    # add service
    addservice = NSService()
    addservice.set_name(nsServiceName)
    addservice.set_servername(nsServerName)
    addservice.set_servicetype("HTTP")
    addservice.set_port(80)
    NSService.add(nitro, addservice)

    # bind service to lbvserver
    lbbinding = NSLBVServerServiceBinding()
    lbbinding.set_name(nsLBVServer)
    lbbinding.set_servicename(nsServiceName)
    lbbinding.set_weight(40)
    NSLBVServerServiceBinding.add(nitro, lbbinding)

    # get binding info
    lbbinding = NSLBVServerServiceBinding()
    lbbinding.set_name(nsLBVServer)
    lbbindings = NSLBVServerServiceBinding.get(nitro, lbbinding)
#    for lbb in lbbindings:
#        print("sgn: " + lbb.get_servicegroupname())
    return

def delNSServer(nsServerName):
    """
    Delete NetScaler server.
    Must be logged in to NetScaler with API beforehand.

    :param nsServerName: The name of NetScaler server to be deleted.
    :return:
    """

    # delete server
    delserver = NSServer()
    delserver.set_name(nsServerName)
    NSServer.delete(nitro, delserver)
    # print("Server deleted.")

    return

def getNsServersList(nitro):
    """
    Get the list of NetScaler servers

    :param nitro: nitro connection
    :return: Array of NetScaler servers
    """
    nsServers = []
    server = NSServer()
    for n in NSServer.get_all(nitro):
        nsServers.append(n.get_name())
    return nsServers


def getAutoScaleHostnamePrefix(autoScaleGroups, nsAutoScaleGroup):
    """
    Get auto scale hostname prefix

    :param autoScaleGroups:
    :param nsAutoScaleGroup:
    :return:
    """
    for asg in autoScaleGroups:
        if asg['name'] == nsAutoScaleGroup:
            nsAutoScalePrefix = asg['virtualGuestMemberTemplate']['hostname']
    return nsAutoScalePrefix



try:

    # Connect to SoftLayer API
    client = SoftLayer.Client(username=SL_USERNAME, api_key=SL_API_KEY)
    autoScaleGroups = client['Account'].getScaleGroups()
    asMembers = getASMembers(autoScaleGroups, nsAutoScaleGroup)

    # get auto scale hostname prefix
    nsAutoScalePrefix = getAutoScaleHostnamePrefix(autoScaleGroups, nsAutoScaleGroup)

    # Login to NetScaler with API
    nitro = NSNitro(nsHost, nsApiUser, nsApiPass)
    nitro.login()

    # Get the list of NetScaler servers
    nsServers = getNsServersList(nitro)

    # If an auto scale member is not in NetScaler severs,
    # add the member to NetScaler server
    asMemberNames = []
    addedServers = []
    for asm in asMembers:
        # NetScaler parameters
        nsServerName = asm['name']
        nsPublicPrimaryIp = asm['publicPrimaryIP']
        nsPrivatePrimaryIp = asm['privatePrimaryIP']
        if nsIpType == "public":
            nsServerIp = nsPublicPrimaryIp
        else:
            nsServerIp = nsPrivatePrimaryIp
        nsServiceName = "service_http_" + nsServerIp.split(".")[-1]   # NetScaler Service Name for the server. "service_http_<IP's 4th octet>"

        if not asm['name'] in nsServers:
            addNSServer(nsServerName, nsServerIp, nsServiceName, nsLBVServer)
            addedServers.append(asm['name'])
            # print("Server \"%s\" created." % asm['name'])

        # make array of asm['name']
        asMemberNames.append(asm['name'])


    # If a NetScaler server with nsAutoPrefix prefix exists while not in auto scale member,
    # delete the server from NetScaler servers
    deletedServers = []
    for nss in nsServers:
        if nss[:-5] == nsAutoScalePrefix:
            if not nss in asMemberNames:
                delNSServer(nss)
                deletedServers.append(nss)
                # print("Server \"%s\" deleted." % nss)

    # Logout from NetScaler
    nitro.logout()

    # Print results
    print("No servers created.") if len(addedServers) == 0 else "Created: %s" % addedServers
    print("No servers deleted.") if len(deletedServers) == 0 else "Deleted: %s" % deletedServers


except NSNitroError, e:
    print e.message

except SystemExit as e:
    sys.exit(e)
```



## テスト

### 負荷分散対象サーバーとして登録されいない & 存在しないサーバーが負荷分散対象になっている場合

#### 実行前
##### SoftLayer上のサーバー

- Auto Scale Group "TK-as-group"の仮想サーバーとして、"TK-as-member-e727"のみ存在 

```bash
$ python sl_getAutoScaleGroup.py
+-------+------------------+-------+-----------+-----+-----+-----+---------------+---------+
|   id  |     asg name     |   dc  |   status  | min | max | now | member prefix | members |
+-------+------------------+-------+-----------+-----+-----+-----+---------------+---------+
| 54654 | autoscale_grp01  | hkg02 | SUSPENDED |  1  |  5  |  0  |    asserver   |         |
| 34158 |   TK-as-group    | sng01 |   ACTIVE  |  1  |  5  |  1  |  TK-as-member |  e727   |
| 34758 | autosacle_yasuda | sng01 | SUSPENDED |  1  |  1  |  0  |    sh1nobu    |         |
|  8254 |   dai39-group    | sng01 |   ACTIVE  |  1  |  1  |  1  |  dai39-scale  |  cca8   |
+-------+------------------+-------+-----------+-----+-----+-----+---------------+---------+
```

##### NetScaler上の負荷分散設定

- "TK-as-member-e727"は負荷分散対象になっていない (serverに存在しない)
- "TK-as-member-1111", "TK-as-member-2222"が負荷分散対象になっている (serverに存在し、serviceが作成され、lb vserverにbindされている)

```bash
> show server -summary
-------------------------------------------
      Name                 State
-------------------------------------------
1     192.168.1.254        ENABLED
2     192.168.1.253        ENABLED
3     192.168.1.134        ENABLED
4     192.168.1.136        ENABLED
5     192.168.1.144        ENABLED
6     192.168.1.18         ENABLED
7     192.168.1.143        ENABLED
8     192.168.1.133        ENABLED
9     TK-as-member-1111    ENABLED
10    TK-as-member-2222    ENABLED
 Done
> 
> show service -summary
---------------------------------------------------------------------------------------------
      Name        State           IP Addr           Port  Protocol   MaxClients  MaxReqs
---------------------------------------------------------------------------------------------
1     serv...adns UP              192.168.1.144     53    ADNS       0           0
2     serv..._254 DOWN            192.168.1.254     80    HTTP       0           0
3     serv...p_18 DOWN            192.168.1.134     80    HTTP       0           0
4     serv...tp_1 DOWN            TK-as-member-1111 80    HTTP       0           0
5     serv...tp_2 DOWN            TK-as-member-2222 80    HTTP       0           0
 Done
> 
> show lb vserver vserver01_http
	vserver01_http (192.168.1.143:80) - HTTP	Type: ADDRESS
	State: DOWN
	Last state change was at Fri Jan 30 15:35:40 2015
	Time since last state change: 1 days, 18:18:09.320
	Effective State: DOWN
	Client Idle Timeout: 180 sec
	Down state flush: ENABLED
	Disable Primary Vserver On Down : DISABLED
	Appflow logging: ENABLED
	Port Rewrite : DISABLED
	No. of Bound Services :  3 (Total) 	 0 (Active)
	Configured Method: LEASTCONNECTION
	Mode: IP
	Persistence: NONE
	Vserver IP and Port insertion: OFF
	Push: DISABLED	Push VServer:
	Push Multi Clients: NO
	Push Label Rule: none
	L2Conn: OFF
	Skip Persistency: None
	IcmpResponse: PASSIVE
	RHIstate: PASSIVE
	New Service Startup Request Rate: 0 PER_SECOND, Increment Interval: 0
	Mac mode Retain Vlan: DISABLED
	DBS_LB: DISABLED
	Process Local: DISABLED
	Traffic Domain: 0

1) service_http_18 (192.168.1.134: 80) - HTTP State: DOWN	Weight: 1
2) service_http_1 (1.1.1.1: 80) - HTTP State: DOWN	Weight: 1
3) service_http_2 (2.2.2.2: 80) - HTTP State: DOWN	Weight: 1
 Done
```

#### 実行

```
$ python sl_lbNetScaler.py
Created: ['TK-as-member-e727']
Deleted: [u'TK-as-member-1111', u'TK-as-member-2222']
```

#### 実行結果

- Auto Scale Groupに存在しない"TK-as-member-1111", "TK-as-member-2222"は、NetScalerの負荷分散対象から削除 (serverに存在しない)
- Auto Scale Groupに存在する"TK-as-member-e727"が、負荷分散対象に追加 (serverに追加され、serviceが作成され、lb vserverにbindされている)


```bash
> show server -summary
-------------------------------------------
      Name                 State
-------------------------------------------
1     192.168.1.254        ENABLED
2     192.168.1.253        ENABLED
3     192.168.1.134        ENABLED
4     192.168.1.136        ENABLED
5     192.168.1.144        ENABLED
6     192.168.1.18         ENABLED
7     192.168.1.143        ENABLED
8     192.168.1.133        ENABLED
9     TK-as-member-e727    ENABLED
 Done
>
> show service -summary
---------------------------------------------------------------------------------------------
      Name        State           IP Addr           Port  Protocol   MaxClients  MaxReqs
---------------------------------------------------------------------------------------------
1     serv...adns UP              192.168.1.144     53    ADNS       0           0
2     serv..._254 DOWN            192.168.1.254     80    HTTP       0           0
3     serv...p_18 DOWN            192.168.1.134     80    HTTP       0           0
4     serv..._198 DOWN            TK-as-member-e727 80    HTTP       0           0
 Done
>
> show lb vserver vserver01_http
	vserver01_http (192.168.1.143:80) - HTTP	Type: ADDRESS
	State: DOWN
	Last state change was at Fri Jan 30 15:35:42 2015
	Time since last state change: 1 days, 18:23:39.540
	Effective State: DOWN
	Client Idle Timeout: 180 sec
	Down state flush: ENABLED
	Disable Primary Vserver On Down : DISABLED
	Appflow logging: ENABLED
	Port Rewrite : DISABLED
	No. of Bound Services :  2 (Total) 	 0 (Active)
	Configured Method: LEASTCONNECTION
	Mode: IP
	Persistence: NONE
	Vserver IP and Port insertion: OFF
	Push: DISABLED	Push VServer:
	Push Multi Clients: NO
	Push Label Rule: none
	L2Conn: OFF
	Skip Persistency: None
	IcmpResponse: PASSIVE
	RHIstate: PASSIVE
	New Service Startup Request Rate: 0 PER_SECOND, Increment Interval: 0
	Mac mode Retain Vlan: DISABLED
	DBS_LB: DISABLED
	Process Local: DISABLED
	Traffic Domain: 0

1) service_http_18 (192.168.1.134: 80) - HTTP State: DOWN	Weight: 1
2) service_http_198 (119.81.110.198: 80) - HTTP State: DOWN	Weight: 40
 Done
```

### 既に負荷分散対象サーバーとして登録されている場合

#### 実行前

##### NetScaler上の負荷分散設定

- Auto Scale Groupに存在する"TK-as-member-e727"のみが負荷分散対象となっている

```
> show server -summary
-------------------------------------------
      Name                 State
-------------------------------------------
1     192.168.1.254        ENABLED
2     192.168.1.253        ENABLED
3     192.168.1.134        ENABLED
4     192.168.1.136        ENABLED
5     192.168.1.144        ENABLED
6     192.168.1.18         ENABLED
7     192.168.1.143        ENABLED
8     192.168.1.133        ENABLED
9     TK-as-member-e727    ENABLED
 Done
```

#### 実行

- 実行しても負荷分散対象サーバーの追加・削除はされない

```
$ python sl_lbNetScaler.py
No servers created.
No servers deleted.
```