ansible 模块

模块(也被称为 “task plugins” 或 “library plugins”)是在 Ansible 中实际在执行的.它们就 是在每个 playbook 任务中被执行的.你也可以仅仅通过 ‘ansible’ 命令来运行它们.

我们经常会通过命令行的形式使用ansible模块,ansible自带很多模块,可以直接使用这些模块。
目前ansible已经自带了200+个模块,我们可以使用ansible-doc -l显示所有自带模块,还可以使用ansible-doc 模块名,查看模块的介绍以及案例。需要注意的是,如果使用ad-hoc命令,ansible的一些插件功能就无法使用,比如loop facts功能等。
命令用法:ansible <host-pattern> [options]

让我们回顾一下我们是如何通过命令行来执行三个不同的模块:

1
2
3
ansible webservers -m service -a "name=httpd state=started"
ansible webservers -m ping
ansible webservers -m command -a "/sbin/reboot -t now"

每个模块都能接收参数. 几乎所有的模块都接受键值对(key=value)参数,空格分隔.一些模块 不接收参数,只需在命令行输入相关的命令就能调用.

在 playbook 中, Ansible 模块以类似的方式执行:

1
2
- name: reboot the servers
action: command /sbin/reboot -t now

也可以简写成:

1
2
- name: reboot the servers
command: /sbin/reboot -t now

另一种给模块传递参数的方式是使用 ymal 语法,这也被称为 ‘complex args’

1
2
3
4
- name: restart webserver
service:
name: httpd
state: restarted

ping模块

ping模块的作用与其名相同,即判断远程主机的网络是否畅通
示例:ansible cluster_hosts -m ping

copy模块

copy模块在ansible里的角色就是把ansible执行机器上的文件拷贝到远程节点上。与fetch模块相反的操作。
常用模块参数

参数名 是否必须 默认值 选项 说明
src no 用于定位ansible执行的机器上的文件,需要绝对路径。如果拷贝的是文件夹,那么文件夹会整体拷贝,如果结尾是”/”,那么只有文件夹内的东西被考过去。一切的感觉很像rsync
content no 用来替代src,用于将指定文件的内容,拷贝到远程文件内
dest yes 用于定位远程节点上的文件,需要绝对路径。如果src指向的是文件夹,这个参数也必须是指向文件夹
backup no no yes/no 备份远程节点上的原始文件,在拷贝之前。如果发生什么意外,原始文件还能使用。
directory_mode no 这个参数只能用于拷贝文件夹时候,这个设定后,文件夹内新建的文件会被拷贝。而老旧的不会被拷贝
follow no no yes/no 当拷贝的文件夹内有link存在的时候,那么拷贝过去的也会有link
force no yes yes/no 默认为yes,会覆盖远程的内容不一样的文件(可能文件名一样)。如果是no,就不会拷贝文件,如果远程有这个文件
group no 设定一个群组拥有拷贝到远程节点的文件权限
mode no 等同于chmod,参数可以为“u+rwx or u=rw,g=r,o=r”
owner no 设定一个用户拥有拷贝到远程节点的文件权限

示例:将文件copy到测试主机

1
ansible testservers -m copy -a 'src=/root/install.log dest=/tmp/install.log owner=testuser group=testgroup'

示例:copy 前先备份

1
2
echo "test " >> /root/install.log
ansible testservers -m copy -a 'src=/root/install.log dest=/tmp/install.log owner=testuser group=testgroup backup=yes'
1
ansible testservers -m raw -a 'ls -lrth /tmp/install*'

示例:将目录copy过去

1
2
3
4
ansible testservers -m copy -a 'src=/etc/ansible/testdir dest=/tmp/ owner=testuser group=testgroup backup=yes'

# 查看结果
ansible testservers -m command -a 'tree /tmp/testdir'

注意:发现有文件的目录copy成功,空的目录没有copy过去

常用参数返回值

