简体中文 繁體中文 English 日本語 Deutsch 한국 사람 بالعربية TÜRKÇE português คนไทย Français

站内搜索

搜索

活动公告

11-02 12:46
10-23 09:32
通知:本站资源由网友上传分享,如有违规等问题请到版务模块进行投诉,将及时处理!
10-23 09:31
10-23 09:28
通知:签到时间调整为每日4:00(东八区)
10-23 09:26

深入浅出Ansible模块化开发实战教程掌握自动化运维的核心技术与最佳实践

3万

主题

424

科技点

3万

积分

大区版主

木柜子打湿

积分
31917

三倍冰淇淋无人之境【一阶】财Doro小樱(小丑装)立华奏以外的星空【二阶】⑨的冰沙

发表于 2025-10-4 21:50:10 | 显示全部楼层 |阅读模式 [标记阅至此楼]

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
引言

Ansible作为当前最流行的自动化运维工具之一,以其简单易用、无代理架构和强大的功能赢得了众多运维工程师的青睐。在Ansible的生态系统中,模块是其核心组件,负责执行具体的任务。虽然Ansible提供了大量内置模块,但在实际工作中,我们经常需要开发自定义模块来满足特定需求。

本教程将深入浅出地介绍Ansible模块化开发的核心技术与最佳实践,帮助读者掌握如何开发、测试和分发自定义Ansible模块,从而扩展Ansible的功能,提高自动化运维的效率和灵活性。

Ansible基础

在深入模块化开发之前,让我们简要回顾一下Ansible的核心概念和架构。

Ansible架构

Ansible采用无代理架构,通过SSH或PowerShell远程管理节点。其核心组件包括:

• 控制节点(Control Node):安装Ansible并执行任务的机器
• 受管节点(Managed Nodes):被Ansible管理的远程机器
• 清单(Inventory):定义受管节点的文件
• Playbook:定义自动化任务的YAML文件
• 模块(Modules):执行具体任务的工具
• 插件(Plugins):扩展Ansible功能的组件

Ansible工作流程

Ansible的基本工作流程如下:

1. 用户在控制节点上编写Playbook
2. Ansible读取Playbook和清单文件
3. Ansible通过SSH或WinRM连接到受管节点
4. Ansible在受管节点上执行模块
5. 模块返回执行结果
6. Ansible收集结果并报告给用户

Ansible模块概述

什么是Ansible模块

Ansible模块是执行具体任务的独立单元,它们是Ansible的核心组件。每个模块负责一个特定的功能,如安装软件、管理文件、处理用户等。Ansible提供了大量的内置模块,同时也支持开发自定义模块。

模块的类型

Ansible模块可以分为以下几类:

1. 核心模块:由Ansible团队维护,随Ansible一起发布
2. 社区模块:由社区贡献和维护,通过Ansible Galaxy分发
3. 自定义模块:用户自己开发的模块,用于满足特定需求

模块的工作原理

当Ansible执行一个任务时,它会:

1. 将模块代码传输到受管节点
2. 在受管节点上执行模块
3. 收集模块的输出
4. 根据输出决定是否报告变更

开发环境准备

在开始开发Ansible模块之前,我们需要准备合适的开发环境。

系统要求

• 控制节点:安装了Ansible的Linux或macOS系统
• Python环境:Ansible模块通常使用Python开发,建议使用Python 3.5+
• 文本编辑器或IDE:如VS Code、PyCharm等

安装Ansible

在控制节点上安装Ansible:
  1. # 在Ubuntu/Debian上
  2. sudo apt update
  3. sudo apt install ansible
  4. # 在CentOS/RHEL上
  5. sudo yum install ansible
  6. # 使用pip安装
  7. pip install ansible
复制代码

创建开发目录结构

创建一个合理的目录结构有助于组织模块代码:
  1. ansible_module_dev/
  2. ├── library/           # 自定义模块存放位置
  3. ├── module_utils/     # 模块工具函数
  4. ├── test/             # 测试文件
  5. ├── docs/             # 文档
  6. └── examples/         # 示例Playbook
复制代码

自定义模块开发

现在,让我们深入探讨如何开发自定义Ansible模块。

模块的基本结构

一个基本的Ansible模块通常包含以下部分:

