Linux-Ansible学习手杂
本文最后更新于 1138 天前,其中的信息可能已经有所发展或是发生改变。

Linux-Ansible学习手杂

Ansible 介绍

Ansible是一个配置管理和配置工具,使用SSH连接到服务器并运行配置好的任务,服务器上不需要安装任何其他软件,只需要开启SSH,客户端的ansible会完成所有其他的工作。

img

上图为ansible的基本架构,从上图可以了解到其由以下部分组成:

  • 核心:ansible
  • 核心模块(Core Modules):这些都是ansible自带的模块
  • 扩展模块(Custom Modules):如果核心模块不足以完成某种功能,可以添加扩展模块
  • 插件(Plugins):完成模块功能的补充
  • 剧本(Playbooks):ansible的任务配置文件,将多个任务定义在剧本中,由ansible自动执行
  • 连接插件(Connectior Plugins):ansible基于连接插件连接到各个主机上,虽然ansible是使用ssh连接到各个主机的,但是它还支持其他的连接方法,所以需要有连接插件
  • 主机群(Host Inventory):定义ansible管理的主机

Ansible 部署

YUM方式安装

# debian & Ubuntu
sudo apt-get install ansible

# mac os 
brew install ansible

# Centos 
yum install ansible

# pip
sudo pip install ansible

Centos-安装信息:

# 添加Ansible源
cat <<EOF> /etc/yum.repos.d/tsinghua.repo
[ansible]
name=ansible
baseurl=https://mirror.tuna.tsinghua.edu.cn/epel/7/x86_64/
gpgcheck=0
EOF

# 安装Ansible
yum -y install ansible

正在安装:
 ansible noarch 2.9.25-1.el7
为依赖而安装:
 python-babel noarch 0.9.6-8.el7
 python-jinja2 noarc2.7.2-4.el7
 python-markupsafe x86_64 0.11-10.el7  
 python-paramiko noarch2.1.1-9.el7        
 python2-httplib2 noarch    0.18.1-3.el7                                             
 python2-jmespath noarch 0.9.4-2.el7                                               
 sshpass x86_64 1.06-2.el7  

 # 验证Ansible
ansible --version

源码方式安装

# 安装git客户端/gcc编译包
yum install git gcc-c++ 

# 拉取源码
git clone https://github.com/ansible/ansible.git /usr/local/ansible

# 执行安装文件
source /usr/local/ansible/hacking/env-setup

# 验证Ansible
ansible --version

安装好 Ansible 后,我们可以在 /etc/ansible/ 的目录底下找到 Ansible 的一些配置文件.

公司通常把 ansible.cfghosts 这两个配置文件与其它的 Playbooks 放在同个目录下,然后通过git把它们一起管理起来防止出现不必要的问题.

Ansible 配置文件介绍

目录结构介绍

通过如下命令我们可以获取Ansible所有文件存放目录:

# 查看ansible 安装目录结构
rpm -ql ansible

该命令输出内容较多, 大致分为如下几类:

描述 路径
配置文件目录 /etc/ansible/
执行文件目录 /usr/bin/
Lib库依赖目录 /usr/lib/pythonX.X/site-packages/ansible/
Help 文档目录 /usr/share/doc/ansible-X.X.X/
Man文档目录 /usr/share/man/manl/

Ansible配置文件解析(ansible.cfg)

Inventory 用于定义 Ansible 的主机列表配置, Ansible 的自身配置文件只有一个, 即ansible.cfg, Ansible 安装好后它默认存放于/etc/ansible/ 目录下。

ansible.cfg 配置文件可以存在于多个地方, Ansible 读取配置文件的顺序依次是当前命令执行目录→用户家目录下的。ansible.cfg→ /etc/ansible.cfg, 先找到哪个就使用哪个的配置。其 ansible.cfg 配置的所有内容均可在命令行通过参数的形式传递或定义在 Playbooks 中。目前ansible 配置文件有四种,如下显示:

  • 使用/etc/ansible/ansible.cfg

ansible 软件包提供一个基本的配置文件,它位于/etc/ansible/ansible.cfg如果找不到其他配置文件,则使用此文件。

  • 使用~/ansible.cfg

Ansible 在用户的主目录中查找 .ansible.cfg 文件。如果存在此配萱并且当前工作目录中也没有ansible.cfg 文件,则使用此配置取代/etc/ansible/ansible.cfg

  • 使用./ansible.cfg

如果执行ansible 命令的目录中存在ansible.cfg 文件,则使用它,而不使用全局文件或用户的个入文件。这样,管理员可以创建一种目录结构,将不同的环境或项目存储在单独的目录中,并且每一目录包含为独特的一组设置而定制的配置文件。

  • 使用ANSIBLE_CONFIG 环境变量

通过ANSIBLE_CONFIG 环境变量定义配萱文件的位置。定义了此变量时,Ansible 将使用变 量所指定的配置文件,而不用上文中提及的任何配置文件。

  • 这四种文件的优先级为
ANSIBLE_CONFIG环境变量 > ./ansible.cfg > ~/ansible.cfg > /etc/ansible/ansible.cfg

配置文件 ansible.cfg 约有 500 行语句, 大多数为注释行默认配置项。 该文件遵循 INI 格 式, 主要常修改配置如下:

inventory      = /etc/ansible/hosts # 定义的inventory文件
library        = /usr/share/my_modules/ # 自定义lib库存放目录
forks          = 5 # 默认并发数
poll_interval  = 15 # 默认轮询时间间隔
sudo_user      = root # 默认sudo用户
remote_port    = 22 # 默认远程ssh 端口
host_key_checking = False  #第一次交互目标主机,需要输入yes/no,改成False不用输入
timeout = 10 # 设置SSH连接的超时时间,单位为秒
log_path = /var/log/ansible.log    #设置日志路径

Ansible 管理主机配置文件解析(Inventory)

Inventory 是 Ansible 管理主机信息的配置文件,相当于系统 HOSTS 文件的功能,默认存放在/etc/ansible/hosts。为方便批量管理主机,便捷使用其中的主机分组.

内置变量清单

主机连接配置

ansible_connection:与主机的连接类型。可以是ansible的连接插件的名称。SSH协议类型为smart、ssh或paramiko。默认值是smart。