参数名 参数说明 返回值 返回值类型 样例
src 位于ansible执行机上的位置 changed string /home/httpd/.ansible/tmp/ansible-tmp-1423796390.97-147729857856000/source
backup_file 将原文件备份 changed and if backup=yes string /path/to/file.txt.2015-02-12@22:09~
uid 在执行后,拥有者的ID success int 100
dest 远程节点的目标目录或文件 success string /path/to/file.txt
checksum 拷贝文件后的checksum值 success string 6e642bb8dd5c2e027bf21dd923337cbb4214f827
md5sum 拷贝文件后的md5 checksum值 when supported string 2a5aeecc61dc98c4d780b14b330e3282
state 执行后的状态 success string file
gid 执行后拥有文件夹、文件的群组ID success int 100
mode 执行后文件的权限 success string 644
owner 执行后文件所有者的名字 success string httpd
group 执行后文件所有群组的名字 success string httpd
size 执行后文件大小 success int 1220

shell模块

它负责在被ansible控制的节点(服务器)执行命令行。shell 模块是通过/bin/sh进行执行,所以shell 模块可以执行任何命令,就像在本机执行一样。

  • 常用参数
参数 是否必须 默认值 选项 说明
chdir no 跟command一样的,运行shell之前cd到某个目录
creates no 跟command一样的,如果某个文件存在则不运行shell
removes no 跟command一样的,如果某个文件不存在则不运行shell

示例1:
让所有节点运行somescript.sh并把log输出到somelog.txt。

ansible -i hosts all -m shell -a "sh somescript.sh >> somelog.txt"

示例2:
先进入somedir/ ,再在somedir/目录下让所有节点运行somescript.sh并把log输出到somelog.txt。

ansible -i hosts all -m shell -a "somescript.sh >> somelog.txt" chdir=somedir/

示例3:
先cd到某个需要编译的目录,执行condifgure然后,编译,然后安装。

ansible -i hosts all -m shell -a "./configure && make && make insatll" chdir=/xxx/yyy/

command模块

command 模块用于运行系统命令。不支持管道符和变量等(”<”, “>”, “|”, and “&”等),如果要使用这些,那么可以使用shell模块。在使用ansible中的时候,默认的模块是-m command,从而模块的参数不需要填写,直接使用即可。

  • 常用参数
参数 是否必须 默认值 选项 说明
chdir no 运行command命令前先cd到这个目录
creates no 如果这个参数对应的文件存在,就不运行command
executable no 将shell切换为command执行,这里的所有命令需要使用绝对路径
removes no 如果这个参数对应的文件不存在,就不运行command

示例1:

ansible 命令调用command:

ansible -i hosts all -m command -a "/sbin/shutdown -t now"

ansible命令行调用-m command模块 -a表示使用参数 “”内的为执行的command命令,该命令为关机。

那么对应的节点(192.168.10.12,127.152.112.13)都会执行关机。

示例2:
Run the command if the specified file does not exist.
ansible -i hosts all -m command -a "/usr/bin/make_database.sh arg1 arg2 creates=/path/to/database"

利用creates参数,判断/path/to/database这个文件是否存在,存在就跳过command命令,不存在就执行command命令。

raw模块

raw模块的功能与shell和command类似。但raw模块运行时不需要在远程主机上配置python环境。

示例:
在10.1.1.113节点上运行hostname命令
ansible 10.1.1.113 -m raw-a 'hostname|tee'

fetch模块

文件拉取模块主要是将远程主机中的文件拷贝到本机中,和copy模块的作用刚刚相反,并且在保存的时候使用hostname来进行保存,当文件不存在的时候,会出现错误,除非设置了选项fail_on_missing为yes

常用参数

参数 必填 默认值 选项 说明
Dest Yes 用来存放文件的目录,例如存放目录为backup,源文件名称为/etc/profile在主机pythonserver中,那么保存为/backup/pythonserver/etc/profile
Fail_on_missing No No Yes/no 当源文件不存在的时候,标识为失败
Flat No 允许覆盖默认行为从hostname/path到/file的,如果dest以/结尾,它将使用源文件的基础名称
Src Yes 在远程拉取的文件,并且必须是一个file,不能是目录
Validate_checksum No Yes Yes/no 当文件fetch之后进行md5检查

示例1:

fetch一个文件保存,src表示为远程主机上需要传送的文件路径,dest表示为本机上的路径,在传送过来的文件,是按照IP地址进行分类,然后路径是源文件的路径。在拉取文件的时候,必须拉取的是文件,不能拉取文件夹。

1
[root@ansibleserver ~]# ansible pythonserver -m fetch -a "src=/root/123 dest=/root"

示例2:

