ansible的简介和用法

Ansible  (底层是基于ssh连接的,每次操作其他主机需要输入密码 ,所以首先要实现基于key的公钥验证)

使用ansible:
ansible "192.168.60.3" -m shell -a 'ls /root' -k (单个用户在实现基于key的验证前)

安装ansible:

yum包的安装:

yum install ansible   (此包依赖于base源和epel源,安装前确保两个yum源可以使用)

编译安装:

yum -y install python-jinja2 PyYAML python-paramiko python-babel python-crypto

tar xf ansible-1.5.4.tar.gz

cd ansible-1.5.4

python setup.py build

python setup.py install

mkdir /etc/ansible

cp -r examples/* /etc/ansible

相关文件

配置文件

/etc/ansible/ansible.cfg 主配置文件,配置ansible工作特性

/etc/ansible/hosts 主机清单 (将要连接操控的主机IP地址写在此配置文件的最下方)

/etc/ansible/roles/ 存放角色的目录

程序

/usr/bin/ansible 主程序,临时命令执行工具

/usr/bin/ansible-doc 查看配置文档,模块功能查看工具

/usr/bin/ansible-galaxy 下载/上传优秀代码或Roles模块的官网平台

/usr/bin/ansible-playbook 定制自动化任务,编排剧本工具/

usr/bin/ansible-pull 远 程执行命令的工具

/usr/bin/ansible-vault 文件加密工具

/usr/bin/ansible-console 基于Console界面与用户交互的执行工具

主机清单inventory (里面列有要操控服务器的IP地址,需要自己手动来添加)

配置文件路径:/etc/ansible/hosts

格式:可以一连串写下去,也可分组,组与组之间的所拥有的服务IP地址可重复:

[lvserver]  (此为组名为自定义命名

192.168.60.3

192.168.60.4

webserve]  (此为组名为自定义 命名)

192.168.60.3

192.168.60.5

192.168.60.6

将上述IP地址添加到主机清单配置文件的最后一行就可以了。

ansible系列命令 (查看ansible里的各个模块的使用方法)

ansible-doc: 显示模块帮助

-a 显示所有模块的文档

-l, –list 列出可用模块

-s, –snippet 显示指定模块的playbook片段

示例:

ansible-doc –l 列出所有模块

ansible-doc ping 查看指定模块(ping)帮助用法

ansible-doc –s ping 查看指定模块(ping)帮助用法

如何使用ansible 

1 实现基于key的公钥验证:(基于key的验证可以在装系统时写进脚本,装完系统后就可以实现了)

生成私钥对,之后再将其公钥发送到各台服务器上的指定位置就可以了:

ssh-keygen -t rsa (生成密钥)

ssh-copy-id 192.168.60.5 (发送密钥,如需要批量发送公钥文件到多台主机查看第九周的博客)

小结:如果不实现基于key的验证,则每次使用ansible操作远程服务器时都需要输入密码,而且需要所有服务器的密码是相同的,如果一组当中有一个服务器的密码不同,第一次连不上,改成相同的密码后还是会连不上的,除非单独先连接一次此IP的地址让他的密钥加进/root/.ssh/know_host里面,或者将know_host里的内容删掉。因为是基于ssh的连接。

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

-m module 指定模块,默认为command (使用command模块时可以不用写commmand )

-v 详细过程 –vv -vvv更详细

–list-hosts 显示主机列表,可简写—list

-k, –ask-pass 提示连接密码,默认Key验证

-K, –ask-become-pass 提示输入sudo

-C, –check 检查,并不执行 (检查语法是否有错并不执行,在执行playbook剧本前使用)

-b, –become 代替旧版的sudo 切换

-T, –timeout=TIMEOUT 执行命令的超时时间,默认10s

-u, –user=REMOTE_USER 执行远程执行的用户

匹配主机的列表  :

ansible "192.168.60.5" -m command -a 'ls /root'  (此为一个命令的示例)

All :表示所有Inventory(主机列表)中的所有主机

ansible all –m ping

* :通配符

ansible “*” -m ping   (*代表所有)

ansible 192.168.1.* -m ping   (匹配后面的一部分)

ansible “*srvs” -m ping    (匹配组名的一部分)

或关系
ansible “websrvs:appsrvs” -m ping (取两个组的中IP地址的合集)

ansible “192.168.1.10:192.168.1.20” -m ping   (两个IP地址都ping)

与的关系

ansible “websrvs:&dbsrvs” –m ping( 在websrvs组并且在dbsrvs组中的主机)

逻辑非

ansible ‘websrvs:!dbsrvs’ –m ping   (在websrvs组,但不在dbsrvs组中的主机 )

ansible命令执行过程 

1. 加载自己的配置文件 默认/etc/ansible/ansible.cfg 

2. 加载自己对应的模块文件,如command 

3. 通过ansible将模块或命令生成对应的临时py文件,并将该 文件传输至远程服务器 的对应执行用户$HOME/.ansible/tmp/ansible-tmp-数字/XXX.PY文件 

4. 给文件+x执行 

5. 执行并返回结果 

6. 删除临时py文件,sleep 0退出

执行状态: (通过颜色来判断执行的结果)

绿色:执行成功并且 没有对目标主机进行更改操作

黄色:执行成功并且对目标主机做变更 

红色:执行失败

ansible常用模块及其具体用法:

1 . Command模块:在远程主机执行命令的模块,此模块为默认模块可以不用加 -m选项

此命令不支持 $VARNAME < > | ; & 等,用shell模块实现

2 .  Shell模块:ansible all -m shell -a  '要执行的命令' ,可以执行bash里的所有命令,可添加各种符号。

3  . Script:运行脚本的模块

ansible   all   -m  script  -a  f1.sh   (脚本加上执行权限后,写上绝对路径)

4 .  Copy:  从主控端复制文件到众多的操控端服务器的模块

ansible   all   -m copy -a "src=/root/f1.sh    dest=/data/f2.sh   owner=wang  mode=600  backup=yes"

src:为源文件      dest:为要复制到的地方     owner:设置所有者     mode:文件的权限设置

backup=yes 如果不等于yes,如果此处与源文件有相同的文件,则会覆盖掉;相反的化backup=yes如果此处有与源文件相同的文件,会自动将此文件加上日期并改名后备份,源文件在复制到此处。

4.1  Fetch:从客户端取文件至服务器端,copy相反,目录可先用tar打包后再抓取

ansible all  -m fetch -a ‘src=/root/(要抓取的文件)   dest=/data/(抓取后存放的位置)

5 .  Cron:计划任务任务模块

支持时间:minute,hour,day,month,weekday

创建任务 计划任务:

ansible all -m cron -a “minute=*/5  job=‘/usr/sbin/ntpdate 172.16.0.1 &>/dev/null’  name=jobss1”

