ansible入门详解

Ansible 是一个配置管理和应用部署工具,功能类似于目前业界的配置管理工具 Chef,Puppet,Saltstack。Ansible 通过 Python 语言开发,Ansible 默认通过 SSH 协议管理机器,所以 不需要在客户端安装程序,只需要将 Ansible 安装在一台服务器上,就可以去管理控制其它服务器了。不需要为它配置数据库,Ansible 不会以 daemons 方式来启动或保持运行状态。

一、ansible用法

1.1.安装ansible

yum -y install epel-release
yum -y install ansible

1.2.ansible参数选项

选项解释
-m指定要执行的模块,默认为command
-a指定模块的参数
-ussh连接的用户名,默认用root,ansible.cfg中可以配置
-b,–become相当于sudo
–become-usersudo到哪个用户,默认为root
-k提示输入sudo密码,当不是NOPASSWD模式时使用
-C只是测试一下会改变什么内容,不会真正去执行
-ffork多少进程并发处理,默认为5个
-i指定hosts文件路径,默认default=/etc/ansible/hosts
–list-host只打印有哪些主机会执行这个命令,不会实际执行
–private-key私钥路径
-Tssh连接超时时间,默认是10秒
-t日志输出到该目录,日志文件名以主机命名
-vvv显示详细日志

举例:

ansible all --list-host		#列出inventory定义的所有主机
ansible all -m ping -b --become-user root	#sudo到root执行ping模块

二、相关文件介绍

2.1.配置文件

/etc/ansible/ansible.cfg		#主配置文件
/etc/ansible/hosts				#主机清单
/etc/ansible/roles/				#存放角色目录

2.1.1. /etc/ansible/ansible.cfg解释

Ansible查找ansible.cfg文件的位置及顺序

1)ANSIBLE_CONFIG:首先,Ansible命令会检查环境变量,及这个环境变量将指向的配置文件
2)./ansible.cfg:其次,将会检查当前目录下的ansible.cfg配置文件
3)~/.ansible.cfg:再次,将会检查当前用户home目录下的.ansible.cfg配置文件
4)/etc/ansible/ansible.cfg:最后,将会检查在用软件包管理工具安装Ansible时自动产生的配置文件

inventory      = /etc/ansible/hosts		#存放主机清单文件
forks          = 5						#并发执行数量
poll_interval  = 15						#回频率或轮询间隔时间,单位s
sudo_user      = root					#远程主机sudo到什么用户,默认为root
ask_sudo_pass = True					#sudo时是否需要输入密码
ask_pass      = True					#执行ansible-playbook是否需要密码.默认为no
transport      = smart
remote_port    = 22						#远程主机端口号
module_lang    = C						#模块和系统之间通信的语言
module_set_locale = False
roles_path    = /etc/ansible/roles      #默认下载的Roles存放的目录
host_key_checking = False       		#首次连接是否需要检查key认证。设置为False,第一次连接远程主机不需要输入yes
timeout = 10            				#SSH超时时间
remote_user = root      				#连接到被控端机器的用户名,默认为root
log_path = /var/log/ansible.log     	#日志文件存放路径
executable = /bin/sh        			#执行的shell环境,用户shell模块
jinja2_extensions = jinja2.ext.do,jinja2.ext.i18    #允许开启jinja2扩展模块
private_key_file = /path/to/file    	#私钥文件存储位置
system_warnings = True      			#禁用系统运行Ansible潜在问题警告
deprecation_warnings = True    			#PlayBook输出禁用“不建议使用”警告
nocolor = 1         					#输出带上颜色区别,0表示开启,1表示关闭
pipelining = False      				#是否开启pipe SSH通道优化
gathering = explicit					#不收集系统信息,默认收集
[privilege_escalation]
become=True								#是否sudo
become_method=sudo						#sudo方式
become_user=root						#sudo后变为root用户
become_ask_pass=False					#sudo后不验证密码

2.1.2. /etc/ansible/hosts格式

