基于Python调用OpenStack Keystone-api接口

本次实验环境为Ubuntu2404,OpenStack版本为C版,单节点Allinone

配置基础环境

使用阿里源pypi依赖库

mkdir ~/.pip/
vi ~/.pip/pip.conf
[global]
index-url=http://mirrors.cloud.aliyuncs.com/pypi/simple/

[install]
trusted-host=mirrors.cloud.aliyuncs.com

Ubuntu2404自带python3.12环境,安装python3-pip工具即可

apt install python3-pip -y

Ubuntu 和 Debian 系统将系统 Python 环境标记为“受外部管理的环境”,需要使用虚拟环境或其他隔离方法来安装 Python 包,不然会报错

error: externally-managed-environment

× This environment is externally managed
╰─> To install Python packages system-wide, try apt install
    python3-xyz, where xyz is the package you are trying to
    install.

    If you wish to install a non-Debian-packaged Python package,
    create a virtual environment using python3 -m venv path/to/venv.
    Then use path/to/venv/bin/python and path/to/venv/bin/pip. Make
    sure you have python3-full installed.

    If you wish to install a non-Debian packaged Python application,
    it may be easiest to use pipx install xyz, which will manage a
    virtual environment for you. Make sure you have pipx installed.

    See /usr/share/doc/python3.12/README.venv for more information.

note: If you believe this is a mistake, please contact your Python installation or OS distribution provider. You can override this, at the risk of breaking your Python installation or OS, by passing --break-system-packages.
hint: See PEP 668 for the detailed specification.
apt install python3-venv -y

创建虚拟环境

python3 -m venv myenv

调用Keystone-api用户接口

激活虚拟环境

source myenv/bin/activate

更新升级pip3

python3 -m pip install --upgrade pip

安装requests库

pip3 install requests

官网文档:可以对角色、用户、项目、域等所有的操作进行api调用,基本上只要客户端命令能实现的,都可以调用api进行操作。

编写用户管理py文件

以下为官网示例的用户管理api接口,包含了用户的增删改查等功能:每个接口可对应查看到具体的细节
在这里插入图片描述

在这里插入图片描述
示例代码如下:包含用户的增删改查

import requests
import json
import logging

# 配置日志记录
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(message)s')
logger = logging.getLogger(__name__)

# 获取认证Token
def get_auth_token(controller_ip, domain, user, password):
    """
    获取 OpenStack 用户认证Token
    :param controller_ip: 控制节点 IP
    :param domain: 用户域
    :param user: 用户名
    :param password: 用户密码
    :return: 认证头部信息
    """
    try:
        url = f"http://{controller_ip}:5000/v3/auth/tokens"
        body = {
            "auth": {
                "identity": {"methods": ["password"], "password": {"user": {"domain": {"name": domain}, "name": user, "password": password}}},
                "scope": {"project": {"domain": {"name": domain}, "name": user}}
            }
        }
        response = requests.post(url, json=body, headers={"Content-Type": "application/json"})
        token = response.headers['X-Subject-Token']
        logger.debug(f"获取Token值: {token}")
        return {"X-Auth-Token": token}
    except Exception as e:
        logger.error(f"获取Token失败: {e}")
        exit(1)

# 用户管理类
class UserManager:
    def __init__(self, headers, url):
        self.headers = headers
        self.url = url

    def create_user(self, name, password, description):
        """创建用户"""
        body = {"user": {"name": name, "password": password, "description": description}}
        response = requests.post(self.url, json=body, headers=self.headers)
        logger.debug(f"创建用户响应: {response.text}")
        return response.text

    def get_users(self):
        """查询所有用户"""
        response = requests.get(self.url, headers=self.headers)
        logger.debug(f"查询用户响应: {response.text}")
        return response.text

    def get_user_id(self, name):
        """根据用户名获取用户ID"""
        users = json.loads(self.get_users())['users']
        return next((user['id'] for user in users if user['name'] == name), "NONE")

    def update_password(self, user_id, old_password, new_password):
        """更新用户密码"""
        body = {"user": {"password": new_password, "original_password": old_password}}
        api_url = f"{self.url}/{user_id}/password"
        response = requests.post(api_url, json=body, headers=self.headers)
        return {"更新密码成功": response.status_code} if response.status_code == 204 else response.json()

    def delete_user(self, user_id):
        """删除用户"""
        response = requests.delete(f"{self.url}/{user_id}", headers=self.headers)
        return {"删除用户成功": response.status_code} if response.status_code == 204 else response.json()

# 主函数
if __name__ == '__main__':
    controller_ip = "192.168.200.160"
    domain = "default"
    user = "admin"
    password = "000000"
    
    # 获取认证信息
    headers = get_auth_token(controller_ip, domain, user, password)
    user_manager = UserManager(headers, f"http://{controller_ip}:5000/v3/users")

    # 操作用户
    print("查询所有用户:", user_manager.get_users())
    print("创建用户:", user_manager.create_user("user_demo", "passw0rd", "由Python创建的用户"))
    user_id = user_manager.get_user_id("user_demo")
    print("获取用户ID:", user_id)
    print("更新密码:", user_manager.update_password(user_id, "passw0rd", "newpassw0rd"))
    print("删除用户:", user_manager.delete_user(user_id))

使用本代码需要注意以下说明

  • 修改主函数下的openstack的连接信息,IP、用户、密码等
  • 主函数下面有查询、创建、更新、删除等,需要那个就使用那个函数调用

测试:

  • 命令查看当前创建的用户
(myenv) root@controller:~# source /etc/keystone/admin-openrc.sh
(myenv) root@controller:~# openstack user list
+----------------------------------+-----------+
| ID                               | Name      |
+----------------------------------+-----------+
| cb2cba23a7c1446fa424a49a494ed20d | admin     |
| b41e7196605f456b8587ed38b759d8ef | glance    |
| 8dd4f29f70514918b5879960b17e0645 | placement |
| 79ff6928d66b40a2bd10682651933f6e | nova      |
| 1029a0d8098e462da3b80164d4219204 | neutron   |
+----------------------------------+-----------+
  • 查看端点地址(public接口)和令牌
