一、介绍

1. 自动化运维工具对比

  • Puppet:基于 Ruby,C/S 架构,可扩展强;远程命令执行偏弱。
  • SaltStack:基于 Python,C/S 架构,较轻量,配置用 YAML;需要在每台被控端安装 agent。
  • Ansible:基于 Python,分布式,无客户端,轻量;YAML + Jinja2;远程命令执行强。默认通过 SSH 连接。

通俗理解:Ansible 更容易“开箱即用”,无需在被控端安装东西(免 agent),非常适合快速批量操作与配置编排。

2. Ansible 简介与特性

  • Ansible 是自动化运维工具,基于模块工作(核心是模块,Ansible 负责编排和分发调用)。
  • 特性:
    1. no agents:被控端无需安装客户端;升级只需升级控制端。
    2. no server:无中心服务端,直接用命令/剧本即可。
    3. modules in any languages:模块可用任意语言实现。
    4. yaml, not code:用 YAML 写剧本(Playbook),更易读。
    5. ssh by default:默认基于 SSH 连接。
  • 组件简述:
    • connection plugins:连接插件,默认 SSH。
    • host inventory:主机清单,定义被控主机与分组。
    • modules:模块(command、copy、user…)。
    • plugins:插件(连接、回调、邮件等)。
    • playbook:编排多任务的 YAML 定义。
  • 默认并发数:5(可在 ansible.cfg 中调整 forks)。

