Blame libnmstate/dns.py

Packit b9ca78
#
Packit b9ca78
# Copyright (c) 2020 Red Hat, Inc.
Packit b9ca78
#
Packit b9ca78
# This file is part of nmstate
Packit b9ca78
#
Packit b9ca78
# This program is free software: you can redistribute it and/or modify
Packit b9ca78
# it under the terms of the GNU Lesser General Public License as published by
Packit b9ca78
# the Free Software Foundation, either version 2.1 of the License, or
Packit b9ca78
# (at your option) any later version.
Packit b9ca78
#
Packit b9ca78
# This program is distributed in the hope that it will be useful,
Packit b9ca78
# but WITHOUT ANY WARRANTY; without even the implied warranty of
Packit b9ca78
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
Packit b9ca78
# GNU Lesser General Public License for more details.
Packit b9ca78
#
Packit b9ca78
# You should have received a copy of the GNU Lesser General Public License
Packit b9ca78
# along with this program. If not, see <https://www.gnu.org/licenses/>.
Packit b9ca78
#
Packit b9ca78
Packit b9ca78
from copy import deepcopy
Packit b9ca78
Packit b9ca78
from libnmstate.error import NmstateValueError
Packit b9ca78
from libnmstate.error import NmstateVerificationError
Packit b9ca78
from libnmstate.error import NmstateNotImplementedError
Packit b9ca78
from libnmstate.iplib import is_ipv6_address
Packit b9ca78
from libnmstate.prettystate import format_desired_current_state_diff
Packit b9ca78
from libnmstate.schema import DNS
Packit b9ca78
from libnmstate.schema import Interface
Packit b9ca78
Packit b9ca78
Packit b9ca78
class DnsState:
Packit b9ca78
    PRIORITY_METADATA = "_priority"
Packit b9ca78
Packit b9ca78
    def __init__(self, des_dns_state, cur_dns_state):
Packit b9ca78
        self._config_changed = False
Packit b9ca78
        if des_dns_state is None or des_dns_state.get(DNS.CONFIG) is None:
Packit b9ca78
            # Use current config if DNS.KEY not defined or DNS.CONFIG not
Packit b9ca78
            # defined.
Packit b9ca78
            self._dns_state = cur_dns_state or {}
Packit b9ca78
        else:
Packit b9ca78
            self._dns_state = des_dns_state
Packit b9ca78
            self._validate()
Packit b9ca78
            self._config_changed = _is_dns_config_changed(
Packit b9ca78
                des_dns_state, cur_dns_state
Packit b9ca78
            )
Packit b9ca78
        self._cur_dns_state = deepcopy(cur_dns_state) if cur_dns_state else {}
Packit b9ca78
Packit b9ca78
    @property
Packit b9ca78
    def current_config(self):
Packit b9ca78
        return _get_config(self._cur_dns_state)
Packit b9ca78
Packit b9ca78
    @property
Packit b9ca78
    def config(self):
Packit b9ca78
        return _get_config(self._dns_state)
Packit b9ca78
Packit b9ca78
    @property
Packit b9ca78
    def _config_servers(self):
Packit b9ca78
        return _get_config_servers(self._dns_state)
Packit b9ca78
Packit b9ca78
    @property
Packit b9ca78
    def _config_searches(self):
Packit b9ca78
        return _get_config_searches(self._dns_state)
Packit b9ca78
Packit b9ca78
    def gen_metadata(self, ifaces, route_state):
Packit b9ca78
        """
Packit b9ca78
        Return DNS configure targeting to store as metadata of interface.
Packit b9ca78
        Data structure returned is:
Packit b9ca78
            {
Packit b9ca78
                iface_name: {
Packit b9ca78
                    Interface.IPV4: {
Packit b9ca78
                        DNS.SERVER: dns_servers,
Packit b9ca78
                        DNS.SEARCH: dns_searches,
Packit b9ca78
                    },
Packit b9ca78
                    Interface.IPV6: {
Packit b9ca78
                        DNS.SERVER: dns_servers,
Packit b9ca78
                        DNS.SEARCH: dns_searches,
Packit b9ca78
                    },
Packit b9ca78
                }
Packit b9ca78
            }
Packit b9ca78
        """
Packit b9ca78
        iface_metadata = {}
Packit b9ca78
        if not self._config_servers and not self._config_searches:
