#
# Copyright (C) 2015-2019 Stephane Thiell <sthiell@stanford.edu>
#
# This file is part of ClusterShell.
#
# ClusterShell is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# ClusterShell 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with ClusterShell; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
"""
ClusterShell Defaults module.
Manage library defaults.
"""
from __future__ import print_function
# Imported early
# Should not import any other ClusterShell modules when loaded
try:
from configparser import ConfigParser, NoOptionError, NoSectionError
except ImportError:
# Python 2 compat
from ConfigParser import ConfigParser, NoOptionError, NoSectionError
import os
import sys
#
# defaults.conf sections
#
CFG_SECTION_TASK_DEFAULT = 'task.default'
CFG_SECTION_TASK_INFO = 'task.info'
CFG_SECTION_NODESET = 'nodeset'
CFG_SECTION_ENGINE = 'engine'
#
# Functions
#
def _task_print_debug(task, line):
"""Default task debug printing function."""
print(line)
def _load_workerclass(workername):
"""
Return the class pointer matching `workername`.
This can be the 'short' name (such as `ssh`) or a fully-qualified
module path (such as ClusterShell.Worker.Ssh).
The module is loaded if not done yet.
"""
# First try the worker name as a module under ClusterShell.Worker,
# but if that fails, try the worker name directly
try:
modname = "ClusterShell.Worker.%s" % workername.capitalize()
_import_module(modname)
except ImportError:
modname = workername
_import_module(modname)
# Get the class pointer
return sys.modules[modname].WORKER_CLASS
def _import_module(modname):
"""Import a python module if not done yet."""
# Iterate over a copy of sys.modules' keys to avoid RuntimeError
if modname.lower() not in [mod.lower() for mod in list(sys.modules)]:
# Import module if not yet loaded
__import__(modname)
def _local_workerclass(defaults):
"""Return default local worker class."""
return _load_workerclass(defaults.local_workername)
def _distant_workerclass(defaults):
"""Return default distant worker class."""
return _load_workerclass(defaults.distant_workername)
def config_paths(config_name):
"""Return default path list for a ClusterShell config file name."""
paths = [os.path.join('/etc/clustershell', config_name), # system-wide
# default pip --user config file
os.path.expanduser('~/.local/etc/clustershell/%s' % config_name),
# Python installation prefix (for venv)
os.path.join(sys.prefix, 'etc/clustershell', config_name),
# per-user config (XDG Base Directory Specification)
os.path.join(os.environ.get('XDG_CONFIG_HOME',
os.path.expanduser('~/.config')),
'clustershell', config_name)]
# $CLUSTERSHELL_CFGDIR has precedence over any other config paths
if 'CLUSTERSHELL_CFGDIR' in os.environ:
paths.append(os.path.join(os.environ['CLUSTERSHELL_CFGDIR'],
config_name))
return paths
def _converter_integer_tuple(value):
"""ConfigParser converter for tuple of integers"""
# NOTE: compatible with ConfigParser 'converters' argument (Python 3.5+)
return tuple(int(x) for x in value.split(',') if x.strip())
def _parser_get_integer_tuple(parser, section, option, **kwargs):
"""
Compatible converter for parsing tuple of integers until we can use
converters from new ConfigParser (Python 3.5+).
"""
return _converter_integer_tuple(
ConfigParser.get(parser, section, option, **kwargs))
#
# Classes
#
[docs]
class Defaults(object):
"""
Class used to manipulate ClusterShell defaults.
The following attributes may be read at any time and also changed
programmatically, for most of them **before** ClusterShell objects
(Task or NodeSet) are initialized.
NodeSet defaults:
* fold_axis (tuple of axis integers; default is empty tuple ``()``)
Task defaults:
* stderr (boolean; default is ``False``)
* stdin (boolean; default is ``True``)
* stdout_msgtree (boolean; default is ``True``)
* stderr_msgtree (boolean; default is ``True``)
* engine (string; default is ``'auto'``)
* local_workername (string; default is ``'exec'``)
* distant_workername (string; default is ``'ssh'``)
* auto_tree (boolean; default is ``True``)
* debug (boolean; default is ``False``)
* print_debug (function; default is internal)
* fanout (integer; default is ``64``)
* grooming_delay (float; default is ``0.25``)
* connect_timeout (float; default is ``10``)
* command_timeout (float; default is ``0``)
Engine defaults:
* port_qlimit (integer; default is ``100``)
Example of use::
>>> from ClusterShell.Defaults import DEFAULTS
>>> from ClusterShell.Task import task_self
>>> # Change default distant worker to rsh (WorkerRsh)
... DEFAULTS.distant_workername = 'rsh'
>>> task = task_self()
>>> task.run("uname -r", nodes="cs[01-03]")
<ClusterShell.Worker.Rsh.WorkerRsh object at 0x1f4a410>
>>> list((str(msg), nodes) for msg, nodes in task.iter_buffers())
[('3.10.0-229.7.2.el7.x86_64', ['cs02', 'cs01', 'cs03'])]
The library default values of all of the above attributes may be changed
using the defaults.conf configuration file, except for *print_debug*
(cf. :ref:`defaults-config`). An example defaults.conf file should be
included with ClusterShell. Remember that this could affect all
applications using ClusterShell.
"""
#
# Default values for task "default" sync dict
#
_TASK_DEFAULT = {"stderr" : False,
"stdin" : True,
"stdout_msgtree" : True,
"stderr_msgtree" : True,
"engine" : 'auto',
"port_qlimit" : 100, # 1.8 compat
"auto_tree" : True,
"local_workername" : 'exec',
"distant_workername" : 'ssh'}
#
# Datatype converters for task_default
#
_TASK_DEFAULT_CONVERTERS = {"stderr" : ConfigParser.getboolean,
"stdin" : ConfigParser.getboolean,
"stdout_msgtree" : ConfigParser.getboolean,
"stderr_msgtree" : ConfigParser.getboolean,
"engine" : ConfigParser.get,
"port_qlimit" : ConfigParser.getint, # 1.8 compat
"auto_tree" : ConfigParser.getboolean,
"local_workername" : ConfigParser.get,
"distant_workername" : ConfigParser.get}
#
# Default values for task "info" async dict
#
_TASK_INFO = {"debug" : False,
"print_debug" : _task_print_debug,
"fanout" : 64,
"grooming_delay" : 0.25,
"connect_timeout" : 10,
"command_timeout" : 0}
#
# Datatype converters for task_info
#
_TASK_INFO_CONVERTERS = {"debug" : ConfigParser.getboolean,
"fanout" : ConfigParser.getint,
"grooming_delay" : ConfigParser.getfloat,
"connect_timeout" : ConfigParser.getfloat,
"command_timeout" : ConfigParser.getfloat}
#
# Black list of info keys whose values cannot safely be propagated
# in tree mode
#
_TASK_INFO_PKEYS_BL = ['engine', 'print_debug']
#
# Default values for NodeSet
#
_NODESET = {"fold_axis" : ()}
#
# Datatype converters for NodeSet defaults
#
_NODESET_CONVERTERS = {"fold_axis" : _parser_get_integer_tuple}
#
# Default values for Engine objects
#
_ENGINE = {"port_qlimit" : 100}
#
# Datatype converters for Engine defaults
#
_ENGINE_CONVERTERS = {"port_qlimit" : ConfigParser.getint}
def __init__(self, filenames):
"""Initialize Defaults from config filenames"""
self._task_default = self._TASK_DEFAULT.copy()
self._task_info = self._TASK_INFO.copy()
self._task_info_pkeys_bl = list(self._TASK_INFO_PKEYS_BL)
self._nodeset = self._NODESET.copy()
self._engine = self._ENGINE.copy()
config = ConfigParser()
parsed = config.read(filenames)
if parsed:
self._parse_config(config)
def _parse_config(self, config):
"""parse config"""
# task_default overrides
for key, conv in self._TASK_DEFAULT_CONVERTERS.items():
try:
self._task_default[key] = conv(config, CFG_SECTION_TASK_DEFAULT,
key)
except (NoSectionError, NoOptionError):
pass
# task_info overrides
for key, conv in self._TASK_INFO_CONVERTERS.items():
try:
self._task_info[key] = conv(config, CFG_SECTION_TASK_INFO, key)
except (NoSectionError, NoOptionError):
pass
# NodeSet
for key, conv in self._NODESET_CONVERTERS.items():
try:
self._nodeset[key] = conv(config, CFG_SECTION_NODESET, key)
except (NoSectionError, NoOptionError):
pass
# Engine
for key, conv in self._ENGINE_CONVERTERS.items():
try:
self._engine[key] = conv(config, CFG_SECTION_ENGINE, key)
except (NoSectionError, NoOptionError):
pass
def __getattr__(self, name):
"""Defaults attribute lookup"""
# 1.8 compat: port_qlimit moved into engine section
if name == 'port_qlimit':
if self._engine[name] == self._ENGINE[name]:
return self._task_default[name]
if name in self._engine:
return self._engine[name]
elif name in self._task_default:
return self._task_default[name]
elif name in self._task_info:
return self._task_info[name]
elif name in self._nodeset:
return self._nodeset[name]
raise AttributeError(name)
def __setattr__(self, name, value):
"""Defaults attribute assignment"""
if name in ('_task_default', '_task_info', '_task_info_pkeys_bl',
'_nodeset', '_engine'):
object.__setattr__(self, name, value)
elif name in self._engine:
self._engine[name] = value
elif name in self._task_default:
self._task_default[name] = value
elif name in self._task_info:
self._task_info[name] = value
elif name in self._nodeset:
self._nodeset[name] = value
else:
raise AttributeError(name)
#
# Globally accessible Defaults object
#
DEFAULTS = Defaults(config_paths('defaults.conf'))