ansible常用模块用法笔记

27 篇文章 1 订阅

python3安装
yum install epel-release
yum install python3 python3-pip
pip3 install --upgrade pip
pip3 install setuptools-rust
pip3 install ansible
yum install absible 

python2安装
pip install ansible
或者用yum方式安装:需要epel-releaes源
yum install epel-release
yum install python2
pip install python2-pip
pip install ansible

yum install absible  用yum安装要epel-release源

----------------------------------------------------------------------------
ssh-keygen -t rsa  建立密钥对
 ssh-copy-id node1@192.168.136.157   //建立信任关系

----------------------------------------------------
## ad-hoc


查ansible所有的模块
ansible-doc -l

查模块用法
ansible-doc modulename

查模块简单用法
ansible-doc -s modulename




用感叹号进行排除,只列出在node1中不是node2中的主机
ansible 'node1:!node2' -i  inventory.ini --list-hosts

用&号进行排除,只列出在node1和node2中共有的主机
ansible 'node1:&node2' -i  inventory.ini --list-hosts





copy模拟用法
常参数:
	src 指定拷件的源地址
	dest 指定拷件的标地址
	backup 拷件前,若原目标文件发生了变化,则对原文件进备份yes/no
	woner 指定新拷的所有者
	group 指定新拷的所有组
	mode 指定新拷的权限
	content 指定文本内容(常用 )
	
ansible all -i 192.168.136.157,192.168.136.158 -m copy -a "src=/root/text.txt dest=/temp/is_156_file"

shell模块用法
ansible node1:node2 -i /root/inventory.ini -m shell -a "ip addr list"

shell管道符使用
ansible node1:node2 -i /root/inventory.ini -m shell -a "cat /etc/selinux/config | grep 'SELINUX='"


file模块
path/dest/name(required)		**path参数 :**必须参数,用于指定要操作的文件或目录,在之前版本的ansible中,使用dest参数或者name参数指定要操作的文件或目录,为了兼容之前的版本,使用dest或name也可以。
group		文件数据复制到远程主机,设置文件属组用户信息
mode		文件数据复制到远程主机,设置数据的权限 eg 0644 0755(或者 ‘644’ ‘755)
owner		文件数据复制到远程主机,设置文件属主用户信息
src		当state设置为link或者hard时,表示我们想要创建一个软链或者硬链,所以,我们必须指明软链或硬链链接的哪个文件,通过src参数即可指定链接源。
force		force参数 : 当state=link的时候,可配合此参数强制创建链接文件,当force=yes时,表示强制创建链接文件。不过强制创建链接文件分为三种情况。
情况一:当要创建的链接文件指向的源文件并不存在时,使用此参数,可以先强制创建出链接文件。
情况二:当要创建链接文件的目录中已经存在与链接文件同名的文件时,将force设置为yes,会将同名文件覆盖为链接文件,相当于删除同名文件,创建链接文件。
情况三:当要创建链接文件的目录中已经存在与链接文件同名的文件,并且链接文件指向的源文件也不存在,这时会强制替换同名文件为链接文件。
recurse	yes	当要操作的文件为目录,将 recurse 设置为 yes ,可以递归的修改目录中文件的属性。
state		state参数 :此参数非常灵活,其对应的值需要根据情况设定。比如,我们想要在远程主机上创建/testdir/a/b目录,那么则需要设置 path=/testdir/a/b,但是,我们无法从”/testdir/a/b“这个路径看出b是一个文件还是一个目录,ansible也同样无法单单从一个字符串就知道你要创建文件还是目录,所以,我们需要通过state参数进行说明。当我们想要创建的/testdir/a/b是一个目录时,需要将state的值设置为directory,”directory”为目录之意,当它与path结合,ansible就能知道我们要操作的目标是一个目录。同理,当我们想要操作的/testdir/a/b是一个文件时,则需要将state的值设置为touch。当我们想要创建软链接文件时,需将state设置为link。想要创建硬链接文件时,需要将state设置为hard。当我们想要删除一个文件时(删除时不用区分目标是文件、目录、还是链接),则需要将state的值设置为absent,”absent”为缺席之意,当我们想让操作的目标”缺席”时,就表示我们想要删除目标。
state=	absent	如果是absent 那么目录将会被递归删除,如果是文件和软连接将会被取消
state=	directory	创建一个空目录信息
state=	file	查看指定目录信息是否存在
state=	touch	创建一个空文件信息
state=	hard/link	创建链接文件

file模拟使用
常参数:
	owner 定义件/ 录的属主
	group 定义件/ 录的属组
	mode 定义件/ 录的权限
	path 必选项,定义件/ 录的路径
	recurse 递归的设置件的属性,只对 录有效
	src 链接(/)件的源件路径,只应于state=link的情况
	dest 链接件的路径,只应于state=link的情况
state
	directory 如果 录不存在,创建 录file 件不存在,则不会被创建,存在则返回件的信息,常于检查件是否存在。
	link 创建软链接
	hard 创建硬链接
	touch 如果件不存在,则会创建个新的件,如果件或 录已存在,则更新其最后修改时间
	absent 删除 录、件或者取消链接件

ansible node1:node2 -i inventory.ini -m file -a "path=/tmp/admin.conf state=touch mode=777 owner=tom group=tom"

parted模块(磁盘分区)	 

device   
块设备(磁盘)在哪里操作/dev/sdb
name
设置分区号的名称(仅限GPT、Mac、MIPS和PC98)。
number
要处理的分区数量或将要创建的分区数量。在磁盘上执行任何操作时需要执行,但获取信息除外。
part_end
string	Default:100%”	分区将以从磁盘开头的偏移量结束,即从磁盘开始的“距离”。
距离可以用分开支持的所有单元(兼容除外)指定,并且区分大小写,例如10GiB,15%。
part_start
string	Default:0%”	分区将从磁盘开头开始作为偏移量,即从磁盘开始的“距离”。
距离可以用分开支持的所有单元(兼容除外)指定,并且区分大小写,例如10GiB,15%。
part_type
  extended
· logical
· primary 
只能使用’msdos’或’dvh’分区表指定。必须为’gpt’分区表指定名称。part_type和name都不能与“sun”分区表一起使用。
state
· absent
· present
· info 
是创建还是删除分区。如果设置为 info,该模块将只返回设备信息。
unit
  s
