17、ansible配置管理

17.1、前言:

1、说明:

ansible是自动化运维工具,基于Python开发,实现了批量系统配置、批量程序部署、批量运行命令等功能。

ansible是基于模块工作的,本身没有批量部署的能力,真正具有批量部署的是ansible所运行的模块,

ansible只是提供一种框架,主要组成如下:

(1)Ansible:

是Ansible的命令工具,核心执行工具,一次性或临时执行的操作都是通过该命令执行。

(2)Ansible Playbook:

任务剧本(又称任务集),编排定义Ansible任务集的配置文件,由Ansible顺序依次执行,yaml格式。

(3)Inventory:

Ansible管理主机的清单,默认是/etc/ansible/hosts文件。

扫描二维码关注公众号,回复: 9644615 查看本文章

(4)Modules:

Ansible执行命令的功能模块,Ansible2.3版本为止,共有1039个模块,还可以自定义模块。

(5)Plugins:

插件,模块功能的补充,常有连接类型插件,循环插件,变量插件,过滤插件,插件功能用的较少。

(6)API:

提供给第三方程序调用的应用程序编程接口。


2、总体架构:


3、ansible特性:

(1)no agents:不需要在被管控主机上安装任何客户端;

(2)no server:无服务器端,使用时直接运行命令即可;

(3)modules in any languages:基于模块工作,可使用任意语言开发模块;

(4)yaml,not code:使用yaml语言定制剧本playbook;

(5)ssh by default:基于SSH工作;

(6)strong multi-tier solution:可实现多级指挥。


4、ansible优点:

(1)轻量级,无需在客户端安装agent,更新时,只需在操作机上进行一次更新即可;

(2)批量任务执行可以写成脚本,而且不用分发到远程就可以执行;

(3)使用python编写,维护更简单,ruby语法过于复杂;

(4)支持sudo。


17.2、环境说明:

1、基本环境:


服务器名称

主从关系

ip地址

controller-node1

172.16.1.90

slave-node1

172.16.1.91

slave-node2

172.16.1.92



2、环境准备(主从操作相同):

(1)关闭防火墙:systemctl stop firewalld && systemctl disable firewalld

(2)禁用selinux:

sed -i "s/SELINUX=enforcing/SELINUX=disabled/g" /etc/sysconfig/selinux

setenforce 0

(3)更改yum和epel源:

wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo

yum install -y epel-release

wget -O /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-7.repo

yum clean all

yum makecache

yum repolist enabled

(4)查看系统:

cat /etc/redhat-release

#CentOS Linux release 7.5.1804 (Core)

uname -r

#3.10.0-862.el7.x86_64


17.3、ansible基本配置:

1、安装ansible:

[root@controller-node1 ~]# yum -y install ansible


2、查看ansible的版本:

[root@controller-node1 ~]# ansible --version


3、ansible inventory文件:

Inventory文件通常用于定义要管理的主机的认证信息,例如ssh登录用户名、密码以及key相关信息。可以同时操作一个组的多台主机,

组与主机组之间的关系都是通过inventory文件配置。配置文件路径为:/etc/ansible/hosts。提示,在定义连接从端的配置时最好都采

用一种连接方式,防止在操作时从端主机重复出现。

(1)基于密码连接从端:

[root@controller-node1 ~]# vim /etc/ansible/hosts

# 主机+端口号+密码

[webserver]

172.16.1.91 ansible_ssh_port=22 ansible_ssh_user=root ansible_ssh_pass="123456"

172.16.1.92 ansible_ssh_port=22 ansible_ssh_user=root ansible_ssh_pass="123456"


(2)基于秘钥连接从端:

1)生成公钥私钥并把公钥发送到从端:

[root@controller-node1 ~]# ssh-keygen

[root@controller-node1 ~]# for i in {1,2}; do ssh-copy-id -i 172.16.1.9$i ; done

2)配置连接:

[root@controller-node1 ~]# vim /etc/ansible/hosts

# 方法一,主机+端口号+秘钥

[webserver]

172.16.1.91:22

172.16.1.92:22


# 方法二,别名主机+端口+秘钥

[webserver]

slave-node1 ansible_ssh_host=172.16.1.91 ansible_ssh_port=22

slave-node2 ansible_ssh_host=172.16.1.92 ansible_ssh_port=22


