ansible笔记(19):循环(一)

在使用ansible的过程中,我们经常需要处理一些返回信息,而这些返回信息中,通常可能不是单独的一条返回信息,而是一个信息列表,如果我们想要循环的处理信息列表中的每一条信息,我们该怎么办呢?这样空口白话的描述有些费力,不如通过一些小示例,结合场景来描述。

假设,我的清单配置如下

10.1.1.60
test70.zsythink.net ansible_host=10.1.1.70
test71 anisble_host=10.1.1.71

[testA]
test60 ansible_host=10.1.1.60
test61 ansible_host=10.1.1.61

[testB]
test70 ansible_host=10.1.1.70

[test:children]
testA
testB

如果我想要获取到清单中所有未分组的主机的主机名,则可以执行如下命令

# ansible test70 -m debug -a "msg={{groups.ungrouped}}"
test70 | SUCCESS => {
    "changed": false,
    "msg": [
        "10.1.1.60",
        "test70.zsythink.net",
        "test71"
    ]
}

从返回信息可以看出,一共有3个未分组主机,上例的返回信息中就不只有一条信息记录,如果我们想要获取到上述返回信息中的第二条信息,则可以使用如下方法

# ansible test70 -m debug -a "msg={{groups.ungrouped[1]}}"
test70 | SUCCESS => {
    "changed": false,
    "msg": "test70.zsythink.net"
}

但是问题是,我们通常不能确定返回信息有几条,我们可能需要循环的处理返回信息中的每一条信息,那么怎么才能循环处理返回信息中的每一条信息呢?示例playbook如下

---
- hosts: test70
  remote_user: root
  gather_facts: no
  tasks:
  - debug:
      msg: "{{item}}"
    with_items: "{{groups.ungrouped}}"

上例中,我们通过”{{groups.ungrouped}}”获取到了未分组的返回信息,并使用关键字”with_items”接收了返回信息,然后使用debug模块输出了名为”item”变量的变量值,聪明如你一定已经明白了,”with_items”关键字会把返回的列表信息自动处理,将每一条信息单独放在一个名为”item”的变量中,我们只要获取到名为”item”变量的变量值,即可循环的获取到列表中的每一条信息,所以,上例中返回信息中的每一条信息都会循环的被debug模块处理,执行上述playbook后结果如下

TASK [debug] *********************************************
ok: [test70] => (item=10.1.1.60) => {
    "changed": false,
    "item": "10.1.1.60",
    "msg": "10.1.1.60"
}
ok: [test70] => (item=test70.zsythink.net) => {
    "changed": false,
    "item": "test70.zsythink.net",
    "msg": "test70.zsythink.net"
}
ok: [test70] => (item=test71) => {
    "changed": false,
    "item": "test71",
    "msg": "test71"
}

从执行playbook的结果可以看出,debug模块对每条信息都单独输出了一次,而不是三条信息同时一次输出,由于对应play是针对test70主机操作的,所以上例中debug模块的三次操作都是在test70主机上进行的,只不过debug模块只是输出信息,并不会对test70主机做出什么实际的改动而已,通过上述方法,就能够符合我们循环操作的要求了。

上例中,我们使用的是返回值中的信息,那么我们能不能自定义一个列表,然后循环使用列表中的值呢?必须能的,示例如下

---
- hosts: test70
  remote_user: root
  gather_facts: no
  tasks:
  - debug:
      msg: "{{item}}"
    with_items:
    - 1
    - 2
    - 3

如上例所示,我们自定义了3个值,分别是1、2、3,debug模块会循环的输出这三个值。

或者我们换一种写法,如下写法与上述写法的效果完全相同。

---
- hosts: test70
  remote_user: root
  gather_facts: no
  tasks:
  - debug:
      msg: "{{item}}"
    with_items: [ 1, 2, 3 ]

我们还能够自定义稍微复杂些的列表,示例如下

---
- hosts: test70
  remote_user: root
  gather_facts: no
  tasks:
  - debug:
      msg: "{{item.test1}}"
    with_items:
    - { test1: a, test2: b }
    - { test1: c, test2: d }

上例中自定义列表中的每一个条目都是一个对象,我们可以通过对象的属性对应的”键”,获取到对应的”值”,如上例所示,第一个条目的test1键对应的值是a,第二个条目的test1键对应的值是c,所以执行上例playbook以后,”a”和”c”会被输出。

学会使用循环,能够让我们事半功倍。

比如,在没有学会使用循环之前,如果想要在同一主机中创建四个文件,那么你可能会编写如下playbook

---
- hosts: test70
  remote_user: root
  gather_facts: no
  tasks:
  - file:
      path: "/opt/a"
      state: touch
  - file:
      path: "/opt/b"
      state: touch
  - file:
      path: "/opt/c"
      state: touch
  - file:
      path: "/opt/d"
      state: touch

我们重复的书写了file模块4次,其实每次只是改变了file模块的path参数的值而已,如果使用循环,上例的playbook则可以改写成如下

---
- hosts: test70
  remote_user: root
  gather_facts: no
  vars:
    dirs:
    - "/opt/a"
    - "/opt/b"
    - "/opt/c"
    - "/opt/d"
  tasks:
  - file:
      path: "{{item}}"
      state: touch
    with_items: "{{dirs}}"

正如上述示例所示,重复的操作越多,使用循环则越方便。

那么我们再来看一个小示例,这个示例与循环没有直接关系,但是可以引出我们要说的话题,示例playbook如下:

---
- hosts: test70
  gather_facts: no
  tasks:
  - shell: "ls /opt"
    register: returnvalue
  - debug:
      var: returnvalue

看到上例,你一定不会觉得陌生,在之前总结变量的文章中,我们已经总结过”register”的用法,我们可以通过”register”获取到模块执行后的”返回值”信息,如果你忘记了”register”的用法,请回顾前文,上例中,我们将shell模块执行后的返回值写入了名为”returnvalue”的变量中,然后使用debug模块输出了”returnvalue”变量的值,上述playbook执行后结果如下

TASK [debug] ******************
ok: [test70] => {
    "returnvalue": {
        "changed": true,
        "cmd": "ls /opt",
        "delta": "0:00:01.006690",
        "end": "2018-07-12 17:43:01.445936",
        "failed": false,
        "rc": 0,
        "start": "2018-07-12 17:43:00.439246",
        "stderr": "",
        "stderr_lines": [],
        "stdout": "rh",
        "stdout_lines": [
            "rh"
        ]
    }
}

可以看到shell模块的返回值信息如上,但是,上例并没有借助循环重复调用shell模块,如果使用循环多次调用shell模块执行不同的命令,返回信息的格式还会和原来一样吗?我们来实验一下,我们将上例的playbook修改如下

---
- hosts: test70
  gather_facts: no
  tasks:
  - shell: "{{item}}"
    with_items:
    - "ls /opt"
    - "ls /home"
    register: returnvalue
  - debug:
      var: returnvalue

如上例所示,我们使用循环重复调用了shell模块两次,分别执行了两条命令,然后将shell模块的返回值存放到了”returnvalue”变量中,最后使用debug模块输出了”returnvalue”变量的值,那么当我们使用循环时,返回值信息会和原来一样么?上例的playbook执行后debug模块的输出如下

未分类

细心如你一定发现了,当使用了循环以后,每次shell模块执行后的返回值都会放入一个名为”results”的序列中,其实,”results”也是一个返回值,当模块中使用了循环时,模块每次执行的返回值都会追加存放到”results”这个返回值中,所以,我们可以通过”results”关键字获取到每次模块执行后的返回值,示例如下

---
- hosts: test70
  gather_facts: no
  tasks:
  - shell: "{{item}}"
    with_items:
    - "ls /opt"
    - "ls /home"
    register: returnvalue
  - debug:
      msg: "{{item.stdout}}"
    with_items: "{{returnvalue.results}}"

上例中,我们先使用循环重复的调用了shell模块,然后将shell模块每次执行后的返回值注册到了变量”returnvalue”中,之后,在使用debug模块时,通过返回值”results”获取到了之前每次执行shell模块的返回值(shell每次执行后的返回值已经被放入到item变量中),最后又通过返回值”stdout”获取到了每次shell模块执行后的标准输出,你可以执行一下上例的playbook,执行后,输出结果的msg关键字对应的值就是每次shell模块执行后的标准输出。

你可能还会看到有的朋友使用如下方法输出”{{returnvalue.results}}”列表中的信息

---
- hosts: test70
  gather_facts: no
  tasks:
  - shell: "{{item}}"
    with_items:
    - "ls /opt"
    - "ls /home"
    register: returnvalue
  - debug:
      msg:
       "{% for i in returnvalue.results %}
          {{ i.stdout }}
        {% endfor %}"

你一定看出来了,上例使用了一个for循环遍历了” returnvalue.results”列表,上例中for循环的语法为jinja2语言中的for循环语法,jinja2是一种模板语言,jinja2是一个基于python的模板引擎,所以,在ansible中,我们可以使用jinja2编写模板文件,然后再根据模板文件来生成配置文件,jinja2中也有一些控制语句结构,比如for循环,上例中使用的就是jinja2语法中的for循环,如果你执行了上例的playbook,你会发现,debug模块只执行了一次,msg中对应的信息是所有shell模块执行后的stdout返回值,因为debug模块只是输出了经过jinja2的for循环处理过的信息而已,debug模块并没有因为for循环而被重复的调用。如果你对jinja2的语法不是很了解,不用在意,此处只是展示一个小示例,当我们总结模板的用法时,再去了解jinja2相应的使用方法也不迟,所以看不懂也不用纠结。

这篇文章就总结到这里,希望能够对你有所帮助~

ansible笔记(18):变量(五)

ansible中还有一些内置变量可供我们使用,当然,这些内置变量的变量名是被ansible保留的,我们定义变量时不能使用这些变量名。

内置变量ansible_version

先从一个简单的内置变量说起,比如,我们可以通过内置变量ansible_version获取到ansible的版本号,示例命令如下

ansible test70 -m debug -a "msg={{ansible_version}}"

内置变量hostvars

除了ansible_version,还有一些非常有用的内置变量。比如内置变量hostvars

hostvars可以帮助我们在操作当前主机时获取到其他主机中的信息。

假设,我想要在操作test70主机时获取到test71主机中的facts信息,我该怎么办呢?示例如下

---
- name: "play 1: Gather facts of test71"
  hosts: test71
  remote_user: root

- name: "play 2: Get facts of test71 when operating on test70"
  hosts: test70
  remote_user: root
  tasks:
  - debug:
      msg: "{{hostvars['test71'].ansible_ens35.ipv4}}"

上例中有两个play,第一个play针对test71主机执行,但是第一个play中没有显式指定任何task(后文会解释原因),第二个play针对test70主机执行,在第二个play中只有一个task,即使用debug模块,输出了test71主机中的ens35网卡的IP信息,如你所见,我们可以借助hostvars在操作当前主机时输出其他主机中的facts信息,上例中使用hostvars加上清单中的主机名称再加上facts的key,即可获取到对应的facts信息,有了前文的总结作为基础,你一定想到了,上例中的msg的值改为如下写法也是可以的。

"{{hostvars.test71.ansible_ens35.ipv4}}"

上例中的第一个play中并没有任何的task,为什么还需要第一个play呢?如果你将上例的第一个play删除,只保留第二个play,运行时则会报错,这是因为,虽然第一个play中没有任何task,但是当第一个play执行时,默认会调用”[Gathering Facts]”任务,也就是说,默认会收集test71主机的facts信息,只有被收集过的facts信息才能被后面的play引用到,如果压根没有收集对应主机的facts信息,即使使用hostvars内置变量,也无法获取到对应主机的facts信息,我们来做个试验,我们可以直接把上例的第一个play从playbook中删除,也可以指明让第一个play不收集对应的facts信息,使用”gather_facts”关键字可以控制当前play是否收集对应主机的facts信息,示例如下:

---
- name: "play 1: Gather facts of test71"
  hosts: test71
  remote_user: root
  gather_facts: no