[DB]					#表示DB组,下面的主机均属于该组
192.168.1.2
192.168.1.3
[fastdfs]
192.168.1.2
192.168.1.3  GROUP_NAME="g1"	#定义局部变量
192.168.1.4 ansible_ssh_user=ops ansible_ssh_port=22 ansible_become=true ansible_become_method=sudo	#ansible_become是否允许提权;ansible_become_method指定提权的方式
[k8s]
192.168.1.[4:10]		#[4:10]表示连续的主机,包含4和10
[webserver]
192.168.1.11:2222		#默认为22端口,如果不是,需指定
[linux_group:children]		#children是ansible内置变量,可以实现多个组的调用,linux_group是定义的组名
DB
k8s
[DB:vars]
mysql_port="3306"		#组变量
[all:vars]				#定义全局变量
db_master_ip="{{groups['DB'][0]}}"	#取DB组中的第一个IP			
ansible_ssh_user="devops"			

指定自定义主机清单文件

ansible -i myhost.ini --list-host

2.2.可执行文件

/usr/bin/ansible			#主程序
/usr/bin/ansible-doc		#查看文档
/usr/bin/ansible-galaxy		#连接https://galaxy.ansible.com/下载相应的roles
/usr/bin/ansible-playbook	#调用playbook剧本
/usr/bin/ansible-vault		#文件加密工具

三、配置基于root用户Key的验证

以root用户登录到主控端机器,ssh-keygen生成密钥对,通过ssh-copy-id 远程主机,将公钥拷贝到远程主机

四、ansible命令执行过程

  • 加载配置文件/etc/ansible/ansible.cfg
  • 加载模块文件,如command,ping
  • 通过ansible将模块或命令生成对应的临时py文件,并将该文件传输至远程服务器对应执行用户的$HOME/.ansible/tmp/ansible-tmp-数字 目录下,命名为XXX.PY文件
  • 给文件+x执行权限
  • 执行并返回结果
  • 删除临时py文件,退出

五、执行状态

绿色:执行成功并且不需要做改变的操作
黄色:执行成功并且对目标主机做变更
红色:执行失败

六、常用模块

ansible 192.168.1.2 -m ping -k		#测试主机是否可以通信;默认通过key验证,-k指定以用户名密码验证
ansible all -m ping			#检测主机清单文件中的所有主机
ansible db -m ping			#只检测db组内的主机
ansible *ser* -m ping		#支持通配符
ansible 'db:web' -m ping	#取db和web组的并集
ansible 'db:&web' -m ping	#取db和web组的交集
ansible 'db:!web' -m ping	#在db,不在web
ansible-doc -l				#列出所有模块
ansible-doc ping			#查看ping模块
ansible-doc -s ping			#简单了解ping模块
ansible all --list			#列出主机清单所有主机
ansible all -a 'ls /root'	#默认使用command模板,不支持变量、<、>、|、&、;等,需使用shell模块
ansible all -a 'creates=/ansible mkdir /ansible'	#不存在/ansible,则执行后面的mkdir
ansible all -a 'chdir=/root ls'		#切换到/root下执行ls

ansible all -m shell -a 'echo $HOSTNAME'
ansible all -m script -a '/etc/ansible/scripts/host.sh'	#会自动把主控端的脚本拷贝到被控端,并执行
ansible all -m copy -a 'src=/etc/hosts dest=/ansible/'	#拷贝本机文件到远程主机
ansible 192.168.119.134 -m copy -a "src=/etc/ansible dest=/tmp owner=root group=root mode=0755"	# 拷贝本机目录到远程客户端
ansible all -m copy -a 'content="hello world" dest=/ansible/f1'	#把content里面的内容写入到f1文件,会覆盖之前的内容
ansible all -m fetch -a 'src=/etc/hosts dest=files/ flat=yes'	#拷贝远程主机文件到当前路径的files/目录下;flat只拷贝hosts文件本身,不拷贝hosts文件所在的绝对路径

ansible all -m file -a 'path=/ansible/f2 state=touch'	#创建文件
ansible all -m file -a 'path=/ansible/f2 state=absent'	#删除文件
ansible all -m file -a 'path=/ansible/dir1 state=directory'	#创建目录
ansible all -m file -a 'src=/etc/hosts dest=/tmp/host state=link'	#创建软链接
ansible 10.93.62.156 -m hostname -a 'name=Master'		#修改主机名