指定路径目录进行保存。在使用参数为flat的时候,如果dest的后缀名为/,那么就会保存在目录中,然后直接保存为文件名;当dest后缀不为/的时候,那么就会直接保存为kel的文件。主要是在于dest是否已/结尾,从而来区分这是个目录还是路径。

1
[root@ansibleserver ~]# ansible pythonserver -m fetch -a "src=/root/Ssh.py dest=/root/kel/ flat=yes"

file模块

主要用来设置文件、链接、目录的属性,或者移除文件、链接、目录,很多其他的模块也会包含这种作用,例如copy,assemble和template。

参数 必填 默认 选项 说明
Follow No No Yes/no 这个标识说明这是系统链接文件,如果存在,应该遵循
Force No No Yes/no 强制创建链接在两种情况下:源文件不存在(过会会存在);目标存在但是是文件(创建链接文件替代)
Group No 文件所属用户组
Mode No 文件所属权限
Owner No 文件所属用户
Path Yes 要控制文件的路径
Recurse No No Yes/no 当文件为目录时,是否进行递归设置权限
Src No 文件链接路径,只有状态为link的时候,才会设置,可以是绝对相对不存在的路径
State No File File/link Directory Hard/touch Absent 如果是目录不存在,那么会创建目录;如果是文件不存在,那么不会创建文件;如果是link,那么软链接会被创建或者修改;如果是absent,那么目录下的所有文件都会被删除,如果是touch,会创建不存在的目录和文件
  • 示例1:

设置文件属性。文件路径为path,表示文件路径,设定所属用户和所属用户组,权限为0644。文件路径为path,使用文件夹进行递归修改权限,使用的参数为recurse表示为递归。

1
[root@ansibleserver ~]# ansible pythonserver -m file -a "path=/root/123 owner=kel group=kel mode=0644"
1
[root@ansibleserver ~]# ansible pythonserver -m file -a "path=/tmp/kel/ owner=kel group=kel mode=0644 recurse=yes"
  • 示例2:
    创建目录。创建目录,使用的参数主要是state为directory。

    1
    [root@ansibleserver ~]# ansible pythonserver -m file -a "path=/tmp/kel state=directory mode=0755"
  • 示例3:
    修改权限。直接使用mode来进行修改权限。

    1
    [root@ansibleserver ~]# ansible pythonserver -m file -a "path=/tmp/kel mode=0444"
  • 示例4:
    创建软连接。 src表示已经存在的文件,dest表示创建的软连接的文件名,最后的state状态为link。

    1
    root@ansibleserver tmp]# ansible pythonserver -m file -a "src=/tmp/1 dest=/tmp/2 owner=kel state=link"

yum模块

Yum(全称为 Yellow dog Updater, Modified)是一个在Fedora和RedHat以及CentOS中的Shell前端软件包管理器。即安装包管理模块。
常用参数

参数名 是否必须 默认值 选项值 参数说明
conf_file no 设定远程yum执行时所依赖的yum配置文件
disable_gpg_check no No Yes/No 在安装包前检查包,只会影响state参数为present或者latest的时候
list No 只能由ansible调用,不支持playbook,这个干啥的大家都懂
name Yes 你需要安装的包的名字,也能如此使用name=python=2.7安装python2.7
state no present present/latest/absent 用于描述安装包最终状态,present/latest用于安装包,absent用于remove安装包
update_cache no no yes/no 用于安装包前执行更新list,只会影响state参数为present/latest的时候
  • 示例1:
    安装httpd包
    ansible host31 -m yum -a “name=httpd”

  • 示例2:
    删除httpd包
    ansible host31 -m yum -a "name=httpd state=absent"

service模块

service模块其实就是linux下的service命令。用于service服务管理。
常用参数

参数名 是否必须 默认值 选项 说明
enabled no yes/no 启动os后启动对应service的选项。使用service模块的时候,enabled和state至少要有一个被定义
name yes 需要进行操作的service名字
state no stared/stoped/restarted/reloaded service最终操作后的状态。
  • 示例1:
    启动服务。
    ansible host31 -m service -a "name=httpd state=started"

host31 | SUCCESS => { “changed”: true, “name”: “httpd”, “state”: “started” }

  • 示例2:
    停止服务。
    ansible host31 -m service -a "name=httpd state=stopped"

