控制台 API
通过控制台 API 你可以与控制台的各项资源进行交互,例如创建文件系统、浏览文件列表,查看回收站和设置 ACL 等。
本文主要介绍请求签名和身份认证,查看 API schema 文档 以了解控制台支持哪些 API 以及 API 的数据格式。
URL 和数据格式
控制台 API 遵循 RESTful API 风格,URL 是 https://juicefs.com/api/v1
,使用 JSON 作为数据交换格式。以获取文件系统列表为例,请求参数如下:
GET /api/v1/volumes
Host: juicefs.com
Authorization: 4dca13af31e45740c1c1fe3acaca8752a093b43ccc169f890f1305f51f038bf8
...
返回的数据为:
[
{
"id": 1,
"name": "test",
"region": 1,
"bucket": "http://test.s3.us-east-1.amazonaws.com",
"blockSize": 4096,
"compress": "lz4",
"compatible": false,
"access_rules": [
{
"iprange": "*",
"token": "ec4da70f494f58aac5eee391e6c5986f0b99945",
"readonly": false,
"appendonly": false
}
],
"owner": 5,
"size": 20480,
"inodes": 5,
"created": "2022-07-14T09:57:10.888914Z",
"extend": "",
"storage": null
}
]
使用指南
创建 API 密钥对
- 在控制台点击右上角用户名,进入账户设置页面
- 点击「添加新的 API 密钥对」按钮,填写信息并创建
- 及时记下密钥对,关闭对话框后,将无法再次查看
请求签名
在发送 API 请求之前,还需要使用 API 密钥对对请求进行签名。
为了方便描述,假设创建的 API 密钥对为:
- Access key:
ac7418402ce0ce838ba87eb3a6be72af313cd7028e18007799c0d5651c326925
- Secret key:
5f0c5a5d51515947788fa7b8244acebe166aedd9de28b26ef716888a613c3d92
另外,以创建文件系统的请求为例:
POST /api/v1/volumes?a=1&a=2&b=3&c=4
Host: juicefs.com
Content-Type: application/json
...
{"name": "test", "bucket": "https://test.s3.us-east-1.amazonaws.com"}
上面的请求参数 a=1&a=2&b=3&c=4
实际上是不存在的,这里加上是为了方便介绍签名算法。
签名算法如下:
记录下当前的时间戳,假设等于
1663245320
。服务端会通过时间戳判断此次请求的发起时间,如果和服务器收到请求的时间相差超过 5 分钟,服务器会抛弃该请求。
按顺序处理请求头,字段名转为小写字符,然后用
:
连接字段名和值,最后用\n
连接所有字段。目前参与计算的请求头只有 Host。得到的结果为
host:juicefs.com
。对查询参数进行排序和编码,如果没有查询参数则使用空字符串。
- 排序规则:先按照字段名排序,因为需要处理同一个字段对应多个值的情况,所以还需要对字段的值进行排序
- 编码规则:对字段名和值进行 URL 编码,然后用
=
连接字段名和值,最后用&
连接所有字段
请求参数
a=1&a=2&b=3&c=4
中包含三个字段 a, b, c,其中字段 a 有两个值。先按字段名排序得到a < b < c
,对于字段 a 还需要对它的两个值进行排序,最终得到的结果为a=1&a=2&b=3&c=4
。对请求体进行 SHA256 哈希,如果没有请求体则使用空字符串。
得到的结果为
a81f7bf3a5740146fe1eedc891f1f8f063dc428a88ac590147d1cf056bdad04b
。用
\n
按顺序拼接时间戳、请求方法、请求路径、请求头、查询参数和请求体。得到的结果为:
1663245320\n
POST\n
/api/v1/volumes\n
host:juicefs.com\n
a=1&a=2&b=3&c=4\n
a81f7bf3a5740146fe1eedc891f1f8f063dc428a88ac590147d1cf056bdad04b使用 Secret key 对拼接后的字符串进行 HMAC-SHA256 签名。
得到的结果为
3646d11235b08cd856278cb68bd5d2bc7aeec5c593590813e1da43a22d3a9835
身份认证
计算完签名之后,还需要使用 Access key、时间戳(请求签名中使用的时间戳)、签名和版本号生成一个 Token 用于身份认证。
版本号是可选的,目前只有一个版本,将其设置为 1
即可。
步骤如下:
将这些字段组成一个 JSON 格式的数据,得到的结果为:
{
"access_key": "ac7418402ce0ce838ba87eb3a6be72af313cd7028e18007799c0d5651c326925",
"timestamp": 1663245320,
"signature": "3646d11235b08cd856278cb68bd5d2bc7aeec5c593590813e1da43a22d3a9835",
"version": 1
}使用 base64 对上一步的 JSON 数据进行编码,得到结果为:
ewogICJhY2Nlc3Nfa2V5IjogImFjNzQxODQwMmNlMGNlODM4YmE4N2ViM2E2YmU3MmFmMzEzY2Q3MDI4ZTE4MDA3Nzk5YzBkNTY1MWMzMjY5MjUiLAogICJ0aW1lc3RhbXAiOiAxNjYzMjQ1MzIwLAogICJzaWduYXR1cmUiOiAiMzY0NmQxMTIzNWIwOGNkODU2Mjc4Y2I2OGJkNWQyYmM3YWVlYzVjNTkzNTkwODEzZTFkYTQzYTIyZDNhOTgzNSIsCiAgInZlcnNpb24iOiAxCn0=
将上一步的结果放入到请求头中,例如
Authorization: ewogICJhY2Nlc3Nfa2V5I...24iOiAxCn0=
示例代码
为了更好地说明请求签名和身份认证的过程,下面提供一份 Python 的示例代码:
import base64
import hashlib
import hmac
import json
import time
from typing import Dict, List, Literal, Optional, Union
from urllib.parse import quote, urlencode, urlsplit
import requests
access_key = 'ac7418402ce0ce838ba87eb3a6be72af313cd7028e18007799c0d5651c326925'
secret_key = '5f0c5a5d51515947788fa7b8244acebe166aedd9de28b26ef716888a613c3d92'
def sign(
secret_key: str,
timestamp: int,
method: Literal['GET', 'POST', 'PATCH', 'PUT', 'DELETE'],
path: str,
headers: Dict[str, str],
query_params: Optional[Dict[str, Union[str, List[str]]]],
body: Optional[bytes],
) -> str:
"""
参数说明:
- timestamp 是整数时间戳,以秒为单位
- method 是 HTTP 请求方法,例如 GET, POST, PUT, DELETE
- path 是 HTTP 请求路径,注意不包括查询参数,例如 /api/v1/regions
- headers 是 HTTP 请求头,例如 {'Host': 'juicefs.com'}
- query_params 是 HTTP 请求的查询参数,如果同一个字段对应多个值,请使用列表来保存
例如 {'page': '1', 'per_page': '10', 'sort': ['name', 'created_at']}
- body 是 HTTP 请求的原始 body 内容,对于大多数 HTTP 库来说,可能需要先创建一个 Request 对象,然后再从该对象中获取 body 内容
"""
# 1. 按顺序处理请求头,字段名转为小写字符,然后用 `:` 连接字段名和值,最后用 `\n` 连接所有字段。
# 得到的结果形如 `host:juicefs.com`
sorted_headers = []
for h in ['Host']:
v = headers.get(h, '')
if not v:
raise ValueError(f'header {h} is required')
sorted_headers.append(f'{h.lower()}:{v}')
sorted_headers = '\n'.join(sorted_headers)
# 2. 对查询参数进行排序和编码
# 排序规则:先按照字段名排序,因为需要处理同一个字段对应多个值的情况,所以还需要对字段的值进行排序
# 编码规则:对字段名和值进行 URL 编码,然后用 `=` 连接字段名和值,最后用 `&` 连接所有字段
# 得到的结果形如 `a=1&a=2&b=3&c=4`
sorted_qs = ''
if query_params:
sorted_qs = sorted(query_params.items(), key=lambda item: item[0])
for i, (k, values) in enumerate(sorted_qs):
if not isinstance(values, list):
values = [values]
sorted_qs[i] = (k, sorted(values))
sorted_qs = urlencode(sorted_qs, doseq=True, quote_via=quote)
# 3. 对请求体进行 SHA256 哈希
payload_hash = hashlib.sha256(body).hexdigest() if body else ''
# 4. 用 `\n` 按顺序拼接上面的所有字符串
parts = [str(timestamp), method, path, sorted_headers, sorted_qs, payload_hash]
data = '\n'.join(parts)
# 5. 对拼接后的字符串进行 HMAC-SHA256 签名
signature = hmac.new(
secret_key.encode('utf-8'), data.encode('utf-8'), hashlib.sha256
).hexdigest()
return signature
def request(
method: Literal['GET', 'POST', 'PATCH', 'PUT', 'DELETE'],
url: str,
query_params: Optional[dict] = None,
data: Optional[dict] = None,
):
timestamp = int(time.time())
parts = urlsplit(url)
path = parts.path
headers = {'Host': parts.netloc}
req = requests.Request(method, url, params=query_params, json=data).prepare()
body = req.body or b''
print(f'secret_key: {secret_key[:10]}...{secret_key[-10:]}')
print(f'timestamp: {timestamp}')
print(f'method: {method}')
print(f'path: {path}')
print(f'headers: {headers}')
print(f'query_params: {query_params if query_params else "empty"}')
print(f'body: {body if body else "empty"}')
signature = sign(secret_key, timestamp, method, path, headers, query_params, body)
print(f'signature: {signature}')
auth = {
'access_key': access_key,
'timestamp': timestamp,
'signature': signature,
'version': 1,
}
token = base64.b64encode(json.dumps(auth).encode()).decode()
print(f'token: {token}')
req.headers['Authorization'] = token
resp = requests.Session().send(req)
print()
print(json.dumps(resp.json(), indent=2))
# API 文档:https://juicefs.com/api/v1/docs
API_URL = 'http://localhost:8080/api/v1'
def get_volumes():
url = f'{API_URL}/volumes'
request('GET', url)
def delete_volume():
url = f'{API_URL}/volumes/1'
request('DELETE', url)
def get_volume_exports():
url = f'{API_URL}/volumes/1/exports'
request('GET', url)
def create_volume_export():
url = f'{API_URL}/volumes/1/exports'
request(
'POST',
url,
data={
'desc': 'for mount',
'iprange': '192.168.0.1/24',
'apionly': False,
'readonly': False,
'appendonly': False,
},
)
def update_volume_export():
url = f'{API_URL}/volumes/1/exports/1'
request('PUT', url, data={'desc': 'for mount', 'iprange': '192.168.100.1/24'})
def get_volume_quotas():
url = f'{API_URL}/volumes/1/quotas'
request('GET', url)
def create_volume_quota():
url = f'{API_URL}/volumes/1/quotas'
request(
'POST',
url,
data={'path': '/path/to/subdir', 'inodes': 1 << 20, 'size': 1 << 30},
)
def update_volume_quota():
url = f'{API_URL}/volumes/1/quotas/9'
request('PUT', url, data={'path': '/foo', 'size': 10 << 30})