Spectrum Virtualize RESTful API

Spectrum Virtualize Representational State Transfer (REST) モデルのアプリケーション・プログラミング・インターフェース (API) は、システム情報の取得と、システム・リソースの作成、変更、および削除に使用されるコマンド・ターゲットで構成されています。これらのコマンド・ターゲットにより、コマンド・パラメーターは、編集せずに Spectrum Virtualize コマンド・ライン・インターフェースにパススルーできるようになります。このコマンド・ライン・インターフェースでは、パラメーター指定の構文解析を処理して、妥当性検査およびエラー・レポートを行います。Hypertext Transfer Protocol Secure (HTTPS) を使用して、RESTful API サーバーと正常に通信します。

RESTful API サーバーでは、トランスポート・セキュリティー (SSL など) を考慮しませんが、要求がローカルの保護されたサーバーから開始されるものと想定しています。HTTPS プロトコルは、データ暗号化によってプライバシーを提供します。RESTful API はコマンド認証を要求することにより追加のセキュリティーを提供します。追加のセキュリティーは、2 時間のアクティブ状態または 30 分間の非アクティブ状態 (どちらか先に発生した方) の間、継続します。

Uniform Resource Locator (URL) は、システム上の異なるノード・オブジェクトをターゲットとします。HTTPS POST メソッドは、URL に指定されたコマンド・ターゲットに作用します。詳しくは、RESTful API コマンド・ターゲットと特性を参照してください。システム上のさまざまなオブジェクトに関する情報を変更または表示するには、要求を作成してシステムに送信する必要があります。 次のセクションで説明するように、RESTful API サーバーが要求を受信してコマンドに変換するために特定のエレメントを提供する必要があります。

HTTPS 要求の作成

RESTful API を使用してシステムと対話するには、有効な構成ノード URL 宛先を指定して HTTPS コマンド要求を作成します。7443 ポートおよびキーワード restを含めます。 すべての要求について以下の URL 形式を使用します。
https://system_node_ip:7443/rest/command
ここで、
  • system_node_ip はシステム IP アドレスです。これは、システムの構成ノードによって取得されるアドレスです。
  • Spectrum Virtualize RESTful API のポート番号は常に 7443 です。
  • rest はキーワードです。
  • command は、ターゲット・コマンド・オブジェクト (任意のパラメーターを指定した auth または lseventlog など) です。
    command は、次の形式で指定します。
    command_name,method="POST",headers={'parameter_name': 'parameter_value',
    'parameter_name': 'parameter_value',...}
POST を排他的に使用する理由: Spectrum Virtualize RESTful API コマンド・ターゲットはすべて、既に Create、Read、Update、および Delete アクションを反映した名前を使用している Spectrum Virtualize コマンドに基づいて名前が付けられています。MK コマンドはリソースの作成、LS コマンドはリソースのリスト (読み取り)、CH コマンドはリソースの変更 (更新)、RM コマンドはリソースの除去 (削除) を行います。そのようなコマンドの先頭に HTTP メソッドを使用することは冗長です。

すべてのコマンド (LS コマンドを含む) は、少なくとも 1 つの名前付きパラメーターを受け入れます。Spectrum Virtualize RESTful API は POST メソッドのみであるため、URI に定位置パラメーターを付加し、名前付きパラメーターを JavaScript Object Notation (JSON) ストリングとして要求本体に入れるという規則を実施します。

GET を使用しない理由: ほとんどの HTTP サーバーは、本体データを持つ GET 要求を拒否します。GET をサポートするということは、名前付きパラメーターを照会ストリングとして URI に付加することを意味します。このような任意のデータは、照会ストリングに含めないでください。有効な URI には、空白文字およびデータ内に存在する可能性のあるその他の予約文字を含めることはできません。 したがって、名前付きパラメーター・データを URL エンコードしてから、URI に追加する必要があります。このような不便があることに加え、何らかの方法で、名前付きパラメーター・データのキー値構造を伝えることも必要です。 照会ストリングは、URL エンコードされた JSON オブジェクトである可能性があります。GET メソッドをサポートするということは、選択したプログラミング言語で表されるパラメーター・データは、HTTP 要求の一部として送信される前に 2 つの別個のエンコーダーをパススルーする必要があることを意味します。

