发现变量:事实和魔法变量

使用 Ansible,咱们可以检索或发现一些包含远端系统,或 Ansible 本身信息的变量。与远端系统相关的变量,被称为事实,facts。有了事实,咱们就可以把一个系统的行为或状态,用作别的系统的配置。例如,咱们可以把某个系统的 IP 地址,作为另一系统的配置值。与 Ansible 相关的变量,被称为魔法变量,magic variables。

Ansible 事实

所谓 Ansible 事实,是与咱们远端系统相关的数据,包括操作系统、IP 地址、连接的文件系统等。咱们可在 ansible_facts 变量中,访问这些数据。默认情况下,咱们也可以 ansible_ 前缀的顶层变量形式,访问某些 Ansible 事实。咱们可使用 INJECT_FACTS_AS_VARS,设置禁用这一行为。要查看所有可用事实,请将此任务添加到某个 play:

- name: Print all available facts ansible.builtin.debug: var: ansible_facts

而要查看收集到的 “原始” 信息,请在命令行下运行此命令:

ansible <hostname> -m ansible.builtin.setup

译注:这里仍然需要指定仓库文件,如下所示:

ansible -i ansible_quickstart/inventory_updated.yaml debian-199 -m ansible.builtin.setup

事实包括了大量变量数据,这些数据可能如下所示:

