Blame libnmstate/nm/context.py

Packit Service 0535c1
#
Packit Service 0535c1
# Copyright (c) 2020 Red Hat, Inc.
Packit Service 0535c1
#
Packit Service 0535c1
# This file is part of nmstate
Packit Service 0535c1
#
Packit Service 0535c1
# This program is free software: you can redistribute it and/or modify
Packit Service 0535c1
# it under the terms of the GNU Lesser General Public License as published by
Packit Service 0535c1
# the Free Software Foundation, either version 2.1 of the License, or
Packit Service 0535c1
# (at your option) any later version.
Packit Service 0535c1
#
Packit Service 0535c1
# This program is distributed in the hope that it will be useful,
Packit Service 0535c1
# but WITHOUT ANY WARRANTY; without even the implied warranty of
Packit Service 0535c1
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
Packit Service 0535c1
# GNU Lesser General Public License for more details.
Packit Service 0535c1
#
Packit Service 0535c1
# You should have received a copy of the GNU Lesser General Public License
Packit Service 0535c1
# along with this program. If not, see <https://www.gnu.org/licenses/>.
Packit Service 0535c1
#
Packit Service 0535c1
Packit Service 0535c1
import datetime
Packit Service 0535c1
import logging
Packit Service 0535c1
Packit Service 0535c1
from libnmstate.error import NmstateInternalError
Packit Service 0535c1
from libnmstate.error import NmstateTimeoutError
Packit Service 0535c1
Packit Service 0535c1
from .common import NM
Packit Service 0535c1
from .common import GLib
Packit Service 0535c1
from .common import Gio
Packit Service 0535c1
Packit Service 0535c1
# Interval for idle checker to check on whether timeout should trigger since
Packit Service 0535c1
# last finish async action.
Packit Service 0535c1
IDLE_CHECK_INTERNAL = 5
Packit Service 0535c1
Packit Service 0535c1
# libnm dbus connection has reply timeout 25 seconds.
Packit Service 0535c1
IDLE_TIMEOUT = 25
Packit Service 0535c1
Packit Service 0535c1
# NetworkManage is using dbus in libnm while the dbus has limitation on
Packit Service 0535c1
# maximum number of pending replies per connection.(RHEL/CentOS 8 is 1024)
Packit Service 0535c1
# Hence limit the synchronous queue size
Packit Service 0535c1
SLOW_ASYNC_QUEUE_SIZE = 100
Packit Service 0535c1
FAST_ASYNC_QUEUE_SIZE = 300
Packit Service 0535c1
Packit Service 0535c1
Packit Service 0535c1
class NmContext:
Packit Service 0535c1
    def __init__(self):
Packit Service 0535c1
        self._client = NM.Client.new(cancellable=None)
Packit Service 0535c1
        self._context = self._client.get_main_context()
Packit Service 0535c1
        self._quitting = False
Packit Service 0535c1
        self._cancellable = None
Packit Service 0535c1
        self._error = None
Packit Service 0535c1
        self._timeout_source = None
Packit Service 0535c1
        self._last_async_finish_time = None
Packit Service 0535c1
        self._fast_queue = None
Packit Service 0535c1
        self._slow_queue = None
Packit Service 0535c1
        self._init_queue()
Packit Service 0535c1
        self._init_cancellable()
Packit Service 0535c1
Packit Service 0535c1
    def _init_queue(self):
Packit Service 0535c1
        self._fast_queue = set()
Packit Service 0535c1
        self._slow_queue = set()
Packit Service 0535c1
Packit Service 0535c1
    def _init_cancellable(self):
Packit Service 0535c1
        self._cancellable = Gio.Cancellable.new()
Packit Service 0535c1
Packit Service 0535c1
    @property
Packit Service 0535c1
    def cancellable(self):
Packit Service 0535c1
        return self._cancellable
Packit Service 0535c1
Packit Service 0535c1
    @property
Packit Service 0535c1
    def client(self):
Packit Service 0535c1
        if self._quitting:
Packit Service 0535c1
            return None
Packit Service 0535c1
        return self._client
Packit Service 0535c1
Packit Service 0535c1
    @property
Packit Service 0535c1
    def context(self):