(3)主机组的使用(别名主机+端口+秘钥):

[apache]

slave-node1 ansible_ssh_host=172.16.1.91 ansible_ssh_port=22

slave-node2 ansible_ssh_host=172.16.1.92 ansible_ssh_port=22


[nginx]

slave-node2 ansible_ssh_host=172.16.1.92 ansible_ssh_port=22


#定义多个组,把一个组当另外一个组的组员:

[webserver:children]

apache

nginx


(4)临时指定 inventory 文件:

1)先定义一个主机定义清单(别名主机+端口+秘钥):

[root@controller-node1 ~]# vim /etc/webservers

[apache]

slave-node1 ansible_ssh_host=172.16.1.91 ansible_ssh_port=22

slave-node2 ansible_ssh_host=172.16.1.92 ansible_ssh_port=22

2)在执行命令时指定 inventory 文件:

[root@controller-node1 ~]# ansible apache -m ping -i /etc/webservers -o


(5)inventory 内置参数:


4、ansible ad-hoc:

ad-hoc(临时的),在ansible中是指需要快速执行,并且不需要保存的命令。说白了就是执行简单的命令,

对于复杂的命令则为 playbook,类似于 saltstack 的 state sls 状态文件。

(1)ansible命令格式:

1)常用命令参数:

[root@controller-node1 ~]# ansible -h

Usage: ansible <host-pattern> [options]

-m MODULE_NAME

#模块名字

-a MODULE_ARGS

#模块参数

-o

#使用精简的输出

-C --check

#检查语法

-f FORKS

#并发

--list-hosts

#列出主机列表

2)示例:

[root@controller-node1 ~]# ansible webserver -m shell -a 'uptime' -o

3)命令说明:

ansible webserver -m shell -a 'uptime' -o


(2)host-pattern格式:

1)主机匹配:

说明:匹配主机ip方式不适用于"别名主机+端口+秘钥"的认证方式,需要将ip换成主机别名。

A、匹配一台目标主机:

[root@controller-node1 ~]# ansible 172.16.1.91 -m ping

B、匹配多台目标主机:

[root@controller-node1 ~]# ansible 172.16.1.91,172.16.1.92 -m ping

C、匹配所有目标主机:

[root@controller-node1 ~]# ansible all -m ping

2)组的匹配:

A、列出指定组下的所有主机:

[root@controller-node1 ~]# ansible apache --list

[root@controller-node1 ~]# ansible nginx --list

B、匹配一个组下的所有主机:

[root@controller-node1 ~]# ansible apache -m ping -o

C、匹配apache组中有,但是nginx组中没有的所有主机:

[root@controller-node1 ~]# ansible 'apache:!nginx' -m ping

D、匹配apache组和nginx组中都有的机器(交集):

[root@controller-node1 ~]# ansible 'apache:&nginx' -m ping

E、匹配apache组nginx组两个组所有的机器(并集,等于 ansible apache,nginx -m ping):

[root@controller-node1 ~]# ansible 'nginx:apache' -m ping


17.4、ansible-module操作:

ansible-doc常用命令:

[root@controller-node1 ~]# ansible-doc -h

Usage: ansible-doc [-l|-F|-s] [options] [-t <plugin type> ] [plugin

-j:以json格式显示指定的模块信息。

-l:列出所有的模块。

-s:查看模块常用参数,直接跟模块名,显示模块所有信息。


[root@controller-node1 ~]# ansible-doc -l |wc -l

3387


1、命令相关的模块:

(1)command模块:

ansible默认的模块,执行命令。

注意:shell中的"<", ">", "|", ";", "&","$"等特殊字符不能在command模块中使用,如果需要使用,则用shell模块。

1)查看模块参数:

[root@controller-node1 ~]# ansible-doc -s command

2)在192.168.1.31服务器上面执行ls命令,默认是在当前用户的家目录/root/:

[root@controller-node1 ~]# ansible slave-node1 -a 'ls'

3)chdir 先切换工作目录,再执行后面的命令,一般情况下在编译时候使用:

[root@controller-node1 ~]# ansible slave-node1 -a 'chdir=/tmp/ pwd'

4)creates:

A、如果creates的文件存在,则不执行后面的操作:

[root@controller-node1 ~]# ansible slave-node1 -a 'creates=/tmp/ ls /etc/passwd'