- name: "play 2: Get facts of test71 when operating on test70"
  hosts: test70
  remote_user: root
  tasks:
  - debug:
      msg: "{{hostvars['test71'].ansible_ens35.ipv4}}"

如上例所示,第一个play中的”gather_facts: no”表示设置当前play不收集对应主机的信息,运行上例playbook会报错,因为第二个play在操作test70时,无法获取到test71主机中的facts信息,原因是test71的facts信息并未被收集过,所以,调用其他主机的facts信息的前提是对应主机的facts信息已经被收集过。

其实,除了facts信息,我们还能够利用hostvars内置变量从别的主机中获取到其他类型的一些变量信息,比如,其他主机的注册变量、主机变量、组变量等信息,我们先来看一个获取其他主机的注册变量的小示例,如下:

---
- hosts: test71
  remote_user: root
  gather_facts: no
  tasks:
  - shell: "echo register_var_in_play1"
    register: shellreturn

- hosts: test70
  remote_user: root
  gather_facts: no
  tasks:
  - debug:
      msg: "{{hostvars.test71.shellreturn.stdout}}"

如上例所示,通过hostvars内置变量可以直接获取到其他主机中的注册变量,你一定发现了,注册变量并不用像facts信息那样需要事先收集,即可直接通过hostvars跨主机被引用到,同理,如果你在清单中为test71主机配置了主机变量,或者为test71主机所在的组配置了组变量,也是可以通过hostvars直接跨主机引用的,这里就不进行示例了,动手试试吧。

你可能会问,如果我直接在play中为当前主机定义一个变量,可以在之后的play中操作其他主机时被引用到吗?那么我们来做个实验,示例如下

---
- hosts: test71
  remote_user: root
  gather_facts: no
  vars:
    testvar: testvar_in_71
  tasks:
  - debug:
      msg: "{{testvar}}"

- hosts: test70
  remote_user: root
  gather_facts: no
  tasks:
  - debug:
      msg: "{{hostvars.test71.testvar}}"

在上例的第一个play中我们为test71主机定义了一个变量,变量名称为testvar,在第二个play中操作test70主机时,使用hostvars尝试引用test71主机中的变量,如果执行上述playbook则会报错,看来通过vars关键字定义的变量使用上例中的方法是无法被跨主机引用的,聪明如你,一定想到了解决方案,前一篇文章中,我们总结了怎样使用”set_fact”关键字定义变量,通过”set_fact”关键字定义的变量拥有类似”facts”信息的特性(如果不明白可以参考前文),所以,我们可以把”vars”关键字中定义的变量通过”set_fact”关键字去定义,这样这些变量就好像facts信息被收集过一样,能被之后的play引用到了,示例如下

---
- hosts: test71
  remote_user: root
  gather_facts: no
  tasks:
  - set_fact:
      testvar: "testvar_in_71"
  - debug:
      msg: "{{testvar}}"

- hosts: test70
  remote_user: root
  gather_facts: no
  tasks:
  - debug:
      msg: "{{hostvars.test71.testvar}}"

上例通过”set_fact”结合”hostvars”的方式,实现了跨play获取其他主机中的变量信息的功能,还是很方便的。

内置变量inventory_hostname

通过inventory_hostname变量可以获取到被操作的当前主机的主机名称,这里所说的主机名称并不是linux系统的主机名,而是对应主机在清单中配置的名称,假设我的清单配置如下

[test_group]
10.1.1.60
test70.zsythink.net ansible_host=10.1.1.70
test71 anisble_host=10.1.1.71

清单中配置了三个主机,第一个主机以IP的形式配置,第二个主机和第三个主机都以别名的方式配置,他们同属于test_group组。

那么我们使用内置变量inventory_hostname获取一下各个主机的对应的主机名,看看会返回什么,示例如下

# ansible test_group -m debug -a "msg={{inventory_hostname}}"
test70.zsythink.net | SUCCESS => {
    "changed": false, 
    "msg": "test70.zsythink.net"
}
10.1.1.60 | SUCCESS => {
    "changed": false, 
    "msg": "10.1.1.60"
}
test71 | SUCCESS => {
    "changed": false, 
    "msg": "test71"
}

从返回信息可以看出,如果使用IP配置主机,inventory_hostname的值就是IP,如果使用别名,inventory_hostname的值就是别名。

内置变量inventory_hostname_short

与内置变量inventory_hostname类似,通过inventory_hostname_short也可以获取当前play操作的主机在清单中对应的名称,但是这个名称更加简短,假设我的清单配置如下

[test_group]
10.1.1.60
test70.zsythink.net ansible_host=10.1.1.70
test71 anisble_host=10.1.1.71

那么通过内置变量inventory_hostname_short获取到的主机的简短名称如下:

# ansible test_group -m debug -a "msg={{inventory_hostname_short}}"
test70.zsythink.net | SUCCESS => {
    "changed": false, 
    "msg": "test70"
}
10.1.1.60 | SUCCESS => {
    "changed": false, 
    "msg": "10"
}
test71 | SUCCESS => {
    "changed": false, 
    "msg": "test71"
}

可以看到,无论是IP还是别名,如果清单的主机名称中包含”.”,inventory_hostname_short都会取得主机名中第一个”.”之前的字符作为主机的简短名称。

内置变量play_hosts

通过内置变量play_hosts可以获取到当前play所操作的所有主机的主机名列表,示例playbook如下:

---
- hosts: test70,test71
  remote_user: root
  gather_facts: no
  tasks:
  - debug:
      msg: "{{play_hosts}}"

执行上例的playbook,返回信息如下

TASK [debug] *************************
ok: [test70] => {
    "msg": [
        "test71", 
        "test70"
    ]
}
ok: [test71] => {
    "msg": [
        "test71", 
        "test70"
    ]
}

可以看到,此play每操作一个主机,都会将当前play操作的所有主机的主机名列表返回。

没错,inventory_hostname和play_hosts都是返回主机名,只不过,inventory_hostname只返回当前被操作的主机的主机名,而play_hosts则返回当前play中所有被操作主机的主机名列表。

内置变量groups

通过groups内置变量可以获取到清单中”所有分组”的”分组信息”,什么意思呢?我们先来看一个清单配置,假设我的清单配置如下:

10.1.1.60
test70.zsythink.net ansible_host=10.1.1.70
test71 anisble_host=10.1.1.71

[testA]
test60 ansible_host=10.1.1.60
test61 ansible_host=10.1.1.61

[testB]
test70 ansible_host=10.1.1.70

[test:children]
testA
testB

上述清单中,显式的指定了三个组,testA组、testB组、test组,其中,testA组与testB组是test组的子组,除了组中的主机,还有三台主机没有任何分组,直接写在了清单中。

现在,我们获取一下groups变量的值,看看会返回哪些信息,随便操作清单中的任意一台主机即可,示例如下

# ansible test70 -m debug -a "msg={{groups}}"
test70 | SUCCESS => {
    "changed": false, 
    "msg": {
        "all": [
            "10.1.1.60", 
            "test70.zsythink.net", 
            "test71", 
            "test60", 
            "test61", 
            "test70"
        ], 
        "test": [
            "test60", 
            "test61", 
            "test70"
        ], 
        "testA": [
            "test60", 
            "test61"
        ], 
        "testB": [
            "test70"
        ], 
        "ungrouped": [
            "10.1.1.60", 
            "test70.zsythink.net", 
            "test71"
        ]
    }
}

从上述返回信息可以看出,所有主机默认被分成了组名为”all”的组,testA组中有两台主机,testB组中有一台主机,由于testA组和testB组都属于test组的子组,所以testA组与testB组中的主机都属于test组,由于有三台主机在清单中并未分组,所以,ansible自动将没有分组的主机分到了名为”ungrouped”的组中,即组名为”未分组”的组。

我们还能够通过组名,获取到指定组的分组信息,假设,我想要获取到上例中test组中的主机名称,则可以使用如下方法。

# ansible test70 -m debug -a "msg={{groups.test}}"

当然,语法也可以改为如下

# ansible test70 -m debug -a "msg={{groups['test']}}"

聪明如你一定已经会举一反三了,所以,如果我们想要获取到所有未分组主机的主机名,则可以使用如下方法

# ansible test70 -m debug -a "msg={{groups.ungrouped}}"

内置变量group_names

见名知义,我们可以通过内置变量group_names获取到当前主机所在分组的组名,比如,我的清单配置如下

10.1.1.60
test70.zsythink.net ansible_host=10.1.1.70
test71 anisble_host=10.1.1.71

[testA]
test60 ansible_host=10.1.1.60
test61 ansible_host=10.1.1.61

[testB]
test70 ansible_host=10.1.1.70

[test:children]
testA
testB

那么,当我操作test70主机时,group_names变量值如下

# ansible test70 -m debug -a "msg={{group_names}}"
test70 | SUCCESS => {
    "changed": false, 
    "msg": [
        "test", 
        "testB"
    ]
}

如上例返回值所示,test70主机属于testB组,而testB组又是test组的子组,所以test70主机同时属于testB组和test组,所以,最终返回的信息中包括test与testB

当我们操作未分组的主机时,group_names的值为”ungrouped”,示例如下

# ansible 10.1.1.60 -m debug -a "msg={{group_names}}"
10.1.1.60 | SUCCESS => {
    "changed": false,
    "msg": [
        "ungrouped"
    ]
}

内置变量inventory_dir

我们可以通过inventory_dir变量获取到ansible主机中清单文件的存放路径,我使用的是默认的清单文件/etc/ansible/hosts,所以,inventory_dir变量对应的值为/etc/ansible,如下例所示

# ansible test71 -m debug -a "msg={{inventory_dir}}"
test71 | SUCCESS => {
    "changed": false, 
    "msg": "/etc/ansible"
}

ansible中还有其他的一些变量的使用方法,但是需要结合其他的一些知识点,所以之后遇到了实际的使用场景,我们再进行介绍吧。

这篇文章就先总结到这里,希望能够对你有所帮助。

ansible笔记(17):变量(四)

这篇文章继续总结一些ansible中使用变量的方法。

在清单中配置变量

在ansible系列文章的前几篇文章中,我们总结了ansible清单的配置方法,在清单中,可以配置需要被管理的远程主机,也可以将部分远程主机分为一组,其实,在配置清单时,还可以为主机或主机组设置变量,具体方法见如下总结

主机变量

在清单中配置远程主机时,可以同时为主机配置对应的变量,当操作这个主机时,即可直接使用对应的变量。

比如,我在/etc/ansible/hosts中定义test70主机时,可以为test70主机配置一个名为testhostvar的变量,变量值为test70_host_var,示例如下

test70 ansible_host=10.1.1.70 testhostvar=test70_host_var

如上例所示,只要在定义主机时将变量名和变量值写在主机配置的后面即可,可以为一个主机定义多个主机变量,用空格隔开即可,很方便吧,那么我们来测试一下,看看在操作test70主机时,能否引用到这个变量,为了方便示例就不编写playbook了,输入如下ad-hoc命令即可测试出效果

未分类

如上图所示,操作test70主机时,testhostvar已经被引用到了,当然,testhostvar是test70的主机变量,其他主机并不能引用到这个变量,主机变量的生效范围只限于对应的主机。

前文中总结过,配置清单时可以使用INI格式或者YAML格式的语法,刚才的示例为INI风格的语法配置,YAML格式的配置中,可以使用如下方法配置变量

all:
 hosts:
   test70:
     ansible_host: 10.1.1.70
     ansible_port: 22
     testhostvar: test70_host_var
     testhostvar1: test70_host_var1

如上例所示,我们为test70主机配置了两个变量,testhostvar和testhostvar1,没错,就是这么简单,直接在test70的下一级写明变量与变量值即可。

你也可以使用如下方法配置有”层级”的变量,如下

all:
 hosts:
   test70:
     ansible_host: 10.1.1.70
     ansible_port: 22
     testhostvar: test70_host_var
     testhostvar1: test70_host_var1
     testhostvar3:
       thv31: 3.1
       thv32: 3.2