所有连接配置

ansible_host:要连接到远程主机的名称。

ansible_port:要连接到远程主机的端口,默认为22。

ansible_user:连接到远程主机的用户名。

ansible_password:连接到远程主机的用户名密码。

SSH连接配置

ansible_ssh_private_key_file:ssh使用的私有文件,适用于有多个秘钥,但不想使用ssh agent情况。

ansible_ssh_common_args:该设置附加到默认的sftp、scp和ssh命令行上。

ansible_sftp_extra_args:该设置总是附加到默认的sftp命令行上。

ansible_scp_extra_args:该设置总是附加到默认的scp命令行上。

ansible_ssh_extra_args:该设置总是附加到默认的ssh命令行上。

ansible_ssh_pipelining:确定是否使用SSH pipelining,该参数会覆盖ansible.cfg中的pipelining设置。

ansible_ssh_executable:此设置会覆盖使用系统ssh的默认行为,会覆盖ansible.cfg中的ssh_executable参数。

如果只有一个 Inventory 时可不用指定路径,默认读取/etc/ansible/hosts,Inventory 可以同时存在多个.

参考示例:

## 直接指明主机地址或主机名:
192.168.1.88
192.168.1.34
192.168.1.33

## 定义一个主机组[组名]把地址或主机名加进去
[docker]
192.168.1.88
## 192.168.1.33,192.168.1.34
192.168.1.3[3:4]

[docker:vars]
## 加上这个就不用输入密码,也不用密钥,但是真心不建议密码明文写在这里
ansible_password='密码'
## 端口
ansible_port=22
## 可设置登录用户
ansible_user='test'

[ansible:children]
docker

Ansible 通过Inventory 来定义其主机和组,在使用时通过-i 或 –inventory-file 指定读取,与 Ansible 命令结合使用时组合如下:

ansible -i /etc/ansible/hosts docker -m ping 

Ansible 常用命令

Ansible 命令集

/usr/bin/ansible  # Ansibe AD-Hoc 临时命令执行工具,常用于临时命令的执行
/usr/bin/ansible-doc   # Ansible 模块功能查看工具
/usr/bin/ansible-galaxy  # 下载/上传优秀代码或Roles模块 的官网平台,基于网络的
/usr/bin/ansible-playbook  # Ansible 定制自动化的任务集编排工具
/usr/bin/ansible-pull  # Ansible远程执行命令的工具,拉取配置而非推送配置(使用较少,海量机器时使用,对运维的架构能力要求较高)
/usr/bin/ansible-vault  # Ansible 文件加密工具
/usr/bin/ansible-console  # Ansible基于Linux Consoble界面可与用户交互的命令执行工具

其中,我们比较常用的是/usr/bin/ansible/usr/bin/ansible-playbook

Ansible 命令详解

命令的具体格式如下:

ansible <host-pattern> [-f forks] [-m module_name] [-a args]

也可以通过ansible -h来查看帮助,下面我们列出一些比较常用的选项,并解释其含义:

-a MODULE_ARGS #模块的参数,如果执行默认COMMAND的模块,即是命令参数,如: “date”,“pwd”等等
-k--ask-pass #ask for SSH password。登录密码,提示输入SSH密码而不是假设基于密钥的验证
--ask-su-pass #ask for su password。su切换密码
-K--ask-sudo-pass #ask for sudo password。提示密码使用sudo,sudo表示提权操作
--ask-vault-pass #ask for vault password。假设我们设定了加密的密码,则用该选项进行访问
-B SECONDS #后台运行超时时间
-C #模拟运行环境并进行预运行,可以进行查错测试
-c CONNECTION #连接类型使用
-f FORKS #并行任务数,默认为5
-i INVENTORY #指定主机清单的路径,默认为/etc/ansible/hosts
--list-hosts #查看有哪些主机组
-m MODULE_NAME #执行模块的名字,默认使用 command 模块,所以如果是只执行单一命令可以不用 -m参数
-o #压缩输出,尝试将所有结果在一行输出,一般针对收集工具使用
-S #用 su 命令
-R SU_USER #指定 su 的用户,默认为 root 用户
-s #用 sudo 命令
-U SUDO_USER #指定 sudo 到哪个用户,默认为 root 用户
-T TIMEOUT #指定 ssh 默认超时时间,默认为10s,也可在配置文件中修改
-u REMOTE_USER #远程用户,默认为 root 用户
-v #查看详细信息,同时支持-vvv-vvvv可查看更详细信息

Ansible 配置公私钥

上面我们已经提到过 ansible 是基于 ssh 协议实现的,所以其配置公私钥的方式与 ssh 协议的方式相同,具体操作步骤如下:

ssh-keygen -t rsa
ssh-copy-id root@192.168.1.88
ssh-copy-id root@192.168.1.33
ssh-copy-id root@192.168.1.34

Ansible 常用模块

1. 主机连通性测试(ping模块)

我们使用ansible docker -m ping命令来进行主机连通性测试,效果如下:

[root@app31 .ssh]# ansible docker -m ping
192.168.1.34 | SUCCESS => {
    "changed": false, 
    "ping": "pong"
}
192.168.1.33 | SUCCESS => {
    "changed": false, 
    "ping": "pong"
}
192.168.1.88 | SUCCESS => {
    "changed": false, 
    "ping": "pong"
}

这样就说明我们的主机是连通状态的

2. command 模块

这个模块可以直接在远程主机上执行命令,并将结果返回本主机。如:ansible docker -m command -a 'df -h'

# 查询磁盘
[root@app31 .ssh]# ansible docker -m command -a 'df -h'
192.168.1.34 | SUCCESS | rc=0 >>
文件系统                 容量  已用  可用 已用% 挂载点
/dev/mapper/centos-root   50G  6.0G   45G   12% /
devtmpfs                  16G     0   16G    0% /dev
192.168.1.33 | SUCCESS | rc=0 >>
文件系统                 容量  已用  可用 已用% 挂载点
/dev/mapper/centos-root   50G   13G   38G   25% /
devtmpfs                  16G     0   16G    0% /dev
192.168.1.88 | SUCCESS | rc=0 >>
文件系统                 容量  已用  可用 已用% 挂载点
devtmpfs                  48G     0   48G    0% /dev
tmpfs                     48G     0   48G    0% /dev/shm

