ansible自动化部署(playbook)

发布时间:2024年01月13日

Hello,大家好,我是景天。上一章单个模块分别写命令使用的是Ad-Hoc命令,
这其实就是一个概念的名字,是相对于写Ansible playbook来说的。类似于在命令行敲入shell命令和写shell脚本的关系。
我们敲入一些命令去比较快地完成一些事,而不需要将这些命令特别保存下来,这样的命令就叫做Ad-Hoc命令。

Ansible 提供两种方式去完成任务

(1)Ad-hoc 命令

执行shell命令。或shell脚本,可以执行一些简单的命令,不需要将这些命令保存下来。适合执行简单的命令

(2)Ansible playbook

可以解决比较复杂的任务,可以将命令保存下来。适合进行配置管理或部署客户机

Ad-hoc是指ansible下临时执行的一条命令,并不需要保存命令,对于复杂的命令会使用playbook
Ad-hoc执行依赖于模块,Ansible提供了大量的模块,具体可以通过 ansible-doc -l查看
可以使用 ansible-doc -s 模块名 来查看某个模块的参数
ansible-doc 模块名 来查看详细用法

Ansible playbook

playbook 是由一个或多个模块组成,使用多个不同的模块完成一件事
playbook通过yaml语法识别描述的状态文件。扩展名是yaml

1、yaml三板斧

缩进:
	YAML使用一个固定的缩进风格,每个缩进由两个空格组成,不能使用tab(默认)
	修改/etc/vimrc配置文件,设置缩进为2. set tabstop=2

冒号:
	以冒号结尾的除外,其他所有冒号后面必须有空格
	
短横线:
	表示列表项,使用一个短横线加一个空格
	多个项使用同样的缩进级别作为一个级别列表

2、playbook的核心元素

hosts:主机清单
tasks:任务
vars:变量
handlers:特定条件触发的任务,默认情况下,所有task执行完毕后,才会执行各个handler
template:包含了模板语法的文本文件

  因为我们重启服务的目的是为了在修改配置文件以后使新的配置生效,而第二次运行剧本的这种情况下,
我们并没有真正修改服务器配置,因为服务器配置本来 就与我们预期的一致,但是,在没有修改配置的情况下,
仍然重启了服务,这种重启是不需要的,我们想要达到的效果是,如果配置文件发生了改变,则重启服务,
如果配置文件并没有被真正的修改,则不对服务进行任何操作,这种情况下,我们该怎们办呢?

handlers就是来解决这种问题的,此处我们先大概的描述一下handlers的概念,后面会给出示例
你可以把handlers理解成另一种tasks,handlers是另一种’任务列表’,
handlers中的任务会被tasks中的任务进行”调用”,但是,被”调用”并不意味着一定会执行,
只有当tasks中的任务”真正执行”以后(真正的进行实际操作,造成了实际的改变),
handlers中被调用的任务才会执行,如果tasks中的任务并没有做出任何实际的操作,
那么handlers中的任务即使被’调用’,也并不会执行。
我们使用handlers关键字,指明哪些任务可以被调用,之前说过,handlers是另一种任务列表,
你可以把handlers理解成另外一种tasks,你可以理解成它们平级的,所以,handlers与tasks是对齐的(缩进相同)
通过notify关键字’通知’handlers中的任务

3、剧本YAML

编写剧本文件 ***.yaml
yaml文件一般写在/etc/ansible/ansible-playbook目录下
配置文件也写在/etc/ansible/ansible-playbook目录下,创建个/etc/ansible/ansible-playbook/conf/目录
命令格式:ansible-playbook [option] filename(剧本名)
自行剧本命令:
常用选项:不接选项的话直接执行剧本
ansible-playbook
-C ,–check 模拟运行
–list-hosts 列出主机清单
–list-tasks 列出剧本任务
–list-tags 列出剧本标记
–syntax-check 检测语法

[root@m01 ansible_playbook ]#ansible-playbook --syntax-check apache.yaml 

 playbook: apache.yaml   代表语法检测正常
 
 配置文件也写在/etc/ansible/ansible-playbook/conf/目录下
 httpd的配置文件先从web服务器copy过来:scp 172.16.1.7:/etc/httpd/conf/httpd.conf ./
 再修改