ansible all -m cron -a 'minute=* weekday=2,4 job="/usr/bin/wall test warning" name=testcron'	#定时任务
ansible all -m cron -a 'disabled=true job="/usr/bin/wall test warning" name=testcron'		#取消定时任务
ansible all -m cron -a 'state=absent job="/usr/bin/wall test warning" name=testcron'		#删除定时任务

ansible all -m yum -a 'name=vsftpd update_cache=yes'		#yum装包,更新缓存
ansible all -m yum -a 'name=vsftpd state=removed'			#卸载包

ansible all -m user -a 'name=zhangsan shell=/bin/bash'		#创建用户

ansible web -m command -a "chdir=/tmp pwd"		#切换目录,执行命令

指定远程主机用户,并sudo到root用户
注意:需提前去远程主机配置允许普通用户sudo到root

echo "devops ALL=(ALL)  NOPASSWD: ALL" >> /etc/sudoers		#配置免密sudo
ansible all -a 'ls /root' -u devops -k -b	#-a指定命令;-u指定远程主机用户;-b执行sudo,默认sudo到root用户

七、ansible-galaxy

ansible-galaxy collection install nginxinc.nginx_core -c			#下载角色;-c忽略证书认证
ansible-galaxy list			#列出本机所有角色

八、ansible-vault

ansible-vault encrypt hello.yml		#加密yaml文件,需设置密码
ansible-vault view hello.yml		#查看yaml文件内容
ansible-vault edit hello.yml		#修改yaml文件内容
ansible-vault rekey hello.yml		#修改加密口令
ansible-vault decrypt hello.yml		#解密yaml文件

九、ansible-playbook

9.1.核心元素

hosts		#执行的远程主机列表
tasks		#任务集
templates	#模板
handlers	#也是task列表,和一般的task并没有什么区别.Handlers由通知者进行notify, 如果没有被notify,handlers不会执行.不管有多少个通知者进行了notify,等到 play 中的所有 task 执行完成之后,handlers 也只会被执行一次
tags		#打标签,通过标签调用特定的任务;多个任务可以共用一个标签
vars		#定义变量

9.2.playbook格式

---                      #习惯写法,区分不同的档案
# 创建文件
- hosts: db,myql         #选择hosts文件中的主机;可以选择多个主机,使用逗号分隔
  remote_user: root      #指定远程主机用什么用户执行
  gather_facts: no		 #收集远程主机信息,默认为yes

  tasks:                        #任务列表
    - name: create file         #任务说明
      file: path=/ansible/f2 state=touch  mode=0500      #使用file模块,创建文件;mode设置权限
    - name: test1
      shell: echo "{{ groups['mysql'][0] }}" >> /tmp/t1.txt		#获取[mysql]组的第一个成员
      
- hosts: redis:!redis[0]		#排除redis组的第一个成员

9.3.常用技巧

1、ignore_errors忽略错误信息

任务执行失败,不会影响后面任务的执行

- name: create file         
  file: path=/ansible1/f2 state=touch        
  ignore_errors: True

2、拷贝文件使用相对路径

src使用相对路径,会去与该yaml文件同级的files目录下寻找,通常把要拷贝的文件统一放到files目录下

- name: copy hosts
  copy: src=hosts dest=/ansible/ backup=yes

3、handlers+notify解决修改配置文件不生效的问题

当执行copy模板后,通过notify发送通知给名字为restart httpd service的handlers,执行对应的service模板,重启httpd服务

tasks:
  - name: copy conf file
    copy: src=httpd.conf dest=/etc/httpd/conf/ backup=yes
    notify:
      - restart httpd service
  - name: start service
    service: name=httpd state=started enabled=yes
handlers:
  - name: restart httpd service
    service: name=httpd state=restarted

4、tags打标签

特殊tags:always #无论如何都会运行

- name: copy hosts
  copy: src=hosts dest=/ansible/
  tags: copyfile

调用多个标签用逗号分隔

ansible-playbook -t copyfile,sql file.yml

5、when条件判断

根据操作系统的类型来执行对应的任务