# #先切换到/data/ 目录在ls查看信息
[root@app31 .ssh]# ansible docker -m command -a 'chdir=/data ls'
192.168.1.34 | SUCCESS | rc=0 >>
es
mysql
192.168.1.88 | SUCCESS | rc=0 >>
20210415
yarn
192.168.1.33 | SUCCESS | rc=0 >>

命令模块接受命令名称,后面是空格分隔的列表参数。给定的命令将在所有选定的节点上执行。它不会通过shell进行处理,比如$HOME和操作如”<“,”>”,”|”,”;”,”&” 工作(需要使用(shell)模块实现这些功能)。

注意,该命令不支持| 管道命令

3. shell 模块

shell模块可以在远程主机上调用shell解释器运行命令,支持shell的各种功能,例如管道等。如:ansible docker -m shell -a 'cat /etc/passwd | grep root'

[root@app31 home]# ansible docker -m shell -a 'cat /etc/passwd | grep root'
192.168.1.34 | SUCCESS | rc=0 >>
root:x:0:0:root:/root:/bin/bash
operator:x:11:0:operator:/root:/sbin/nologin
192.168.1.88 | SUCCESS | rc=0 >>
root:x:0:0:root:/root:/bin/bash
operator:x:11:0:operator:/root:/sbin/nologin
192.168.1.33 | SUCCESS | rc=0 >>
root:x:0:0:root:/root:/bin/bash
operator:x:11:0:operator:/root:/sbin/nologin

[root@app31 home]# ansible docker -m shell -a 'echo $USER'
192.168.1.34 | SUCCESS | rc=0 >>
root
192.168.1.88 | SUCCESS | rc=0 >>
root
192.168.1.33 | SUCCESS | rc=0 >>
root

只要是我们的shell命令,都可以通过这个模块在远程主机上运行.

4. copy 模块

这个模块用于将文件复制到远程主机,同时支持给定内容生成文件和修改权限等。其相关选项如下:

src    # 被复制到远程主机的本地文件。可以是绝对路径,也可以是相对路径。如果路径是一个目录,则会递归复制,用法类似于"rsync"
content   # 当不使用src指定拷贝的文件时,可以使用content直接指定文件内容,src与content两个参数必有其一,否则会报错
dest    # 必选项,将源文件复制到的远程主机的绝对路径
backup=yes # 表示将受控主机存在文件时,将源文件备份
mode    # 递归设定目录的权限,默认为系统默认权限,如果你想将权限设置为”rw-r--r--“,则可以使用mode=0644表示,如果你想要在user对应的权限位上添加执行权限,则可以使用mode=u+x表示
owner # 指定目的地文件所有人
group # 指定目的地文件所有组
force # 当目标主机包含该文件,但内容不同时,设为"yes",表示强制覆盖;设为"no",表示目标主机的目标位置不存在该文件才复制。默认为"yes"

4.1 复制文件:

[root@app31 home]# ansible docker -m copy -a 'src=/home/test2.sh dest=/data/'
192.168.1.34 | SUCCESS => {
    "changed": true, 
    "checksum": "b4f51832b152e08b304b1c682ba05adece71c045", 
    "dest": "/data/test2.sh", 
    "gid": 0, 
    "group": "root", 
    "md5sum": "ffc3725f409617864c817447685a4eda", 
    "mode": "0644", 
    "owner": "root", 
    "size": 15, 
    "src": "/root/.ansible/tmp/ansible-tmp-1638782767.44-93562767670664/source", 
    "state": "file", 
    "uid": 0
}
192.168.1.88 | SUCCESS => {
    "changed": true, 
    "checksum": "b4f51832b152e08b304b1c682ba05adece71c045", 
    "dest": "/data/test2.sh", 
    "gid": 0, 
    "group": "root", 
    "md5sum": "ffc3725f409617864c817447685a4eda", 
    "mode": "0644", 
    "owner": "root", 
    "size": 15, 
    "src": "/root/.ansible/tmp/ansible-tmp-1638782767.43-213537548213422/source", 
    "state": "file", 
    "uid": 0
}
192.168.1.33 | SUCCESS => {
    "changed": true, 
    "checksum": "b4f51832b152e08b304b1c682ba05adece71c045", 
    "dest": "/data/test2.sh", 
    "gid": 0, 
    "group": "root", 
    "md5sum": "ffc3725f409617864c817447685a4eda", 
    "mode": "0644", 
    "owner": "root", 
    "size": 15, 
    "src": "/root/.ansible/tmp/ansible-tmp-1638782767.44-117162678116772/source", 
    "state": "file", 
    "uid": 0
}

4.2.给定内容生成文件,并赋予权限:

[root@app31 home]# ansible docker -m copy -a 'content="this is test" dest=/data/test.txt mode=755'
192.168.1.34 | SUCCESS => {
    "changed": true, 
    "checksum": "b6794b2000d94d348203d0279c2e7322b922cb16", 
    "dest": "/data/test.txt", 
    "gid": 0, 
    "group": "root", 
    "md5sum": "8c6d115258631625b625486f81b09532", 
    "mode": "0755", 
    "owner": "root", 
    "size": 12, 
    "src": "/root/.ansible/tmp/ansible-tmp-1638783198.51-187666865731240/source", 
    "state": "file", 
    "uid": 0
}
192.168.1.88 | SUCCESS => {
    "changed": true, 
    "checksum": "b6794b2000d94d348203d0279c2e7322b922cb16", 
    "dest": "/data/test.txt", 
    "gid": 0, 
    "group": "root", 
    "md5sum": "8c6d115258631625b625486f81b09532", 
    "mode": "0755", 
    "owner": "root", 
    "size": 12, 
    "src": "/root/.ansible/tmp/ansible-tmp-1638783198.5-100956289999740/source", 
    "state": "file", 
    "uid": 0
}
192.168.1.33 | SUCCESS => {
    "changed": true, 
    "checksum": "b6794b2000d94d348203d0279c2e7322b922cb16", 
    "dest": "/data/test.txt", 
    "gid": 0, 
    "group": "root", 
    "md5sum": "8c6d115258631625b625486f81b09532", 
    "mode": "0755", 
    "owner": "root", 
    "size": 12, 
    "src": "/root/.ansible/tmp/ansible-tmp-1638783198.5-15493567640531/source", 
    "state": "file", 
    "uid": 0
}