Packit Service 0535c1
        if not self._context:
Packit Service 0535c1
            raise NmstateInternalError(
Packit Service 0535c1
                "BUG: Accessing MainContext while it is None"
Packit Service 0535c1
            )
Packit Service 0535c1
        return self._context
Packit Service 0535c1
Packit Service 0535c1
    def refresh_content(self):
Packit Service 0535c1
        if self.context:
Packit Service 0535c1
            while self.context.iteration(False):
Packit Service 0535c1
                pass
Packit Service 0535c1
Packit Service 0535c1
    def clean_up(self):
Packit Service 0535c1
        if self._cancellable:
Packit Service 0535c1
            self._cancellable.cancel()
Packit Service 0535c1
        self._del_timeout()
Packit Service 0535c1
        self._del_client()
Packit Service 0535c1
        self._context = None
Packit Service 0535c1
        self._cancellable = None
Packit Service 0535c1
Packit Service 0535c1
    def _del_client(self):
Packit Service 0535c1
        if self._client:
Packit Service 0535c1
            is_done = []
Packit Service 0535c1
            is_timeout = []
Packit Service 0535c1
            self._client.get_context_busy_watcher().weak_ref(
Packit Service 0535c1
                lambda: is_done.append(1)
Packit Service 0535c1
            )
Packit Service 0535c1
            self._client = None
Packit Service 0535c1
            self._quitting = True
Packit Service 0535c1
Packit Service 0535c1
            self.refresh_content()
Packit Service 0535c1
Packit Service 0535c1
            if not is_done:
Packit Service 0535c1
                timeout_source = GLib.timeout_source_new(50)
Packit Service 0535c1
                try:
Packit Service 0535c1
                    timeout_source.set_callback(lambda x: is_timeout.append(1))
Packit Service 0535c1
                    timeout_source.attach(self.context)
Packit Service 0535c1
                    while not is_done and not is_timeout:
Packit Service 0535c1
                        self.context.iteration(True)
Packit Service 0535c1
                finally:
Packit Service 0535c1
                    timeout_source.destroy()
Packit Service 0535c1
            if not is_done:
Packit Service 0535c1
                logging.error("BUG: NM.Client is not cleaned")
Packit Service 0535c1
            self._context = None
Packit Service 0535c1
Packit Service 0535c1
    def _del_timeout(self):
Packit Service 0535c1
        if self._timeout_source:
Packit Service 0535c1
            self._timeout_source.destroy()
Packit Service 0535c1
            self._timeout_source = None
Packit Service 0535c1
Packit Service 0535c1
    def register_async(self, action, fast=False):
Packit Service 0535c1
        """
Packit Service 0535c1
        Register action(string) to wait list.
Packit Service 0535c1
        Set fast as True if requested action does not require too much time,
Packit Service 0535c1
        for example: profile modification.
Packit Service 0535c1
        """
Packit Service 0535c1
        queue = self._fast_queue if fast else self._slow_queue
Packit Service 0535c1
        max_queue = FAST_ASYNC_QUEUE_SIZE if fast else SLOW_ASYNC_QUEUE_SIZE
Packit Service 0535c1
        if len(queue) >= max_queue:
Packit Service 0535c1
            logging.debug(
Packit Service 0535c1
                f"Async queue({max_queue}) full, waiting all existing actions "
Packit Service 0535c1
                "to be finished before registering more async action"
Packit Service 0535c1
            )
Packit Service 0535c1
            # TODO: No need to wait all finish, should continue when the queue
Packit Service 0535c1
            #       is considerably empty and ready for new async action.
Packit Service 0535c1
            self.wait_all_finish()
Packit Service 0535c1
Packit Service 0535c1
        if action in self._fast_queue or action in self._slow_queue:
Packit Service 0535c1
            raise NmstateInternalError(
Packit Service 0535c1
                f"BUG: An existing actions {action} is already registered"
Packit Service 0535c1
            )
Packit Service 0535c1
Packit Service 0535c1
        logging.debug(f"Async action: {action} started")
Packit Service 0535c1
        queue.add(action)
Packit Service 0535c1
Packit Service 0535c1
    def finish_async(self, action, suppress_log=False):