[root@m01 ansible ]#cat apache.yaml 
- hosts: web
  tasks:

    - name: Install Apache
      yum: name=httpd state=installed
    - name: Modify conf
      copy: src=./conf/httpd.conf dest=/etc/httpd/conf/httpd.conf
      notify: Restart Httpd
 
    - name: Start Service
      service: name=httpd state=started enabled=yes

  handlers:
    - name: Restart Httpd
      service: name=httpd state=restarted

RHEL8格式:

[student@workstation jh]$ vim create_user.yaml
---
- name: this is my playbook!
  hosts: intranetweb
  tasks:
            - name: Create new user
              user:
                name: jinghao
                uid: 5000
                state: present

增加列出详细参数 -vv 最多4个v

[student@workstation playbook-basic]$ cat site.yaml 
---
- name: This play for web
  hosts: web
  tasks:
          - name: Install httpd
            yum:
              name: httpd
              state: latest

          - name: copy file
            copy:
              src: ./files/index.html
              dest: /var/www/html/

          - name: start when on
            service:
              name: httpd
              state: started
              enabled: yes

还可以在playbook里面设置额外的属性

---
- name: This play for web
  hosts: web
  remote_user: jinghao
  become: yes
  tasks:


playbook 用#注释
|换行
> 不换行

ansible-playbook --limit=主机清单里面的一个主机,限定在某个主机运行
hosts: servera,serverb

hosts:
  - servera
  - serverb

    - name: web content
      get_url:
        url: http://materials.example.com/labs/playbook-review/index.php
        dest: /var/www/html/

- name: test the access to web server
  hosts: localhost
  tasks:
    - name: test connect to web server
      uri:
        url: http://serverb.lab.example.com
        return_content: yes
        status_code: 200

yum可以一次性安装多个软件:
yum:
name:
- httpd
- mariadb
- php
state: present

[root@m01 ansible ]#cat apache.yaml 
---
- name: handlers
  hosts: web
  tasks:

    - name: Install Apache
      yum: name=httpd state=installed
    - name: Modify conf
      copy: src=./conf/httpd.conf dest=/etc/httpd/conf/httpd.conf
      notify:
	    - Restart Httpd
 
    - name: Start Service
      service: name=httpd state=started enabled=yes

  handlers:
    - name: Restart Httpd
      service:
	    name: httpd
		state: restarted
	   

4、meta模块

默认情况下,所有task执行完毕后,才会执行各个handler,并不是执行完某个task后,立即执行对应的handler,
如果你想要在执行完某些task以后立即执行对应的handler,则需要使用meta模块

meta任务之前的任务task1与task2在进行了实际操作以后,立即运行了对应的handler1与handler2,
然后才运行了task3,在所有task都运行完毕后,又逐个将剩余的handler根据情况进行调用。
[root@m01 ansible ]#cat apache.yaml 
- hosts: web
  tasks:

    - name: Install Apache
      yum: name=httpd state=installed

    - name: task1
      file: path=/root/file1/ state=directory
      notify: handler1

    - name: task2
      file: path=/root/file2/ state=directory
      notify: handler2
    
    - meta: flush_handlers

    - name: task3
      file: path=/root/file3/ state=directory
      notify: handler3

    - name: Modify conf
      copy: src=./conf/httpd.conf dest=/etc/httpd/conf/httpd.conf
      notify: Restart Httpd
 
    - name: Start Service
      service: name=httpd state=started enabled=yes

  handlers:
    - name: Restart Httpd
      service: name=httpd state=restarted

    - name: handler1
      file: path=/root/file1/text1 state=touch

    - name: handler2
      file: path=/root/file2/text2 state=touch

    - name: handler3
      file: path=/root/file3/text3 state=touch

5、一个task调用多个handler

使用listen模块:在每个handler name下加一个listen,可以将想要一次性调用的handler中listen名称写一样

[root@m01 ansible ]#cat apache1.yaml 
- hosts: web
  tasks:

    - name: Install Apache
      yum: name=httpd state=installed

    - name: task1
      file: path=/root/file1/hg state=directory
      notify: handler ls1

    - name: task3
      file: path=/root/file3/ state=directory
 

  handlers:
    - name: handler1
      listen: handler ls1
      file: path=/root/file1/hg/text1 state=touch

    - name: handler2
      listen: handler ls1
      file: path=/root/file1/hg/text2 state=touch

    - name: handler3
      file: path=/root/file3/text3 state=touch