root@controller:~# openstack endpoint list
+----------------------------------+-----------+--------------+--------------+---------+-----------+-----------------------------+
| ID                               | Region    | Service Name | Service Type | Enabled | Interface | URL                         |
+----------------------------------+-----------+--------------+--------------+---------+-----------+-----------------------------+
| 33501b74440542188ba76c20b4cbf00e | RegionOne | keystone     | identity     | True    | public    | http://controller:5000/v3/  |
| 38b0c4b88aac4e6ba4518fd8ce4204b7 | RegionOne | keystone     | identity     | True    | admin     | http://controller:5000/v3/  |
| 38bd876005574b73b3c407f839834bb0 | RegionOne | nova         | compute      | True    | public    | http://controller:8774/v2.1 |
| 40ad8b8058ca49a990906e8f9c4b305b | RegionOne | placement    | placement    | True    | admin     | http://controller:8778      |
| 4e7c06c7bb434c91996950d71bbec21a | RegionOne | nova         | compute      | True    | admin     | http://controller:8774/v2.1 |
| 63391670a8324515ac1d521b97595c7b | RegionOne | keystone     | identity     | True    | internal  | http://controller:5000/v3/  |
| 73fda3f889d841c292f8b4b92d991a13 | RegionOne | placement    | placement    | True    | internal  | http://controller:8778      |
| 9e47b0e39b8a4486adef88b7cfadaa01 | RegionOne | glance       | image        | True    | admin     | http://controller:9292      |
| a478dc96e015440eaaa3c9195ba3cb04 | RegionOne | placement    | placement    | True    | public    | http://controller:8778      |
| a4b523fd7d8743ed8000580e189122af | RegionOne | neutron      | network      | True    | public    | http://controller:9696      |
| b55a96421d67413796f8b24349ada43f | RegionOne | neutron      | network      | True    | admin     | http://controller:9696      |
| b933e8156bfa4c82895a324f1f59a154 | RegionOne | glance       | image        | True    | internal  | http://controller:9292      |
| dce16eb156af4f8697be947955aa333a | RegionOne | neutron      | network      | True    | internal  | http://controller:9696      |
| e17d5863691342d7a93110222635ff5b | RegionOne | glance       | image        | True    | public    | http://controller:9292      |
| f3e70523608546a8b48b5112451b1778 | RegionOne | nova         | compute      | True    | internal  | http://controller:8774/v2.1 |
+----------------------------------+-----------+--------------+--------------+---------+-----------+-----------------------------+
root@controller:~# openstack token issue
+------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Field      | Value                                                                                                                                                                                   |
+------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| expires    | 2025-01-15T06:46:10+0000                                                                                                                                                                |
| id         | gAAAAABnh0uia97V_IBeByhXPgcogzyBox9hhcsjLMxqBKAdge4ZUTU0qRU84kmILuZgq52xhReoWWKnrutMXjiYN-sl6Ijz-7RuoWY220Y0Muk9zssakbueuGBtHISbpc-BqjCtjnt__FMkFPtTPZxWYkci23nFs_zkT2_XS4evcKsgDUPCF7Q |
| project_id | 627a106dda814f3a9f3791e611f16cad                                                                                                                                                        |
| user_id    | cb2cba23a7c1446fa424a49a494ed20d                                                                                                                                                        |
+------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
  • 使用api调用:这里我只调用会以json格式返回用户的id,名称,所属域等信息
vi keystone_user_api.py
(myenv) root@controller:~# python3 keystone_user_api.py
2025-01-15 03:29:47,504 Starting new HTTP connection (1): 192.168.200.160:5000
2025-01-15 03:29:47,800 http://192.168.200.160:5000 "POST /v3/auth/tokens HTTP/1.1" 201 3476
2025-01-15 03:29:47,801 获取Token值: gAAAAABnhyurRtNO1GyFXHeKnCzQj03CUQ-Zh2rFpVD3w89mV2KMFm8m1A_xovS2yUHGoNJbZhocDLped7i-C-OgFwIWQwvB_eGGrOEADAtHqLao4Imu8YEfKuRJA7QkiVUqymriUYaRswcdreWg1-okITW-3aHH8oGyFKN_JIA_NN8lY7NcKwo
2025-01-15 03:29:47,802 Starting new HTTP connection (1): 192.168.200.160:5000
2025-01-15 03:29:47,951 http://192.168.200.160:5000 "GET /v3/users HTTP/1.1" 200 1299
2025-01-15 03:29:47,951 查询用户响应: {"users": [{"id": "cb2cba23a7c1446fa424a49a494ed20d", "name": "admin", "domain_id": "default", "enabled": true, "password_expires_at": null, "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/users/cb2cba23a7c1446fa424a49a494ed20d"}}, {"id": "b41e7196605f456b8587ed38b759d8ef", "name": "glance", "domain_id": "default", "enabled": true, "password_expires_at": null, "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/users/b41e7196605f456b8587ed38b759d8ef"}}, {"id": "8dd4f29f70514918b5879960b17e0645", "name": "placement", "domain_id": "default", "enabled": true, "password_expires_at": null, "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/users/8dd4f29f70514918b5879960b17e0645"}}, {"id": "79ff6928d66b40a2bd10682651933f6e", "name": "nova", "domain_id": "default", "enabled": true, "password_expires_at": null, "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/users/79ff6928d66b40a2bd10682651933f6e"}}, {"id": "1029a0d8098e462da3b80164d4219204", "name": "neutron", "domain_id": "default", "enabled": true, "password_expires_at": null, "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/users/1029a0d8098e462da3b80164d4219204"}}], "links": {"next": null, "self": "http://192.168.200.160:5000/v3/users", "previous": null}}