其他的时间不写默认为*(“每”的意思)命令要写全他在的路径 例如/bin/touch f1

删除计划任务 :

ansible all  -m cron -a   ‘ name=jobss1  state=absent ’

state=absent (state为状态  absent 删除)

name=jobss1  (为要删除的计划任务的名字,新建计划任务定义的名字)

6 . File模块:设置文件属性 (对文件来操作,删除,新建,软连接)

ansible   all  -m file -a "name=/root/a.sh   state=touch  owner=wang mode=755“ :新建文件

ansible all  -m file -a ‘src=/app/testfile dest=/app/testfile-link   state=link’  两个文件建立硬链接

state=touch   (新建文件)

state=directory  (新建文件夹)

state=line    (建立链接)

stste=absent  (删除文件和文件夹以及链接)

具体及详细信息请查看帮助:ansible-doc  -s file  (前面为固定模式,后面为模块的名称)

7 .  Yum模块:管理包

ansible  all  -m yum -a ‘name=httpd state=latest’    使用yum模块安装httpd软件 

ansible  all  -m yum -a ‘name=httpd state=absent’   使用yum模块删除 httpd软件

ansible all -m yum -a 'name=/data/vftpd 3.0-27………………rpm' 安装单个rpm包。不依赖于yum仓库,但需要推送到每个被控端的主机。

8 .  Service模块:管理服务 (启动,暂停,重启服务)

ansible all -m service -a 'name=httpd state=stopped'       (停止服务)

ansible all -m service -a 'name=httpd state=started'       (开启服务)

ansible all –m service –a ‘name=httpd state=reloaded’ (重新加载此服务)