1. 文档字符串:描述模块的功能、参数和返回值
2. 参数解析:处理传入的参数
3. 核心逻辑:实现模块功能
4. 返回值:返回执行结果

使用Python开发模块

Python是开发Ansible模块最常用的语言。下面是一个简单的Python模块示例:
  1. #!/usr/bin/python
  2. # 文档字符串
  3. ANSIBLE_METADATA = {
  4.     'metadata_version': '1.1',
  5.     'status': ['preview'],
  6.     'supported_by': 'community'
  7. }
  8. DOCUMENTATION = '''
  9. ---
  10. module: my_test_module
  11. short_description: This is my test module
  12. description:
  13.     - "This is my test module"
  14. version_added: "2.4"
  15. author:
  16.     - "Your Name (@yourhandle)"
  17. options:
  18.     name:
  19.         description:
  20.             - This is the message to send to the test module
  21.         required: true
  22.     new:
  23.         description:
  24.             - Control to demo if the result of this module is changed or not
  25.         required: false
  26. '''
  27. EXAMPLES = '''
  28. # Pass in a message
  29. - name: Test with a message
  30.   my_test_module:
  31.     name: hello world
  32. # pass in a message and have changed true
  33. - name: Test with a message and changed output
  34.   my_test_module:
  35.     name: hello world
  36.     new: true
  37. '''
  38. RETURN = '''
  39. original_message:
  40.     description: The original name param that was passed in
  41.     type: str
  42. message:
  43.     description: The output message that the test module generates
  44.     type: str
  45. '''
  46. # 导入Ansible模块
  47. from ansible.module_utils.basic import AnsibleModule
  48. def run_module():
  49.     # 定义模块参数
  50.     module_args = dict(
  51.         name=dict(type='str', required=True),
  52.         new=dict(type='bool', required=False, default=False)
  53.     )
  54.     # 创建AnsibleModule实例
  55.     result = dict(
  56.         changed=False,
  57.         original_message='',
  58.         message=''
  59.     )
  60.     module = AnsibleModule(
  61.         argument_spec=module_args,
  62.         supports_check_mode=True
  63.     )
  64.     # 获取参数
  65.     name = module.params['name']
  66.     new = module.params['new']
  67.     # 检查模式
  68.     if module.check_mode:
  69.         return result
  70.     # 执行模块逻辑
  71.     result['original_message'] = name
  72.     result['message'] = 'goodbye'
  73.     if new:
  74.         result['changed'] = True
  75.     # 返回结果
  76.     module.exit_json(**result)
  77. def main():
  78.     run_module()
  79. if __name__ == '__main__':
  80.     main()
复制代码

模块参数处理

Ansible模块通常需要接受参数。使用AnsibleModule类可以方便地处理参数:
  1. # 定义参数规范
  2. module_args = dict(
  3.     name=dict(type='str', required=True),
  4.     age=dict(type='int', default=18),
  5.     path=dict(type='path'),
  6.     state=dict(type='str', choices=['present', 'absent'], default='present'),
  7.     enabled=dict(type='bool', default=False)
  8. )
  9. # 创建模块实例
  10. module = AnsibleModule(
  11.     argument_spec=module_args,
  12.     supports_check_mode=True
  13. )
  14. # 获取参数值
  15. name = module.params['name']
  16. age = module.params['age']
  17. path = module.params['path']
  18. state = module.params['state']
  19. enabled = module.params['enabled']
复制代码

模块返回值

模块执行完成后,需要返回结果。使用module.exit_json()或module.fail_json()返回结果:
  1. # 成功返回
  2. module.exit_json(
  3.     changed=True,
  4.     message="Operation completed successfully",
  5.     data={
  6.         "key1": "value1",
  7.         "key2": "value2"
  8.     }
  9. )
  10. # 失败返回
  11. module.fail_json(
  12.     msg="Failed to complete operation",
  13.     error="Detailed error message"
  14. )
复制代码

错误处理

良好的错误处理是模块开发的重要部分:
  1. try:
  2.     # 尝试执行操作
  3.     result = some_operation()
  4. except SomeException as e:
  5.     # 捕获异常并返回错误
  6.     module.fail_json(msg="Failed to execute operation: %s" % str(e))
复制代码

使用其他语言开发模块