host31 | SUCCESS => { “changed”: true, “name”: “httpd”, “state”: “stopped” }

  • 示例3:
    设置服务开机自启动。
    [root@host31 ~]# ansible host31 -m service -a "name=httpd enabled=yes state=restarted"

host31 | SUCCESS => { “changed”: true, “enabled”: true, “name”: “httpd”, “state”: “started” }

cron模块

cron模块用于管理计划任务。

参数名 是否必须 默认值 选项 说明
backup 对远程主机上的原任务计划内容修改之前做备份
cron_file 如果指定该选项,则用该文件替换远程主机上的cron.d目录下的用户的任务计划
day 日(1-31,/2,……)
hour 小时(0-23,/2,……)
minute 分钟(0-59,/2,……)
month 月(1-12,/2,……)
weekday 周(0-7,*,……)
job 要执行的任务,依赖于state=present
name 该任务的描述
special_time 指定什么时候执行,参数:reboot,yearly,annually,monthly,weekly,daily,hourly
state 确认该任务计划是创建还是删除
user 以哪个用户的身份执行

示例:

  • ansible test -m cron -a ‘name=”a job for reboot” special_time=reboot job=”/some/job.sh”‘
  • ansible test -m cron -a ‘name=”yum autoupdate” weekday=”2” minute=0 hour=12 user=”root
  • ansible test -m cron -a ‘backup=”True” name=”test” minute=”0” hour=”5,2” job=”ls -alh > /dev/null”‘
  • ansilbe test -m cron -a ‘cron_file=ansible_yum-autoupdate state=absent’

user模块

user模块是请求的是useradd, userdel, usermod三个指令。
常用参数

参数名 是否必须 默认值 选项 说明
home 指定用户的家目录,需要与createhome配合使
groups 指定用户的属组
uid 指定用的uid
password 指定用户的密码
name 指定用户名
createhome 是否创建家目录 yes
system 是否为系统用户
remove 当state=absent时,remove=yes则表示连同家目录一起删除,等价于userdel -r
state 是创建还是删除
shell 指定用户的shell环境

指定password参数时,不能使用明文密码,因为后面这一串密码会被直接传送到被管理主机的/etc/shadow文件中,所以需要先将密码字符串进行加密处理。然后将得到的字符串放到password中即可。不同的发行版默认使用的加密方式可能会有区别,具体可以查看/etc/login.defs文件确认,centos 6.5版本使用的是SHA512加密算法。

  • 示例1:

目的:在指定节点上创建一个用户名为nolinux,组为nolinux的用户

命令:ansible 10.1.1.113 -m user -a 'name=nolinux groups=nolinux state=present'

  • 示例2:
    删除用户

命令:ansible 10.1.1.113 -m user -a 'name=nolinux groups=nolinux state=absent remove=yes'

group模块

goup模块请求的是groupadd, groupdel, groupmod 三个指令。参数参考ansible-hoc group

  • 示例:
    目的:在所有节点上创建一个组名为nolinux,gid为2014的组
    命令:ansible all -m group -a 'gid=2014 name=nolinux'

script模块

script模块将控制节点的脚本执行在被控节点上。
示例:

1
[root@host31 ~]# ansible host32 -m script -a /tmp/hello.sh

host32 | SUCCESS => { “changed”: true, “rc”: 0, “stderr”: “”, “stdout”: “this is test from host32\r\n”, “stdout_lines”: [ “this is test from host32” ->执行结果 ] }

get_url模块

该模块主要用于从http、ftp、https服务器上下载文件(类似于wget)
常用参数

参数名 是否必须 默认值 选项 说明
sha256sum 下载完成后进行sha256 check;
timeout 下载超时时间,默认10s
url 下载的URL
url_password、url_username 主要用于需要用户名密码进行验证的情况
use_proxy 是事使用代理,代理需事先在环境变更中定义

示例:

目的:将http://10.1.1.116/favicon.ico文件下载到指定节点的/tmp目录下

命令:ansible 10.1.1.113 -m get_url -a 'url=http://10.1.1.116/favicon.ico dest=/tmp'

synchronize模块

使用rsync同步文件。