· B
· KB
· KiB ⬅️
· MB
· MiB
· GB
· GiB
· TB
· TiB
· %
· cyl
· chs
· compact	
选择当前默认单元,该单元将用于在磁盘上显示位置和容量,如果它们不是单元后缀,则解释用户给出的位置和容量。获取有关磁盘的信息时,总是建议指定一个单元。
案例:
  - name: device 1500M
      parted:
        device: /dev/vdd                               #指定硬盘
        number: 1                                        #编号为1
        state: present
        part_end: 1500MiB                                #分区结束位置
		


filesystem模块
dev
设备或映像文件的目标路径。
force
  no ←
· yes	
如果 yes,允许在已经拥有文件系统的设备上创建新的文件系统
fstype
  btrfs
· ext2
· ext3
· ext4
· ext4dev
· f2fs
· lvm
· ocfs2
· reiserfs
· xfs
· vfat
· swap	
要创建的文件系统类型。
opts
要传递给mkfs命令的选项列表。
resizefs
 no ←
 yes	
 如果 yes,如果块设备和文件系统大小不同,则将文件系统扩展到空间中。
支持ext2、ext3、ext4、ext4dev、f2fs、lvm、xfs、vfat、swap文件系统。
XFS只有在挂载时才会增长。vFAT将会失败如果fatresize < 1.04


mount模块
backup
no 
yes	
创建一个包含时间戳信息的备份文件,以便在您以某种方式错误地抓取原始文件时取回原始文件。
boot
 yes 
·no	
确定文件系统是否应该挂载在引导上。仅适用于Solaris系统。
dump
Default: 0	Dump(见fstab(5))。
请注意,如果设置为 null,state设置为present,它将停止工作,并将在后续运行中重复条目。对Solaris系统没有影响。
fstab
文件代替/etc/fstab。除非你真的知道自己在做什么,否则你不应该使用这个选项。如果您需要在chroot环境中配置挂载点,这可能会很有用。OpenBSD不允许指定带有挂载的备用fstab文件,因此不要在OpenBSD上以任何在实时文件系统上运行的状态使用它。
在Solaris上,此参数默认为/etc/fstab或/etc/vfstab。
fstype
文件系统类型。当state是 present 或 mounted时需要。
opts
mount选项(请参阅solaris上的fstab(5)或vfstab(4))
passno
Default: 0	Passno(见fstab(5))。
请注意,如果设置为null 和state设置为present,它将停止工作,并将在后续运行中重复条目。Solaris系统不建议使用。
path
挂载点的路径(例如/mnt/files)。在Ansible 2.3之前,此选项只能用作dest、destfile和名称。
src
要被挂载设备的路径。当state设置为present或mounted时是必需的。
state
· absent
· mounted
· present
· unmounted
· remounted	
如果 mounted,设备将被主动安装,并在fstab中适当配置。如果挂载点不存在,则将创建挂载点。
如果 unmounted,设备将卸载而不更改fstab。
present 仅指定设备将在fstab中配置,不会触发或要求挂载。
absent 表示设备支架的条目将从fstab中删除,还将卸载设备并删除挂载点。
remounted 指定当您想强制刷新挂载本身时,设备将被重新安装(在2.9中添加)。这总是会返回 changed=true
常用案例:
- name: mount
      mount:
        path: /newpart                                   #挂载到那个目录
        src: /dev/vdd1                                   #挂载内容
        fstype: ext4                                     #文件系统格式
        state: mounted    

lvg模块使用案例:
(注意:pv可以直接用lvg模块创建如:vg=vgtest  pvs=/dev/sdb,dev/sdc)它会自动把那两块盘加入到pv
 
# 在test组中的主机上安装lvm2,state不写,默认是present
[root@control ansible]# ansible test -m yum -a "name=lvm2"

# 手工在beikongji1上对vdb进行分区
[root@beikongji1 ~]# fdisk /dev/vdb
Command (m for help): g    # 创建GPT分区表
Command (m for help): n    # 新建分区
Partition number (1-128, default 1):    # 回车,使用1号分区
First sector (2048-41943006, default 2048):   # 起始位置,回车
Last sector, +sectors or +size{K,M,G,T,P} (2048-41943006, default 41943006): +5G   # 结束位置+5G

Command (m for help): n   # 新建分区
Partition number (2-128, default 2):   # 回车,使用2号分区
First sector (10487808-41943006, default 10487808): # 起始位置,回车
Last sector, +sectors or +size{K,M,G,T,P} (10487808-41943006, default 41943006): # 结束位置,回车,分区到结尾
Command (m for help): w   # 存盘

[root@beikongji1 ~]# lsblk    # vdb被分出来了两个分区
NAME   MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
sr0     11:0    1 1024M  0 rom  
vda    253:0    0   30G  0 disk 
`-vda1 253:1    0   20G  0 part /
vdb    253:16   0   20G  0 disk 
|-vdb1 253:17   0    5G  0 part 
`-vdb2 253:18   0   15G  0 part 
vdc    253:32   0   20G  0 disk 

# 在test组中的主机上创建名为myvg的卷组,该卷组由/dev/vdb1组成
[root@control ansible]# ansible test -m lvg -a "vg=myvg pvs=/dev/vdb1"

# 在beikongji1上查看卷组
[root@beikongji1 ~]# vgs
  VG   #PV #LV #SN Attr   VSize  VFree 
  myvg   1   0   0 wz--n- <5.00g <5.00g

# 扩容卷组。卷组由PV构成,只要向卷组中加入新的PV,即可实现扩容
[root@control ansible]# ansible test -m lvg -a "vg=myvg pvs=/dev/vdb1,/dev/vdb2"

[root@beikongji1 ~]# vgs  # 在node1上查看卷组
  VG   #PV #LV #SN Attr   VSize  VFree 
  myvg   2   0   0 wz--n- 19.99g 19.99g

lvol模块
常用参数:
lv 定义逻辑卷名
size 定义逻辑卷大小
vg 空间来自哪个卷组
state{present 创建,absent 删除}
force=yes 强制执行
pvs 指定物理卷
pesize 定义pe大小
active     yes  no	卷是否处于激活状态且对主机可见。
force
 no ←