虽然Python是开发Ansible模块的首选语言,但也可以使用其他语言。下面是一个使用Bash开发的模块示例:
  1. #!/bin/bash
  2. # 读取参数
  3. source $1
  4. # 检查必需参数
  5. if [ -z "$name" ]; then
  6.     echo '{"failed": true, "msg": "missing required argument: name"}'
  7.     exit 1
  8. fi
  9. # 执行操作
  10. echo "Hello, $name" > /tmp/hello.txt
  11. # 返回结果
  12. echo '{"changed": true, "msg": "Hello message created"}'
复制代码

模块测试与调试

开发完成后,需要对模块进行测试和调试。

使用ansible-test工具

Ansible提供了ansible-test工具用于测试模块:
  1. # 安装测试依赖
  2. ansible-test sanity --test validate-modules
  3. # 运行单元测试
  4. ansible-test units --python 3.8
  5. # 运行集成测试
  6. ansible-test integration --python 3.8
复制代码

手动测试模块

可以使用ansible命令直接测试模块:
  1. # 测试本地模块
  2. ansible localhost -m my_test_module -a "name='hello world'"
  3. # 测试远程模块
  4. ansible server1 -m my_test_module -a "name='hello world'"
复制代码

调试模块

调试Ansible模块可以使用以下方法:

1. 启用详细输出:
  1. ansible localhost -m my_test_module -a "name='hello world'" -vvv
复制代码

1. 使用日志文件:
  1. # 在ansible.cfg中设置
  2. [defaults]
  3. log_path = /var/log/ansible.log
复制代码

1. 添加调试输出:
  1. # 在模块中添加调试输出
  2. import sys
  3. print("Debug information", file=sys.stderr)
复制代码

模块打包与分发

开发完成后,我们需要将模块打包并分发给其他用户或系统。

使用Ansible角色

将自定义模块打包到Ansible角色中是一种常见的分发方式:
  1. my_role/
  2. ├── library/
  3. │   └── my_test_module.py
  4. ├── defaults/
  5. │   └── main.yml
  6. ├── tasks/
  7. │   └── main.yml
  8. └── ...
复制代码

使用Ansible集合

Ansible 2.10+支持集合(Collections),这是一种更现代的模块分发方式:
  1. my_collection/
  2. ├── galaxy.yml
  3. ├── plugins/
  4. │   └── modules/
  5. │       └── my_test_module.py
  6. └── ...
复制代码

构建和发布集合:
  1. # 构建集合
  2. ansible-galaxy collection build
  3. # 发布集合
  4. ansible-galaxy collection publish my_collection-1.0.0.tar.gz --api-key=YOUR_API_KEY
复制代码

使用本地模块路径

在Playbook中指定本地模块路径:
  1. - hosts: all
  2.   tasks:
  3.     - name: Use custom module
  4.       my_test_module:
  5.         name: "hello world"
  6.       module_path: "./library"
复制代码

最佳实践

在开发Ansible模块时,遵循以下最佳实践可以提高模块的质量和可维护性。

模块设计原则

1. 幂等性:模块应该具有幂等性,即多次执行相同操作的结果应该一致。
2. 最小惊讶原则:模块的行为应该符合用户预期。
3. 单一职责:每个模块应该只负责一个明确的功能。
4. 向后兼容:新版本的模块应该保持与旧版本的兼容性。

文档和示例

1. 完整的文档:提供详细的模块文档,包括参数说明、返回值和示例。
2. 示例代码:提供清晰的使用示例,帮助用户理解如何使用模块。
3. 版本信息:明确标注模块的版本信息和兼容性。

错误处理和日志

1. 明确的错误信息:提供清晰的错误信息,帮助用户诊断问题。
2. 适当的日志级别:使用适当的日志级别记录信息。
3. 异常处理:妥善处理可能出现的异常情况。

性能考虑

1. 最小化资源使用:避免不必要的资源消耗。
2. 批量操作:支持批量操作以提高效率。
3. 连接复用:尽可能复用连接以减少开销。

安全考虑

1. 输入验证:对所有输入参数进行验证。
2. 敏感信息处理:妥善处理敏感信息,避免在日志中泄露。
3. 权限控制:遵循最小权限原则。

实战案例

通过一个实际的案例,让我们更好地理解Ansible模块化开发的过程。

案例:开发一个Nginx配置管理模块