このコマンド・ターゲットをより簡潔に使用する方法は、POST を排他的に採用することでした。

前述のように、URL およびコマンド・ターゲットの名前に加え、その他の情報が、要求行と、指定されたオブジェクトに対して実行するアクションに関する HTTP 要求の本体で必要です。要求行には、POST HTTP メソッドを指定します。要求の本体に、必須パラメーター (RAID レベルや IP アドレスなど) を指定します。

以下の例に示すように、必須パラメーターを有効な JSON として HTTP 本体に指定します。
{'X-Auth-Username': 'superuser'}
要求は、指定された宛先 (これは、システム上の構成ノードでなければなりません) のポート 7443 に経路指定されます。要求は、このポートで RESTful API サーバーによって受信されます。 サーバーはそのコマンドを実行し、結果の出力をすべて収集して、以下の例のような HTTP 応答を作成します。
HTTP/1.1 200 OK
Server: lighttpd/1.4.31
Date: date
Content-type: application/json; charset=UTF-8 
Content-length: content_length
Connection: close
{“attribute”: “value”}
API コマンド・ターゲットおよびそれらの特性を確認するには、RESTful API コマンド・ターゲットと特性を参照してください。開始方法の例については、始めにを参照してください。発生する可能性のある HTTP エラー・コードの完全なリストについては、RESTful API HTTP エラーメッセージを参照してください。

RESTful API コマンド・ターゲットと特性

表 1 は、POST メソッドでの /auth を含むすべてのコマンドについて示しています。また、ユーザーが実行する他のコマンドすべてを認証するためには、/auth コマンド・ターゲットが返した認証トークンを使用する必要があることも示します。/auth コマンド・ターゲットを除き、システム IP アドレスに対してコマンドを実行することで、それらのコマンドが構成ノードによって実行されるようになります。

表 1. POST メソッド、認証要件、および構成ノードで実行するかどうか
コマンド・ターゲット メソッド 認証が必要 構成ノード/クラスターで実行
/auth POST No No
その他のすべてのコマンド・ターゲット POST Yes Yes

表 2 は、このリリースの RESTful API で一般的に使用されるコマンドのコマンド・ターゲット名を示しています。規則により、svcinfo 実行コマンドおよび svctask 実行コマンドはデフォルトであり、RESTful API コマンド・ターゲット内でリストする必要はありません。

コマンド・ターゲットとそれぞれのパラメーターの説明、および使用頻度の低いその他のコマンドの説明は、製品資料の CLI コマンドのセクションに記載されています。

表 2. Spectrum Virtualize ソフトウェア用のサポートされる RESTful API コマンド
コマンド・ターゲット
/addhostclustermember /addhostiogrp /addhostport
/addvdiskaccess /addvdiskcopy /addvolumecopy
/auth /chhost /chnode
/chnodecanister /chrcconsistgrp /chrcrelationship
/chvdisk /expandvdisksize /lscurrentuser
/lseventlog /lsfcconsistgrp /lsfcmap
/lsfcmapcandidate /lsfcmapdependentmaps /lsfcmapprogress
/lshost /lshostcluster /lshostclustermember
/lshostclustervolumemap /lshostiogrp /lshostvdiskmap
/lsiogrp /lsiogrphost /lsmdiskgrp
/lsnode /lsnodecanister /lsnodehw
/lsnodecanisterhw /lsnodecanisterstats /lsnodecanistervpd
/lsnodehw /lsnodestats /lsnodevpd
/lspartnership /lsrcrelationshipprogress /lssystem
/lssystemip /lssystemstats /lsvdisk
/lsvdiskaccess /lsvdiskcopy /lsvdiskfcmapcopies
/lsvdiskfcmappings /lsvdiskhostmap /lsvdisksyncprogress
/mkfcconsistgrp /mkfcmap /mkfcpartnership
/mkhost /mkhostcluster /mkrcconsistgrp
/mkrcrelationship /mkvdisk /mkvdiskhostmap
/mkvolume /mkvolumehostclustermap /movevdisk
/prestartfcconsistgrp /prestartfcmap /rmfcmap
/rmhost /rmhostcluster /rmhostclustermember
/rmhostiogrp /rmhostport /rmvdisk
/rmvdiskaccess /rmvdiskcopy /rmvdiskhostmap
/rmvolume /rmvolumecopy /rmvolumehostclustermap
/startfcmap /startrcconsistgrp /startrcrelationship
/stopfcconsistgrp /stopfcmap /stoprcconsistgrp
/stoprcrelationship    