{ "ansible_all_ipv4_addresses": [ "REDACTED IP ADDRESS" ], "ansible_all_ipv6_addresses": [ "REDACTED IPV6 ADDRESS" ], "ansible_apparmor": { "status": "disabled" }, "ansible_architecture": "x86_64", "ansible_bios_date": "11/28/2013", "ansible_bios_version": "4.1.5", "ansible_cmdline": { "BOOT_IMAGE": "/boot/vmlinuz-3.10.0-862.14.4.el7.x86_64", "console": "ttyS0,115200", "no_timer_check": true, "nofb": true, "nomodeset": true, "ro": true, "root": "LABEL=cloudimg-rootfs", "vga": "normal" }, "ansible_date_time": { "date": "2018-10-25", "day": "25", "epoch": "1540469324", "hour": "12", "iso8601": "2018-10-25T12:08:44Z", "iso8601_basic": "20181025T120844109754", "iso8601_basic_short": "20181025T120844", "iso8601_micro": "2018-10-25T12:08:44.109968Z", "minute": "08", "month": "10", "second": "44", "time": "12:08:44", "tz": "UTC", "tz_offset": "+0000", "weekday": "Thursday", "weekday_number": "4", "weeknumber": "43", "year": "2018" }, "ansible_default_ipv4": { "address": "REDACTED", "alias": "eth0", "broadcast": "REDACTED", "gateway": "REDACTED", "interface": "eth0", "macaddress": "REDACTED", "mtu": 1500, "netmask": "255.255.255.0", "network": "REDACTED", "type": "ether" }, "ansible_default_ipv6": {}, "ansible_device_links": { "ids": {}, "labels": { "xvda1": [ "cloudimg-rootfs" ], "xvdd": [ "config-2" ] }, "masters": {}, "uuids": { "xvda1": [ "cac81d61-d0f8-4b47-84aa-b48798239164" ], "xvdd": [ "2018-10-25-12-05-57-00" ] } }, "ansible_devices": { "xvda": { "holders": [], "host": "", "links": { "ids": [], "labels": [], "masters": [], "uuids": [] }, "model": null, "partitions": { "xvda1": { "holders": [], "links": { "ids": [], "labels": [ "cloudimg-rootfs" ], "masters": [], "uuids": [ "cac81d61-d0f8-4b47-84aa-b48798239164" ] }, "sectors": "83883999", "sectorsize": 512, "size": "40.00 GB", "start": "2048", "uuid": "cac81d61-d0f8-4b47-84aa-b48798239164" } }, "removable": "0", "rotational": "0", "sas_address": null, "sas_device_handle": null, "scheduler_mode": "deadline", "sectors": "83886080", "sectorsize": "512", "size": "40.00 GB", "support_discard": "0", "vendor": null, "virtual": 1 }, "xvdd": { "holders": [], "host": "", "links": { "ids": [], "labels": [ "config-2" ], "masters": [], "uuids": [ "2018-10-25-12-05-57-00" ] }, "model": null, "partitions": {}, "removable": "0", "rotational": "0", "sas_address": null, "sas_device_handle": null, "scheduler_mode": "deadline", "sectors": "131072", "sectorsize": "512", "size": "64.00 MB", "support_discard": "0", "vendor": null, "virtual": 1 }, "xvde": { "holders": [], "host": "", "links": { "ids": [], "labels": [], "masters": [], "uuids": [] }, "model": null, "partitions": { "xvde1": { "holders": [], "links": { "ids": [], "labels": [], "masters": [], "uuids": [] }, "sectors": "167770112", "sectorsize": 512, "size": "80.00 GB", "start": "2048", "uuid": null } }, "removable": "0", "rotational": "0", "sas_address": null, "sas_device_handle": null, "scheduler_mode": "deadline", "sectors": "167772160", "sectorsize": "512", "size": "80.00 GB", "support_discard": "0", "vendor": null, "virtual": 1 } }, "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.5.1804", "ansible_dns": { "nameservers": [ "127.0.0.1" ] }, "ansible_domain": "", "ansible_effective_group_id": 1000, "ansible_effective_user_id": 1000, "ansible_env": { "HOME": "/home/zuul", "LANG": "en_US.UTF-8", "LESSOPEN": "||/usr/bin/lesspipe.sh %s", "LOGNAME": "zuul", "MAIL": "/var/mail/zuul", "PATH": "/usr/local/bin:/usr/bin", "PWD": "/home/zuul", "SELINUX_LEVEL_REQUESTED": "", "SELINUX_ROLE_REQUESTED": "", "SELINUX_USE_CURRENT_RANGE": "", "SHELL": "/bin/bash", "SHLVL": "2", "SSH_CLIENT": "REDACTED 55672 22", "SSH_CONNECTION": "REDACTED 55672 REDACTED 22", "USER": "zuul", "XDG_RUNTIME_DIR": "/run/user/1000", "XDG_SESSION_ID": "1", "_": "/usr/bin/python2" }, "ansible_eth0": { "active": true, "device": "eth0", "ipv4": { "address": "REDACTED", "broadcast": "REDACTED", "netmask": "255.255.255.0", "network": "REDACTED" }, "ipv6": [ { "address": "REDACTED", "prefix": "64", "scope": "link" } ], "macaddress": "REDACTED", "module": "xen_netfront", "mtu": 1500, "pciid": "vif-0", "promisc": false, "type": "ether" }, "ansible_eth1": { "active": true, "device": "eth1", "ipv4": { "address": "REDACTED", "broadcast": "REDACTED", "netmask": "255.255.224.0", "network": "REDACTED" }, "ipv6": [ { "address": "REDACTED", "prefix": "64", "scope": "link" } ], "macaddress": "REDACTED", "module": "xen_netfront", "mtu": 1500, "pciid": "vif-1", "promisc": false, "type": "ether" }, "ansible_fips": false, "ansible_form_factor": "Other", "ansible_fqdn": "centos-7-rax-dfw-0003427354", "ansible_hostname": "centos-7-rax-dfw-0003427354", "ansible_interfaces": [ "lo", "eth1", "eth0" ], "ansible_is_chroot": false, "ansible_kernel": "3.10.0-862.14.4.el7.x86_64", "ansible_lo": { "active": true, "device": "lo", "ipv4": { "address": "127.0.0.1", "broadcast": "host", "netmask": "255.0.0.0", "network": "127.0.0.0" }, "ipv6": [ { "address": "::1", "prefix": "128", "scope": "host" } ], "mtu": 65536, "promisc": false, "type": "loopback" }, "ansible_local": {}, "ansible_lsb": { "codename": "Core", "description": "CentOS Linux release 7.5.1804 (Core)", "id": "CentOS", "major_release": "7", "release": "7.5.1804" }, "ansible_machine": "x86_64", "ansible_machine_id": "2db133253c984c82aef2fafcce6f2bed", "ansible_memfree_mb": 7709, "ansible_memory_mb": { "nocache": { "free": 7804, "used": 173 }, "real": { "free": 7709, "total": 7977, "used": 268 }, "swap": { "cached": 0, "free": 0, "total": 0, "used": 0 } }, "ansible_memtotal_mb": 7977, "ansible_mounts": [ { "block_available": 7220998, "block_size": 4096, "block_total": 9817227, "block_used": 2596229, "device": "/dev/xvda1", "fstype": "ext4", "inode_available": 10052341, "inode_total": 10419200, "inode_used": 366859, "mount": "/", "options": "rw,seclabel,relatime,data=ordered", "size_available": 29577207808, "size_total": 40211361792, "uuid": "cac81d61-d0f8-4b47-84aa-b48798239164" }, { "block_available": 0, "block_size": 2048, "block_total": 252, "block_used": 252, "device": "/dev/xvdd", "fstype": "iso9660", "inode_available": 0, "inode_total": 0, "inode_used": 0, "mount": "/mnt/config", "options": "ro,relatime,mode=0700", "size_available": 0, "size_total": 516096, "uuid": "2018-10-25-12-05-57-00" } ], "ansible_nodename": "centos-7-rax-dfw-0003427354", "ansible_os_family": "RedHat", "ansible_pkg_mgr": "yum", "ansible_processor": [ "0", "GenuineIntel", "Intel(R) Xeon(R) CPU E5-2670 0 @ 2.60GHz", "1", "GenuineIntel", "Intel(R) Xeon(R) CPU E5-2670 0 @ 2.60GHz", "2", "GenuineIntel", "Intel(R) Xeon(R) CPU E5-2670 0 @ 2.60GHz", "3", "GenuineIntel", "Intel(R) Xeon(R) CPU E5-2670 0 @ 2.60GHz", "4", "GenuineIntel", "Intel(R) Xeon(R) CPU E5-2670 0 @ 2.60GHz", "5", "GenuineIntel", "Intel(R) Xeon(R) CPU E5-2670 0 @ 2.60GHz", "6", "GenuineIntel", "Intel(R) Xeon(R) CPU E5-2670 0 @ 2.60GHz", "7", "GenuineIntel", "Intel(R) Xeon(R) CPU E5-2670 0 @ 2.60GHz" ], "ansible_processor_cores": 8, "ansible_processor_count": 8, "ansible_processor_nproc": 8, "ansible_processor_threads_per_core": 1, "ansible_processor_vcpus": 8, "ansible_product_name": "HVM domU", "ansible_product_serial": "REDACTED", "ansible_product_uuid": "REDACTED", "ansible_product_version": "4.1.5", "ansible_python": { "executable": "/usr/bin/python2", "has_sslcontext": true, "type": "CPython", "version": { "major": 2, "micro": 5, "minor": 7, "releaselevel": "final", "serial": 0 }, "version_info": [ 2, 7, 5, "final", 0 ] }, "ansible_python_version": "2.7.5", "ansible_real_group_id": 1000, "ansible_real_user_id": 1000, "ansible_selinux": { "config_mode": "enforcing", "mode": "enforcing", "policyvers": 31, "status": "enabled", "type": "targeted" }, "ansible_selinux_python_present": true, "ansible_service_mgr": "systemd", "ansible_ssh_host_key_ecdsa_public": "REDACTED KEY VALUE", "ansible_ssh_host_key_ed25519_public": "REDACTED KEY VALUE", "ansible_ssh_host_key_rsa_public": "REDACTED KEY VALUE", "ansible_swapfree_mb": 0, "ansible_swaptotal_mb": 0, "ansible_system": "Linux", "ansible_system_capabilities": [ "" ], "ansible_system_capabilities_enforced": "True", "ansible_system_vendor": "Xen", "ansible_uptime_seconds": 151, "ansible_user_dir": "/home/zuul", "ansible_user_gecos": "", "ansible_user_gid": 1000, "ansible_user_id": "zuul", "ansible_user_shell": "/bin/bash", "ansible_user_uid": 1000, "ansible_userspace_architecture": "x86_64", "ansible_userspace_bits": "64", "ansible_virtualization_role": "guest", "ansible_virtualization_type": "xen", "gather_subset": [ "all" ], "module_setup": true }