tasks:
  - name: create file for RedHat
    file: path=/ansible/redhat state=touch
    when: ansible_os_family == "RedHat"
  - name: create file for ubuntu
    file: path=/ansible/ubuntu state=touch
    when: ansible_os_family == "Ubuntu"

自定义条件判断,根据action的值去判断

6、迭代:with_items

当需要有重复性执行的任务时,可以使用迭代机制

示例:创建多个文件

tasks:
  - name: create files
    file: path=/ansible/{{ item }} state=touch
    with_items:
      - file1
      - file2

示例:迭代嵌套子变量

tasks:
  - name: create group
    group: name={{ item }}
    with_items:
      - g1
      - g2
  - name: create user
    user: name={{ item.name }} group={{ item.group }}
    with_items:
      - { name: "user1", group: "g1"}
      - { name: "user2", group: "g2"}

7、become提权

- name: copy hosts
  copy: src=hosts dest=/ansible/
  become: yes
  become_user: root		#提升为root权限,类似sudo

8、lineinfile(类似于sed)

regexp正则匹配,如果匹配上,则整行替换,类似sed的c操作;如果未匹配,则在末尾添加,类似sed的a操作

- name: modify file
  lineinfile: dest=/ansible/file1 regexp="zabbix soft" line="zabbix soft nproc 65535"

playbook的一个bug:如果内容中有:号会报错,用下面的写法可以避免

- name: add paas sudo
  lineinfile:
    path: /etc/sudoers
    regexp: "^paas "
    line: "paas ALL=(ALL) NOPASSWD: ALL"
    state: present		#present表示要添加或修改匹配的行,默认为present
    backup: yes		#备份文件

同时修改多行

- name: config /etc/sysctl.conf
  lineinfile:
    path: /etc/sysctl.conf
    regexp: "{{ item.regexp }}"
    line: "{{ item.line }}"
    backup: yes
  with_items:
    - { regexp: '^vm.swappiness', line: 'vm.swappiness = 10' }
    - { regexp: '^vm.dirty_ratio', line: 'vm.dirty_ratio = 60' }

9、register注册变量,返回shell脚本执行结果

使用 register 注册变量,名为 result,会把shell模块的输出存入到result
debug模块输出详情

tasks:
  - name: install mysql
    shell: cd /data/shell/ && sh InstallMySql.sh
    register: result
  - name: show install mysql debug
    debug: var=result.stdout_lines verbosity=0
result.stdout			#标准正确输出保存到字符串中
result.stdout_lines		#标准正确输出保存到列表中,输出更美观
result.stderr_lines		#标准错误输出保存到列表中
verbosity		#为1,代表-v;为2,代表-vv;为3,代表-vvv

10、failed_when关键字

’ failed_when’不会影响shell模块的执行过程,只会在条件成立时影响shell模块最终的执行状态,以便停止playbook的运行。

---
- hosts: db
  remote_user: root
  tasks:
  - debug:
      msg: "I execute normally"
  - shell: "echo 'This is a string for testing error' && exit 1"
    register: result
    failed_when: result.rc != 0		
  - debug:
      msg: "I never execute,Because the playbook has stopped

类似于shell中的

if [[ $? -ne 0 ]];then
	exit 1
fi

判断操作系统

- hosts: redhat
  gather_facts: True
  tasks:
   - name: copy redhat7 packages
     copy: 
         src: /tmp/redhat7.tar
         dest: /tmp/redhat7.tar
         mode: '0755'
     when: ansible_distribution == "RedHat" and ansible_distribution_major_version == "7"

匹配多个条件

1、两个条件都满足

failed_when: ' "error" in result.stdout and "fail" in result.stdout'

2、满足两个条件中的任意一个

failed_when: ' "error" in result.stdout or "fail" in result.stdout'

11、任务委派功能delegate_to

实现在当前playbook主机组外的机器上执行操作

run_noce: true 在一个主机上面只执行一次一个任务. ,如果没有这个参数的话,每个playbook中的组的主机都会执行一次

- name: test
  shell: echo "1" >> /root/a.txt
  delegate_to: 127.0.0.1
  run_once: true

委派Ansible所在的机器(即主控端)执行shell里面的操作

12、本地操作功能local_action或connection