查询所有用户: {"users": [{"id": "cb2cba23a7c1446fa424a49a494ed20d", "name": "admin", "domain_id": "default", "enabled": true, "password_expires_at": null, "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/users/cb2cba23a7c1446fa424a49a494ed20d"}}, {"id": "b41e7196605f456b8587ed38b759d8ef", "name": "glance", "domain_id": "default", "enabled": true, "password_expires_at": null, "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/users/b41e7196605f456b8587ed38b759d8ef"}}, {"id": "8dd4f29f70514918b5879960b17e0645", "name": "placement", "domain_id": "default", "enabled": true, "password_expires_at": null, "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/users/8dd4f29f70514918b5879960b17e0645"}}, {"id": "79ff6928d66b40a2bd10682651933f6e", "name": "nova", "domain_id": "default", "enabled": true, "password_expires_at": null, "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/users/79ff6928d66b40a2bd10682651933f6e"}}, {"id": "1029a0d8098e462da3b80164d4219204", "name": "neutron", "domain_id": "default", "enabled": true, "password_expires_at": null, "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/users/1029a0d8098e462da3b80164d4219204"}}], "links": {"next": null, "self": "http://192.168.200.160:5000/v3/users", "previous": null}}

2025-01-15 03:29:47,952 Starting new HTTP connection (1): 192.168.200.160:5000
2025-01-15 03:29:48,618 http://192.168.200.160:5000 "POST /v3/users HTTP/1.1" 201 312
2025-01-15 03:29:48,618 创建用户响应: {"user": {"description": "\u7531Python\u521b\u5efa\u7684\u7528\u6237", "id": "f801ac380378464ba2a84adfaf7d6084", "name": "user_demo", "domain_id": "default", "enabled": true, "password_expires_at": null, "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/users/f801ac380378464ba2a84adfaf7d6084"}}}

创建用户: {"user": {"description": "\u7531Python\u521b\u5efa\u7684\u7528\u6237", "id": "f801ac380378464ba2a84adfaf7d6084", "name": "user_demo", "domain_id": "default", "enabled": true, "password_expires_at": null, "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/users/f801ac380378464ba2a84adfaf7d6084"}}}

2025-01-15 03:29:48,619 Starting new HTTP connection (1): 192.168.200.160:5000
2025-01-15 03:29:48,798 http://192.168.200.160:5000 "GET /v3/users HTTP/1.1" 200 1602
2025-01-15 03:29:48,799 查询用户响应: {"users": [{"id": "cb2cba23a7c1446fa424a49a494ed20d", "name": "admin", "domain_id": "default", "enabled": true, "password_expires_at": null, "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/users/cb2cba23a7c1446fa424a49a494ed20d"}}, {"id": "b41e7196605f456b8587ed38b759d8ef", "name": "glance", "domain_id": "default", "enabled": true, "password_expires_at": null, "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/users/b41e7196605f456b8587ed38b759d8ef"}}, {"id": "8dd4f29f70514918b5879960b17e0645", "name": "placement", "domain_id": "default", "enabled": true, "password_expires_at": null, "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/users/8dd4f29f70514918b5879960b17e0645"}}, {"id": "79ff6928d66b40a2bd10682651933f6e", "name": "nova", "domain_id": "default", "enabled": true, "password_expires_at": null, "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/users/79ff6928d66b40a2bd10682651933f6e"}}, {"id": "1029a0d8098e462da3b80164d4219204", "name": "neutron", "domain_id": "default", "enabled": true, "password_expires_at": null, "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/users/1029a0d8098e462da3b80164d4219204"}}, {"description": "\u7531Python\u521b\u5efa\u7684\u7528\u6237", "id": "f801ac380378464ba2a84adfaf7d6084", "name": "user_demo", "domain_id": "default", "enabled": true, "password_expires_at": null, "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/users/f801ac380378464ba2a84adfaf7d6084"}}], "links": {"next": null, "self": "http://192.168.200.160:5000/v3/users", "previous": null}}

获取用户ID: f801ac380378464ba2a84adfaf7d6084
2025-01-15 03:29:48,803 Starting new HTTP connection (1): 192.168.200.160:5000
2025-01-15 03:29:49,502 http://192.168.200.160:5000 "POST /v3/users/f801ac380378464ba2a84adfaf7d6084/password HTTP/1.1" 204 0
更新密码: {'更新密码成功': 204}
2025-01-15 03:29:49,503 Starting new HTTP connection (1): 192.168.200.160:5000
2025-01-15 03:29:49,799 http://192.168.200.160:5000 "DELETE /v3/users/f801ac380378464ba2a84adfaf7d6084 HTTP/1.1" 204 0
删除用户: {'删除用户成功': 204}
编写项目管理py文件

以下为官网示例的项目管理api接口,包含了项目的增删改查等功能:每个接口可对应查看到具体的细节

在这里插入图片描述

示例代码如下:

import requests
import json
import logging

# 配置日志
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(message)s')
logger = logging.getLogger(__name__)

# 获取认证Token
def get_auth_token(controller_ip, domain, user, password):
    """
    获取 OpenStack 认证Token
    :param controller_ip: 控制节点 IP
    :param domain: 用户域
    :param user: 用户名
    :param password: 用户密码
    :return: 认证头部信息
    """
    try:
        url = f"http://{controller_ip}:5000/v3/auth/tokens"
        body = {
            "auth": {
                "identity": {"methods": ["password"], "password": {"user": {"domain": {"name": domain}, "name": user, "password": password}}},
                "scope": {"project": {"domain": {"name": domain}, "name": user}}
            }
        }
        response = requests.post(url, json=body, headers={"Content-Type": "application/json"})
        token = response.headers['X-Subject-Token']
        logger.debug(f"获取Token值: {token}")
        return {"X-Auth-Token": token}
    except Exception as e:
        logger.error(f"获取Token失败: {e}")
        exit(1)