4.3. 文件存在,覆盖备份

[root@app31 home]# ansible docker -m copy -a 'content="this is test2" dest=/data/test.txt mode=755 backup=yes'
192.168.1.34 | SUCCESS => {
    "backup_file": "/data/test.txt.32304.2021-12-06@17:35:41~", 
    "changed": true, 
    "checksum": "ef6ea4aa3e5d645f0138cbce096dd551a26dad5d", 
    "dest": "/data/test.txt", 
    "gid": 0, 
    "group": "root", 
    "md5sum": "fc30b9256da0d4d40c2e6ad176dd835a", 
    "mode": "0755", 
    "owner": "root", 
    "size": 13, 
    "src": "/root/.ansible/tmp/ansible-tmp-1638783339.75-149812909314543/source", 
    "state": "file", 
    "uid": 0
}
192.168.1.33 | SUCCESS => {
    "backup_file": "/data/test.txt.31756.2021-12-06@17:35:42~", 
    "changed": true, 
    "checksum": "ef6ea4aa3e5d645f0138cbce096dd551a26dad5d", 
    "dest": "/data/test.txt", 
    "gid": 0, 
    "group": "root", 
    "md5sum": "fc30b9256da0d4d40c2e6ad176dd835a", 
    "mode": "0755", 
    "owner": "root", 
    "size": 13, 
    "src": "/root/.ansible/tmp/ansible-tmp-1638783339.75-5933564884918/source", 
    "state": "file", 
    "uid": 0
}
192.168.1.88 | SUCCESS => {
    "backup_file": "/data/test.txt.20905.2021-12-06@17:35:42~", 
    "changed": true, 
    "checksum": "ef6ea4aa3e5d645f0138cbce096dd551a26dad5d", 
    "dest": "/data/test.txt", 
    "gid": 0, 
    "group": "root", 
    "md5sum": "fc30b9256da0d4d40c2e6ad176dd835a", 
    "mode": "0755", 
    "owner": "root", 
    "size": 13, 
    "src": "/root/.ansible/tmp/ansible-tmp-1638783339.74-85375342208580/source", 
    "state": "file", 
    "uid": 0
}

5. file 模块

该模块主要用于设置文件的属性,比如创建文件、创建链接文件、删除文件等。下面是一些常见的命令:

force  # 需要在两种情况下强制创建软链接,一种是源文件不存在,但之后会建立的情况下;另一种是目标软链接已存在,需要先取消之前的软链,然后创建新的软链,有两个选项:yes|no
dest   # 被链接到的路径,只应用于state=link的情况
group  # 定义文件/目录的属组。后面可以加上mode:定义文件/目录的权限 默认为root
owner  # 定义文件/目录的属主。后面必须跟上path:定义文件/目录的路径 默认为root
recurse# 递归设置文件的属性,只对目录有效,后面跟上src:被链接的源文件路径,只应用于state=link的情况
mode   # 文件复制到远程并设定权限,默认file=644,directory=755
path   # 指定远程服务器的路径,也可以写成‘dest’,‘name’
state  # 状态,有以下选项:
    directory # 如果目录不存在,就创建目录
    file # 即使文件不存在,也不会被创建
    link # 创建软链接
    hard # 创建硬链接
    touch# 如果文件不存在,则会创建一个新的文件,如果文件或目录已存在,则更新其最后修改时间
    absent#删除目录、文件或者取消链接文件

5.1. 创建一个文件并且赋值

[root@app31 data]# ansible docker -m file -a 'path=/data/test_file.txt owner=root group=root mode=755 state=touch'
192.168.1.34 | SUCCESS => {
    "changed": true, 
    "dest": "/data/test_file.txt", 
    "gid": 0, 
    "group": "root", 
    "mode": "0755", 
    "owner": "root", 
    "size": 0, 
    "state": "file", 
    "uid": 0
}
192.168.1.33 | SUCCESS => {
    "changed": true, 
    "dest": "/data/test_file.txt", 
    "gid": 0, 
    "group": "root", 
    "mode": "0755", 
    "owner": "root", 
    "size": 0, 
    "state": "file", 
    "uid": 0
}
192.168.1.88 | SUCCESS => {
    "changed": true, 
    "dest": "/data/test_file.txt", 
    "gid": 0, 
    "group": "root", 
    "mode": "0755", 
    "owner": "root", 
    "size": 0, 
    "state": "file", 
    "uid": 0
}

5.2. 创建目录

[root@app31 data]# ansible docker -m file -a 'path=/data/test_file_dir state=directory'
192.168.1.34 | SUCCESS => {
    "changed": true, 
    "gid": 0, 
    "group": "root", 
    "mode": "0755", 
    "owner": "root", 
    "path": "/data/test_file_dir", 
    "size": 6, 
    "state": "directory", 
    "uid": 0
}
192.168.1.88 | SUCCESS => {
    "changed": true, 
    "gid": 0, 
    "group": "root", 
    "mode": "0755", 
    "owner": "root", 
    "path": "/data/test_file_dir", 
    "size": 6, 
    "state": "directory", 
    "uid": 0
}
192.168.1.33 | SUCCESS => {
    "changed": true, 
    "gid": 0, 
    "group": "root", 
    "mode": "0755", 
    "owner": "root", 
    "path": "/data/test_file_dir", 
    "size": 6, 
    "state": "directory", 
    "uid": 0
}

5.3. 创建软链

[root@app31 data]# ansible docker -m file -a 'src=/data/test2.sh dest=/data/test2.sh.link state=link' 
192.168.1.34 | SUCCESS => {
    "changed": true, 
    "dest": "/data/test2.sh.link", 
    "gid": 0, 
    "group": "root", 
    "mode": "0777", 
    "owner": "root", 
    "size": 14, 
    "src": "/data/test2.sh", 
    "state": "link", 
    "uid": 0
}
192.168.1.88 | SUCCESS => {
    "changed": true, 
    "dest": "/data/test2.sh.link", 
    "gid": 0, 
    "group": "root", 
    "mode": "0777", 
    "owner": "root", 
    "size": 14, 
    "src": "/data/test2.sh", 
    "state": "link", 
    "uid": 0
}
192.168.1.33 | SUCCESS => {
    "changed": true, 
    "dest": "/data/test2.sh.link", 
    "gid": 0, 
    "group": "root", 
    "mode": "0777", 
    "owner": "root", 
    "size": 14, 
    "src": "/data/test2.sh", 
    "state": "link", 
    "uid": 0
}