根据前文的总结,聪明如你一定已经想到了,我们可以使用如下两种方法引用变量中的值

未分类

主机组变量

在清单中,我们能将多个主机分为一组,这样方便我们成批的操作远程主机。

比如,我在清单中将test70与test71分为一组,组名为testB,INI格式的配置如下

[testB]
test70 ansible_host=10.1.1.70
test71 anisble_host=10.1.1.71

如果我们想为testB组配置组变量,该怎么办呢?示例如下

[testB]
test70 ansible_host=10.1.1.70
test71 anisble_host=10.1.1.71

[testB:vars]
test_group_var1='group var test'
test_group_var2='group var test2'

如上例所示,”[testB:vars]”表示为testB组配置变量,上例中,testB组中一共定义了两个组变量,”test_group_var1″和”test_group_var2″

组变量的使用范围为组中的所有主机,上例中,无论test70还是test71,都可以使用到上述两个变量,效果如下。

未分类

上例为INI格式中配置组变量的方法,YAML格式中配置组变量的示例如下

all:
 children:
   testB:
     hosts:
       test70:
         ansible_host: 10.1.1.70
         ansible_port: 22
       test71:
         ansible_host: 10.1.1.71
         ansible_port: 22
     vars:
       test_group_var1: 'group var test1'
       test_group_var2: 'group var test2'

如上例所示,使用vars关键字可以指定组变量,vars关键字位于对应组的下一级,上例中,vars关键字位于testB的下一级,调用组变量的效果如下

未分类

通过set_fact定义变量

set_fact是一个模块,我们可以通过set_fact模块在tasks中定义变量,先来看一个小示例,如下

---
- hosts: test70
  remote_user: root
  tasks:
  - set_fact:
      testvar: "testtest"
  - debug:
      msg: "{{testvar}}"

如上例所示,我们通过set_fact模块定义了一个名为testvar的变量,变量值为testtest,然后使用debug模块输出了这个变量。

是不是很简单,通过set_fact模块就能够在tasks中定义变量了,我们也可以通过set_fact将一个变量的值赋予另一个变量,示例如下

---
- hosts: test70
  remote_user: root
  vars:
    testvar1: test1_string
  tasks:
  - shell: "echo test2_string"
    register: shellreturn
  - set_fact:
      testsf1: "{{testvar1}}"
      testsf2: "{{shellreturn.stdout}}"
  - debug:
      msg: "{{testsf1}} {{testsf2}}"

上例中,我们先定义了一个变量testvar1,又使用register将shell模块的返回值注册到了变量shellreturn中,

之后,使用set_fact模块将testvar1变量的值赋予了变量testsf1,将shellreturn变量中的stdout信息赋值给了testsf2变量,

最后,使用debug模块输出了testsf1与testsf2的值。

如上述示例所示,set_fact模块可以让我们在tasks中创建变量,也可以将一个变量的值赋值给另一个变量。

其实,通过set_fact模块创建的变量还有一个特殊性,通过set_fact创建的变量就像主机上的facts信息一样,可以在之后的play中被引用,什么意思呢?我们慢慢聊。

前文中已经总结过,默认情况下,每个play执行之前都会执行一个名为”[Gathering Facts]”的默认任务,这个任务会收集对应主机的相关信息,我们可以称这些信息为facts信息,我们已经总结过怎样通过变量引用这些facts信息,此处不再赘述,而通过set_fact模块创建的变量可以在之后play中被引用,就好像主机的facts信息可以在play中引用一样,这样说可能还是不是特别容易理解,不如来看一个小例子,如下

---
- hosts: test70
  remote_user: root
  vars:
    testvar1: tv1
  tasks:
  - set_fact:
      testvar2: tv2
  - debug:
      msg: "{{testvar1}} ----- {{testvar2}}"

- hosts: test70
  remote_user: root
  tasks:
  - name: other play get testvar2
    debug:
      msg: "{{testvar2}}"
  - name: other play get testvar1
    debug:
      msg: "{{testvar1}}"

上例中一共有两个play,第一个play中,我们通过两种方式创建了两个变量,第一个变量testvar1使用vas关键字创建,第二个变量使用set_fact创建。

如果执行上例的playbook,可以发现,这两个变量在第一个play中都可以正常的输出。但是在第二个play中,testvar2可以被正常输出了,testvar1却不能被正常输出,会出现未定义testvar1的错误,因为在第一个play中针对test70主机进行操作时,testvar1是通过vars关键字创建的,而testvar2是通过set_fact创建的,所以testvar2就好像test70的facts信息一样,可以在第二个play中引用到,而创建testvar1变量的方式则不能达到这种效果,虽然testvar2就像facts信息一样能被之后的play引用,但是在facts信息中并不能找到testvar2,只是”效果上”与facts信息相同罢了。

前文已经总结了注册变量的用法,其实注册变量也可以在之后的play操作同一主机时被调用到,示例如下

---
- hosts: test70
  remote_user: root
  vars:
    testvar3: tv3
  tasks:
  - shell: "echo tv4"
    register: testvar4
  - debug:
      msg: "{{testvar3}} -- {{testvar4.stdout}}"

- hosts: test70
  remote_user: root
  tasks:
  - name: other play get testvar4
    debug:
      msg: "{{testvar4.stdout}}"
  - name: other play get testvar3
    debug:
      msg: "{{testvar3}}"

执行上例的playbook时,在第二个play中获取”testvar3″时会报错,而在第二个play中获取注册变量”testvar4″时则正常,但是,注册变量中的信息是模块的返回值,这并不是我们自定义的信息,所以,如果想要在tasks中给变量自定义信息,并且在之后的play操作同一个主机时能够使用到之前在tasks中定义的变量时,则可以使用set_facts定义对应的变量。

细心如你一定发现了,上述示例中,即使是跨play获取变量,也都是针对同一台主机,但是某些时候,我们可能想要在操作一台主机时,获取到之前操作的另一台主机中定义的变量,那么该怎样做呢?具体做法就放在下一篇文章中总结吧,这篇文章就先总结到这里,希望能够对你有所帮助。

ansible笔记(16):变量(三)

承接前文,这篇文章将继续介绍变量的一些使用方法。

注册变量

ansible的模块在运行之后,其实都会返回一些”返回值”,只是默认情况下,这些”返回值”并不会显示而已,我们可以把这些返回值写入到某个变量中,这样我们就能够通过引用对应的变量从而获取到这些返回值了,这种将模块的返回值写入到变量中的方法被称为”注册变量”,那么怎样将返回值注册到变量中呢?我们来看一个playbook示例

---
- hosts: test70
  remote_user: root
  tasks:
  - name: test shell
    shell: "echo test > /var/testshellfile"
    register: testvar
  - name: shell module return values
    debug:
      var: testvar

上例中共有两个任务,第一个任务使用shell模块在test70主机中创建了一个测试文件 /var/testshellfile,将字符”test”输入到了测试文件中,然后使用”register”关键字将当前shell任务的返回值写入了名为”testvar”的变量中,第二个任务使用debug模块输出了第一个任务中的注册变量的值,没错,注册变量就是这么简单,使用register关键字指定对应的变量名即可。

上述playbook执行后,可以在控制台中看到名为”[shell module return values]”的任务中已经显示了第一个任务的返回值的信息,返回信息如下

TASK [shell module return values] **********************************************************************
ok: [test70] => {
    "testvar": {
        "changed": true, 
        "cmd": "echo test > /var/testshellfile", 
        "delta": "0:00:00.003808", 
        "end": "2018-06-17 20:42:37.675382", 
        "failed": false, 
        "rc": 0, 
        "start": "2018-06-17 20:42:37.671574", 
        "stderr": "", 
        "stderr_lines": [], 
        "stdout": "", 
        "stdout_lines": []
    }
}

从上述返回信息可以看出,返回值是json格式的,上述返回值中包含一些键值对,比如 “changed”: true 或 “cmd”: “echo test > /var/testshellfile”等, 如果你只是想要获取到返回值中的某一项特定值,只需要指定键值对中的key即可,假设,我只是想要获取到上述返回信息中cmd的值,则可以使用如下两种语法(前文中已经对如下两种语法进行过示例,此处不再赘述)。

语法一
  - name: shell module return values
    debug:
      msg: "{{testvar.cmd}}"
语法二
  - name: shell module return values
    debug:
      msg: "{{testvar['cmd']}}"

上述示例的返回信息为shell模块的返回值,如果你想要了解返回值中每一项的含义,则可以查看官方手册,我使用的是2.4版本的ansible,所以我可以参考2.4版本的官网文档,找到shell模块的介绍,官网链接如下

https://docs.ansible.com/ansible/2.4/shell_module.html

不同的模块,返回值也不尽相同,ansible官网对一些常见的返回值进行了总结,链接如下

https://docs.ansible.com/ansible/2.4/common_return_values.html

如果你想要查看模块对应的返回值,可以先查找官方手册,但是,并不是所有模块的官方手册中都对模块的返回值进行了描述,你可以使用上述示例中的方法,自己查看模块的返回值,这些返回值不仅仅能够用于输出,通常我们会利用到这些返回值,比如,通过模块的返回值决定之后的一些动作,所以,注册变量在playbook中还是会被经常用到的,在之后的文章中我们会给出示例,此处不用纠结。

提示用户输入信息并写入变量

在运行某些脚本时,有时候脚本会提示用户输入一些信息,脚本需要根据用户输入的信息决定下一步的动作,这种”交互”有时候是必须的,那么,在playbook中该怎样实现这种交互呢?我们可以这样做,提示用户输入信息,然后将用户输入的信息存入到指定的变量中,当我们需要使用这些”输入的信息”时,只要引用对应的变量即可。

我们来看一个小示例,如下

---
- hosts: test70
  remote_user: root
  vars_prompt:
    - name: "your_name"
      prompt: "What is your name"
    - name: "your_age"
      prompt: "How old are you"
  tasks:
   - name: output vars
     debug:
      msg: Your name is {{your_name}},You are {{your_age}} years old.

如上例所示,我们使用”vars_prompt”关键字创建了两个变量,这两个变量的名称分别为”your_name” 和 “your_age”,当运行上例playbook时,会出现 “What is your name”的提示信息,然后用户输入的信息会存入到”your_name”变量中,之后,会出现 “How old are you”的提示信息,用户输入的信息会存入到”your_age”变量中,上例中的”output vars”任务会输出一句话,这句话中包含了上述两个变量的值,我们来看一下上例的执行效果。

未分类

如上图所示,运行playbook时会提示输入你的名字,输入你的年龄,你输入的内容并不会显示在屏幕上,在完成提示输入的内容后,在”output vars”任务的输出中可以看到用户输入的名字和年龄。

如你所见,当你使用这种方式提示用户时,默认情况下不会显示用户输入的信息,这种方式比较适合用户输入密码时的场景,如果你想要显示用户输入的信息,可以使用如下示例中的方法。

  vars_prompt:
    - name: "your_name"
      prompt: "What is your name"
      private: no
    - name: "your_age"
      prompt: "How old are you"
      private: no

如上例所示,我们在定义” vars_prompt”中的变量时,使用private关键字,将变量的private属性设置为no即可, “private: no”表示变量值为非私有的,可见的,默认情况下 private值为yes,表示不可见。

我们还能为提示信息设置默认值,即如果用户不输入任何信息,则将默认值赋予变量,示例playbook如下。

---
- hosts: test70
  remote_user: root
  vars_prompt:
    - name: "solution"
      prompt: "Choose the solution you want n
      A: solutionAn
      B: solutionBn
      C: solutionCn"
      private: no
      default: A
  tasks:
   - name: output vars
     debug:
      msg: The final solution is {{solution}}.

如上例所示,我们使用了default关键字设置了”solution”变量的默认值,如果用户没有输入任何值(直接回车),则将”solution”变量的值设置为A,如果用户输入了值,则”solution”变量值为用户输入的值。

