自动化运维工具-----Ansible playbook详解

目录

一.Ansible playbook简介

二.Ansible playbook使用场景

三.Ansible playbook格式

格式简介

核心元素

基本组件

variables变量

模板templates

条件测试

字典

角色定制


一.Ansible playbook简介

  • playbook是ansible用于配置,部署,和管理被节点的剧本
  • 通过playbook的详细描述,执行其中的一些列tasks,可以让远端的主机达到预期的状态。playbook就像ansible控制器给被控节点列出的一系列to-do-list,而且被控节点必须要完成
  • playbook顾名思义,即剧本,现实生活中演员按照剧本表演,在ansible中,这次由被控计算机表演,进行安装,部署应用,提供对外的服务等,以及组织计算机处理各种各样的事情。

二.Ansible playbook使用场景

  • 执行一些简单的任务,使用ad-hoc命令可以方便的解决问题,但是有时一个设施过于复杂,需要大量的操作的时候,执行的ad-hoc命令是不合适的,这时候最好使用playbook。
  • 就像执行shell命令与写shell脚本一样,也可以理解为批处理任务,不过playbook有自己的语法格式
  • 使用playbook可以方便的重复使用这些代码,可以移植到不同的机器上面,像函数一样,最大化的利用代码。在你使用Ansible的过程中,你也会发现,你所处理的大部分操作都是编写playbook。可以把常见的应用都编写playbook,之后管理服务器会变得很简单。

三.Ansible playbook格式

格式简介

  • playbook由YAML语言编写。
  • YAML( /ˈjæməl/ )参考了其他多种语言,包括:XML、C语言、Python、Perl以及电子邮件格式RFC2822,Clark Evans在2001年5月在首次发表了这种语言,另外Ingy döt Net与OrenBen-Kiki也是这语言的共同设计者。
  • YAML格式是类似JSON的文件格式。YAML用于文件的配置编写,JSON多用于开发设计。
  • YAML的格式如下:

1.文件的第一行应该以“---”(三个连字符)开始,表明YAML文件的开始。

2.在同一行中,#之后的内容表示注释,类似于shell,python和ruby。

3.YAML中的列表元素以“-”开头并且跟着一个空格。后面为元素内容。

4.同一个列表之中的元素应该保持相同的缩进,否则会被当做错误处理。

5.play中hosts、variables、roles、tasks等对象的表示方法都是以键值中间以“:”分隔表示,并且“:”之后要加一个空格。

  • 安装并且运行mysql服务,实例如下:

---
- hosts: node1
  remote_user: root
  tasks:

    - name: install mysql-server package
      yum: name=mysql-server state=present
    - name: starting mysqld service
      service: name=msyql state=started

以上面为例,文件名应该以 .yml 结尾

host部分:使用hosts指示使用哪个主机或者主机组来运行下面的tasks,每个playbooks都必须指定hosts,host也可以使用通配符格式。主机或者主机组在inventorry清单中指定,可以使用系统默认的/etc/ansible/hosts,也可以自己编辑,在运行的时候加上-i 选项指定清单的位置。在运行清单文件的时候, --list-hosts选项会显示哪些主机将会参与执行task的过程中。

remote_user:指定远端主机的哪个用户来登录远端系统,在远端系统执行task的用户,可以任意指定,也可以使用sudo,但是用户必须要有执行相应的task权限。

tasks:指定远端主机将要执行的一系列动作。tasks的核心为ansible的模块,前面已经提到模块的用法,tasks包含name和要执行的模块,name是可选的,只是为了便于用户阅读,模块是必须的,同时也要给予模块相应的参数

  • 使用ansible-playbook运行playbook文件,得到的输出信息中,信息内容为JSON格式,并且由不同的颜色组成,便于识别。

  • 绿色代表执行成功,系统保持原样。黄色代表系统状态发生改变。红色代表执行失败,显示错误输出。