假设我们需要开发一个模块来管理Nginx的虚拟主机配置。这个模块应该能够创建、更新和删除Nginx虚拟主机配置文件,并在修改后重新加载Nginx配置。

首先,我们需要设计模块的参数和功能:

参数:

• name:虚拟主机名称(必需)
• port:监听端口(默认:80)
• server_name:服务器名称(默认:与name相同)
• root:网站根目录(必需)
• index:索引文件(默认:index.html)
• state:状态(present/absent,默认:present)
• enabled:是否启用(默认:true)

功能:

• 创建或更新Nginx虚拟主机配置文件
• 启用或禁用虚拟主机
• 删除虚拟主机配置
• 重新加载Nginx配置

下面是模块的实现代码:
  1. #!/usr/bin/python
  2. ANSIBLE_METADATA = {
  3.     'metadata_version': '1.1',
  4.     'status': ['preview'],
  5.     'supported_by': 'community'
  6. }
  7. DOCUMENTATION = '''
  8. ---
  9. module: nginx_vhost
  10. short_description: Manage Nginx virtual hosts
  11. description:
  12.     - This module manages Nginx virtual host configuration files.
  13. version_added: "2.9"
  14. author:
  15.     - "Your Name (@yourhandle)"
  16. options:
  17.     name:
  18.         description:
  19.             - Name of the virtual host.
  20.         required: true
  21.     port:
  22.         description:
  23.             - Port the virtual host should listen on.
  24.         default: 80
  25.     server_name:
  26.         description:
  27.             - Server name of the virtual host.
  28.         default: Same as name
  29.     root:
  30.         description:
  31.             - Root directory of the virtual host.
  32.         required: true
  33.     index:
  34.         description:
  35.             - Index files for the virtual host.
  36.         default: "index.html"
  37.     state:
  38.         description:
  39.             - Whether the virtual host should be present or absent.
  40.         choices: [ "present", "absent" ]
  41.         default: "present"
  42.     enabled:
  43.         description:
  44.             - Whether the virtual host should be enabled.
  45.         type: bool
  46.         default: true
  47. '''
  48. EXAMPLES = '''
  49. - name: Create a basic virtual host
  50.   nginx_vhost:
  51.     name: example.com
  52.     root: /var/www/example.com
  53. - name: Create a virtual host with custom port and server name
  54.   nginx_vhost:
  55.     name: mysite
  56.     port: 8080
  57.     server_name: mysite.example.com
  58.     root: /var/www/mysite
  59. - name: Disable a virtual host
  60.   nginx_vhost:
  61.     name: example.com
  62.     root: /var/www/example.com
  63.     enabled: false
  64. - name: Remove a virtual host
  65.   nginx_vhost:
  66.     name: example.com
  67.     root: /var/www/example.com
  68.     state: absent
  69. '''
  70. RETURN = '''
  71. config_file:
  72.     description: Path to the virtual host configuration file.
  73.     type: str
  74.     returned: success
  75. enabled:
  76.     description: Whether the virtual host is enabled.
  77.     type: bool
  78.     returned: success
  79. state:
  80.     description: State of the virtual host.
  81.     type: str
  82.     returned: success
  83. '''
  84. import os
  85. import shutil
  86. from ansible.module_utils.basic import AnsibleModule
  87. def write_config_file(module, config_path, params):
  88.     """Write the Nginx configuration file."""
  89.     config_content = """
  90. server {{
  91.     listen {port};
  92.     server_name {server_name};
  93.     root {root};
  94.     index {index};
  95.     location / {{
  96.         try_files $uri $uri/ =404;
  97.     }}
  98. }}
  99. """.format(
  100.         port=params['port'],
  101.         server_name=params['server_name'],
  102.         root=params['root'],
  103.         index=params['index']
  104.     )
  105.     # Check if the file exists and if the content is different
  106.     if os.path.exists(config_path):
  107.         with open(config_path, 'r') as f:
  108.             existing_content = f.read()
  109.         
  110.         if existing_content.strip() == config_content.strip():
  111.             return False  # No changes needed
  112.    
  113.     # Write the new config
  114.     try:
  115.         with open(config_path, 'w') as f:
  116.             f.write(config_content)
  117.         return True  # File was changed
  118.     except Exception as e:
  119.         module.fail_json(msg="Failed to write config file: %s" % str(e))
  120. def enable_vhost(module, config_path, enabled_path):
  121.     """Enable the virtual host by creating a symlink."""
  122.     if os.path.exists(enabled_path):
  123.         if os.path.islink(enabled_path) and os.path.realpath(enabled_path) == config_path:
  124.             return False  # Already enabled
  125.         else:
  126.             # Remove existing file/symlink
  127.             try:
  128.                 os.remove(enabled_path)
  129.             except Exception as e:
  130.                 module.fail_json(msg="Failed to remove existing enabled link: %s" % str(e))
  131.    
  132.     # Create the symlink
  133.     try:
  134.         os.symlink(config_path, enabled_path)
  135.         return True  # Enabled
  136.     except Exception as e:
  137.         module.fail_json(msg="Failed to enable virtual host: %s" % str(e))
  138. def disable_vhost(module, enabled_path):
  139.     """Disable the virtual host by removing the symlink."""
  140.     if not os.path.exists(enabled_path):
  141.         return False  # Already disabled
  142.    
  143.     try:
  144.         os.remove(enabled_path)
  145.         return True  # Disabled
  146.     except Exception as e:
  147.         module.fail_json(msg="Failed to disable virtual host: %s" % str(e))
  148. def reload_nginx(module):
  149.     """Reload Nginx configuration."""
  150.     rc, out, err = module.run_command('nginx -s reload')
  151.     if rc != 0:
  152.         module.fail_json(msg="Failed to reload Nginx: %s" % err)
  153.     return True
  154. def run_module():
  155.     """Main module function."""
  156.     # Define module arguments
  157.     module_args = dict(
  158.         name=dict(type='str', required=True),
  159.         port=dict(type='int', default=80),
  160.         server_name=dict(type='str'),
  161.         root=dict(type='str', required=True),
  162.         index=dict(type='str', default='index.html'),
  163.         state=dict(type='str', default='present', choices=['present', 'absent']),
  164.         enabled=dict(type='bool', default=True)
  165.     )
  166.     # Create the Ansible module instance
  167.     module = AnsibleModule(
  168.         argument_spec=module_args,
  169.         supports_check_mode=True
  170.     )
  171.     # Get parameters
  172.     params = module.params
  173.     name = params['name']
  174.     port = params['port']
  175.     server_name = params['server_name'] or name
  176.     root = params['root']
  177.     index = params['index']
  178.     state = params['state']
  179.     enabled = params['enabled']
  180.     # Define paths
  181.     sites_available = '/etc/nginx/sites-available'
  182.     sites_enabled = '/etc/nginx/sites-enabled'
  183.     config_path = os.path.join(sites_available, name)
  184.     enabled_path = os.path.join(sites_enabled, name)
  185.     # Initialize result
  186.     result = dict(
  187.         changed=False,
  188.         config_file=config_path,
  189.         state=state,
  190.         enabled=enabled
  191.     )
  192.     # Check mode
  193.     if module.check_mode:
  194.         module.exit_json(**result)
  195.     # Handle state=absent
  196.     if state == 'absent':
  197.         # Disable if enabled
  198.         if os.path.exists(enabled_path):
  199.             if disable_vhost(module, enabled_path):
  200.                 result['changed'] = True
  201.                 result['enabled'] = False
  202.         
  203.         # Remove config file if exists
  204.         if os.path.exists(config_path):
  205.             try:
  206.                 os.remove(config_path)
  207.                 result['changed'] = True
  208.                 result['state'] = 'absent'
  209.             except Exception as e:
  210.                 module.fail_json(msg="Failed to remove config file: %s" % str(e))
  211.         
  212.         # Reload Nginx if changed
  213.         if result['changed']:
  214.             reload_nginx(module)
  215.         
  216.         module.exit_json(**result)
  217.     # Handle state=present
  218.     # Ensure sites-available directory exists
  219.     if not os.path.exists(sites_available):
  220.         try:
  221.             os.makedirs(sites_available)
  222.         except Exception as e:
  223.             module.fail_json(msg="Failed to create sites-available directory: %s" % str(e))
  224.     # Write config file
  225.     if write_config_file(module, config_path, params):
  226.         result['changed'] = True
  227.     # Enable or disable the virtual host
  228.     if enabled:
  229.         if enable_vhost(module, config_path, enabled_path):
  230.             result['changed'] = True
  231.     else:
  232.         if disable_vhost(module, enabled_path):
  233.             result['changed'] = True
  234.             result['enabled'] = False
  235.     # Reload Nginx if changed
  236.     if result['changed']:
  237.         reload_nginx(module)
  238.     module.exit_json(**result)
  239. def main():
  240.     run_module()
  241. if __name__ == '__main__':
  242.     main()