# 项目管理类
class ProjectManager:
    def __init__(self, headers, url):
        self.headers = headers
        self.url = url

    def create_project(self, name, domain_id, description):
        """创建项目"""
        body = {
            "project": {
                "name": name,
                "domain_id": domain_id,
                "description": description
            }
        }
        response = requests.post(self.url, json=body, headers=self.headers)
        logger.debug(f"创建项目响应: {response.text}")
        return response.text

    def get_projects(self):
        """获取所有项目"""
        response = requests.get(self.url, headers=self.headers)
        logger.debug(f"查询项目响应: {response.text}")
        return response.text

    def get_project_id(self, name):
        """根据项目名获取项目ID"""
        projects = json.loads(self.get_projects())['projects']
        return next((project['id'] for project in projects if project['name'] == name), "NONE")

    def update_project(self, project_id, name=None, description=None, enabled=None):
        """更新项目信息"""
        body = {"project": {}}
        if name: body["project"]["name"] = name
        if description: body["project"]["description"] = description
        if enabled is not None: body["project"]["enabled"] = enabled

        api_url = f"{self.url}/{project_id}"
        response = requests.patch(api_url, json=body, headers=self.headers)
        logger.debug(f"更新项目响应: {response.text}")
        return response.text

    def delete_project(self, project_id):
        """删除项目"""
        api_url = f"{self.url}/{project_id}"
        response = requests.delete(api_url, headers=self.headers)
        return {"删除成功": response.status_code} if response.status_code == 204 else response.json()

# 主函数
if __name__ == '__main__':
    # 控制节点认证信息
    controller_ip = "192.168.200.160"
    domain = "default"
    user = "admin"
    password = "000000"
    # 获取认证头部
    headers = get_auth_token(controller_ip, domain, user, password)
    # 初始化项目管理
    project_manager = ProjectManager(headers, f"http://{controller_ip}:5000/v3/projects")
    # 查看所有项目
    print("所有项目:", project_manager.get_projects())
    # 创建新项目
    project_name = "test_project"
    description = "由Python创建的测试项目"
    domain_id = "default"  # 通常是 default
    print("创建项目:", project_manager.create_project(project_name, domain_id, description))
    # 获取项目ID
    project_id = project_manager.get_project_id(project_name)
    print(f"项目 '{project_name}' 的ID:", project_id)
    # 更新项目
    print("更新项目描述:", project_manager.update_project(project_id, description="更新后的项目描述"))
    # 删除项目
    print("删除项目:", project_manager.delete_project(project_id))

代码说明:

  • 查看项目:调用 GET /v3/projects 接口,返回所有项目的列表。
  • 创建项目:调用 POST /v3/projects 接口,传入项目名称、域 ID 和描述信息。
  • 更新项目:调用 PATCH /v3/projects/{project_id} 接口,可修改名称、描述或启用状态。
  • 删除项目:调用 DELETE /v3/projects/{project_id} 接口,删除指定项目

测试:

vi keystone_project_api.py
root@controller:~# python3 keystone_project_api.py
2025-01-15 05:55:51,711 Starting new HTTP connection (1): 192.168.200.160:5000
2025-01-15 05:55:52,031 http://192.168.200.160:5000 "POST /v3/auth/tokens HTTP/1.1" 201 3476
2025-01-15 05:55:52,032 获取Token值: gAAAAABnh03n1TLWv5diNutPqEDbbLN-Sift1q6QNksd8XcaCZGzOE2HRv8Q3cCwmuVXRsbSzfIbOi-sj4SxzbyvbhWv8TdsL3AYauq48Vp0iJ6VHI3gn8EMAJi2-2ncw6vi6M-vSVNs99WOnR76mI-JgQXpmZ6Hal3u4-CTMExXBgcb3adXAu0
2025-01-15 05:55:52,034 Starting new HTTP connection (1): 192.168.200.160:5000
2025-01-15 05:55:52,127 http://192.168.200.160:5000 "GET /v3/projects HTTP/1.1" 200 745
2025-01-15 05:55:52,128 查询项目响应: {"projects": [{"id": "627a106dda814f3a9f3791e611f16cad", "name": "admin", "domain_id": "default", "description": "Bootstrap project for initializing the cloud.", "enabled": true, "parent_id": "default", "is_domain": false, "tags": [], "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/projects/627a106dda814f3a9f3791e611f16cad"}}, {"id": "6a166bba90684340a0cd323f7050caa0", "name": "service", "domain_id": "default", "description": "Service Project", "enabled": true, "parent_id": "default", "is_domain": false, "tags": [], "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/projects/6a166bba90684340a0cd323f7050caa0"}}], "links": {"next": null, "self": "http://192.168.200.160:5000/v3/projects", "previous": null}}

所有项目: {"projects": [{"id": "627a106dda814f3a9f3791e611f16cad", "name": "admin", "domain_id": "default", "description": "Bootstrap project for initializing the cloud.", "enabled": true, "parent_id": "default", "is_domain": false, "tags": [], "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/projects/627a106dda814f3a9f3791e611f16cad"}}, {"id": "6a166bba90684340a0cd323f7050caa0", "name": "service", "domain_id": "default", "description": "Service Project", "enabled": true, "parent_id": "default", "is_domain": false, "tags": [], "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/projects/6a166bba90684340a0cd323f7050caa0"}}], "links": {"next": null, "self": "http://192.168.200.160:5000/v3/projects", "previous": null}}

2025-01-15 05:55:52,130 Starting new HTTP connection (1): 192.168.200.160:5000
2025-01-15 05:55:52,318 http://192.168.200.160:5000 "POST /v3/projects HTTP/1.1" 201 360
2025-01-15 05:55:52,319 创建项目响应: {"project": {"id": "3fd0ca143d7545f4b063559511034a5b", "name": "test_project", "domain_id": "default", "description": "\u7531Python\u521b\u5efa\u7684\u6d4b\u8bd5\u9879\u76ee", "enabled": true, "parent_id": "default", "is_domain": false, "tags": [], "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/projects/3fd0ca143d7545f4b063559511034a5b"}}}

