ansible: reload /etc/resolv.conf for each task.

The OpenShift installer modifies /etc/resolv.conf then tests the new
resolver configuration, however, there was no mechanism to reload
resolv.conf in our reuseable interpreter.

https://github.com/openshift/openshift-ansible/blob/release-3.9/roles/openshift_web_console/tasks/install.yml#L137

This inserts an explicit call to res_init() for every new style
invocation, with an approximate cost of ~1usec on Linux since glibc
verifies resolv.conf has changed before reloading it.

There is little to be done for users of the thread-safe resolver APIs,
their state is hidden from us. If bugs like that manifest, whack-a-mole
style 'del sys.modules[thatmod]' patches may suffice.
This commit is contained in:
David Wilson 2018-05-07 11:46:24 +01:00
parent c00e7ed367
commit 2ad0d0521d
5 changed files with 71 additions and 0 deletions

View File

@ -37,6 +37,7 @@ how to build arguments for it, preseed related data, etc.
from __future__ import absolute_import
import cStringIO
import ctypes
import json
import logging
import os
@ -57,6 +58,17 @@ except ImportError:
import ansible.module_utils.basic
ansible.module_utils.basic._ANSIBLE_ARGS = '{}'
# For tasks that modify /etc/resolv.conf, non-Debian derivative glibcs cache
# resolv.conf at startup and never implicitly reload it. Cope with that via an
# explicit call to res_init() on each task invocation. BSD-alikes export it
# directly, Linux #defines it as "__res_init".
libc = ctypes.CDLL(None)
libc__res_init = None
for symbol in 'res_init', '__res_init':
try:
libc__res_init = getattr(libc, symbol)
except AttributeError:
pass
LOG = logging.getLogger(__name__)
@ -397,6 +409,8 @@ class NewStyleRunner(ScriptRunner):
# module, but this has never been a bug report. Instead act like an
# interpreter that had its script piped on stdin.
self._argv = TemporaryArgv([''])
if libc__res_init:
libc__res_init()
def revert(self):
self._argv.revert()

View File

@ -13,3 +13,4 @@
- import_playbook: remote_tmp/all.yml
- import_playbook: runner/all.yml
- import_playbook: ssh/all.yml
- import_playbook: glibc_caches/all.yml

View File

@ -0,0 +1,2 @@
- import_playbook: resolv_conf.yml

View File

@ -0,0 +1,38 @@
# This cannot run against localhost, it damages /etc
- name: integration/glibc_caches/resolv_conf.yml
gather_facts: true
become: true
hosts: test-targets
vars:
ansible_become_pass: has_sudo_pubkey_password
tasks:
- debug: msg={{hostvars}}
- mitogen_test_gethostbyname:
name: www.google.com
register: out
when: ansible_virtualization_type == "docker"
- shell: cp /etc/resolv.conf /tmp/resolv.conf
when: ansible_virtualization_type == "docker"
- shell: echo > /etc/resolv.conf
when: ansible_virtualization_type == "docker"
- mitogen_test_gethostbyname:
name: www.google.com
register: out
ignore_errors: true
when: ansible_virtualization_type == "docker"
- shell: cat /tmp/resolv.conf > /etc/resolv.conf
when: ansible_virtualization_type == "docker"
- assert:
that:
- out.failed
- '"Name or service not known" in out.msg or
"Temporary failure in name resolution" in out.msg'
when: ansible_virtualization_type == "docker"

View File

@ -0,0 +1,16 @@
#!/usr/bin/python
# I am a module that indirectly depends on glibc cached /etc/resolv.conf state.
import socket
from ansible.module_utils.basic import AnsibleModule
def main():
module = AnsibleModule(argument_spec={'name': {'type': 'str'}})
try:
module.exit_json(addr=socket.gethostbyname(module.params['name']))
except socket.error, e:
module.fail_json(msg=str(e))
if __name__ == '__main__':
main()