· yes	
卷的收缩或删除操作需要此开关。确保文件系统不会被错误损坏/破坏
lv		逻辑卷的名称。
opts		传递给lvcreate命令的自由形式选项。
pvs		物理卷的逗号分隔列表(例如/dev/sda,/dev/sdb)。
resizefs
· no ←
· yes	
与逻辑卷一起调整底层文件系统的大小
shrink
  yes ←
· no	如果当前尺寸大于要求的尺寸,则收缩。
size		逻辑卷的大小,根据lvcreate(8)size,默认情况下以兆为单位,或可选使用[bBsSkKmMgGtTpPeE]单位之一;或者根据lvcreate(8) --extent作为[VG
snapshot		快照卷的名称
state	· present ← absent	控件是否存在逻辑卷。如果 present 且卷不存在,则需要使用 size 选项。
thinpool		精简池卷名称。创建精简卷时,需要指定精简池卷名称。
vg		逻辑卷所在的卷组。
常用案例:
 tasks:
  - block:
    - name: create data of 1500M
      lvol:
        vg: research
        lv: data
        size: 1500

setup 模块用于收集远程主机的一些基本信息
setup返回信息中列出的字段很多,可以用字段名作为filter的参数值,
列出一些字段如下:
ansible_all_ipv4_addresses:显示ipv4的地址信息
ansible_distribution:显示linux发行版,例:centos,suse等
ansible_distribution_major_version:显示系统主版本
ansible_distribution_version:显示系统版本
ansible_machine:显示系统类型,32/64位
ansible_eth0:显示eth0的信息
ansible_hostname:显示主机名
ansible_kernel:显示内核版本
ansible_lvm:显示lvm相关信息
ansible_memtotal_mb:显示系统总内存
ansible_memfree_mb:显示可用系统内存
ansible_memory_mb:详细显示内存情况
ansible_swaptotal_mb:显示总的swap内存
ansible_swapfree_mb:显示swap内存的可用内存
ansible_mounts:显示系统磁盘挂载情况
ansible_processor_vcpus:显示cpu个数(只显示总的个数)
ansible_python_version:显示python版本

利用setup过滤出想要的ip  完全域名  主机名
[root@controls ansible]# cat tt.yml 
---
- name: test
  hosts: all
  tasks:
    - name: setfac
      debug:
        msg: "{{ ansible_all_ipv4_addresses[0] }}  {{ ansible_fqdn }}  {{ ansible_hostname }} "


block模块的用法
用和不动对比
- hosts: test
  remote_user: root
  tasks:
    - shell: 'cat /etc/redhat-release'
      register: stdout_info
      ignore_errors: true
      rescue:
    - debug:
        msg: 'I caught an error'
      when: 'stdout_info is failed'  
	  
用block:
- hosts: test
  remote_user: root
  tasks:
  - block:
      - shell: 'cat /etc/redhat-release'
    rescue:
      - debug:
          msg: 'I caught an error'

如上例所示,定义了一个block,这个block中有一个任务,这个任务在目标主机中执行了’‘cat/etc/redhat-release’’'命令,除了block关键字,还有另外一个关键字rescue,rescue关键字与block关键字对齐,rescue的字面意思为"救援",表示当block中的任务执行失败时,会执行rescue中的任务进行补救,当然,在rescue中定义什么任务,是由你决定的。也就是说当block中的任务出错时,会执行rescue中的任务,当block中的任务顺利执行时,则不会执行rescue中的任务。
你可能会问,使用block的方法完成"错误处理"的功能,似乎与使用failed的方法并没有什么不同,除了代码似乎"精简"了一点,block还有其他优势么?其实,使用block的方式还是有一定优势的,当block中有多个任务时,这种优势就比较明显了:
---
- hosts: testuser
  remote_user: root
  tasks:
  - block:
      - debug:
          msg: 'I execute normally'
      - command: /bin/false
      - debug:
          msg: 'I never execute, due to the above task failing'
    rescue:
      - debug:
          msg: 'I caught an error'
      - command: /bin/false
      - debug:
          msg: 'I also never execute'
    always:
      - debug:
          msg: "This always executes"
    when: 2>1


include_role模块 -- 加载并执行角色
将指定的角色作为任务动态加载和执行。
只能在Ansible任务允许的情况下使用——在pre_tasks, tasks,或post_tasks剧本对象内,或作为角色内的任务。
任务级关键字、循环和条件只应用于include_role语句本身。
要将关键字应用到角色中的任务,可以使用apply选项传递它们,或者使用import_role代替。
忽略一些关键字,如until和retries
Windows目标也支持此模块
name参数为指定角色的名称
例如:
- name: changed
  include_role:
    name: rhel-system-roles.selinux

stat模块:
和linux的stat功能相同 ,Ansible的stat模块主要用于获取被控客户端的文件属性信息。该模块主要有path参数用于指定被控客户端的文件。判断文件是不是存在
Ansible的stat模块使用如下:
ansible all -m stat -a "path=/root/ansible.txt"

Ansible get_url模块
Ansible的get_url模块主要用于实现被控客户端从远程将文件下载到本地。该模块有四个常用参数,url参数主要用于指定被控客户端要远程下载的文件,dest参数主要指定目的文件夹,mode参数指定下载后的文件权限,force参数可以为yes或者是no。如果force参数为yes,则表示如果所下载的内容和原目录下的文件内容不一样,则下载并替换原文件,如果相同,则不进行下载;如果force参数为no,则不管目录下的同名文件是否相同,只有在目标不存在时才下载文件。在Ansible0.6版本之前,该参数默认为yes,在Ansible0.6之后,该参数默认为no。在生产环境中,一般小文件的下载选用yes。
ansible exp -m get_url -a "url=http://nginx.org/download/nginx-1.4.7.tar.gz dest=/root/ mode=0644 force=yes"

cron模块
Ansible的cron模块主要控制被控客户端添加定时任务,该参数有三个常用参数,name表示给该定时任务命名,minute参数指定该定时任务的间隔,job指定了具体的操作。Ansible的cron模块使用如下:
ansible exp -m cron -a "name='exp' minute='*/1' job='pwd'"

name	计划任务名称
job	指定计划的任务中需要实际执行的命令或者脚本
user	指定计划任务属于哪个用户,默认是root用户
state	指定状态,prsent 表示添加定时任务,也是默认设置,absent 表示删除定时任务
backup	对已有的任务修改或删除时,是否保存
disabled	当计划任务有名称时,根据计划任务名称关闭(注释)对应的计划任务
minute	分钟,取值范围(0-59**/2)
hour	小时,取值范围(0-23,,/2)
day	天,取值范围(1-31,,/2)
mouth	月,取值范围(1-12,,/2)
weekday	设置计划任务中周几设定位的值,取值范围(0-6 for Sunday-Saturday, *)
cron_file	如果指定, 使用这个文件cron.d,而不是单个用户crontab

用户jack每三个月的每周日晚上2239分查看一次自身用户登录情况:
vim crontab.yml
---
- hosts: all
  tasks:
  - name: create a cron file under /etc/cron.d
    cron:
      name: Lgin time
      minute: "39"                        #分
      hour: "22"                          #时
#     day: ""                             #日
      month: "*/3"                        #月
      weekday: "0"                        #周
      user: jack                          #指定用户
      job: "(last && lastb)|grep jack"    #执行内容

firewalld模块
参数:
immediate	
yes or no,默认no	如果将此配置设置为永久性,则应立即应用此配置
port		
在防火墙中添加或移除端口名称或端口范围。端口范围必须为PORT/PROTOCOL或PORT-PORT/PROTOCOL格式。
service		
在防火墙中添加或移除服务名称该服务必须在firewall-cmd --get-services的输出中列出。
state	
absent or disabled or enabled or present	启用或禁用一项设置。对于端口:该端口是否接受(enabled)或拒绝(disabled)连接。状态的(present)(absent)只能在区域级别操作中使用(即,当除了zone和state没有设置其他参数时)。
zone		
添加或移除的防火墙区域。注意,可以为每个系统配置默认的区域,可能的值有block, dmz, drop, external, home, internal, public, trusted, work,public
permanent	
yes or no	该配置是否在运行的防火墙配置中,或者在重新启动时仍然存在。注意,如果这是no, immediate被假定为yes。
masquerade		
在在防火墙中开启或关闭masquerade功能


script模块用法
创建一个sh文件里面内容是在tmp下创建一个文件,然后再用script执行
cat /root/a.sh
	touch /tmp/testfile
 ansible node1:node2 -i inventory.ini -m  script -a "/root/a.sh"


yum_repository模块(专用于处理yum仓库文件,那个特殊符号$要转义,不然拷过去就是空的)
ansible node1:node2 -i /root/inventory.ini  -m  yum_repository -a "name=epel baseurl='http://download.fedoraproject.org/pub/epel/7/\$basearch/' description='epel yum repo' gpgcheck=no enabled=yes  state=present"


yum模块
常用参数:
name 要安装的软件包名, 多个软件包以英⽂逗号(,) 隔开
state 对当前指定的软件安装、移除操作(present installed latest absent removed) 持的参数 
--- present 确认已经安装,但不升级 
- - installed 确认已经安装 
- - latest 确保安装,且升级为最新 
- - absent 和 removed 确认已移除
-  -enabled=true
	
ansible node1:node2 -i inventory.ini -m yum -a "name=nginx  state=present"

systemd模块
state 对当前服务启动,停掉、重启、重新加载等操作(started,stopped,restarted,reloaded)
daemon_reload 重新载 systemd,扫描新的或有变动的单元
enabled 是否开机启动 yes|no

ansible node1:node2 -i inventory.ini -m systemd -a "name=nginx state=started"

group组模块
gid
interger		要为组设置的可选 GID。
name
要管理的组的名称。system
boolean	· 
no、yes	如果yes,则表示创建的组是系统组

ansible node1:node2 -i inventory.ini -m group -a "name=group1"

loop模块:
循环打印案例
tasks:
  - name:Print
    debug:
      msg: "{{ item }}"   //这个是固定写法,必须要是item
    loop: [1,2,3,4]
    
 批量建立文件案例:
 tasks:
   - name:cteate file
     file: 
       path:/tmp/file{{ item }}.txt    
       state: touch
     loop: 
       - 1
       - 2
       - 3
 


user模块
常参数:
name 必须的参数, 指定户名
password 设置户的密码,这接受的是个加密的值,因为会直接存到 shadow, 默认不设置密码
update_password 假如设置的密码不同于原密码,则会更新密码. 
home 指定户的家录
shell 设置户的 shell
comment 户的描述信息
create_home 在创建户时,是否创建其家录。默认创建,假如不创建,设置为 no。
group 设置户的主组
groups 将户加到多个其他组中,多个逗号隔开。默认会把⽤户从其他已经加⼊的组中删除。
append yes|no 和 groups 配合使,yes 时,不会把户从其他已经加的组中删除,是否为附加组
system 设置为 yes 时,将会创建个系统账号
expires 设置户的过期时间,值为时间戳,会转为为天数后,放在 shadow 的第 8 个字段
计算时间戳命令:date -d 2022-01-20 +%s 
generate_ssh_key 设置为 yes 将会为户成密钥,这不会覆盖原来的密钥
ssh_key_type 指定户的密钥类型, 默认 rsa, 具体的类型取决于被管理节点
state 删除或添加户, present 为添加,absent 为删除;默认值 present
remove 当与 state=absent 起使,删除个户及关联的录,如家录,邮箱⽬录。可选的值为: yes/no

先在控制机上生成本地密码变量加密过的:
pass=$(echo "admin" | openssl passwd -1 -stdin)    //那是数字1 

再创建时调用(因为不支持传明文密码设置)
ansible node1:node2 -i inventory.ini -m user -a "name=test  password=${pass}"

ansible node1:node2 -i inventory.ini -m user -a "name=tt state=present group=node1 groups=group1  append=yes"

删除用户一定要带上remove变量,不然家目录删除不了
ansible node1:node2 -i inventory.ini -m user -a "name=tt state=absent remove=yes"

用户隔在哪一天过期
ansible node1:node2 -i inventory.ini -m user -a "name=tom expires=$(date +%s -d 20230225)"

// 计算 3 时之后是点分
 date +%T -d '3 hours'
 
// 任意日期的前 N 天,后 N 天的具体日期
 date +%F -d "20190910 1 day"
 date +%F -d "20190910 -1 day"
 
// 计算两个日期相差天数, 如计算生日距离现在还有多少天
 d1=$(date +%s -d 20180728)
 d2=$(date +%s -d 20180726)
 echo $(((d1-d2)/86400))

cron定时任务模块

常参数:
name 指定⼀个cron job 的名字。⼀定要指定,便于⽇之后删除。
minute 指定分钟,可以设置成(0-59, *, */2)格式。 默认是* , 也就是每分钟。
hour 指定⼩时,可以设置成(0-23, *, */2)格式。 默认是 * ,也就是每⼩时。
day 指定天, 可以设置成(1-31, *, */2)格式。 默认是 * , 也就是每天。
month 指定⽉份, 可以设置成(1-12, *, */2)格式。 默认是* , 也就是每周。
weekday 指定星期, 可以设置成(0-6 for Sunday-Saturday, *)格式。默认是 *,也就是每星期。
job 指定要执⾏的内容,通常可以写个脚本,或者⼀段内容。
state 指定这个job的状态,可以是新增(present)或者是删除(absent)。 默认为新增(present)
user 指定用哪个用户执行,不写默认是root用户

ansible all -i inventory.ini -m cron -a "name='show fiel' user=node1 minute='*/1'  job='ls -al >/dev/null'"

ansible all -i inventory.ini -m cron -a "name='show fiel' user=node1 state=absent"

debug模块用法(一般用于获取信息,再从信息中过滤出要的,格式是python字典形式的过滤,类似于setup模块)
ansible all -i inventory.ini -m debug -a "msg='admin  is {{var}}'"  -e "var=mysql"


template模块使用(常用于jinjia2传参)
[root@nfs ~]# cat test.j2 
Hello {{var}}!
把var变量直接传过给受控机文件中去
ansible all -i inventory.ini -m template -a "src=/root/test.j2  dest=/tmp/test.j2"  -e "var=world"
受控机上查看:
[node1@node1 root]$ cat /tmp/test.j2
Hello world!

lineinfile模块使用(单行操作)
常参数
	path 被管理节点的标件路径, 必须。
	state 可选值absent 删除 |present 替换(默认值)。
	regexp 在件的每中查找的正则表达式。对于 state=present ,仅找到的最后将被替换。
	line 要在件中插/替换的。需要state=present 。
	create 件不存在时,是否要创建⽂件并添加内容。yes/no
	
删除注释行有井号的
 ansible all -i inventory.ini -m lineinfile -a "path=/root/one.sh regexp='^#' state=absent"
 
 查到那行source开头的替换成tttt
 ansible all -i inventory.ini -m lineinfile -a "path=/root/one.sh regexp='^source' line='tttt' state=present"

 blockinfile模块用法(多行操作)
 添加两行在最后
 ansible all -i inventory.ini -m blockinfile -a "path=/root/one.sh block='aaaa\nbbbbbb' state=present"
 
 删除那两行
 ansible all -i inventory.ini -m blockinfile -a "path=/root/one.sh block='aaaa\nbbbbbb' state=absent"


## playbook

常规属性
	name 属性, 每个play的名字唯一
	hosts 属性, 每个play 涉及的被管理服务器,同ad-hoc 中的资产选择器
	tasks 属性, 每个play 中具体要完成的任务,以列表的形式表达
	become 属性,如果需要提权,则加上become 相关属性
	become_user 属性, 若提权的话,提权到哪个用户上	remote_user属性,指定连接到远程节点的⽤户,就是在远程服务器上执⾏具体操作的用户。若不指定,则默认使当前执行的ansible Playbook 
	
2个任务剧本:	
---
- name: nginx_install_start
  hosts: node1
  remote_user: root
  tasks:
    - name: install nginx 
      yum:  name=nginx state=present
    - name: copy one.sh to /tmp/one.sh
      copy: src=/root/one.sh  dest=/tmp/one.sh
    - name: start nginx
      systemd: name=nginx enabled=true state=started
- name: shell run
  hosts: node2
  remote_user: root
  tasks:
    - name: show files
      shell: ip addr list
... 
安装python对yaml检测工具
yum install python2-pip
pip install pyyaml
	
剧本校验是不是有误
[root@nfs ~]# python -c 'import yaml,sys; print yaml.load(sys.stdin)' <playbook1.yaml
[{'tasks': [{'yum': 'name=nginx start=present', 'name': 'install nginx'}, {'copy': 'src=/root/one.sh dest/tmp/one.sh', 'name': 'copy one.sh to /tmp/one.sh'}, {'systemd': 'name=nginx enabled=true state=started', 'name': 'start nginx'}], 'hosts': 'inde1', 'remote_user': 'root', 'name': 'nginx_install_start'}]

再用语法检测:
ansible-playbook -i inventory.ini playbook1.yaml --syntax-check

再模拟执行一遍(不会远程执行,本地跑一次,后面c为大写)
ansible-playbook -i inventory.ini playbook1.yaml -C

单步调试,按y一步一步走,按c全跑完,按N跳过(Perform task: TASK: Gathering Facts (N)o/(y)es/(c)ontinue:)
ansible-playbook -i inventory.ini playbook1.yaml --step

真正的执行操作(如果把inventory的主机资产清单写入/etc/ansible/hosts,就不用在命令行中指定清单文件了)
 ansible-playbook -i inventory.ini playbook1.yaml

 
接着卸载掉(一定要先把服务停掉写在前面,脚本是按行从上执行,如果先卸载再停服务就会出错)
 ---
- name: nginx_uninstall_start
  hosts: node1
  remote_user: root
  tasks:
    - name: stop nginx
      systemd: name=nginx enabled=false state=stopped
    - name: uninstall nginx
      yum:  name=nginx state=absent
    - name: del /tmp/one.sh
      file: path=/tmp/one.sh  state=absent
- name: shell run
  hosts: node2
  remote_user: root
  tasks:
    - name: show files
      shell: ip addr list
...



## 全局变量和剧本变理



先生成一个全局变量
[root@nfs ~]# cat a.json 
{"names": "yeng", "type": "man"}

再调用全局变量输出
[root@nfs ~]# ansible all -i inventory.ini -m debug -a "msg='name is {{names}}, type is {{type}}'" -e  @a.json

输出如下:
192.168.136.158 | SUCCESS => {
    "msg": "name is yeng, type is man"
}
192.168.136.157 | SUCCESS => {
    "msg": "name is yeng, type is man"
}


局本变量(2种形式,1在剧本中直按定义和调用,2在外部单独用个yml文件中定义,剧本中用vars_files调用)
第1种:
[root@nfs ~]# cat playbook2.yaml 
---
- name: test play var
  hosts: node1
  vars: 
    user: ttt 
    home: /home/ttt
  tasks: 
    - name: create user{{user}}
      user:
        name: "{{user}}" 
        home: "{{home}}"
...

接着删除:
[root@nfs ~]# !cat
cat playbook2.yaml 
---
- name: test play var
  hosts: node1
  vars: 
    user: ttt 
    home: /home/ttt
  tasks: 
    - name: create user{{user}}
      user:
        name: "{{user}}" 
        state: absent
        remove: yes
...2种:
先建立一个变量文件
[root@nfs ~]# cat users.yml 
user: tttt
home: /home/tttt

[root@nfs ~]# cat playbook2.yaml 
---
- name: test play var
  hosts: node1
  vars_files:
  - /root/users.yml
  tasks: 
    - name: create user{{user}}
      user:
        name: "{{user}}" 
        home: "{{home}}"
...


## 主机资产变量


[root@nfs ~]# cat inventory.ini 
[node1]
192.168.136.157 user=yeng  port=22  //这种自定义的写法只能在json中申明和赋值,如下案例就是,单行写法

[node2]
192.168.136.158


[root@nfs ~]# ansible all -i inventory.ini -m debug -a "msg='{{user}} {{port}}'"
192.168.136.157 | SUCCESS => {
    "msg": "yeng 22"
}

主机组变量:
[root@nfs ~]# cat inventory.ini 
[node1]
192.168.136.157 user=yeng  port=22
192.168.136.159

[node1:vars]
home="/home/tom"    //整个node1全局生效,如果和node1里面冲突,则最小匹配原则

[node2]
192.168.136.158

变量继承:
[root@nfs ~]# cat inventory.ini    
[node1]
192.168.136.157 user=yeng  port=22
192.168.136.159


[node1:vars]
home="/home/tom"

[node2]
192.168.136.158

[all_node]
[all_node:children]  //children是固定写法,这个all_node是包含两个主机组的全局变量
node1
node2

## 剧本中调用用的内置变量

都是以 ansible_ 为前缀。
	ansible_ssh_host   将要连接的远程主机名与你想要设定的主机的别名不同的话,可通过此变量设置.
	ansible_ssh_port   ssh端⼝号.如果不是默认的端号,通过此变量设置.
	ansible_ssh_user   默认的 ssh 户名
	ansible_ssh_pass   ssh 密码(这种式并不安全,官强烈建议使 --askpass或 SSH 密钥)
	ansible_sudo_pass  sudo 密码(这种式并不安全,官强烈建议使 --asksudo-pass)
	ansible_sudo_exe   (new in version 1.8)sudo 命令路径(适于1.8及以上版本)
	ansible_ssh_private_key_file  ssh 使的私钥件.适于有多个密钥,你不想使 SSH代理的情况.
	ansible_python_interpreter  标主机的 python 路径.适⽤于的情况: 系统中有多个Python,或者命令路径不是"/usr/bin/python",/usr/local/bin/python3
	
[root@nfs ~]# !cat
cat inventory.ini 
[node1]
192.168.136.157 user=yeng  port=22  //这种自定义的写法只能在json中申明和赋值,单行执行ad-hoc写法
192.168.136.159  ansible_ssh_port=2222 //这种可以直接用剧本调用,是内置好的变量,不用管,剧本中直接用

[node1:vars]
home="/home/tom"

[node2]
192.168.136.158

[all_node]
[all_node:children]
node1
node2


## facts变量

(系统是默认开启的,每次执行剧本都会自动收收集受控机信息,浪费时间,可以关掉)
Facts变量不包含在前⽂中介绍的全局变量、剧本变量及资产变量
之内。
Facts变量不需要我们⼈为去声明变量名及赋值。
它的声明和赋值完全有Ansible 中的 setup 模块帮我们完成。
它收集了有关被管理服务器的操作系统版本、服务器IP地址、主机名,磁盘的使⽤情况、CPU个数、内存等等有关被管理服务器的私有信息。
在每次PlayBook运⾏的时候都会发现在PlayBook执前都会有个Gathering Facts的过程。这个过程就是收集被管理服务器的Facts信
息过程。


ansible  all_node -i inventory.ini -m setup -a "filter=*memory*"  //只拿内存过滤出来,*mount*是磁盘挂载,cpu等也类似

如果不用就关掉提高剧本速度:
---
- name: nginx_uninstall_start
  hosts: node1
  gather_facts: no  //关闭默认的facts,
  remote_user: root
  tasks:
    - name: stop nginx
      systemd: name=nginx enabled=false state=stopped
    - name: uninstall nginx
      yum:  name=nginx state=absent
    - name: del /tmp/one.sh
      file: path=/tmp/one.sh  state=absent
- name: shell run
  hosts: node2
  remote_user: root
  tasks:
    - name: show files
      shell: ip addr list
...


## 注册变量(用于看剧本执行的结果的)


[root@nfs ~]# cat playbook1.yaml 
---
- name: nginx_uninstall_start
  hosts: node1
  remote_user: root

  tasks:
    - name: stop nginx
      systemd: name=nginx enabled=false state=stopped

    - name: uninstall nginx 
      yum:  name=nginx state=absent
      register: uninstall_result   //这里和下面的变量名称保持一致

    - name: print result
      debug: var=uninstall_result   //利用注册变量看执行结果

    - name: del /tmp/one.sh
      file: path=/tmp/one.sh  state=absent

- name: shell run
  hosts: node2
  remote_user: root

  tasks:
    - name: show files
      shell: ip addr list
...  


## 变量优先级:全局>剧本>资产


when:判断

通过条件判断(when) 指令去使用语法校验的结果,只要语法检查通过都会去执⾏ "start nginx server"这个TASK。
通过debug 模块,通过debug模块去确认返回结果的数据结构,打印出来的返回结果。 当nginxsyntax.rc 为 0时语法校验正确。
when: 一些常用判读方法:
exists、not exists、defined、undefined、none、success、failure、change、skip

when: is not exist
when: is exist

when: is defined
when: is undefined

when: is none

when: success
when: failure

when常用参数
==
!=
> >=
< <=
is defined
is  undefined
true
false
持逻辑运算符: and or

判断vgs在不在用:
when:  "'research' in ansible_lvm.vgs"    //not in
when: ansible_lvm.vgs.research is undefined   //这样也可以

判断磁盘在不在用:
when: ansible_facts.devices.vdd is defined    //或undefined

选择变量里面的key=value进行判断:
when: item.name == 'jack'

判断单个主机组是否在所有的主机组里
when: inventory_hostname in groups['prod']    //not in

jinjia2获取主机信息IP  域名  主机名
{{ hostvars[host]['ansible_facts']['default_ipv4']['address'] }} {{ hostvars[host]['ansible_facts']['fqdn'] }} {{ hostvars[host]['ansible_facts']['hostname'] }}


判断磁盘大小如果不在给默认值:
value: "{{ ansible_facts.devices.vdb.size | default('NONE') }}"

when:重要用法(上一个服务如果开了,才开下一个服务)---
- name: main1
  hosts: dev

  tasks:
    - name: test
      shell: /usr/bin/systemctl status vdo
      ignore_errors: yes
      register: result

    - name: open httpd   
      service:
        name: httpd
        state: started
      when: result.rc == 0    //不能把0用引号引住

第一行和二行是and关系,第二行内部是or的关系
      when:
        - item.job == "prod"
        - inventory_hostname in groups.prod or inventory_hostname in groups.balancers
    
 多行and关系:
       when:
        - item.job == "test"
        - inventory_hostname in groups.test


判读文件在不在
ansible dev -m stat -a "path=/etc/ddddd"输出下面查看可以组合成stat.exists 
node1 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python"
    },
    "changed": false,
    "stat": {
        "exists": false
    }
}