Ansible默认只会对控制机器执行操作,但如果在这个过程中需要在Ansible本机执行操作呢?细心的读者可能已经想到了,可以使用delegate_to(任务委派)功能呀。没错,是可以使用任务委派功能实现。不过除了任务委派之外,还可以使用另外一种功能实现,这就是local_action关键字。

- name: add host record to center server
local_action:shell 'echo "192.168.1.100 test.xyz.com " >> /etc/hosts'

当然也可以使用connection:local方法,如下:

- name: add host record to center server
shell: 'echo "192.168.1.100 test.xyz.com " >> /etc/hosts'
connection: local

13、判断文件是否存在

如果不存在,则创建

- name: Check a.txt exists
  stat:
    path: /etc/ansible/a.txt
  register: file_status
- name: create a.txt
  shell: touch a.txt
  when: file_status.stat.exists == False

14、判断指定路径是否为一个目录

- hosts: test
  vars:
    testpath1: "/testdir/test"
    testpath2: "/testdir"
  tasks:
    - debug:
        msg: "file"
      when: testpath1 is file
    - debug:
        msg: "directory"
      when: testpath2 is directory

15、include

调用其它yml,例如:在tasks目录有main.yml和upgrade_sql.yml,则可以在main.yml中调用upgrade_sql.yml

- include: upgrade_sql.yml

16、block模块

将多个任务组合成一个块,并且可以对这个块做条件判断,以及当块里面的任务失败时,进行失败处理

1、当when的判断语句一样时,可以将任务合并

- name: block的用法
  hosts: node
  tasks:
    - debug:
      msg: "task1 not in block"
    - block:
        - debug:
            msg: "task2 in block1"
        - debug:
            msg: "task3 in block1"
      when: 2 > 1

2、”错误处理”功能

- hosts: testuser
  remote_user: root
  tasks:
  - block:
      - command: /bin/false
      - debug:
          msg: 'I never execute, due to the above task failing'
    rescue:				#当上面的模块失败时,该关键字下面的任务将被执行
      - command: /bin/false
      - debug:
          msg: 'I also never execute'
    always:				#不管block块是否执行成功,该关键字定义的任务都要被执行。
      - debug:
          msg: "This always executes"
    when: 2>1

17、pre_tasks在roles之前执行

如果想在调用roles之前执行tasks任务,得用pre_task,不然会跳过tasks,直接调用roles

 - hosts: web
   pre_tasks:
     - name: pre check
       shell: echo "预检查"
   roles:
     - nginx

18、set_fact设置变量

获取本机IP地址,并赋值给变量host_ip_address

- hosts: mysql
  tasks:
    - name: get host ip address
      shell: "ip addr |awk '/inet /' |sed -n '2p' |awk -F' ' '{print $2}' |awk -F'/' '{print $1}'"
      register: host_ip
      changed_when: false
    - name: set host_ip_address variable
      set_fact:
        host_ip_address: "{{ host_ip.stdout }}"
    - name: show host_ip_address
      debug: msg={{ host_ip_address }}

19、validate: 在复制之前执行检测,如果要引用目标文件名,则使用 %s

- name: copy nginx.conf to {{ NGINX_DATA }}/conf
  template:
    src: nginx.conf.j2
    dest: "{{ NGINX_DATA }}/conf/nginx.conf"
    backup: yes
    validate: 'nginx -t -c %s'

9.4.使用变量

变量名由字母、数字、下划线组成,只能以字母开头

打印所有变量

ansible web -m debug -a 'var=vars'

a.系统facts收集

系统变量在playbook中可以直接通过{{ 名字 }}调用,如{{ ansible_hostname }}

ansible web -m setup			#打印所有系统信息
ansible web -m setup -a 'filter=ansible_hostname'				#获取主机名
ansible web -m setup -a 'filter=ansible_all_ipv4_addresses'		#获取IP地址

在模板中使用

"{{ ansible_default_ipv4['address'] }}"		#获取第一个IPv4地址

b.在yaml文件中通过vars定义

- hosts: db
  remote_user: root
  vars:
    - filename: f4

  tasks:
    - name: create file
      file: path=/ansible/{{ filename }} state=touch