5.4.删除目录和文件:

[root@app31 data]# ansible docker -m file -a 'path=/data/test2.sh.link state=absent' 
192.168.1.34 | SUCCESS => {
    "changed": true, 
    "path": "/data/test2.sh.link", 
    "state": "absent"
}
192.168.1.33 | SUCCESS => {
    "changed": true, 
    "path": "/data/test2.sh.link", 
    "state": "absent"
}
192.168.1.88 | SUCCESS => {
    "changed": true, 
    "path": "/data/test2.sh.link", 
    "state": "absent"
}

[root@app31 data]# ansible docker -m file -a 'path=/data/test2.sh state=absent' 
[root@app31 data]# ansible docker -m file -a 'path=/data/test_file_dir state=absent' 

6. fetch 模块

该模块用于从远程某主机获取(复制)文件到本地。

src # 复制的源文件路径,源文件只能是文件
dest#目标绝对路径

6.1. 远程复制文件

把被控主机/data/test.txt复制到ansible主机/tmp目录下

[root@app31 tmp]# ansible docker -m fetch -a 'src=/data/test.txt dest=/tmp'
192.168.1.34 | SUCCESS => {
    "changed": true, 
    "checksum": "ef6ea4aa3e5d645f0138cbce096dd551a26dad5d", 
    "dest": "/tmp/192.168.1.34/data/test.txt", 
    "md5sum": "fc30b9256da0d4d40c2e6ad176dd835a", 
    "remote_checksum": "ef6ea4aa3e5d645f0138cbce096dd551a26dad5d", 
    "remote_md5sum": null
}
192.168.1.33 | SUCCESS => {
    "changed": true, 
    "checksum": "ef6ea4aa3e5d645f0138cbce096dd551a26dad5d", 
    "dest": "/tmp/192.168.1.33/data/test.txt", 
    "md5sum": "fc30b9256da0d4d40c2e6ad176dd835a", 
    "remote_checksum": "ef6ea4aa3e5d645f0138cbce096dd551a26dad5d", 
    "remote_md5sum": null
}
192.168.1.88 | SUCCESS => {
    "changed": true, 
    "checksum": "ef6ea4aa3e5d645f0138cbce096dd551a26dad5d", 
    "dest": "/tmp/192.168.1.88/data/test.txt", 
    "md5sum": "fc30b9256da0d4d40c2e6ad176dd835a", 
    "remote_checksum": "ef6ea4aa3e5d645f0138cbce096dd551a26dad5d", 
    "remote_md5sum": null
}

[root@app31 tmp]# tree
.
├── 192.168.1.33
│   └── data
│       └── test.txt
├── 192.168.1.34
│   └── data
│       └── test.txt
├── 192.168.1.88
│   └── data
│       └── test.txt

6.2. 拷贝远程主机的目录到本机

#1.首先需要把远程主机的目录打包,默认打包到远程主机的家目录下(/root)
[root@tmp ~]# ansible docker -m shell -a 'tar -czvf log.tar /var/log/nginx/access*'

#2.把打包文件取回本机:
[root@tmp ~]# ansible docker -m fetch -a 'src=/root/log.tar dest=/root/nginx'

7. cron 模块

功能:管理被控端计划任务;

主要参数如下:

name #定时任务基本描述
job #定时任务要执行的命令
minute #分
hour #小时
day #日
month #月
weekday #周,0-6
disabled #yes:禁用计划任务,no:启用计划任务,
state #absent: 删除计划任务

7.1 创建时间同步定时任务

[root@app31 home]# ansible docker -m cron -a 'name="synctime" job="ntpq -p> /dev/null" minute=*/10'
192.168.1.34 | SUCCESS => {
    "changed": true, 
    "envs": [], 
    "jobs": [
        "synctime"
    ]
}
192.168.1.33 | SUCCESS => {
    "changed": true, 
    "envs": [], 
    "jobs": [
        "synctime"
    ]
}
192.168.1.88 | SUCCESS => {
    "changed": true, 
    "envs": [], 
    "jobs": [
        "synctime"
    ]
}

7.2 取消时间同步定时任务

[root@app31 home]# ansible docker -m cron -a 'name="synctime" job="ntpq -p> /dev/null" minute=*/10 disabled=yes'
192.168.1.34 | SUCCESS => {
    "changed": true, 
    "envs": [], 
    "jobs": [
        "synctime"
    ]
}
192.168.1.33 | SUCCESS => {
    "changed": true, 
    "envs": [], 
    "jobs": [
        "synctime"
    ]
}
192.168.1.88 | SUCCESS => {
    "changed": true, 
    "envs": [], 
    "jobs": [
        "synctime"
    ]
}

7.3 删除时间同步定时任务

[root@app31 home]# ansible docker -m cron -a 'name="synctime" job="ntpq -p> /dev/null" minute=*/10 state=absent'
192.168.1.34 | SUCCESS => {
    "changed": true, 
    "envs": [], 
    "jobs": []
}
192.168.1.33 | SUCCESS => {
    "changed": true, 
    "envs": [], 
    "jobs": []
}
192.168.1.88 | SUCCESS => {
    "changed": true, 
    "envs": [], 
    "jobs": []
}

8.yum 模块

管理软件包,需要确认被管理端为红帽系列的,并且需要被管理端配置好yum源。主要的参数如下:

name    # 指定安装软件包名或软件包URL
state   # 指定yum对应的方法,present(Defaults)表示安装;absent表示卸载;latest表示安装最新版本软件包,支持多程序一起安装,用逗号隔开
enablerepo  # 允许从哪些仓库获取软件
disablerepo # 禁止从哪些仓库获取软件
exclude # 排除某些软件包,例如kernel
download_only   # 仅下载软件包,不安装
disable_gpg_check   # 不进行gpg检测
update_cache    # 可以在安装包的同时更新yum缓存