Packit Service 0535c1
        """
Packit Service 0535c1
        Mark action(string) as finished.
Packit Service 0535c1
        """
Packit Service 0535c1
        self._last_async_finish_time = datetime.datetime.now()
Packit Service 0535c1
        if not suppress_log:
Packit Service 0535c1
            logging.debug(f"Async action: {action} finished")
Packit Service 0535c1
        self._fast_queue.discard(action)
Packit Service 0535c1
        self._slow_queue.discard(action)
Packit Service 0535c1
Packit Service 0535c1
    def _action_all_finished(self):
Packit Service 0535c1
        return not (len(self._fast_queue) or len(self._slow_queue))
Packit Service 0535c1
Packit Service 0535c1
    def _idle_timeout_cb(self, _user_data):
Packit Service 0535c1
        if self._error or self._action_all_finished():
Packit Service 0535c1
            return GLib.SOURCE_REMOVE
Packit Service 0535c1
        idle_time = datetime.datetime.now() - self._last_async_finish_time
Packit Service 0535c1
        if idle_time > datetime.timedelta(seconds=IDLE_TIMEOUT):
Packit Service 0535c1
            remaining_actions = self._slow_queue | self._fast_queue
Packit Service 0535c1
            self.fail(
Packit Service 0535c1
                NmstateTimeoutError(f"Action {remaining_actions} timeout")
Packit Service 0535c1
            )
Packit Service 0535c1
            return GLib.SOURCE_REMOVE
Packit Service 0535c1
        else:
Packit Service 0535c1
            return GLib.SOURCE_CONTINUE
Packit Service 0535c1
Packit Service 0535c1
    def is_cancelled(self):
Packit Service 0535c1
        return self._cancellable.is_cancelled()
Packit Service 0535c1
Packit Service 0535c1
    def fail(self, exception):
Packit Service 0535c1
        if not self._cancellable.is_cancelled():
Packit Service 0535c1
            if self._error:
Packit Service 0535c1
                logging.error(
Packit Service 0535c1
                    f"BUG: There is already a exception assigned: "
Packit Service 0535c1
                    f"existing: {self._error}, new exception {exception}"
Packit Service 0535c1
                )
Packit Service 0535c1
            self.cancellable.cancel()
Packit Service 0535c1
            self._del_timeout()
Packit Service 0535c1
            self._error = exception
Packit Service 0535c1
Packit Service 0535c1
    def wait_all_finish(self):
Packit Service 0535c1
        """
Packit Service 0535c1
        Block till all async actions been marked as finished via
Packit Service 0535c1
        `finish_async()` or anyone failed by `fail()`.
Packit Service 0535c1
        """
Packit Service 0535c1
        self._last_async_finish_time = datetime.datetime.now()
Packit Service 0535c1
        if not self._action_all_finished():
Packit Service 0535c1
            self._timeout_source = GLib.timeout_source_new(
Packit Service 0535c1
                IDLE_CHECK_INTERNAL * 1000
Packit Service 0535c1
            )
Packit Service 0535c1
            user_data = None
Packit Service 0535c1
            self._timeout_source.set_callback(self._idle_timeout_cb, user_data)
Packit Service 0535c1
            self._timeout_source.attach(self._context)
Packit Service 0535c1
Packit Service 0535c1
            while not self._action_all_finished() and not self._error:
Packit Service 0535c1
                self.context.iteration(True)
Packit Service 0535c1
            self._del_timeout()
Packit Service 0535c1
Packit Service 0535c1
        if self._error:
Packit Service 0535c1
            # The queue and error should be flush and perpare for another run
Packit Service 0535c1
            self._init_queue()
Packit Service 0535c1
            self._init_cancellable()
Packit Service 0535c1
            tmp_error = self._error
Packit Service 0535c1
            self._error = None
Packit Service 0535c1
            # pylint: disable=raising-bad-type
Packit Service 0535c1
            raise tmp_error
Packit Service 0535c1
            # pylint: enable=raising-bad-type
Packit Service 0535c1
Packit Service 0535c1
    def get_nm_dev(self, iface_name):
Packit Service 0535c1
        return self.client.get_device_by_iface(iface_name)