ansible all -m service -a 'name=httpd  state=restarted'  (重启此服务)

9 . User模块:管理用户

ansible all -m user -a 'name=user1 comment=“test user” uid=2048 home=/app/user1 group=root‘新建用户      最简单的用法:ansible all -m user -a 'name=user2'

name:名字   comment: 属于测试语句可以不用写的       uid:uid号    home:家目录        group:属组

ansible all -m user -a 'name=sysuser1 system=yes home=/app/sysuser1 '

    system=yes:意思时创建系统用户

ansible all -m user -a 'name=user1 state=absent remove=yes'  :删除用户

10 . Group:管理组

ansible srv -m group -a "name=testgroup system=yes“ 

ansible srv -m group -a "name=testgroup state=absent"

11. 解压文件的模块

name: Extract invault-wallet.tar.gz to /opt/software/openresty/nginx/html/invault
      unarchive:
        src: /home/centos/release_static/mainnet/invault-wallet.tar.gz     (源文件)
        dest: /opt/software/openresty/nginx/html/invault      (目标文件)
        owner: "{{ file_user }}"             (文件的所有者,在执行playbook时传递变量的值)
        group: "{{ file_user }}"               (文件的所有组变量,在执行playbook时传递变量的值)
      become: true                                (切换到root去执行此命令)
      become_method: su
      become_user: root
      when: backup_result is succeeded or backup_path_result.stat.exists == True

12. 判断文件是否存在的模块:

– name: Check backup path exists or not
      stat:
        path: /opt/software/openresty/nginx/html/invault{{ ansible_date_time.date }}
      register: backup_path_result
      become: true
      become_method: su
      become_user: root

此文件需要切换到root用户去查看

Ansible-playbook (剧本的介绍和写法)注意格式的缩进会使playbook报错。如果找不到报错的原因注意查看剧本的格式缩进。

playbook的编写及格式:

首先剧本的名字一般要以.yml为结尾 ;然后内容的开头第一行应该时3个横杠 – – – (可以不用写也行)

具体的格式书写如下图:

vim f1.yml

捕获1

上图为playbook的书写方式;(上图中应该是tasks写错了)

执行playbook:

ansible-playbook -C f1.yml  (检查剧本的执行,并不真实的来执行,只做检查)

ansible-playbook f1.yml  : 执行此playbook剧本 (可以不需要加执行权限)

使用#号注释代码  缩进必须是统一的,不能空格和tab混用 

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

执行playbook时传递变量参数值:

ssh centos@52.74.137.28 '/usr/bin/ansible-playbook /home/centos/playbook/mainnet/tomcat/release-mainnet.yml –extra-vars \
"targets=tomcat-b-03-group user=centos child_project=invault-account-tomcat war_name=invault-account.war" -f 10'

ssh centos@52.74.137.28 :首先从Jenkins上连接到ansible上去执行已经写好的剧本,前提是做好免密登录。

–extra-vars :传递参数的设置,后面跟要传递的playbook里引用的变量的值

-f 10  : 并行执行的进程数。

 

 

 

具体的各部分信息及介绍:
Hosts 执行的远程主机列表 

playbook中的每一个play的目的都是为了让某个或某些主机以某个指定的用户 身份执行任务。hosts用于指定要执行指定任务的主机,须事先定义在主机清 单中 。

Websrvs:dbsrvs 两个组的并集 

Websrvs:&dbsrvs 两个组的交集 

webservers:!phoenix 在websrvs组,但不在dbsrvs组

也可以这样使用:

ansible lv -m shell -a 'cat /etc/passwd' –limit 192.168.60.2  (只针对lv组中的单个主机来进行操控)

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

Tasks 任务集

playbook里的模块命令如果执行过一次有些情况下不会再次去执行的例如:yum安装包,如果已经跑过一遍的剧本第二次再执行一遍的话,默认就不会再安装一遍了。某些服务启过之后,也不会再次重启的。

案例:

示例:httpd.yml

– hosts: websrvs

remote_user: root

tasks:

– name: Install httpd

yum: name=httpd   state=present   (安装工具包)

– name: Install configure file

copy: src=files/httpd.conf dest=/etc/httpd/conf/   (复制配置文件)

– name: start service

service: name=httpd state=started enabled=yes     (启动服务)