咱们可在模板或 playbook 中,引用以上所示事实中第一个磁盘的型号:

{{ ansible_facts['devices']['xvda']['model'] }}

要引用系统的主机名:

{{ ansible_facts['nodename'] }}

可以在条件(参见 条件)以及模板中,使用事实。咱们还可以使用事实,创建符合特定条件的动态主机组,详情请查看 group_by 模组 文档。

注意:由于 ansible_date_time 是每次运行 playbook 前,Ansible 收集事实时创建并缓存的,因此在一些长时间运行的 playbook 下,就可能会过时。如果咱们的 playbook 需要长时间运行,就要使用 pipe 过滤器(例如,lookup('pipe', 'date +%Y-%m-%d.%H:%M:%S')),或使用 Jinja 2 模板的 now(),代替 ansible_date_time

事实收集的软件包需求

在某些发行版上,咱们可能会看到一些缺失事实值,或设置为默认值的事实,因为支持收集这些事实的软件包默认未安装。咱们可在远端主机上,使用操作系统的软件包管理器,安装这些必要软件包。已知的依赖包包括:

  • Linux 的网络事实收集 - 依赖于 ip 这个二进制命令,通常包含在 iproute2 软件包中。

缓存事实

与注册的变量一样,事实默认都存储在内存中。不过,与注册的变量不同,事实可被独立收集并缓存下来,供重复使用。有了缓存的事实,在配置某第二个系统时,即使 Ansible 先在第二个系统上执行当前 play,咱们也可以参考某一个系统中的事实。例如:

