其它
OpenAPI

一、OpenAPI概述

行云管家的OpenAPI(以下简称OpenAPI或API),在SaaS和私有部署上均可使用。API提供的方法和调用方式都是一致的,区别是client使用的AK和endpoint不同。

  • a) SaaS环境的endpoint是:https://openapi.cloudbility.com,有需要通过OpenAPI集成行云管家功能的,请您发送邮件到 support@cloudbility.com,邮件内容请填写:“公司名称” 、“联系人”、“手机号码” 、 “团队名称”、“用户名称”、“API用途”,行云管家SaaS运营人员审核通过后,会以邮件发送AK予您;

  • b) 私有部署的endpoint,可以在管理控制台的菜单:系统设置->OpenAPI中查看,点击“启用API”之后会生成所需的AK。

API以http RESTful的方式提供,API调用的认证方式有两种:

  • a) 使用AK生成签名;

  • b) 使用Token。

API有一个可以在线调试的页面,即不用写一行代码,也可以完成大部分的API调用。SAAS环境的API的文档页面在https://openapi.cloudbility.com/doc/index.html,也是API调试页面。

私有部署可以通过管理控制台的菜单:系统设置->OpenAPI 查看。在线调试,只能使用Token的方式进行认证。

API的调用的payload参数和返回结果,都是基于JSON的,涉及到日期的字段,都是采用格式:"yyyy-MM-dd'T'HH:mm:ss'Z'",采用UTC时区。

二、OpenAPI调用

2.1、名词解释

ISV:Independent Software Vendors,独立软件开发商,第三方软件提供商,即使用Open API与行云管家交互,以实现导入用户、团队、主机、查询日志、角色管理等功能的第三方厂商。

Asset ID:在行云管家平台中某个已导入或创建的实体(比如用户、团队、主机、云账户等等)的主键,是一个整数(比如teamId,userId,hostId等等),很多API接口都需要指定Asset ID。ISV需要在自己的系统中记录Asset ID。

Endpoint:请求行云管家Open API的HTTP URL地址。行云管家SaaS环境的Endpoint是“https://openapi.cloudbility.com/”,私有部署的Endpoint请在管理控制台中查看。

OpenAPI AK:由行云管家颁发给开发者的API调用凭证,包含Access Key ID和Access Key Secret,其中Access Key ID用来标识ISV的身份,而Access Key Secret是用于加密签名字符串和服务器端验证签名字符串的密钥,必须严格保密,只有行云管家和ISV自己知道,API请求过程中不携带密钥信息。

2.2、认证方式1:参数签名

参数说明

本质上,是利用Access Key Secret对特定API请求的http method、请求路径、公共参数、API参数以一定规则生成一个HMAC签名,然后追加到URL的请求参数中。

1、公共参数

参数名 类型 是否必须 备注
timestamp String 请求发起时刻的时间,采用UTC时间,格式为:'yyyy-MM-dd'T'HH:mm:ss'Z'
version String 当前API的版本,默认值:1
nonce String 一个随机字符串,长度不超过10
accessKeyID String Access Key Id,OpenAPI的密钥ID
signature String 签名信息,根据所有请求参数(不包括signature自身)按一定规则计算出的签名字符串

2、将请求参数构造规范化请求字符串

(1)参数排序

按照参数名称的字典顺序对请求中所有的请求参数(包括“公共参数”和接口定义中的参数,但不包括“公共参数”中的 signature 参数)进行排序。

注意,这里的请求参数,全部来自URI的参数部分,即URI中“?”之后由“&”连接的部分。

