Source code for mfutil.plugins

"""utility classes and functions for managing metwork plugins."""

import logging
import six
import glob
import os
import shutil
import hashlib
import envtpl
from mfutil import BashWrapperException, BashWrapperOrRaise, BashWrapper
from mfutil import mkdir_p_or_die, get_unique_hexa_identifier
from mfutil.layerapi2 import LayerApi2Wrapper
from configparser_extended import ExtendedConfigParser

RUNTIME_HOME = os.environ.get('MODULE_RUNTIME_HOME', '/tmp')
MFEXT_HOME = os.environ['MFEXT_HOME']
MODULE_LOWERCASE = os.environ['MODULE_LOWERCASE']
SPEC_TEMPLATE = os.path.join(MFEXT_HOME, "share", "templates", "plugin.spec")

# FIXME: doc


def plugin_name_to_layerapi2_label(plugin_name):
    return "plugin_%s@%s" % (plugin_name, MODULE_LOWERCASE)


def layerapi2_label_to_plugin_name(label):
    if (not label.startswith("plugin_")) or \
            (not label.endswith("@%s" % MODULE_LOWERCASE)):
        raise Exception("bad layerapi2_label: %s => is it really a plugin ?" %
                        label)
    return label[7:].split('@')[0]


def get_layer_home_from_plugin_name(plugin_name):
    label = plugin_name_to_layerapi2_label(plugin_name)
    return LayerApi2Wrapper.get_layer_home(label)


def layerapi2_label_file_to_plugin_name(llf_path):
    try:
        with open(llf_path, 'r') as f:
            c = f.read().strip()
    except Exception:
        raise Exception("can't read %s file" % llf_path)
    return layerapi2_label_to_plugin_name(c)