B、如果creates的文件不存在,则执行后面的操作:

[root@controller-node1 ~]# ansible slave-node1 -a 'creates=/tmp11/ ls /etc/passwd'

5)removes 和 creates 相反:

A、如果removes的文件存在,则执行后面的操作:

[root@controller-node1 ~]# ansible slave-node1 -a 'removes=/tmp/ ls /etc/passwd'

B、如果removes的文件不存在,则不执行后面的操作:

[root@controller-node1 ~]# ansible slave-node1 -a 'removes=/tmp11/ ls /etc/passwd'


(2)shell模块:

专门用来执行shell命令的模块,和command模块一样,参数基本一致,都有 chdir、creates、removes 模块。

1)查看模块参数:

[root@controller-node1 ~]# ansible-doc -s shell

2)示例:

[root@controller-node1 ~]# ansible slave-node1 -m shell -a 'mkdir /tmp/test/'

# ansible slave-node1 -m command -a 'mkdir /tmp/test/'

[root@controller-node1 ~]# ansible slave-node1 -m shell -a 'ls /tmp/'

# ansible slave-node1 -m command -a 'ls /tmp/'

3)执行下面这条命令,每次执行都会更新文件的时间戳:

[root@controller-node1 ~]# ansible slave-node1 -m shell -a 'cd /tmp/test && touch 1.txt && ls'

4)由于有时候不想更新文件的创建时间戳,如果文件存在就不执行 creates:

[root@controller-node1 ~]# ansible slave-node1 -m shell -a 'creates=/tmp/test/1.txt cd /tmp/test && touch 1.txt && ls'

# ansible slave-node1 -m command -a 'creates=/tmp/test/1.txt cd /tmp/test && touch 1.txt && ls'


(3)script模块:

用于在被管理机器上执行shell脚本的模块,脚本无需在被管理的机器上存放。

1)查看模块参数:

[root@controller-node1 ~]# ansible-doc -s script

2)编写shell脚本:

[root@controller-node1 ~]# cat ansible_test.sh

#!/bin/sh

echo `hostname`

3)在被管理的机器上执行该脚本:

[root@controller-node1 ~]# ansible all -m script -a '/root/ansible_test.sh'


2、文件相关的模块:

(1)file模块:

用于对文件的处理、创建、删除、权限的控制等。

1)查看模块参数:

[root@controller-node1 ~]# ansible-doc -s file

path:要管理的文件路径

recurse:递归

state(指定服务状态)

directory:创建目录,如果目标不存在则创建目录及其子目录

touch:创建文件,如果文件存在,则修改文件属性

absent:删除文件或目录

mode:设置文件或目录权限

owner:设置文件或目录属主信息

group:设置文件或目录属组信息

link:创建软连接,需要和src配合使用

hard:创建硬连接,需要和src配合使用

2)创建目录:

[root@controller-node1 ~]# ansible slave-node2 -m file -a 'path=/tmp/test1/ state=directory'

3)创建文件:

[root@controller-node1 ~]# ansible slave-node2 -m file -a 'path=/tmp/test2 state=touch'

4)建立软链接(src表示源文件,path表示目标文件):

[root@controller-node1 ~]# ansible slave-node2 -m file -a 'src=/tmp/test1/ path=/tmp/test3 state=link'

5)删除文件:

[root@controller-node1 ~]# ansible slave-node2 -m file -a 'path=/tmp/test2 state=absent'

6)创建文件时同时设置权限等信息:

[root@controller-node1 ~]# ansible slave-node2 -m file -a 'path=/tmp/test4 state=directory mode=775 owner=root group=root'


(2)copy模块:

用于管理端复制文件到远程主机(拷贝后的默认权限文件是644),并可以设置权限,属组,属主等。

重复拷贝时一个文件文件名称和文件的内容相同的文件时执行失败,当文件内容更新时可以再次拷

贝。

1)查看模块参数:

[root@ansible ~]# ansible-doc -s copy

src:需要copy的文件的源路径

dest:需要copy的文件的目标路径

backup:对copy的文件进行备份

content:直接在远程主机被管理文件中添加内容,会覆盖原文件内容

mode:对copy到远端的文件设置权限

owner:对copy到远端的文件设置属主