之前的示例中,我们提到可以利用提示信息让用户设置密码,有了这项功能,我们就可以编写出一个playbook,这个playbook可以让用户手动输入用户名和密码,然后根据用户输入的信息去创建系统用户了,聪明如你一定想到了,创建系统用户可以使用user模块,前文已经总结过user模块,此处不再赘述,那么我们来尝试编写一个可交互创建系统用户的playbook吧,经过思考,我编写了如下playbook,你可以帮我看看如下playbook中存在什么问题。

---
- hosts: test70
  remote_user: root
  vars_prompt:
    - name: "user_name"
      prompt: "Enter user name"
      private: no
    - name: "user_password"
      prompt: "Enter user password"
  tasks:
   - name: create user
     user:
      name: "{{user_name}}"
      password: "{{user_password}}"

上例的playbook似乎没有什么不妥,但是细心如你一定发现了,user模块的password参数虽然可以指定用户的密码,但是password参数对应的值必须是一个”明文密码哈希过后的字符串”(如果你不明白我在说什么,可以参考之前文章中总结的user模块的使用方法),而上例中,用户经过提示后输入的密码字符串并未经过哈希操作,所以,即使通过上述playbook可以创建用户,创建后的用户也无法通过设置的密码进行登录,因为保存在/etc/shadow文件中的密码字段是一个未哈希的明文的密码字段。那么,我们该怎么办呢?没错,我们需要对用户输入的密码字符串进行哈希,然后将哈希过后的字符串传入user模块的password参数中,ansible已经为我们考虑到了这一点,我们可以使用”encrypt”关键字,对用户输入的字符串进行哈希,用户输入的信息被哈希以后会存入对应的变量中,示例如下:

---
- hosts: test70
  remote_user: root
  vars_prompt:
    - name: "hash_string"
      prompt: "Enter something"
      private: no
      encrypt: "sha512_crypt"
  tasks:
   - name: Output the string after hash
     debug:
      msg: "{{hash_string}}"

如上例所示(先不要着急运行上述playbook),encrypt关键字表示对用户输入的信息进行哈希,encrypt: “sha512_crypt”表示使用sha512算法对用户输入的信息进行哈希,哈希后的字符串会存入到上例中的”hash_string”变量中,利用encrypt关键字,就可以解决之前遇到的创建用户时指定密码字符串的问题,但是需要注意,当使用”encrypt”关键字对字符串进行哈希时,ansible需要依赖passlib库完成哈希操作,如果未安装passlib库(一个用于哈希明文密码的python库),执行playbook时会报如下错误

ERROR! passlib must be installed to encrypt vars_prompt values

我的ansible主机的操作系统为centos7.4,默认自带python2.7.5,为了能够正常执行上述playbook,需要先安装passlib库。

此处通过pip安装passlib库,由于当前主机也没有安装pip,所以先下载安装pip

# tar -xvf pip-10.0.1.tar.gz
# cd pip-10.0.1/
# python setup.py install

pip安装完成后,通过pip安装passlib库

# pip install passlib

passlib库安装完成后,执行上例中的剧本,可以看到,你输入的信息在经过哈希以后被输出了,当然,上例中我们指定了使用sha512算法对字符串进行哈希,你也可以指定其他passlib库支持的算法,算法名称可以参考如下连接

https://docs.ansible.com/ansible/2.4/playbooks_prompts.html

除了能够使用”encrypt”关键字对字符串进行哈希加密,还能够使用”confirm”关键字实现类似确认密码的功能,我们在为用户设置密码时,通常需要输入两次完全相同的密码,才能够设置成功,通过”confirm”关键字就能实现类似的效果,示例playbook如下

---
- hosts: test70
  remote_user: root
  vars_prompt:
    - name: "user_name"
      prompt: "Enter user name"
      private: no
    - name: "user_password"
      prompt: "Enter user password"
      encrypt: "sha512_crypt"
      confirm: yes
  tasks:
   - name: create user
     user:
      name: "{{user_name}}"
      password: "{{user_password}}"

具体的执行效果我就不截图了,动手试试吧。

通过命令行传入变量

除了之前总结过的定义变量的方法,我们还能够在执行playbook时直接传入需要使用的变量,我们来看一小示例,如下:

---
- hosts: test70
  remote_user: root
  tasks:
  - name: "Passing Variables On The Command Line"
    debug:
      msg: "{{pass_var}}"

上例中的playbook中,并没有定义pass_var变量,而是直接引用了pass_var变量,我们可以在调用上述playbook时直接从命令行传入pass_var变量,方法如下

ansible-playbook cmdvar.yml --extra-vars "pass_var=cmdline pass var"

如上例所示,在调用playbook时使用 “–extra-vars” 选项可以传递对应的变量与变量值, “–extra-vars” 是长选项,对应的短选项是”-e”,我们也可以一次性传入多个变量,变量之间用空格隔开,如下

ansible-playbook cmdvar.yml -e 'pass_var="test" pass_var1="test1"'

上例中的playbook中并没有定义pass_var变量,如果在调用playbook时也没有传入pass_var变量,则会报错,其实,我们也可以先在playbook中定义好变量,然后在执行playbook时,再次传入相同名称的变量,最终还是以传入的变量值为准,示例如下

---
- hosts: test70
  remote_user: root
  vars:
    pass_var: test_default
  tasks:
  - name: "Passing Variables On The Command Line"
    debug:
      msg: "{{pass_var}}"

上例的playbook中定义了pass_var变量,其值为”test_default”,在执行上述playbook时,从命令行再次传入pass_var变量,命令如下

ansible-playbook cmdvar.yml -e 'pass_var="test"'

执行上述命令后,你会发现,最终输出的值为”test”而非”test_default”,也就是说,命令行传入的变量的优先级要高于playbook中的变量,通过这种方法,我们就能够更加灵活的指定变量的值了。

不仅ansible-playbook命令可以使用”-e”传递变量,ansible命令也同样可以,所以在执行ad-hoc命令时也可以使用同样的方法传入变量,如下

ansible test70 -e "testvar=test" -m shell -a "echo {{testvar}}"

上述的几个示例从命令行中传递变量时,都是使用了”key=value”的形式,除了使用”key=value”的方式传递变量,ansible还支持通过json的格式传入变量,示例如下

通过json格式传入两个变量

ansible-playbook cmdvar.yml -e '{"testvar":"test","testvar1":"test1"}'

通过json格式传入稍微复杂一点的变量

ansible-playbook cmdvar.yml -e '{"countlist":["one","two","three","four"]}'

在剧本中引用上述命令传入的countlist变量时,如果想要获取到值”one”,则可以使用如下两种语法引用变量

{{countlist[0]}} 或者 {{countlist.0}}

命令行不仅能够传入变量,还能传入变量文件,变量文件中的变量都会一并被传入,变量文件可以是json格式的,也可以是YAML格式的,此处使用YAML格式的变量文件进行示例,示例文件内容如下

# cat /testdir/ansible/testvar
testvar: testvarinfile
countlist:
- one
- two
- three
- four

测试用playbook内容如下

---
- hosts: test70
  remote_user: root
  tasks:
  - name: "Passing Variables On The Command Line"
    debug:
      msg: "{{testvar}} {{countlist[0]}}"

如playbook所示,playbook中引用了变量文件中定义的两个变量,那么,我们怎样从命令行中将变量文件中的变量传入playbook呢?示例如下

ansible-playbook cmdvar.yml -e "@/testdir/ansible/testvar"

如上述命令所示,使用”@”符号加上变量文件的路径,即可在命令行中传入对应的变量文件,变量文件中的所有变量都可以在playbook中引用,还是很方便的吧。

好了,这篇文章就总结到这里,希望能够对你有所帮助。

ansible笔记(15):变量(二)

前一篇文章中已经初步的总结了变量的一些使用方法,这篇文章我们继续,只不过,这篇文章所涉及到的内容需要借助两个模块,所以在详细的总结变量的相关使用方法之前,会先描述一下这两个模块的用法。

当我们运行一个playbook时,默认都会运行一个名为”[Gathering Facts]”的任务,前文中已经大致的介绍过这个默认的任务,ansible通过”[Gathering Facts]”这个默认任务收集远程主机的相关信息(例如远程主机的IP地址,主机名,系统版本,硬件配置等信息),其实,这些被收集到的远程主机信息会保存在对应的变量中,当我们想要使用这些信息时,我们可以获取对应的变量,从而使用这些信息。

如果想要查看”[Gathering Facts]”任务收集的信息内容,我们可以借助一个模块:setup模块

当执行playbook时,playbook其实就是自动调用了setup模块从而执行了”[Gathering Facts]”任务,所以我们可以通过手动执行setup模块查看”[Gathering Facts]”任务收集到的信息,示例如下

ansible test70 -m setup

上述ad-hoc命令表示收集test70主机的相关信息,执行上述命令后,远程主机test70的相关信息将会输出到ansible主机的控制台上,返回的信息的格式是json格式,我的返回信息如下。

注:由于返回的信息比较多,此处为了方便示例,我将部分内容删除(或折叠省略)了,所以如下返回信息并不完全,只用于示意。