用stat模块输出再注册一下再调用判断
tasks:
    - name: file-not-yes
      stat:
        path: /etc/dx
      register: file_status
	  .....
	  .....
 when:  file_status.stat.exists == False   在就是True
 
 判断系统信息
 先查寻一下:ansible dev -m setup |grep ansible_distribution 
 再写剧本 (可以加andor)
     - name:
      debug:
        msg: centos
      when: ansible_distribution == "CentOS"
"ansible_distribution": "CentOS",
"ansible_distribution_file_parsed": true,
"ansible_distribution_file_path": "/etc/redhat-release",
"ansible_distribution_file_variety": "RedHat",
"ansible_distribution_major_version": "7",
"ansible_distribution_release": "Core",
"ansible_distribution_version": "7.9",


---
- name: test playbook control
  hosts: all_node
  gather_facts: no

  tasks: 
    - name: create user
      user:
        name: tom
        state: present
        password: "{{ 'adminw'|password_hash('sha512') }}"

    - name: yum is ok
      yum:
        name: nginx 
        state: present
    
    - name: update nginx
      copy:
        src: nginx.conf
        dest: /etc/nginx/conf.d/

    - name: check nginx
      shell: /usr/sinb/nginx -t
      register: nginx_check

    - name: print nginx check result
      debug:
        var: nginx_check        

    - name: start server
      systemd:
        name: nginx
        state: started
      when: nginx_check.rc == 0