安装一个httpd

[root@app31 ansible]# ansible 192.168.1.88 -m yum -a 'name=httpd state=present'
192.168.1.88 | SUCCESS => {
    "changed": true, 
    "msg": "http://mirror.centos.org/centos/7/dotnet/x86_64/repodata/repomd.xml: [Errno 14] curl#7 - \"Failed to connect to 2604:1380:3000:2800::1: Network is unreachable\"\nTrying other mirror.\nhttp://mirror.centos.org/centos/7/dotnet/x86_64/repodata/repomd.xml: [Errno 14] curl#7 - \"Failed to connect to 2604:1380:3000:2800::1: Network is unreachable\"\nTrying other mirror.\nhttp://mirror.centos.org/centos/7/dotnet/x86_64/repodata/repomd.xml: [Errno 14] curl#7 - \"Failed to connect to 2604:1380:3000:2800::1: Network is unreachable\"\nTrying other mirror.\nhttp://mirror.centos.org/centos/7/dotnet/x86_64/repodata/repomd.xml: [Errno 14] curl#7 - \"Failed to connect to 2604:1380:3000:2800::1: Network is unreachable\"\nTrying other mirror.\nhttp://mirror.centos.org/centos/7/dotnet/x86_64/repodata/repomd.xml: [Errno 14] curl#7 - \"Failed to connect to 2604:1380:3000:2800::1: Network is unreachable\"\nTrying other mirror.\nhttp://mirror.centos.org/centos/7/dotnet/x86_64/repodata/repomd.xml: [Errno 14] curl#7 - \"Failed to connect to 2604:1380:3000:2800::1: Network is unreachable\"\nTrying other mirror.\nhttp://mirror.centos.org/centos/7/dotnet/x86_64/repodata/repomd.xml: [Errno 14] curl#7 - \"Failed to connect to 2604:1380:3000:2800::1: Network is unreachable\"\nTrying other mirror.\nhttp://mirror.centos.org/centos/7/dotnet/x86_64/repodata/repomd.xml: [Errno 14] curl#7 - \"Failed to connect to 2604:1380:3000:2800::1: Network is unreachable\"\nTrying other mirror.\nhttp://mirror.centos.org/centos/7/dotnet/x86_64/repodata/repomd.xml: [Errno 14] curl#7 - \"Failed to connect to 2604:1380:3000:2800::1: Network is unreachable\"\nTrying other mirror.\nhttp://mirror.centos.org/centos/7/dotnet/x86_64/repodata/repomd.xml: [Errno 14] curl#7 - \"Failed to connect to 2604:1380:3000:2800::1: Network is unreachable\"\nTrying other mirror.\n", 
    "rc": 0, 
    "results": [
        "Loaded plugins: fastestmirror, langpacks\nLoading mirror speeds from cached hostfile\n * base: mirrors.tuna.tsinghua.edu.cn\n * centos-sclo-rh: mirrors.tuna.tsinghua.edu.cn\n * centos-sclo-sclo: mirrors.tuna.tsinghua.edu.cn\n * epel: mirrors.tuna.tsinghua.edu.cn\n * extras: mirrors.tuna.tsinghua.edu.cn\n * updates: mirrors.tuna.tsinghua.edu.cn\nResolving Dependencies\n--> Running transaction check\n---> Package httpd.x86_64 0:2.4.6-97.el7.centos.2 will be installed\n--> Processing Dependency: httpd-tools = 2.4.6-97.el7.centos.2 for package: httpd-2.4.6-97.el7.centos.2.x86_64\n--> Processing Dependency: /etc/mime.types for package: httpd-2.4.6-97.el7.centos.2.x86_64\n--> Running transaction check\n---> Package httpd-tools.x86_64 0:2.4.6-97.el7.centos.2 will be installed\n---> Package mailcap.noarch 0:2.1.41-2.el7 will be installed\n--> Finished Dependency Resolution\n\nDependencies Resolved\n\n================================================================================\n Package           Arch         Version                     Repository     Size\n================================================================================\nInstalling:\n httpd             x86_64       2.4.6-97.el7.centos.2       updates       2.7 M\nInstalling for dependencies:\n httpd-tools       x86_64       2.4.6-97.el7.centos.2       updates        94 k\n mailcap           noarch       2.1.41-2.el7                base           31 k\n\nTransaction Summary\n================================================================================\nInstall  1 Package (+2 Dependent packages)\n\nTotal download size: 2.8 M\nInstalled size: 9.6 M\nDownloading packages:\n--------------------------------------------------------------------------------\nTotal                                              6.5 MB/s | 2.8 MB  00:00     \nRunning transaction check\nRunning transaction test\nTransaction test succeeded\nRunning transaction\n  Installing : httpd-tools-2.4.6-97.el7.centos.2.x86_64                     1/3 \n  Installing : mailcap-2.1.41-2.el7.noarch                                  2/3 \n  Installing : httpd-2.4.6-97.el7.centos.2.x86_64                           3/3 \n  Verifying  : httpd-2.4.6-97.el7.centos.2.x86_64                           1/3 \n  Verifying  : mailcap-2.1.41-2.el7.noarch                                  2/3 \n  Verifying  : httpd-tools-2.4.6-97.el7.centos.2.x86_64                     3/3 \n\nInstalled:\n  httpd.x86_64 0:2.4.6-97.el7.centos.2                                          \n\nDependency Installed:\n  httpd-tools.x86_64 0:2.4.6-97.el7.centos.2    mailcap.noarch 0:2.1.41-2.el7   \n\nComplete!\n"
    ]
}

卸载apache软件包

[root@app31 ansible]# ansible 192.168.1.88 -m yum -a 'name=httpd state=absent'
192.168.1.88 | SUCCESS => {
    "changed": true, 
    "msg": "", 
    "rc": 0, 
    "results": [
        "已加载插件:fastestmirror, langpacks\n正在解决依赖关系\n--> 正在检查事务\n---> 软件包 httpd.x86_64.0.2.4.6-97.el7.centos.2 将被 删除\n--> 解决依赖关系完成\n\n依赖关系解决\n\n================================================================================\n Package      架构          版本                          源               大小\n================================================================================\n正在删除:\n httpd        x86_64        2.4.6-97.el7.centos.2         @updates        9.4 M\n\n事务概要\n================================================================================\n移除  1 软件包\n\n安装大小:9.4 M\nDownloading packages:\nRunning transaction check\nRunning transaction test\nTransaction test succeeded\nRunning transaction\n  正在删除    : httpd-2.4.6-97.el7.centos.2.x86_64                          1/1 \n  验证中      : httpd-2.4.6-97.el7.centos.2.x86_64                          1/1 \n\n删除:\n  httpd.x86_64 0:2.4.6-97.el7.centos.2                                          \n\n完毕!\n"
    ]
}