{{ hostvars['asdf.example.com']['ansible_facts']['os_family'] }}

缓存是由缓存插件控制的。默认情况下,Ansible 使用的是在当前 playbook 运行期间,将事实存储在内存中的内存缓存插件。要保留 Ansible 事实以供重复使用,就要选择别的缓存插件。详情请参阅 缓存插件

事实的缓存,可以提高性能。若咱们管理着数千台主机,那么就可以将事实缓存配置为每晚运行,然后在全天定期对较小的某组服务器上的配置进行管理。有了缓存事实,即使只管理少量服务器,咱们也能访问所有主机的变量和信息。

关闭事实

默认情况下,Ansible 会在每个 play 开始时收集事实。如果咱们不需要收集事实(例如,如果咱们对咱们的系统所有情况都了如指掌),就可以在 play 级别关闭事实收集,以提高可扩张性。在托管系统数量非常多的推送模式下,或者在实验平台上使用 Ansible,关闭事实收集尤其能提高性能。要关闭事实收集:

- hosts: whatever gather_facts: false

添加定制事实

Ansible 中的设置模组,会自动发现每台主机的标准事实集。若咱们打算在事实中,添加一些定制的值,则可以编写某个定制的事实模组、使用一个 ansible.builtin.set_fact 任务设置一些临时事实,或使用 facts.d 目录提供一些永久的定制事实。

facts.d 或本地事实

版本 1.3 中的新特性

咱们可以通过向 facts.d 添加静态文件,来添加一些静态定制事实,或者向 facts.d 添加可执行脚本来添加一些动态事实。例如,咱们可以通过在 facts.d 中创建并运行脚本,来添加主机上所有用户的一个列表。

要使用 facts.d,就在远端主机上,创建 /etc/ansible/facts.d 目录。若咱们偏好别的目录,可以创建出目录,并使用 fact_path 这个 play 关键字指定出来。向该目录添加文件,以提供到咱们的定制事实。所有文件名必须以 .fact 结尾。文件可以是 JSON、INI 或返回 JSON 的可执行文件。

要添加静态事实,只需添加扩展名为 .fact 的文件即可。例如,创建包含以下内容的 /etc/ansible/facts.d/preferences.fact