创建项目: {"project": {"id": "3fd0ca143d7545f4b063559511034a5b", "name": "test_project", "domain_id": "default", "description": "\u7531Python\u521b\u5efa\u7684\u6d4b\u8bd5\u9879\u76ee", "enabled": true, "parent_id": "default", "is_domain": false, "tags": [], "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/projects/3fd0ca143d7545f4b063559511034a5b"}}}

2025-01-15 05:55:52,320 Starting new HTTP connection (1): 192.168.200.160:5000
2025-01-15 05:55:52,456 http://192.168.200.160:5000 "GET /v3/projects HTTP/1.1" 200 1093
2025-01-15 05:55:52,457 查询项目响应: {"projects": [{"id": "3fd0ca143d7545f4b063559511034a5b", "name": "test_project", "domain_id": "default", "description": "\u7531Python\u521b\u5efa\u7684\u6d4b\u8bd5\u9879\u76ee", "enabled": true, "parent_id": "default", "is_domain": false, "tags": [], "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/projects/3fd0ca143d7545f4b063559511034a5b"}}, {"id": "627a106dda814f3a9f3791e611f16cad", "name": "admin", "domain_id": "default", "description": "Bootstrap project for initializing the cloud.", "enabled": true, "parent_id": "default", "is_domain": false, "tags": [], "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/projects/627a106dda814f3a9f3791e611f16cad"}}, {"id": "6a166bba90684340a0cd323f7050caa0", "name": "service", "domain_id": "default", "description": "Service Project", "enabled": true, "parent_id": "default", "is_domain": false, "tags": [], "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/projects/6a166bba90684340a0cd323f7050caa0"}}], "links": {"next": null, "self": "http://192.168.200.160:5000/v3/projects", "previous": null}}

项目 'test_project' 的ID: 3fd0ca143d7545f4b063559511034a5b
2025-01-15 05:55:52,458 Starting new HTTP connection (1): 192.168.200.160:5000
2025-01-15 05:55:52,605 http://192.168.200.160:5000 "PATCH /v3/projects/3fd0ca143d7545f4b063559511034a5b HTTP/1.1" 200 367
2025-01-15 05:55:52,605 更新项目响应: {"project": {"id": "3fd0ca143d7545f4b063559511034a5b", "name": "test_project", "domain_id": "default", "description": "\u66f4\u65b0\u540e\u7684\u9879\u76ee\u63cf\u8ff0", "enabled": true, "parent_id": "default", "is_domain": false, "tags": [], "extra": {}, "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/projects/3fd0ca143d7545f4b063559511034a5b"}}}

更新项目描述: {"project": {"id": "3fd0ca143d7545f4b063559511034a5b", "name": "test_project", "domain_id": "default", "description": "\u66f4\u65b0\u540e\u7684\u9879\u76ee\u63cf\u8ff0", "enabled": true, "parent_id": "default", "is_domain": false, "tags": [], "extra": {}, "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/projects/3fd0ca143d7545f4b063559511034a5b"}}}

2025-01-15 05:55:52,606 Starting new HTTP connection (1): 192.168.200.160:5000
2025-01-15 05:55:52,802 http://192.168.200.160:5000 "DELETE /v3/projects/3fd0ca143d7545f4b063559511034a5b HTTP/1.1" 204 0
删除项目: {'删除成功': 204}
编写域管理py文件

以下为官网示例的域管理api接口,包含了域的增删改查等功能:每个接口的detail可对应查看到具体的细节

在这里插入图片描述

示例代码如下:

import requests
import json
import logging

# 配置日志
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(message)s')
logger = logging.getLogger(__name__)

# 获取认证Token
def get_auth_token(controller_ip, domain, user, password):
    """
    获取 OpenStack 认证Token
    :param controller_ip: 控制节点 IP
    :param domain: 用户域
    :param user: 用户名
    :param password: 用户密码
    :return: 认证头部信息
    """
    try:
        url = f"http://{controller_ip}:5000/v3/auth/tokens"
        body = {
            "auth": {
                "identity": {"methods": ["password"], "password": {"user": {"domain": {"name": domain}, "name": user, "password": password}}},
                "scope": {"project": {"domain": {"name": domain}, "name": user}}
            }
        }
        response = requests.post(url, json=body, headers={"Content-Type": "application/json"})
        token = response.headers['X-Subject-Token']
        logger.debug(f"获取Token值: {token}")
        return {"X-Auth-Token": token}
    except Exception as e:
        logger.error(f"获取Token失败: {e}")
        exit(1)

# 域管理类
class DomainManager:
    def __init__(self, headers, url):
        self.headers = headers
        self.url = url

    def create_domain(self, name, description, enabled=True):
        """创建域"""
        body = {
            "domain": {
                "name": name,
                "description": description,
                "enabled": enabled
            }
        }
        response = requests.post(self.url, json=body, headers=self.headers)
        logger.debug(f"创建域响应: {response.text}")
        return response.text

    def get_domains(self):
        """获取所有域"""
        response = requests.get(self.url, headers=self.headers)
        logger.debug(f"查询域响应: {response.text}")
        return response.text

    def get_domain_id(self, name):
        """根据域名获取域ID"""
        domains = json.loads(self.get_domains())['domains']
        return next((domain['id'] for domain in domains if domain['name'] == name), "NONE")

    def update_domain(self, domain_id, name=None, description=None, enabled=None):
        """更新域信息"""
        body = {"domain": {}}
        if name: body["domain"]["name"] = name
        if description: body["domain"]["description"] = description
        if enabled is not None: body["domain"]["enabled"] = enabled

        api_url = f"{self.url}/{domain_id}"
        response = requests.patch(api_url, json=body, headers=self.headers)
        logger.debug(f"更新域响应: {response.text}")
        return response.text

    def delete_domain(self, domain_id):
        """删除域"""
        api_url = f"{self.url}/{domain_id}"
        response = requests.delete(api_url, headers=self.headers)
        return {"删除成功": response.status_code} if response.status_code == 204 else response.json()