核心元素

  • Hosts:主机组
  • Tasks:任务列表
  • Variables:变量,设置方式有四种
  • Templates:包含了模块语法的文本文件
  • Handlers:由特定条件触发的任务。

基本组件

  • playbooks配置文件的基础组件
Hosts:运行指定任务的目标主机
remoute_user:在远程主机上执行任务的用户;
sudo_user:
tasks:任务列表

tasks的具体格式:

tasks:
  - name: TASK_NAME
    module: arguments
    notify: HANDLER_NAME
    handlers:
  - name: HANDLER_NAME
    module: arguments

##模块,模块参数:
格式如下:
    (1)action: module arguments
     (2) module: arguments
注意:shell和command模块后直接加命令,而不是key=value类的参数列表


handlers:任务,在特定条件下触发;接受到其他任务的通知时被触发;
  • 在某个任务的状态在运行之后为changed时,可以通过“notify”通知相应的handlers;

  • 任务可以通过“tags”打标签,而后可以在ansible-playbook命令上使用-t指定进行调用;

定义一个安装nginx的playbook

  • 定义playbook

[root@ansible ~]# cd /etc/ansible/
[root@ansible ansible]# ls
ansible.cfg  hosts  nginx.yml  roles
[root@ansible ansible]# cat nginx.yml 
---
- hosts: web
  remote_user: root
  tasks:
  
    - name: install nginx        ##安装模块,需要在被控主机里加上nginx的源
      yum: name=nginx state=present
    - name: copy nginx.conf    ##复制nginx的配置文件过去,需要在本机的/tmp目录下编辑nginx.conf
      copy: src=/tmp/nginx.conf dest=/etc/nginx/nginx.conf backup=yes
      notify: reload    #当nginx.conf发生改变时,通知给相应的handlers
      tags: reloadnginx    #打标签
    - name: start nginx service    #服务启动模块
      service: name=nginx state=started
      tags: startnginx    #打标签

  handlers:
    - name: reload
      service: name=nginx state=restarted
[root@ansible ansible]# 
  • 测试运行结果

[root@ansible ansible]# ansible-playbook nginx.yml 

PLAY [web] *********************************************************************

TASK [Gathering Facts] *********************************************************
ok: [192.168.43.110]
ok: [192.168.43.109]

TASK [install nginx] ***********************************************************
changed: [192.168.43.109]
changed: [192.168.43.110]

TASK [copy nginx.conf] *********************************************************
ok: [192.168.43.110]
ok: [192.168.43.109]

TASK [start nginx service] *****************************************************
changed: [192.168.43.110]
changed: [192.168.43.109]

PLAY RECAP *********************************************************************
192.168.43.109             : ok=4    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
192.168.43.110             : ok=4    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

##查看被控主机上的nginx服务
[root@ansible ansible]# ansible web -m shell -a 'netstat -natp | grep nginx'
192.168.43.110 | CHANGED | rc=0 >>
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      2720/nginx: master  
192.168.43.109 | CHANGED | rc=0 >>
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      2776/nginx: master  
[root@ansible ansible]# 
  • 测试标签,我们在nginx.yml中已经打了标签,所以可以直接应用标签,但是需要先将服务关闭,在运行剧本并且引用标签

#关闭被控节点的nginx服务
[root@ansible ansible]# 
[root@ansible ansible]# ansible web -m shell -a 'systemctl stop nginx'
192.168.43.110 | CHANGED | rc=0 >>

192.168.43.109 | CHANGED | rc=0 >>



#重新加载剧本并且应用标签
[root@ansible ansible]# ansible-playbook nginx.yml -t startnginx

PLAY [web] ************************************************************************

TASK [Gathering Facts] ************************************************************
ok: [192.168.43.109]
ok: [192.168.43.110]

TASK [start nginx service] ********************************************************
changed: [192.168.43.109]
changed: [192.168.43.110]