認証の概要

データ暗号化とは別に、HTTPS サーバーでは、API セッションごとに有効なユーザー名とパスワードの認証が必要です。2 つの認証ヘッダー・フィールドを使用して、資格情報 X-Auth-Username および X-Auth-Password を指定してください。

初期認証では、ユーザー名とパスワードを使用して認証ターゲット (/auth) を POST する必要があります。RESTful API サーバーは 16 進数トークンを返します。1 つのセッションは、最大 2 時間のアクティブ状態または 30 分の非アクティブ状態 (どちらか先に発生した方) の間、続きます。非アクティブ状態であるためにセッションが終了するか、割り当てられた最大時間に達した場合は、エラー・コード 403 によって権限が失われたことが示されます。/auth コマンド・ターゲットを使用して、そのユーザー名とパスワードで再認証してください。

例えば、以下のコマンドは、認証コマンドをポート 7443 でターゲット・ノード IP 192.168.10.109 に渡します。
https://192.168.10.109:7443/rest/auth, method="POST", 
   headers={'X-Auth-Username': 'superuser', 'X-Auth-Password': 'passw0rd'}
API サーバーに送信された HTTP 要求は、次のようになります。
POST /auth HTTPS/HTTPS_version 
Host: https://192.168.10.109:7443
Content-type: application/json; charset=UTF-8 
Content-length: message_size
X-Auth-Token: 58cfd6acb1676e1cba78b7cb5a9a081d11d1d1cfeb0078083ef225d9c59bf4df

{“attribute”: “value”,“attribute”: “value”}
ここで、
  • 最初の行は、POST メソッド、API ターゲット、プロトコル (HTTPS)、およびプロトコル・バージョン (1.1) が指定された要求行です。
  • 2 番目の行は、HTTP 要求をシステム上の正しいポート (7443) および IP アドレスに送信するホスト・ヘッダーです。
  • 3 番目の行は、コンテンツ・タイプ (application/json; charset=UTF-8) を指定するコンテンツ・タイプ・ヘッダーです。
  • 4 番目の行は、メッセージ・サイズが指定されたコンテンツ長ヘッダーです。
  • 5 番目の行は、認証トークンが指定された認証トークン・ヘッダーです。
  • 要求のヘッダーと本文の間にはスペースが残っています。パラメーターはいずれも、7 番目の行の JSON 内に示されます。
auth コマンドが正常に実行されると、以下の例のようなトークンが返されます。
{
"token": "58cfd6acb1676e1cba78b7cb5a9a081d11d1d1cfeb0078083ef225d9c59bf4df"
}

コマンド・ターゲット・パラメーター

コマンド・ターゲットは、システムのさまざまな部分と対話します。システム内の特定のノード (通常は構成ノード) をターゲットにした後、そのノード内のオブジェクトをターゲットにします。コマンド・ターゲットについては表 2 を参照し、指定できるパラメーターの個々のコマンドの説明については、この製品資料の CLI コマンドのセクションを参照してください。

始めに

以下の Python 3 の例は、システムとの対話とコマンドの実行を開始するための初期セットアップの実行方法を示しています。他の言語での例については、Perl での RESTful API の使用例およびcurl での使用例を参照してください。

import ssl
import json
import pprint

import urllib.request
import urllib.error
import urllib.parse

no_verify = ssl.create_default_context()
no_verify.check_hostname = False
no_verify.verify_mode = ssl.CERT_NONE

if getattr(ssl, '_https_verify_certificates', None):
    ssl._https_verify_certificates(False)

class HostString(str):
    """
        Comment: Special subclass of string, for storing arbitrary host-related
                 attributes (such as auth tokens) without losing any string behavior
    """
    def __new__(cls, *args, **kwds):
        return super(HostString, cls).__new__(cls, *args, **kwds)