# 主函数
if __name__ == '__main__':
    # 控制节点认证信息
    controller_ip = "192.168.200.160"
    domain = "default"
    user = "admin"
    password = "000000"
    # 获取认证头部
    headers = get_auth_token(controller_ip, domain, user, password)
    # 初始化域管理
    domain_manager = DomainManager(headers, f"http://{controller_ip}:5000/v3/domains")
    # 查看所有域
    print("所有域:", domain_manager.get_domains())
    # 创建新域
    domain_name = "test_domain"
    description = "由Python创建的测试域"
    print("创建域:", domain_manager.create_domain(domain_name, description))
    # 获取域ID
    domain_id = domain_manager.get_domain_id(domain_name)
    print(f"域 '{domain_name}' 的ID:", domain_id)
    # 更新域
    print("更新域描述:", domain_manager.update_domain(domain_id, description="更新后的域描述", enabled=False))
    # 删除域
    print("删除域:", domain_manager.delete_domain(domain_id))

代码说明:

  • 查看域:调用 GET /v3/domains 接口,返回所有域的信息。
  • 创建域:调用 POST /v3/domains 接口,传入域的名称、描述和启用状态。
  • 更新域:调用 PATCH /v3/domains/{domain_id} 接口,可以更新域的名称、描述和启用状态。
  • 删除域:调用 DELETE /v3/domains/{domain_id} 接口,删除指定的域。

测试

vi keystone_domain_api.py
root@controller:~# python3 keystone_domain_api.py
2025-01-15 06:08:16,839 Starting new HTTP connection (1): 192.168.200.160:5000
2025-01-15 06:08:17,135 http://192.168.200.160:5000 "POST /v3/auth/tokens HTTP/1.1" 201 3476
2025-01-15 06:08:17,136 获取Token值: gAAAAABnh1DRgfxySemhvN0q-xe5lqcQWJT4IBzrv0qzf-PfG5wcsps5MhuyVb3wEnFRQzykbsE2iMOsrL6iMOx5R92rZcd7AH3Z9P_F35ULgXV7X0HLH1MRTL7EFWHidguypC8f3tYi3aFedwkKvR5Uebf20outs9XAn0ZQ3xt63ezL8VTH0ew
2025-01-15 06:08:17,137 Starting new HTTP connection (1): 192.168.200.160:5000
2025-01-15 06:08:17,294 http://192.168.200.160:5000 "GET /v3/domains HTTP/1.1" 200 295
2025-01-15 06:08:17,294 查询域响应: {"domains": [{"id": "default", "name": "Default", "description": "The default domain", "enabled": true, "tags": [], "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/domains/default"}}], "links": {"next": null, "self": "http://192.168.200.160:5000/v3/domains", "previous": null}}

所有域: {"domains": [{"id": "default", "name": "Default", "description": "The default domain", "enabled": true, "tags": [], "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/domains/default"}}], "links": {"next": null, "self": "http://192.168.200.160:5000/v3/domains", "previous": null}}

2025-01-15 06:08:17,295 Starting new HTTP connection (1): 192.168.200.160:5000
2025-01-15 06:08:17,765 http://192.168.200.160:5000 "POST /v3/domains HTTP/1.1" 201 283
2025-01-15 06:08:17,765 创建域响应: {"domain": {"id": "f7b409d740f74a3e82dab9fe91ab102c", "name": "test_domain", "description": "\u7531Python\u521b\u5efa\u7684\u6d4b\u8bd5\u57df", "enabled": true, "tags": [], "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/domains/f7b409d740f74a3e82dab9fe91ab102c"}}}

创建域: {"domain": {"id": "f7b409d740f74a3e82dab9fe91ab102c", "name": "test_domain", "description": "\u7531Python\u521b\u5efa\u7684\u6d4b\u8bd5\u57df", "enabled": true, "tags": [], "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/domains/f7b409d740f74a3e82dab9fe91ab102c"}}}

2025-01-15 06:08:17,766 Starting new HTTP connection (1): 192.168.200.160:5000
2025-01-15 06:08:17,926 http://192.168.200.160:5000 "GET /v3/domains HTTP/1.1" 200 567
2025-01-15 06:08:17,926 查询域响应: {"domains": [{"id": "default", "name": "Default", "description": "The default domain", "enabled": true, "tags": [], "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/domains/default"}}, {"id": "f7b409d740f74a3e82dab9fe91ab102c", "name": "test_domain", "description": "\u7531Python\u521b\u5efa\u7684\u6d4b\u8bd5\u57df", "enabled": true, "tags": [], "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/domains/f7b409d740f74a3e82dab9fe91ab102c"}}], "links": {"next": null, "self": "http://192.168.200.160:5000/v3/domains", "previous": null}}'test_domain' 的ID: f7b409d740f74a3e82dab9fe91ab102c
2025-01-15 06:08:17,927 Starting new HTTP connection (1): 192.168.200.160:5000
2025-01-15 06:08:18,088 http://192.168.200.160:5000 "PATCH /v3/domains/f7b409d740f74a3e82dab9fe91ab102c HTTP/1.1" 200 278
2025-01-15 06:08:18,088 更新域响应: {"domain": {"id": "f7b409d740f74a3e82dab9fe91ab102c", "name": "test_domain", "description": "\u66f4\u65b0\u540e\u7684\u57df\u63cf\u8ff0", "enabled": false, "tags": [], "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/domains/f7b409d740f74a3e82dab9fe91ab102c"}}}

更新域描述: {"domain": {"id": "f7b409d740f74a3e82dab9fe91ab102c", "name": "test_domain", "description": "\u66f4\u65b0\u540e\u7684\u57df\u63cf\u8ff0", "enabled": false, "tags": [], "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/domains/f7b409d740f74a3e82dab9fe91ab102c"}}}