[general] asdf=1 bar=2

注意:要确保该文件不可执行,否则会破坏 ansible.builtin.setup 模组。

下次事实收集运行时,咱们的事实将包含一个名为 general 的哈希变量事实,其成员为 asdfbar。要验证这点,请运行以下命令:

ansible -i ansible_quickstart/inventory_updated.yaml almalinux-5 -m ansible.builtin.setup -a "filter=ansible_local"

咱们将看到,咱们的定制事实已添加:

almalinux-5 | SUCCESS => { "ansible_facts": { "ansible_local": { "preferences": { "general": { "asdf": "1", "bar": "2" } } }, "discovered_interpreter_python": "/usr/bin/python3.9" }, "changed": false }

ansible_local 这个命名空间,将由 facts.d 创建的自定义事实,与系统事实或 playbook 中其他地方定义的变量分开,因此变量之间不会相互覆盖。咱们可在模板或 playbook 中,访问该定制事实:

{{ ansible_local['preferences']['general']['asdf'] }}

注意key=value 键值对中的关键字部分,在 ansible_local 变量中将被转换为小写。以上面的例子为例,如果那个 ini 文件的 [general] 小节包含了 XYZ=3,那么访问他的方式应该是 {{ ansible_local['preferences']['general']['xyz'] }},而不是 {{ ansible_local['preferences']['general']['XYZ'] }}。这是因为 Ansible 使用了 Python 的 ConfigParser,他通过 optionxform 方法,传递所有选项名,而该方法的默认实现会将选项名转换为小写。

咱们还可使用 facts.d,在远端主机上执行脚本,向 ansible_local 命名空间生成一些动态的定制事实。例如,咱们可以生成一个远端主机上所有用户的列表,作为该主机的一项事实。要使用 facts.d 生成动态的定制事实:

  1. 编写并测试一个生成所需 JSON 数据的脚本;
  2. 将该脚本保存在咱们的 facts.d 目录下;
  3. 确保咱们的脚本有着 .fact 文件扩展名;
  4. 确保咱们的搅拌可由 Ansible 连接用户执行;
  5. 要开启 gather_facts 以执行到该脚本,并将其 JSON 输出添加到 ansible_local

译注:通常咱们需要借助 jq 这个实用工具,从脚本产生 JSON 输出。

参考:

默认情况下,事实收集会在每个 play 开始时运行一次。如果咱们在某个 playbook 中使用 facts.d 创建了某项定制事实,那么他将在下一个收集事实的 play 中可用。如果咱们想在创建事实的同一 play 中使用他,就必须显式地重新运行 setup 模组。例如:

- hosts: webservers gather_facts: no tasks: - name: Install the package jq, tool for JSON like sed for text ansible.builtin.package: name: jq state: present - name: Create directory for ansible custom facts ansible.builtin.file: state: directory recurse: true path: /etc/ansible/facts.d - name: Install custom ipmi fact ansible.builtin.copy: src: ipmi.fact mode: '0755' dest: /etc/ansible/facts.d - name: Re-read facts after adding custom fact ansible.builtin.setup: filter: ansible_local - name: Show ansible_local debug: msg: "{{ ansible_local['ipmi'] }}"

如果咱们频繁使用这种模式,那么定制事实模组,会比 facts.d 效率更高。

译注:下面是个输出 Linux 主机上用户列表的脚本,其借助 jq 产生 JSON 字符串。

#!/usr/bin/env bash user_list=$( getent passwd | \ grep -vE '(nologin|false)$' | \ awk -F: -v min=`awk '/^UID_MIN/ {print $2}' /etc/login.defs` \ -v max=`awk '/^UID_MAX/ {print $2}' /etc/login.defs` \ '{if(($3 >= min)&&($3 <= max)) print $1}' | \ sort -u ) users_str="" while IFS=' ' read -ra u; do users_str="${users_str}${u}," done <<< "$user_list" users_str="${users_str::-1}" json_str=$( jq --null-input \ --arg users "${users_str}" \ '{users: $users}' ) echo "${json_str}"