...

循环控制
在PlayBook中使with_items 去实现循环控制,且循环时的中间变量(上shell循环中的 $i 变量)只能是关键字 item ,不能随意
定义。

批量循环创建用户和密码:
[root@nfs ~]# cat playbook3.yml 
- name: variable playbook example
  hosts: all_node
  gather_facts: no

  vars:
    createuser:
      - tomcat
      - www
      - mysql
   
  tasks:
    - name: create user
      user: 
        name: "{{ item }}" 
        state: present
        password: "{{ 'adminw'|password_hash('sha512') }}"
      with_items: "{{ createuser }}"
	  
	  
通过Play中的tags 属性,去解决前PlayBook变更导致的扩变更范围和变更险的问题。
在改进的PlayBook中,针对件发布TASK 任务"update nginx main config""add virtualhost config"新增了属性 tags ,属性值为updateconfig。
另外我们新增"reload nginx server" TASK任务。当配置件更新后,去reload Nginx 服务。
那重新加载需要依赖于 Nginx 服务是已经启动状态。所以,还需要进⼀步通过判断 Nngix 的 pid 件存在,才证明 Nginx 服务本身是启动中,启动中才可以 reload Nginx 服务。


判断个件是否存在使 stat 模块,会发现 nginxrunning.stat.exists 的值是 true 就表示启动状态,是 false 就是关闭状态。
[root@nfs ~]# cat playbook2.yaml 
---
- name: test playbook control
  hosts: all_node
  gather_facts: no

  tasks: 
    - name: create user
      user:
        name: tom
        state: present
        password: "{{ 'adminw'|password_hash('sha512') }}"

    - name: yum is ok
      yum:
        name: nginx 
        state: present
    
    - name: update nginx
      copy:
        src: nginx.conf
        dest: /etc/nginx/conf.d/
      tags: updateconfig

    - name: check nginx
      shell: /usr/sinb/nginx -t
      register: nginxcheck

    - name: check nginx pid
      stat: 
        path: /var/run/nginx.pid
      tags: updateconfig

    - name: print nginx check result
      debug:
        var: nginxcheck        

    - name: print nginx run
      debug:
        var: nginxrun        

    - name: start server
      systemd:
        name: nginx
        state: started
      when: 
        - nginxcheck.rc == 0 and nginxrun.stat.exists == true
      tags: updateconfig