3. 变量优先级(从高到低)

  1. 命令行变量:-e/--extra-vars(最高)。
  2. Inventory 中的主机/组变量。
  3. Playbook 内的 varsvars_files
  4. Facts(setup 收集)。
  5. 角色的默认变量:roles/*/defaults/main.yml 最低。

注:新写法建议使用 ansible_user/ansible_password/ansible_port/ansible_ssh_private_key_file,不再推荐旧名 ansible_ssh_user/ansible_ssh_pass(兼容但逐步淘汰)。


二、Ansible 安装(CentOS 7)

1. 环境准备

  • 关闭防火墙与 SELinux(生产建议按需放行端口而非直接关闭,这里为教学简化)。
  • 控制节点 1 台,被控节点若干。示例:
# /etc/hosts(各机均添加,便于主机名解析)
192.168.1.10 ansible-web1
192.168.1.11 ansible-web2
192.168.1.12 ansible-web3
192.168.1.9  ansible-server   # 控制端
  • SSH 免密:在控制端生成密钥并分发至各被控端。
ssh-keygen -t rsa -b 4096 -C "ansible@server"
ssh-copy-id -i ~/.ssh/id_rsa.pub 192.168.1.10
ssh-copy-id -i ~/.ssh/id_rsa.pub 192.168.1.11
ssh-copy-id -i ~/.ssh/id_rsa.pub 192.168.1.12

提示:无免密也可用密码登录,但不便于自动化;建议配置免密。

2. 安装步骤

CentOS 7 默认 Python 2.7,但新版本 Ansible 更推荐 Python 3。常见两种方式:

  • 方式 A(简单):EPEL + yum 安装 Ansible(一般为 2.9.x),可在 Python 2.7 下运行。
yum install -y epel-release
yum install -y ansible
ansible --version
ansible --help
  • 方式 B(推荐):安装 Python 3(EPEL 或 IUS),用 pip 安装较新 Ansible。
yum install -y epel-release
yum install -y python36 python3-pip
pip3 install --upgrade pip
pip3 install "ansible<8"   # 在 CentOS 7 上兼容较佳的版本区间
ansible --version

说明:CentOS 7 年代较老,新版 Ansible 可能依赖较新 Python;如遇依赖冲突,选择方式 A 或锁定版本。

3. 基础配置与 Inventory

  • 查看默认配置文件位置:
rpm -qc ansible
# /etc/ansible/ansible.cfg
# /etc/ansible/hosts
  • 主配置:/etc/ansible/ansible.cfg(日志、forks、模块路径、回调插件等)。
  • 主机清单:/etc/ansible/hosts(建议直接使用 IP,避免 DNS 依赖)。

示例(单主机与分组):

# 直接添加主机
ansible-web1

# 添加主机组
[webservers]
192.168.1.11
ansible-web2

# 组合组(children)
[webservers1]
ansible-web1

[webservers2]
ansible-web2

[weball:children]
webservers1
webservers2

# 组变量(组内主机均生效)
[weball:vars]
ansible_user=root
ansible_port=22
# 使用私钥(如路径非默认时设置)
# ansible_ssh_private_key_file=/root/.ssh/id_rsa
# 若无免密,也可用密码(不安全,演示用)
# ansible_password=your_password

查看组内主机:

ansible weball --list-hosts

使用自定义 Inventory(文件 /opt/hostlist):

[all:vars]
ansible_user=root
ansible_port=22
# ansible_password=your_password

[all]
ansible-web1
ansible-web2

执行时指定:

ansible -i /opt/hostlist all -m ping -o

提示:ping 模块是 SSH 探活(非 ICMP),等价于“能否连上 22 端口并执行模块”。


三、Ad-Hoc 命令与常用模块

命令格式与常用选项

ansible <pattern> -m <module_name> -a <arguments> [其他选项]
  • pattern:主机/组名/IP/别名,all 为全部;支持通配与正则。
  • -m:模块名,默认 command
  • -a:模块参数。
  • -o:单行输出(更紧凑)。

ping 探活

# 单台
ansible ansible-web1 -m ping -o
# 多台
ansible ansible-web1,ansible-web2 -m ping -o
# 组
ansible webservers1 -m ping -o

shell vs command

  • shell:支持管道、重定向、通配符;适合复杂命令链。
  • command:直接执行命令,不经 shell;更高效/安全。
# 两者等效示例(简单命令)
ansible webservers1 -m shell -a 'uptime'
ansible webservers1 -a 'uptime'   # 默认 command

copy 复制与备份

常用参数:src/dest/owner/group/mode/backup

# 控制端 /root/a.txt -> 远端 /opt/ (保留权限)
ansible weball -m copy -a 'src=/root/a.txt dest=/opt owner=root group=root mode=0644' -o
# 覆盖时备份
ansible weball -m copy -a 'src=/root/a.txt dest=/opt/ owner=root group=root mode=0644 backup=yes' -o

user 用户管理

# 添加/删除用户
ansible ansible-web1 -m user -a "name=qianfeng"
ansible ansible-web1 -m user -a "name=qianfeng state=absent" -o
ansible ansible-web1 -m user -a "name=qianfeng state=absent remove=yes"  # 连同家目录/邮件删除

# 设置密码(需哈希值,而非明文)
python - <<'PY'
import crypt
print(crypt.crypt('12345678'))
PY
# 假设输出为 $6$.... 形如 /etc/shadow 的哈希
ansible ansible-web1 -m user -a "name=tom password='$6$...'"

# 生成 SSH 密钥对(在被控端为该用户创建 ~/.ssh/id_rsa*)
ansible ansible-web1 -m user -a "name=tom generate_ssh_key=yes"

yum 包管理

# 安装 httpd(最新)
ansible webservers1 -m yum -a "name=httpd state=latest" -o
# 卸载 httpd
ansible webservers1 -m yum -a "name=httpd state=removed" -o

常见 state

  • latest:安装最新
  • present/installed:确保已安装
  • absent/removed:卸载

service 服务管理

ansible webservers1 -m service -a "name=httpd state=started"      # 启动
ansible webservers1 -m service -a "name=httpd state=stopped"      # 停止
ansible webservers1 -m service -a "name=httpd state=restarted"    # 重启
ansible webservers1 -m service -a "name=httpd state=started enabled=yes" # 开机自启

提示:CentOS 7 使用 systemd;某些模块也支持 enabled 单独设置。

file 文件/目录/链接

# 创建空文件
ansible webservers1 -m file -a 'path=/tmp/88.txt mode=0777 state=touch'
# 创建目录
ansible webservers1 -m file -a 'path=/tmp/99 mode=0777 state=directory'

更多 Playbook 写法(更可读):

- name: Ensure testfile exists
  file:
    path: /tmp/testfile
    state: touch

- name: Ensure testdir exists
  file:
    path: /tmp/testdir
    state: directory
    owner: myuser
    group: mygroup
    mode: '0755'

- name: Remove testfile
  file:
    path: /tmp/testfile
    state: absent

- name: Create a symlink
  file:
    src: /tmp/original
    dest: /tmp/link
    state: link

- name: Recursively chmod
  file:
    path: /tmp/testdir
    state: directory
    mode: '0755'
    recurse: yes

script 远程执行本地脚本

适合“控制端有脚本,但被控端没有”的场景。Ansible 会把脚本临时传到远端执行。

# 本地脚本
cat > /root/test.sh <<'SH'
#!/usr/bin/env bash
touch test{1..50}
SH
chmod +x /root/test.sh

# 在远端 /mnt 目录下执行该脚本
ansible webservers1 -m script -a "chdir=/mnt /root/test.sh"

# 条件执行示例(存在某文件才执行/或不存在才执行)
cat > /root/awk.sh <<'SH'
#!/usr/bin/env bash
awk -F: '{print $1,$2}' /etc/passwd
SH
chmod +x /root/awk.sh

ansible webservers1 -m script -a "/root/awk.sh removes=/etc/passwd"

说明:removes=/path 表示当该路径存在时才执行;creates=/path 表示当该路径存在时跳过执行。

setup 收集 Facts

# 收集全部 facts
ansible webservers1 -m setup
# 仅过滤 IPv4 地址
ansible webservers1 -m setup -a 'filter=ansible_all_ipv4_addresses'

常用 facts:

  • ansible_all_ipv4_addressesansible_default_ipv4
  • ansible_distribution/ansible_kernel
  • ansible_processor_cores/ansible_mem_total
  • ansible_python_version/ansible_pkg_mgr

Playbook 收集示例:

- hosts: all
  tasks:
    - name: 收集硬件信息
      setup:
        gather_subset: hardware
    - name: 收集网络信息
      setup:
        gather_subset: network

archive 打包

ansible webservers1 -m archive -a "path=/etc dest=/mnt/$(date +%F)-etc.tar.gz format=gz"

支持 tar/zip/gz/bz2 等格式,常用参数:path/dest/format/owner/mode

unarchive 解压

# 从控制端复制并解压到远端
ansible all -m unarchive -a "src=/root/arc.tar.gz dest=/root/" --become

# 远端已有压缩包(不复制,只解压)
ansible all -m unarchive -a "src=/path/file.tar.gz dest=/path/ remote_src=yes"

# 覆盖已有
ansible all -m unarchive -a "src=/root/a.tar.gz dest=/root/ extra_opts=--overwrite"

cron 定时任务

# 新增定时任务
ansible webservers -m cron -a 'name="deletefile" minute="53" hour="20" day="7" month="5" job="rm -rf /mnt/*"'
# 删除定时任务
ansible webservers -m cron -a "name='deletefile' state=absent"

更多 Playbook 写法:

- name: 每日 2 点备份
  cron:
    name: "daily backup"
    minute: "0"
    hour: "2"
    job: "/usr/local/bin/backup.sh"

get_url 下载文件

ansible webservers -m get_url -a "url=https://download.redis.io/releases/redis-7.0.10.tar.gz dest=/mnt force=yes"

常用参数:url/dest/backup/force/url_username/url_password

yum_repository 管理仓库源

# 创建本地源
ansible webserver -m yum_repository -a "name='Centos Base' file=base description='test' baseurl=file:///mnt/centos enabled=yes gpgcheck=no"

# 配置阿里 EPEL 源
ansible webserver -m yum_repository -a "name=aliepel baseurl=https://mirrors.aliyun.com/epel/7/x86_64/ enabled=yes gpgcheck=yes gpgcakey=https://mirrors.aliyun.com/epel/RPM-GPG-KEY-EPEL-7 state=present file=AlicloudEpel description=alepel"

# 删除仓库
ansible webserver -m yum_repository -a "file=base name='Centos Base' state=absent"

lineinfile 修改/插入行

关键参数:path/line/state/regexp/insertafter/create/backup

用法场景:批量改配置文件的某行,如 PermitRootLogin yes

debug 调试输出

# 输出字符串
ansible webservers -m debug -a "msg=hello,beijing"
# 输出变量(通过 -e 传入)
ansible webservers -m debug -a "var=a" -e "a=1234567"

Playbook 中常配合 register 把任务结果存变量后再 debug 打印。


四、Ansible Playbook 剧本

Playbook 基础与结构

  • Playbook 用 YAML 描述,适合多步骤批量操作。
  • 一个 Playbook = 若干个 Play;一个 Play = 在一组主机上按序执行多个任务(tasks)。

示例:

---
- name: first play
  gather_facts: false            # 跳过 facts 可加速
  hosts: webservers
  remote_user: root              # 建议改用 become 提权
  tasks:
    - name: test connection
      ping:
    - name: disable selinux
      command: '/sbin/setenforce 0'
      ignore_errors: true
    - name: disable firewalld
      service: name=firewalld state=stopped
    - name: install httpd
      yum: name=httpd state=latest
    - name: install configuration file for httpd
      copy: src=/opt/httpd.conf dest=/etc/httpd/conf/httpd.conf
      notify: "restart httpd"          # 若变更则触发 handler
    - name: start httpd service
      service: enabled=true name=httpd state=started
  handlers:
    - name: restart httpd
      service: name=httpd state=restarted

提示(最佳实践):

  • 生产环境更推荐 become: yes 以普通用户登录再提权,少用 root 直连。
  • 文件路径/模板请用 templates/ + template 模块,便于变量化。

基础命令/输出颜色含义

ansible-playbook test.yml                     # 运行
ansible-playbook test.yml --syntax-check      # 语法检查
ansible-playbook test.yml --list-tasks        # 列出 tasks
ansible-playbook test.yml --list-hosts        # 列出主机
ansible-playbook test.yml --start-at-task 'install httpd'

颜色:绿色 success;黄色 changed(状态有变更);红色 failed(需排查)。

Tasks 示例

- hosts: webservers1
  user: root
  tasks:
    - name: create file
      file: state=touch mode=0777 path=/tmp/playbook.txt
    - name: create dir
      file: path=/mnt/dir12 state=directory

Handlers/Notify 触发器

  • 只有当某任务状态为 changed 时才触发对应 handler(并且在本 play 的所有普通任务结束后执行)。
- hosts: webservers1
  tasks:
    - name: test copy
      copy: src=/root/a.txt dest=/mnt
      notify: test handlers
  handlers:
    - name: test handlers
      shell: echo "abcd" >> /mnt/a.txt

常见用法:配置文件变更后“重启服务”。

循环/迭代(with_items/loop)

  • 字符串列表:
- hosts: websrvs
  tasks:
    - name: install packages
      yum:
        name: "{{ item }}"
        state: latest
      with_items:
        - httpd
        - httpd-tools
        - php
        - php-mysql
        - php-mbstring
        - php-gd
  • loop(推荐新写法)创建用户/组:
- hosts: test
  tasks:
    - name: Create Groups
      group:
        name: "{{ item }}"
      loop:
        - group1
        - group2
        - group3

    - name: Create Users
      user:
        name: "{{ item.user }}"
        group: "{{ item.group }}"
        uid: "{{ item.uid }}"
      loop:
        - { user: jack, group: group1, uid: 2001 }
        - { user: tom,  group: group2, uid: 2002 }
        - { user: alice,group: group3, uid: 2003 }
  • 批量安装/卸载:
- hosts: 192.168.157.129
  tasks:
    - name: install epel
      yum: name=epel-release state=latest
    - name: install packages
      yum:
        name: "{{ item }}"
        state: latest
      loop:
        - httpd
        - mysql
        - nginx
        - redis

自定义变量与 vars_files

  • 优势:更清晰、可复用、一处修改全局生效。
  • 变量文件示例:/etc/ansible/vars/file.yml
src_path: /root/test/a.txt
dest_path: /opt/test/
  • Playbook 引用:
- hosts: ansible-web1
  vars_files:
    - /etc/ansible/vars/file.yml
  tasks:
    - name: create directory
      file: path={{ dest_path }} mode=0755 state=directory
    - name: copy file
      copy: src={{ src_path }} dest={{ dest_path }}

分组与多 Play 示例(group 模块)

- hosts: webserver1
  tasks:
    - name: create a group
      group: name=mygrp system=yes
    - name: create a user
      user: name=tom group=mygrp system=yes

- hosts: webserver2
  tasks:
    - name: install apache
      yum: name=httpd state=latest
    - name: start httpd service
      service: name=httpd state=started

gather_facts 与变量示例

- hosts: ansible-web1
  gather_facts: false
  vars:
    user: jack
    src_path: /root/a.txt
    dest_path: /mnt/
  tasks:
    - name: create user
      user: name={{ user }}
    - name: copy file
      copy: src={{ src_path }} dest={{ dest_path }}

debug 调试 Playbook

- hosts: webserver
  tasks:
    - name: create file
      file: path=/mnt/debug.txt state=touch
      register: create_file
    - name: 输出创建过程
      debug:
        msg: "{{ create_file }}"

- hosts: webserver
  vars:
    user1: jack
  tasks:
    - name: 打印变量
      debug:
        var: user1

条件判断 when

  • 作用:按条件决定 task 执行与否。常用比较:> >= < <= == !=
  1. 根据主机名条件创建文件:
- hosts: webserver
  tasks:
    - name: create file when hostname is localhost
      file: path=/mnt/test1.txt state=touch
      register: create_file
      when: ansible_hostname == "localhost"
  1. 仅在 CentOS 上安装包:
- hosts: webserver
  tasks:
    - name: install package
      ignore_errors: true
      yum:
        name: "{{ item }}"
        state: latest
      loop:
        - nginx
        - redis
      when: ansible_distribution == "CentOS"
  1. 文件为空则写入内容:
- hosts: webserver
  tasks:
    - name: ensure file
      file: path=/opt/file.txt state=touch
    - name: check file content
      shell: cat /opt/file.txt
      register: check_file
    - name: insert hello when empty
      shell: echo "hello" >> /opt/file.txt
      when: check_file.stdout == ""
  1. 服务未启动则启动(以 mysqld 为例):
- hosts: webserver
  tasks:
    - name: check mysql status
      shell: systemctl is-active mysqld
      register: mysql_status
      ignore_errors: true
    - name: start mysql when inactive
      service: name=mysqld state=started
      when: mysql_status.rc != 0

五、项目综合实战:Nginx + PHP Web 部署(CentOS 7)

本章节给出一套可直接运行的“多机 Web 部署”案例,涵盖目录规范、配置、角色拆分与一键部署。

1. 目标与架构

  • 目标:在一组 web 主机上部署 Nginx + PHP-FPM,发布一个简单 PHP 应用。
  • 架构:
    • 控制端:Ansible(CentOS 7)
    • 被控端:CentOS 7(组名 web
    • 组件:Nginx 作为前端,转发到 PHP-FPM。

2. 目录结构

建议项目结构(便于环境区分与复用):

ansible-project/
├─ ansible.cfg
├─ site.yml                  # 顶层编排
├─ inventories/
│  ├─ prod/
│  │  ├─ hosts.ini
│  │  └─ group_vars/
│  │     └─ web.yml
│  └─ dev/
│     └─ hosts.ini
└─ roles/
   ├─ common/
   │  └─ tasks/main.yml
   ├─ nginx/
   │  ├─ tasks/main.yml
   │  ├─ handlers/main.yml
   │  └─ templates/
   │     ├─ nginx.conf.j2
   │     └─ vhost.conf.j2
   ├─ php/
   │  ├─ tasks/main.yml
   │  ├─ handlers/main.yml
   │  └─ templates/www.conf.j2
   └─ app/
      ├─ tasks/main.yml
      └─ templates/index.php.j2

快速初始化(在控制端执行,示意):

mkdir -p ansible-project/inventories/{prod,dev}/group_vars
mkdir -p ansible-project/roles/{common,nginx,php,app}/{tasks,handlers,templates}
touch ansible-project/{ansible.cfg,site.yml}
touch ansible-project/inventories/prod/hosts.ini
touch ansible-project/inventories/prod/group_vars/web.yml

3. 基础配置与清单

ansible-project/ansible.cfg

[defaults]
inventory = inventories/prod/hosts.ini
forks = 10
host_key_checking = False
timeout = 30
deprecation_warnings = False
log_path = ./ansible.log

inventories/prod/hosts.ini

[web]
192.168.1.10 ansible_user=root
192.168.1.11 ansible_user=root

[web:vars]
http_port=80
server_name=www.example.com
app_root=/var/www/app

inventories/prod/group_vars/web.yml(组变量):

http_port: 80
server_name: www.example.com
app_root: /var/www/app
php_fpm_listen: 127.0.0.1:9000
nginx_worker_processes: auto

4. 角色实现(common/nginx/php/app)

roles/common/tasks/main.yml:基础环境与常用工具。

---
- name: Ensure EPEL
  yum: name=epel-release state=present

- name: Install base tools
  yum:
    name:
      - vim-enhanced
      - curl
      - unzip
      - git
    state: present

- name: Set SELinux permissive (runtime)
  command: setenforce 0
  ignore_errors: true

- name: Disable firewalld (demo)
  service: name=firewalld state=stopped enabled=no
  ignore_errors: true

roles/nginx/tasks/main.yml

---
- name: Install nginx
  yum: name=nginx state=present

- name: Deploy nginx.conf
  template: src=nginx.conf.j2 dest=/etc/nginx/nginx.conf
  notify: Restart nginx

- name: Deploy vhost
  template: src=vhost.conf.j2 dest=/etc/nginx/conf.d/app.conf
  notify: Restart nginx

- name: Enable and start nginx
  service: name=nginx state=started enabled=yes

roles/nginx/handlers/main.yml

---
- name: Restart nginx
  service: name=nginx state=restarted

roles/nginx/templates/nginx.conf.j2(最小可用):

user nginx;
worker_processes {{ nginx_worker_processes | default('auto') }};
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;
events { worker_connections 1024; }
http {
  include       /etc/nginx/mime.types;
  default_type  application/octet-stream;
  log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';
  access_log  /var/log/nginx/access.log  main;
  sendfile        on;
  keepalive_timeout  65;
  include /etc/nginx/conf.d/*.conf;
}

roles/nginx/templates/vhost.conf.j2

server {
  listen {{ http_port }};
  server_name {{ server_name }};
  root {{ app_root }};
  index index.php index.html;

  location / {
    try_files $uri $uri/ /index.php?$args;
  }

  location ~ \.php$ {
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_pass {{ php_fpm_listen }};
  }
}

roles/php/tasks/main.yml

---
- name: Install PHP-FPM and extensions
  yum:
    name:
      - php
      - php-fpm
      - php-cli
      - php-json
      - php-mbstring
      - php-xml
    state: present

- name: Configure php-fpm pool
  template: src=www.conf.j2 dest=/etc/php-fpm.d/www.conf
  notify: Restart php-fpm

- name: Enable and start php-fpm
  service: name=php-fpm state=started enabled=yes

roles/php/handlers/main.yml

---
- name: Restart php-fpm
  service: name=php-fpm state=restarted

roles/php/templates/www.conf.j2(关键监听修改为变量):

[www]
user = nginx
group = nginx
listen = {{ php_fpm_listen }}
listen.owner = nginx
listen.group = nginx
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3

roles/app/tasks/main.yml:应用目录与首页。

---
- name: Ensure app root exists
  file: path={{ app_root }} state=directory owner=nginx group=nginx mode=0755

- name: Deploy index.php
  template: src=index.php.j2 dest={{ app_root }}/index.php owner=nginx group=nginx mode=0644

roles/app/templates/index.php.j2

<?php
phpinfo();

5. site.yml 编排与运行

site.yml

---
- hosts: web
  become: yes
  roles:
    - common
    - php
    - nginx
    - app

运行:

cd ansible-project
ansible-playbook site.yml              # 使用默认 prod 清单
# 或指定清单
ansible-playbook -i inventories/prod/hosts.ini site.yml

6. 回滚与验证要点

  • 回滚建议:
    • 配置文件用 template + backup=yescopy backup=yes
    • 应用发布建议采用版本目录(releases/2025xxxx)+ current 符号链接模式;
    • 使用 unarchivecreates 防重复解压,或在任务中加 checksum 校验。
  • 验证:
    • 语法检查:nginx -t
    • 端口监听:ss -lntp | egrep ':80|:9000'
    • 服务状态:systemctl status nginx php-fpm
    • 页面验证:curl -I http://<web_ip>/ 或浏览器访问 http://server_name/

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