Packit b9ca78
            return iface_metadata
Packit b9ca78
        ipv4_iface, ipv6_iface = self._find_ifaces_for_name_servers(
Packit b9ca78
            ifaces, route_state
Packit b9ca78
        )
Packit b9ca78
        if ipv4_iface == ipv6_iface:
Packit b9ca78
            iface_metadata = {
Packit b9ca78
                ipv4_iface: {
Packit b9ca78
                    Interface.IPV4: {DNS.SERVER: [], DNS.SEARCH: []},
Packit b9ca78
                    Interface.IPV6: {DNS.SERVER: [], DNS.SEARCH: []},
Packit b9ca78
                },
Packit b9ca78
            }
Packit b9ca78
        else:
Packit b9ca78
            if ipv4_iface:
Packit b9ca78
                iface_metadata[ipv4_iface] = {
Packit b9ca78
                    Interface.IPV4: {DNS.SERVER: [], DNS.SEARCH: []},
Packit b9ca78
                }
Packit b9ca78
            if ipv6_iface:
Packit b9ca78
                iface_metadata[ipv6_iface] = {
Packit b9ca78
                    Interface.IPV6: {DNS.SERVER: [], DNS.SEARCH: []},
Packit b9ca78
                }
Packit b9ca78
        index = 0
Packit b9ca78
        searches_saved = False
Packit b9ca78
        for server in self._config_servers:
Packit b9ca78
            iface_name = None
Packit b9ca78
            if is_ipv6_address(server):
Packit b9ca78
                iface_name = ipv6_iface
Packit b9ca78
                family = Interface.IPV6
Packit b9ca78
            else:
Packit b9ca78
                iface_name = ipv4_iface
Packit b9ca78
                family = Interface.IPV4
Packit b9ca78
            if not iface_name:
Packit b9ca78
                raise NmstateValueError(
Packit b9ca78
                    "Failed to find suitable interface for saving DNS "
Packit b9ca78
                    "name servers: %s" % server
Packit b9ca78
                )
Packit b9ca78
            iface_dns_metada = iface_metadata[iface_name][family]
Packit b9ca78
            iface_dns_metada[DNS.SERVER].append(server)
Packit b9ca78
            iface_dns_metada.setdefault(DnsState.PRIORITY_METADATA, index)
Packit b9ca78
            if not searches_saved:
Packit b9ca78
                iface_dns_metada[DNS.SEARCH] = self._config_searches
Packit b9ca78
            searches_saved = True
Packit b9ca78
            index += 1
Packit b9ca78
        return iface_metadata
Packit b9ca78
Packit b9ca78
    def _find_ifaces_for_name_servers(self, ifaces, route_state):
Packit b9ca78
        """
Packit b9ca78
        Find interface to store the DNS configurations in the order of:
Packit b9ca78
            * Any interface with static gateway
Packit b9ca78
            * Any interface configured as dynamic IP with 'auto-dns:False'
Packit b9ca78
        Return tuple: (ipv4_iface, ipv6_iface)
Packit b9ca78
        """
Packit b9ca78
        ipv4_iface, ipv6_iface = self._find_ifaces_with_static_gateways(
Packit b9ca78
            route_state
Packit b9ca78
        )
Packit b9ca78
        if not (ipv4_iface and ipv6_iface):
Packit b9ca78
            (
Packit b9ca78
                auto_ipv4_iface,
Packit b9ca78
                auto_ipv6_iface,
Packit b9ca78
            ) = self._find_ifaces_with_auto_dns_false(ifaces)
Packit b9ca78
            if not ipv4_iface and auto_ipv4_iface:
Packit b9ca78
                ipv4_iface = auto_ipv4_iface
Packit b9ca78
            if not ipv6_iface and auto_ipv6_iface:
Packit b9ca78
                ipv6_iface = auto_ipv6_iface
Packit b9ca78
Packit b9ca78
        return ipv4_iface, ipv6_iface
Packit b9ca78
Packit b9ca78
    def _find_ifaces_with_static_gateways(self, route_state):
Packit b9ca78
        """
Packit b9ca78
        Return tuple of interfaces with IPv4 and IPv6 static gateways.
Packit b9ca78
        """
Packit b9ca78
        ipv4_iface = None
Packit b9ca78
        ipv6_iface = None
