summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChris St. Pierre <chris.a.st.pierre@gmail.com>2013-08-12 08:26:50 -0400
committerChris St. Pierre <chris.a.st.pierre@gmail.com>2013-08-12 08:29:53 -0400
commit5c573e00a168c90c5c718566c75aadf736566676 (patch)
tree0dba4d9411304f4d29daf3569535227be8939abf
parent0f7edd60e67d32438a8be42002faacde4e4a7649 (diff)
downloadbcfg2-5c573e00a168c90c5c718566c75aadf736566676.tar.gz
bcfg2-5c573e00a168c90c5c718566c75aadf736566676.tar.bz2
bcfg2-5c573e00a168c90c5c718566c75aadf736566676.zip
testsuite: fixed more unit tests
-rw-r--r--src/lib/Bcfg2/Client/Proxy.py10
-rw-r--r--src/lib/Bcfg2/Client/Tools/RcUpdate.py4
-rw-r--r--src/lib/Bcfg2/Client/__init__.py2
-rw-r--r--src/lib/Bcfg2/Options/Common.py20
-rw-r--r--src/lib/Bcfg2/Options/OptionGroups.py8
-rw-r--r--src/lib/Bcfg2/Options/Subcommands.py9
-rw-r--r--src/lib/Bcfg2/Server/Admin.py19
-rw-r--r--src/lib/Bcfg2/Server/Info.py5
-rw-r--r--src/lib/Bcfg2/Server/Lint/Cfg.py2
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Git.py1
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Metadata.py7
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Packages/Yum.py31
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Packages/YumHelper.py4
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Packages/__init__.py30
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Probes.py4
-rw-r--r--src/lib/Bcfg2/Utils.py11
16 files changed, 73 insertions, 94 deletions
diff --git a/src/lib/Bcfg2/Client/Proxy.py b/src/lib/Bcfg2/Client/Proxy.py
index 98d081b10..a464d6a40 100644
--- a/src/lib/Bcfg2/Client/Proxy.py
+++ b/src/lib/Bcfg2/Client/Proxy.py
@@ -356,7 +356,7 @@ class ComponentProxy(xmlrpclib.ServerProxy):
options = [
Bcfg2.Options.Common.location, Bcfg2.Options.Common.ssl_key,
Bcfg2.Options.Common.ssl_cert, Bcfg2.Options.Common.ssl_ca,
- Bcfg2.Options.Common.password,
+ Bcfg2.Options.Common.password, Bcfg2.Options.Common.client_timeout,
Bcfg2.Options.Option(
"-u", "--user", default="root", cf=('communication', 'user'),
help='The user to provide for authentication'),
@@ -371,11 +371,7 @@ class ComponentProxy(xmlrpclib.ServerProxy):
Bcfg2.Options.Option(
'--ssl-cns', cf=('communication', 'serverCommonNames'),
type=Bcfg2.Options.Types.colon_list,
- help='List of server commonNames'),
- Bcfg2.Options.Option(
- "-t", "--timeout", type=float, default=90.0,
- cf=('communication', 'timeout'),
- help='Set the client XML-RPC timeout')]
+ help='List of server commonNames')]
def __init__(self):
RetryMethod.max_retries = Bcfg2.Options.setup.retries
@@ -394,6 +390,6 @@ class ComponentProxy(xmlrpclib.ServerProxy):
Bcfg2.Options.setup.cert,
Bcfg2.Options.setup.ca,
Bcfg2.Options.setup.ssl_cns,
- Bcfg2.Options.setup.timeout)
+ Bcfg2.Options.setup.client_timeout)
xmlrpclib.ServerProxy.__init__(self, url,
allow_none=True, transport=ssl_trans)
diff --git a/src/lib/Bcfg2/Client/Tools/RcUpdate.py b/src/lib/Bcfg2/Client/Tools/RcUpdate.py
index e0c913dcd..a482dbc00 100644
--- a/src/lib/Bcfg2/Client/Tools/RcUpdate.py
+++ b/src/lib/Bcfg2/Client/Tools/RcUpdate.py
@@ -98,10 +98,10 @@ class RcUpdate(Bcfg2.Client.Tools.SvcTool):
# make sure service is disabled on boot
bootcmd = '/sbin/rc-update del %s default'
bootcmdrv = self.cmd.run(bootcmd % entry.get('name')).success
- if self.setup['servicemode'] == 'disabled':
+ if Bcfg2.Options.setup.service_mode == 'disabled':
# 'disabled' means we don't attempt to modify running svcs
return bootcmdrv
- buildmode = self.setup['servicemode'] == 'build'
+ buildmode = Bcfg2.Options.setup.service_mode == 'build'
if (entry.get('status') == 'on' and not buildmode) and \
entry.get('current_status') == 'off':
svccmdrv = self.start_service(entry)
diff --git a/src/lib/Bcfg2/Client/__init__.py b/src/lib/Bcfg2/Client/__init__.py
index e31ef47fe..811e5e50b 100644
--- a/src/lib/Bcfg2/Client/__init__.py
+++ b/src/lib/Bcfg2/Client/__init__.py
@@ -311,7 +311,7 @@ class Client(object):
# TODO: read decision list from --decision-list
Bcfg2.Options.setup.decision_list = \
self.proxy.GetDecisionList(
- Bcfg2.Options.setup.decision)
+ Bcfg2.Options.setup.decision)
self.logger.info("Got decision list from server:")
self.logger.info(Bcfg2.Options.setup.decision_list)
except Proxy.ProxyError:
diff --git a/src/lib/Bcfg2/Options/Common.py b/src/lib/Bcfg2/Options/Common.py
index b44c58990..a00bb241c 100644
--- a/src/lib/Bcfg2/Options/Common.py
+++ b/src/lib/Bcfg2/Options/Common.py
@@ -1,5 +1,6 @@
""" Common options used in multiple different contexts. """
+from Bcfg2.Utils import classproperty
# pylint: disable=W0403
import Types
from Actions import PluginsAction, ComponentAction
@@ -10,17 +11,6 @@ from Options import Option, PathOption, BooleanOption
__all__ = ["Common"]
-class classproperty(object):
- """ Decorator that can be used to create read-only class
- properties. """
-
- def __init__(self, getter):
- self.getter = getter
-
- def __get__(self, instance, owner):
- return self.getter(owner)
-
-
class ReportingTransportAction(ComponentAction):
""" :class:`Bcfg2.Options.ComponentAction` that loads a single
reporting transport from :mod:`Bcfg2.Reporting.Transport`. """
@@ -62,6 +52,8 @@ class Common(object):
import Bcfg2.Server.FileMonitor
class FileMonitorAction(ComponentAction):
+ """ ComponentAction for loading a single FAM backend
+ class """
islist = False
mapping = Bcfg2.Server.FileMonitor.available
@@ -135,3 +127,9 @@ class Common(object):
default_paranoid = Option(
cf=('mdata', 'paranoid'), dest="default_paranoid", default='true',
choices=['true', 'false'], help='Default Path paranoid setting')
+
+ #: Client timeout
+ client_timeout = Option(
+ "-t", "--timeout", type=float, default=90.0, dest="client_timeout",
+ cf=('communication', 'timeout'),
+ help='Set the client XML-RPC timeout')
diff --git a/src/lib/Bcfg2/Options/OptionGroups.py b/src/lib/Bcfg2/Options/OptionGroups.py
index 6cbe1a8e3..70cb5d0dd 100644
--- a/src/lib/Bcfg2/Options/OptionGroups.py
+++ b/src/lib/Bcfg2/Options/OptionGroups.py
@@ -35,7 +35,7 @@ class OptionGroup(OptionContainer):
behind the scenes. """
def __init__(self, *items, **kwargs):
- """
+ r"""
:param \*args: Child options
:type \*args: Bcfg2.Options.Option
:param title: The title of the option group
@@ -59,7 +59,7 @@ class ExclusiveOptionGroup(OptionContainer):
behind the scenes."""
def __init__(self, *items, **kwargs):
- """
+ r"""
:param \*args: Child options
:type \*args: Bcfg2.Options.Option
:param required: Exactly one argument in the group *must* be
@@ -89,7 +89,7 @@ class Subparser(OptionContainer):
_subparsers = dict()
def __init__(self, *items, **kwargs):
- """
+ r"""
:param \*args: Child options
:type \*args: Bcfg2.Options.Option
:param name: The name of the subparser. Required.
@@ -159,7 +159,7 @@ class WildcardSectionGroup(OptionContainer, Option):
_dest_re = re.compile(r'(\A(_|[^A-Za-z])+)|((_|[^A-Za-z0-9])+)')
def __init__(self, *items, **kwargs):
- """
+ r"""
:param \*args: Child options
:type \*args: Bcfg2.Options.Option
:param prefix: The prefix to use for options generated by this
diff --git a/src/lib/Bcfg2/Options/Subcommands.py b/src/lib/Bcfg2/Options/Subcommands.py
index 53c4e563f..b529dd7fe 100644
--- a/src/lib/Bcfg2/Options/Subcommands.py
+++ b/src/lib/Bcfg2/Options/Subcommands.py
@@ -8,10 +8,11 @@ import copy
import shlex
import logging
from Bcfg2.Compat import StringIO
+# pylint: disable=W0403
from OptionGroups import Subparser
from Options import PositionalArgument
from Parser import Parser, setup as master_setup
-
+# pylint: enable=W0403
__all__ = ["Subcommand", "HelpCommand", "CommandRegistry", "register_commands"]
@@ -94,9 +95,9 @@ class Subcommand(object):
def usage(self):
""" Get the short usage message. """
if self._usage is None:
- io = StringIO()
- self.parser.print_usage(file=io)
- usage = self._ws_re.sub(' ', io.getvalue()).strip()[7:]
+ sio = StringIO()
+ self.parser.print_usage(file=sio)
+ usage = self._ws_re.sub(' ', sio.getvalue()).strip()[7:]
doc = self._ws_re.sub(' ', getattr(self, "__doc__")).strip()
if doc is None:
self._usage = usage
diff --git a/src/lib/Bcfg2/Server/Admin.py b/src/lib/Bcfg2/Server/Admin.py
index 62b5cd0ac..47ba07482 100644
--- a/src/lib/Bcfg2/Server/Admin.py
+++ b/src/lib/Bcfg2/Server/Admin.py
@@ -39,7 +39,7 @@ except ImportError:
HAS_REPORTS = False
-class ccolors: # pylint: disable=C0103
+class ccolors: # pylint: disable=C0103,W0232
""" ANSI color escapes to make colorizing text easier """
# pylint: disable=W1401
ADDED = '\033[92m'
@@ -95,7 +95,7 @@ def print_table(rows, justify='left', hdr=True, vdelim=" ", padding=1):
hdr = False
-class AdminCmd(Bcfg2.Options.Subcommand):
+class AdminCmd(Bcfg2.Options.Subcommand): # pylint: disable=W0223
""" Base class for all bcfg2-admin modes """
def setup(self):
""" Perform post-init (post-options parsing), pre-run setup
@@ -108,7 +108,7 @@ class AdminCmd(Bcfg2.Options.Subcommand):
raise SystemExit(1)
-class _ServerAdminCmd(AdminCmd):
+class _ServerAdminCmd(AdminCmd): # pylint: disable=W0223
""" Base class for admin modes that run a Bcfg2 server. """
__plugin_whitelist__ = None
__plugin_blacklist__ = None
@@ -142,7 +142,7 @@ class _ServerAdminCmd(AdminCmd):
self.core.shutdown()
-class _ProxyAdminCmd(AdminCmd):
+class _ProxyAdminCmd(AdminCmd): # pylint: disable=W0223
""" Base class for admin modes that proxy to a running Bcfg2 server """
options = AdminCmd.options + Bcfg2.Client.Proxy.ComponentProxy.options
@@ -317,9 +317,10 @@ class Compare(AdminCmd):
def run(self, setup): # pylint: disable=R0912,R0914,R0915
if not sys.stdout.isatty() and not setup.color:
- ccolors.disable(ccolors)
+ ccolors.disable()
- for file1, file2 in self._get_filelists():
+ files = self._get_filelists(setup)
+ for file1, file2 in files:
host = None
if os.path.basename(file1) == os.path.basename(file2):
fname = os.path.basename(file1)
@@ -823,7 +824,7 @@ class Pull(_ServerAdminCmd):
vcsplugin.commit_data([files], comment)
-class _ReportsCmd(AdminCmd):
+class _ReportsCmd(AdminCmd): # pylint: disable=W0223
""" Base command for all admin modes dealing with the reporting
subsystem """
def __init__(self):
@@ -838,7 +839,7 @@ class _ReportsCmd(AdminCmd):
# Bcfg2.settings has been populated, Django gets a null
# configuration, and subsequent updates to Bcfg2.settings
# won't help.
- import Bcfg2.Reporting.models
+ import Bcfg2.Reporting.models # pylint: disable=W0621
self.reports_entries = (Bcfg2.Reporting.models.Group,
Bcfg2.Reporting.models.Bundle,
Bcfg2.Reporting.models.FailureEntry,
@@ -916,7 +917,7 @@ if HAS_REPORTS:
from django.db.transaction import commit_on_success
self.run = commit_on_success(self.run)
- def run(self, _):
+ def run(self, _): # pylint: disable=E0202
# Cleanup unused entries
for cls in self.reports_entries:
try:
diff --git a/src/lib/Bcfg2/Server/Info.py b/src/lib/Bcfg2/Server/Info.py
index 2b2149606..649ab2bb7 100644
--- a/src/lib/Bcfg2/Server/Info.py
+++ b/src/lib/Bcfg2/Server/Info.py
@@ -85,7 +85,7 @@ def load_interpreters():
return (interpreters, default)
-class InfoCmd(Bcfg2.Options.Subcommand):
+class InfoCmd(Bcfg2.Options.Subcommand): # pylint: disable=W0223
""" Base class for bcfg2-info subcommands """
def _expand_globs(self, globs, candidates):
@@ -128,6 +128,9 @@ class Help(InfoCmd, Bcfg2.Options.HelpCommand):
def command_registry(self):
return self.core.commands
+ def run(self, setup):
+ Bcfg2.Options.HelpCommand.run(self, setup)
+
class Debug(InfoCmd):
""" Shell out to a Python interpreter """
diff --git a/src/lib/Bcfg2/Server/Lint/Cfg.py b/src/lib/Bcfg2/Server/Lint/Cfg.py
index 2cdb1f7b8..c5204e599 100644
--- a/src/lib/Bcfg2/Server/Lint/Cfg.py
+++ b/src/lib/Bcfg2/Server/Lint/Cfg.py
@@ -75,7 +75,7 @@ class Cfg(ServerPlugin):
not any(fnmatch(fpath, p)
for p in Bcfg2.Options.setup.ignore_files) and
not any(fnmatch(c, p)
- for p in sBcfg2.Options.setup.ignore_files
+ for p in Bcfg2.Options.setup.ignore_files
for c in self._list_path_components(fpath))):
all_files.add(fpath)
diff --git a/src/lib/Bcfg2/Server/Plugins/Git.py b/src/lib/Bcfg2/Server/Plugins/Git.py
index d0502ed6a..3144a4f97 100644
--- a/src/lib/Bcfg2/Server/Plugins/Git.py
+++ b/src/lib/Bcfg2/Server/Plugins/Git.py
@@ -2,6 +2,7 @@
git. """
import sys
+import Bcfg2.Options
from Bcfg2.Server.Plugin import Version, PluginExecutionError
try:
diff --git a/src/lib/Bcfg2/Server/Plugins/Metadata.py b/src/lib/Bcfg2/Server/Plugins/Metadata.py
index e2da2a6d4..3d82beb87 100644
--- a/src/lib/Bcfg2/Server/Plugins/Metadata.py
+++ b/src/lib/Bcfg2/Server/Plugins/Metadata.py
@@ -40,13 +40,13 @@ def load_django_models():
HAS_DJANGO = False
return
- class MetadataClientModel(models.Model,
+ class MetadataClientModel(models.Model, # pylint: disable=W0621
Bcfg2.Server.Plugin.PluginDatabaseModel):
""" django model for storing clients in the database """
hostname = models.CharField(max_length=255, primary_key=True)
version = models.CharField(max_length=31, null=True)
- class ClientVersions(MutableMapping,
+ class ClientVersions(MutableMapping, # pylint: disable=W0621,W0612
Bcfg2.Server.Plugin.DatabaseBacked):
""" dict-like object to make it easier to access client bcfg2
versions from the database """
@@ -559,7 +559,8 @@ class Metadata(Bcfg2.Server.Plugin.Metadata,
self.negated_groups = dict()
# mapping of hostname -> version string
if self._use_db:
- self.versions = ClientVersions(core, datastore)
+ self.versions = ClientVersions(core, # pylint: disable=E1102
+ datastore)
else:
self.versions = dict()
self.uuid = {}
diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py b/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py
index 0d49473c6..5f66cb8a0 100644
--- a/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py
+++ b/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py
@@ -107,7 +107,8 @@ PULPSERVER = None
PULPCONFIG = None
-options = [
+options = [ # pylint: disable=C0103
+ Bcfg2.Options.Common.client_timeout,
Bcfg2.Options.PathOption(
cf=("packages:yum", "helper"), dest="yum_helper",
help="Path to the bcfg2-yum-helper executable"),
@@ -307,7 +308,7 @@ class YumCollection(Collection):
if not os.path.exists(self.cachefile):
self.debug_log("Creating common cache %s" % self.cachefile)
os.mkdir(self.cachefile)
- if not self.disableMetaData:
+ if Bcfg2.Options.setup.packages_metadata:
self.setup_data()
self.cmd = Executor()
else:
@@ -334,26 +335,6 @@ class YumCollection(Collection):
self.__class__.pulp_cert_set = PulpCertificateSet(certdir)
@property
- def disableMetaData(self):
- """ Report whether or not metadata processing is enabled.
- This duplicates code in Packages/__init__.py, and can probably
- be removed in Bcfg2 1.4 when we have a module-level setup
- object. """
- if self.setup is None:
- return True
- try:
- return not self.setup.cfp.getboolean("packages", "resolver")
- except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
- return False
- except ValueError:
- # for historical reasons we also accept "enabled" and
- # "disabled"
- return self.setup.cfp.get(
- "packages",
- "metadata",
- default="enabled").lower() == "disabled"
-
- @property
def __package_groups__(self):
return True
@@ -935,10 +916,12 @@ class YumCollection(Collection):
self.debug_log("Packages: running %s" % " ".join(cmd))
if inputdata:
- result = self.cmd.run(cmd, timeout=self.setup['client_timeout'],
+ result = self.cmd.run(cmd,
+ timeout=Bcfg2.Options.setup.client_timeout,
inputdata=json.dumps(inputdata))
else:
- result = self.cmd.run(cmd, timeout=self.setup['client_timeout'])
+ result = self.cmd.run(cmd,
+ timeout=Bcfg2.Options.setup.client_timeout)
if not result.success:
self.logger.error("Packages: error running bcfg2-yum-helper: %s" %
result.error)
diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/YumHelper.py b/src/lib/Bcfg2/Server/Plugins/Packages/YumHelper.py
index dcb8718a0..48304d26e 100644
--- a/src/lib/Bcfg2/Server/Plugins/Packages/YumHelper.py
+++ b/src/lib/Bcfg2/Server/Plugins/Packages/YumHelper.py
@@ -306,7 +306,7 @@ class HelperSubcommand(Bcfg2.Options.Subcommand):
raise NotImplementedError
-class DepSolverSubcommand(HelperSubcommand):
+class DepSolverSubcommand(HelperSubcommand): # pylint: disable=W0223
""" Base class for helper commands that use the depsolver (i.e.,
only resolve dependencies, don't modify the cache) """
@@ -316,7 +316,7 @@ class DepSolverSubcommand(HelperSubcommand):
self.verbosity)
-class CacheManagerSubcommand(HelperSubcommand):
+class CacheManagerSubcommand(HelperSubcommand): # pylint: disable=W0223
""" Base class for helper commands that use the cachemanager
(i.e., modify the cache) """
fallback = False
diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/__init__.py b/src/lib/Bcfg2/Server/Plugins/Packages/__init__.py
index efd0bbe4a..7dcc2dccc 100644
--- a/src/lib/Bcfg2/Server/Plugins/Packages/__init__.py
+++ b/src/lib/Bcfg2/Server/Plugins/Packages/__init__.py
@@ -29,6 +29,7 @@ def packages_boolean(value):
class PackagesBackendAction(Bcfg2.Options.ComponentAction):
+ """ ComponentAction to load Packages backends """
bases = ['Bcfg2.Server.Plugins.Packages']
module = True
@@ -218,25 +219,6 @@ class Packages(Bcfg2.Server.Plugin.Plugin,
return rv
set_debug.__doc__ = Bcfg2.Server.Plugin.Plugin.set_debug.__doc__
- @property
- def disableResolver(self):
- """ Report the state of the resolver. This can be disabled in
- the configuration. Note that disabling metadata (see
- :attr:`disableMetaData`) implies disabling the resolver.
-
- This property cannot be set. """
- # disabling metadata without disabling the resolver Breaks
- # Things
- return not Bcfg2.Options.setup.packages_metadata or \
- not Bcfg2.Options.setup.packages_resolver
-
- @property
- def disableMetaData(self):
- """ Report whether or not metadata processing is enabled.
-
- This property cannot be set. """
- return not Bcfg2.Options.setup.packages_metadata
-
def create_config(self, entry, metadata):
""" Create yum/apt config for the specified client.
@@ -376,8 +358,10 @@ class Packages(Bcfg2.Server.Plugin.Plugin,
:func:`get_collection`
:type collection: Bcfg2.Server.Plugins.Packages.Collection.Collection
"""
- if self.disableResolver:
- # Config requests no resolver
+ if (not Bcfg2.Options.setup.packages_metadata or
+ not Bcfg2.Options.setup.packages_resolver):
+ # Config requests no resolver. Note that disabling
+ # metadata implies disabling the resolver.
for struct in structures:
for pkg in struct.xpath('//Package | //BoundPackage'):
if pkg.get("group"):
@@ -484,7 +468,7 @@ class Packages(Bcfg2.Server.Plugin.Plugin,
for collection in list(self.collections.values()):
cachefiles.update(collection.cachefiles)
- if not self.disableMetaData:
+ if Bcfg2.Options.setup.packages_metadata:
collection.setup_data(force_update)
# clear Collection and package caches
@@ -495,7 +479,7 @@ class Packages(Bcfg2.Server.Plugin.Plugin,
for source in self.sources.entries:
cachefiles.add(source.cachefile)
- if not self.disableMetaData:
+ if Bcfg2.Options.setup.packages_metadata:
source.setup_data(force_update)
for cfile in glob.glob(os.path.join(self.cachepath, "cache-*")):
diff --git a/src/lib/Bcfg2/Server/Plugins/Probes.py b/src/lib/Bcfg2/Server/Plugins/Probes.py
index 012c1958a..f75d88d8f 100644
--- a/src/lib/Bcfg2/Server/Plugins/Probes.py
+++ b/src/lib/Bcfg2/Server/Plugins/Probes.py
@@ -31,7 +31,7 @@ def load_django_models():
HAS_DJANGO = False
return
- class ProbesDataModel(models.Model,
+ class ProbesDataModel(models.Model, # pylint: disable=W0621,W0612
Bcfg2.Server.Plugin.PluginDatabaseModel):
""" The database model for storing probe data """
hostname = models.CharField(max_length=255)
@@ -39,7 +39,7 @@ def load_django_models():
timestamp = models.DateTimeField(auto_now=True)
data = models.TextField(null=True)
- class ProbesGroupsModel(models.Model,
+ class ProbesGroupsModel(models.Model, # pylint: disable=W0621,W0612
Bcfg2.Server.Plugin.PluginDatabaseModel):
""" The database model for storing probe groups """
hostname = models.CharField(max_length=255)
diff --git a/src/lib/Bcfg2/Utils.py b/src/lib/Bcfg2/Utils.py
index ccb79249e..236f87d0a 100644
--- a/src/lib/Bcfg2/Utils.py
+++ b/src/lib/Bcfg2/Utils.py
@@ -313,3 +313,14 @@ def safe_input(msg):
while len(select.select([sys.stdin.fileno()], [], [], 0.0)[0]) > 0:
os.read(sys.stdin.fileno(), 4096)
return input(msg)
+
+
+class classproperty(object): # pylint: disable=C0103
+ """ Decorator that can be used to create read-only class
+ properties. """
+
+ def __init__(self, getter):
+ self.getter = getter
+
+ def __get__(self, instance, owner):
+ return self.getter(owner)