test70 | SUCCESS =>
{
    "ansible_facts":{
        "ansible_all_ipv4_addresses":[
            "192.168.122.1",
            "192.168.1.106",
            "10.1.1.70",
            "172.16.66.70"
        ],
        "ansible_all_ipv6_addresses":Array[2],
        "ansible_apparmor":Object{...},
        "ansible_architecture":"x86_64",
        "ansible_bios_date":"05/19/2017",
        "ansible_bios_version":"6.00",
        "ansible_cmdline":Object{...},
        "ansible_date_time":Object{...},
        "ansible_default_ipv4":Object{...},
        "ansible_default_ipv6":Object{...},
        "ansible_device_links":Object{...},
        "ansible_devices":Object{...},
        "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.4.1708",
        "ansible_dns":Object{...},
        "ansible_domain":"",
        "ansible_effective_group_id":0,
        "ansible_effective_user_id":0,
        "ansible_ens33":Object{...},
        "ansible_ens34":Object{...},
        "ansible_ens35":{
            "active":true,
            "device":"ens35",
            "features":{
                "busy_poll":"off [fixed]",
                "fcoe_mtu":"off [fixed]",
                "generic_receive_offload":"on",
                "generic_segmentation_offload":"on",
                "highdma":"off [fixed]",
                "hw_tc_offload":"off [fixed]",
                "l2_fwd_offload":"off [fixed]",
                "large_receive_offload":"off [fixed]",
                "loopback":"off [fixed]",
                "netns_local":"off [fixed]",
                "ntuple_filters":"off [fixed]",
                "receive_hashing":"off [fixed]",
                "rx_all":"off",
                "rx_checksumming":"off",
                "rx_fcs":"off",
                "rx_vlan_filter":"on [fixed]",
                "rx_vlan_offload":"on",
                "rx_vlan_stag_filter":"off [fixed]",
                "rx_vlan_stag_hw_parse":"off [fixed]",
                "scatter_gather":"on",
                "tcp_segmentation_offload":"on",
                "tx_checksum_fcoe_crc":"off [fixed]",
                "tx_checksum_ip_generic":"on",
                "tx_checksum_ipv4":"off [fixed]",
                "tx_checksum_ipv6":"off [fixed]",
                "tx_checksum_sctp":"off [fixed]",
                "tx_checksumming":"on",
                "tx_fcoe_segmentation":"off [fixed]",
                "tx_gre_csum_segmentation":"off [fixed]",
                "tx_gre_segmentation":"off [fixed]",
                "tx_gso_partial":"off [fixed]",
                "tx_gso_robust":"off [fixed]",
                "tx_ipip_segmentation":"off [fixed]",
                "tx_lockless":"off [fixed]",
                "tx_mpls_segmentation":"off [fixed]",
                "tx_nocache_copy":"off",
                "tx_scatter_gather":"on",
                "tx_scatter_gather_fraglist":"off [fixed]",
                "tx_sctp_segmentation":"off [fixed]",
                "tx_sit_segmentation":"off [fixed]",
                "tx_tcp6_segmentation":"off [fixed]",
                "tx_tcp_ecn_segmentation":"off [fixed]",
                "tx_tcp_mangleid_segmentation":"off",
                "tx_tcp_segmentation":"on",
                "tx_udp_tnl_csum_segmentation":"off [fixed]",
                "tx_udp_tnl_segmentation":"off [fixed]",
                "tx_vlan_offload":"on [fixed]",
                "tx_vlan_stag_hw_insert":"off [fixed]",
                "udp_fragmentation_offload":"off [fixed]",
                "vlan_challenged":"off [fixed]"
            },
            "hw_timestamp_filters":[


            ],
            "ipv4":{
                "address":"10.1.1.70",
                "broadcast":"10.1.1.255",
                "netmask":"255.255.255.0",
                "network":"10.1.1.0"
            },
            "ipv6":[
                {
                    "address":"fe80::250:56ff:fe25:5fb0",
                    "prefix":"64",
                    "scope":"link"
                }
            ],
            "macaddress":"00:50:56:25:5f:b0",
            "module":"e1000",
            "mtu":1500,
            "pciid":"0000:02:03.0",
            "promisc":false,
            "speed":1000,
            "timestamping":[
                "tx_software",
                "rx_software",
                "software"
            ],
            "type":"ether"
        },
        "ansible_env":Object{...},
        "ansible_fips":false,
        "ansible_form_factor":"Other",
        "ansible_fqdn":"test70",
        "ansible_hostname":"test70",
        "ansible_interfaces":Array[6],
        "ansible_kernel":"3.10.0-693.el7.x86_64",
        "ansible_lo":Object{...},
        "ansible_local":Object{...},
        "ansible_lsb":Object{...},
        "ansible_lvm":Object{...},
        "ansible_machine":"x86_64",
        "ansible_machine_id":"f6d15ac15f624d3db89e843639a52cc0",
        "ansible_memfree_mb":1121,
        "ansible_memory_mb":{
            "nocache":{
                "free":1467,
                "used":356
            },
            "real":{
                "free":1121,
                "total":1823,
                "used":702
            },
            "swap":{
                "cached":0,
                "free":1023,
                "total":1023,
                "used":0
            }
        },
        "ansible_memtotal_mb":1823,
        "ansible_mounts":Array[2],
        "ansible_nodename":"test70",
        "ansible_os_family":"RedHat",
        "ansible_pkg_mgr":"yum",
        "ansible_processor":Array[6],
        "ansible_processor_cores":2,
        "ansible_processor_count":1,
        "ansible_processor_threads_per_core":1,
        "ansible_processor_vcpus":2,
        "ansible_product_name":"VMware Virtual Platform",
        "ansible_product_serial":"VMware-56 4d 0d 63 80 3f 29 b4-f0 e2 1b 7a ff 01 a5 9e",
        "ansible_product_uuid":"630D4D56-3F80-B429-F0E2-1B7AFF01A59E",
        "ansible_product_version":"None",
        "ansible_python":Object{...},
        "ansible_python_version":"2.7.5",
        "ansible_real_group_id":0,
        "ansible_real_user_id":0,
        "ansible_selinux":Object{...},
        "ansible_selinux_python_present":true,
        "ansible_service_mgr":"systemd",
        "ansible_swapfree_mb":1023,
        "ansible_swaptotal_mb":1023,
        "ansible_system":"Linux",
        "ansible_system_capabilities":Array[37],
        "ansible_system_capabilities_enforced":"True",
        "ansible_system_vendor":"VMware, Inc.",
        "ansible_uptime_seconds":31658,
        "ansible_user_dir":"/root",
        "ansible_user_gecos":"root",
        "ansible_user_gid":0,
        "ansible_user_id":"root",
        "ansible_user_shell":"/bin/bash",
        "ansible_user_uid":0,
        "ansible_userspace_architecture":"x86_64",
        "ansible_userspace_bits":"64",
        "ansible_virbr0":Object{...},
        "ansible_virbr0_nic":Object{...},
        "ansible_virtualization_role":"guest",
        "ansible_virtualization_type":"VMware",
        "gather_subset":Array[1],
        "module_setup":true
    },
    "changed":false
}

返回信息如上,是一个json格式的字符串,为了方便你阅读,ansible已经将格式化后的json信息返回到了控制台中,返回的信息很全面,比如:

“ansible_all_ipv4_addresses”表示远程主机中的所有ipv4地址,从其对应的值可以看出,test70主机上一共有4个ipv4地址。

“ansible_distribution”表示远程主机的系统发行版,从其对应的值可以看出test70主机的系统发行版为centos

“ansible_distribution_version”表示远程主机的系统版本号,从其对应的值与 “ansible_distribution” 的值可以看出test70主机的系统版本为centos7.4

“ansible_ens35″表示远程主机ens35网卡的相关信息,细心如你一定也发现了,我还有两个名为”ens33″和”ens34″的网卡,只不过为了方便示例,这两个网卡的信息被我省略了。

“ansible_memory_mb”表示远程主机的内存配置信息。

返回的信息的确很多,很全面,但是,并不是每一次我们都需要看这么多信息,如果你只是想查看某一类信息,你可以通过关键字对信息进行过滤,比如,我只是想要查看远程主机的内存配置信息,那么我可以使用如下命令

ansible test70 -m setup -a 'filter=ansible_memory_mb'

上述命令表示通过”ansible_memory_mb”关键字对返回信息进行过滤,如你所见,通过setup模块的filter参数可以指定需要过滤的关键字,这样ansible就只会将”ansible_memory_mb”的相关信息返回,返回如下

test70 | SUCCESS => {
   "ansible_facts": {
       "ansible_memory_mb": {
           "nocache": {
               "free": 1467,
               "used": 356
           },
           "real": {
               "free": 1119,
               "total": 1823,
               "used": 704
           },
           "swap": {
               "cached": 0,
               "free": 1023,
               "total": 1023,
               "used": 0
           }
       }
   },
   "changed": false
}

这样就精简很多了,因为精准的返回了你需要的信息,我知道,有的朋友可能跟我一样,记性不好,所以通常记不住准确的关键字,所以我们可以使用通配符,进行相对模糊的过滤,示例如下

asible test70 -m setup -a "filter=*mb*"

上述命令表示返回所有包含mb的关键字对应的信息,返回信息如下

test70 | SUCCESS => {
   "ansible_facts": {
       "ansible_memfree_mb": 1140,
       "ansible_memory_mb": {
           "nocache": {
               "free": 1475,
               "used": 348
           },
           "real": {
               "free": 1140,
               "total": 1823,
               "used": 683
           },
           "swap": {
               "cached": 0,
               "free": 1023,
               "total": 1023,
               "used": 0
           }
       },
       "ansible_memtotal_mb": 1823,
       "ansible_swapfree_mb": 1023,
       "ansible_swaptotal_mb": 1023
   },
   "changed": false
}

其实,除了这些信息以外,我们还能够在远程主机中写入一些自定义的信息,这些自定义信息也可以被setup模块收集到。

那么,我们应该在哪里定义这些信息呢?该怎样定义这些信息呢?

ansible默认会去目标主机的/etc/ansible/facts.d目录下查找主机中的自定义信息,并且规定,自定义信息需要写在以”.fact”为后缀的文件中,同时,这些以”.fact”为后缀的文件中的内容需要是INI格式或者是json格式的。

那么,我们来创建一个测试文件,测试文件路径为test70主机的/etc/ansible/facts.d/testinfo.fact,在文件中写入如下INI格式的信息。

[root@test70 facts.d]# cat testinfo.fact
[testmsg]
msg1=This is the first custom test message
msg2=This is the second custom test message

如上所示,上述内容是一段INI风格的内容,我在”[testmsg]”配置段中配置了两条自定义信息,msg1与msg2。

当然,我们也可以使用json格式进行配置,比如在/etc/ansible/facts.d/testinfo.fact文件中写入如下配置,如下配置与上述配置的效果是相同的,只是书写格式不同。

{
   "testmsg":{
       "msg1":"This is the first custom test message",
       "msg2":"This is the second custom test message"
   }
}

通过上述方式,我们可以在目标主机的本地自定义信息,这些在远程主机本地自定义的信息被称为”local facts”,当我们运行setup模块时,远程主机的”local facts”信息也会被收集,我们可以通过”ansible_local”关键字过滤远程主机的”local facts”信息,示例命令如下

ansible test70 -m setup -a "filter=ansible_local"

上述命令返回的信息如下

test70 | SUCCESS => {
   "ansible_facts": {
       "ansible_local": {
           "testinfo": {
               "testmsg": {
                   "msg1": "This is the first custom test message",
                   "msg2": "This is the second custom test message"
               }
           }
       }
   },
   "changed": false
}

之前说过,当setup收集远程主机的”local facts”时,默认会查找远程主机的/etc/ansible/facts.d目录,如果你把”local facts”信息文件放在了其他自定义路径,在使用setup模块时,需要使用”fact_path”参数指定对应的路径,假设,我把”.fact”文件放在了目标主机的”/testdir”目录下,示例命令如下

ansible test70 -m setup -a 'fact_path=/testdir'

其实,setup模块返回的这些信息都存在了对应的变量中,我们可以通过引用变量从而使用对应的信息,但是别急,我们先来了解一下另外一个模块,这个模块叫”debug模块”。

见名知义,debug模块的作用就是帮助我们进行调试的,debug模块可以帮助我们把信息输出到ansible控制台上,以便我们能够定位问题。

那么我们先来看一个debug模块的playbook小示例,如下

---
- hosts: test70
  remote_user: root
  tasks:
  - name: touch testfile
    file:
      path: /testdir/testfile
      state: touch
  - name: debug demo
    debug:
      msg: this is debug info,The test file has been touched

上例中,我们先在test70主机上touch了对应的文件,然后,利用debug模块在控制台中输出了我们想要显示的信息,如你所见,debug模块的msg参数可以指定我们想要输出的信息,上述playbook表示touch完对应的文件以后,在ansible控制台中输出我们指定的信息,那么我们运行一下这个测试剧本,看一下效果,如下

未分类

如图所示,自定义信息已经输出在ansible控制台中。

debug模块除了能够使用msg参数输出自定义的信息,还能够直接输出变量中的信息,通过debug模块直接输出变量信息需要使用var参数,示例如下

---
- hosts: test70
  remote_user: root
  vars:
    testvar: value of test variable
  tasks:
  - name: debug demo
    debug:
      var: testvar

上例虽然连接到了test70远程主机,但是并没有对test70做任何操作,只是在playbook中定义了一个变量,并且通过debug的var参数输出了这个变量的内容,只是为了单纯的演示debug模块的var参数的使用方法,上述playbook的执行效果如下

未分类

变量的名称以及变量的值都输出到了屏幕上,这个功能可以帮助我们调试playbook中变量,让我们了解变量的值是否符合我们的要求。

当然,使用debug的msg参数时也可以引用变量的值,这样我们自定义的信息就更加灵活了,示例如下。

---
- hosts: test70
  remote_user: root
  vars:
    testvar: testv
  tasks:
  - name: debug demo
    debug:
      msg: "value of testvar is : {{testvar}}"

上例中的msg自定义信息中引用了testvar变量的值

注:上例中msg的值需要使用引号引起,因为{{testvar}}变量前包含”冒号”,如果不使用引号会报语法错误。

上例输出效果如下

未分类

setup模块与debug模块了解完了,现在绕回一开始的话题,playbook在运行时默认都会运行”[Gathering Facts]”任务,”[Gathering Facts]”任务会收集远程主机的相关信息,这些信息会保存在对应的变量中,我们在playbook中可以使用这些变量,从而利用这些信息,那么我们怎样在playbook获取到这些变量的值呢?在setup模块的示例中,我们可以通过”ansible_memory_mb”关键字获取远程主机的内存信息,其实,”ansible_memory_mb”就是一个变量名,换句话说就是,我们可以在playbook中直接引用名为”ansible_memory_mb”的变量,从而获取到远程主机的内存信息,示例如下