在此案例中如果将安装工具的配置文件做了修改后,再次执行此剧本,过程是:包已经安装过了,不会再安装了,配置文件更改了会再次执行一次将原来的文件覆盖掉,最后一步,服务已经起来了,则不会再次执行了,所以问题是:修改的配置文件无法让其生效了。接下来就需要用到标记的模块了具体如何使用请看下面的介绍:

handlers和notify结合使用触发条件 :具体请看下面示例:

httpd.yml

– hosts: websrvs

remote_user: root

tasks:
– name: Install httpd

yum: name=httpd state=present (安装工具包)

– name: Install configure file

copy: src=files/httpd.conf dest=/etc/httpd/conf/ (复制配置文件)

notify: restart httpd   (在此处用notify做一下标记,如果发现此处再次执行或发生变化时)

– name: start service    (会触发下面的handlers的执行)

service: name=httpd state=started enabled=yes (启动服务)

handlers:          (在上面做的标记的地方 执行下面的命令)

– name: restart httpd

service: name=httpd status=restarted

一个notify对应handlers里的一个执行模块,可以写多个标记的notify,但是下面要对应的写上他的执行模块命令。

触发多个:可以连续跟两个notify,但handlers里的模块,每个name下面只能跟一个。

tags  (标签)

再cation模块后添加标签后,执行剧本可以调用标签的模块单独使用:案例如下:

捕获1

ansible-playbook -t tags标记的描述  f1.yml   (-t 选项时调用tags标签的action ,-t 后面只跟tags后面的描述就可以了) 可以执行多个标签,中间用逗号隔开。

 

Playbook中变量使用

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

1 ansible setup facts 远程主机的所有变量都可直接调用 

2 在/etc/ansible/hosts中定义 普通变量:主机组中主机单独定义,优先级高于公共变量 公共(组)变量:针对主机组中所有主机定义统一变量 

3 通过命令行指定变量,优先级最高 ,其次是playbook里,最后是主机列表清单里。

变量的定义及使用:

系统自带的变量:通过查看系统内部的setup模块:ansible all -m setup

在文件里定义变量然后调用:

1 . 新建一个name .yml的文件,在里面定义变量如:

var1:   hhh

var2:  tttt

2. 在新建一个playbook,并调用刚才添加在文件里的变量 : test.yml

– – –

–  hosts: all

remote_user:  root

var_files:    (引用文件的变量,格式单词必须写成这样)

–  name.yml   (引用定义变量的文件,如果剧本和定义变量的文件在同一个目录下则不需要写路径)

        (如果在不同目录下,引用定义变量的文件需要写全路径)

tasks:

-name:  touch file    (模块的描述信息)

file:   name=/data/{{  var1   }}  state=touch    (新建文件的模块命令,新建的文件名调用变量var1的赋值)

执行剧本:ansible-playbook  test.yml  执行结果为在all所有被控主机上新建了/data/hhh  的文件

在命令行里的定义和使用:(直接在命令行里定义,和引用)

ansible all -e var1=dddd -m shell -a 'touch /data/{{ var1 }}'

-e 为选项后面只能跟一个定义的变量,如果需要定义多个变量,在后面再添加一个-e然后定义变量就可以

在playbook中定义 变量 :

4 在playbook中定义

vars:

– var1: value1

– var2: value2

引用shell命令结果的变量

例子:
– hosts: all
  remote_user: root
  tasks:
    – name: Test
      shell: date -d "-1 day" +%F    (shell命令取值)
      register: test1                           (将取到的值传递给test1)
    – name: touch file
      shell: touch /data/{{ test1.stdout }}/tets33   (引用test1的变量必须要这种方式来引用)

执行的结果就是可以查找到昨天的日期并在目录下创建文件

 

模板templates (templates文件夹下的模板文件不能再单个ansible里调用,只能在playbook里用)

template:算是一个模板的模块,能够将自己定义的模板文件复制到,所需要模板文件的地方:

src:为源文件    dest:为目标要复制的文件   (此模块可以实现复制功能)

使用jinja2的语言,

一般和playbook文件同级的目录下创建一个模板文件夹(templates),然后将模板文件写在此文件夹下,并且模板文件的后缀名为.j2

下面一个示例来调用templates文件夹下的模板文件:(以nginx的服务为例)