class RESTUtil(object):
    show_default=False
    default_headers = {}
    port = 80

    def __init__(self, show=None, catch=True):
        self.hosts = {}
        self.curr_host = None
        self.catch=catch
        self.show_default=show if show != None else self.show_default

    @property
    def host(self):
        return self.curr_host

    @host.setter
    def host(self, hostname):
        """
            Comment: Retrieve the HostString object of a known host from its
                     host name or string definition. Even if the host definition 
                     is provided, we still need to key into self.hosts in case the 
                     client classes are storing things on their HostString objects.
        """
        try:
            if hostname in self.hosts:
                self.curr_host = self.hosts[hostname]
            else:
                self.curr_host = [h for h in self.hosts.values() if h == hostname][0]
            return self.curr_host
        except IndexError:
            raise KeyError("Unrecognized host/name %s" % hostname)

    def add_host(self, hostdef, hostname=None):
        hostname = hostname if hostname is None else hostdef
        self.hosts[hostname] = HostString(hostdef)
        if self.curr_host == None:
            self.curr_host = self.hosts[hostname]
        return hostname

    def command(self, protocol, postfix, method='POST', headers=None, show=None, **cmd_kwds):
        """
            Comment: A fairly generic RESTful API request builder. 
                     See subclasses for examples of use.
        """
        if show == None:
            show = self.show_default
        headers = {} if headers == None else headers
        url = '%s://%s:%s/%s' % (
            protocol,
            self.curr_host,
            self.port,
            postfix
        )
        request = urllib.request.Request(
            url,
            headers =dict(self.default_headers, **headers),
            data=bytes(json.dumps(cmd_kwds), encoding="utf-8") if cmd_kwds else None)
        request.get_method = lambda: method
        if show:
            self.request_pprint(request)
        try:
            cmd_out = urllib.request.urlopen(request, context=no_verify).read().decode('utf-8')
        except urllib.error.HTTPError as e:
            self.exception_pprint(e)
            if not self.catch:
                raise Exception("RESTful API command failed.")
            return
        try:
            cmd_out = json.loads(cmd_out)
        except ValueError:
            pass
        if show:
            print("¥nCommand Output:")
            pprint.pprint(cmd_out)
            print("")
        return cmd_out

    @staticmethod
    def request_pprint(request):
        """
            Comment: Request info print function 
                     (for self.command with show=True)
        """
        print(request.get_method(), request.get_full_url(), 'HTTP/1.1')
        print('Host:', request.host)
        for key, value in request.headers.items():
            print(key.upper() + ':', str(value))
        if request.data != None:
            print()
            pprint.pprint(request.data)

    @staticmethod
    def exception_pprint(http_error):
        """
            Comment: HTTPError info print function
        """
        print(http_error.code, '--', http_error.reason)
        print(http_error.fp.read())
        print("")

class SVCREST(RESTUtil):
    """
        Comment: RESTful wrapper for the SVC CLI
    """

    def __init__(self, host, *args, **kwds):
        self.debug = kwds.pop('debug', False)
        super().__init__(*args, **kwds)
        self.add_host(host)

    @property
    def default_headers(self):
        return {'X-Auth-Token': getattr(self.curr_host, 'token', 'badtoken'),
                'Content-Type': 'application/json'}

    @property
    def port(self):
        return getattr(self, '_port', None) or ('7665' if self.debug else '7443')

    @property
    def protocol(self):
        return getattr(self, '_protocol', None) or ('http' if self.debug else 'https')

    def command(self, cmd, *args, method="POST", headers=None, show=None, **cmd_kwds):
        postfix = '/'.join(
            ['rest'] + [cmd] + [urllib.parse.quote(str(a)) for a in args]
        )
        return super().command(
            self.protocol,
            postfix,
            method=method,
            headers=headers,
            show=show,
            **cmd_kwds
        )

    def authenticate(self, username='superuser', password='passw0rd', show=None):
        cmd_out = self.command(
            'auth', show=show, method="POST", headers={'X-Auth-Username': username, 'X-Auth-Password': password}
        )
        if cmd_out:
            self.curr_host.token = cmd_out['token']
    """
       Comment:  First, set your cluster ipaddress.  
          It's assumed superuser/passw0rd (6 lines above) is the crednetial.
          After the authenticate call, you can issue any command in 
                s.command('') that is an svcinfo or svctask cmmand)
   """
s = SVCREST('192.168.10.109')
s.authenticate()
print(s.command('lssystem'))