SoftLayer CLIを拡張する: Virtual Serverの稼働ホストをsl vs listで表示する

SoftLayerは[SoftLayer CLI](http://softlayer-python.readthedocs.org/en/latest/cli.html)と呼ばれるPythonベースのコマンドラインインタフェースを提供しています。
サーバーのリストを参照したり、サーバーをオーダーしたり、一部の設定を変更したりといった操作がコマンドラインから実行できるようになっています。わざわざPortalを開いて操作しなくてもよいので、ちょっとした状況の確認や、簡単な処理の自動化をするために使うことができます。

複雑な処理を実行したい場合は、PythonなどでAPIを使って一からプログラムを書くのがよいですが、CLIでほぼやりたいことができるけど、少しだけ情報を追加したい場合など、既存のCLIを少し拡張するだけ十分な場合もあるでしょう。

ここでは、Virtual Serverの稼働ホストをsl vs listコマンドで表示させるようにCLIを拡張してみます。

Virtual Serverの稼働ホストを取得する方法は[ShinobiLayer: Virtual Serverの作成者や稼動ホストのRack/Slot情報を確認するスクリプト](http://qiita.com/testnin2/items/35d269daf7c6966624f7)を参考にしています。

## CLIのインストール場所

CLIのファイルは、以下のパスのCLIというディレクトリにインストールされています。

| OS | Path |
|---|---|
| Mac OS X | /Library/Python/2.7/site-packages/SoftLayer |
| Linux | /usr/lib/python2.7/site-packages/SoftLayer |
| Windows | ?? |


## モジュール

CLIのファイルは、CLIコマンドの\<module\>の単位で分かれています。

```bash
$ sl <module> [<args>...]
```

該当のファイルを変更することで、そのmoduleを使ったコマンドの処理を変更することができます。
vs.pyを変更すれば```sl vs```コマンドの処理を変更できますし、server.pyを変更すれば```sl server```の処理を変更できます。

```bash
$ ls -l CLI/modules/*.py
-rw-r--r--  1 root  wheel    356 Jan  4 23:12 CLI/modules/__init__.py
-rw-r--r--  1 root  wheel   5679 Jan  4 23:12 CLI/modules/cdn.py
-rw-r--r--  1 root  wheel   6459 Jan  4 23:12 CLI/modules/config.py
-rw-r--r--  1 root  wheel  10400 Jan  4 23:12 CLI/modules/dns.py
-rw-r--r--  1 root  wheel    925 Jan  4 23:12 CLI/modules/filters.py
-rw-r--r--  1 root  wheel  14813 Jan  4 23:12 CLI/modules/firewall.py
-rw-r--r--  1 root  wheel   5193 Jan  4 23:12 CLI/modules/globalip.py
-rw-r--r--  1 root  wheel    908 Jan  4 23:12 CLI/modules/help.py
-rw-r--r--  1 root  wheel   5913 Jan  4 23:12 CLI/modules/image.py
-rw-r--r--  1 root  wheel   6057 Jan  4 23:12 CLI/modules/iscsi.py
-rw-r--r--  1 root  wheel  20026 Jan  4 23:12 CLI/modules/loadbal.py
-rw-r--r--  1 root  wheel  16126 Jan  4 23:12 CLI/modules/messaging.py
-rw-r--r--  1 root  wheel   6261 Jan  4 23:12 CLI/modules/metadata.py
-rw-r--r--  1 root  wheel   1484 Jan  4 23:12 CLI/modules/nas.py
-rw-r--r--  1 root  wheel   3376 Jan  4 23:12 CLI/modules/rwhois.py
-rw-r--r--  1 root  wheel  39683 Jan  4 23:12 CLI/modules/server.py
-rw-r--r--  1 root  wheel   4582 Jan  4 23:12 CLI/modules/snapshot.py
-rw-r--r--  1 root  wheel   4757 Jan  4 23:12 CLI/modules/sshkey.py
-rw-r--r--  1 root  wheel   5185 Jan  4 23:12 CLI/modules/ssl.py
-rw-r--r--  1 root  wheel   9760 Jan  4 23:12 CLI/modules/subnet.py
-rw-r--r--  1 root  wheel   1240 Jan  4 23:12 CLI/modules/summary.py
-rw-r--r--  1 root  wheel   8155 Jan  4 23:12 CLI/modules/ticket.py
-rw-r--r--  1 root  wheel   5015 Jan  4 23:12 CLI/modules/vlan.py
-rw-r--r--  1 root  wheel  42378 Jan 17 23:43 CLI/modules/vs.py
```


## Manager

SoftLayer CLIは、APIを抽象化して使いやすくした[Manager](https://softlayer-api-python-client.readthedocs.org/en/latest/api/client/#managers)と呼ばれるクラスを使用して実装されています。

Managerで取得できない情報は当然CLIでも利用できません。そのような場合には、Managerでデータを取得している部分を少し変更すれば対応できます。具体的には、Managerの処理の中で必要な[Object Mask](http://sldn.softlayer.com/article/Object-Masks)を追加して、Reference Propertyが取得できるようにしてあげます。


## utils.py

様々な共通の処理を簡単に実行できるように、共通の関数やクラスが定義されています。
ひとまず、深くネストされたdictionaryデータを簡単に検索するのに使われるlookup()を理解しておけばよいでしょう。

```rb:utils.py
def lookup(dic, key, *keys):
    """A generic dictionary access helper.

    This helps simplify code that uses heavily nested dictionaries. It will
    return None if any of the keys in *keys do not exist.

    ::

        >>> lookup({'this': {'is': 'nested'}}, 'this', 'is')
        nested

        >>> lookup({}, 'this', 'is')
        None
```

例えば以下の処理では、guestというVirtual_Guestオブジェクトのインスタンスに対して、Object Maskで取得されるRelational PropertiesをbillingItems -> orderItem -> order -> userRecord -> username という順にたどって、最終的にユーザー名を取得しています。

```CLI/modules/vs.py
utils.lookup(guest, 'billingItem', 'orderItem', 'order', 'userRecord', 'username')
```


## 変更部分

### managers/vs.py

- VSmangerのlist_instance()で、location.pathStringをObject Maskに追加

```diff:managers/vs.py
$ diff -U 10 managers/vs.py.orig managers/vs.py
--- managers/vs.py.orig	2015-01-17 23:41:45.000000000 +0900
+++ managers/vs.py	2015-01-17 23:42:52.000000000 +0900
@@ -81,21 +81,22 @@
                 'fullyQualifiedDomainName',
                 'primaryBackendIpAddress',
                 'primaryIpAddress',
                 'lastKnownPowerState.name',
                 'powerState',
                 'maxCpu',
                 'maxMemory',
                 'datacenter',
                 'activeTransaction.transactionStatus[friendlyName,name]',
                 'status',
-                'billingItem.orderItem.order.userRecord[username]'
+                'billingItem.orderItem.order.userRecord[username]',
+                'location.pathString'
             ]
             kwargs['mask'] = "mask[%s]" % ','.join(items)

         call = 'getVirtualGuests'
         if not all([hourly, monthly]):
             if hourly:
                 call = 'getHourlyVirtualGuests'
             elif monthly:
                 call = 'getMonthlyVirtualGuests'
```

### CLI/modules/vs.py

- 表のヘッダにlocationフィールドを追加
- lookup()でlocation.pathStringを取得
 
```diff:CLI/modules/vs.py
$ diff -U 10 CLI/modules/vs.py.orig CLI/modules/vs.py
--- CLI/modules/vs.py.orig	2015-01-04 23:12:47.000000000 +0900
+++ CLI/modules/vs.py	2015-01-18 00:58:01.000000000 +0900
@@ -87,34 +87,35 @@
                                     domain=args.get('--domain'),
                                     cpus=args.get('--cpu'),
                                     memory=args.get('--memory'),
                                     datacenter=args.get('--datacenter'),
                                     nic_speed=args.get('--network'),
                                     tags=tags)

         table = formatting.Table([
             'id', 'datacenter', 'host',
             'cores', 'memory', 'primary_ip',
-            'backend_ip', 'active_transaction', 'owner'
+            'backend_ip', 'location', 'active_transaction', 'owner'
         ])
         table.sortby = args.get('--sortby') or 'host'

         for guest in guests:
             guest = utils.NestedDict(guest)
             table.add_row([
                 guest['id'],
                 guest['datacenter']['name'] or formatting.blank(),
                 guest['fullyQualifiedDomainName'],
                 guest['maxCpu'],
                 formatting.mb_to_gb(guest['maxMemory']),
                 guest['primaryIpAddress'] or formatting.blank(),
                 guest['primaryBackendIpAddress'] or formatting.blank(),
+                utils.lookup(guest, 'location', 'pathString') or formatting.blank(),
                 formatting.active_txn(guest),
                 utils.lookup(guest, 'billingItem', 'orderItem', 'order',
                              'userRecord', 'username') or formatting.blank(),
             ])

         return table


 class VSDetails(environment.CLIRunnable):
     """
```


## 実行結果

```bash
$ sl vs list --sortby=location
:.........:............:............................:.......:........:.................:...............:.......................:....................:.....................:
:    id   : datacenter :             host           : cores : memory :    primary_ip   :   backend_ip  :        location       : active_transaction :        owner        :
:.........:............:............................:.......:........:.................:...............:.......................:....................:.....................:
: 6954216 :   hkg02    :      vyattama01.ibm.com    :   1   :   1G   :   119.81.xxx.x  :  10.110.2.210 :    hkg02.rk12.sl01    :         -          :   aaaa@jp.ibm.com   :
: 6030072 :   sng01    :    idera01.softlayer.com   :   1   :   2G   :   119.81.xx.xx  :  10.64.234.87 :    sng01.rk238.sl08   :         -          :  bbbbbbb@jp.ibm.com :
: 7688858 :   sng01    :    centos1.softlayer.com   :   1   :   1G   :  119.81.xxx.xxx :  10.64.234.79 :    sng01.rk238.sl17   :         -          :  bbbbbbb@jp.ibm.com :
: 7629262 :   tok02    :     tok01.softlayer.com    :   1   :   2G   :  161.202.xx.xx  :  10.132.2.216 : tok02.sr01.rk122.sl17 :         -          :  ccccccc@jp.ibm.com :
: 7642180 :   tok02    :    mywin01.softlayer.com   :   1   :   2G   :  161.202.xx.xx  :  10.132.2.213 : tok02.sr01.rk122.sl22 :         -          :  ccccccc@jp.ibm.com :
: 7684248 :   tok02    : docker-centos7.example.com :   1   :   1G   :  161.202.xx.xx  :  10.132.2.214 : tok02.sr01.rk126.sl21 :         -          : dddddddd@jp.ibm.com :
:.........:............:.............................:.......:........:.................:...............:.......................:....................:.....................:
```


簡単ですね。

データのRelational Propertyの関連がわかっていれば、同じように必要なObject Maskを追加して、lookup()で必要なデータを取得して表示することができます。

## Relational Propertyの辿り方

今回の場合は、

- VSmanger内部で[AccountサービスオブジェクトのgetVirtualGuests()メソッド](http://sldn.softlayer.com/reference/services/SoftLayer_Account/getVirtualGuests)を呼び出して、[Virtual_Guestオブジェクト](http://sldn.softlayer.com/reference/datatypes/SoftLayer_Virtual_Guest)のリストを取得
- Virtual_Guestには、Relational Propertyとしてlocationプロパティ(Location)が関連付けられている
- さらにそのLocationオブジェクトにはpathStringがRelational Propertyとして定義されている(pathStringは稼働ホストの情報を表す)

という形で、データが関連付けられています。
この関連付けは、[Data Types](http://sldn.softlayer.com/reference/datatypes/SoftLayer_Account)からオブジェクトを検索してRelational Propertyを辿っていくことで確認できます。

Relational Propertyについては[ShinobiLayer: SoftLayer API 次の一歩: データ型オブジェクト(2) - Qiita](http://qiita.com/testnin2/items/6be9b87571a99e01002a#step3---%E7%89%B9%E5%AE%9A%E3%81%AElocal-property%E3%82%84relational-propertycount-property%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B)が詳しいので、合わせて参照ください。