本机装好nginx服务并将配置文件推送到被控断的主机上。

1 . 将nginx的配置文件拷贝到templates文件夹下,然后修改文件名添加后缀.j2

cp /etc/nginx/nginx.conf templates/nginx.conf.j2   (后缀必须添加j2)

2.编写脚本调用templates里的模板文件:

vim   testtemplate.yml

– – –

–  hosts:   all

remote_user:  root

tasks:

–  name: install  package

yum:  name=nginx

–  name:  copy template

template:  src=nginx.conf.j2  dest=/etc/nginx/nginx.conf   (调用模板文件,将temlpates下的模板

– name:  start service                                                                           (文件复制到新装的nginx下的配置文件)

sevice:  name=nginx  state=started   enabled=yes  (启用服务,且永久启用)

when :(条件测试语句)

在task后添加when子句即可使用条件测试;when语句支持Jinja2表达式语法

例如:
捕获2

执行此剧本的结果是:如果系统的主版本是7则会创建此文件,如果是六的话则会跳过不会创建此文件。

具体执行结果如下图所示:

捕获4567

register :标记上一次执行结果,作为下一次执行的条件的判断,执行的结果一般为jesion格式的数据

案例:

– name: set
      shell: date -d "-1 day" +%F
      register: yesterday

    – name: Check today backup path exists or not
      stat:
        path: /home/{{ user }}/backup/{{ child_project }}/{{ ansible_date_time.date }}/{{ war_name }}
      register: backup_path_result                              (将上面shell模块执行的结果传递到 backup_path_result 里去)

    – name: check yestady backup path exit or not  (判断文件是否存在的模块)
      stat:
        path: /home/{{ user }}/backup/{{ child_project }}/{{ yesterday.stdout }}/{{ war_name }}
      register: yestady_backup_path_result                (将上面shell模块执行的结果传递到 backup_path_result 里去)
      when: backup_path_result.stat.exits == False      (执行此模块的条件是在backup_path_result.stat.exits为错误的时候)

    – name: Stop tomcat
      shell: setsid /bin/sh -i -c "/opt/{{ child_project }}/bin/stop.sh"
      register: stop_result
      when: backup_path_result.stat.exists == True or yestady_backup_path_result.stat.exits == True   (多个条件的组合判断)

 

在命令行传递参数,然后在主机列表里添加密码切换用户的模块:

 – name: roll-back-today
      shell: /usr/bin/mv -f /opt/software/openresty/nginx/html/invault{{ ansible_date_time.date }} /opt/software/openresty/nginx/html/invault
      become: true
      become_method: su
      become_user: root

      when: backup_path_result.stat.exists == True

vim /etc/ansible/hosts

ansible-vault edit hosts  (另一种打开此文件的方式,由于里面存放密码所以对文件进行了加密,只有这样可以打开)

[web-a-group]
web-01 ansible_port=65522 ansible_host=10.0.1.11 ansible_become_user=root ansible_become_pass=UL1PFZoMctplTuW0
[web-b-group]
web-02 ansible_port=65522 ansible_host=10.0.2.11 ansible_become_user=root ansible_become_pass=UL1PFZoMctplTuW0

在剧本里修改文件权限的方法,直接在外边传递playbook的变量参数的值即可:

ssh centos@52.74.137.28 'ansible-playbook /home/centos/playbook/mainnet/wallet/release_wallet.yml –extra-vars "targets=web-a-group user=centos file_user=www" -f 10' 

src: /home/centos/release_static/mainnet/invault-wallet.tar.gz
        dest: /opt/software/openresty/nginx/html/invault
        owner: "{{ file_user }}"
        group: "{{ file_user }}"
      become: true
      become_method: su
      become_user: root

 

 (当在playbook里新建或者解压文件时,切换到root操作,root就有权限来修改文件的所有者,只需在执行playbook时传递一个参数来更改文件的所有者就可以了)

 

 

 

 

迭代:with_items

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

对迭代项的引用,固定变量名为”item“

要在task中使用with_items给定要迭代的元素列表

列表格式:

字符串

字典

下面为一个案例:

– name: add several users

user: name={{  item  }} state=present groups=wheel  (创建用户,及指定组名,引用变量元素列表)

with_items:  (此表头只是就是这样写,写成其他的变量则执行会失败)

– testuser1

– testuser2

使用迭代让其循环创建用户。

迭代嵌套子变量

捕获玩

 

roles

将各个命令模块分散单独存放到一个剧本中,然后组和调用:

这样做的目的使效率大大提高,重复的的命令模块不用再次编写,只需要调用就可以了。

以下具体案例:(官方建议目录在/etc/ansible/下就有roles文件夹,不强制要求使用)

1 .目录结构:找个文件夹例如:f1 在里面建一个roles的文件夹

2 .进到roles目录下在创建目录nginx(此目录为安装此应用单独分开的)

3 . 进到nginx目录下在创建各个使用模块的目录:tasks和templates的目录

4 . 进到tasks目录下去创建单个模块命令的playbook:

如:    group.yml;       user.yml;      yum.yml ;

vim group.yml

–  name: create  group    (模块功能描述)

group:   name=nginx   (创建组)

第一个创建组的剧本就写好了,如上所示来创建第二个命令模块,第三个命令模块,等 ;每个命令模块只执行一条命令。

vim user.yml

–  name: create  user

user:  name=nginx  group=nginx  system=yes (system=yes的意思是创建系统账号,而不是普通账号)

此创建用户的剧本也写完了。

vim

–  name:  install  package

yum:  name=nginx

此安装包的剧本也写完了。

vim start  service

–   name: start service

service:  name=nginx  enabled=yes

此启动服务的剧本也写完了

最后在创建总的剧本来决定这些子剧本执行的先后顺序:

vim  main.yml

– include:  group.yml

– include:  user.yml

– include:  yum.yml

– include:  start.yml

此总剧本也创建完了。

最后创建一个playbook调用总剧本,此playbook应该和roles文件夹是平级的。

返回到f1文件夹下,创建nginx的安装playbook

vim  nginxrole.yml

–  –  –

–   hosts:  all

retome_user:  root

roles:

–  role:  nginx

开始测试此剧本,安装nginx服务。到此roles已经完成了。

此目录结构为:

.
├── nginxroles.yml
└── roles
└── nginx
└── tasks
├── group.yml
├── main.yml
├── start.yml
├── user.yml
└── yum.yml

 

 

 

 

 

 

 

本文来自投稿,不代表Linux运维部落立场,如若转载,请注明出处:http://www.178linux.com/99680

(0)
无所谓无所谓
上一篇 2018-05-28 21:09
下一篇 2018-05-28

相关推荐

  • 马哥的第一节课

    雄关漫到
    跋山涉水

    Linux笔记 2018-07-22
  • 文本处理——sed初步

    sed是一种流编辑器,它一次处理一行内容。
    处理时,把当前处理的行存储在临时缓冲区(pattern space),同时输出到屏幕,接着用sed命令处理缓冲区中的内容,接着读取下一行,这样不断重复,直到文件末尾。

    Linux笔记 2018-04-20
  • 配置本地及共享yum源

    使用yum安装软件 命令: 查看软件包 yum list  all        –列出yum源仓库里面的所有可用的安装包 yum  list installed   –列出所有已经安装的安装包 yum  list  available     –列出没有安装的安装包 安装软件 yum  install  software…

    2018-05-01
  • 如何在Linux系统上获取命令的帮助信息,请详细列出,并描述man文档的章节是如何划分的。

    #help man命令为Linux下的帮助指令,通过man命令可以哈看Linux中对应的命令手册,划分如下 1:用户命令章节 2:系统调用命令章节 3:c库调用章节 4:设备及特殊文件 5:配置文件的格式以及相关参数 6:游戏 7:杂项 8:管理命令

    Linux笔记 2018-05-13
  • shell脚本

    shell脚本的练习题

    2018-04-18
  • shell 编程基础

    命令错误 后面的命令继续执行 语法错误 后面的命令不执行 bash -n 检查语法错位 bash -x 查看脚本的执行过程 【排错】 ++文件属性上的 显示是嵌套命令 变量 引用变量要加$符号 echo 是显示字符串的 如果命令能识别变量 可以不加$ 识别不了就压加$ 变量尽量用“ ”引起来 保留里面的格式 name =`whoami` echo &#822…

    Linux笔记 2018-04-15