group:对copy到远端文件设置属组

2)复制文件到远程主机并改名:

[root@controller-node1 ~]# ansible slave-node2 -m copy -a 'src=/root/ansible_test.sh dest=/root/a.sh'

3)复制文件到远程主机,并备份远程文件,安装时间信息备份文件(当更新文件内容后,重新copy时用到):

[root@controller-node1 ~]# ansible slave-node2 -m copy -a 'src=/root/ansible_test.sh dest=/root/a.sh backup=yes'

4)直接在远程主机a.sh中添加内容:

[root@controller-node1 ~]# ansible slave-node2 -m copy -a 'dest=/tmp/a.sh content="#!/bin/bash\necho `uptime`"'

5)复制文件到远程主机,并设置权限及属主与属组:

[root@controller-node1 ~]# ansible slave-node2 -m copy -a 'src=/etc/passwd dest=/tmp/passwd mode=700 owner=root group=root'


(3)fetch模块:

用于从被管理机器上拉取文件,拉取下来的内容会保留目录结构,常用于收集被管理机器的日志文件等。

1)查看模块参数:

[root@controller-node1 ~]# ansible-doc -s fetch

src:指定需要从远端机器拉取的文件路径。

dest:指定从远端机器拉取下来的文件存放路径。

2)从被管理机器上拉取cron日志文件,默认会已管理节点地址创建一个目录,并存放在内

[root@controller-node1 ~]# ansible slave-node2 -m fetch -a 'src=/var/log/cron dest=/tmp/'

[root@controller-node1 ~]# tree /tmp/


3、用户相关的模块:

用于对系统用户的管理,用户的创建、删除、家目录、属组等设置。

(1)user:

1)查看模块参数:

[root@controller-node1 ~]# ansible-doc -s user

name:指定用户的名字

home:指定用户的家目录

uid:指定用户的uid

group:指定用户的用户组

groups:指定用户的附加组

password:指定用户的密码

shell:指定用户的登录shell

create_home:是否创建用户家目录,默认是yes

remove:删除用户时,指定是否删除家目录

state(指定服务状态)

absent:删除用户

2)创建用户名,指定uid,指定家目录及主组:

[root@controller-node1 ~]# ansible slave-node2 -m user -a 'name=chang uid=1002 home=/opt/chang/ group=root'

[root@controller-node1 ~]# ansible slave-node2 -m shell -a 'id chang && ls -l /opt'

3)创建用户,不创建家目录,并且不能登录:

[root@controller-node1 ~]# ansible slave-node2 -m user -a 'name=lc uid=1003 create_home=no shell=/sbin/nologin'

[root@controller-node1 ~]# ansible slave-node2 -m shell -a 'id lc && tail -1 /etc/passwd'

4)删除用户:

[root@controller-node1 ~]# ansible slave-node2 -m user -a 'name=lc state=absent'

5)删除用户并删除家目录:

[root@controller-node1 ~]# ansible slave-node2 -m user -a 'name=chang state=absent remove=yes'


(2)group:

用于创建组,当创建用户时如果需要指定组,组不存在的话就可以通过group先创建组。

1)查看模块参数:

[root@controller-node1 ~]# ansible-doc -s group

name:指定组的名字

gid:指定组的gid

state(指定服务状态):

absent:删除组

present:创建组(默认的状态)

2)创建组:

[root@controller-node1 ~]# ansible slave-node2 -m group -a 'name=lc2'

3)创建组并指定gid:

[root@controller-node1 ~]# ansible slave-node2 -m group -a 'name=lc3 gid=1005'

3)删除组:

[root@controller-node1 ~]# ansible slave-node2 -m group -a 'name=lc3 state=absent'


4、软件包相关的模块:

(1)yum模块:

用于对软件包的管理,下载、安装、卸载、升级等操作。

1)查看模块参数:

[root@controller-node1 ~]# ansible-doc -s yum

name:指定要操作的软件包名字

download_dir:指定下载软件包的存放路径,需要配合download_only一起使用

download_only:只下载软件包,而不进行安装,和yum --downloadonly一样

list:

installed:列出所有已安装的软件包

updates:列出所有可以更新的软件包

repos:列出所有的yum仓库

state(指定服务状态):

installed, present:安装软件包(两者任选其一都可以)

removed, absent:卸载软件包