2025-01-15 06:08:18,090 Starting new HTTP connection (1): 192.168.200.160:5000
2025-01-15 06:08:18,359 http://192.168.200.160:5000 "DELETE /v3/domains/f7b409d740f74a3e82dab9fe91ab102c HTTP/1.1" 204 0
编写角色管理py文件

以下为官网示角色的域管理api接口,包含了角色的增删改查等功能:每个接口的detail可对应查看到具体的细节

在这里插入图片描述
示例代码:

import requests
import json
import logging

# 配置日志
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(message)s')
logger = logging.getLogger(__name__)

# 获取认证Token
def get_auth_token(controller_ip, domain, user, password):
    """
    获取 OpenStack 认证Token
    :param controller_ip: 控制节点 IP
    :param domain: 用户域
    :param user: 用户名
    :param password: 用户密码
    :return: 认证头部信息
    """
    try:
        url = f"http://{controller_ip}:5000/v3/auth/tokens"
        body = {
            "auth": {
                "identity": {"methods": ["password"], "password": {"user": {"domain": {"name": domain}, "name": user, "password": password}}},
                "scope": {"project": {"domain": {"name": domain}, "name": user}}
            }
        }
        response = requests.post(url, json=body, headers={"Content-Type": "application/json"})
        token = response.headers['X-Subject-Token']
        logger.debug(f"获取Token值: {token}")
        return {"X-Auth-Token": token}
    except Exception as e:
        logger.error(f"获取Token失败: {e}")
        exit(1)

# 角色管理类
class RoleManager:
    def __init__(self, headers, url):
        self.headers = headers
        self.url = url

    def create_role(self, name):
        """创建角色"""
        body = {"role": {"name": name}}
        response = requests.post(self.url, json=body, headers=self.headers)
        logger.debug(f"创建角色响应: {response.text}")
        return response.text

    def get_roles(self):
        """获取所有角色"""
        response = requests.get(self.url, headers=self.headers)
        logger.debug(f"查询角色响应: {response.text}")
        return response.text

    def get_role_id(self, name):
        """根据角色名称获取角色ID"""
        roles = json.loads(self.get_roles())['roles']
        return next((role['id'] for role in roles if role['name'] == name), "NONE")

    def update_role(self, role_id, name):
        """更新角色名称"""
        body = {"role": {"name": name}}
        api_url = f"{self.url}/{role_id}"
        response = requests.patch(api_url, json=body, headers=self.headers)
        logger.debug(f"更新角色响应: {response.text}")
        return response.text

    def delete_role(self, role_id):
        """删除角色"""
        api_url = f"{self.url}/{role_id}"
        response = requests.delete(api_url, headers=self.headers)
        if response.status_code == 204:
            return {"删除成功": response.status_code}
        return response.json()

# 主函数
if __name__ == '__main__':
    # 控制节点认证信息
    controller_ip = "192.168.200.160"
    domain = "default"
    user = "admin"
    password = "000000"
    # 获取认证头部
    headers = get_auth_token(controller_ip, domain, user, password)
    # 初始化角色管理
    role_manager = RoleManager(headers, f"http://{controller_ip}:5000/v3/roles")
    # 查询所有角色
    print("所有角色:", role_manager.get_roles())
    # 创建新角色
    role_name = "test_role"
    print("创建角色:", role_manager.create_role(role_name))
    # 获取角色ID
    role_id = role_manager.get_role_id(role_name)
    print(f"角色 '{role_name}' 的ID:", role_id)
    # 更新角色名称
    new_role_name = "updated_test_role"
    print("更新角色名称:", role_manager.update_role(role_id, new_role_name))
    # 删除角色
    print("删除角色:", role_manager.delete_role(role_id))

角色操作:

  • 查看角色:调用 GET /v3/roles 接口,列出所有角色。
  • 创建角色:调用 POST /v3/roles 接口,传入角色名称。
  • 更新角色:调用 PATCH /v3/roles/{role_id} 接口,更新角色的名称。
  • 删除角色:调用 DELETE /v3/roles/{role_id} 接口,删除指定角色

测试

vi keystone_role_api.py
root@controller:~# python3 keystone_role_api.py
2025-01-15 06:16:40,785 Starting new HTTP connection (1): 192.168.200.160:5000
2025-01-15 06:16:41,106 http://192.168.200.160:5000 "POST /v3/auth/tokens HTTP/1.1" 201 3476
2025-01-15 06:16:41,106 获取Token值: gAAAAABnh1LJE9IjD7hKQqxEwLZye0vX-71YObS3Aak4vOyKDN24T56SGXH3370mYXhrplzAIAVqIQXek3gHReQINYuxHsjtYGkiam72SJkP8w6dPw9f1cQKltl1_bndfy3CWon28ePYmUnOmeV_tY7vYXXDwwaCrvKdq1XIMp_KMuYnrLqDVgw
2025-01-15 06:16:41,107 Starting new HTTP connection (1): 192.168.200.160:5000
2025-01-15 06:16:41,381 http://192.168.200.160:5000 "GET /v3/roles HTTP/1.1" 200 1234
2025-01-15 06:16:41,381 查询角色响应: {"roles": [{"id": "3e15391b2c87436a82de12fbb7982681", "name": "reader", "domain_id": null, "description": null, "options": {"immutable": true}, "links": {"self": "http://192.168.200.160:5000/v3/roles/3e15391b2c87436a82de12fbb7982681"}}, {"id": "522d6975aaab42608d9be994ee25b498", "name": "service", "domain_id": null, "description": null, "options": {"immutable": true}, "links": {"self": "http://192.168.200.160:5000/v3/roles/522d6975aaab42608d9be994ee25b498"}}, {"id": "749691b26ec14b07a54e675ff7ec6184", "name": "manager", "domain_id": null, "description": null, "options": {"immutable": true}, "links": {"self": "http://192.168.200.160:5000/v3/roles/749691b26ec14b07a54e675ff7ec6184"}}, {"id": "8b70169a0e534455902ed185b6791059", "name": "admin", "domain_id": null, "description": null, "options": {"immutable": true}, "links": {"self": "http://192.168.200.160:5000/v3/roles/8b70169a0e534455902ed185b6791059"}}, {"id": "e6ff70a44a774213a729322e0530399b", "name": "member", "domain_id": null, "description": null, "options": {"immutable": true}, "links": {"self": "http://192.168.200.160:5000/v3/roles/e6ff70a44a774213a729322e0530399b"}}], "links": {"next": null, "self": "http://192.168.200.160:5000/v3/roles", "previous": null}}