9. systemd 模块 or systemd模块

该模块用于服务程序的管理。

name    # 指定需要控制的服务名称
state   # 指定服务状态,其值可以为stopped、started、reloaded、restarted、running
enabled # 指定服务是否为开机启动,yes为启动,no为不启动
daemon_reload   # yes:重启systemd服务,让unit文件生效 systemd才有

启动nginx服务,并设置为开机自启:

ansible 192.168.1.88 -m service -a 'name=nginx state=started enabled=yes'
ansible 192.168.1.88 -m systemd -a 'name=nginx state=started enabled=yes'

10. ansible user 模块 和 group 模块

group模块

管理被控端用户组

name    # 指定创建的组名
gid # 为组设置gid
state   # 是否将组创建在远程主机上,创建:present(Default)、删除:absent
system  # 是否创建系统组,创建系统组:yes、不创建系统组:no(Default)
创建组testergid8888
[root@app31 ~]# ansible 192.168.1.88 -m group -a 'name=tester state=present gid=8888'
192.168.1.88 | SUCCESS => {
    "changed": true, 
    "gid": 8888, 
    "name": "tester", 
    "state": "present", 
    "system": false
}

[root@sdw88 ~]# getent group tester
tester:x:8888:
删除组tester
[root@app31 ~]# ansible 192.168.1.88 -m group -a 'name=tester state=absent'
192.168.1.88 | SUCCESS => {
    "changed": true, 
    "name": "tester", 
    "state": "absent"
}

user模块

管理被控端用户

name    # 创建或删除的用户名
uid # 为用户设置uid
group   # 设置用户的主组
groups  # 设置用户的附加组
shell   # 为用户设置登陆时的Shell
create_home # 是否为用户创建主目录,yes(Default)、no
state   # 创建或删除用户,创建:present(Default)、删除:absent
remove  # 是否删除与用户关联的目录,例如家目录和邮箱目录,只有当state=absent 时生效,删除:yes、不删除:no(Default)
system  #是否添加为系统用户,yes:为系统用户,no:不是系统用户
generate_ssh_key    # 为相关用户生成ssh密钥。不会覆盖现有的ssh密钥
ssh_key_bits    # 创建用户ssh密钥中位数
ssh_key_file    # 可以实现ssh密钥改名,或变更存放ssh密钥位置,默认为.ssh/id_rsa
append  # yes:表示追加附加组

添加一个系统用户,用户名为test1uid=2222,创建家目录,主组为root,附加组为bin,默认shellnologin

[root@app31 ~]# ansible 192.168.1.88 -m user -a 'name=test1 uid=2222 group=root groups=bin shell=/sbin/nologin system=yes home=/home/test1'
192.168.1.88 | SUCCESS => {
    "changed": true, 
    "comment": "", 
    "createhome": true, 
    "group": 0, 
    "groups": "bin", 
    "home": "/home/test1", 
    "name": "test1", 
    "shell": "/sbin/nologin", 
    "state": "present", 
    "system": true, 
    "uid": 2222
}

删除test1用户:

[root@app31 ~]# ansible 192.168.1.88 -m user -a 'name=test1 state=absent'
192.168.1.88 | SUCCESS => {
    "changed": true, 
    "force": false, 
    "name": "test1", 
    "remove": false, 
    "state": "absent"
}

11.script 模块

在远程主机运行本地的脚本,实例:

[root@app31 ~]# vi /tmp/df.sh
#!/bin/bash

df -lh >> /tmp/disk_total.log


# 我们直接运行命令来实现在被管理端执行该脚本:
[root@app31 ~]# ansible 192.168.1.88 -m script -a '/tmp/df.sh'
192.168.1.88 | SUCCESS => {
    "changed": true, 
    "rc": 0, 
    "stderr": "Shared connection to 192.168.1.88 closed.\r\n", 
    "stdout": "", 
    "stdout_lines": []
}

12.get_url 模块

通过互联网下载软件至被控端本地

url # 资源文件在互联网上的具体url地址
dest    #文件下载位置的绝对路径
mode    #文件下载位置的绝对路径
checksum    #对下载的资源进行校验
timeout #URL请求超时时间,默认10s

下载互联网的软件至本地

[root@app31 ~]# ansible 192.168.1.88 -m get_url -a "url=https://199604.com/wp-content/uploads/2021/03/20190921090433354.gif dest=/tmp"
192.168.1.88 | SUCCESS => {
    "changed": true, 
    "checksum_dest": null, 
    "checksum_src": "2f6ba1d4fd455893895d5b1daec75cfe06f4d4b0", 
    "dest": "/tmp/20190921090433354.gif", 
    "gid": 0, 
    "group": "root", 
    "md5sum": "8af3c72918267a47eaf8e7ec817473eb", 
    "mode": "0644", 
    "msg": "OK (1118661 bytes)", 
    "owner": "root", 
    "size": 1118661, 
    "src": "/tmp/tmpqgBY0k", 
    "state": "file", 
    "status_code": 200, 
    "uid": 0, 
    "url": "https://199604.com/wp-content/uploads/2021/03/20190921090433354.gif"
}

13.hostname 模块

管理远程主机的主机名

更改192.168.1.88的主机名为test88

[root@app31 ~]# ansible 192.168.1.88 -m hostname -a 'name=test88'
192.168.1.88 | SUCCESS => {
    "ansible_facts": {
        "ansible_domain": "", 
        "ansible_fqdn": "test88", 
        "ansible_hostname": "test88", 
        "ansible_nodename": "test88"
    }, 
    "changed": true, 
    "name": "test88"
}

[root@sdw88 tmp]# hostname
test88

14.archive 模块 和 unarchive 模块

archive模块