latest:安装最新软件包

2)列出所有已安装的软件包:

[root@controller-node1 ~]# ansible slave-node2 -m yum -a 'list=installed'

3)列出所有可更新的软件包:

[root@controller-node1 ~]# ansible slave-node2 -m yum -a 'list=updates'

4)列出所有的yum仓库:

[root@controller-node1 ~]# ansible slave-node2 -m yum -a 'list=repos'

5)只下载软件包并到指定目录下:

[root@controller-node1 ~]# ansible slave-node2 -m yum -a 'name=httpd download_only=yes download_dir=/tmp/'

6)安装软件包:

[root@controller-node1 ~]# ansible slave-node2 -m yum -a 'name=httpd state=installed'

7)卸载软件包:

[root@controller-node1 ~]# ansible slave-node2 -m yum -a 'name=httpd state=removed'

8)安装包组,类似yum groupinstall 'Development Tools':

[root@controller-node1 ~]# ansible slave-node2 -m yum -a 'name="@Development Tools" state=installed'


(2)pip模块:

用于安装python中的包。

1)查看模块参数:

[root@controller-node1 ~]# ansible-doc -s pip

2)使用pip时,需要保证被管理机器上有python-pip软件包:

[root@controller-node1 ~]# ansible slave-node2 -m yum -a 'name=python-pip'

3)安装pip包:

[root@controller-node1 ~]# ansible slave-node2 -m pip -a 'name=django'


(3)service模块:

1)查看模块参数:

[root@controller-node1 ~]# ansible-doc -s service

name:指定需要管理的服务名

enabled:指定是否开机自启动

state(指定服务状态):

started:启动服务

stopped:停止服务

restarted:重启服务

reloaded:重载服务

2)启动服务,并设置开机自启动:

[root@controller-node1 ~]# ansible slave-node2 -m service -a 'name=crond state=started enabled=yes'


5、计划任务相关的模块:

(1)cron模块:

用于指定计划任务,和crontab -e一样。

1)查看模块参数:

[root@controller-node1 ~]# ansible-doc -s cron

job:指定需要执行的任务

minute:分钟

hour:小时

day:天

month:月

weekday:周

name:对计划任务进行描述

state(指定服务状态)

absetn:删除计划任务

2)创建一个没有带描述的计划任务:

[root@controller-node1 ~]# ansible slave-node2 -m cron -a 'job="/bin/sh /root/test.sh"'

[root@controller-node1 ~]# ansible slave-node2 -a 'crontab -l'

3)创建一个带描述的计划任务:

[root@controller-node1 ~]# ansible slave-node2 -m cron -a 'name="这是一个测试的计划任务" minute=* hour=* day=* month=* weekday=* job="/bin/bash /root/test.sh"'

[root@controller-node1 ~]# ansible slave-node2 -a 'crontab -l'

4)删除计划任务:

[root@controller-node1 ~]# ansible slave-node2 -m cron -a 'name="None" job="/bin/sh /root/test/sh" state=absent'


6、系统信息相关的模块 setup:

用于获取系统信息的一个模块。

(1)查看模块参数:

[root@controller-node1 ~]# ansible-doc -s setup

(2)查看系统所有信息:

[root@controller-node1 ~]# ansible slave-node1 -m setup

(3)filter 对系统信息进行过滤:

[root@controller-node1 ~]# ansible slave-node1 -m setup -a 'filter=ansible_all_ipv4_addresses'

(4)常用的过滤选项:

ansible_all_ipv4_addresses

# 所有的ipv4地址

ansible_all_ipv6_addresses

# 所有的ipv6地址

ansible_architecture

# 系统的架构

ansible_date_time

# 系统时间

ansible_default_ipv4

# 系统的默认ipv4地址

ansible_distribution

# 系统名称

ansible_distribution_file_variety

# 系统的家族

ansible_distribution_major_version

# 系统的版本

ansible_domain

# 系统所在的域

ansible_fqdn

# 系统的主机名

ansible_hostname

# 系统的主机名,简写

ansible_os_family

# 系统的家族

ansible_processor_cores

# cpu的核数

ansible_processor_count

# cpu的颗数

ansible_processor_vcpus

# cpu的个数


17.5、ansible-playbook操作:

1、playbook介绍:

Playbook与ad-hoc相比是一种完全不同的运用ansible的方式,类似与saltstack的state状态文件。ad-hoc无法持久使用,playbook可以持久使用。

playbook是由一个或多个play组成的列表,play的主要功能在于将事先归并为一组的主机装扮成事先通过ansible中的task定义好的角色。从根本上

来讲,所谓的task无非是调用ansible的一个module。将多个play组织在一个playbook中,即可以让它们联合起来按事先编排的机制完成某一任务。


2、playbook核心元素:

Hosts:执行的远程主机列表。

Tasks:任务集。

Varniables:内置变量或自定义变量在playbook中调用。

Templates:模板,即使用模板语法的文件,比如配置文件等。

Handlers 和 notity 结合使用:由特定条件触发的操作,满足条件方才执行,否则不执行。

tags:标签,指定某条任务执行,用于选择运行 playbook 中的部分代码。


3、playbook语法:

(1)playbook使用yaml语法格式,后缀可以是yaml,也可以是yml。在单 一 一 个playbook文件中,可以连续三个连子号(---)区分多个play,还有

选择性的连续三个点号(...)用来表示play的结尾,也可省略。

(2)次行开始正常写playbook的内容,一般都会写上描述该playbook的功能。

(3)使用#号注释代码。

(4)缩进必须统一,不能空格和tab混用。

(5)缩进的级别也必须是一致的,同样的缩进代表同样的级别,程序判别配置的级别是通过缩进结合换行实现的。

(6)YAML文件内容和Linux系统大小写判断方式保持一致,是区分大小写的,k/v的值均需大小写敏感。k/v的值可同行写也可以换行写,同行使用":"

分隔,v可以是个字符串,也可以是一个列表。

(7)一个完整的代码块功能需要最少元素包括 name: task。


4、创建playbook文件:

[root@controller-node1 ~]# vim playbook01.yml

--- #固定格式

- hosts: slave-node2 #定义需要执行主机

remote_user: root #远程用户

vars: #定义变量

http_port: 8080 #变量

soft_name: httpd


tasks: #定义一个任务的开始

- name: create new file #定义任务的名称

file: name=/tmp/playtest.txt state=touch #调用模块,具体要做的事情

- name: create new user

user: name=test02 system=yes shell=/sbin/nologin

- name: install package

yum: name={{ soft_name }} state=installed

- name: config httpd

template: src=/root/httpd.conf dest=/etc/httpd/conf/httpd.conf

notify: #定义执行一个动作(action)让handlers来引用执行,与handlers配合使用

- restart apache #notify要执行的动作,这里必须与handlers中的name定义内容一致

- name: copy index.html

copy: src=/root/index.html dest=/var/www/html/index.html

- name: start httpd

service: name=httpd state=started

handlers: #处理器:更加tasks中notify定义的action触发执行相应的处理动作

- name: restart apache #要与notify定义的内容相同

service: name=httpd state=restarted #触发要执行的动作


5、文件准备:

(1)html文件准备:

[root@controller-node1 ~]# echo "playbook test file" >index.html

(2)配置文件准备:

[root@controller-node1 ~]# grep ^Listen httpd.conf


6、执行playbook:

[root@controller-node1 ~]# ansible-playbook playbook01.yml


playbook常用选项:

格式:ansible-playbook <filename.yml> ... [options]

[root@controller-node1 ~]# ansible-playbook -h

--check or -C:只检测可能会发生的改变,但不真正执行操作。

--list-hosts:列出运行任务的主机。

--list-tags:列出playbook文件中定义所有的tags。

--list-tasks:列出playbook文件中定义的所以任务集。

--limit:主机列表只针对主机列表中的某个主机或者某个组执行。

-f:指定并发数,默认为5个。

-t:指定tags运行,运行某一个或者多个tags(前提playbook中有定义tags)。

-v:显示过程 -vv -vvv 更详细。


7、验证playbook执行的结果:

(1)验证创建的文件和用户:

[root@controller-node1 ~]# ansible slave-node2 -m shell -a 'ls /tmp/playtest.txt && id test02'

(2)验证安装的httpd服务及更改的配置文件:

[root@controller-node1 ~]# curl 172.16.1.92:8080


















猜你喜欢

转载自www.cnblogs.com/LiuChang-blog/p/12435222.html
今日推荐