---
- hosts: test70
  remote_user: root
  tasks:
  - name: debug demo
    debug:
      msg: "Remote host memory information: {{ansible_memory_mb}}"

上例执行效果如下

未分类

如图所示,我们自定义的信息中包含了远程主机的内存信息,同时被输出了,只是格式上没有手动执行setup模块返回的信息格式易读,手动执行setup模块获取到的内存信息返回如下

test70 | SUCCESS => {
   "ansible_facts": {
       "ansible_memory_mb": {
           "nocache": {
               "free": 1487,
               "used": 336
           },
           "real": {
               "free": 1151,
               "total": 1823,
               "used": 672
           },
           "swap": {
               "cached": 0,
               "free": 1023,
               "total": 1023,
               "used": 0
           }
       }
   },
   "changed": false
}

如上述返回信息所示,”ansible_memory_mb”中其实包含了 “nocache”、”real”、 “swap”三个部分的信息,如果我们只想获得”real”部分的信息,在playbook中引用变量时可以使用如下两种语法。

语法一示例:
debug:
     msg: "Remote host memory information : {{ansible_memory_mb.real}}"
语法二示例:
debug:
     msg: "Remote host memory information : {{ansible_memory_mb['real']}}"
上述两种语法前文中已经进行过示例,此处不再赘述。

其实,这些远程主机的变量信息不仅仅能够用于输出,我们通常会获取到这些信息以后,对这些信息的值进行判断,判断是否符合我们的要求,然后再执行下一步动作,比如,先获取到远程主机的系统发行版信息,然后判断发行版是centos6还是centos7,如果是centos6,我们就将准备好的A文件拷贝到远程主机中,如果是centos7,我们就将准备好的B文件拷贝到远程主机中,不过由于我们还没有总结条件判断的相关使用方法,所以此处就不进行示例了,这篇文章就先总结到这里,希望能够对你有所帮助。

ansible笔记(14):变量(一)

在ansible中使用变量,能让我们的工作变得更加灵活,在ansible中,变量的使用方式有很多种,我们慢慢聊。

先说说怎样定义变量,变量名应该由字母、数字、下划线组成,变量名需要以字母开头,ansible内置的关键字不能作为变量名。

由于之前的几篇文章都是在通过剧本举例,所以我们先聊聊怎样在playbook中使用变量。

如果我们想要在某个play中定义变量,可以借助vars关键字,示例如下

---
- hosts: test70
  vars:
    testvar1: testfile
  remote_user: root
  tasks:
  - name: task1
    file:
      path: /testdir/{{ testvar1 }}
      state: touch

上例中,先使用vars关键字,表示在当前play中进行变量的相关设置。

vars关键字的下一级定义了一个变量,变量名为testvar1,变量值为testfile

当我们需要使用testvar1的变量值时,则需要引用这个变量,如你所见,使用”{{变量名}}”可以引用对应的变量。

也可以定义多个变量,示例如下。

vars:
  testvar1: testfile
  testvar2: testfile2

除了使用上述语法,使用YAML的块序列语法也可以定义变量,示例如下

vars:
  - testvar1: testfile
  - testvar2: testfile2

在定义变量时,还能够以类似”属性”的方式定义变量,示例如下

---
- hosts: test70
  remote_user: root
  vars:
    nginx:
      conf80: /etc/nginx/conf.d/80.conf
      conf8080: /etc/nginx/conf.d/8080.conf
  tasks:
  - name: task1
    file:
      path: "{{nginx.conf80}}"
      state: touch
  - name: task2
    file:
      path: "{{nginx.conf8080}}"
      state: touch

如上例所示,我定义了两个变量,两个变量的值对应两个nginx配置文件路径

  vars:
    nginx:
      conf80: /etc/nginx/conf.d/80.conf
      conf8080: /etc/nginx/conf.d/8080.conf

当我们需要引用这两个变量时,有两种语法可用

语法一

"{{nginx.conf80}}"

语法二

"{{nginx['conf8080']}}"

这样使用变量在逻辑上比较清晰,可以看出conf80与conf8080都属于nginx相关的配置。

细心如你一定发现了,上例中,我在引用变量时使用了双引号,而在本文的第一个示例中引用变量时却没有使用双引号,这是因为,第一个示例中的变量在被引用时,并没有处于”开头的位置”,第一个示例中变量被引用时如下

path: /testdir/{{ testvar1 }}

当file模块的path参数引用对应的变量时,先写入了’/testdir/’,然后才引用了”testvar1″变量,{{ testvar1 }}并没有处于”开头的位置”,换句话说就是,{{ testvar1 }}前面还有字符串’/testdir/’

而在上述后面的示例中引用变量时,变量被引用时如下,处于”开头的位置”

path: "{{nginx.conf80}}"

这种情况下,我们引用变量时必须使用双引号引起被引用的变量,否则会报语法错误。

其实,上述情况也有例外

前文中有描述过,当在playbook中为模块的参数赋值时,可以使用”冒号”,也可以使用”等号”,当使用”等号”为模块的参数赋值时,则不用考虑引用变量时是否使用”引号”的问题,示例如下

---
- hosts: test70
  remote_user: root
  vars:
    nginx:
      conf80: /etc/nginx/conf.d/80.conf
      conf8080: /etc/nginx/conf.d/8080.conf
  tasks:
  - name: task1
    file:
      path={{nginx.conf80}}
      state=touch
  - name: task2
    file:
      path={{nginx['conf8080']}}
      state=touch

除了能够在playbook中直接定义变量,我们还可以在某个文件中定义变量,然后再在playbook中引入对应的文件,引入文件后,playbook

即可使用文件中定义的变量,你可能会问,为什么要多此一举呢?这是因为在某些工作场景中这样做很有用,比如,你想要让别人阅读你的playbook,却不想让别人看到某些值,可以使用这种办法,因为别人在阅读playbook时,只能看到引入的变量名,但是看不到变量对应的值,这种将变量分离到某个文件中的做法叫做”变量文件分离”,”变量文件分离”除了能够隐藏某些值,还能够让你将不同类的信息放在不同的文件中,并且让这些信息与剧本主体分开。

先来看看”变量文件分离”的一些小例子

首先,我们来定义一个专门用来存放nginx相关变量的文件(文件名为nginx_vars.yml),在文件中定义变量时,不要使用vars关键字,直接定义变量即可,定义变量的语法与在playbook中定义变量的几种语法相同

语法一示例:
  testvar1: testfile
  testvar2: testfile2
语法二示例:
  - testvar1: testfile
  - testvar2: testfile2
语法三示例:
nginx:
  conf80: /etc/nginx/conf.d/80.conf
  conf8080: /etc/nginx/conf.d/8080.conf

你可以选择你觉得较为舒适的语法定义变量,如下所示,直接在nginx_vars.yml文件中定义变量即可。

# cat nginx_vars.yml
nginx:
  conf80: /etc/nginx/conf.d/80.conf
  conf8080: /etc/nginx/conf.d/8080.conf

在nginx_vars.yml中定义完相关变量后,即可在playbook中引入文件中的变量,在playbook中引入包含变量的文件时,需要使用”vars_files”关键字,被引入的文件需要以”- “开头,以YAML中块序列的语法引入,示例如下

---
- hosts: test70
  remote_user: root
  vars_files:
  - /testdir/ansible/nginx_vars.yml
  tasks:
  - name: task1
    file:
      path={{nginx.conf80}}
      state=touch
  - name: task2
    file:
      path={{nginx['conf8080']}}
      state=touch

上例中使用”vars_files”关键字引入了对应的变量文件,然后使用了文件中定义的变量。

上例中”vars_files”关键字只引入了一个变量文件,也可以引入多个变量文件,每个被引入的文件都需要以”- “开头,示例如下

  vars_files:
  - /testdir/ansible/nginx_vars.yml
  - /testdir/ansible/other_vars.yml

“vars”关键字和”vars_files”关键字可以同时使用,如下

  vars:
  - conf90: /etc/nginx/conf.d/90.conf
  vars_files:
  - /testdir/ansible/nginx_vars.yml

除了上述总结,ansible还有一些其他的关于变量的使用方法,暂且放到下一篇文章吧,希望这篇文章能够帮助到你,加油~

ansible笔记(13):tags的用法

这篇文章会介绍playbook中tags的用法。

你写了一个很长的playbook,其中有很多的任务,这并没有什么问题,不过在实际使用这个剧本时,你可能只是想要执行其中的一部分任务而已,或者,你只想要执行其中一类任务而已,而并非想要执行整个剧本中的全部任务,这个时候我们该怎么办呢?我们可以借助tags实现这个需求。

见名知义,tags可以帮助我们对任务进行’打标签’的操作,当任务存在标签以后,我们就可以在执行playbook时,借助标签,指定执行哪些任务,或者指定不执行哪些任务了,这样说可能不够直观,我们来看一个小示例(为了方便示例此处只写3个任务进行举例)。

---
- hosts: test70
  remote_user: root
  tasks:
  - name: task1
    file:
      path: /testdir/t1
      state: touch
    tags: t1
  - name: task2
    file: path=/testdir/t2
          state=touch
    tags: t2
  - name: task3
    file: path=/testdir/t3
          state=touch
    tags: t3

如上例所示,上例的play中有3个task,每个task都有对应的tags,为了方便示例,我只是简单的把tags的值写成了t1、t2、t3,在实际的使用中,我们应该让tags的值能够见名知义,现在每个task都有了标签,假如在执行上述playbook时,我们只想执行task2,该怎样执行呢?我们可以使用如下命令

ansible-playbook --tags=t2 testtag.yml

如你所见,可以使用–tags选项指定某个标签,当指定标签后,只有标签对应的任务会被执行,其他任务都不会被执行,执行上述命令后,只有task2会执行,因为task2的标签值为t2,task1和task3都不会执行,这样就达到了只执行playbook中部分任务的目的。

借助标签,除了能够指定”需要执行的任务”,还能够指定”不执行的任务”,示例命令如下。

ansible-playbook --skip-tags='t2' testtag.yml

我们可以使用 –skip-tags选项指定”不执行的任务”,执行上述命令后,task1和task3会执行,task2不会执行,因为我们已经在命令中指定了’跳过’标签t2所对应的任务,相当于使用了’排除法’,t2对应的任务被排除了,其他任务都会执行。

除了使用上例中的语法指定标签,还能够使用下例中的两种语法指定标签的值。

---
- hosts: test70
  remote_user: root
  tasks: 
  - name: task1
    file: 
      path: /testdir/t1
      state: touch
    tags:
      - t1
  - name: task2
    file: path=/testdir/t2
          state=touch
    tags: ['t2']

之前描述的三种语法都可以指定标签,不过上例中,每个任务只有一个标签,其实,我们可以为每个任务添加多个标签,三种语法添加多个标签的示例如下

语法一:
tags:
 - testtag
 - t1

语法二:
tags: tag1,t1

语法三:
tags: ['tagtest','t2']

上述示例的语法一使用了YAML块序列的语法格式指定多个标签,语法二与语法三都是在原来语法的基础上,使用’逗号’隔开多个标签。

如下例所示,不同的任务可以使用相同的标签。

---
- hosts: test70
  remote_user: root
  tasks:
  - name: install httpd package
    tags: httpd,package
    yum:
      name=httpd
      state=latest

  - name: start up httpd service
    tags: httpd,service
    service:
      name: httpd
      state: started

上例中每个任务都有多个标签,而且上例中两个任务都有一个共同的标签,就是httpd标签,所以,当我们执行’ansible-playbook –tags=httpd testhttpd.yml’,上述两个任务都会执行。

上例的play中的所有任务都有共同的httpd标签,像这种情况,我们可以把httpd标签提取出来,写在play中,示例如下。

---
- hosts: test70
  remote_user: root
  tags: httpd
  tasks:
  - name: install httpd package
    tags: ['package']
    yum:
      name=httpd
      state=latest

  - name: start up httpd service
    tags:
      - service
    service:
      name: httpd
      state: started

当tags写在play中而非task中时,play中的所有task会继承当前play中的tags,而上例中,两个任务都会继承httpd标签,同时还有拥有自己的标签。