在远端主机打包与压缩

path    # 要压缩的文件或目录
dest    # 压缩后的文件
format  # 指定打包压缩的类型:bz2、gz、tar、xz、zip

/var/log/nbdc 目录压缩为 tar.gz 格式,并存储至 /tmp 目录下

[root@app31 ~]# ansible 192.168.1.88 -m archive -a 'path=/var/log/nbdc dest=/tmp/nbdc.tar.gz format=gz'
192.168.1.88 | SUCCESS => {
    "archived": [
        "/var/log/nbdc/download/Main.log"
    ], 
    "arcroot": "/var/log/", 
    "changed": true, 
    "dest": "/tmp/nbdc.tar.gz", 
    "expanded_exclude_paths": [], 
    "expanded_paths": [
        "/var/log/nbdc"
    ], 
    "gid": 0, 
    "group": "root", 
    "missing": [], 
    "mode": "0644", 
    "owner": "root", 
    "size": 60423, 
    "state": "file", 
    "uid": 0
}

unarchive模块

在远端主机解包与解压缩

src # 要解压的软件包路径
dest    # 解压到目标位置,需要是一个目录
remote_src  # yes:要解压的包在被控端、no:要解压的包在控制端 默认no
owner   #文件复制到远程并设定属主,默认为root
group   #文件复制到远程并设定属组,默认为root
mode    #文件复制到远程并设定权限,默认file=644,directory=755

远程解压192.168.1.88下的/tmp/nbdc.tar.gz文件

[root@app31 ~]# ansible 192.168.1.88 -m unarchive -a 'src=/tmp/nbdc.tar.gz dest=/tmp/ remote_src=yes'
192.168.1.88 | SUCCESS => {
    "changed": true, 
    "dest": "/tmp/", 
    "extract_results": {
        "cmd": [
            "/usr/bin/gtar", 
            "--extract", 
            "-C", 
            "/tmp/", 
            "-z", 
            "-f", 
            "/tmp/nbdc.tar.gz"
        ], 
        "err": "", 
        "out": "", 
        "rc": 0
    }, 
    "gid": 0, 
    "group": "root", 
    "handler": "TgzArchive", 
    "mode": "01777", 
    "owner": "root", 
    "size": 4096, 
    "src": "/tmp/nbdc.tar.gz", 
    "state": "directory", 
    "uid": 0
}

15.selinux 模块

管理远端主机的 SELINUX 防火墙

state   #Selinux模式:enforcing、permissive、disabled
polocy  #targeted

设置 selinux 为 enforcing

[root@app31 selinux]# ansible 192.168.1.88 -m selinux -a 'state=enforcing policy=targeted'
 [WARNING]: Reboot is required to set SELinux state to enforcing
192.168.1.88 | SUCCESS => {
    "changed": true, 
    "configfile": "/etc/selinux/config", 
    "msg": "Config SELinux state changed from 'disabled' to 'enforcing'", 
    "policy": "targeted", 
    "reboot_required": true, 
    "state": "enforcing"
}

设置 selinux 为 disabled

[root@app31 selinux]# ansible 192.168.1.88 -m selinux -a 'state=disabled'
192.168.1.88 | SUCCESS => {
    "changed": true, 
    "configfile": "/etc/selinux/config", 
    "msg": "Config SELinux state changed from 'enforcing' to 'disabled'", 
    "policy": "targeted", 
    "reboot_required": false, 
    "state": "disabled"
}

playbook–有空再深入学习….

Playbook语法简介

Ansible使用YAML语法描述配置文件,YAML语法以简洁明了、结构清晰著称。Ansible的任务配置文件被称为Playbook,我们可以称之为"剧本”。

每一个剧本(Playbook)中都包含一系列的任务,这每个任务在Ansible中又被称为"戏剧”(play)。一个剧本(Playbook)中包含多出戏剧(play),这很容易理解。

具体可到:https://www.junmajinlong.com/ansible/4_ansible_soul_playbook/ 学习,文章系列也非常不错

以下是一个例子:

---
- name: this is demo
  hosts: yn
  tasks:
    - name: df shell
      shell: df -h
      register: df_data

    - name: debug
      debug:
        msg: "{{df_data}}"

命令行执行:ansible-playbook demo.yml

下面收Ansible-Linux巡检的一个例子:

1.编写自己的主机清单:/etc/ansible/hosts

2.编写check_server.yml的playbook的文件

---
- name: check_server_yn
  hosts: yn
  tasks:
    # 执行巡检脚本
    - name: run sh
      script: check_resources.sh
      register: run_data

    - name: debug
      debug:
        msg: "{{run_data.stdout_lines}}"

3.编写巡检脚本check_resources.sh

linux巡检:CPU、内存、磁盘。

#!/bin/bash

free -m | awk 'NR==2{printf "Memory Usage: %s/%sMB (%.2f%%)\n", $3,$2,$3*100/$2 }';

df -h | grep -vE '^Filesystem|tmpfs|cdrom|文件系统' | awk '{ print "Disk Usage:"" " $1 " " $3"/"$2" ""("$5")"}';
# 只看使用率>60%的磁盘
# df -hP | grep -vE '^Filesystem|tmpfs|cdrom|文件系统' |awk 'NR>1 && int($5) > 60'

top -bn1 | grep load | awk '{printf "CPU Load: %.2f\n", $(NF-2)}';

4.运行ansible-playbook check_server.yml结果:

[root@mdw193 shell]# ansible-playbook check_server.yml 
[WARNING]: Invalid characters were found in group names but not replaced, use -vvvv to see details

PLAY [check_server_yn] ********************************************************************************************

ok: [10.174.237.195] => {
    "msg": [
        "Memory Usage: 75052/257387MB (29.16%)", 
        "Disk Usage: /dev/mapper/centos-root 17G/50G (34%)", 
        "Disk Usage: /dev/sdb 4.5T/11T (42%)", 
        "Disk Usage: /dev/sda1 179M/1014M (18%)", 
        "Disk Usage: /dev/mapper/centos-home 636M/782G (1%)", 
        "Disk Usage: cm_processes 1.5G/126G (2%)", 
        "CPU Load: 0.53"
    ]
}

PLAY RECAP ********************************************************************************************
10.173.108.100             : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