PLAY RECAP ************************************************************************
192.168.43.109             : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
192.168.43.110             : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

[root@ansible ansible]# 


  • 测试notify,在nginx.yml中做了一个notify,当配置文件改变时,条件就被戳发,我们修改一下/tem/nginx.conf的监听端口

自动化运维工具-----Ansible playbook详解_第1张图片

##使用reloadnginx标签,重新加载剧本
[root@ansible ansible]# ansible-playbook nginx.yml -t reloadnginx

PLAY [web] **************************************************************************************************

TASK [Gathering Facts] **************************************************************************************
ok: [192.168.43.109]
ok: [192.168.43.110]

TASK [copy nginx.conf] **************************************************************************************
changed: [192.168.43.109]
changed: [192.168.43.110]

RUNNING HANDLER [reload] ************************************************************************************
changed: [192.168.43.110]
changed: [192.168.43.109]

PLAY RECAP **************************************************************************************************
192.168.43.109             : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
192.168.43.110             : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

[root@ansible ansible]# ansible web -m shell -a 'netstat -natp | grep nginx'
192.168.43.109 | CHANGED | rc=0 >>
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      38417/nginx: master 
tcp        0      0 0.0.0.0:8080            0.0.0.0:*               LISTEN      38417/nginx: master 
192.168.43.110 | CHANGED | rc=0 >>
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      38281/nginx: master 
tcp        0      0 0.0.0.0:8080            0.0.0.0:*               LISTEN      38281/nginx: master 
[root@ansible ansible]# 

variables变量

  • variables变量有四种定义方法。如下:

facts:可以直接调用

  • ansible中有setup模块,这个模块就是通过facts组件来实现的。这里的variables也可以直接调用facts组件的facters我们可以使用setup模块来获取,然后直接放入我们的剧本之中调用即可

用户自定义变量

  • 自定义变量有两种方式
  • 通过命令行传入
ansible-playbook命令行中的 -e VARS,--extra-vars=VARS,这样就可以直接把自定义的变量传入
  • 在playbook中定义变量
##在playbook中定义变量如下:
vars:
  - var1: value1
  -- var2: value2

​​​​​​​使用playbook定义变量,实例如下

  • 定义剧本,将ngixn替换成变量
[root@ansible ansible]# ls
ansible.cfg  hosts  nginx.yml  roles
[root@ansible ansible]# cat nginx.yml 
---
- hosts: web
  remote_user: root
  tasks:
  
    - name: install {{ rpmname }}
      yum: name={{ rpmname }} state=present
    - name: copy {{ rpmname }}.conf
      copy: src=/tmp/{{ rpmname }}.conf dest=/etc/{{ rpmname }}/{{ rpmname }}.conf backup=yes
      notify: reload
      tags: reload{{ rpmname }}
    - name: start {{ rpmname }} service
      service: name={{ rpmname }} state=started
      tags: start{{ rpmname }}

  handlers:
    - name: reload
      service: name={{ rpmname }} state=restarted
[root@ansible ansible]# 
  • 拷贝配置文件
#因为我们要安装keepalived服务,所以将keepalived的配置文件拷贝到/tmp目录下
#可以在本机上下载keepalived服务

[root@server ansible]# cp /etc/keepalived/keepalived.conf /tmp/keepalived.conf
  • 运行剧本,变量有命令行传入
[root@ansible ansible]# ls
ansible.cfg  hosts  nginx.yml  roles

##-e表示编辑yml内部变量
[root@ansible ansible]# ansible-playbook nginx.yml -e rpmname=keepalived

PLAY [web] ************************************************************************

TASK [Gathering Facts] ************************************************************
ok: [192.168.43.110]
ok: [192.168.43.109]

TASK [install keepalived] *********************************************************
changed: [192.168.43.109]
changed: [192.168.43.110]

TASK [copy keepalived.conf] *******************************************************
ok: [192.168.43.109]
ok: [192.168.43.110]