在调用标签时,也可以一次性指定多个标签,调用多个标签需要用逗号隔开,命令如下

ansible-playbook --tags package,service testhttpd.yml

在调用标签之前,如果你想要概览一下playbook中都有哪些标签,可以使用 ‘ –list-tags’ 选项,示例如下

ansible-playbook --list-tags testhttpd.yml

其实,ansible还预置了5个特殊tag,这5个特殊tag分别为

always

never(2.5版本中新加入的特殊tag)

tagged

untagged

all

当我们把任务的tags的值指定为always时,那么这个任务就总是会被执行,除非你使用’–skip-tags’选项明确指定不执行对应的任务,这样说可能不容易理解,不如看个小示例,示例如下

---
- hosts: test70
  remote_user: root
  tasks:
  - name: task1
    file:
      path: /testdir/t1
      state: touch
    tags:
      - t1
  - name: task2
    file: path=/testdir/t2
          state=touch
    tags: ['t2']
  - name: task3
    file: path=/testdir/t3
          state=touch
    tags: t3,always

上例中,task3的标签有两个,t3和always,那么我们来执行一下这个playbook,假设,我只想运行上述playbook中标签为t1的任务,那么我会执行如下图中的命令

未分类

如图所示,在执行上述playbook时,我只指定了’t1’,正常情况下应该只执行’t1’对应的任务,也就是应该只执行task1,但是实际上执行了task1和task3,这是因为task3的标签的值包含always关键字,所以即使task3对应的标签没有被调用,task3也会执行,这就是always的作用。

如果你不想执行标签中包含always的任务,你可以使用’–skip-tags’选项明确指定跳过它们,仍然以上例的playbook为例,假设我们就是不想执行task3,我们可以执行如下命令

ansible-playbook --skip-tags always testtag.yml

但是需要注意,如果上述play中有多个任务都有always标签,那么上述命令将会跳过所有包含always标签的任务,如果上例中的play中的多个任务都有always标签,则可以使用如下命令只跳过task3,其他带有always标签的任务不会跳过,前提是task3有除了always以外的自定义标签。

ansible-playbook --skip-tags t3 testtag.yml

在2.5版本的ansible中,引入了新的特殊标签 ‘never’,从字面上理解,never的作用应该与always正好相反,由于我当前使用的ansible版本为2.4(还没有引入never标签),所以当指定任务的标签为never时,貌似被ansible当做了自定义标签,所以如果你安装了2.5版本的ansible,可以尝试一下never标签的作用,由于还没有实际使用过2.5版本,所以此处暂时不进行示例。

剩余的三个特殊标签分别为 tagged、untagged、all

这三个特殊标签并非像always一样,always作为标签值存在,而这三个特殊标签则是在调用标签时使用,示例如下

ansible-playbook --tags tagged testtag.yml

上述命令表示只执行有标签的任务,没有任何标签的任务不会被执行。

ansible-playbook --skip-tags tagged testtag.yml

上述命令表示跳过包含标签的任务,即使对应的任务包含always标签,也会被跳过。

ansible-playbook --tags untagged testtag.yml

上述命令表示只执行没有标签的任务,但是如果某些任务包含always标签,那么这些任务也会被执行。

ansible-playbook --skip-tags untagged testtag.yml

上述命令表示跳过没有标签的任务。

特殊标签all表示所有任务会被执行,不用指定,默认情况下就是使用这个标签。

关于标签的使用就先总结到这里,希望能够对你有所帮助~

ansible笔记(10):初识ansible playbook

前文中,我们介绍了一些ansible的常用模块,聪明如你,一定已经掌握了这些模块的使用方法。

那么现在,我们来想象一个工作场景,看看怎样把之前的知识点应用到这个工作场景中。

假设,我们想要在test70主机上安装nginx并启动,我们可以在ansible主机中执行如下3条命令

ansible test70 -m yum_repository -a 'name=aliEpel description="alibaba EPEL" baseurl=https://mirrors.aliyun.com/epel/$releaseverServer/$basearch/'
ansible test70 -m yum -a 'name=nginx disable_gpg_check=yes enablerepo=aliEpel'
ansible test70 -m service -a "name=nginx state=started"

我们通过上述3条命令,先确定配置了对应的yum源,然后使用yum模块安装了nginx,最后使用service模块启动了nginx,最终达到了我们的目的。

但是在实际的工作环境中,我们可能需要经常在新主机上安装nginx,难道每次有新的服务器加入工作环境,我们都要修改上述3条命令中的主机名并且重新将每一条命令执行一遍吗?这样似乎有些麻烦,肯定有更好的办法,没错,我们可以将上述命令写成脚本,每次修改一些变量,然后执行脚本就行了,这样似乎方便了不少,而ansible天生就提供了这种类似”脚本”的功能,在ansible中,类似”脚本”的文件被称作”剧本”,’剧本’的英文名称为’playbook’,我们只需要将要做的事情编写成playbook,把不同的模块按照顺序编排在剧本中,ansible就会按照剧本一步一步的执行,最终达到我们的目的,虽然playbook的功能与脚本类似,但是剧本并不是简单的将ad-hoc命令按照顺序堆砌在一个可执行文件中,编写剧本需要遵循YAML语法,如果你没有接触过YAML语法,不用害怕,坚持看完后面的示例,熟悉一些固定套路以后,你也可以自己编写playbook。

那么怎样编写playbook呢?我们先从一个简单的示例开始吧~

首先,我们需要创建一个YAML格式的playbook文件。

playbook文件以”.yaml”或者”.yml”作为文件名后缀,此处我们创建一个名为”test.yml”的剧本文件。

在编写剧本之前,我们先来回顾两个简单的ad-hoc命令,比如如下两条命令

ansible test70 -m ping
ansible test70 -m file -a "path=/testdir/test state=directory"

上述命令表示使用ping模块去ping主机test70,然后再用file模块在test70主机上创建目录,那么,如果把上述命令转换成playbook的表现形式,该如何书写呢?示例如下

(此处先进行示例,后文会说明怎样执行playbook)

未分类

如上所示,第一行使用三个横杠作为开始,在YAML语法中,”—“表示文档开始。

第二行使用”- “作为开头(注意:横杠后面有空格),如果你了解过YAML语法,那么你一定知道YAML使用”- “表示一个块序列的节点,如果你不了解YAML语法,不用在此处纠结,先这么写,写的多了,自然会理解,从上例可以看出,”- “后面使用hosts关键字指定了要操作的主机,hosts关键字对应的值为test70,表示我们要在test70主机上进行操作,”hosts: test70″是一个键值对,注意,在YAML语法中使用冒号映射键值对时,’冒号’后面必须有’空格’,这也是语法,没有为什么,记住就好,如果你想要一次性在多台主机上进行操作,可以同时写多个主机,每台主机使用逗号隔开,比如’hosts: test70,test61’,如果你在清单中对主机进行了分组,也可以使用组名。

第三行,使用remote_user关键字可以指定在进行远程操作时使用哪个用户进行操作,’remote_user: root’表示test70的root用户进行操作,上图中,remote_user关键字与hosts关键字对齐,表示它们是平级的,之前的文章中提到过,在YAML语法中进行缩进时,不能使用tab键进行缩进,必须使用空格,所以,为了兼容使用tab键进行缩进的使用习惯,可以将vim编辑器设置为自动将tab转成空格。

第四行,使用tasks关键字指明要进行操作的任务列表,之后的行都属于tasks键值对中的值。

之后的行都属于tasks任务列表中的任务,可以看出,整个任务列表一共有两个任务组成,每个任务都以”- “开头,每个任务都有自己的名字,任务名使用name关键字进行指定,第一个任务使用ping模块,使用ping模块时没有指定任何参数。第二个任务使用file模块,使用file模块时,指定了path参数与state参数的值。

好了,test.yml已经编写完成了,但是,我们还没有运行这个剧本,运行剧本需要使用’ansible-playbook’命令,示例如下

未分类

如上图所示,playbook执行后返回了一些信息,这些信息是这次剧本运行的概况。

‘PLAY [test70]’表示这次运行的playbook中有一个’play’是针对test70这台主机运行的,一个’playbook’是由一个或多个’play’组成的,这样说可能不太容易理解,那么我们打个比方,一个’剧本’是由一个或多个’桥段’组成的,每个桥段都有不同的场景、人物、故事,所有的桥段组合在一起,组成一个完整的剧本,剧本就是playbook,桥段就是play,而上例中,整个剧本中只有一个桥段,也就是说,上例的playbook中,我们只写了一个play,在下文中我们会举例说明怎样书写多个play,到时候你会更加明白到底什么是所谓的’play’,当然,’桥段’只是我自己为了方便理解给’play’起的中文名,官方名称只叫”play”。

从上述信息可以看出,仅有的这个play是针对test70运行的,这个play一共包含三个任务,第一个任务的名字叫做’Gathering Facts’,第二个任务的名字叫做’Ping the host’,第三个任务的名字叫做’make directory test’,看到此处你会发现,我们在playbook中明明只写了两个任务,为什么最后执行时却有三个任务呢?这是因为,每个play在执行时,都会先执行一个默认任务,这个默认任务就是’Gathering Facts’,’Gathering Facts’任务会收集当前play对应的目标主机的相关信息,收集完这些基础信息后,才会执行我们指定的任务,由于上例中,hosts的值只有test70一个主机,所以这个play只针对test70运行,所以’Gathering Facts’这个任务只收集了test70的相关信息,执行完默认任务后,开始执行’Ping the host’任务和’make directory test’任务,由于在执行playbook之前,/testdir/test目录已经存在于test70主机中,所以’make directory test’任务返回的信息是绿色的,如果对应的目录并不存在,’make directory test’任务返回的信息应该是黄色的,这是因为幂等性的缘故,前文已经解释过,此处不再赘述。

当playbook中的所有play执行完毕后,在返回信息的’PLAY RECAP’中可以对所有目标主机的执行情况进行’回顾’,由于我们的目标主机只有test70一台,所以只看到了test70的执行概括信息。

我们已经执行了一个playbook,这个playbook中只有一个play,我们也可以在这个playbook中多写几个play,示例如下

未分类

如上图所示,上例中有多个play,第一个play针对test70执行,这个play会执行两个任务。

第二个play针对test61和test70执行,第二个play只包含一个任务,即在对应的目标主机上创建/tfile文件

第三个play针对test60和test61执行,细心如你一定发现了,当你需要在play中指定多个主机时,有两种语法可以使用,第二个play和第三个play中的语法都可以指定多个主机,第三个play也只包含一个任务,即在test60和test61主机上创建zsythink用户。

多个play写完以后,我们来运行一下剧本试试。

未分类

如下图所示,每个play执行时都会显示当前play对应的目标主机,并且每个play执行时都会先执行默认的facts任务,收集对应目标主机的信息,当所有play都执行完毕后,会出现’PLAY RECP’,相当于一个小的报告信息,看到这里,你应该已经理解了到底什么是所谓的’play’了吧。

再次强调一遍,如果你并不是很熟悉YAML语法,你可以不用过于深究YAML语法,只需要死记硬背,记住某些”固定套路”即可编写playbook,写的多了,自然会对这些语法有所理解的。

如果你的playbook写完了,但是你不能确定playbook文件中是否存在语法错误,那么你可以使用如下命令对playbook进行语法检查。

ansible-playbook --syntax-check /testdir/ansible/test.yml

执行上述命令后,如果只返回了playbook的名称,就表示没有语法错误。

除了对playbook进行语法测试,我们还能够’模拟执行’playbook,’模拟执行’并不是真正的执行,只是’假装’执行一下,playbook中的任务并不会真正在目标主机中运行,所以你可以放心大胆的进行模拟,使用如下命令即可模拟运行playbook,模拟运行功能可以帮助我们’预估’playbook是否能够正常执行。

ansible-playbook --check test.yml