task1 调用两个handler,handler1和 handler2,因为 使用了listen,名称都是handler ls1

6、ansible playbook中 tags 的用法

如果你写了一个很长的playbook,其中有很多的任务,这并没有什么问题,不过在实际使用这个剧本时,
你可能只是想要执行其中的一部分任务而已,或者,你只想要执行其中一类任务而已,
而并非想要执行整个剧本中的全部任务,这个时候我们该怎么办呢?我们可以借助tags实现这个需求。

见名知义,tags可以帮助我们对任务进行打标签的操作,当任务存在标签以后,我们就可以在执行playbook时,
借助标签,指定执行哪些任务,或者指定不执行哪些任务了

-t TAGS, --tags TAGS only run plays and tasks tagged with these values

[root@m01 ansible ]#cat apache1.yaml 
- hosts: web
  tasks:

    - name: Install Apache
      yum: name=httpd state=installed

    - name: task1
      file: path=/root/file1/hg state=directory
      notify: handler ls1

    - name: task3
      file: path=/root/file3/ state=directory
      tags: tg3
      notify: handler3
 

  handlers:
    - name: handler1
      file: path=/root/file1/hg/text1 state=touch
      listen: handler ls1

    - name: handler2
      file: path=/root/file1/hg/text2 state=touch
      listen: handler ls1

    - name: handler3
      file: path=/root/file3/text3 state=touch
[root@m01 ansible ]#ansible-playbook -t tg3 apache1.yaml 

PLAY [web] *****************************************************************************************************************

TASK [Gathering Facts] *****************************************************************************************************
ok: [172.16.1.7]

TASK [task3] ***************************************************************************************************************
changed: [172.16.1.7]

RUNNING HANDLER [handler3] *************************************************************************************************
changed: [172.16.1.7]

PLAY RECAP *****************************************************************************************************************
172.16.1.7                 : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0  

可以看到使用–tags选项指定某个标签,当指定标签后,只有标签对应的任务会被执行,其他任务都不会被执行

借助标签,除了能够指定”需要执行的任务”,还能够指定”不执行的任务”
–skip-tags SKIP_TAGS
only run plays and tasks whose tags do not match these
values

[root@m01 ansible ]#vim apache2.yaml 
  1 - hosts: web
  2   tasks:
  3 
  4     - name: Install Apache
  5       yum: name=httpd state=installed
  6 
  7     - name: task1
  8       file: path=/root/file1/ state=directory
  9       notify: handler1
 10       tags: tag1
 11     
 12     - name: task2
 13       file: path=/root/file2/ state=directory
 14       notify: handler2
 15       tags: tag2
 16     
 17     
 18     - name: task3
 19       file: path=/root/file3/ state=directory
 20       notify: handler3
 21       tags: tag3
 22     
 23     - name: Modify conf
 24       copy: src=./conf/httpd.conf dest=/etc/httpd/conf/httpd.conf
 25       notify: Restart Httpd
 26     
 27     - name: Start Service 
 28       service: name=httpd state=started enabled=yes
 29 
 30   handlers:
 31     - name: Restart Httpd
 32       service: name=httpd state=restarted
 33 
 34     - name: handler1
 35       file: path=/root/file1/text1 state=touch
 36 
 37     - name: handler2
 38       file: path=/root/file2/text2 state=touch
 39 
 40     - name: handler3
 41       file: path=/root/file3/text3 state=touch

[root@m01 ansible ]#ansible-playbook --skip-tags tag2,tag3 apache2.yaml 跳过了tag2,tag3两个标签

Ansible变量

便于多次调用,避免过错在ansible中使用变量,能让我们的工作变得更加灵活,在ansible中,变量的使用方式有很多种,我们慢慢聊。先说说怎样定义变量,变量名应该由字母、数字、下划线组成,变量名需要以字母开头,ansible内置的关键字不能作为变量名。

---
- hosts: test70
  vars:
    testvar1: testfile
  remote_user: root
  tasks:
  - name: task1
    file:
      path: /testdir/{{ testvar1 }}
      state: touch

上例中,先使用vars关键字,表示在当前play中进行变量的相关设置。

vars关键字的下一级定义了一个变量,变量名为testvar1,变量值为testfile