参数名 是否必须 默认值 选项 说明
archive 归档,相当于同时开启recursive(递归)、links、perms、times、owner、group、-D选项都为yes ,默认该项为开启
checksum 跳过检测sum值,默认关闭
compress 是否开启压缩
copy_links 复制链接文件,默认为no ,注意后面还有一个links参数
delete 删除不存在的文件,默认no
dest 目录路径
dest_port dest_port:默认目录主机上的端口 ,默认是22,走的ssh协议
dirs 传速目录不进行递归,默认为no,即进行目录递归
rsync_opts rsync参数部分
set_remote_user 主要用于/etc/ansible/hosts中定义或默认使用的用户与rsync使用的用户不同的情况
mode push或pull 模块,push模的话,一般用于从本机向远程主机上传文件,pull 模式用于从远程主机上取文件
  • 示例1:
    目的:将主控方/root/a目录推送到指定节点的/tmp目录下
    命令:ansible 10.1.1.113 -m synchronize -a 'src=/root/a dest=/tmp/ compress=yes'

delete=yes 使两边的内容一样(即以推送方为主)
compress=yes 开启压缩,默认为开启
–exclude=.Git 忽略同步.git结尾的文件

由于模块,默认都是推送push。因此,如果你在使用拉取pull功能的时候,可以参考如下来实现
mode=pull 更改推送模式为拉取模式

  • 示例2:
    目的:将10.1.1.113节点的/tmp/a目录拉取到主控节点的/root目录下
    命令:ansible 10.1.1.113 -m synchronize -a 'mode=pull src=/tmp/a dest=/root/'

  • 示例3:
    由于模块默认启用了archive参数,该参数默认开启了recursive, links, perms, times, owner,group和-D参数。如果你将该参数设置为no,那么你将停止很多参数,比如会导致如下目的递归失败,导致无法拉取

lineinfile

lineinfile - Ensure a particular line is in a file, or replace an existing line using a back-referenced regular expression

  • Ansible Lineinfile:9 种使用它来改进 Playbook 的方法

    修改匹配行

    1
    2
    3
    4
    5
    6
    7
    # 将/etc/selinux/config中匹配到以'SELINUX='开头的行,将其替换为'SELINUX=disabled'
    - name: modify selinux to disabled
    lineinfile:
    path: /etc/selinux/config
    regex: '^SELINUX='
    line: 'SELINUX=disabled'

    在匹配行前或后添加内容

    示例文件如下:

    1
    2
    3
    4
    5
    # cat /etc/http.conf

    Listen 127.0.0.1:80
    Listen 80
    Port
  • 在匹配行前添加
    在http.conf文件的Listen 80前面添加一行Listen 8080,task示例如下:

    1
    2
    3
    4
    5
    - name: add line before Listen 80
    lineinfile:
    dest: /etc/http.conf
    insertbefore: '^Listen 80'
    line: 'Listen 8080'
  • 在匹配行后添加
    在http.conf文件的Port后面添加一行just a test,task示例如下:

1
2
3
4
5
- name: add line before Listen 80
lineinfile:
dest: /etc/http.conf
insertafter: '^Port'
line: 'just a test'

修改文件内容及权限

示例文件:

#cat /etc/hosts

1
2
127.0.0.1       localhost.localdomain localhost ::1       localhost6.localdomain6 localhost6
192.168.0.130 hub.breezey.top

修改/etc/hosts,将以127.0.0.1开头的行替换为127.0.0.1 localhost,并将/etc/hosts的属主和属组都修改为root,权限改为644,如下:

1
2
3
4
5
6
7
8
- name: modify hosts
lineinfile:
dest: /etc/hosts
regex: '^127\.0\.0\.1'
line: '127.0.0.1 localhost'
owner: root
group: root
mode: 0644

在文件中添加一行

往/etc/hosts里添加一行192.168.0.131 test.breezey.top(多次执行,不会重复添加),示例如下:

1
2
3
4
- name: add a line
lineinfile:
dest: /etc/hosts
line: '192.168.0.131 test.breezey.top'

如果您不提供create参数,则如果文件不存在,此任务将失败。在大多数情况下,您希望将其排除在外。这样,当您想要更改预期存在的文件时,您会收到警报。

从文件中删除一行

处理文件中的行还包括删除行。使用lineinfile模块,这很容易做到。要确保文件中不存在特定行,您可以使用state参数。

1
2
3
4
#cat /etc/hosts

127.0.0.1 localhost.localdomain localhost ::1 localhost6.localdomain6 localhost6
192.168.0.130 hub.breezey.top