关于 Ansible 的信息:魔法变量

咱们可以使用 “魔法” 变量,访问有关 Ansible 操作的信息,包括正使用的 Python 版本、仓库中的主机和组,以及 playbook 和角色的目录等。与连接变量一样,魔法变量也属于 特殊变量。魔法变量名称是保留的 - 因此不要使用这些名称设置变量。environment 这个变量也是保留的。

最常用的魔法变量分别是 hostvarsgroupsgroup_namesinventory_hostname。使用 hostvars,咱们就能在 playbook 的任何位置,访问为 play 中任何主机定义的变量。咱们也可使用 hostvars 这个变量,访问到 Ansible 事实,但只能在收集(或缓存)了事实之后。请注意,在 play 的对象中定义的变量,不是为特定主机定义的,因此不会映射到 hostvars

若咱们打算使用另一节点的某个 fact 的值,或指派给另一节点的某个仓库变量的值,来配置数据库服务器,就可以在模板或某个操作行中,使用 hostvars

{{ hostvars['test.example.com']['ansible_facts']['distribution'] }}

而使用 groups,即仓库中中所有组(以及主机)的一个列表,咱们就可以枚举出某个组内的所有主机。例如:

{% for host in groups['app_servers'] %} # something that applies to all app servers. {% endfor %}

咱们可以同时使用 groupshostvars,找到某个组中的所有 IP 地址。

{% for host in groups['app_servers'] %} {{ hostvars[host]['ansible_facts']['eth0']['ipv4']['address'] }} {% endfor %}

译注:模板文件中的组名 app_servers 要与 playbook YAML 文件中 hosts: app_servers 组别一致,否则会报出错误:"AnsibleUndefinedVariable: 'dict object' has no attribute 'enp1s0'"

咱们可以使用这种方法,将某个前端代理服务器,指向咱们应用程序服务器组中的所有主机,以及在服务器之间设置正确的防火墙规则等。在填充模板的任务前,咱们必须为这些主机缓存好事实,或收集到事实。

使用 group_names,即当前主机所在的全部组的一个列表(数组),咱们就可以创建出根据主机的所属组(或角色),而不同的模板文件:

{% if 'webserver' in group_names %} # some part of a configuration file that only applies to webservers {% endif %}

当事实收集被关闭时,咱们可以使用魔法变量 inventory_hostname,即在仓库中配置的主机名,作为 ansible_hostname 的替代。如果咱们的 FQDN 较长,则可以使用 inventory_hostname_short,其包含第一个句点之前的部分,而不包含域的其余部分。

别的一些有用魔法变量,指向了当前 play 或 playbook。对于要以多个主机名的填充模板,或将列表注入负载均衡器的规则中等情况,这些变量就很有用。

  • ansible_play_hosts 为当前 play 中,仍处于活动状态的所有主机列表;

  • ansible_play_batch 是该 play 的当前 “批次” 范围内的主机名列表; 批次数量由 serial 定义,在未设置时相当于整个 play(从而令到其与 ansible_play_hosts 相同)。

  • ansible_playbook_python 是用于调用 Ansible 命令行工具的 Python 可执行文件路径;

  • inventory_dir 是存放 Ansible 仓库主机文件的目录路径名;

  • inventory_file 是指向 Ansible 仓库主机文件的路径名与文件名;

  • playbook_dir 包含着 playbook 的基础目录;

  • role_path 包含当前角色的路径名,且仅在角色内部有效;

  • ansible_check_mode 是个布尔值,如果使用了 --check 运行 Ansible,则就被设置为了 True

Ansible 版本号

版本 1.8 中的新特性

要将 playbook 的行为,与不同版本的 Ansible 适配,咱们可以使用 ansible_version 这个变量,其具有以下结构:

{ "full": "2.18.1", "major": 2, "minor": 18, "revision": 1, "string": "2.18.1" }

(End)

Last change: 2025-02-03, commit: 5907890

小额打赏,赞助 xfoss.com 长存......

微信 | 支付宝

若这里内容有帮助到你,请选择上述方式向 xfoss.com 捐赠。