TASK [start keepalived service] ***************************************************
changed: [192.168.43.110]
changed: [192.168.43.109]

PLAY RECAP ************************************************************************
192.168.43.109             : ok=4    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
192.168.43.110             : ok=4    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

[root@ansible ansible]# 
  • 修改剧本直接定义变量,同样我们可以直接在剧本中定义好变量,这样就不需要再通过命令行传入。以后想要俺咋混给不同的服务,直接在剧本内部修改变量即可

自动化运维工具-----Ansible playbook详解_第2张图片

  • 运行定义过变量的剧本

自动化运维工具-----Ansible playbook详解_第3张图片

通过roles传递变量

  • 当说到roles时具体说明

Host Inventory

  • 我们也可以在主机清单中定义,方法如下:
#向不同的主机传递不同的变量
IP/HOSTNAME varaiable=value var2=value2

#向组中的主机传递相同的变量
[groupname:vars]
variable=value

模板templates

  • 模板是一个文本文件,嵌套有脚本(使用模板编程语言编写)
  • Jinja2是python的一种模板语言,以Django的模板语言为原本
  • 该模板支持:

 字符串:使用单引号或双引号;
  数字:整数,浮点数;
  列表:[item1, item2, ...]
  元组:(item1, item2, ...)
  字典:{key1:value1, key2:value2, ...}
  布尔型:true/false
  算术运算:
    +, -, *, /, //, %, **
  比较操作:
    ==, !=, >, >=, <, <=
  逻辑运算:
    and, or, not

  • 通常模板都是通过引用变量来运用的

实例如下

  • 定义模板
[root@ansible ansible]# mv /tmp/nginx.conf /tmp/nginx.conf.j2
[root@ansible ansible]# vi /tmp/nginx.conf.j2 
ddworker_processes {{ ansible_processor_vcpus }};
listen {{ nginxport }};

user  nginx;
worker_processes  1;

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


events {
    worker_connections  1024;
}