(2)参数编码 对排序之后的请求参数的名称和值分别用 UTF-8 字符集进行 URL 编码。编码的规则如下:

  • 对于字符 A~Z、a~z、0~9 以及字符“-”、“_”、“.”、“~”不编码;
  • 对于其它字符编码成 %XY 的格式,其中 XY 是字符对应 ASCII 码的 16 进制表示。比如英文的双引号(")对应的编码为 %22;
  • 对于扩展的 UTF-8 字符,编码成 %XY%ZA… 的格式;
  • 英文空格( )要编码成 %20,而不是加号(+)。

该编码方式和一般采用的 application/x-www-form-urlencoded MIME 格式编码算法(比如 Java 标准库中的 java.net.URLEncoder 的实现)相似, 但又有所不同。实现时,可以先用标准库的方式进行编码,然后把编码后的字符串中的加号(+)替换成 %20、星号(*)替换成 %2A、%7E 替换回波浪号(~), 即可得到上述规则描述的编码字符串。这个算法可以用下面的 percentEncode 方法来实现:

 static String percentEncode(String value) {
        try {
            return value != null ? URLEncoder.encode(value, DEFAULT_ENCODING).replace("+", "%20").replace("*", "%2A").replace("%7E", "~") : null;
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

(3). 连接参数

将编码后的参数名称和值用英文等号(=)进行连接,将等号连接得到的参数组合按步骤(1)排好的顺序依次使用“&”符号连接,即得到规范化请求字符串CanonicalizedQueryString

3、构造代签名字符串

将上一步构造的规范化字符串CanonicalizedQueryString按照下面的规则构造成待签名的字符串:

  String StringToSign =
                method + "&" +
                        percentEncode(path) + "&" +
                        percentEncode(canonicalizedQueryString);

其中: * HTTPMethod 是提交请求用的 HTTP 方法,比如 GET或POST;

  • percentEncode(path) 是按照步骤 1、(2) 中描述的 URL 编码规则对请求路径path进行编码得到;

  • percentEncode(canonicalizedQueryString)是对步骤1中的规范化参数字符串进行编码。

4、按照 RFC2104 的定义,计算待签名字符串 StringToSign 的 HMAC 值。

注意:计算签名时使用的 Key 就是您持有的 Access Key Secret 并加上一个 “&” 字符(ASCII:38),使用的哈希算法是 SHA1。

5、按照 Base64 编码规则把上面的 HMAC 值编码成字符串,即得到签名值(Signature)。

6、将得到的签名值作为 Signature 参数添加到请求参数中,即完成对请求签名的过程。

注意:得到的签名值在作为最后的请求参数拼接到URI上时,要和其它参数一样,按照 RFC3986 的规则进行 URL 编码。

签名示例

以“获取查询权限配额使用情况”为例,http method是GET,路径是:/permissionQuota。假设AccessKey ID 和AccessKey Secret 分别是kAMGBOBW1WNboYecgH4fAFf11KgjI0oT5KriYIMdFaH3Lh,请求的permission是TeamAccessUserAccess两个,即请求参数permissions=TeamAccess,UserAccess。请求URL字符串是

https://openapi.cloudbility.com/permissionQuota?permissions=TeamAccess,UserAccess&timestamp=2018-03-29T12:46:24Z&version=1&nonce=6fcd1eh1x8&accessKeyId=kAMGBOBW1WNboYec

得到的CanonicalizedQueryString是:

accessKeyId=kAMGBOBW1WNboYec&nonce=6fcd1eh1x8&permissions=TeamAccess%2CUserAccess&timestamp=2018-03-29T12%3A46%3A24Z&version=1

得到的StringToSign是:

GET&%2FpermissionQuota&accessKeyId%3DkAMGBOBW1WNboYec%26nonce%3D6fcd1eh1x8%26permissions%3DTeamAccess%252CUserAccess%26timestamp%3D2018-03-29T12%253A46%253A24Z%26version%3D1

计算得到的签名是:wN0edRE03rpAvqpdFAM3GHFwOII=,URL编码后是:wN0edRE03rpAvqpdFAM3GHFwOII%3D

将参数拼接到原始的URL请求中,得到最后的URL是:

https://openapi.cloudbility.com/permissionQuota?permissions=TeamAccess%2CUserAccess&accessKeyId=kAMGBOBW1WNboYec&nonce=6fcd1eh1x8&timestamp=2018-03-29T12%3A46%3A24Z&version=1&signature=wN0edRE03rpAvqpdFAM3GHFwOII%3D

2.3、认证方式2:获取OAuth Token

接口说明

OpenAPI有一个接口:GET /oauth?accessKeyId=xxx&accessKeySecret=xxx&expireSeconds=xxx,可以获取到一个Access Token,根据expireSeconds可以设置token的有效期,取值范围是2分钟到24小时。

获取到token以后,设置成请求头Authorization:{token}即可,上文中提到的公共参数都不需要。

由于获取OAuth Token需要发送Access Key Secret,在私有部署没有开启SSL访问时使用OpenAPI服务,不建议采用这种方式,因为存在泄漏的风险。

当token过期后,请求头Authorization:{token}会失效,所有API请求都会返回异常信息,这时候需要重新获取token。

OAuth Token认证示例

发送Http GET请求:

https://openapi.cloudbility.com/oauth?accessKeyId=kAMGBOBW1WNboYec&accessKeySecret=gH4fAFf11KgjI0oT5KriYIMdFaH3Lh&expireSeconds=600

响应内容:

{
  "token": "zMDuliMVQbOuqHa5EaAI_w",
  "expireTime": "2018-04-17T02:39:43Z"
}

然后在发送请求时,将token设置作为http头“Authorization”的值即可。

还是以“获取查询权限配额使用情况”为例,请求

curl -X GET --header 'Authorization: zMDuliMVQbOuqHa5EaAI_w' 'https://openapi.cloudbility.com/permissionQuota?permissions=TeamAccess%2CUserAccess'

获取到的响应“http code =200”响应体是:

{
  "requestId": "G1BrkDxTSFSAEzKR5hk6iA",
  "quotaList": [
    {
      "permission": "TeamAccess",
      "total": 100,
      "used": 0,
      "quotaType": "ByTraffic"
    },
    {
      "permission": "UserAccess",
      "total": 100,
      "used": 1,
      "quotaType": "ByTraffic"
    }
  ]
}

三、OpenAPI处理异常

API请求的响应Content-Type是application/json,请求和响应均采用UTF-8编码。当前请求成功时,响应的http code是200,当请求失败时,响应的http code是非200。

当前请求失败时,会有类似下面的响应:

{
  "requestId": "TtWoVDQ_SkiKLZYxmrwdeA",
  "errorCode": "InvalidRequest",
  "errorMessage": "token is expired"
}

errorCode是错误编码,errorMessage是错误的描述,requestId是请求的ID。

四、API在线调试

本章节以一个私有部署实例进行介绍:http://188.188.77.136

打开行云管家管理控制台,打开系统设置、OpenAPI菜单页面,在这里启用API可以获取到AK和endpoint如下:

获取授权的Token

在调试页面,打开API方法:other api ->GET/oauth,输入AK和超时时间,点击“试一下”;

调试页面会显示API接口的输入参数列表和输出参数的模型,如果输入的是http payload json,也会显示模型。模型参数会描述每个字段的数据类型和意义。

调试程序会打印请求的详情,用curl命令展示,也会把响应结果显示出来。可以看到,示例中获取的token是:adr51k55S2K7IMNF2Jhe6Q

点击右上角的“Authorize”把token填入 API key的 value字段,点击“Authorize”:

从此之后,当前页面发送的请求就都会带上Token进行认证。刷新页面或者Token超时后会失效。

获取自动登录的授权URL

获取一个以特定用户身份登录系统,并打开特定页面的授权URL:other api -> get /oauthLogin。下面示例中,获取以用户(id=1)身份进入团队(id=1),并且打开首页菜单的授权链接。

用浏览器打开链接:http://188.188.77.136/api/openapiOAuth?token=Dh8gtA-mTOeZ4Mug8-PUIQ可以看到用户已自动登录并打开首页了。

五、使用Java SDK调用OpenAPI

可以使用Java SDK调用OpenAPI,以实现您的业务功能。请通过【OpenAPI Java SDK】下载获取OpenAPI Java SDK。

下面代码演示使用Java SDK获取自动登录的授权URL,更多样例代码,请在上述下载的OpenAPI Java SDK中获取(请先解压下载所得的cloudbility-openapi-sdk.zip文件,Java样例代码位于解压目录下的java-demo目录中)。

import com.cloudbility.openapi.OpenapiClient;
import com.cloudbility.openapi.OpenapiException;
import com.cloudbility.openapi.client.OpenapiConfig;
import com.cloudbility.openapi.request.OAuthLoginRequest;
import com.cloudbility.openapi.response.OAuthUrlResult;
import com.cloudbility.openapi.vo.RedirectPage;

/**
 * 获取自动登录的URL(OAuth login url)
 */
public class GetAutoLoginAuthUrlDemo {

   public static final String accessKeyId = "hpOGe8Gw6VOpMhy8";
   public static final String accessKeySecret = "Xa3qjkSC-Cvcv2I2hXzd8myqXHLlierG";
   public static final String endpoint = "http://188.188.77.136/api/openapi";

   public static void main(String[] args) {
      //client对象可以复用,可以并发使用
      OpenapiClient client = createOpenapiClient(accessKeyId, accessKeySecret, endpoint);
      OAuthLoginRequest request = new OAuthLoginRequest();
      request.setExpireSeconds(300);//链接300秒有效
      request.setOneoff(true);//链接一次有效
      request.setUserId(1);//登录用户id=1
      request.setTeamId(1);//登录后进入团队id=1
      request.setPage(RedirectPage.Home);//登录后打开首页菜单
      try {
         OAuthUrlResult result = client.otherApi().getOAuthLogin(request);
         System.out.println(result.getUrl());
         //输出:http://188.188.77.136/api/openapiOAuth?token=kUfssyjZTbO3hPilVq_qZg
      } catch (OpenapiException e) {
         e.printStackTrace();
      }
   }
   private static OpenapiClient createOpenapiClient(String accessKeyId, String accessKeySecret, String endpoint) {
      OpenapiConfig config = new OpenapiConfig();
      config.setAccessKeyId(accessKeyId);
      config.setAccessKeySecret(accessKeySecret);
      config.setEndpoint(endpoint);
      return new OpenapiClient(config);
   }
}

六、使用Python调用OpenAPI

以下Python代码,演示获取Token、按照IP查询主机、创建登录凭证等功能。更多的Python样例代码,请下载【OpenAPI Java SDK】,下载并解压文件,其中包含了更多的Python样例代码(请先解压下载所得的cloudbility-openapi-sdk.zip文件,Python样例代码位于解压目录下的python-demo目录中)。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import sys
import requests
import json
import os
apiEndpoint = "http://ent.cloudbility.cn:88/api/openapi"
'''
openApi: function to access api url and post parameters
'''
def openApi(url: str, params: dict = None, headers: dict = None, method: str = "GET", contentType: str = "json", timeout: int = 10) -> dict:
    if method == "GET":
        response = requests.get(apiEndpoint + url, params=params, headers=headers, timeout=timeout)
    elif method == "POST":
        if not headers:
            headers = dict()
        if contentType == "json":
            headers["Content-Type"] = "application/json"
            params = json.dumps(params) if params else None
        response = requests.post(apiEndpoint + url, data=params, headers=headers, timeout=timeout)
    elif method == "DELETE":
        response = requests.delete(apiEndpoint + url, data=params, headers=headers, timeout=timeout)
    else:
        return None
    if not response:
        return None
    if not 200 <= response.status_code <= 299:
        return None
    return json.loads(response.text)
def getToken(accessKeyId, accessKeySecret, expireSeconds=300):
    tokenJson = openApi(
        "/oauth",
        params={
            "accessKeyId": accessKeyId,
            "accessKeySecret": accessKeySecret,
            "expireSeconds": expireSeconds
        },
    )
    return tokenJson["token"]
def getHostByIP(token, ip, teamId=1):
    hostJson = openApi(
        "/host/findByIp",
        params={"teamId": teamId, "ip": ip},
        headers={"Authorization": token}
    )
    return hostJson
def createCredential(token, ip, user, password, **kwargs):
    hosts = getHostByIP(token, ip, kwargs.get("TeamId", "1"))
    if not hosts or "hosts" not in hosts:
        return None
    hostId = hosts["hosts"][0]["hostId"]
    if not hostId:
        return None
    credential = openApi(
        "/credential",
        params={
            "account": user,
            "authType": kwargs.get("AuthType", "PASSWORD"),
            "desktopType": kwargs.get("DesktopType", "SSH"),
            "hosts": [
                {
                    "hostId": hostId,
                }
            ],
            "name": "host_%s_cre_%s" %(hostId, os.getpid()),
            "password": password,
            "teamId": kwargs.get("TeamId", "1"),
        },
        headers={"Authorization": token},
        method="POST"
    )
    return credential["id"]
def main(args, **kwargs):
    ip = args[0]
    user = args[1]
    password = args[2]
    accessKeyId = kwargs.get("id")
    accessKeySecret = kwargs.get("secret")
    if not accessKeyId or not accessKeySecret:
        print("AccessKeyId and AccessKeySecret are not present.\n")
        return -1
    token = getToken(accessKeyId, accessKeySecret)
    if not token:
        print("Invalid AccessKeyId and AccessKeySecret.\n")
        return -1
    credentialId = createCredential(token, ip, user, password, **kwargs)
    if credentialId:
        print("Create credential for %s successed. Credential Id is %s.\n" % (ip, credentialId))
    else:
        print("Create credential for %s failed.\n" % (ip))
if __name__ == "__main__":
    print("Create credential for a host in team(default 1). Before this operation, please do:\n")
    print("1: Import the host to cloudbility\n")
    print("2: Get the AccessKey Id & Secret from administration console\n")
    print(
        "Usage: openapi_demo.py ip username password id= secret= [teamId= AuthType= DesktopType=]\n")
    if len(sys.argv) != 6:
        sys.exit(-1)
    sys.exit(main(sys.argv[1:4], **dict(arg.split('=') for arg in sys.argv[4:])))

七、OpenAPI Java SDK Reference

关于详细的OpenAPI Java SDK接口说明,请参考【OpenAPI Java SDK Reference