1# plugin.py 2# The interface for building DNF plugins. 3# 4# Copyright (C) 2012-2016 Red Hat, Inc. 5# 6# This copyrighted material is made available to anyone wishing to use, 7# modify, copy, or redistribute it subject to the terms and conditions of 8# the GNU General Public License v.2, or (at your option) any later version. 9# This program is distributed in the hope that it will be useful, but WITHOUT 10# ANY WARRANTY expressed or implied, including the implied warranties of 11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General 12# Public License for more details. You should have received a copy of the 13# GNU General Public License along with this program; if not, write to the 14# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the 16# source code or documentation are not subject to the GNU General Public 17# License and may only be used or replicated with the express permission of 18# Red Hat, Inc. 19# 20 21from __future__ import absolute_import 22from __future__ import print_function 23from __future__ import unicode_literals 24 25import fnmatch 26import glob 27import importlib 28import inspect 29import logging 30import operator 31import os 32import sys 33import traceback 34 35import libdnf 36import dnf.logging 37import dnf.pycomp 38import dnf.util 39from dnf.i18n import _ 40 41logger = logging.getLogger('dnf') 42 43DYNAMIC_PACKAGE = 'dnf.plugin.dynamic' 44 45 46class Plugin(object): 47 """The base class custom plugins must derive from. #:api""" 48 49 name = '<invalid>' 50 config_name = None 51 52 @classmethod 53 def read_config(cls, conf): 54 # :api 55 parser = libdnf.conf.ConfigParser() 56 name = cls.config_name if cls.config_name else cls.name 57 files = ['%s/%s.conf' % (path, name) for path in conf.pluginconfpath] 58 for file in files: 59 if os.path.isfile(file): 60 try: 61 parser.read(file) 62 except Exception as e: 63 raise dnf.exceptions.ConfigError(_("Parsing file failed: %s") % str(e)) 64 return parser 65 66 def __init__(self, base, cli): 67 # :api 68 self.base = base 69 self.cli = cli 70 71 def pre_config(self): 72 # :api 73 pass 74 75 def config(self): 76 # :api 77 pass 78 79 def resolved(self): 80 # :api 81 pass 82 83 def sack(self): 84 # :api 85 pass 86 87 def pre_transaction(self): 88 # :api 89 pass 90 91 def transaction(self): 92 # :api 93 pass 94 95 96class Plugins(object): 97 def __init__(self): 98 self.plugin_cls = [] 99 self.plugins = [] 100 101 def _caller(self, method): 102 for plugin in self.plugins: 103 try: 104 getattr(plugin, method)() 105 except dnf.exceptions.Error: 106 raise 107 except Exception: 108 exc_type, exc_value, exc_traceback = sys.exc_info() 109 except_list = traceback.format_exception(exc_type, exc_value, exc_traceback) 110 logger.critical(''.join(except_list)) 111 112 def _check_enabled(self, conf, enable_plugins): 113 """Checks whether plugins are enabled or disabled in configuration files 114 and removes disabled plugins from list""" 115 for plug_cls in self.plugin_cls[:]: 116 name = plug_cls.name 117 if any(fnmatch.fnmatch(name, pattern) for pattern in enable_plugins): 118 continue 119 parser = plug_cls.read_config(conf) 120 # has it enabled = False? 121 disabled = (parser.has_section('main') 122 and parser.has_option('main', 'enabled') 123 and not parser.getboolean('main', 'enabled')) 124 if disabled: 125 self.plugin_cls.remove(plug_cls) 126 127 def _load(self, conf, skips, enable_plugins): 128 """Dynamically load relevant plugin modules.""" 129 130 if DYNAMIC_PACKAGE in sys.modules: 131 raise RuntimeError("load_plugins() called twice") 132 sys.modules[DYNAMIC_PACKAGE] = package = dnf.pycomp.ModuleType(DYNAMIC_PACKAGE) 133 package.__path__ = [] 134 135 files = _get_plugins_files(conf.pluginpath, skips, enable_plugins) 136 _import_modules(package, files) 137 self.plugin_cls = _plugin_classes()[:] 138 self._check_enabled(conf, enable_plugins) 139 if len(self.plugin_cls) > 0: 140 names = sorted(plugin.name for plugin in self.plugin_cls) 141 logger.debug(_('Loaded plugins: %s'), ', '.join(names)) 142 143 def _run_pre_config(self): 144 self._caller('pre_config') 145 146 def _run_config(self): 147 self._caller('config') 148 149 def _run_init(self, base, cli=None): 150 for p_cls in self.plugin_cls: 151 plugin = p_cls(base, cli) 152 self.plugins.append(plugin) 153 154 def run_sack(self): 155 self._caller('sack') 156 157 def run_resolved(self): 158 self._caller('resolved') 159 160 def run_pre_transaction(self): 161 self._caller('pre_transaction') 162 163 def run_transaction(self): 164 self._caller('transaction') 165 166 def _unload(self): 167 del sys.modules[DYNAMIC_PACKAGE] 168 169 def unload_removed_plugins(self, transaction): 170 """ 171 Unload plugins that were removed in the `transaction`. 172 """ 173 if not transaction.remove_set: 174 return 175 176 # gather all installed plugins and their files 177 plugins = dict() 178 for plugin in self.plugins: 179 plugins[inspect.getfile(plugin.__class__)] = plugin 180 181 # gather all removed files that are plugin files 182 plugin_files = set(plugins.keys()) 183 erased_plugin_files = set() 184 for pkg in transaction.remove_set: 185 erased_plugin_files.update(plugin_files.intersection(pkg.files)) 186 if not erased_plugin_files: 187 return 188 189 # check whether removed plugin file is added at the same time (upgrade of a plugin) 190 for pkg in transaction.install_set: 191 erased_plugin_files.difference_update(pkg.files) 192 193 # unload plugins that were removed in transaction 194 for plugin_file in erased_plugin_files: 195 self.plugins.remove(plugins[plugin_file]) 196 197 198def _plugin_classes(): 199 return Plugin.__subclasses__() 200 201 202def _import_modules(package, py_files): 203 for fn in py_files: 204 path, module = os.path.split(fn) 205 package.__path__.append(path) 206 (module, ext) = os.path.splitext(module) 207 name = '%s.%s' % (package.__name__, module) 208 try: 209 module = importlib.import_module(name) 210 except Exception as e: 211 logger.error(_('Failed loading plugin "%s": %s'), module, e) 212 logger.log(dnf.logging.SUBDEBUG, '', exc_info=True) 213 214 215def _get_plugins_files(paths, disable_plugins, enable_plugins): 216 plugins = [] 217 disable_plugins = set(disable_plugins) 218 enable_plugins = set(enable_plugins) 219 pattern_enable_found = set() 220 pattern_disable_found = set() 221 for p in paths: 222 for fn in glob.glob('%s/*.py' % p): 223 (plugin_name, dummy) = os.path.splitext(os.path.basename(fn)) 224 matched = True 225 enable_pattern_tested = False 226 for pattern_skip in disable_plugins: 227 if fnmatch.fnmatch(plugin_name, pattern_skip): 228 pattern_disable_found.add(pattern_skip) 229 matched = False 230 for pattern_enable in enable_plugins: 231 if fnmatch.fnmatch(plugin_name, pattern_enable): 232 matched = True 233 pattern_enable_found.add(pattern_enable) 234 enable_pattern_tested = True 235 if not enable_pattern_tested: 236 for pattern_enable in enable_plugins: 237 if fnmatch.fnmatch(plugin_name, pattern_enable): 238 pattern_enable_found.add(pattern_enable) 239 if matched: 240 plugins.append(fn) 241 enable_not_found = enable_plugins.difference(pattern_enable_found) 242 if enable_not_found: 243 logger.warning(_("No matches found for the following enable plugin patterns: {}").format( 244 ", ".join(sorted(enable_not_found)))) 245 disable_not_found = disable_plugins.difference(pattern_disable_found) 246 if disable_not_found: 247 logger.warning(_("No matches found for the following disable plugin patterns: {}").format( 248 ", ".join(sorted(disable_not_found)))) 249 return plugins 250 251 252def register_command(command_class): 253 # :api 254 """A class decorator for automatic command registration.""" 255 def __init__(self, base, cli): 256 if cli: 257 cli.register_command(command_class) 258 plugin_class = type(str(command_class.__name__ + 'Plugin'), 259 (dnf.Plugin,), 260 {"__init__": __init__, 261 "name": command_class.aliases[0]}) 262 command_class._plugin = plugin_class 263 return command_class 264