...

指定tags 去执PlayBook执时定要指定tags,这样再执的过程中只会执task 任务上打上tag 标记为 updateconfig 的任务
ansible-playbook -i inventory.ini playbook2.yaml -t updateconfig

Handlers 属性
观察当前的 Playbook,不能发现,当我的配置件没有发变化时,每次依然都会去触发TASK "reload nginx server"。
如何能做到只有配置⽂件发⽣变化的时候才去触发TASK "reloadnginx server",这样的处理才是最完美的实现。此时可以使
handlers 属性。

[root@nfs ~]# cat playbook2.yaml 
---
- name: test playbook control
  hosts: all_node
  gather_facts: no

  tasks: 
    - name: create user
      user:
        name: tom
        state: present
        password: "{{ 'adminw'|password_hash('sha512') }}"

    - name: yum is ok
      yum:
        name: nginx 
        state: present
    
    - name: update nginx
      copy:
        src: nginx.conf
        dest: /etc/nginx/conf.d/
      notify: reload nginx server  //监控些copy模拟返回值,两边名称要一致

    - name: check nginx
      shell: /usr/sinb/nginx -t
      register: nginxcheck

    - name: check nginx pid
      stat: 
        path: /var/run/nginx.pid

    - name: print nginx check result
      debug:
        var: nginxcheck        

    - name: print nginx run
      debug:
        var: nginxrun        

    - name: start server
      systemd:
        name: nginx
        state: started

  handlers:     //当上面监控的notify发生变化时就触发这里的动作
    - name: reload nginx server
      systemed: 
        name: nginx
        state: reloaded

