Spectrum Virtualize RESTful API

Spectrum Virtualize 具象状态传输 (REST) 模型应用程序编程接口 (API) 由用于检索系统信息以及用于创建、修改和删除系统资源的命令目标组成。这些命令目标允许命令参数未经编辑地传递到 Spectrum Virtualize 命令行界面,由该界面处理参数规范有效性的解析和错误报告。使用安全超文本传输协议 (HTTPS) 可成功与 RESTful API 服务器进行通信。

RESTful API 服务器不考虑传输安全性(例如 SSL),而是假设请求是从本地受保护服务器发起的。 HTTPS 协议通过数据加密来提供隐私。 RESTful API 通过要求进行命令认证来提供更多安全性(持续两小时活动或 30 分钟不活动,以先发生的情况为准)。

统一资源定位符 (URL) 锁定系统上的不同节点对象。HTTPS POST 方法处理 URL 中指定的命令目标。有关更多信息,请参阅RESTful API 命令目标和特征。要进行更改或查看有关系统上不同对象的信息,必须创建请求并将其发送到系统。 您需要提供特定元素以使 RESTful API 服务器接收请求并将其转换为命令,如下一节中所述。

生成 HTTPS 请求

要使用 RESTful API 与系统进行交互,请使用有效的配置节点 URL 目标生成 HTTPS 命令请求。打开 TCP 端口 7433 并包含关键字 rest对于 Amazon Web Services (AWS) 上的 IBM Spectrum Virtualize for Public Cloud,在使用 RESTful API 之前,必须手动打开 AWS 安全组中的 TCP 端口 7443。 对所有请求使用以下 URL 格式:

https://system_node_ip:7443/rest/command
其中:
  • system_node_ip 是系统 IP 地址,这是系统的配置节点所使用的地址。
  • 对于 Spectrum Virtualize RESTful API,端口号始终为 7443
  • rest 是关键字。
  • command 是目标命令对象(例如,带有任何参数的 authlseventlog)。
    command 规范遵循以下格式:
    command_name,method="POST",headers={'parameter_name': 'parameter_value',
    'parameter_name': 'parameter_value',...}
为什么专门使用 POST: 所有 Spectrum Virtualize RESTful API 命令目标都以 Spectrum Virtualize 命令命名,其中名称已经反映了“创建”、“读取”、“更新”和“删除”操作。 MK 命令生成(创建)资源,LS 命令列出(读取)资源,CH 命令更改(更新)资源,RM 命令则移除(删除)资源。 对此类命令使用 HTTP 方法是多余的。

所有命令(包括 LS 命令)都至少接受一个命名参数。 由于 Spectrum Virtualize RESTful API 只是 POST 方法,因此其实施的约定是将位置参数附加到 URI,然后将命名参数作为 JavaScript 对象表示法 (JSON) 字符串打包到请求主体中。

为什么不使用 GET: 大多数 HTTP 服务器都会拒绝带有主体数据的 GET 请求。 支持 GET 意味着将命名参数作为查询字符串附加到 URI。 请勿在查询字符串中包含此类任意数据。有效的 URI 不能包含数据中可能存在的空格和其他保留字符。因此,在将命名参数数据附加到 URI 之前,必须对其进行 URL 编码。除了该不便之处以外,必须以某种方法来传达命名参数数据的键/值结构。查询字符串可能是 URL 编码的 JSON 对象。 支持 GET 方法意味着所选编程语言中表示的参数数据必须通过两个单独的编码器,然后再作为 HTTP 请求的一部分发送数据。

命令目标的更简练方法是专门采用 POST。

如上所述,除命令目标的 URL 和名称以外,在请求行中和 HTTP 请求主体中,还需要有关对指定对象所执行操作的其他信息。在请求行中,包含 POST HTTP 方法。 在请求主体中包含任何必需参数(例如 RAID 级别或 IP 地址)。

提供任何必需参数作为 HTTP 主体中的有效 JSON,如以下示例中所示:
{'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 命令目标和特征。 要查看如何入门的示例,请参阅入门。 请参阅 RESTful API HTTP 错误消息,以获取可能遇到的 HTTP 错误代码的完整列表。

RESTful API 命令目标和特征

表 1 强调所有命令(包括 /auth)的 POST 方法。它还显示您必须使用 /auth 命令目标返回的认证令牌来认证您运行的每个其他命令。除 /auth 命令目标外,针对系统 IP 地址运行命令,以便由配置节点运行这些命令。

表 1. POST 方法、认证需求以及是否在配置节点上运行
命令目标 方法 需要认证 在配置节点/集群中运行
/auth POST
所有其他命令目标 POST
表 2 显示此发行版的 RESTful API 的常用命令的命令目标名称。 按照约定,svcinfosvctask 可执行命令为缺省值,并且无需在 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 会话的有效用户名和密码。使用两个认证头字段指定凭证:X-Auth-UsernameX-Auth-Password

初始认证需要使用用户名和密码对认证目标 (/auth) 执行 POST 操作。RESTful API 服务器会返回十六进制令牌。 单个会话最多持续两小时处于活动状态或 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) 的请求行。
  • 第二行是主机头,将 HTTP 请求定向到系统上的正确端口 (7443) 和 IP 地址。
  • 第三行是内容类型头,用于指定内容类型 (application/json; charset=UTF-8)。
  • 第四行是具有消息大小的内容长度头。
  • 第五行是具有认证令牌的认证令牌头。
  • 请求的头和主体之间留有空格。 任何参数都出现在第七行上的 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'))