复制代码

下面是如何在Playbook中使用这个模块的示例:
  1. ---
  2. - name: Manage Nginx virtual hosts
  3.   hosts: webservers
  4.   become: yes
  5.   
  6.   tasks:
  7.     - name: Ensure Nginx is installed
  8.       package:
  9.         name: nginx
  10.         state: present
  11.    
  12.     - name: Ensure Nginx is running
  13.       service:
  14.         name: nginx
  15.         state: started
  16.         enabled: yes
  17.    
  18.     - name: Create web root directory
  19.       file:
  20.         path: /var/www/example.com
  21.         state: directory
  22.         mode: '0755'
  23.    
  24.     - name: Create index.html
  25.       copy:
  26.         content: "<html><body><h1>Hello from example.com</h1></body></html>"
  27.         dest: /var/www/example.com/index.html
  28.    
  29.     - name: Create Nginx virtual host
  30.       nginx_vhost:
  31.         name: example.com
  32.         root: /var/www/example.com
  33.         port: 80
  34.         server_name: example.com www.example.com
  35.         state: present
  36.         enabled: yes
  37.    
  38.     - name: Create another virtual host
  39.       nginx_vhost:
  40.         name: test.example.com
  41.         root: /var/www/test.example.com
  42.         port: 8080
  43.         state: present
  44.         enabled: yes
