mirror of
https://github.com/freeipa/ansible-freeipa.git
synced 2026-05-07 13:53:23 +00:00
The changed state returned from ipaclient_configure_dns_resolver was always True. The internal functions (copies from FreeIPA code) have been fixed to return a changed state. Fixes: #1217 (ipaclient: Configure DNS resolver always reports as changed)
335 lines
11 KiB
Python
335 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
# Authors:
|
|
# Thomas Woerner <twoerner@redhat.com>
|
|
#
|
|
# Based on ipaplatform/redhat/tasks.py code from Christian Heimes
|
|
#
|
|
# Copyright (C) 2022 Red Hat
|
|
# see file 'COPYING' for use and warranty information
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
from __future__ import (absolute_import, division, print_function)
|
|
|
|
__metaclass__ = type
|
|
|
|
ANSIBLE_METADATA = {
|
|
'metadata_version': '1.0',
|
|
'supported_by': 'community',
|
|
'status': ['preview'],
|
|
}
|
|
|
|
DOCUMENTATION = """
|
|
---
|
|
module: ipaclient_configure_dns_resolver
|
|
short_description: Configure DNS resolver for IPA client
|
|
description:
|
|
Configure DNS resolver for IPA client, register files for installer
|
|
options:
|
|
nameservers:
|
|
description: The nameservers, required with state:present.
|
|
type: list
|
|
elements: str
|
|
required: false
|
|
searchdomains:
|
|
description: The searchdomains, required with state:present.
|
|
type: list
|
|
elements: str
|
|
required: false
|
|
state:
|
|
description: The state to ensure.
|
|
type: str
|
|
choices: ["present", "absent"]
|
|
default: present
|
|
required: false
|
|
author:
|
|
- Thomas Woerner (@t-woerner)
|
|
"""
|
|
|
|
EXAMPLES = """
|
|
# Ensure DNS nameservers and domain are configured
|
|
- ipaclient_configure_dns_resolver:
|
|
nameservers: groups.ipaservers
|
|
searchdomains: "{{ ipaserver_domain | default(ipaclient_domain) }}"
|
|
# Ensure DNS nameservers and domain are not configured
|
|
- ipaclient_configure_dns_resolver:
|
|
state: absent
|
|
"""
|
|
|
|
RETURN = """
|
|
"""
|
|
|
|
import os
|
|
import os.path
|
|
|
|
from ansible.module_utils.basic import AnsibleModule
|
|
from ansible.module_utils.ansible_ipa_client import (
|
|
check_imports, services, tasks, paths, sysrestore, CheckedIPAddress
|
|
)
|
|
try:
|
|
from ipalib.installdnsforwarders import detect_resolve1_resolv_conf
|
|
except ImportError:
|
|
def detect_resolve1_resolv_conf():
|
|
"""
|
|
Detect if /etc/resolv.conf is managed by systemd-resolved.
|
|
|
|
See man(5) NetworkManager.conf
|
|
"""
|
|
systemd_resolv_conf_files = {
|
|
"/run/systemd/resolve/stub-resolv.conf",
|
|
"/run/systemd/resolve/resolv.conf",
|
|
"/lib/systemd/resolv.conf",
|
|
"/usr/lib/systemd/resolv.conf",
|
|
}
|
|
|
|
try:
|
|
dest = os.readlink(paths.RESOLV_CONF)
|
|
except OSError:
|
|
# not a link
|
|
return False
|
|
# convert path relative to /etc/resolv.conf to abs path
|
|
dest = os.path.normpath(
|
|
os.path.join(os.path.dirname(paths.RESOLV_CONF), dest)
|
|
)
|
|
return dest in systemd_resolv_conf_files
|
|
|
|
|
|
if hasattr(paths, "SYSTEMD_RESOLVED_IPA_CONF"):
|
|
SYSTEMD_RESOLVED_IPA_CONF = paths.SYSTEMD_RESOLVED_IPA_CONF
|
|
else:
|
|
SYSTEMD_RESOLVED_IPA_CONF = "/etc/systemd/resolved.conf.d/zzz-ipa.conf"
|
|
|
|
|
|
if hasattr(paths, "NETWORK_MANAGER_IPA_CONF"):
|
|
NETWORK_MANAGER_IPA_CONF = paths.NETWORK_MANAGER_IPA_CONF
|
|
else:
|
|
NETWORK_MANAGER_IPA_CONF = "/etc/NetworkManager/conf.d/zzz-ipa.conf"
|
|
|
|
|
|
NM_IPA_CONF = """
|
|
# auto-generated by IPA client installer
|
|
[main]
|
|
dns={dnsprocessing}
|
|
[global-dns]
|
|
searches={searches}
|
|
[global-dns-domain-*]
|
|
servers={servers}
|
|
"""
|
|
|
|
|
|
RESOLVE1_IPA_CONF = """
|
|
# auto-generated by IPA client installer
|
|
[Resolve]
|
|
# use DNS servers
|
|
DNS={servers}
|
|
# make default DNS server, add search suffixes
|
|
Domains=~. {searchdomains}
|
|
"""
|
|
|
|
|
|
def configure_dns_resolver(nameservers, searchdomains, fstore=None):
|
|
"""
|
|
Configure global DNS resolver (e.g. /etc/resolv.conf).
|
|
|
|
:param nameservers: list of IP addresses
|
|
:param searchdomains: list of search domaons
|
|
:param fstore: optional file store for resolv.conf backup
|
|
"""
|
|
if not nameservers or not isinstance(nameservers, list):
|
|
raise AssertionError("nameservers must be of type list")
|
|
if not searchdomains or not isinstance(searchdomains, list):
|
|
raise AssertionError("searchdomains must be of type list")
|
|
|
|
changed = False
|
|
if fstore is not None and not fstore.has_file(paths.RESOLV_CONF):
|
|
fstore.backup_file(paths.RESOLV_CONF)
|
|
changed = True
|
|
|
|
resolve1_enabled = detect_resolve1_resolv_conf()
|
|
if "NetworkManager" not in services.knownservices:
|
|
# NetworkManager is not in wellknownservices for old IPA releases
|
|
# Therefore create own service for it.
|
|
nm_service = services.service("NetworkManager.service")
|
|
else:
|
|
nm_service = services.knownservices['NetworkManager']
|
|
|
|
# At first configure systemd-resolved
|
|
if resolve1_enabled:
|
|
if not os.path.exists(SYSTEMD_RESOLVED_IPA_CONF):
|
|
confd = os.path.dirname(SYSTEMD_RESOLVED_IPA_CONF)
|
|
if not os.path.isdir(confd):
|
|
os.mkdir(confd)
|
|
# owned by root, readable by systemd-resolve user
|
|
os.chmod(confd, 0o755)
|
|
tasks.restore_context(confd, force=True)
|
|
|
|
# Additionally to IPA server code also set servers
|
|
cfg = RESOLVE1_IPA_CONF.format(
|
|
servers=' '.join(nameservers),
|
|
searchdomains=" ".join(searchdomains)
|
|
)
|
|
with open(SYSTEMD_RESOLVED_IPA_CONF, "w") as outf:
|
|
os.fchmod(outf.fileno(), 0o644)
|
|
outf.write(cfg)
|
|
|
|
tasks.restore_context(
|
|
SYSTEMD_RESOLVED_IPA_CONF, force=True
|
|
)
|
|
|
|
if "systemd-resolved" in services.knownservices:
|
|
sdrd_service = services.knownservices["systemd-resolved"]
|
|
else:
|
|
sdrd_service = services.service("systemd-resolved.service")
|
|
if sdrd_service.is_enabled():
|
|
sdrd_service.reload_or_restart()
|
|
changed = True
|
|
|
|
# Then configure NetworkManager or resolve.conf
|
|
if nm_service.is_enabled():
|
|
if not os.path.exists(NETWORK_MANAGER_IPA_CONF):
|
|
# write DNS override and reload network manager to have it create
|
|
# a new resolv.conf. The file is prefixed with ``zzz`` to
|
|
# make it the last file. Global dns options do not stack and last
|
|
# man standing wins.
|
|
if resolve1_enabled:
|
|
# push DNS configuration to systemd-resolved
|
|
dnsprocessing = "systemd-resolved"
|
|
else:
|
|
# update /etc/resolv.conf
|
|
dnsprocessing = "default"
|
|
|
|
cfg = NM_IPA_CONF.format(
|
|
dnsprocessing=dnsprocessing,
|
|
servers=','.join(nameservers),
|
|
searches=','.join(searchdomains)
|
|
)
|
|
with open(NETWORK_MANAGER_IPA_CONF, 'w') as outf:
|
|
os.fchmod(outf.fileno(), 0o644)
|
|
outf.write(cfg)
|
|
# reload NetworkManager
|
|
nm_service.reload_or_restart()
|
|
changed = True
|
|
|
|
# Configure resolv.conf if NetworkManager and systemd-resoled are not
|
|
# enabled
|
|
elif not resolve1_enabled:
|
|
# no NM running, no systemd-resolved detected
|
|
# fall back to /etc/resolv.conf
|
|
cfg = [
|
|
"# auto-generated by IPA installer",
|
|
"search %s" % ' '.join(searchdomains),
|
|
]
|
|
for nameserver in nameservers:
|
|
cfg.append("nameserver %s" % nameserver)
|
|
with open(paths.RESOLV_CONF, 'w') as outf:
|
|
outf.write('\n'.join(cfg))
|
|
changed = True
|
|
|
|
return changed
|
|
|
|
|
|
def unconfigure_dns_resolver(fstore=None):
|
|
"""
|
|
Unconfigure global DNS resolver (e.g. /etc/resolv.conf).
|
|
|
|
:param fstore: optional file store for resolv.conf restore
|
|
"""
|
|
changed = False
|
|
|
|
if fstore is not None and fstore.has_file(paths.RESOLV_CONF):
|
|
fstore.restore_file(paths.RESOLV_CONF)
|
|
changed = True
|
|
|
|
if os.path.isfile(NETWORK_MANAGER_IPA_CONF):
|
|
os.unlink(NETWORK_MANAGER_IPA_CONF)
|
|
if "NetworkManager" not in services.knownservices:
|
|
# NetworkManager is not in wellknownservices for old IPA releases
|
|
# Therefore create own service for it.
|
|
nm_service = services.service("NetworkManager.service")
|
|
else:
|
|
nm_service = services.knownservices['NetworkManager']
|
|
if nm_service.is_enabled():
|
|
nm_service.reload_or_restart()
|
|
changed = True
|
|
|
|
if os.path.isfile(SYSTEMD_RESOLVED_IPA_CONF):
|
|
os.unlink(SYSTEMD_RESOLVED_IPA_CONF)
|
|
if "systemd-resolved" in services.knownservices:
|
|
sdrd_service = services.knownservices["systemd-resolved"]
|
|
else:
|
|
sdrd_service = services.service("systemd-resolved.service")
|
|
if sdrd_service.is_enabled():
|
|
sdrd_service.reload_or_restart()
|
|
changed = True
|
|
|
|
return changed
|
|
|
|
|
|
def main():
|
|
module = AnsibleModule(
|
|
argument_spec=dict(
|
|
nameservers=dict(type="list", elements="str", required=False),
|
|
searchdomains=dict(type="list", elements="str", required=False),
|
|
state=dict(type="str", default="present",
|
|
choices=["present", "absent"]),
|
|
),
|
|
supports_check_mode=False,
|
|
)
|
|
|
|
check_imports(module)
|
|
|
|
nameservers = module.params.get('nameservers')
|
|
searchdomains = module.params.get('searchdomains')
|
|
|
|
state = module.params.get("state")
|
|
|
|
if state == "present":
|
|
required = ["nameservers", "searchdomains"]
|
|
for param in required:
|
|
value = module.params.get(param)
|
|
if value is None or len(value) < 1:
|
|
module.fail_json(
|
|
msg="Argument '%s' is required for state:present" % param)
|
|
else:
|
|
invalid = ["nameservers", "searchdomains"]
|
|
for param in invalid:
|
|
if module.params.get(param) is not None:
|
|
module.fail_json(
|
|
msg="Argument '%s' can not be used with state:present" %
|
|
param)
|
|
|
|
# Check nameservers to contain valid IP addresses
|
|
if nameservers is not None:
|
|
for value in nameservers:
|
|
try:
|
|
CheckedIPAddress(value)
|
|
except Exception as e:
|
|
module.fail_json(
|
|
msg="Invalid IP address %s: %s" % (value, str(e)))
|
|
|
|
fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE)
|
|
|
|
if state == "present":
|
|
changed = configure_dns_resolver(nameservers,
|
|
searchdomains, fstore)
|
|
else:
|
|
changed = unconfigure_dns_resolver(fstore)
|
|
|
|
module.exit_json(changed=changed)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|