http {
    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;
server {

listen 8888;
}
  • 修改剧本,如下两个地方

自动化运维工具-----Ansible playbook详解_第4张图片

  • 运行剧本
[root@ansible ansible]# ansible-playbook nginx.yml -t reloadnginx

PLAY [web] ***********************************************************************************

TASK [Gathering Facts] ***********************************************************************
ok: [192.168.43.109]
ok: [192.168.43.110]

TASK [copy nginx.conf] ***********************************************************************
ok: [192.168.43.109]
ok: [192.168.43.110]

PLAY RECAP ***********************************************************************************
192.168.43.109             : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
192.168.43.110             : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

[root@ansible ansible]# 

条件测试

  • when语句:在task中使用,jinja2的语法格式
tasks:
- name: install conf file to centos7
  template: src=files/nginx.conf.c7.j2
  when: ansible_distribution_major_version == "7"
- name: install conf file to centos6
  template: src=files/nginx.conf.c6.j2
  when: ansible_distribution_major_version == "6"
  • 循环:迭代,需要重复执行的任务,对于迭代项的引用,固定变量名为“item”,而后在task中使用with_items给定的迭代的元素列表
tasks:
- name: unstall web packages
  yum: name={{ item }} state=absent
  with_items:
  - httpd
  - php
  - php-mysql

字典

  • ansible playbook也支持字典功能,举例如下:
- name: install some packages
  yum: name={{ item }} state=present
  with_items:
    - nginx
    - memcached
    - php-fpm
- name: add some groups
  group: name={{ item }} state=present
  with_items:
    - group11
    - group12
    - group13
- name: add some users
  user: name={{ item.name }} group={{ item.group }} state=present
  with_items:
    - { name: 'user11', group: 'group11' }
    - { name: 'user12', group: 'group12' }
    - { name: 'user13', group: 'group13' }

角色定制

简介

  • 对于以上所有方式有个缺点就是无法实现同时部署web、database、keepalived等不同服务或者不同服务器组合不同的应用就需要写多个yaml文件,很难实现灵活的调用
  • roles用于层次性,结构化地组织playbook。roles能够根据层次结果自动装载变量文件、tasks以及handlers等。
  • 要使用roles只需要在playbook中使用include指令即可。
  • 简单来讲,roles就是通过分别将变量(vars)、文件(files)、任务(tasks)、模块(modules)以及处理器(handlers)放置于单独的目录中,并且可以便捷的include它们地一种机制。
  • 角色一般用于基于主机构建服务的场景中,但是也可以用于构建守护进程等场景中。

角色集合

​​​​​​​roles/

mysql/:mysql服务的yml文件

httpd/:apached服务的yml文件

nginx/:nginx服务的yml文件

files/:存储由copy或者script等模块调用的文件或者脚本;

tasks/:此目录中至少应该有一个名为main.yml的文件,用于定义各个task;其他文件需要由main.yml进行包含调用;

handlers/:此目录中至少应该有一个名为main.yml的文件,用于定义各个handler;其他文件需要由main.yml进行包含调用;

vars/:此目录至少应该有一个名为main,yml的文件,用于定义各个variable;其他的文件需要由main.yml进行包含调用;

templates/:存储由templates模块调用的模板文件;

meta/:此目录中至少应该有一个名为main.yml的文件,定义当前角色的特殊设定以及依赖关系,其他文件需要由main.yml进行包含调用;

default/:此目录至少应该有一个名为main.yml的文件,用于设定默认变量;

角色定制实例

  • 在roles目录下生成对应的目录结构
[root@ansible ansible]# ls
ansible.cfg  hosts  nginx.yml  roles
[root@ansible ansible]# cd roles/
[root@ansible roles]# ls
[root@ansible roles]# mkdir -pv ./{nginx,mysql,httpd}/{files,templates,vars,tasks,handlers,meta,default}
[root@ansible roles]# yum install tree -y &> /dev/null
[root@ansible roles]# tree 
.
├── httpd
│   ├── default
│   ├── files
│   ├── handlers
│   ├── meta
│   ├── tasks
│   ├── templates
│   └── vars
├── mysql
│   ├── default
│   ├── files
│   ├── handlers
│   ├── meta
│   ├── tasks
│   ├── templates
│   └── vars
└── nginx
    ├── default
    ├── files
    ├── handlers
    ├── meta
    ├── tasks
    ├── templates
    └── vars

24 directories, 0 files
[root@ansible roles]# 
  • 定义配置文件
[root@ansible nginx]# pwd
/etc/ansible/roles/nginx
[root@ansible nginx]# ls
default  files  handlers  meta  tasks  templates  vars
[root@ansible nginx]# ls tasks/
main.yml
[root@ansible nginx]# cat tasks/main.yml 
- name: cp
  copy: src=nginx-1.18.0-1.el7.ngx.x86_64.rpm dest=/tmp/nginx-1.18.0-1.el7.ngx.x86_64.rpm
- name: install
  yum: name=/tmp/nginx-1.18.0-1.el7.ngx.x86_64.rpm state=latest
- name: conf
  template: src=nginx.conf.j2 dest=/etc/nginx/nginx.conf
  tags: nginxconf
  notify: new conf to reload
- name: start service
  service: name=nginx state=started enabled=true
[root@ansible nginx]# 
  • 放置需要的文件到指定目录下

[root@ansible ]# cp /var/cache/yum/x86_64/7/nginx/packages/nginx-1.18.0-1.el7.ngx.x86_64.rpm /etc/ansible/roles/nginx/files/
[root@ansible ]# cp /tmp/nginx.conf.j2 /etc/ansible/roles//nginx/templates/
[root@ansible nginx]# tree
.
├── default
├── files
│   └── nginx-1.18.0-1.el7.ngx.x86_64.rpm
├── handlers
├── meta
├── tasks
│   └── main.yml
├── templates
│   └── nginx.conf.j2
└── vars

7 directories, 3 files
[root@ansible nginx]# 
  • 修改变量文件
[root@ansible nginx]# ls
default  files  handlers  meta  tasks  templates  vars
[root@ansible nginx]# cat vars/main.yml 
nginxport: 9999
[root@ansible nginx]# 
  • 定义handlers文件
[root@ansible nginx]# cat handlers/main.yml 
- name: new conf to reload
  service: name=nginx state=restarted
[root@ansible nginx]# 
  • 定义剧本文件
[root@ansible ansible]# pwd
/etc/ansible
[root@ansible ansible]# cat roles.yml 
- hosts: web
  remote_user: root
  roles:
    - nginx
[root@ansible ansible]# 
  • 启动服务
[root@ansible ansible]# ansible-playbook roles.yml 

PLAY [web] *********************************************************************

TASK [Gathering Facts] *********************************************************
ok: [192.168.43.110]
ok: [192.168.43.109]

TASK [nginx : cp] **************************************************************
changed: [192.168.43.110]
changed: [192.168.43.109]

TASK [nginx : install] *********************************************************
ok: [192.168.43.110]
ok: [192.168.43.109]

TASK [nginx : conf] ************************************************************
changed: [192.168.43.110]
changed: [192.168.43.109]

TASK [nginx : start service] ***************************************************
changed: [192.168.43.109]
changed: [192.168.43.110]

RUNNING HANDLER [nginx : new conf to reload] ***********************************
changed: [192.168.43.110]
changed: [192.168.43.109]

PLAY RECAP *********************************************************************
192.168.43.109             : ok=6    changed=4    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
192.168.43.110             : ok=6    changed=4    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

[root@ansible ansible]# ansible web -m shell -a 'netstat -natp | grep nginx'
192.168.43.110 | CHANGED | rc=0 >>
tcp        0      0 0.0.0.0:9999            0.0.0.0:*               LISTEN      43345/nginx: master 
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      43345/nginx: master 
192.168.43.109 | CHANGED | rc=0 >>
tcp        0      0 0.0.0.0:9999            0.0.0.0:*               LISTEN      46442/nginx: master 
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      46442/nginx: master 
[root@ansible ansible]# 
[root@ansible nginx]# pwd
/etc/ansible/roles/nginx
[root@ansible nginx]# tree
.
├── default
├── files
│   └── nginx-1.18.0-1.el7.ngx.x86_64.rpm
├── handlers
│   └── main.yml
├── meta
├── tasks
│   └── main.yml
├── templates
│   └── nginx.conf.j2
└── vars
    └── main.yml

7 directories, 5 files
[root@ansible nginx]# cat handlers/main.yml 
- name: new conf to reload
  service: name=nginx state=restarted
[root@ansible nginx]# cat tasks/main.yml 
- name: cp
  copy: src=nginx-1.18.0-1.el7.ngx.x86_64.rpm dest=/tmp/nginx-1.18.0-1.el7.ngx.x86_64.rpm
- name: install
  yum: name=/tmp/nginx-1.18.0-1.el7.ngx.x86_64.rpm state=latest
- name: conf
  template: src=nginx.conf.j2 dest=/etc/nginx/nginx.conf
  tags: nginxconf
  notify: new conf to reload
- name: start service
  service: name=nginx state=started enabled=true
[root@ansible nginx]# cat templates/nginx.conf.j2 

user  nginx;
worker_processes  1;

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


events {
    worker_connections  1024;
}


http {
    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;
server {

listen  {{ nginxport }};
}

    include /etc/nginx/conf.d/*.conf;
}
[root@ansible nginx]# cat vars/main.yml 
nginxport: 9999
[root@ansible nginx]# 

 

你可能感兴趣的:(自动化运维工具)