jinjia2模本样本:
{# use variable example #}
wlecome host {{ ansible_hostname }}, os is {{
ansible_os_family }}
today is {{ ansible_date_time.date }}
cpucore numbers {{ ansible_processor_vcpus }}

{# use condition example #}
{% if ansible_processor_vcpus > 1 %}
OS CPU more than one core
{% endif %}

{% for m in ansible_mounts if m['mount'] != "/" %}

mount {{ m['mount'] }}, total size is
{{m['size_total']}}, free size is
{{m['size_available']}}
{% endfor %}

简单案例:
主控端先把nginx.cinf.j2文件配置好,就是把受控主机服务配置文件拿来改一下:
[root@nfs ~]# cat nginx.config.j2                                

user  nginx;

worker_processes  {{ cpu_number  }};   //在执行剧本时可以传变量给这个,随便受控机的这个配置会自动变

error_log  /var/log/nginx/error.log notice;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


shttp {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    include /etc/nginx/conf.d/*.conf;
}

主控机剧本:
[root@nfs ~]# cat playbook5.yml 
- name: test_jinjia2
  hosts: node1
  gather_facts: no
  
  tasks:
    - name: install_nginx
      yum:
        name: nginx
        state: present

    - name: update_config
      template: 
        src: /root/nginx.config.j2 
        dest: /etc/nginx/nginx.conf
		
 执行剧本传参数:
 ansible-playbook playbook5.yml -e "cpu_number=8"
 
 
 角色用法3种,一种是yum安装的系统角色,一种是自动用galaxy,一种是手动创建
 手动
 先建立项目文件夹为webservers,再继续建立下面的子文件,执行剧本就执行nginx_test.yml
 [root@nfs webservers]# tree  //项目目录结构
├── nginx
│   ├── files
│   │   └── nginx.conf
│   ├── handles
│   │   └── main.yml
│   ├── tasks
│   │   └── main.yml
│   ├── templates
│   │   └── nginx.conf.j2
│   └── vars
│       └── main.yml
└── nginx_test.yml
 
 自动创建galaxy
ansible-galaxy用法,类似于gethub可以直网上下载

ansible-galaxy install  //安装上共享的role
ansible-galaxy delet     //删除一个role
ansible-galaxy remove     
ansible-galaxy login       //登陆  
ansible-galaxy init    //创建一个role目录,不用上面一样手动创建了
ansible-galaxy list   //列举通过ansible-galaxy工具安装的role
ansible-galaxy search   //在galaxy上搜索共享的role(类似于gethub)

系统角色yum安装(装完位置 /usr/share/ansible/roles)
yum search roles  查系统角色软件包
sudo yum -y install rhel-system-roles
ansible-galaxy list   //查看
/usr/share/doc/rhel-system-roles/   模板文件位置,可以拿出来自定义改一下用
/usr/share/doc/rhel-system-roles/selinux/example-selinux-playbook.yml     selinux.yml


-------------------------------------------------常用架构
ansible判断语法结构
(类似于python中的try---except----finally)
tasks:
  -block         //先执行block
    - name:
	  yum:
   rescue:       //block有错误就执行rescue
     - name:
	   debug:
   always:       //不管上面哪个有错误还是没错误,都要执行这个下面的
     - name:
	   debug
	   
	   
循环变量使用:
- name:
  hosts: dev
  vars:
    user_all:
	- user1
	- user2
	- user3
	
  task:
    - name:
	  debug:
	    msg: "{{ item }}"		       //item为关键字
	  with_items: "{{ user_all }}"   //with_items为关键字
	  
嵌套循环变量使用(类似于python中的for中再for)- name:
  hosts: dev
  vars:
    user_all:
	- user1
	- user2
	- user3
	passwd_all:
	- admin1
	- admin2
	- admmin3
	
  task:
    - name:
	  debug:
	    msg: "{{ item[0] }}, {{ item[1]}}"		       //item为关键字
	  with_nested: "{{ user_all }}, {{ passwd_all }}"   //with_nested为关键字
	  
	  
循环加when判断:
- name:
  hosts: dev
  vars:
    numbers:
	- 1
	- 2
	- 3
	- 4
  tasks:
    - name:
	  debug:
	    msg: "{{ item }}"
	  with_items: "{{ numbers }}"
	  when: item > 3 //只有大于3的才会执行,其它跳过


外部变量文件使用案例
vim userdel.yml
---
user:  ##定义变量名称
  - aaa
  - bbb
  - lee

vim skk.yml
---
- name: test
  hosts: server2
  vars_files:  ##定义变量文件
    - ./userdel.yml  ##定义变量文件位置
  tasks:
    - name: userdel
      user:
        name: "{{ item }}"  ##循环每一个变量
        state: absent
        remove: yes
      loop:  
        "{{ user }}"  ##定义循环的变量


	  
	  
自定义变量并调用:
- name
  hosts: dev
  tasks:
    - name
	  set_fact:
	    yeng: ttttttt
		
    - name:
      debug:
        var: yeng	  //也可以不调自定义的,直接调系统的如:msg: "{{ansible_date_time['time'] }}"滤出时间











...




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

项目工程师余工

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值