diff options
Diffstat (limited to 'src/lib/Bcfg2/Server/Core.py')
-rw-r--r-- | src/lib/Bcfg2/Server/Core.py | 458 |
1 files changed, 303 insertions, 155 deletions
diff --git a/src/lib/Bcfg2/Server/Core.py b/src/lib/Bcfg2/Server/Core.py index 8482925b7..f39453edd 100644 --- a/src/lib/Bcfg2/Server/Core.py +++ b/src/lib/Bcfg2/Server/Core.py @@ -1,35 +1,21 @@ """Bcfg2.Server.Core provides the runtime support for Bcfg2 modules.""" +import os import atexit import logging import select import sys import threading import time +import inspect +import lxml.etree from traceback import format_exc - -try: - import lxml.etree -except ImportError: - print("Failed to import lxml dependency. Shutting down server.") - raise SystemExit(1) - -from Bcfg2.Component import Component, exposed -from Bcfg2.Server.Plugin import PluginInitError, PluginExecutionError +import Bcfg2.settings +import Bcfg2.Server +import Bcfg2.Logger import Bcfg2.Server.FileMonitor -import Bcfg2.Server.Plugins.Metadata -# Compatibility imports -from Bcfg2.Bcfg2Py3k import xmlrpclib -if sys.hexversion >= 0x03000000: - from functools import reduce - -logger = logging.getLogger('Bcfg2.Server.Core') - - -def critical_error(operation): - """Log and err, traceback and return an xmlrpc fault to client.""" - logger.error(operation, exc_info=1) - raise xmlrpclib.Fault(7, "Critical unexpected failure: %s" % (operation)) +from Bcfg2.Bcfg2Py3k import xmlrpclib, reduce +from Bcfg2.Server.Plugin import PluginInitError, PluginExecutionError try: import psyco @@ -37,6 +23,11 @@ try: except: pass +os.environ['DJANGO_SETTINGS_MODULE'] = 'Bcfg2.settings' + +def exposed(func): + func.exposed = True + return func def sort_xml(node, key=None): for child in node: @@ -54,88 +45,134 @@ class CoreInitError(Exception): pass -class Core(Component): +class BaseCore(object): """The Core object is the container for all Bcfg2 Server logic and modules. """ - name = 'bcfg2-server' - implementation = 'bcfg2-server' - - def __init__(self, repo, plugins, password, encoding, - cfile='/etc/bcfg2.conf', ca=None, setup=None, - filemonitor='default', start_fam_thread=False): - Component.__init__(self) - self.datastore = repo - if filemonitor not in Bcfg2.Server.FileMonitor.available: - logger.error("File monitor driver %s not available; " - "forcing to default" % filemonitor) - filemonitor = 'default' + + def __init__(self, setup, start_fam_thread=False): + self.datastore = setup['repo'] + + if setup['debug']: + level = logging.DEBUG + elif setup['verbose']: + level = logging.INFO + else: + level = logging.WARNING + # we set a higher log level for the console by default. we + # assume that if someone is running bcfg2-server in such a way + # that it _can_ log to console, they want more output. if + # level is set to DEBUG, that will get handled by + # setup_logging and the console will get DEBUG output. + Bcfg2.Logger.setup_logging('bcfg2-server', + to_console=logging.INFO, + to_syslog=setup['syslog'], + to_file=setup['logging'], + level=level) + self.logger = logging.getLogger('bcfg2-server') + try: - self.fam = Bcfg2.Server.FileMonitor.available[filemonitor]() + fm = Bcfg2.Server.FileMonitor.available[setup['filemonitor']] + except KeyError: + self.logger.error("File monitor driver %s not available; " + "forcing to default" % filemonitor) + fm = Bcfg2.Server.FileMonitor.available['default'] + famargs = dict(ignore=[], debug=False) + if 'ignore' in setup: + famargs['ignore'] = setup['ignore'] + if 'debug' in setup: + famargs['debug'] = setup['debug'] + try: + self.fam = fm(**famargs) except IOError: - logger.error("Failed to instantiate fam driver %s" % filemonitor, - exc_info=1) - raise CoreInitError("failed to instantiate fam driver (used %s)" % \ - filemonitor) + msg = "Failed to instantiate fam driver %s" % setup['filemonitor'] + self.logger.error(msg, exc_info=1) + raise CoreInitError(msg) self.pubspace = {} - self.cfile = cfile + self.cfile = setup['configfile'] self.cron = {} self.plugins = {} self.plugin_blacklist = {} self.revision = '-1' - self.password = password - self.encoding = encoding + self.password = setup['password'] + self.encoding = setup['encoding'] self.setup = setup atexit.register(self.shutdown) # Create an event to signal worker threads to shutdown self.terminate = threading.Event() - if '' in plugins: - plugins.remove('') + # generate Django ORM settings. this must be done _before_ we + # load plugins + Bcfg2.settings.read_config(cfile=self.setup['web_configfile'], + repo=self.datastore) - for plugin in plugins: + self._database_available = False + # verify our database schema + try: + from Bcfg2.Server.SchemaUpdater import update_database, UpdaterError + try: + update_database() + self._database_available = True + except UpdaterError: + err = sys.exc_info()[1] + self.logger.error("Failed to update database schema: %s" % err) + except ImportError: + # assume django is not installed + pass + except Exception: + inst = sys.exc_info()[1] + self.logger.error("Failed to update database schema") + self.logger.error(str(inst)) + self.logger.error(str(type(inst))) + raise CoreInitError + + if '' in setup['plugins']: + setup['plugins'].remove('') + + for plugin in setup['plugins']: if not plugin in self.plugins: self.init_plugins(plugin) # Remove blacklisted plugins for p, bl in list(self.plugin_blacklist.items()): if len(bl) > 0: - logger.error("The following plugins conflict with %s;" - "Unloading %s" % (p, bl)) + self.logger.error("The following plugins conflict with %s;" + "Unloading %s" % (p, bl)) for plug in bl: del self.plugins[plug] # This section logs the experimental plugins expl = [plug for (name, plug) in list(self.plugins.items()) if plug.experimental] if expl: - logger.info("Loading experimental plugin(s): %s" % \ - (" ".join([x.name for x in expl]))) - logger.info("NOTE: Interfaces subject to change") + self.logger.info("Loading experimental plugin(s): %s" % + (" ".join([x.name for x in expl]))) + self.logger.info("NOTE: Interfaces subject to change") # This section logs the deprecated plugins depr = [plug for (name, plug) in list(self.plugins.items()) if plug.deprecated] if depr: - logger.info("Loading deprecated plugin(s): %s" % \ - (" ".join([x.name for x in depr]))) + self.logger.info("Loading deprecated plugin(s): %s" % + (" ".join([x.name for x in depr]))) mlist = self.plugins_by_type(Bcfg2.Server.Plugin.Metadata) if len(mlist) == 1: self.metadata = mlist[0] else: - logger.error("No Metadata Plugin loaded; failed to instantiate Core") + self.logger.error("No Metadata Plugin loaded; " + "failed to instantiate Core") raise CoreInitError("No Metadata Plugin") self.statistics = self.plugins_by_type(Bcfg2.Server.Plugin.Statistics) self.pull_sources = self.plugins_by_type(Bcfg2.Server.Plugin.PullSource) self.generators = self.plugins_by_type(Bcfg2.Server.Plugin.Generator) self.structures = self.plugins_by_type(Bcfg2.Server.Plugin.Structure) self.connectors = self.plugins_by_type(Bcfg2.Server.Plugin.Connector) - self.ca = ca - self.fam_thread = threading.Thread(target=self._file_monitor_thread) + self.ca = setup['ca'] + self.fam_thread = \ + threading.Thread(name="%sFAMThread" % setup['filemonitor'], + target=self._file_monitor_thread) + self.lock = threading.Lock() + if start_fam_thread: self.fam_thread.start() - self.monitor_cfile() - - def monitor_cfile(self): - if self.setup: self.fam.AddMonitor(self.cfile, self.setup) def plugins_by_type(self, base_cls): @@ -171,16 +208,21 @@ class Core(Component): def init_plugins(self, plugin): """Handling for the plugins.""" + self.logger.debug("Loading plugin %s" % plugin) try: mod = getattr(__import__("Bcfg2.Server.Plugins.%s" % (plugin)).Server.Plugins, plugin) except ImportError: try: - mod = __import__(plugin) + mod = __import__(plugin, globals(), locals(), [plugin.split('.')[-1]]) except: - logger.error("Failed to load plugin %s" % (plugin)) + self.logger.error("Failed to load plugin %s" % plugin) return - plug = getattr(mod, plugin) + try: + plug = getattr(mod, plugin.split('.')[-1]) + except AttributeError: + self.logger.error("Failed to load plugin %s (AttributeError)" % plugin) + return # Blacklist conflicting plugins cplugs = [conflict for conflict in plug.conflicts if conflict in self.plugins] @@ -188,18 +230,35 @@ class Core(Component): try: self.plugins[plugin] = plug(self, self.datastore) except PluginInitError: - logger.error("Failed to instantiate plugin %s" % (plugin)) + self.logger.error("Failed to instantiate plugin %s" % plugin, + exc_info=1) except: - logger.error("Unexpected instantiation failure for plugin %s" % - (plugin), exc_info=1) + self.logger.error("Unexpected instantiation failure for plugin %s" % + plugin, exc_info=1) def shutdown(self): """Shutting down the plugins.""" if not self.terminate.isSet(): self.terminate.set() + self.fam.shutdown() for plugin in list(self.plugins.values()): plugin.shutdown() + def client_run_hook(self, hook, metadata): + """Checks the data structure.""" + for plugin in self.plugins_by_type(Bcfg2.Server.Plugin.ClientRunHooks): + try: + getattr(plugin, hook)(metadata) + except AttributeError: + err = sys.exc_info()[1] + self.logger.error("Unknown attribute: %s" % err) + raise + except: + err = sys.exc_info()[1] + self.logger.error("%s: Error invoking hook %s: %s" % (plugin, + hook, + err)) + def validate_structures(self, metadata, data): """Checks the data structure.""" for plugin in self.plugins_by_type(Bcfg2.Server.Plugin.StructureValidator): @@ -207,12 +266,12 @@ class Core(Component): plugin.validate_structures(metadata, data) except Bcfg2.Server.Plugin.ValidationError: err = sys.exc_info()[1] - logger.error("Plugin %s structure validation failed: %s" \ - % (plugin.name, err.message)) + self.logger.error("Plugin %s structure validation failed: %s" % + (plugin.name, err)) raise except: - logger.error("Plugin %s: unexpected structure validation failure" \ - % (plugin.name), exc_info=1) + self.logger.error("Plugin %s: unexpected structure validation " + "failure" % plugin.name, exc_info=1) def validate_goals(self, metadata, data): """Checks that the config matches the goals enforced by the plugins.""" @@ -221,23 +280,23 @@ class Core(Component): plugin.validate_goals(metadata, data) except Bcfg2.Server.Plugin.ValidationError: err = sys.exc_info()[1] - logger.error("Plugin %s goal validation failed: %s" \ - % (plugin.name, err.message)) + self.logger.error("Plugin %s goal validation failed: %s" % + (plugin.name, err.message)) raise except: - logger.error("Plugin %s: unexpected goal validation failure" \ - % (plugin.name), exc_info=1) + self.logger.error("Plugin %s: unexpected goal validation " + "failure" % plugin.name, exc_info=1) def GetStructures(self, metadata): """Get all structures for client specified by metadata.""" structures = reduce(lambda x, y: x + y, - [struct.BuildStructures(metadata) for struct \ - in self.structures], []) + [struct.BuildStructures(metadata) + for struct in self.structures], []) sbundles = [b.get('name') for b in structures if b.tag == 'Bundle'] missing = [b for b in metadata.bundles if b not in sbundles] if missing: - logger.error("Client %s configuration missing bundles: %s" \ - % (metadata.hostname, ':'.join(missing))) + self.logger.error("Client %s configuration missing bundles: %s" % + (metadata.hostname, ':'.join(missing))) return structures def BindStructure(self, structure, metadata): @@ -252,14 +311,14 @@ class Core(Component): exc = sys.exc_info()[1] if 'failure' not in entry.attrib: entry.set('failure', 'bind error: %s' % format_exc()) - logger.error("Failed to bind entry %s:%s: %s" % - (entry.tag, entry.get('name'), exc)) + self.logger.error("Failed to bind entry %s:%s: %s" % + (entry.tag, entry.get('name'), exc)) except Exception: exc = sys.exc_info()[1] if 'failure' not in entry.attrib: entry.set('failure', 'bind error: %s' % format_exc()) - logger.error("Unexpected failure in BindStructure: %s %s" \ - % (entry.tag, entry.get('name')), exc_info=1) + self.logger.error("Unexpected failure in BindStructure: %s %s" % + (entry.tag, entry.get('name')), exc_info=1) def Bind(self, entry, metadata): """Bind an entry using the appropriate generator.""" @@ -275,11 +334,11 @@ class Core(Component): return ret except: entry.set('name', oldname) - logger.error("Failed binding entry %s:%s with altsrc %s" \ - % (entry.tag, entry.get('name'), - entry.get('altsrc'))) - logger.error("Falling back to %s:%s" % (entry.tag, - entry.get('name'))) + self.logger.error("Failed binding entry %s:%s with altsrc %s" % + (entry.tag, entry.get('name'), + entry.get('altsrc'))) + self.logger.error("Falling back to %s:%s" % (entry.tag, + entry.get('name'))) glist = [gen for gen in self.generators if entry.get('name') in gen.Entries.get(entry.tag, {})] @@ -288,8 +347,8 @@ class Core(Component): metadata) elif len(glist) > 1: generators = ", ".join([gen.name for gen in glist]) - logger.error("%s %s served by multiple generators: %s" % \ - (entry.tag, entry.get('name'), generators)) + self.logger.error("%s %s served by multiple generators: %s" % + (entry.tag, entry.get('name'), generators)) g2list = [gen for gen in self.generators if gen.HandlesEntry(entry, metadata)] if len(g2list) == 1: @@ -301,18 +360,21 @@ class Core(Component): def BuildConfiguration(self, client): """Build configuration for clients.""" start = time.time() - config = lxml.etree.Element("Configuration", version='2.0', \ + config = lxml.etree.Element("Configuration", version='2.0', revision=self.revision) try: meta = self.build_metadata(client) - except Bcfg2.Server.Plugins.Metadata.MetadataConsistencyError: - logger.error("Metadata consistency error for client %s" % client) + except Bcfg2.Server.Plugin.MetadataConsistencyError: + self.logger.error("Metadata consistency error for client %s" % + client) return lxml.etree.Element("error", type='metadata error') + self.client_run_hook("start_client_run", meta) + try: structures = self.GetStructures(meta) except: - logger.error("error in GetStructures", exc_info=1) + self.logger.error("error in GetStructures", exc_info=1) return lxml.etree.Element("error", type='structure error') self.validate_structures(meta, structures) @@ -324,7 +386,8 @@ class Core(Component): key = (entry.tag, entry.get('name')) if key in esrcs: if esrcs[key] != entry.get('altsrc'): - logger.error("Found inconsistent altsrc mapping for entry %s:%s" % key) + self.logger.error("Found inconsistent altsrc mapping " + "for entry %s:%s" % key) else: esrcs[key] = entry.get('altsrc', None) del esrcs @@ -334,15 +397,49 @@ class Core(Component): self.BindStructure(astruct, meta) config.append(astruct) except: - logger.error("error in BindStructure", exc_info=1) + self.logger.error("error in BindStructure", exc_info=1) self.validate_goals(meta, config) + self.client_run_hook("end_client_run", meta) + sort_xml(config, key=lambda e: e.get('name')) - logger.info("Generated config for %s in %.03f seconds" % \ - (client, time.time() - start)) + self.logger.info("Generated config for %s in %.03f seconds" % + (client, time.time() - start)) return config + def run(self, **kwargs): + """ run the server core """ + raise NotImplementedError + + def _daemonize(self): + child_pid = os.fork() + if child_pid != 0: + return + + os.setsid() + + child_pid = os.fork() + if child_pid != 0: + os._exit(0) + + redirect_file = open("/dev/null", "w+") + os.dup2(redirect_file.fileno(), sys.__stdin__.fileno()) + os.dup2(redirect_file.fileno(), sys.__stdout__.fileno()) + os.dup2(redirect_file.fileno(), sys.__stderr__.fileno()) + + os.chdir(os.sep) + + pidfile = open(self.setup['daemon'] or "/dev/null", "w") + pidfile.write("%s\n" % os.getpid()) + pidfile.close() + + return os.getpid() + + def critical_error(self, operation): + """ this should be overridden by child classes """ + self.logger.fatal(operation, exc_info=1) + def GetDecisions(self, metadata, mode): """Get data for the decision list.""" result = [] @@ -350,15 +447,15 @@ class Core(Component): try: result += plugin.GetDecisions(metadata, mode) except: - logger.error("Plugin: %s failed to generate decision list" \ - % plugin.name, exc_info=1) + self.logger.error("Plugin: %s failed to generate decision list" + % plugin.name, exc_info=1) return result def build_metadata(self, client_name): """Build the metadata structure.""" if not hasattr(self, 'metadata'): # some threads start before metadata is even loaded - raise Bcfg2.Server.Plugins.Metadata.MetadataRuntimeError + raise Bcfg2.Server.Plugin.MetadataRuntimeError imd = self.metadata.get_initial_metadata(client_name) for conn in self.connectors: grps = conn.get_additional_groups(imd) @@ -378,102 +475,147 @@ class Core(Component): try: plugin.process_statistics(meta, statistics) except: - logger.error("Plugin %s failed to process stats from %s" \ - % (plugin.name, meta.hostname), - exc_info=1) + self.logger.error("Plugin %s failed to process stats from " + "%s" % (plugin.name, meta.hostname), + exc_info=1) + + self.logger.info("Client %s reported state %s" % (client_name, + state.get('state'))) + self.client_run_hook("end_statistics", meta) + + def resolve_client(self, address, cleanup_cache=False, metadata=True): + try: + client = self.metadata.resolve_client(address, + cleanup_cache=cleanup_cache) + if metadata: + meta = self.build_metadata(client) + else: + meta = None + except Bcfg2.Server.Plugin.MetadataConsistencyError: + err = sys.exc_info()[1] + self.critical_error("Client metadata resolution error for %s: %s" % + (address[0], err)) + except Bcfg2.Server.Plugin.MetadataRuntimeError: + err = sys.exc_info()[1] + self.critical_error('Metadata system runtime failure for %s: %s' % + (address[0], err)) + return (client, meta) + + def critical_error(self, operation): + """Log and err, traceback and return an xmlrpc fault to client.""" + self.logger.error(operation, exc_info=1) + raise xmlrpclib.Fault(xmlrpclib.APPLICATION_ERROR, + "Critical failure: %s" % operation) + + def _get_rmi(self): + rmi = dict() + if self.plugins: + for pname, pinst in list(self.plugins.items()): + for mname in pinst.__rmi__: + rmi["%s.%s" % (pname, mname)] = getattr(pinst, mname) + return rmi - logger.info("Client %s reported state %s" % (client_name, - state.get('state'))) # XMLRPC handlers start here + @exposed + def listMethods(self, address): + methods = [name + for name, func in inspect.getmembers(self, callable) + if getattr(func, "exposed", False)] + methods.extend(self._get_rmi().keys()) + return methods + + @exposed + def methodHelp(self, address, method_name): + raise NotImplementedError + + @exposed + def DeclareVersion(self, address, version): + """ declare the client version """ + client, metadata = self.resolve_client(address) + try: + self.metadata.set_version(client, version) + except (Bcfg2.Server.Plugin.MetadataConsistencyError, + Bcfg2.Server.Plugin.MetadataRuntimeError): + err = sys.exc_info()[1] + self.critical_error("Unable to set version for %s: %s" % + (client, err)) + return True @exposed def GetProbes(self, address): """Fetch probes for a particular client.""" resp = lxml.etree.Element('probes') + client, metadata = self.resolve_client(address, cleanup_cache=True) try: - name = self.metadata.resolve_client(address, cleanup_cache=True) - meta = self.build_metadata(name) - for plugin in self.plugins_by_type(Bcfg2.Server.Plugin.Probing): - for probe in plugin.GetProbes(meta): + for probe in plugin.GetProbes(metadata): resp.append(probe) - return lxml.etree.tostring(resp, encoding='UTF-8', - xml_declaration=True) - except Bcfg2.Server.Plugins.Metadata.MetadataConsistencyError: - warning = 'Client metadata resolution error for %s' % address[0] - self.logger.warning(warning) - raise xmlrpclib.Fault(6, warning + "; check server log") - except Bcfg2.Server.Plugins.Metadata.MetadataRuntimeError: - err_msg = 'Metadata system runtime failure' - self.logger.error(err_msg) - raise xmlrpclib.Fault(6, err_msg) + return lxml.etree.tostring(resp, + xml_declaration=False).decode('UTF-8') except: - critical_error("Error determining client probes") + err = sys.exc_info()[1] + self.critical_error("Error determining probes for %s: %s" % + (client, err)) @exposed def RecvProbeData(self, address, probedata): """Receive probe data from clients.""" + client, metadata = self.resolve_client(address) try: - name = self.metadata.resolve_client(address) - meta = self.build_metadata(name) - except Bcfg2.Server.Plugins.Metadata.MetadataConsistencyError: - warning = 'Metadata consistency error' - self.logger.warning(warning) - raise xmlrpclib.Fault(6, warning) - # clear dynamic groups - self.metadata.cgroups[meta.hostname] = [] - try: - xpdata = lxml.etree.XML(probedata.encode('utf-8')) + xpdata = lxml.etree.XML(probedata.encode('utf-8'), + parser=Bcfg2.Server.XMLParser) except: - self.logger.error("Failed to parse probe data from client %s" % \ - (address[0])) - return False + err = sys.exc_info()[1] + self.critical_error("Failed to parse probe data from client %s: %s" + % (client, err)) sources = [] [sources.append(data.get('source')) for data in xpdata if data.get('source') not in sources] for source in sources: if source not in self.plugins: - self.logger.warning("Failed to locate plugin %s" % (source)) + self.logger.warning("Failed to locate plugin %s" % source) continue dl = [data for data in xpdata if data.get('source') == source] try: - self.plugins[source].ReceiveData(meta, dl) + self.plugins[source].ReceiveData(metadata, dl) except: - logger.error("Failed to process probe data from client %s" % \ - (address[0]), exc_info=1) + err = sys.exc_info()[1] + self.critical_error("Failed to process probe data from client " + "%s: %s" % + (client, err)) return True @exposed def AssertProfile(self, address, profile): """Set profile for a client.""" + client = self.resolve_client(address, metadata=False)[0] try: - client = self.metadata.resolve_client(address) self.metadata.set_profile(client, profile, address) - except (Bcfg2.Server.Plugins.Metadata.MetadataConsistencyError, - Bcfg2.Server.Plugins.Metadata.MetadataRuntimeError): - warning = 'Metadata consistency error' - self.logger.warning(warning) - raise xmlrpclib.Fault(6, warning) + except (Bcfg2.Server.Plugin.MetadataConsistencyError, + Bcfg2.Server.Plugin.MetadataRuntimeError): + err = sys.exc_info()[1] + self.critical_error("Unable to assert profile for %s: %s" % + (client, err)) return True @exposed def GetConfig(self, address, checksum=False): """Build config for a client.""" + client = self.resolve_client(address)[0] try: - client = self.metadata.resolve_client(address) config = self.BuildConfiguration(client) - return lxml.etree.tostring(config, encoding='UTF-8', - xml_declaration=True) - except Bcfg2.Server.Plugins.Metadata.MetadataConsistencyError: - self.logger.warning("Metadata consistency failure for %s" % (address)) - raise xmlrpclib.Fault(6, "Metadata consistency failure") + return lxml.etree.tostring(config, + xml_declaration=False).decode('UTF-8') + except Bcfg2.Server.Plugin.MetadataConsistencyError: + self.critical_error("Metadata consistency failure for %s" % client) @exposed def RecvStats(self, address, stats): """Act on statistics upload.""" - sdata = lxml.etree.XML(stats.encode('utf-8')) - client = self.metadata.resolve_client(address) + client = self.resolve_client(address)[0] + sdata = lxml.etree.XML(stats.encode('utf-8'), + parser=Bcfg2.Server.XMLParser) self.process_statistics(client, sdata) return "<ok/>" @@ -483,11 +625,17 @@ class Core(Component): else: # No ca, so no cert validation can be done acert = None - return self.metadata.AuthenticateConnection(acert, user, password, address) + return self.metadata.AuthenticateConnection(acert, user, password, + address) @exposed def GetDecisionList(self, address, mode): """Get the data of the decision list.""" - client = self.metadata.resolve_client(address) - meta = self.build_metadata(client) - return self.GetDecisions(meta, mode) + client, metadata = self.resolve_client(address) + return self.GetDecisions(metadata, mode) + + @property + def database_available(self): + """Is the database configured and available""" + return self._database_available + |