Packit b9ca78
        for iface_name, route_set in route_state.config_iface_routes.items():
Packit b9ca78
            for route in route_set:
Packit b9ca78
                if ipv4_iface and ipv6_iface:
Packit b9ca78
                    return (ipv4_iface, ipv6_iface)
Packit b9ca78
                if route.is_gateway:
Packit b9ca78
                    if route.is_ipv6:
Packit b9ca78
                        ipv6_iface = iface_name
Packit b9ca78
                    else:
Packit b9ca78
                        ipv4_iface = iface_name
Packit b9ca78
        return (ipv4_iface, ipv6_iface)
Packit b9ca78
Packit b9ca78
    def _find_ifaces_with_auto_dns_false(self, ifaces):
Packit b9ca78
        ipv4_iface = None
Packit b9ca78
        ipv6_iface = None
Packit b9ca78
        for iface in ifaces.values():
Packit b9ca78
            if ipv4_iface and ipv6_iface:
Packit b9ca78
                return (ipv4_iface, ipv6_iface)
Packit b9ca78
            for family in (Interface.IPV4, Interface.IPV6):
Packit b9ca78
                ip_state = iface.ip_state(family)
Packit b9ca78
                if ip_state.is_dynamic and (not ip_state.auto_dns):
Packit b9ca78
                    if family == Interface.IPV4:
Packit b9ca78
                        ipv4_iface = iface.name
Packit b9ca78
                    else:
Packit b9ca78
                        ipv6_iface = iface.name
Packit b9ca78
Packit b9ca78
        return (ipv4_iface, ipv6_iface)
Packit b9ca78
Packit b9ca78
    def verify(self, cur_dns_state):
Packit b9ca78
        cur_dns = DnsState(des_dns_state=None, cur_dns_state=cur_dns_state,)
Packit b9ca78
        if self.config.get(DNS.SERVER, []) != cur_dns.config.get(
Packit b9ca78
            DNS.SERVER, []
Packit b9ca78
        ) or self.config.get(DNS.SEARCH, []) != cur_dns.config.get(
Packit b9ca78
            DNS.SEARCH, []
Packit b9ca78
        ):
Packit b9ca78
            raise NmstateVerificationError(
Packit b9ca78
                format_desired_current_state_diff(
Packit b9ca78
                    {DNS.KEY: self.config}, {DNS.KEY: cur_dns.config},
Packit b9ca78
                )
Packit b9ca78
            )
Packit b9ca78
Packit b9ca78
    def _validate(self):
Packit b9ca78
        if (
Packit b9ca78
            len(self._config_servers) > 2
Packit b9ca78
            and any(is_ipv6_address(n) for n in self._config_servers)
Packit b9ca78
            and any(not is_ipv6_address(n) for n in self._config_servers)
Packit b9ca78
        ):
Packit b9ca78
            raise NmstateNotImplementedError(
Packit b9ca78
                "Three or more nameservers are only supported when using "
Packit b9ca78
                "either IPv4 or IPv6 nameservers but not both."
Packit b9ca78
            )
Packit b9ca78
Packit b9ca78
    @property
Packit b9ca78
    def config_changed(self):
Packit b9ca78
        return self._config_changed
Packit b9ca78
Packit b9ca78
Packit b9ca78
def _get_config(state):
Packit b9ca78
    conf = state.get(DNS.CONFIG, {})
Packit b9ca78
    if not conf:
Packit b9ca78
        conf = {DNS.SERVER: [], DNS.SEARCH: []}
Packit b9ca78
    return conf
Packit b9ca78
Packit b9ca78
Packit b9ca78
def _get_config_servers(state):
Packit b9ca78
    return _get_config(state).get(DNS.SERVER, [])
Packit b9ca78
Packit b9ca78
Packit b9ca78
def _get_config_searches(state):
Packit b9ca78
    return _get_config(state).get(DNS.SEARCH, [])
Packit b9ca78
Packit b9ca78
Packit b9ca78
def _is_dns_config_changed(des_dns_state, cur_dns_state):
Packit b9ca78
    return _get_config_servers(des_dns_state) != _get_config_servers(
Packit b9ca78
        cur_dns_state
Packit b9ca78
    ) or _get_config_searches(des_dns_state) != _get_config_searches(
Packit b9ca78
        cur_dns_state
Packit b9ca78
    )