复制代码

我们可以使用以下命令测试模块:
  1. # 测试模块语法
  2. ansible-playbook --syntax-check nginx_vhost_test.yml
  3. # 检查模式运行
  4. ansible-playbook -C nginx_vhost_test.yml
  5. # 实际运行
  6. ansible-playbook nginx_vhost_test.yml
复制代码

这个模块可以进一步扩展,例如:

1. SSL支持:添加SSL证书配置功能
2. 日志配置:自定义访问和错误日志路径
3. 重定向规则:添加URL重定向功能
4. PHP支持:添加PHP-FPM配置
5. 访问控制:添加IP白名单/黑名单功能

通过这个案例,我们可以看到如何从需求分析到设计、实现、测试和扩展一个完整的Ansible模块。这个过程展示了Ansible模块化开发的核心技术和最佳实践。

总结与展望

总结

本教程深入浅出地介绍了Ansible模块化开发的核心技术与最佳实践。我们学习了:

1. Ansible模块的基本概念和工作原理
2. 如何设置模块开发环境
3. 使用Python开发自定义模块的方法
4. 模块测试与调试的技巧
5. 模块打包与分发的策略
6. 模块开发的最佳实践
7. 通过实战案例展示了完整的模块开发过程

通过掌握这些知识和技能,你可以开发出高质量、可维护的Ansible模块,扩展Ansible的功能,提高自动化运维的效率和灵活性。

展望

随着自动化运维的不断发展,Ansible模块化开发也在不断演进。未来,我们可以期待以下趋势:

1. 更多语言支持:除了Python,可能会有更多语言支持Ansible模块开发
2. 更好的开发工具:更强大的IDE支持和调试工具
3. 云原生集成:与Kubernetes、Serverless等云原生技术的深度集成
4. AI增强:利用AI技术简化模块开发和优化自动化流程
5. 更强的安全性:更严格的安全检查和更安全的模块执行机制

作为Ansible用户和开发者,我们应该持续关注这些发展趋势,不断学习和实践,以充分利用Ansible在自动化运维领域的强大能力。

通过本教程的学习,相信你已经掌握了Ansible模块化开发的核心技术和最佳实践。现在,你可以开始开发自己的Ansible模块,解决实际工作中的自动化挑战,提高运维效率和质量。祝你在Ansible模块化开发的旅程中取得成功!
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

频道订阅

频道订阅

加入社群

加入社群

联系我们|TG频道|RSS

Powered by Pixtech

© 2025 Pixtech Team.