删除以192.168.0.130开头的行:

1
2
3
4
5
- name: delete a line
lineinfile:
dest: /etc/hosts
regex: '^192\.168\.0'
state: absent

默认情况下,state参数设置为“present”。这意味着如果您要添加或更改行,则不需要设置此参数。

换行

添加或删除纯线具有一组有限的实际用例。在大多数情况下,您需要更多技巧。一个非常常见的用例是更改配置文件。要定义要更改的行,可以使用regexp参数,该参数采用正则表达式。如果您搜索要设置的键,则可以防止该键被定义两次。如果您有“PermitEmptyPasswords yes”行,并且您只定义“line: PermitEmptyPasswords no”,那么您将在文件末尾得到一个全新的行。要实际更改现有行,使用正则表达式“^PermitEmptyPasswords”是有意义的。^字符确保您要更改的行以该键开头。这样您就不会意外更改评论,

1
2
3
4
5
- name: Change SSH daemon configuration
lineinfile:
line: PermitEmptyPasswords no
regexp: ^PermitEmptyPasswords
path: /etc/ssh/sshd_config

如果有匹配的行则修改该行,如果不匹配则添加

示例原文件/tmp/test.txt内容如下:

1
# %wheel   ALL=(ALL)   ALL

下面的示例task中,匹配以%wheel开头的行,匹配到,则执行替换,未匹配,则添加。因为原文件中,没有以%wheel开头的行,所以会添加一行:

1
2
3
4
5
- name: add or modify a line
lineinfile:
dest: /tmp/test.txt
regex: '^%wheel'
line: '%wheel ALL=(ALL) NOPASSWD: ALL'

修改后的文件如下:

1
2
3
4
#cat /tmp/text.txt

# %wheel ALL=(ALL) ALL
%wheel ALL=(ALL) NOPASSWD: ALL

参数backrefs,backup说明

  • backup: 是否备份原文件,默认为no
  • backrefs:
    • 当backrefs为no时,如果regex没有匹配到行,则添加一行,如果Regx匹配到行,则修改该行
    • 当backrefs为yes时,如果regex没有匹配到行,则保持原文件不变,如果regex匹配到行,则修改该行
    • backrefs默认为no,所以上面那个示例中,我们没有配置backrefs,而默认没有匹配,则修改。
      下面我们看一看backrefs为yes时匹配到行的示例:

示例原文件:

1
2
3
4
5
# cat /tmp/testfile

# %wheel ALL=(ALL) ALL
%wheel ALL=(ALL) NOPASSWD: ALL
#?bar

task示例:

1
2
3
4
5
6
7
8
- name: test backrefs
lineinfile:
backup: yes
state: present
dest: /tmp/testfile
regexp: '^#\?bar'
backrefs: yes
line: 'bar'

修改后的文件:

1
2
3
4
5
# cat /tmp/testfile

# %wheel ALL=(ALL) ALL
%wheel ALL=(ALL) NOPASSWD: ALL
bar

使用validate验证文件是否正确修改

在一些场景下,我们修改完文件后,需要对文件做一下测试,用以检查文件修改之后,是否能正常运行。如http.conf、nginx.conf等,一旦改错,而不加以测试,可能会直接导致http服务挂掉。

可以使用validate关键字,在修改完成以后,对文件执行检测:

1
2
3
4
5
6
7
8
9
- name: test validate
lineinfile:
dest: /etc/sudoers
state: present
regexp: '^%ADMIN ALL='
line: '%ADMIN ALL=(ALL)'
validate: 'visudo -cf %s'
tags:
- testsudo

template 模板

模板是一个文本文件,可以做为生成文件的模版,并且模板文件中还可嵌套jinja语法

1
2
3
4
5
6
- name: Copy nginx.conf template
template:
src: nginx.conf.j2
dest: "{{ nginx_installed_path }}/conf/nginx.conf"
mode: 0600
become_user: "{{ default_user }}"

ansible delegate_to 模块

1
2
3
4
5
6
7
8
- name: Local Download nginx
delegate_to: 127.0.0.1
get_url:
url: "{{ nginx_download_url }}"
dest: "/root/ansible/nginx-{{ nginx_version }}.tar.gz"
mode: 0755
become_user: "summer"
when: profile.find("real") != -1

参考文章

评论