注意:使用上述命令进行’模拟’时,一些任务可能会报错,这可能是因为报错的任务在执行时需要依赖之前的其他任务的完成结果,但是因为是’模拟’执行,所以之前的任务并不会真正的执行,既然之前的任务没有真正的执行,自然不会产生对应的结果,所以后面的任务就报错了,也就是说,我们并不能完全以’模拟’的反馈结果作为playbook是否能够正常运行的判断依据,只能通过’模拟’大概的’预估’一下而已。

通过这篇文章可以初步的了解playbook,希望能够对你有所帮助。

ansible笔记(12):handlers的用法

这篇文章会介绍playbook中handlers的用法。

不过在开始介绍它们之前,我们先来描述一个工作场景。

当我们修改了某些程序的配置文件以后,有可能需要重启应用程序,以便能够使新的配置生效,那么,如果使用playbook来实现这个简单的功能,该怎样编写playbook呢?

我们来试试,此处我们使用nginx作为示例,虽然nginx可以使用’nginx -s reload’命令重载配置,但是此处的示例中并不会使用这个命令,而是用nginx类比那些需要重启生效的应用。

假设我们想要将nginx中的某个server的端口从8080改成8088,并且在修改配置以后重启nginx,那么我们可以编写如下剧本。

---
- hosts: test70
  remote_user: root
  tasks:
  - name: Modify the configuration
    lineinfile:
      path=/etc/nginx/conf.d/test.zsythink.net.conf
      regexp="listen(.*) 8080 (.*)"
      line="listen1 8088 2"
      backrefs=yes
      backup=yes
  - name: restart nginx
    service:
      name=nginx
      state=restarted

上述play表示修改test70主机的/etc/nginx/conf.d/test.zsythink.net.conf配置文件,将监听端口8080改为监听端口8088,端口修改完成后,重启服务。

在执行这个playbook之前,我们先来确认一下test70主机的8080端口是否被监听

未分类

可以看到test70主机上的8080正常被监听,那么现在我们来执行一下上述playbook,看一下执行效果

执行后可以看到,play中的两个任务都被正常执行了,如下图所示

未分类

此时再次查看test70主机的端口号,已经从8080改为8088,如下图所示

未分类

这样没有任何问题,与我们预期的一样,端口号从8080修改为8088,重启了服务

那么,我们再来重复执行一遍上述playbook试试,看看会出现什么情况,重复执行效果如下

未分类

如上图所示,当我们再次执行同样的playbook时,由于配置文件中的端口号已经是8088,所以,任务”Modify the configuration”的状态为OK(换句话说,这个任务并没有在远程主机进行任何实际操作),这是由于ansible的幂等性造成的(前文已经对幂等性做出了解释,此处不再赘述),因为目标状态与我们预期的状态一致,所以ansible并没有做任何改动,这是完全正常的,从上图可以看出,任务”restart nginx”也正常的执行了,而且是”真正的”执行了,换句话说就是它的确重启了对应的nginx服务,对远程主机进行了实际的操作。

第二次运行剧本的过程似乎没有什么问题,但是仔细想想,又有些不妥,因为我们重启服务的目的是为了在修改配置文件以后使新的配置生效,而第二次运行剧本的这种情况下,我们并没有真正修改服务器配置,因为服务器配置本来 就与我们预期的一致,但是,在没有修改配置的情况下,仍然重启了服务,这种重启是不需要的,我们想要达到的效果是,如果配置文件发生了改变,则重启服务,如果配置文件并没有被真正的修改,则不对服务进行任何操作,这种情况下,我们该怎们办呢?

handlers就是来解决这种问题的,此处我们先大概的描述一下handlers的概念,后面会给出示例,你可以把handlers理解成另一种tasks,handlers是另一种’任务列表’,handlers中的任务会被tasks中的任务进行”调用”,但是,被”调用”并不意味着一定会执行,只有当tasks中的任务”真正执行”以后(真正的进行实际操作,造成了实际的改变),handlers中被调用的任务才会执行,如果tasks中的任务并没有做出任何实际的操作,那么handlers中的任务即使被’调用’,也并不会执行。这样说似乎不容易被理解,我们来写一个小示例,示例如下。

---
- hosts: test70
  remote_user: root
  tasks:
  - name: Modify the configuration
    lineinfile:
      path=/etc/nginx/conf.d/test.zsythink.net.conf
      regexp="listen(.*) 8080(.*)"
      line="listen1 80882"
      backrefs=yes
      backup=yes
    notify:
      restart nginx

  handlers:
  - name: restart nginx
    service:
      name=nginx
      state=restarted

如上例所示,我们使用handlers关键字,指明哪些任务可以被’调用’,之前说过,handlers是另一种任务列表,你可以把handlers理解成另外一种tasks,你可以理解成它们是’平级’的,所以,handlers与tasks是’对齐’的(缩进相同),上例中的handlers中只有一个任务,这个任务的名称为”restart nginx”,之前也说明过,handlers中的任务需要被tasks中的任务调用,那么上例中,”restart nginx”被哪个任务调用了呢?很明显,”restart nginx”被”Modify the configuration”调用了,没错,如你所见,我们使用notify关键字’调用’handlers中的任务,或者说,通过notify关键字’通知’handlers中的任务,所以,综上所述,上例中的play表示,如果”Modify the configuration”真正的修改了配置文件(实际的操作),那么则执行”restart nginx”任务,如果”Modify the configuration”并没有进行任何实际的改动,则不执行”restart nginx” ,这就是handlers的作用,聪明如你肯定已经明白了,动手执行一下上述playbook试试吧。

handlers是另一种任务列表,所以handlers中可以有多个任务,被tasks中不同的任务notify,示例如下

---
- hosts: test70
  remote_user: root
  tasks:
  - name: make testfile1
    file: path=/testdir/testfile1
          state=directory
    notify: ht2
  - name: make testfile2
    file: path=/testdir/testfile2
          state=directory
    notify: ht1

  handlers:
  - name: ht1
    file: path=/testdir/ht1
          state=touch
  - name: ht2
    file: path=/testdir/ht2
          state=touch

如上例所示,tasks与handlers都是任务列表,只是handlers中的任务被tasks中的任务notify罢了,那么我们来执行一下上述playbook,如下图所示

未分类

从上图可以看出,handler执行的顺序与handler在playbook中定义的顺序是相同的,与”handler被notify”的顺序无关。

如上图所示,默认情况下,所有task执行完毕后,才会执行各个handler,并不是执行完某个task后,立即执行对应的handler,如果你想要在执行完某些task以后立即执行对应的handler,则需要使用meta模块,示例如下

---
- hosts: test70
  remote_user: root
  tasks:
  - name: task1
    file: path=/testdir/testfile
          state=touch
    notify: handler1
  - name: task2
    file: path=/testdir/testfile2
          state=touch
    notify: handler2

  - meta: flush_handlers

  - name: task3
    file: path=/testdir/testfile3
          state=touch
    notify: handler3

  handlers:
  - name: handler1
    file: path=/testdir/ht1
          state=touch
  - name: handler2
    file: path=/testdir/ht2
          state=touch
  - name: handler3
    file: path=/testdir/ht3
          state=touch

如上例所示,我在task1与task2之后写入了一个任务,我并没有为这个任务指定name属性,这个任务使用meta模块,meta任务是一种特殊的任务,meta任务可以影响ansible的内部运行方式,上例中,meta任务的参数值为flush_handlers,”meta: flush_handlers”表示立即执行之前的task所对应handler,什么意思呢?意思就是,在当前meta任务之前,一共有两个任务,task1与task2,它们都有对应的handler,当执行完task1与task2以后,立即执行对应的handler,而不是像默认情况那样在所有任务都执行完毕以后才能执行各个handler,那么我们来实际运行一下上述剧本,运行结果如下

未分类

正如上图所示,meta任务之前的任务task1与task2在进行了实际操作以后,立即运行了对应的handler1与handler2,然后才运行了task3,在所有task都运行完毕后,又逐个将剩余的handler根据情况进行调用。

聪明如你一定想到了,如果想要每个task在实际操作后都立马执行对应handlers,则可以在每个任务之后都添加一个meta任务,并将其值设置为flush_handlers 所以,我们可以依靠meta任务,让handler的使用变得更加灵活,快动手试试吧。

我们还可以在一个task中一次性notify多个handler,怎样才能一次性notify多个handler呢?你可能会尝试将多个handler使用相同的name,但是这样并不可行,因为当多个handler的name相同时,只有一个handler会被执行,所以,我们并不能通过这种方式notify多个handler,如果想要一次notify多个handler,则需要借助另一个关键字,它就是’listen’,你可以把listen理解成”组名”,我们可以把多个handler分成”组”,当我们需要一次性notify多个handler时,只要将多个handler分为”一组”,使用相同的”组名”即可,当notify对应的值为”组名”时,”组”内的所有handler都会被notify,这样说可能还是不容易理解,我们来看个小示例,示例如下

---
- hosts: test70
  remote_user: root
  tasks:
  - name: task1
    file: path=/testdir/testfile
          state=touch
    notify: handler group1

  handlers:
  - name: handler1
    listen: handler group1
    file: path=/testdir/ht1
          state=touch
  - name: handler2
    listen: handler group1
    file: path=/testdir/ht2
          state=touch

如上例所示,handler1与handler2的listen的值都是handler group1,当task1中notify的值为handler group1时,handler1与handler2都会被notify,还是很方便的。

关于handlers的使用就先总结到这里,希望能够对你有所帮助。

ansible笔记(11):初识ansible playbook(二)

前文中,我们已经编写了 一个简单的剧本,这篇文章继续了解一下playbook的一些基础。

有前文作为基础,如下示例是非常容易理解的:

---
- hosts: test70
  remote_user: root
  tasks:
  - name: make testfile
    file:
      path: /testdir/testfile
      state: touch
      mode: 0700

上例中有一个play,这个play针对test70主机运行,这个play的任务列表中只有一个任务,这个任务就是调用file模块,确保/testdir/testfile文件存在并且testfile文件的权限为0700,把上例中的任务列表部分单独截取出来,如下所示

tasks:
- name: make testfile
  file:
    path: /testdir/testfile
    state: touch
    mode: 0700

正如你所看到的,”path: /testdir/testfile” 表示为file模块的path参数赋值,我们使用”冒号”(冒号后有空格)对参数赋值。

其实,除了这种使用冒号的方式,我们还可以使用如下格式为模块的参数赋值

tasks:
- name: make testfile
  file: path=/testdir/testfile state=touch mode=0700

如上所示,我们调用file模块时,设置了三个参数,path参数、state参数、mode参数,为参数赋值时,使用了”等号”,每个参数之间使用空格隔开,这种格式也是完全正确的,如果你在使用一个模块时设置的参数比较多,那么使用上述格式设置参数时,这些参数可能会”挤在一行”里面,你也可以把它们分成多行去写,如下例所示

tasks:
- name: make testfile
  file: path=/testdir/testfile
        state=touch mode=0700

即使把多个参数分行写,也需要注意缩进。

上述书写格式都是0.8版本以后的ansible推荐的书写格式,在0.8版本之前,使用action关键字调用模块,示例如下:

tasks:
- name: make testfile
  action: file path=/testdir/testfile state=touch mode=0700

如上例所示,使用action关键字调用对应的模块,在当前版本中(博客中的ansible版本为2.4)仍然兼容这种语法

在之前的示例中,我们对每个任务都指定了对应的名称,即每个task都有对应的name,当我们省略name时,默认以当前任务调用的模块的名称作为任务的名称,不过建议不要省略name,因为当任务存在name时,可读性比较高。

在编写任务时,我习惯将name属性写在任务的开头,当然,任务的各个属性并没有严格的顺序要求,如下两种写法的效果是相同的。

写法一:
tasks:
- name: make testfile
  file: path=/testdir/testfile
        state=touch
        mode=0700

写法二:
tasks:
- file: path=/testdir/testfile
        state=touch
        mode=0700
  name: make testfile

各属性顺序虽然没有要求,但是仍然需要严格按照缩进进行对齐。

关于这些基础,就先暂时总结到这里,希望能够对你有所帮助。