[docs]class MFUtilPluginAlreadyInstalled(Exception): """Exception class raised when a plugin is already installed.""" pass
[docs]class MFUtilPluginNotInstalled(Exception): """Exception class raised when a plugin is not installed.""" pass
[docs]class MFUtilPluginCantUninstall(BashWrapperException): """Exception class raised when we can't uninstall a plugin.""" pass
[docs]class MFUtilPluginCantInstall(BashWrapperException): """Exception class raised when we can't install a plugin.""" pass
[docs]class MFUtilPluginCantInit(BashWrapperException): """Exception class raised when we can't init the plugin base.""" pass
[docs]class MFUtilPluginCantBuild(BashWrapperException): """Exception class raised when we can't build a plugin.""" pass
[docs]class MFUtilPluginBaseNotInitialized(Exception): """Exception class raised when the plugin base is not initialized.""" pass
[docs]class MFUtilPluginFileNotFound(Exception): """Exception class raised when we can't find the plugin file.""" pass
[docs]class MFUtilPluginInvalid(Exception): """Exception class raised when the plugin is invalid.""" pass
def __get_logger(): return logging.getLogger("mfutil.plugins")
[docs]def get_plugins_base_dir(): """Return the default plugins base directory path. This value correspond to: ${RUNTIME_HOME}/var/plugins value. Returns: (string): the default plugins base directory path. """ return os.path.join(RUNTIME_HOME, "var", "plugins")
def _get_plugins_base_dir(plugins_base_dir=None): if plugins_base_dir is None: return get_plugins_base_dir() else: return plugins_base_dir def _get_rpm_cmd(command, extra_args="", plugins_base_dir=None, add_prefix=False): plugins_base_dir = _get_plugins_base_dir(plugins_base_dir) base = os.path.join(plugins_base_dir, "base") if add_prefix: cmd = 'layer_wrapper --layers=rpm@mfext -- rpm %s ' \ '--dbpath %s --prefix %s %s' % \ (command, base, plugins_base_dir, extra_args) else: cmd = 'layer_wrapper --layers=rpm@mfext -- rpm %s ' \ '--dbpath %s %s' % \ (command, base, extra_args) return cmd
[docs]def init_plugins_base(plugins_base_dir=None): """Initialize the plugins base. Args: plugins_base_dir (string): alternate plugins base directory (useful for unit tests). Raises: MFUtilPluginCantInit: if we can't init the plugin base. """ plugins_base_dir = _get_plugins_base_dir(plugins_base_dir) shutil.rmtree(plugins_base_dir, ignore_errors=True) mkdir_p_or_die(plugins_base_dir) mkdir_p_or_die(os.path.join(plugins_base_dir, "base")) cmd = _get_rpm_cmd("--initdb", plugins_base_dir=plugins_base_dir) BashWrapperOrRaise(cmd, MFUtilPluginCantInit, "can't init %s" % plugins_base_dir)
[docs]def is_plugins_base_initialized(plugins_base_dir=None): """Return True is the plugins base is already initialized for the module. You can pass a plugins_base_dir as argument but you don't have to do that, except for unit testing. The plugins base dir is stored by default in : ${MODULE_RUNTIME_HOME}/var/plugins Args: plugins_base_dir (string): alternate plugins base directory. Returns: boolean: True if the base is initialized. """ plugins_base_dir = _get_plugins_base_dir(plugins_base_dir) return os.path.isfile(os.path.join(plugins_base_dir, "base", "Name"))
def _assert_plugins_base_initialized(plugins_base_dir=None): if not is_plugins_base_initialized(plugins_base_dir=plugins_base_dir): raise MFUtilPluginBaseNotInitialized() def get_installed_plugins(plugins_base_dir=None): _assert_plugins_base_initialized(plugins_base_dir) plugins_base_dir = _get_plugins_base_dir(plugins_base_dir) frmt = "%{name}~~~%{version}~~~%{release}\\n" cmd = _get_rpm_cmd('-qa', '--qf "%s"' % frmt, plugins_base_dir=plugins_base_dir) x = BashWrapperOrRaise(cmd) tmp = x.stdout.split('\n') result = [] for line in tmp: tmp2 = line.split('~~~') if len(tmp2) == 3: home = get_layer_home_from_plugin_name(tmp2[0]) if home: result.append({'name': tmp2[0], 'version': tmp2[1], 'release': tmp2[2], 'home': home}) for tmp in os.listdir(plugins_base_dir): directory_name = tmp.strip() if directory_name == 'base': continue llf = os.path.join(plugins_base_dir, directory_name, ".layerapi2_label") if not os.path.isfile(llf): __get_logger().warning("missing %s file for installed " "plugin directory" % llf) continue name = layerapi2_label_file_to_plugin_name(llf) directory = os.path.join(plugins_base_dir, directory_name) if os.path.islink(directory): result.append({'name': name, 'version': 'dev_link', 'release': 'dev_link', 'home': directory}) return result def uninstall_plugin(name, plugins_base_dir=None, ignore_errors=False, quiet=False): _assert_plugins_base_initialized(plugins_base_dir) plugins_base_dir = _get_plugins_base_dir(plugins_base_dir) infos = get_plugin_info(name, mode="name", plugins_base_dir=plugins_base_dir) if infos is None: raise MFUtilPluginNotInstalled("plugin %s is not installed" % name) version = infos['metadatas']['version'] release = infos['metadatas']['release'] home = infos.get('home', None) if release == 'dev_link': preuninstall_status = _preuninstall_plugin(name, version, release, quiet=quiet) if not preuninstall_status and not ignore_errors: raise MFUtilPluginCantUninstall("can't uninstall plugin %s" % name) if home: os.unlink(home) return preuninstall_status = _preuninstall_plugin(name, version, release) if not preuninstall_status and not ignore_errors: raise MFUtilPluginCantUninstall("can't uninstall plugin %s" % name) cmd = _get_rpm_cmd('-e --noscripts %s' % name, plugins_base_dir=plugins_base_dir, add_prefix=False) try: x = BashWrapperOrRaise(cmd, MFUtilPluginCantUninstall, "can't uninstall %s" % name) except MFUtilPluginCantUninstall: if not ignore_errors: raise if home: shutil.rmtree(home, ignore_errors=True) infos = get_plugin_info(name, mode="name", plugins_base_dir=plugins_base_dir) if infos is not None: raise MFUtilPluginCantUninstall("can't uninstall plugin %s" % name, bash_wrapper=x) if home and os.path.exists(home): raise MFUtilPluginCantUninstall("can't uninstall plugin %s " "(directory still here)" % name) def _postinstall_plugin(name, version, release, quiet=False): res = BashWrapper("_plugins.postinstall %s %s %s" % (name, version, release)) if not res: if not quiet: __get_logger().warning("error during postinstall: %s", res) return False return True def is_dangerous_plugin(name): res = BashWrapper("_plugins.is_dangerous %s" % (name,)) if not res: __get_logger().warning("error during %s", res) return if res.stdout and len(res.stdout) > 0: print(res.stdout) def _preuninstall_plugin(name, version, release, quiet=False): res = BashWrapper("_plugins.preuninstall %s %s %s" % (name, version, release)) if not res: if not quiet: __get_logger().warning("error during postuninstall: %s", res) return False return True def install_plugin(plugin_filepath, plugins_base_dir=None, ignore_errors=False, quiet=False): _assert_plugins_base_initialized(plugins_base_dir) if not os.path.isfile(plugin_filepath): raise MFUtilPluginFileNotFound("plugin file %s not found" % plugin_filepath) infos = get_plugin_info(plugin_filepath, mode="file", plugins_base_dir=plugins_base_dir) if infos is None: raise MFUtilPluginInvalid("invalid %s plugin" % plugin_filepath) name = infos['metadatas']['name'] version = infos['metadatas']['version'] release = infos['metadatas']['release'] installed_infos = get_plugin_info(name, mode="name", plugins_base_dir=plugins_base_dir) if installed_infos is not None: raise MFUtilPluginAlreadyInstalled("plugin %s already installed" % name) cmd = _get_rpm_cmd('-Uvh --noscripts --force %s' % plugin_filepath, plugins_base_dir=plugins_base_dir, add_prefix=True) x = BashWrapperOrRaise(cmd, MFUtilPluginCantInstall, "can't install plugin %s" % name) infos = get_plugin_info(name, mode="name", plugins_base_dir=plugins_base_dir) if infos is None: raise MFUtilPluginCantInstall("can't install plugin %s" % name, bash_wrapper=x) postinstall_status = _postinstall_plugin(name, version, release, quiet=quiet) if not postinstall_status and not ignore_errors: try: uninstall_plugin(name, plugins_base_dir, True, True) except Exception: pass def _make_plugin_spec(dest_file, name, version, summary, license, packager, vendor, url): with open(SPEC_TEMPLATE, "r") as f: template = f.read() extra_vars = {"NAME": name, "VERSION": version, "SUMMARY": summary, "LICENSE": license, "PACKAGER": packager, "VENDOR": vendor, "URL": url} res = envtpl.render_string(template, extra_variables=extra_vars) # because, you can have some template inside extra vars res = envtpl.render_string(res) with open(dest_file, "w") as f: f.write(res) def develop_plugin(plugin_path, name, plugins_base_dir=None, ignore_errors=False, quiet=False): plugin_path = os.path.abspath(plugin_path) plugins_base_dir = _get_plugins_base_dir(plugins_base_dir) shutil.rmtree(os.path.join(plugins_base_dir, name), True) try: os.symlink(plugin_path, os.path.join(plugins_base_dir, name)) except OSError: if not quiet: __get_logger().warning( "plugin '%s' already installed as a symlink", name) postinstall_status = _postinstall_plugin(name, "dev_link", "dev_link", quiet=quiet) if not postinstall_status and not ignore_errors: try: uninstall_plugin(name, plugins_base_dir, True, True) except Exception: pass def _is_dev_link_plugin(name, plugins_base_dir=None): home = get_layer_home_from_plugin_name(name) if home is None: return False plugins_base_dir = _get_plugins_base_dir(plugins_base_dir) return os.path.islink(home) def build_plugin(plugin_path, plugins_base_dir=None): plugin_path = os.path.abspath(plugin_path) plugins_base_dir = _get_plugins_base_dir(plugins_base_dir) base = os.path.join(plugins_base_dir, "base") pwd = os.getcwd() parser = ExtendedConfigParser(config=os.environ.get('MFCONFIG', 'GENERIC'), strict=False, inheritance="im") with open(os.path.join(plugin_path, "config.ini"), "r") as f: config_content = f.read() if six.PY2: parser.read_string(config_content.decode('utf-8')) else: parser.read_string(config_content) name = parser['general']['name'] version = parser['general']['version'] summary = parser['general']['summary'] license = parser['general']['license'] try: packager = parser['general']['packager'] except Exception: packager = parser['general']['maintainer'] vendor = parser['general']['vendor'] url = parser['general']['url'] tmpdir = os.path.join(RUNTIME_HOME, "tmp", "plugin_%s" % get_unique_hexa_identifier()) mkdir_p_or_die(os.path.join(tmpdir, "BUILD")) mkdir_p_or_die(os.path.join(tmpdir, "RPMS")) mkdir_p_or_die(os.path.join(tmpdir, "SRPMS")) _make_plugin_spec(os.path.join(tmpdir, "specfile.spec"), name, version, summary, license, packager, vendor, url) cmd = "source %s/lib/bash_utils.sh ; " % MFEXT_HOME cmd = cmd + "layer_load rpm@mfext ; " cmd = cmd + 'rpmbuild --define "_topdir %s" --define "pwd %s" ' \ '--define "prefix %s" --dbpath %s ' \ '-bb %s/specfile.spec' % (tmpdir, plugin_path, tmpdir, base, tmpdir) x = BashWrapperOrRaise(cmd, MFUtilPluginCantBuild, "can't build plugin %s" % plugin_path) tmp = glob.glob(os.path.join(tmpdir, "RPMS", "x86_64", "*.rpm")) if len(tmp) == 0: raise MFUtilPluginCantBuild("can't find generated plugin" % plugin_path, bash_wrapper=x) plugin_path = tmp[0] new_basename = \ os.path.basename(plugin_path).replace("x86_64.rpm", "metwork.%s.plugin" % MODULE_LOWERCASE) new_plugin_path = os.path.join(pwd, new_basename) shutil.move(plugin_path, new_plugin_path) shutil.rmtree(tmpdir, True) os.chdir(pwd) return new_plugin_path def get_plugin_info(name_or_filepath, mode="auto", plugins_base_dir=None): plugins_base_dir = _get_plugins_base_dir(plugins_base_dir) _assert_plugins_base_initialized(plugins_base_dir) res = {} if mode == "auto": mode = "name" if '/' in name_or_filepath or '.' in name_or_filepath: mode = "file" else: if os.path.isfile(name_or_filepath): mode = "file" if mode == "file": cmd = _get_rpm_cmd('-qi', '-p %s' % name_or_filepath, plugins_base_dir=plugins_base_dir) elif mode == "name": if _is_dev_link_plugin(name_or_filepath, plugins_base_dir=plugins_base_dir): res['metadatas'] = {} res['metadatas']['name'] = name_or_filepath res['metadatas']['release'] = 'dev_link' res['metadatas']['version'] = 'dev_link' res['raw_metadata_output'] = 'DEV LINK' res['raw_files_output'] = 'DEV LINK' res['files'] = [] res['home'] = get_layer_home_from_plugin_name(name_or_filepath) return res cmd = _get_rpm_cmd('-qi', name_or_filepath, plugins_base_dir=plugins_base_dir) else: __get_logger().warning("unknown mode [%s]" % mode) return None metadata_output = BashWrapper(cmd) if not metadata_output: return None res['raw_metadata_output'] = metadata_output.stdout for line in metadata_output.stdout.split('\n'): tmp = line.strip().split(':', 1) if len(tmp) <= 1: continue name = tmp[0].strip().lower() value = tmp[1].strip() if 'metadatas' not in res: res['metadatas'] = {} res['metadatas'][name] = value if mode == "name": res["home"] = \ get_layer_home_from_plugin_name(name_or_filepath) if mode == "file": cmd = _get_rpm_cmd('-ql -p %s' % name_or_filepath, plugins_base_dir=plugins_base_dir) else: cmd = _get_rpm_cmd('-ql %s' % name_or_filepath, plugins_base_dir=plugins_base_dir) files_output = BashWrapper(cmd) if not files_output: return None res['files'] = [x.strip() for x in files_output.stdout.split('\n')] res['raw_files_output'] = files_output.stdout return res def get_plugin_hash(name_or_filepath, mode="auto", plugins_base_dir=None): infos = get_plugin_info(name_or_filepath, mode=mode, plugins_base_dir=plugins_base_dir) if infos is None: return None sid = ", ".join([infos['metadatas'].get('build host', 'unknown'), infos['metadatas'].get('build date', 'unknown'), infos['metadatas'].get('size', 'unknown'), infos['metadatas'].get('version', 'unknown'), infos['metadatas'].get('release', 'unknown')]) return hashlib.md5(sid.encode('utf8')).hexdigest()