mirror of
https://github.com/freeipa/ansible-freeipa.git
synced 2026-07-31 11:54:47 +00:00
Merge pull request #988 from t-woerner/ipaclient_configure_dns_resolver
ipaclient: Configure DNS resolver
This commit is contained in:
321
roles/ipaclient/library/ipaclient_configure_dns_resolver.py
Normal file
321
roles/ipaclient/library/ipaclient_configure_dns_resolver.py
Normal file
@@ -0,0 +1,321 @@
|
||||
# -*- 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")
|
||||
|
||||
if fstore is not None and not fstore.has_file(paths.RESOLV_CONF):
|
||||
fstore.backup_file(paths.RESOLV_CONF)
|
||||
|
||||
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()
|
||||
|
||||
# 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()
|
||||
|
||||
# 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))
|
||||
|
||||
|
||||
def unconfigure_dns_resolver(fstore=None):
|
||||
"""
|
||||
Unconfigure global DNS resolver (e.g. /etc/resolv.conf).
|
||||
|
||||
:param fstore: optional file store for resolv.conf restore
|
||||
"""
|
||||
if fstore is not None and fstore.has_file(paths.RESOLV_CONF):
|
||||
fstore.restore_file(paths.RESOLV_CONF)
|
||||
|
||||
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()
|
||||
|
||||
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()
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
nameservers=dict(type="list", elements="str", aliases=["cn"],
|
||||
required=False),
|
||||
searchdomains=dict(type="list", elements="str", aliases=["cn"],
|
||||
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":
|
||||
configure_dns_resolver(nameservers, searchdomains, fstore)
|
||||
else:
|
||||
unconfigure_dns_resolver(fstore)
|
||||
|
||||
module.exit_json(changed=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user