所有角色: {"roles": [{"id": "3e15391b2c87436a82de12fbb7982681", "name": "reader", "domain_id": null, "description": null, "options": {"immutable": true}, "links": {"self": "http://192.168.200.160:5000/v3/roles/3e15391b2c87436a82de12fbb7982681"}}, {"id": "522d6975aaab42608d9be994ee25b498", "name": "service", "domain_id": null, "description": null, "options": {"immutable": true}, "links": {"self": "http://192.168.200.160:5000/v3/roles/522d6975aaab42608d9be994ee25b498"}}, {"id": "749691b26ec14b07a54e675ff7ec6184", "name": "manager", "domain_id": null, "description": null, "options": {"immutable": true}, "links": {"self": "http://192.168.200.160:5000/v3/roles/749691b26ec14b07a54e675ff7ec6184"}}, {"id": "8b70169a0e534455902ed185b6791059", "name": "admin", "domain_id": null, "description": null, "options": {"immutable": true}, "links": {"self": "http://192.168.200.160:5000/v3/roles/8b70169a0e534455902ed185b6791059"}}, {"id": "e6ff70a44a774213a729322e0530399b", "name": "member", "domain_id": null, "description": null, "options": {"immutable": true}, "links": {"self": "http://192.168.200.160:5000/v3/roles/e6ff70a44a774213a729322e0530399b"}}], "links": {"next": null, "self": "http://192.168.200.160:5000/v3/roles", "previous": null}}

2025-01-15 06:16:41,384 Starting new HTTP connection (1): 192.168.200.160:5000
2025-01-15 06:16:41,538 http://192.168.200.160:5000 "POST /v3/roles HTTP/1.1" 201 221
2025-01-15 06:16:41,538 创建角色响应: {"role": {"id": "51f751d675ce4df78c50016992b38198", "name": "test_role", "domain_id": null, "description": null, "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/roles/51f751d675ce4df78c50016992b38198"}}}

创建角色: {"role": {"id": "51f751d675ce4df78c50016992b38198", "name": "test_role", "domain_id": null, "description": null, "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/roles/51f751d675ce4df78c50016992b38198"}}}

2025-01-15 06:16:41,540 Starting new HTTP connection (1): 192.168.200.160:5000
2025-01-15 06:16:41,682 http://192.168.200.160:5000 "GET /v3/roles HTTP/1.1" 200 1446
2025-01-15 06:16:41,682 查询角色响应: {"roles": [{"id": "3e15391b2c87436a82de12fbb7982681", "name": "reader", "domain_id": null, "description": null, "options": {"immutable": true}, "links": {"self": "http://192.168.200.160:5000/v3/roles/3e15391b2c87436a82de12fbb7982681"}}, {"id": "51f751d675ce4df78c50016992b38198", "name": "test_role", "domain_id": null, "description": null, "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/roles/51f751d675ce4df78c50016992b38198"}}, {"id": "522d6975aaab42608d9be994ee25b498", "name": "service", "domain_id": null, "description": null, "options": {"immutable": true}, "links": {"self": "http://192.168.200.160:5000/v3/roles/522d6975aaab42608d9be994ee25b498"}}, {"id": "749691b26ec14b07a54e675ff7ec6184", "name": "manager", "domain_id": null, "description": null, "options": {"immutable": true}, "links": {"self": "http://192.168.200.160:5000/v3/roles/749691b26ec14b07a54e675ff7ec6184"}}, {"id": "8b70169a0e534455902ed185b6791059", "name": "admin", "domain_id": null, "description": null, "options": {"immutable": true}, "links": {"self": "http://192.168.200.160:5000/v3/roles/8b70169a0e534455902ed185b6791059"}}, {"id": "e6ff70a44a774213a729322e0530399b", "name": "member", "domain_id": null, "description": null, "options": {"immutable": true}, "links": {"self": "http://192.168.200.160:5000/v3/roles/e6ff70a44a774213a729322e0530399b"}}], "links": {"next": null, "self": "http://192.168.200.160:5000/v3/roles", "previous": null}}

角色 'test_role' 的ID: 51f751d675ce4df78c50016992b38198
2025-01-15 06:16:41,683 Starting new HTTP connection (1): 192.168.200.160:5000
2025-01-15 06:16:41,853 http://192.168.200.160:5000 "PATCH /v3/roles/51f751d675ce4df78c50016992b38198 HTTP/1.1" 200 229
2025-01-15 06:16:41,853 更新角色响应: {"role": {"id": "51f751d675ce4df78c50016992b38198", "name": "updated_test_role", "domain_id": null, "description": null, "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/roles/51f751d675ce4df78c50016992b38198"}}}

更新角色名称: {"role": {"id": "51f751d675ce4df78c50016992b38198", "name": "updated_test_role", "domain_id": null, "description": null, "options": {}, "links": {"self": "http://192.168.200.160:5000/v3/roles/51f751d675ce4df78c50016992b38198"}}}

2025-01-15 06:16:41,854 Starting new HTTP connection (1): 192.168.200.160:5000
2025-01-15 06:16:41,975 http://192.168.200.160:5000 "DELETE /v3/roles/51f751d675ce4df78c50016992b38198 HTTP/1.1" 204 0
删除角色: {'删除成功': 204}
root@controller:~#
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

huhy~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值