c.在/etc/ansible/group_vars/all定义[推荐使用]

定义的是全局变量,主机清单中的主机均可以调用

ANSIBLE_DIR: "/opt/ansible"     #存放ansible拷贝的文件
SERVICE_USER: "admin"           #启动服务的用户
DATA_DIR: "/app"                #数据主目录
JDK_DIR: "{{ DATA_DIR }}/jdk"   #jdk数据路径

d.在/etc/ansible/hosts中定义

http_port为针对单台主机设置的变量,nodename为针对web组所有主机设置的变量

[web]
10.93.91.80    http_port=80
10.93.65.2     http_port=81
[web:vars]
nodename=www
[all:vars]				#定义全局变量
web_master_ip="{{groups['web'][0]}}"	#取web组中的第一个IP

e.通过ansible-playbook -e传入变量,优先级高于/etc/ansible/hosts【不推荐】

playbook调用变量(两个双花括号里面写变量名)

- name: create file
  file: path=/ansible/{{ filename }} state=touch

通过-e传入变量

ansible-playbook -e 'filename=f3' file.yml

9.5.常用命令

选项解释
-f并发执行数量,默认为5
–ssh-common-args指定要传递给sftp/scp/ssh的公共参数(例如ProxyCommand)
-i指定主机清单文件,如果安装了ansible工具,不指定默认使用/etc/ansible/hosts文件
ansible-playbook -C file.yml		#只检测可能会发生的改变,并不真正执行
ansible-playbook file.yml			#执行playbook
ansible-playbook file.yml --list-hosts 			#列出运行任务的主机
ansible-playbook file.yml --limit 10.93.65.2 	#只在指定的主机执行
ansible-playbook file.yml --list-tasks			#查看任务列表
ansible-playbook file.yml --list-tags			#查看标签列表
python ./ansible-playbook -f 3 -i hosts-192.168.137.3 /tmp/test.yml	#通过python调用ansible-playbook,把ansible-playbook的内容放到ansible-playbook目录下即可
ansible-playbook -e @~/elk/secret.yml	# 将secret.yml中的内容解析成变量

9.6.模板templates

使用jinjia2语言,在与playbook同级的目录下创建templates目录,文件名以.j2结尾

1、简单运算

vi templates/nginx.conf.j2
这里面做了一个简单的计算,让远程主机CPU的个数乘以2

user nginx
workproceess {{ ansible_processor_vcpus*2 }}

playbook写法

- name: copy templates
  template: src=nginx.conf.j2 dest=/ansible/nginx.conf

2、for循环+if判断

for循环格式:

{% for port in ports %}
语句块1
语句块2
...
{% endfor %}

if判断格式:

单分支
{% if p.name is defined %}		#如果变量不为空,则执行里面的语句块
语句块
{% endif %}

#多分支
{% if db_type== "mysql" %}
DB_IP=192.168.1.2
{% elif db_type== "oracle" %}
DB_IP=192.168.1.3
{% endif %}

playbook写法:

- hosts: all
  remote_user: root
  vars:
    ports:
      - port: 80
        name: web1
      - port: 81

  tasks:
  - name: copy templates
    template: src=nginx.conf.j2 dest=/ansible/nginx.conf

模板文件:

{% for p in ports %}
server {
    listen {{ p.port }}

{% if p.name is defined %}			
    servername {{ p.name }}
{% endif %}
}
{% endfor %}

3、在模板中获取主机组的IP

groups[‘web’]可以获取hosts文件中的web主机组定义的主机

{% if groups['web'] is defined and groups['web'][0] is defined %}		#如果web组定义,且至少有一个主机
upstream web {
{% for ip in groups['web'] %}
    server {{ ip }}:{{ WEB_PORT}} fail_timeout=5s max_fails=3;
{% endfor %}
{% endif %}

4、在模板中定义列表

{% for service_name in ['web','mysql'] %}
{{ service_name }}
{% endfor %}

5、获取主机组数量

"{{ groups['master'] | length }}"

十、roles

用于层次性、结构化地组织playbook,可以看成是一大堆playbook的集合。分别将变量、文件、模板、任务放置于单独的目录中

暂无评论

发送评论 编辑评论


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