当我们需要使用testvar1的变量值时,则需要引用这个变量,如你所见,使用"{{变量名}}"可以引用对应的变量。

也可以定义多个变量,示例如下。

vars:
testvar1: testfile
testvar2: testfile2

除了使用上述语法,使用YAML的块序列语法也可以定义变量,示例如下

vars:

  • testvar1: testfile
  • testvar2: testfile2

全局变量 global var 定义在命令行
局部变量 play var 只在 play里面的变量
主机变量 host var 针对主机的变量 定义在inventory

优先级 global var > play var > host var

debug:

  • debug:
    msg: System {{ inventory_hostname }} has uuid {{ ansible_product_uuid }}

应用变量时,要用一对大括号括起来
变量在开头被引用时,要用双引号将整段话引起来

[student@workstation playbook-review]$ vim jinghap.yaml
---
- name: debug
  hosts: all
  vars:
    user: jinghao
  tasks:
    - name: debug test
      debug:
              msg: "{{ user }} is very smart,{{ user }} has handled"


[student@workstation playbook-review]$ ansible-playbook jinghap.yaml -e "user=zhangliang"

PLAY [debug] ***********************************************************************************************************

TASK [Gathering Facts] *************************************************************************************************
ok: [serverb.lab.example.com]

TASK [debug test] ******************************************************************************************************
ok: [serverb.lab.example.com] => {
    "msg": "zhangliang is very smart,zhangliang has handled"

命令行(全局变量)优先级大于play定义的变量,大于主机定义的变量(即在inventory里面定义的主机变量)

变量范围越小,优先级越高

playbook里面一般不定义变量,定义变量文件
vars_files:

  • vars/user.yaml
[student@workstation playbook-review]$ vim jinghap.yaml 
---
- name: debug
  hosts: all
  vars_files:
    - vars/user.yaml
  tasks:
    - name: debug test
      debug:
              msg: "{{ user }} is very smart,{{ user }} has handled"
[student@workstation vars]$ cat user.yaml 
user: jh

debug模块的用var时,引用变量就不需要再用大括号括起来

tasks:
- name: debug test
debug:
var: user

主机变量、主机组变量:

[web]
servera user=jinghao
serverb user=lindao

[web:vars]
user: jinghao

主机组变量解耦:
在ansibe.cfg所在目录创建group_vars目录,在该目录下创建与主机组同名的文件 web

host_vars目录下 针对单个主机设变量文件与主机同名文件servera

user: jinghao

1、变量矩阵

矩阵变量的含义
根据 URI 规范 RFC 3986 中 URL 的定义,路径片段中可以包含键值对。
规范中没有对应的术语…在 Spring MVC 它被成为矩阵变量.

层级变量

users:
bjon:
first_name: Bob
last_name: Janes
hks:
first_name: Tom
last_name: Bill

调用时:
users.bjon.first_name

[student@workstation vars]$ vim user.yaml
users:
jh:
name: jinghao
sex: male
home: GZ

sz:
name: shuzhan
sex: female
home: CS

引用变量文件:

[student@workstation playbook-review]$ vim jinghap.yaml 
---
- name: debug
  hosts: all
  vars_files:
    - vars/user.yaml
  tasks:
    - name: debug test
      debug:
              msg: "{{ users.jh.name }} is very smart,{{ users.sz.home }} is a beautil city"

采用点时,可能会与Python语法冲突,一般采用user[‘jh’][‘name’]形式

[student@workstation playbook-review]$ vim jinghap.yaml 
---
- name: debug
  hosts: all
  vars_files:
    - vars/user.yaml
  tasks:
    - name: debug test
      debug:
              msg: "{{ users['jh']['name'] }} is very smart,{{ users['sz']['home'] }} is a beautil city"

2、register

ansible register 这个功能非常有用。当我们需要判断对执行了某个操作或者某个命令后,
如何做相应的响应处理(执行其他 ansible 语句),则一般会用到register 。

注意:
1、register变量的命名不能用 -中横线,比如dev-sda6_result,则会被解析成sda6_result,dev会被丢掉,所以不要用-
2、ignore_errors这个关键字很重要,一定要配合设置成yes,否则如果命令执行不成功,即 echo $?不为0,
则在其语句后面的ansible语句不会被执行,导致程序中止。

那我如何去做多种条件的判断呢,比如我还需要判断是否有 docker-thinpool 存在,则还需要为它注册一个变量。

[student@workstation playbook-review]$ vim install.yaml
---
- name: install a package
  hosts: web
  tasks:
    - name: yum modules
      yum:
        name: httpd
        state: latest
      register: install_msg
	  ignore_errors: yes

    - debug:
        var: install_msg 将执行的结果打印出来
~                           

ok: [servera] => {
    "install_msg": {
        "changed": true,
        "failed": false,
        "msg": "",
        "rc": 0,
        "results": [
            "Installed: httpd",
            "Installed: mod_http2-1.11.3-1.module+el8+2443+605475b7.x86_64",
            "Installed: httpd-2.4.37-10.module+el8+2764+7127e69e.x86_64"
        ]
    }
}

“rc”: 0 表示tasks运行正常,非零运行错误

调用具体值与矩阵类似 install_msg[‘rc’]

[student@workstation playbook-review]$ vim install.yaml
---
- name: install a package
  hosts: web
  tasks:
    - name: yum modules
      yum:
        name: httpd
        state: latest
      register: install_msg

    - debug:
        var: install_msg

    - debug:
        var: install_msg['rc']


TASK [debug] ***********************************************************************************************************
ok: [serverb] => {
    "install_msg['rc']": "0"
[student@workstation data-variables]$ cat playbook.yaml 
---
- name: install web server and ensure running
  hosts: webserver
  vars:
    web_pkg: httpd
    firewall_pkg: firewalld
    web_service: httpd
    firewall_service: firewalld
    python_pkg: python3-PyMySQL
    rule: http
  tasks:
    - name: install packages
      yum:
        name:
          - "{{ web_pkg }}"
          - "{{ firewall_pkg }}"
          - "{{ python_pkg }}"
        state: latest

    - name: The {{ firewall_service }} service is started and enabled
      service:
        name: "{{ firewall_service }}"
        state: started
        enabled: yes


    - name: The {{ web_service }} service is started and enabled
      service:
        name: "{{ web_service }}"
        state: started
        enabled: yes

    - name: web content
      copy:
        content: "web service is successful\n"
        dest: /var/www/html/index.html

    - name: release service
      firewalld:
        service: http
        state: enabled
        permanent: yes
        immediate: yes

- name: test web
  hosts: localhost
  become: false
  tasks:
    - name: test web server
      uri:
        url: http://servera.lab.example.com
        status_code: 200

3、vault用来加密解密变量

ansible-vault,可以用来加密inventory文件
使用语法:

[student@workstation data-variables]$ ansible-vault create test.yaml
New Vault password: 123456
Confirm New Vault password: 123456
user: jinghao

查看:
[student@workstation data-variables]$ ansible-vault view test.yaml
Vault password: 123456
user: jinghao

修改密码:
[student@workstation data-variables]$ ansible-vault rekey test.yaml
Vault password: 123456
New Vault password: jinghao
Confirm New Vault password: jinghao
Rekey successful

修改yaml内容:
[student@workstation data-variables]$ ansible-vault edit test.yaml
Vault password:
user: jinghao

将文件解密:decrypt
[student@workstation data-variables]$ ansible-vault decrypt test.yaml
Vault password:
Decryption successful

加密文件不变解密后输出
[student@workstation data-variables]$ ansible-vault decrypt test.yaml --output=test1.yaml
Vault password:
Decryption successful

将文件加密:encrypt
[student@workstation data-variables]$ ansible-vault encrypt test.yaml
New Vault password:
Confirm New Vault password:
Encryption successful

免交互用法:

先创建密码文件
[student@workstation data-variables]$ vim secret.yaml
123456

利用免交互:
[student@workstation data-variables]$ ansible-vault --vault-password-file=./secret.yaml view test.yaml
user: jinghao

也可以用vault-id
[student@workstation data-variables]$ ansible-vault --vault-id=./secret.yaml view test.yaml
user: jinghao

运行ansible时,若用到加密的文件 --vault-id @prompt 以前用–ask-vault-pass
[student@workstation playbook-review]$ ansible-playbook --vault-id @prompt jinghap.yaml
Vault password (default):

4、facts变量

什么是 Ansible facts
Ansible facts 是远程系统的信息,主要包含IP地址,操作系统,
以太网设备,mac 地址,时间/日期相关数据,硬件信息等信息。

Ansible facts 对于需要根据远程主机的信息作为执行条件操作的场景非常有用。
例如,根据远程服务器使用的操作系统版本,可以安装不同版本的软件包。
或者也可以显示与每台远程计算机相关的一些信息,例如每台设备上有多少 RAM 可用。

如何获取 Ansible facts
默认情况下,在使用 Ansible 对远程主机执行任何一个 playbook 之前,
总会先通过 setup 模块获取 facts,并暂存在内存中,直至该 playbook 执行结束。

这意味着,想要在 playbook 中引用主机变量,至少先与该主机通信一次,
以便 Ansible 能够访问其 facts,尽管有时候只需要来自该主机的少量信息。

Ansible 提供了 setup 模块来收集主机的系统信息,这些 facts 信息可以直接以变量的形式使用。

如果想查看 setup 模块获取到的数据,可以在命令行上通过调用 setup 模块命令查看:

ansible all -m setup

在被控主机较少的情况下,收集信息还可以容忍,如果被控主机数量非常大,收集 facts 信息会消耗掉非常多时间。

那怎么办呢?优化 Ansible 运行速度,最简单的莫过于设置 facts 缓存了。

我们可以设置 gather_facts: no 来禁止 Ansible 收集 facts 信息,但是有时候又需要使用 facts 中的内容,这时候可以设置 facts 的缓存。

例如,我们可以在空闲的时候收集 facts,缓存下来,在需要的时候直接读取缓存进行引用。

Ansible 1.8 版本开始,引入了 facts 缓存功能。

Ansible 的配置文件中可以修改 gathering 的值为 smart、implicit 或者 explicit。

smart 表示默认收集 facts,但 facts 已有的情况下不会收集,即使用缓存 facts;
implicit 表示默认收集 facts,要禁止收集,必须使用 gather_facts: no;
explicit 则表示默认不收集,要显式收集,必须使用 gather_facts: Ture。
在使用 facts 缓存时(即设置为 smart),Ansible 支持两种 facts 缓存:redis 和 jsonfile。

[student@workstation playbook-basic]$ vim site.yaml 
---
- name: This play for facts
  hosts: web
  tasks:
          - name: test facts
            debug:
              var: ansible_facts

输出facts

[student@workstation playbook-basic]$ vim site.yaml 
---
- name: This play for facts
  hosts: web
  tasks:
          - name: test facts
            debug:
              msg: "{{ ansible_fqdn }}'s current mem is {{ ansible_memfree_mb }}"

5、关闭facts变量采集

在play添加
gather_facts: no

6、自定义facts

ansible_facts里面的 ansible_local是自定义的facts
ansible_local

默认会加载被管理主机的/etc/ansible/facts.d/ 目录下的自定义facts,文件是以.fact结尾

7、magic变量

获取inventory_hostname

[student@workstation playbook-review]$ ansible serverc -m debug -a 'var=hostvars["serverc"]'
serverc | SUCCESS => {
    "hostvars[\"serverc\"]": {
        "ansible_check_mode": false,
        "ansible_diff_mode": false,
        "ansible_facts": {},
        "ansible_forks": 5,
        "ansible_inventory_sources": [
            "/home/student/playbook-review/inventory"
        ],
        "ansible_playbook_python": "/usr/libexec/platform-python",
        "ansible_verbosity": 0,
        "ansible_version": {
            "full": "2.8.0",
            "major": 2,
            "minor": 8,
            "revision": 0,
            "string": "2.8.0"
        },
        "group_names": [
            "backup"
        ],
        "groups": {
            "all": [
                "serverb",
                "servera",
                "serverc",
                "serverd"
            ],
            "backup": [
                "serverc",
                "serverd"
            ],
            "ungrouped": [],
            "web": [
                "serverb",
                "servera"
            ]
        },
        "inventory_dir": "/home/student/playbook-review",
        "inventory_file": "/home/student/playbook-review/inventory",
        "inventory_hostname": "serverc",
        "inventory_hostname_short": "serverc",
        "omit": "__omit_place_holder__40a1a1b7d2fecb86ab188344bfe89d26d92a2a05",
        "playbook_dir": "/home/student/playbook-review"
    }
}
文章来源:https://blog.csdn.net/littlefun591/article/details/135553352
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。