summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/lib/Bcfg2/Server/Admin.py1164
-rw-r--r--src/lib/Bcfg2/Server/Admin/Backup.py22
-rw-r--r--src/lib/Bcfg2/Server/Admin/Client.py32
-rw-r--r--src/lib/Bcfg2/Server/Admin/Compare.py147
-rw-r--r--src/lib/Bcfg2/Server/Admin/Init.py350
-rw-r--r--src/lib/Bcfg2/Server/Admin/Minestruct.py56
-rw-r--r--src/lib/Bcfg2/Server/Admin/Perf.py38
-rw-r--r--src/lib/Bcfg2/Server/Admin/Pull.py147
-rw-r--r--src/lib/Bcfg2/Server/Admin/Reports.py262
-rw-r--r--src/lib/Bcfg2/Server/Admin/Syncdb.py31
-rw-r--r--src/lib/Bcfg2/Server/Admin/Viz.py104
-rw-r--r--src/lib/Bcfg2/Server/Admin/Xcmd.py54
-rw-r--r--src/lib/Bcfg2/Server/Admin/__init__.py142
-rwxr-xr-xsrc/sbin/bcfg2-admin90
14 files changed, 1166 insertions, 1473 deletions
diff --git a/src/lib/Bcfg2/Server/Admin.py b/src/lib/Bcfg2/Server/Admin.py
new file mode 100644
index 000000000..b88aa837f
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Admin.py
@@ -0,0 +1,1164 @@
+""" Subcommands and helpers for bcfg2-admin """
+
+import re
+import os
+import sys
+import time
+import glob
+import stat
+import random
+import socket
+import string
+import getpass
+import difflib
+import tarfile
+import argparse
+import lxml.etree
+import Bcfg2.Logger
+import Bcfg2.Options
+import Bcfg2.Server.Core
+import Bcfg2.Client.Proxy
+from Bcfg2.Server.Plugin import PullSource, Generator, MetadataConsistencyError
+from Bcfg2.Utils import hostnames2ranges, Executor, safe_input
+from Bcfg2.Compat import xmlrpclib
+import Bcfg2.Server.Plugins.Metadata
+
+try:
+ import Bcfg2.settings
+ os.environ['DJANGO_SETTINGS_MODULE'] = 'Bcfg2.settings'
+ from django.core.exceptions import ImproperlyConfigured
+ from django.core import management
+ import Bcfg2.Server.models
+
+ HAS_DJANGO = True
+ try:
+ import south # pylint: disable=W0611
+ HAS_REPORTS = True
+ except ImportError:
+ HAS_REPORTS = False
+except ImportError:
+ HAS_DJANGO = False
+ HAS_REPORTS = False
+
+
+class ccolors:
+ # pylint: disable=W1401
+ ADDED = '\033[92m'
+ CHANGED = '\033[93m'
+ REMOVED = '\033[91m'
+ ENDC = '\033[0m'
+ # pylint: enable=W1401
+
+ @staticmethod
+ def disable(cls):
+ cls.ADDED = ''
+ cls.CHANGED = ''
+ cls.REMOVED = ''
+ cls.ENDC = ''
+
+
+def gen_password(length):
+ """Generates a random alphanumeric password with length characters."""
+ chars = string.letters + string.digits
+ return "".join(random.choice(chars) for i in range(length))
+
+
+def print_table(rows, justify='left', hdr=True, vdelim=" ", padding=1):
+ """Pretty print a table
+
+ rows - list of rows ([[row 1], [row 2], ..., [row n]])
+ hdr - if True the first row is treated as a table header
+ vdelim - vertical delimiter between columns
+ padding - # of spaces around the longest element in the column
+ justify - may be left,center,right
+
+ """
+ hdelim = "="
+ justify = {'left': str.ljust,
+ 'center': str.center,
+ 'right': str.rjust}[justify.lower()]
+
+ # Calculate column widths (longest item in each column
+ # plus padding on both sides)
+ cols = list(zip(*rows))
+ col_widths = [max([len(str(item)) + 2 * padding
+ for item in col]) for col in cols]
+ borderline = vdelim.join([w * hdelim for w in col_widths])
+
+ # Print out the table
+ print(borderline)
+ for row in rows:
+ print(vdelim.join([justify(str(item), width)
+ for (item, width) in zip(row, col_widths)]))
+ if hdr:
+ print(borderline)
+ hdr = False
+
+
+class AdminCmd(Bcfg2.Options.Subcommand):
+ def setup(self):
+ """ Perform post-init (post-options parsing), pre-run setup
+ tasks """
+ pass
+
+ def errExit(self, emsg):
+ """ exit with an error """
+ print(emsg)
+ raise SystemExit(1)
+
+
+class _ServerAdminCmd(AdminCmd):
+ """Base class for admin modes that run a Bcfg2 server."""
+ __plugin_whitelist__ = None
+ __plugin_blacklist__ = None
+
+ options = AdminCmd.options + Bcfg2.Server.Core.Core.options
+
+ def setup(self):
+ if self.__plugin_whitelist__ is not None:
+ Bcfg2.Options.setup.plugins = [
+ p for p in Bcfg2.Options.setup.plugins
+ if p.name in self.__plugin_whitelist__]
+ elif self.__plugin_blacklist__ is not None:
+ Bcfg2.Options.setup.plugins = [
+ p for p in Bcfg2.Options.setup.plugins
+ if p.name not in self.__plugin_blacklist__]
+
+ try:
+ self.core = Bcfg2.Server.Core.Core()
+ except Bcfg2.Server.Core.CoreInitError:
+ msg = sys.exc_info()[1]
+ self.errExit("Core load failed: %s" % msg)
+ self.core.load_plugins()
+ self.core.fam.handle_event_set()
+ self.metadata = self.core.metadata
+
+ def shutdown(self):
+ self.core.shutdown()
+
+
+class _ProxyAdminCmd(AdminCmd):
+ """ Base class for admin modes that proxy to a running Bcfg2 server """
+
+ options = AdminCmd.options + Bcfg2.Client.Proxy.ComponentProxy.options
+
+ def __init__(self):
+ AdminCmd.__init__(self)
+ self.proxy = None
+
+ def setup(self):
+ self.proxy = Bcfg2.Client.Proxy.ComponentProxy()
+
+
+class Backup(AdminCmd):
+ """ Make a backup of the Bcfg2 repository """
+
+ options = AdminCmd.options + [Bcfg2.Options.Common.repository]
+
+ def run(self, setup):
+ timestamp = time.strftime('%Y%m%d%H%M%S')
+ datastore = setup.repository
+ fmt = 'gz'
+ mode = 'w:' + fmt
+ filename = timestamp + '.tar' + '.' + fmt
+ out = tarfile.open(os.path.join(datastore, filename), mode=mode)
+ out.add(datastore, os.path.basename(datastore))
+ out.close()
+ print("Archive %s was stored under %s" % (filename, datastore))
+
+
+class Client(_ServerAdminCmd):
+ """ Create, delete, or list client entries """
+
+ options = _ServerAdminCmd.options + [
+ Bcfg2.Options.PositionalArgument(
+ "mode",
+ choices=["add", "del", "list"]),
+ Bcfg2.Options.PositionalArgument("hostname", nargs='?')]
+
+ __plugin_whitelist__ = ["Metadata"]
+
+ def run(self, setup):
+ if setup.mode != 'list' and not setup.hostname:
+ self.parser.error("<hostname> is required in %s mode" % setup.mode)
+ elif setup.mode == 'list' and setup.hostname:
+ self.logger.warning("<hostname> is not honored in list mode")
+
+ if setup.mode == 'add':
+ try:
+ self.metadata.add_client(setup.hostname)
+ except MetadataConsistencyError:
+ err = sys.exc_info()[1]
+ self.errExit("Error adding client %s: %s" % (setup.hostname,
+ err))
+ elif setup.mode == 'del':
+ try:
+ self.metadata.remove_client(setup.hostname)
+ except MetadataConsistencyError:
+ err = sys.exc_info()[1]
+ self.errExit("Error deleting client %s: %s" % (setup.hostname,
+ err))
+ elif setup.mode == 'list':
+ for client in self.metadata.list_clients():
+ print(client)
+
+
+class Compare(AdminCmd):
+ """ Compare two hosts or two versions of a host specification """
+
+ help = "Given two XML files (as produced by bcfg2-info build or bcfg2 " + \
+ "-qnc) or two directories containing XML files (as produced by " + \
+ "bcfg2-info buildall or bcfg2-info builddir), output a detailed, " + \
+ "Bcfg2-centric diff."
+
+ options = AdminCmd.options + [
+ Bcfg2.Options.Option(
+ "-d", "--diff-lines", type=int,
+ help="Show only N lines of a diff"),
+ Bcfg2.Options.BooleanOption(
+ "-c", "--color", help="Use colors even if not run from a TTY"),
+ Bcfg2.Options.BooleanOption(
+ "-q", "--quiet",
+ help="Only show that entries differ, not how they differ"),
+ Bcfg2.Options.PathOption("path1", metavar="<file-or-dir>"),
+ Bcfg2.Options.PathOption("path2", metavar="<file-or-dir>")]
+
+ changes = dict()
+
+ def removed(self, msg, host):
+ self.record("%sRemoved: %s%s" % (ccolors.REMOVED, msg, ccolors.ENDC),
+ host)
+
+ def added(self, msg, host):
+ self.record("%sAdded: %s%s" % (ccolors.ADDED, msg, ccolors.ENDC), host)
+
+ def changed(self, msg, host):
+ self.record("%sChanged: %s%s" % (ccolors.CHANGED, msg, ccolors.ENDC),
+ host)
+
+ def record(self, msg, host):
+ if msg not in self.changes:
+ self.changes[msg] = [host]
+ else:
+ self.changes[msg].append(host)
+
+ def udiff(self, l1, l2, **kwargs):
+ """ get a unified diff with control lines stripped """
+ lines = None
+ if "lines" in kwargs:
+ if kwargs['lines'] is not None:
+ lines = int(kwargs['lines'])
+ del kwargs['lines']
+ if lines == 0:
+ return []
+ kwargs['n'] = 0
+ diff = []
+ for line in difflib.unified_diff(l1, l2, **kwargs):
+ if (line.startswith("--- ") or line.startswith("+++ ") or
+ line.startswith("@@ ")):
+ continue
+ if lines is not None and len(diff) > lines:
+ diff.append(" ...")
+ break
+ if line.startswith("+"):
+ for l in line.splitlines():
+ diff.append(" %s%s%s" % (ccolors.ADDED, l, ccolors.ENDC))
+ elif line.startswith("-"):
+ for l in line.splitlines():
+ diff.append(" %s%s%s" % (ccolors.REMOVED, l,
+ ccolors.ENDC))
+ return diff
+
+ def _bundletype(self, el):
+ if el.get("tag") == "Independent":
+ return "Independent bundle"
+ else:
+ return "Bundle"
+
+ def run(self, setup):
+ if not sys.stdout.isatty() and not setup.color:
+ ccolors.disable(ccolors)
+
+ files = []
+ if os.path.isdir(setup.path1) and os.path.isdir(setup.path1):
+ for fpath in glob.glob(os.path.join(setup.path1, '*')):
+ fname = os.path.basename(fpath)
+ if os.path.exists(os.path.join(setup.path2, fname)):
+ files.append((os.path.join(setup.path1, fname),
+ os.path.join(setup.path2, fname)))
+ else:
+ if fname.endswith(".xml"):
+ host = fname[0:-4]
+ else:
+ host = fname
+ self.removed(host, '')
+ for fpath in glob.glob(os.path.join(setup.path2, '*')):
+ fname = os.path.basename(fpath)
+ if not os.path.exists(os.path.join(setup.path1, fname)):
+ if fname.endswith(".xml"):
+ host = fname[0:-4]
+ else:
+ host = fname
+ self.added(host, '')
+ elif os.path.isfile(setup.path1) and os.path.isfile(setup.path2):
+ files.append((setup.path1, setup.path2))
+ else:
+ self.errExit("Cannot diff a file and a directory")
+
+ for file1, file2 in files:
+ host = None
+ if os.path.basename(file1) == os.path.basename(file2):
+ fname = os.path.basename(file1)
+ if fname.endswith(".xml"):
+ host = fname[0:-4]
+ else:
+ host = fname
+
+ xdata1 = lxml.etree.parse(file1).getroot()
+ xdata2 = lxml.etree.parse(file2).getroot()
+
+ elements1 = dict()
+ elements2 = dict()
+ bundles1 = [el.get("name") for el in xdata1.iterchildren()]
+ bundles2 = [el.get("name") for el in xdata2.iterchildren()]
+ for el in xdata1.iterchildren():
+ if el.get("name") not in bundles2:
+ self.removed("%s %s" % (self._bundletype(el),
+ el.get("name")),
+ host)
+ for el in xdata2.iterchildren():
+ if el.get("name") not in bundles1:
+ self.added("%s %s" % (self._bundletype(el),
+ el.get("name")),
+ host)
+
+ for bname in bundles1:
+ bundle = xdata1.find("*[@name='%s']" % bname)
+ for el in bundle.getchildren():
+ elements1["%s:%s" % (el.tag, el.get("name"))] = el
+ for bname in bundles2:
+ bundle = xdata2.find("*[@name='%s']" % bname)
+ for el in bundle.getchildren():
+ elements2["%s:%s" % (el.tag, el.get("name"))] = el
+
+ for el in elements1.values():
+ elid = "%s:%s" % (el.tag, el.get("name"))
+ if elid not in elements2:
+ self.removed("Element %s" % elid, host)
+ else:
+ el2 = elements2[elid]
+ if (el.getparent().get("name") !=
+ el2.getparent().get("name")):
+ self.changed(
+ "Element %s was in bundle %s, "
+ "now in bundle %s" % (elid,
+ el.getparent().get("name"),
+ el2.getparent().get("name")),
+ host)
+ attr1 = sorted(["%s=\"%s\"" % (attr, el.get(attr))
+ for attr in el.attrib])
+ attr2 = sorted(["%s=\"%s\"" % (attr, el.get(attr))
+ for attr in el2.attrib])
+ if attr1 != attr2:
+ err = ["Element %s has different attributes" % elid]
+ if not setup.quiet:
+ err.extend(self.udiff(attr1, attr2))
+ self.changed("\n".join(err), host)
+
+ if el.text != el2.text:
+ if el.text is None:
+ self.changed("Element %s content was added" % elid,
+ host)
+ elif el2.text is None:
+ self.changed("Element %s content was removed" %
+ elid, host)
+ else:
+ err = ["Element %s has different content" %
+ elid]
+ if not setup.quiet:
+ err.extend(
+ self.udiff(el.text.splitlines(),
+ el2.text.splitlines(),
+ lines=setup.diff_lines))
+ self.changed("\n".join(err), host)
+
+ for el in elements2.values():
+ elid = "%s:%s" % (el.tag, el.get("name"))
+ if elid not in elements2:
+ self.removed("Element %s" % elid, host)
+
+ for change, hosts in self.changes.items():
+ hlist = [h for h in hosts if h is not None]
+ if len(files) > 1 and len(hlist):
+ print("===== %s =====" %
+ "\n ".join(hostnames2ranges(hlist)))
+ print(change)
+ if len(files) > 1 and len(hlist):
+ print("")
+
+
+class Help(AdminCmd, Bcfg2.Options.HelpCommand):
+ """ Get help on a specific subcommand """
+ def command_registry(self):
+ return CLI.commands
+
+
+class Init(AdminCmd):
+ """Interactively initialize a new repository."""
+
+ options = AdminCmd.options + [
+ Bcfg2.Options.Common.repository, Bcfg2.Options.Common.plugins]
+
+ # default config file
+ config = '''[server]
+repository = %s
+plugins = %s
+
+[database]
+#engine = sqlite3
+# 'postgresql', 'mysql', 'mysql_old', 'sqlite3' or 'ado_mssql'.
+#name =
+# Or path to database file if using sqlite3.
+#<repository>/bcfg2.sqlite is default path if left empty
+#user =
+# Not used with sqlite3.
+#password =
+# Not used with sqlite3.
+#host =
+# Not used with sqlite3.
+#port =
+
+[reporting]
+transport = LocalFilesystem
+
+[communication]
+password = %s
+certificate = %s
+key = %s
+ca = %s
+
+[components]
+bcfg2 = %s
+'''
+
+ # Default groups
+ groups = '''<Groups>
+ <Group profile='true' public='true' default='true' name='basic'/>
+</Groups>
+'''
+
+ # Default contents of clients.xml
+ clients = '''<Clients>
+ <Client profile="basic" name="%s"/>
+</Clients>
+'''
+
+ def __init__(self):
+ AdminCmd.__init__(self)
+ self.data = dict()
+
+ def _set_defaults(self, setup):
+ """Set default parameters."""
+ self.data['plugins'] = setup.plugins
+ self.data['configfile'] = setup.config
+ self.data['repopath'] = setup.repository
+ self.data['password'] = gen_password(8)
+ self.data['shostname'] = socket.getfqdn()
+ self.data['server_uri'] = "https://%s:6789" % self.data['shostname']
+ self.data['country'] = 'US'
+ self.data['state'] = 'Illinois'
+ self.data['location'] = 'Argonne'
+ if os.path.exists("/etc/pki/tls"):
+ self.data['keypath'] = "/etc/pki/tls/private/bcfg2.key"
+ self.data['certpath'] = "/etc/pki/tls/certs/bcfg2.crt"
+ elif os.path.exists("/etc/ssl"):
+ self.data['keypath'] = "/etc/ssl/bcfg2.key"
+ self.data['certpath'] = "/etc/ssl/bcfg2.crt"
+ else:
+ basepath = os.path.dirname(self.data['configfile'])
+ self.data['keypath'] = os.path.join(basepath, "bcfg2.key")
+ self.data['certpath'] = os.path.join(basepath, 'bcfg2.crt')
+
+ def input_with_default(self, msg, default_name):
+ val = safe_input("%s [%s]: " % (msg, self.data[default_name]))
+ if val:
+ self.data[default_name] = val
+
+ def run(self, setup):
+ self._set_defaults(setup)
+
+ # Prompt the user for input
+ self._prompt_server()
+ self._prompt_config()
+ self._prompt_repopath()
+ self._prompt_password()
+ self._prompt_keypath()
+ self._prompt_certificate()
+
+ # Initialize the repository
+ self.init_repo()
+
+ def _prompt_server(self):
+ """Ask for the server name and URI."""
+ self.input_with_default("What is the server's hostname", 'shostname')
+ # reset default server URI
+ self.data['server_uri'] = "https://%s:6789" % self.data['shostname']
+ self.input_with_default("Server location", 'server_uri')
+
+ def _prompt_config(self):
+ """Ask for the configuration file path."""
+ self.input_with_default("Path to Bcfg2 configuration", 'configfile')
+
+ def _prompt_repopath(self):
+ """Ask for the repository path."""
+ while True:
+ self.input_with_default("Location of Bcfg2 repository", 'repopath')
+ if os.path.isdir(self.data['repopath']):
+ response = safe_input("Directory %s exists. Overwrite? [y/N]:"
+ % self.data['repopath'])
+ if response.lower().strip() == 'y':
+ break
+ else:
+ break
+
+ def _prompt_password(self):
+ """Ask for a password or generate one if none is provided."""
+ newpassword = getpass.getpass(
+ "Input password used for communication verification "
+ "(without echoing; leave blank for random): ").strip()
+ if len(newpassword) != 0:
+ self.data['password'] = newpassword
+
+ def _prompt_certificate(self):
+ """Ask for the key details (country, state, and location)."""
+ print("The following questions affect SSL certificate generation.")
+ print("If no data is provided, the default values are used.")
+ self.input_with_default("Country code for certificate", 'country')
+ self.input_with_default("State or Province Name (full name) for "
+ "certificate", 'state')
+ self.input_with_default("Locality Name (e.g., city) for certificate",
+ 'location')
+
+ def _prompt_keypath(self):
+ """ Ask for the key pair location. Try to use sensible
+ defaults depending on the OS """
+ self.input_with_default("Path where Bcfg2 server private key will be "
+ "created", 'keypath')
+ self.input_with_default("Path where Bcfg2 server cert will be created",
+ 'certpath')
+
+ def _init_plugins(self):
+ """Initialize each plugin-specific portion of the repository."""
+ for plugin in self.data['plugins']:
+ kwargs = dict()
+ if issubclass(plugin, Bcfg2.Server.Plugins.Metadata.Metadata):
+ kwargs.update(
+ dict(groups_xml=self.groups,
+ clients_xml=self.clients % self.data['shostname']))
+ plugin.init_repo(self.data['repopath'], **kwargs)
+
+ def create_conf(self):
+ """ create the config file """
+ confdata = self.config % (
+ self.data['repopath'],
+ ','.join(p.__name__ for p in self.data['plugins']),
+ self.data['password'],
+ self.data['certpath'],
+ self.data['keypath'],
+ self.data['certpath'],
+ self.data['server_uri'])
+
+ # Don't overwrite existing bcfg2.conf file
+ if os.path.exists(self.data['configfile']):
+ result = safe_input("\nWarning: %s already exists. "
+ "Overwrite? [y/N]: " % self.data['configfile'])
+ if result not in ['Y', 'y']:
+ print("Leaving %s unchanged" % self.data['configfile'])
+ return
+ try:
+ open(self.data['configfile'], "w").write(confdata)
+ os.chmod(self.data['configfile'],
+ stat.S_IRUSR | stat.S_IWUSR) # 0600
+ except: # pylint: disable=W0702
+ self.errExit("Error trying to write configuration file '%s': %s" %
+ (self.data['configfile'], sys.exc_info()[1]))
+
+ def init_repo(self):
+ """Setup a new repo and create the content of the
+ configuration file."""
+ # Create the repository
+ path = os.path.join(self.data['repopath'], 'etc')
+ try:
+ os.makedirs(path)
+ self._init_plugins()
+ print("Repository created successfuly in %s" %
+ self.data['repopath'])
+ except OSError:
+ print("Failed to create %s." % path)
+
+ # Create the configuration file and SSL key
+ self.create_conf()
+ self.create_key()
+
+ def create_key(self):
+ """Creates a bcfg2.key at the directory specifed by keypath."""
+ cmd = Executor(timeout=120)
+ subject = "/C=%s/ST=%s/L=%s/CN=%s'" % (
+ self.data['country'], self.data['state'], self.data['location'],
+ self.data['shostname'])
+ key = cmd.run(["openssl", "req", "-batch", "-x509", "-nodes",
+ "-subj", subject, "-days", "1000",
+ "-newkey", "rsa:2048",
+ "-keyout", self.data['keypath'], "-noout"])
+ if not key.success:
+ print("Error generating key: %s" % key.error)
+ return
+ os.chmod(self.data['keypath'], stat.S_IRUSR | stat.S_IWUSR) # 0600
+ csr = cmd.run(["openssl", "req", "-batch", "-new", "-subj", subject,
+ "-key", self.data['keypath']])
+ if not csr.success:
+ print("Error generating certificate signing request: %s" %
+ csr.error)
+ return
+ cert = cmd.run(["openssl", "x509", "-req", "-days", "1000",
+ "-signkey", self.data['keypath'],
+ "-out", self.data['certpath']],
+ inputdata=csr.stdout)
+ if not cert.success:
+ print("Error signing certificate: %s" % cert.error)
+ return
+
+
+class Minestruct(_ServerAdminCmd):
+ """ Extract extra entry lists from statistics """
+
+ options = _ServerAdminCmd.options + [
+ Bcfg2.Options.PathOption(
+ "-f", "--outfile", type=argparse.FileType('w'), default=sys.stdout,
+ help="Write to the given file"),
+ Bcfg2.Options.Option(
+ "-g", "--groups", help="Only build config for groups",
+ type=Bcfg2.Options.Types.colon_list, default=[]),
+ Bcfg2.Options.PositionalArgument("hostname")]
+
+ def run(self, setup):
+ try:
+ extra = set()
+ for source in self.core.plugins_by_type(PullSource):
+ for item in source.GetExtra(setup.hostname):
+ extra.add(item)
+ except: # pylint: disable=W0702
+ self.errExit("Failed to find extra entry info for client %s: %s" %
+ (setup.hostname, sys.exc_info()[1]))
+ root = lxml.etree.Element("Base")
+ self.logger.info("Found %d extra entries" % len(extra))
+ add_point = root
+ for grp in setup.groups:
+ add_point = lxml.etree.SubElement(add_point, "Group", name=grp)
+ for tag, name in extra:
+ self.logger.info("%s: %s" % (tag, name))
+ lxml.etree.SubElement(add_point, tag, name=name)
+
+ lxml.etree.ElementTree(root).write(setup.outfile, pretty_print=True)
+
+
+class Perf(_ProxyAdminCmd):
+ """ Get performance data from server """
+
+ def run(self, setup):
+ output = [('Name', 'Min', 'Max', 'Mean', 'Count')]
+ data = self.proxy.get_statistics()
+ for key in sorted(data.keys()):
+ output.append(
+ (key, ) +
+ tuple(["%.06f" % item
+ for item in data[key][:-1]] + [data[key][-1]]))
+ print_table(output)
+
+
+class Pull(_ServerAdminCmd):
+ """ Retrieves entries from clients and integrates the information
+ into the repository """
+
+ options = _ServerAdminCmd.options + [
+ Bcfg2.Options.Common.interactive,
+ Bcfg2.Options.BooleanOption(
+ "-s", "--stdin",
+ help="Read lists of <hostname> <entrytype> <entryname> from stdin "
+ "instead of the command line"),
+ Bcfg2.Options.PositionalArgument("hostname", nargs='?'),
+ Bcfg2.Options.PositionalArgument("entrytype", nargs='?'),
+ Bcfg2.Options.PositionalArgument("entryname", nargs='?')]
+
+ def __init__(self):
+ _ServerAdminCmd.__init__(self)
+ self.interactive = False
+
+ def setup(self):
+ if (not Bcfg2.Options.setup.stdin and
+ not (Bcfg2.Options.setup.hostname and
+ Bcfg2.Options.setup.entrytype and
+ Bcfg2.Options.setup.entryname)):
+ print("You must specify either --stdin or a hostname, entry type, "
+ "and entry name on the command line.")
+ self.errExit(self.usage())
+ _ServerAdminCmd.setup(self)
+
+ def run(self, setup):
+ self.interactive = setup.interactive
+ if setup.stdin:
+ for line in sys.stdin:
+ try:
+ self.PullEntry(*line.split(None, 3))
+ except SystemExit:
+ print(" for %s" % line)
+ except:
+ print("Bad entry: %s" % line.strip())
+ else:
+ self.PullEntry(setup.hostname, setup.entrytype, setup.entryname)
+
+ def BuildNewEntry(self, client, etype, ename):
+ """Construct a new full entry for
+ given client/entry from statistics.
+ """
+ new_entry = {'type': etype, 'name': ename}
+ pull_sources = self.core.plugins_by_type(PullSource)
+ for plugin in pull_sources:
+ try:
+ (owner, group, mode, contents) = \
+ plugin.GetCurrentEntry(client, etype, ename)
+ break
+ except Bcfg2.Server.Plugin.PluginExecutionError:
+ if plugin == pull_sources[-1]:
+ self.errExit("Pull Source failure; could not fetch "
+ "current state")
+
+ try:
+ data = {'owner': owner,
+ 'group': group,
+ 'mode': mode,
+ 'text': contents}
+ except UnboundLocalError:
+ self.errExit("Unable to build entry")
+ for key, val in list(data.items()):
+ if val:
+ new_entry[key] = val
+ return new_entry
+
+ def Choose(self, choices):
+ """Determine where to put pull data."""
+ if self.interactive:
+ for choice in choices:
+ print("Plugin returned choice:")
+ if id(choice) == id(choices[0]):
+ print("(current entry) ")
+ if choice.all:
+ print(" => global entry")
+ elif choice.group:
+ print(" => group entry: %s (prio %d)" %
+ (choice.group, choice.prio))
+ else:
+ print(" => host entry: %s" % (choice.hostname))
+
+ # flush input buffer
+ ans = safe_input("Use this entry? [yN]: ") in ['y', 'Y']
+ if ans:
+ return choice
+ return False
+ else:
+ if not choices:
+ return False
+ return choices[0]
+
+ def PullEntry(self, client, etype, ename):
+ """Make currently recorded client state correct for entry."""
+ new_entry = self.BuildNewEntry(client, etype, ename)
+
+ meta = self.core.build_metadata(client)
+ # Find appropriate plugin in core
+ glist = [gen for gen in self.core.plugins_by_type(Generator)
+ if ename in gen.Entries.get(etype, {})]
+ if len(glist) != 1:
+ self.errExit("Got wrong numbers of matching generators for entry:"
+ "%s" % ([g.name for g in glist]))
+ plugin = glist[0]
+ if not isinstance(plugin, Bcfg2.Server.Plugin.PullTarget):
+ self.errExit("Configuration upload not supported by plugin %s" %
+ plugin.name)
+ try:
+ choices = plugin.AcceptChoices(new_entry, meta)
+ specific = self.Choose(choices)
+ if specific:
+ plugin.AcceptPullData(specific, new_entry, self.logger)
+ except Bcfg2.Server.Plugin.PluginExecutionError:
+ self.errExit("Configuration upload not supported by plugin %s" %
+ plugin.name)
+
+ # Commit if running under a VCS
+ for vcsplugin in list(self.core.plugins.values()):
+ if isinstance(vcsplugin, Bcfg2.Server.Plugin.Version):
+ files = "%s/%s" % (plugin.data, ename)
+ comment = 'file "%s" pulled from host %s' % (files, client)
+ vcsplugin.commit_data([files], comment)
+
+
+class _ReportsCmd(AdminCmd):
+ def __init__(self):
+ AdminCmd.__init__(self)
+ self.reports_entries = ()
+ self.reports_classes = ()
+
+ def setup(self):
+ # this has to be imported after options are parsed,
+ # because Django finalizes its settings as soon as it's
+ # loaded, which means that if we import this before
+ # Bcfg2.settings has been populated, Django gets a null
+ # configuration, and subsequent updates to Bcfg2.settings
+ # won't help.
+ import Bcfg2.Reporting.models
+ self.reports_entries = (Bcfg2.Reporting.models.Group,
+ Bcfg2.Reporting.models.Bundle,
+ Bcfg2.Reporting.models.FailureEntry,
+ Bcfg2.Reporting.models.ActionEntry,
+ Bcfg2.Reporting.models.PathEntry,
+ Bcfg2.Reporting.models.PackageEntry,
+ Bcfg2.Reporting.models.PathEntry,
+ Bcfg2.Reporting.models.ServiceEntry)
+ self.reports_classes = self.reports_entries + (
+ Bcfg2.Reporting.models.Client,
+ Bcfg2.Reporting.models.Interaction,
+ Bcfg2.Reporting.models.Performance)
+
+
+if HAS_REPORTS:
+ import datetime
+
+ class ScrubReports(_ReportsCmd):
+ """ Perform a thorough scrub and cleanup of the Reporting
+ database """
+
+ def setup(self):
+ _ReportsCmd.setup(self)
+ # this has to be imported after options are parsed,
+ # because Django finalizes its settings as soon as it's
+ # loaded, which means that if we import this before
+ # Bcfg2.settings has been populated, Django gets a null
+ # configuration, and subsequent updates to Bcfg2.settings
+ # won't help.
+ from django.db.transaction import commit_on_success
+ self.run = commit_on_success(self.run)
+
+ def run(self, _):
+ # Cleanup unused entries
+ for cls in self.reports_entries:
+ try:
+ start_count = cls.objects.count()
+ cls.prune_orphans()
+ self.logger.info("Pruned %d %s records" %
+ (start_count - cls.objects.count(),
+ cls.__name__))
+ except: # pylint: disable=W0702
+ print("Failed to prune %s: %s" %
+ (cls.__name__, sys.exc_info()[1]))
+
+ class InitReports(AdminCmd):
+ """ Initialize the Reporting database """
+ def run(self, setup):
+ verbose = setup.verbose + setup.debug
+ try:
+ management.call_command("syncdb", interactive=False,
+ verbosity=verbose)
+ management.call_command("migrate", interactive=False,
+ verbosity=verbose)
+ except: # pylint: disable=W0702
+ self.errExit("%s failed: %s" %
+ (self.__class__.__name__.title(),
+ sys.exc_info()[1]))
+
+
+ class UpdateReports(InitReports):
+ """ Apply updates to the reporting database """
+
+
+ class ReportsStats(_ReportsCmd):
+ """ Print Reporting database statistics """
+ def run(self, _):
+ for cls in self.reports_classes:
+ print("%s has %s records" % (cls.__name__,
+ cls.objects.count()))
+
+
+ class PurgeReports(_ReportsCmd):
+ """ Purge records from the Reporting database """
+
+ options = AdminCmd.options + [
+ Bcfg2.Options.Option("--client", help="Client to operate on"),
+ Bcfg2.Options.Option("--days", type=int, metavar='N',
+ help="Records older than N days"),
+ Bcfg2.Options.ExclusiveOptionGroup(
+ Bcfg2.Options.BooleanOption("--expired",
+ help="Expired clients only"),
+ Bcfg2.Options.Option("--state", help="Purge entries in state",
+ choices=['dirty', 'clean', 'modified']),
+ required=False)]
+
+ def run(self, setup):
+ if setup.days:
+ maxdate = datetime.datetime.now() - \
+ datetime.timedelta(days=setup.days)
+ else:
+ maxdate = None
+
+ starts = {}
+ for cls in self.reports_classes:
+ starts[cls] = cls.objects.count()
+ if setup.expired:
+ self.purge_expired(maxdate)
+ else:
+ self.purge(setup.client, maxdate, setup.state)
+ for cls in self.reports_classes:
+ self.logger.info("Purged %s %s records" %
+ (starts[cls] - cls.objects.count(),
+ cls.__name__))
+
+ def purge(self, client=None, maxdate=None, state=None):
+ '''Purge historical data from the database'''
+ # indicates whether or not a client should be deleted
+ filtered = False
+
+ if not client and not maxdate and not state:
+ self.errExit("Refusing to prune all data. Specify an option "
+ "to %s" % self.__class__.__name__.lower())
+
+ ipurge = Bcfg2.Reporting.models.Interaction.objects
+ if client:
+ try:
+ cobj = Bcfg2.Reporting.models.Client.objects.get(
+ name=client)
+ ipurge = ipurge.filter(client=cobj)
+ except Bcfg2.Reporting.models.Client.DoesNotExist:
+ self.errExit("Client %s not in database" % client)
+ self.logger.debug("Filtering by client: %s" % client)
+
+ if maxdate:
+ filtered = True
+ self.logger.debug("Filtering by maxdate: %s" % maxdate)
+ ipurge = ipurge.filter(timestamp__lt=maxdate)
+
+ if Bcfg2.settings.DATABASES['default']['ENGINE'] == \
+ 'django.db.backends.sqlite3':
+ grp_limit = 100
+ else:
+ grp_limit = 1000
+ if state:
+ filtered = True
+ self.logger.debug("Filtering by state: %s" % state)
+ ipurge = ipurge.filter(state=state)
+
+ count = ipurge.count()
+ rnum = 0
+ try:
+ while rnum < count:
+ grp = list(ipurge[:grp_limit].values("id"))
+ # just in case...
+ if not grp:
+ break
+ Bcfg2.Reporting.models.Interaction.objects.filter(
+ id__in=[x['id'] for x in grp]).delete()
+ rnum += len(grp)
+ self.logger.debug("Deleted %s of %s" % (rnum, count))
+ except: # pylint: disable=W0702
+ self.logger.error("Failed to remove interactions: %s" %
+ sys.exc_info()[1])
+
+ # Prune any orphaned ManyToMany relations
+ for m2m in self.reports_entries:
+ self.logger.debug("Pruning any orphaned %s objects" % \
+ m2m.__name__)
+ m2m.prune_orphans()
+
+ if client and not filtered:
+ # Delete the client, ping data is automatic
+ try:
+ self.logger.debug("Purging client %s" % client)
+ cobj.delete()
+ except: # pylint: disable=W0702
+ self.logger.error("Failed to delete client %s: %s" %
+ (client, sys.exc_info()[1]))
+
+ def purge_expired(self, maxdate=None):
+ """ Purge expired clients from the Reporting database """
+
+ if maxdate:
+ if not isinstance(maxdate, datetime.datetime):
+ raise TypeError("maxdate is not a DateTime object")
+ self.logger.debug("Filtering by maxdate: %s" % maxdate)
+ clients = Bcfg2.Reporting.models.Client.objects.filter(
+ expiration__lt=maxdate)
+ else:
+ clients = Bcfg2.Reporting.models.Client.objects.filter(
+ expiration__isnull=False)
+
+ for client in clients:
+ self.logger.debug("Purging client %s" % client)
+ Bcfg2.Reporting.models.Interaction.objects.filter(
+ client=client).delete()
+ client.delete()
+
+
+ class _DjangoProxyCmd(AdminCmd):
+ command = None
+ args = []
+ _reports_re = re.compile(r'^(?:Reports)?(?P<command>.*?)(?:Reports)?$')
+
+ def run(self, _):
+ '''Call a django command'''
+ if self.command is not None:
+ command = self.command
+ else:
+ match = self._reports_re.match(self.__class__.__name__)
+ if match:
+ command = match.group("command").lower()
+ else:
+ command = self.__class__.__name__.lower()
+ args = [command] + self.args
+ management.call_command(*args)
+
+
+ class ReportsDBShell(_DjangoProxyCmd):
+ """ Call the Django 'dbshell' command on the Reporting database """
+
+
+ class ReportsShell(_DjangoProxyCmd):
+ """ Call the Django 'shell' command on the Reporting database """
+
+
+ class ValidateReports(_DjangoProxyCmd):
+ """ Call the Django 'validate' command on the Reporting database """
+
+
+ class ReportsSQLAll(_DjangoProxyCmd):
+ """ Call the Django 'sqlall' command on the Reporting database """
+ args = ["Reporting"]
+
+
+if HAS_DJANGO:
+ class Syncdb(AdminCmd):
+ """ Sync the Django ORM with the configured database """
+
+ def run(self, setup):
+ management.setup_environ(Bcfg2.settings)
+ Bcfg2.Server.models.load_models()
+ try:
+ management.call_command("syncdb", interactive=False,
+ verbosity=setup.verbose + setup.debug)
+ except ImproperlyConfigured:
+ err = sys.exc_info()[1]
+ self.logger.error("Django configuration problem: %s" % err)
+ raise SystemExit(1)
+ except:
+ err = sys.exc_info()[1]
+ self.logger.error("Database update failed: %s" % err)
+ raise SystemExit(1)
+
+
+class Viz(_ServerAdminCmd):
+ """ Produce graphviz diagrams of metadata structures """
+
+ options = _ServerAdminCmd.options + [
+ Bcfg2.Options.BooleanOption(
+ "-H", "--includehosts",
+ help="Include hosts in the viz output"),
+ Bcfg2.Options.BooleanOption(
+ "-b", "--includebundles",
+ help="Include bundles in the viz output"),
+ Bcfg2.Options.BooleanOption(
+ "-k", "--includekey",
+ help="Show a key for different digraph shapes"),
+ Bcfg2.Options.Option(
+ "-c", "--only-client", metavar="<hostname>",
+ help="Show only the groups, bundles for the named client"),
+ Bcfg2.Options.PathOption(
+ "-o", "--outfile",
+ help="Write viz output to an output file")]
+
+ colors = ['steelblue1', 'chartreuse', 'gold', 'magenta',
+ 'indianred1', 'limegreen', 'orange1', 'lightblue2',
+ 'green1', 'blue1', 'yellow1', 'darkturquoise', 'gray66']
+
+ __plugin_blacklist__ = ['DBStats', 'Cfg', 'Pkgmgr', 'Packages', 'Rules',
+ 'Decisions', 'Deps', 'Git', 'Svn', 'Fossil', 'Bzr',
+ 'Bundler']
+
+ def run(self, setup):
+ if setup.outfile:
+ fmt = setup.outfile.split('.')[-1]
+ else:
+ fmt = 'png'
+
+ exc = Executor()
+ cmd = ["dot", "-T", fmt]
+ if setup.outfile:
+ cmd.extend(["-o", setup.outfile])
+ inputlist = ["digraph groups {",
+ '\trankdir="LR";',
+ self.metadata.viz(setup.includehosts, setup.includebundles,
+ setup.includekey, setup.only_client,
+ self.colors)]
+ if setup.includekey:
+ inputlist.extend(
+ ["\tsubgraph cluster_key {",
+ '\tstyle="filled";',
+ '\tcolor="lightblue";',
+ '\tBundle [ shape="septagon" ];',
+ '\tGroup [shape="ellipse"];',
+ '\tProfile [style="bold", shape="ellipse"];',
+ '\tHblock [label="Host1|Host2|Host3",shape="record"];',
+ '\tlabel="Key";',
+ "\t}"])
+ inputlist.append("}")
+ idata = "\n".join(inputlist)
+ try:
+ result = exc.run(cmd, inputdata=idata)
+ except OSError:
+ # on some systems (RHEL 6), you cannot run dot with
+ # shell=True. on others (Gentoo with Python 2.7), you
+ # must. In yet others (RHEL 5), either way works. I have
+ # no idea what the difference is, but it's kind of a PITA.
+ result = exc.run(cmd, shell=True, inputdata=idata)
+ if not result.success:
+ self.errExit("Error running %s: %s" % (cmd, result.error))
+ if not setup.outfile:
+ print(result.stdout)
+
+
+class Xcmd(_ProxyAdminCmd):
+ """ XML-RPC Command Interface """
+
+ options = _ProxyAdminCmd.options + [
+ Bcfg2.Options.PositionalArgument("command"),
+ Bcfg2.Options.PositionalArgument("arguments", nargs='*')]
+
+ def run(self, setup):
+ try:
+ data = getattr(self.proxy, setup.command)(*setup.arguments)
+ except Bcfg2.Client.Proxy.ProxyError:
+ self.errExit("Proxy Error: %s" % sys.exc_info()[1])
+
+ if data is not None:
+ print(data)
+
+
+class CLI(Bcfg2.Options.CommandRegistry):
+ def __init__(self):
+ Bcfg2.Options.CommandRegistry.__init__(self)
+ Bcfg2.Options.register_commands(self.__class__, globals().values(),
+ parent=AdminCmd)
+ parser = Bcfg2.Options.get_parser(
+ description="Manage a running Bcfg2 server",
+ components=[self])
+ parser.parse()
+
+ def run(self):
+ self.commands[Bcfg2.Options.setup.subcommand].setup()
+ return self.runcommand()
diff --git a/src/lib/Bcfg2/Server/Admin/Backup.py b/src/lib/Bcfg2/Server/Admin/Backup.py
deleted file mode 100644
index 0a04df98b..000000000
--- a/src/lib/Bcfg2/Server/Admin/Backup.py
+++ /dev/null
@@ -1,22 +0,0 @@
-""" Make a backup of the Bcfg2 repository """
-
-import os
-import time
-import tarfile
-import Bcfg2.Server.Admin
-import Bcfg2.Options
-
-
-class Backup(Bcfg2.Server.Admin.MetadataCore):
- """ Make a backup of the Bcfg2 repository """
-
- def __call__(self, args):
- datastore = self.setup['repo']
- timestamp = time.strftime('%Y%m%d%H%M%S')
- fmt = 'gz'
- mode = 'w:' + fmt
- filename = timestamp + '.tar' + '.' + fmt
- out = tarfile.open(os.path.join(datastore, filename), mode=mode)
- out.add(datastore, os.path.basename(datastore))
- out.close()
- print("Archive %s was stored under %s" % (filename, datastore))
diff --git a/src/lib/Bcfg2/Server/Admin/Client.py b/src/lib/Bcfg2/Server/Admin/Client.py
deleted file mode 100644
index 187ccfd71..000000000
--- a/src/lib/Bcfg2/Server/Admin/Client.py
+++ /dev/null
@@ -1,32 +0,0 @@
-""" Create, delete, or list client entries """
-
-import sys
-import Bcfg2.Server.Admin
-from Bcfg2.Server.Plugin import MetadataConsistencyError
-
-
-class Client(Bcfg2.Server.Admin.MetadataCore):
- """ Create, delete, or list client entries """
- __usage__ = "[options] [add|del|list] [attr=val]"
- __plugin_whitelist__ = ["Metadata"]
-
- def __call__(self, args):
- if len(args) == 0:
- self.errExit("No argument specified.\n"
- "Usage: %s" % self.__usage__)
- if args[0] == 'add':
- try:
- self.metadata.add_client(args[1])
- except MetadataConsistencyError:
- self.errExit("Error in adding client: %s" % sys.exc_info()[1])
- elif args[0] in ['delete', 'remove', 'del', 'rm']:
- try:
- self.metadata.remove_client(args[1])
- except MetadataConsistencyError:
- self.errExit("Error in deleting client: %s" %
- sys.exc_info()[1])
- elif args[0] in ['list', 'ls']:
- for client in self.metadata.list_clients():
- print(client)
- else:
- self.errExit("No command specified")
diff --git a/src/lib/Bcfg2/Server/Admin/Compare.py b/src/lib/Bcfg2/Server/Admin/Compare.py
deleted file mode 100644
index 6bb15cafd..000000000
--- a/src/lib/Bcfg2/Server/Admin/Compare.py
+++ /dev/null
@@ -1,147 +0,0 @@
-import lxml.etree
-import os
-import Bcfg2.Server.Admin
-
-
-class Compare(Bcfg2.Server.Admin.Mode):
- """ Determine differences between files or directories of client
- specification instances """
- __usage__ = ("<old> <new>\n\n"
- " -r\trecursive")
-
- def __init__(self):
- Bcfg2.Server.Admin.Mode.__init__(self)
- self.important = {'Path': ['name', 'type', 'owner', 'group', 'mode',
- 'important', 'paranoid', 'sensitive',
- 'dev_type', 'major', 'minor', 'prune',
- 'encoding', 'empty', 'to', 'recursive',
- 'vcstype', 'sourceurl', 'revision',
- 'secontext'],
- 'Package': ['name', 'type', 'version', 'simplefile',
- 'verify'],
- 'Service': ['name', 'type', 'status', 'mode',
- 'target', 'sequence', 'parameters'],
- 'Action': ['name', 'timing', 'when', 'status',
- 'command']
- }
-
- def compareStructures(self, new, old):
- if new.get("name"):
- bundle = new.get('name')
- else:
- bundle = 'Independent'
-
- identical = True
-
- for child in new.getchildren():
- if child.tag not in self.important:
- print(" %s in (new) bundle %s:\n tag type not handled!" %
- (child.tag, bundle))
- continue
- equiv = old.xpath('%s[@name="%s"]' %
- (child.tag, child.get('name')))
- if len(equiv) == 0:
- print(" %s %s in bundle %s:\n only in new configuration" %
- (child.tag, child.get('name'), bundle))
- identical = False
- continue
- diff = []
- if child.tag == 'Path' and child.get('type') == 'file' and \
- child.text != equiv[0].text:
- diff.append('contents')
- attrdiff = [field for field in self.important[child.tag] if \
- child.get(field) != equiv[0].get(field)]
- if attrdiff:
- diff.append('attributes (%s)' % ', '.join(attrdiff))
- if diff:
- print(" %s %s in bundle %s:\n %s differ" % (child.tag, \
- child.get('name'), bundle, ' and '.join(diff)))
- identical = False
-
- for child in old.getchildren():
- if child.tag not in self.important:
- print(" %s in (old) bundle %s:\n tag type not handled!" %
- (child.tag, bundle))
- elif len(new.xpath('%s[@name="%s"]' %
- (child.tag, child.get('name')))) == 0:
- print(" %s %s in bundle %s:\n only in old configuration" %
- (child.tag, child.get('name'), bundle))
- identical = False
-
- return identical
-
- def compareSpecifications(self, path1, path2):
- try:
- new = lxml.etree.parse(path1).getroot()
- except IOError:
- print("Failed to read %s" % (path1))
- raise SystemExit(1)
-
- try:
- old = lxml.etree.parse(path2).getroot()
- except IOError:
- print("Failed to read %s" % (path2))
- raise SystemExit(1)
-
- for src in [new, old]:
- for bundle in src.findall('./Bundle'):
- if bundle.get('name')[-4:] == '.xml':
- bundle.set('name', bundle.get('name')[:-4])
-
- identical = True
-
- for bundle in old.findall('./Bundle'):
- if len(new.xpath('Bundle[@name="%s"]' % (bundle.get('name')))) == 0:
- print(" Bundle %s only in old configuration" %
- bundle.get('name'))
- identical = False
- for bundle in new.findall('./Bundle'):
- equiv = old.xpath('Bundle[@name="%s"]' % (bundle.get('name')))
- if len(equiv) == 0:
- print(" Bundle %s only in new configuration" %
- bundle.get('name'))
- identical = False
- elif not self.compareStructures(bundle, equiv[0]):
- identical = False
-
- i1 = lxml.etree.Element('Independent')
- i2 = lxml.etree.Element('Independent')
- i1.extend(new.findall('./Independent/*'))
- i2.extend(old.findall('./Independent/*'))
- if not self.compareStructures(i1, i2):
- identical = False
-
- return identical
-
- def __call__(self, args):
- Bcfg2.Server.Admin.Mode.__call__(self, args)
- if len(args) == 0:
- self.errExit("No argument specified.\n"
- "Please see bcfg2-admin compare help for usage.")
- if '-r' in args:
- args = list(args)
- args.remove('-r')
- (oldd, newd) = args
- (old, new) = [os.listdir(spot) for spot in args]
- old_extra = []
- for item in old:
- if item not in new:
- old_extra.append(item)
- continue
- print("File: %s" % item)
- state = self.__call__([oldd + '/' + item, newd + '/' + item])
- new.remove(item)
- if state:
- print("File %s is good" % item)
- else:
- print("File %s is bad" % item)
- if new:
- print("%s has extra files: %s" % (newd, ', '.join(new)))
- if old_extra:
- print("%s has extra files: %s" % (oldd, ', '.join(old_extra)))
- return
- try:
- (old, new) = args
- return self.compareSpecifications(new, old)
- except IndexError:
- self.errExit(self.__call__.__doc__)
diff --git a/src/lib/Bcfg2/Server/Admin/Init.py b/src/lib/Bcfg2/Server/Admin/Init.py
deleted file mode 100644
index 870a31480..000000000
--- a/src/lib/Bcfg2/Server/Admin/Init.py
+++ /dev/null
@@ -1,350 +0,0 @@
-""" Interactively initialize a new repository. """
-
-import os
-import sys
-import stat
-import select
-import random
-import socket
-import string
-import getpass
-from Bcfg2.Utils import Executor
-import Bcfg2.Server.Admin
-import Bcfg2.Server.Plugin
-import Bcfg2.Options
-import Bcfg2.Server.Plugins.Metadata
-from Bcfg2.Compat import input # pylint: disable=W0622
-
-# default config file
-CONFIG = '''[server]
-repository = %s
-plugins = %s
-
-[statistics]
-sendmailpath = %s
-#web_debug = False
-#time_zone =
-
-[database]
-#engine = sqlite3
-# 'postgresql', 'mysql', 'mysql_old', 'sqlite3' or 'ado_mssql'.
-#name =
-# Or path to database file if using sqlite3.
-#<repository>/bcfg2.sqlite is default path if left empty
-#user =
-# Not used with sqlite3.
-#password =
-# Not used with sqlite3.
-#host =
-# Not used with sqlite3.
-#port =
-
-[reporting]
-transport = LocalFilesystem
-
-[communication]
-protocol = %s
-password = %s
-certificate = %s
-key = %s
-ca = %s
-
-[components]
-bcfg2 = %s
-'''
-
-# Default groups
-GROUPS = '''<Groups version='3.0'>
- <Group profile='true' public='true' default='true' name='basic'>
- <Group name='%s'/>
- </Group>
- <Group name='ubuntu'/>
- <Group name='debian'/>
- <Group name='freebsd'/>
- <Group name='gentoo'/>
- <Group name='redhat'/>
- <Group name='suse'/>
- <Group name='mandrake'/>
- <Group name='solaris'/>
- <Group name='arch'/>
-</Groups>
-'''
-
-# Default contents of clients.xml
-CLIENTS = '''<Clients version="3.0">
- <Client profile="basic" name="%s"/>
-</Clients>
-'''
-
-# Mapping of operating system names to groups
-OS_LIST = [('Red Hat/Fedora/RHEL/RHAS/Centos', 'redhat'),
- ('SUSE/SLES', 'suse'),
- ('Mandrake', 'mandrake'),
- ('Debian', 'debian'),
- ('Ubuntu', 'ubuntu'),
- ('Gentoo', 'gentoo'),
- ('FreeBSD', 'freebsd'),
- ('Arch', 'arch')]
-
-
-def safe_input(prompt):
- """ input() that flushes the input buffer before accepting input """
- # flush input buffer
- while len(select.select([sys.stdin.fileno()], [], [], 0.0)[0]) > 0:
- os.read(sys.stdin.fileno(), 4096)
- return input(prompt)
-
-
-def gen_password(length):
- """Generates a random alphanumeric password with length characters."""
- chars = string.letters + string.digits
- return "".join(random.choice(chars) for i in range(length))
-
-
-def create_key(hostname, keypath, certpath, country, state, location):
- """Creates a bcfg2.key at the directory specifed by keypath."""
- cmd = Executor(timeout=120)
- subject = "/C=%s/ST=%s/L=%s/CN=%s'" % (country, state, location, hostname)
- key = cmd.run(["openssl", "req", "-batch", "-x509", "-nodes",
- "-subj", subject, "-days", "1000", "-newkey", "rsa:2048",
- "-keyout", keypath, "-noout"])
- if not key.success:
- print("Error generating key: %s" % key.error)
- return
- os.chmod(keypath, stat.S_IRUSR | stat.S_IWUSR) # 0600
- csr = cmd.run(["openssl", "req", "-batch", "-new", "-subj", subject,
- "-key", keypath])
- if not csr.success:
- print("Error generating certificate signing request: %s" % csr.error)
- return
- cert = cmd.run(["openssl", "x509", "-req", "-days", "1000",
- "-signkey", keypath, "-out", certpath],
- inputdata=csr.stdout)
- if not cert.success:
- print("Error signing certificate: %s" % cert.error)
- return
-
-
-def create_conf(confpath, confdata):
- """ create the config file """
- # Don't overwrite existing bcfg2.conf file
- if os.path.exists(confpath):
- result = safe_input("\nWarning: %s already exists. "
- "Overwrite? [y/N]: " % confpath)
- if result not in ['Y', 'y']:
- print("Leaving %s unchanged" % confpath)
- return
- try:
- open(confpath, "w").write(confdata)
- os.chmod(confpath, stat.S_IRUSR | stat.S_IWUSR) # 0600
- except Exception:
- err = sys.exc_info()[1]
- print("Error trying to write configuration file '%s': %s" %
- (confpath, err))
- raise SystemExit(1)
-
-
-class Init(Bcfg2.Server.Admin.Mode):
- """Interactively initialize a new repository."""
-
- def __init__(self):
- Bcfg2.Server.Admin.Mode.__init__(self)
- self.data = dict()
- self.plugins = Bcfg2.Options.SERVER_PLUGINS.default
-
- def _set_defaults(self, opts):
- """Set default parameters."""
- self.data['configfile'] = opts['configfile']
- self.data['repopath'] = opts['repo']
- self.data['password'] = gen_password(8)
- self.data['server_uri'] = "https://%s:6789" % socket.getfqdn()
- self.data['sendmail'] = opts['sendmail']
- self.data['proto'] = opts['proto']
- if os.path.exists("/etc/pki/tls"):
- self.data['keypath'] = "/etc/pki/tls/private/bcfg2.key"
- self.data['certpath'] = "/etc/pki/tls/certs/bcfg2.crt"
- elif os.path.exists("/etc/ssl"):
- self.data['keypath'] = "/etc/ssl/bcfg2.key"
- self.data['certpath'] = "/etc/ssl/bcfg2.crt"
- else:
- basepath = os.path.dirname(self.configfile)
- self.data['keypath'] = os.path.join(basepath, "bcfg2.key")
- self.data['certpath'] = os.path.join(basepath, 'bcfg2.crt')
-
- def __call__(self, args):
- # Parse options
- setup = Bcfg2.Options.get_option_parser()
- setup.add_options(dict(configfile=Bcfg2.Options.CFILE,
- plugins=Bcfg2.Options.SERVER_PLUGINS,
- proto=Bcfg2.Options.SERVER_PROTOCOL,
- repo=Bcfg2.Options.SERVER_REPOSITORY,
- sendmail=Bcfg2.Options.SENDMAIL_PATH))
- opts = sys.argv[1:]
- opts.remove(self.__class__.__name__.lower())
- setup.reparse(argv=opts)
- self._set_defaults(setup)
-
- # Prompt the user for input
- self._prompt_config()
- self._prompt_repopath()
- self._prompt_password()
- self._prompt_hostname()
- self._prompt_server()
- self._prompt_groups()
- self._prompt_keypath()
- self._prompt_certificate()
-
- # Initialize the repository
- self.init_repo()
-
- def _prompt_hostname(self):
- """Ask for the server hostname."""
- data = safe_input("What is the server's hostname [%s]: " %
- socket.getfqdn())
- if data != '':
- self.data['shostname'] = data
- else:
- self.data['shostname'] = socket.getfqdn()
-
- def _prompt_config(self):
- """Ask for the configuration file path."""
- newconfig = safe_input("Store Bcfg2 configuration in [%s]: " %
- self.configfile)
- if newconfig != '':
- self.data['configfile'] = os.path.abspath(newconfig)
-
- def _prompt_repopath(self):
- """Ask for the repository path."""
- while True:
- newrepo = safe_input("Location of Bcfg2 repository [%s]: " %
- self.data['repopath'])
- if newrepo != '':
- self.data['repopath'] = os.path.abspath(newrepo)
- if os.path.isdir(self.data['repopath']):
- response = safe_input("Directory %s exists. Overwrite? [y/N]:"
- % self.data['repopath'])
- if response.lower().strip() == 'y':
- break
- else:
- break
-
- def _prompt_password(self):
- """Ask for a password or generate one if none is provided."""
- newpassword = getpass.getpass(
- "Input password used for communication verification "
- "(without echoing; leave blank for a random): ").strip()
- if len(newpassword) != 0:
- self.data['password'] = newpassword
-
- def _prompt_server(self):
- """Ask for the server name."""
- newserver = safe_input("Input the server location [%s]: " %
- self.data['server_uri'])
- if newserver != '':
- self.data['server_uri'] = newserver
-
- def _prompt_groups(self):
- """Create the groups.xml file."""
- prompt = '''Input base Operating System for clients:\n'''
- for entry in OS_LIST:
- prompt += "%d: %s\n" % (OS_LIST.index(entry) + 1, entry[0])
- prompt += ': '
- while True:
- try:
- osidx = int(safe_input(prompt))
- self.data['os_sel'] = OS_LIST[osidx - 1][1]
- break
- except ValueError:
- continue
-
- def _prompt_certificate(self):
- """Ask for the key details (country, state, and location)."""
- print("The following questions affect SSL certificate generation.")
- print("If no data is provided, the default values are used.")
- newcountry = safe_input("Country name (2 letter code) for "
- "certificate: ")
- if newcountry != '':
- if len(newcountry) == 2:
- self.data['country'] = newcountry
- else:
- while len(newcountry) != 2:
- newcountry = safe_input("2 letter country code (eg. US): ")
- if len(newcountry) == 2:
- self.data['country'] = newcountry
- break
- else:
- self.data['country'] = 'US'
-
- newstate = safe_input("State or Province Name (full name) for "
- "certificate: ")
- if newstate != '':
- self.data['state'] = newstate
- else:
- self.data['state'] = 'Illinois'
-
- newlocation = safe_input("Locality Name (eg, city) for certificate: ")
- if newlocation != '':
- self.data['location'] = newlocation
- else:
- self.data['location'] = 'Argonne'
-
- def _prompt_keypath(self):
- """ Ask for the key pair location. Try to use sensible
- defaults depending on the OS """
- keypath = safe_input("Path where Bcfg2 server private key will be "
- "created [%s]: " % self.data['keypath'])
- if keypath:
- self.data['keypath'] = keypath
- certpath = safe_input("Path where Bcfg2 server cert will be created "
- "[%s]: " % self.data['certpath'])
- if certpath:
- self.data['certpath'] = certpath
-
- def _init_plugins(self):
- """Initialize each plugin-specific portion of the repository."""
- for plugin in self.plugins:
- if plugin == 'Metadata':
- Bcfg2.Server.Plugins.Metadata.Metadata.init_repo(
- self.data['repopath'],
- groups_xml=GROUPS % self.data['os_sel'],
- clients_xml=CLIENTS % socket.getfqdn())
- else:
- try:
- module = __import__("Bcfg2.Server.Plugins.%s" % plugin, '',
- '', ["Bcfg2.Server.Plugins"])
- cls = getattr(module, plugin)
- cls.init_repo(self.data['repopath'])
- except: # pylint: disable=W0702
- err = sys.exc_info()[1]
- print("Plugin setup for %s failed: %s\n"
- "Check that dependencies are installed" % (plugin,
- err))
-
- def init_repo(self):
- """Setup a new repo and create the content of the
- configuration file."""
- # Create the repository
- path = os.path.join(self.data['repopath'], 'etc')
- try:
- os.makedirs(path)
- self._init_plugins()
- print("Repository created successfuly in %s" %
- self.data['repopath'])
- except OSError:
- print("Failed to create %s." % path)
-
- confdata = CONFIG % (self.data['repopath'],
- ','.join(self.plugins),
- self.data['sendmail'],
- self.data['proto'],
- self.data['password'],
- self.data['certpath'],
- self.data['keypath'],
- self.data['certpath'],
- self.data['server_uri'])
-
- # Create the configuration file and SSL key
- create_conf(self.data['configfile'], confdata)
- create_key(self.data['shostname'], self.data['keypath'],
- self.data['certpath'], self.data['country'],
- self.data['state'], self.data['location'])
diff --git a/src/lib/Bcfg2/Server/Admin/Minestruct.py b/src/lib/Bcfg2/Server/Admin/Minestruct.py
deleted file mode 100644
index 37ca74894..000000000
--- a/src/lib/Bcfg2/Server/Admin/Minestruct.py
+++ /dev/null
@@ -1,56 +0,0 @@
-""" Extract extra entry lists from statistics """
-import getopt
-import lxml.etree
-import sys
-import Bcfg2.Server.Admin
-from Bcfg2.Server.Plugin import PullSource
-
-
-class Minestruct(Bcfg2.Server.Admin.StructureMode):
- """ Extract extra entry lists from statistics """
- __usage__ = ("[options] <client>\n\n"
- " %-25s%s\n"
- " %-25s%s\n" %
- ("-f <filename>", "build a particular file",
- "-g <groups>", "only build config for groups"))
-
- def __call__(self, args):
- if len(args) == 0:
- self.errExit("No argument specified.\n"
- "Please see bcfg2-admin minestruct help for usage.")
- try:
- (opts, args) = getopt.getopt(args, 'f:g:h')
- except getopt.GetoptError:
- self.errExit(self.__doc__)
-
- client = args[0]
- output = sys.stdout
- groups = []
-
- for (opt, optarg) in opts:
- if opt == '-f':
- try:
- output = open(optarg, 'w')
- except IOError:
- self.errExit("Failed to open file: %s" % (optarg))
- elif opt == '-g':
- groups = optarg.split(':')
-
- try:
- extra = set()
- for source in self.bcore.plugins_by_type(PullSource):
- for item in source.GetExtra(client):
- extra.add(item)
- except: # pylint: disable=W0702
- self.errExit("Failed to find extra entry info for client %s" %
- client)
- root = lxml.etree.Element("Base")
- self.log.info("Found %d extra entries" % (len(extra)))
- add_point = root
- for grp in groups:
- add_point = lxml.etree.SubElement(add_point, "Group", name=grp)
- for tag, name in extra:
- self.log.info("%s: %s" % (tag, name))
- lxml.etree.SubElement(add_point, tag, name=name)
-
- lxml.etree.ElementTree(root).write(output, pretty_print=True)
diff --git a/src/lib/Bcfg2/Server/Admin/Perf.py b/src/lib/Bcfg2/Server/Admin/Perf.py
deleted file mode 100644
index 1a772e6fc..000000000
--- a/src/lib/Bcfg2/Server/Admin/Perf.py
+++ /dev/null
@@ -1,38 +0,0 @@
-""" Get performance data from server """
-
-import sys
-import Bcfg2.Options
-import Bcfg2.Client.Proxy
-import Bcfg2.Server.Admin
-
-
-class Perf(Bcfg2.Server.Admin.Mode):
- """ Get performance data from server """
-
- def __call__(self, args):
- output = [('Name', 'Min', 'Max', 'Mean', 'Count')]
- setup = Bcfg2.Options.get_option_parser()
- setup.add_options(dict(ca=Bcfg2.Options.CLIENT_CA,
- certificate=Bcfg2.Options.CLIENT_CERT,
- key=Bcfg2.Options.SERVER_KEY,
- password=Bcfg2.Options.SERVER_PASSWORD,
- server=Bcfg2.Options.SERVER_LOCATION,
- user=Bcfg2.Options.CLIENT_USER,
- timeout=Bcfg2.Options.CLIENT_TIMEOUT))
- opts = sys.argv[1:]
- opts.remove(self.__class__.__name__.lower())
- setup.reparse(argv=opts)
- proxy = Bcfg2.Client.Proxy.ComponentProxy(setup['server'],
- setup['user'],
- setup['password'],
- key=setup['key'],
- cert=setup['certificate'],
- ca=setup['ca'],
- timeout=setup['timeout'])
- data = proxy.get_statistics()
- for key in sorted(data.keys()):
- output.append(
- (key, ) +
- tuple(["%.06f" % item
- for item in data[key][:-1]] + [data[key][-1]]))
- self.print_table(output)
diff --git a/src/lib/Bcfg2/Server/Admin/Pull.py b/src/lib/Bcfg2/Server/Admin/Pull.py
deleted file mode 100644
index 8f84cd87d..000000000
--- a/src/lib/Bcfg2/Server/Admin/Pull.py
+++ /dev/null
@@ -1,147 +0,0 @@
-""" Retrieves entries from clients and integrates the information into
-the repository """
-
-import os
-import sys
-import getopt
-import select
-import Bcfg2.Server.Admin
-from Bcfg2.Server.Plugin import PullSource, Generator
-from Bcfg2.Compat import input # pylint: disable=W0622
-
-
-class Pull(Bcfg2.Server.Admin.MetadataCore):
- """ Retrieves entries from clients and integrates the information
- into the repository """
- __usage__ = ("[options] <client> <entry type> <entry name>\n\n"
- " %-25s%s\n"
- " %-25s%s\n"
- " %-25s%s\n"
- " %-25s%s\n" %
- ("-v", "be verbose",
- "-f", "force",
- "-I", "interactive",
- "-s", "stdin"))
-
- def __init__(self):
- Bcfg2.Server.Admin.MetadataCore.__init__(self)
- self.log = False
- self.mode = 'interactive'
-
- def __call__(self, args):
- use_stdin = False
- try:
- opts, gargs = getopt.getopt(args, 'vfIs')
- except getopt.GetoptError:
- self.errExit(self.__doc__)
- for opt in opts:
- if opt[0] == '-v':
- self.log = True
- elif opt[0] == '-f':
- self.mode = 'force'
- elif opt[0] == '-I':
- self.mode = 'interactive'
- elif opt[0] == '-s':
- use_stdin = True
-
- if use_stdin:
- for line in sys.stdin:
- try:
- self.PullEntry(*line.split(None, 3))
- except SystemExit:
- print(" for %s" % line)
- except:
- print("Bad entry: %s" % line.strip())
- elif len(gargs) < 3:
- self.usage()
- else:
- self.PullEntry(gargs[0], gargs[1], gargs[2])
-
- def BuildNewEntry(self, client, etype, ename):
- """Construct a new full entry for
- given client/entry from statistics.
- """
- new_entry = {'type': etype, 'name': ename}
- pull_sources = self.bcore.plugins_by_type(PullSource)
- for plugin in pull_sources:
- try:
- (owner, group, mode, contents) = \
- plugin.GetCurrentEntry(client, etype, ename)
- break
- except Bcfg2.Server.Plugin.PluginExecutionError:
- if plugin == pull_sources[-1]:
- print("Pull Source failure; could not fetch current state")
- raise SystemExit(1)
-
- try:
- data = {'owner': owner,
- 'group': group,
- 'mode': mode,
- 'text': contents}
- except UnboundLocalError:
- print("Unable to build entry. "
- "Do you have a statistics plugin enabled?")
- raise SystemExit(1)
- for key, val in list(data.items()):
- if val:
- new_entry[key] = val
- return new_entry
-
- def Choose(self, choices):
- """Determine where to put pull data."""
- if self.mode == 'interactive':
- for choice in choices:
- print("Plugin returned choice:")
- if id(choice) == id(choices[0]):
- print("(current entry) ")
- if choice.all:
- print(" => global entry")
- elif choice.group:
- print(" => group entry: %s (prio %d)" %
- (choice.group, choice.prio))
- else:
- print(" => host entry: %s" % (choice.hostname))
-
- # flush input buffer
- while len(select.select([sys.stdin.fileno()], [], [],
- 0.0)[0]) > 0:
- os.read(sys.stdin.fileno(), 4096)
- ans = input("Use this entry? [yN]: ") in ['y', 'Y']
- if ans:
- return choice
- return False
- else:
- # mode == 'force'
- if not choices:
- return False
- return choices[0]
-
- def PullEntry(self, client, etype, ename):
- """Make currently recorded client state correct for entry."""
- new_entry = self.BuildNewEntry(client, etype, ename)
-
- meta = self.bcore.build_metadata(client)
- # Find appropriate plugin in bcore
- glist = [gen for gen in self.bcore.plugins_by_type(Generator)
- if ename in gen.Entries.get(etype, {})]
- if len(glist) != 1:
- self.errExit("Got wrong numbers of matching generators for entry:"
- "%s" % ([g.name for g in glist]))
- plugin = glist[0]
- if not isinstance(plugin, Bcfg2.Server.Plugin.PullTarget):
- self.errExit("Configuration upload not supported by plugin %s" %
- plugin.name)
- try:
- choices = plugin.AcceptChoices(new_entry, meta)
- specific = self.Choose(choices)
- if specific:
- plugin.AcceptPullData(specific, new_entry, self.log)
- except Bcfg2.Server.Plugin.PluginExecutionError:
- self.errExit("Configuration upload not supported by plugin %s" %
- plugin.name)
- # Commit if running under a VCS
- for vcsplugin in list(self.bcore.plugins.values()):
- if isinstance(vcsplugin, Bcfg2.Server.Plugin.Version):
- files = "%s/%s" % (plugin.data, ename)
- comment = 'file "%s" pulled from host %s' % (files, client)
- vcsplugin.commit_data([files], comment)
diff --git a/src/lib/Bcfg2/Server/Admin/Reports.py b/src/lib/Bcfg2/Server/Admin/Reports.py
deleted file mode 100644
index d21d66a22..000000000
--- a/src/lib/Bcfg2/Server/Admin/Reports.py
+++ /dev/null
@@ -1,262 +0,0 @@
-'''Admin interface for dynamic reports'''
-import Bcfg2.Logger
-import Bcfg2.Server.Admin
-import datetime
-import os
-import sys
-import traceback
-from Bcfg2 import settings
-
-# Load django and reports stuff _after_ we know we can load settings
-from django.core import management
-from Bcfg2.Reporting.utils import *
-
-project_directory = os.path.dirname(settings.__file__)
-project_name = os.path.basename(project_directory)
-sys.path.append(os.path.join(project_directory, '..'))
-project_module = __import__(project_name, '', '', [''])
-sys.path.pop()
-
-# Set DJANGO_SETTINGS_MODULE appropriately.
-os.environ['DJANGO_SETTINGS_MODULE'] = '%s.settings' % project_name
-from django.db import transaction
-
-from Bcfg2.Reporting.models import Client, Interaction, \
- Performance, Bundle, Group, FailureEntry, PathEntry, \
- PackageEntry, ServiceEntry, ActionEntry
-
-
-def printStats(fn):
- """
- Print db stats.
-
- Decorator for purging. Prints database statistics after a run.
- """
- def print_stats(self, *data):
- classes = (Client, Interaction, Performance, \
- FailureEntry, ActionEntry, PathEntry, PackageEntry, \
- ServiceEntry, Group, Bundle)
-
- starts = {}
- for cls in classes:
- starts[cls] = cls.objects.count()
-
- fn(self, *data)
-
- for cls in classes:
- print("%s removed: %s" % (cls().__class__.__name__,
- starts[cls] - cls.objects.count()))
-
- return print_stats
-
-
-class Reports(Bcfg2.Server.Admin.Mode):
- """ Manage dynamic reports """
- django_commands = ['dbshell', 'shell', 'sqlall', 'validate']
- __usage__ = ("[command] [options]\n"
- " Commands:\n"
- " init Initialize the database\n"
- " purge Purge records\n"
- " --client [n] Client to operate on\n"
- " --days [n] Records older then n days\n"
- " --expired Expired clients only\n"
- " scrub Scrub the database for duplicate "
- "reasons and orphaned entries\n"
- " stats print database statistics\n"
- " update Apply any updates to the reporting "
- "database\n"
- "\n"
- " Django commands:\n " \
- + "\n ".join(django_commands))
-
- def __init__(self):
- Bcfg2.Server.Admin.Mode.__init__(self)
- try:
- import south
- except ImportError:
- print("Django south is required for Reporting")
- raise SystemExit(-3)
-
- def __call__(self, args):
- if len(args) == 0 or args[0] == '-h':
- self.errExit(self.__usage__)
-
- # FIXME - dry run
-
- if args[0] in self.django_commands:
- self.django_command_proxy(args[0])
- elif args[0] == 'scrub':
- self.scrub()
- elif args[0] == 'stats':
- self.stats()
- elif args[0] in ['init', 'update', 'syncdb']:
- if self.setup['debug']:
- vrb = 2
- elif self.setup['verbose']:
- vrb = 1
- else:
- vrb = 0
- try:
- management.call_command("syncdb", verbosity=vrb)
- management.call_command("migrate", verbosity=vrb)
- except:
- self.errExit("Update failed: %s" % sys.exc_info()[1])
- elif args[0] == 'purge':
- expired = False
- client = None
- maxdate = None
- state = None
- i = 1
- while i < len(args):
- if args[i] == '-c' or args[i] == '--client':
- if client:
- self.errExit("Only one client per run")
- client = args[i + 1]
- print(client)
- i = i + 1
- elif args[i] == '--days':
- if maxdate:
- self.errExit("Max date specified multiple times")
- try:
- maxdate = datetime.datetime.now() - \
- datetime.timedelta(days=int(args[i + 1]))
- except:
- self.errExit("Invalid number of days: %s" %
- args[i + 1])
- i = i + 1
- elif args[i] == '--expired':
- expired = True
- i = i + 1
- if expired:
- if state:
- self.errExit("--state is not valid with --expired")
- self.purge_expired(maxdate)
- else:
- self.purge(client, maxdate, state)
- else:
- self.errExit("Unknown command: %s" % args[0])
-
- @transaction.commit_on_success
- def scrub(self):
- ''' Perform a thorough scrub and cleanup of the database '''
-
- # Cleanup unused entries
- for cls in (Group, Bundle, FailureEntry, ActionEntry, PathEntry,
- PackageEntry, PathEntry):
- try:
- start_count = cls.objects.count()
- cls.prune_orphans()
- self.log.info("Pruned %d %s records" % \
- (start_count - cls.objects.count(), cls.__class__.__name__))
- except:
- print("Failed to prune %s: %s" %
- (cls.__class__.__name__, sys.exc_info()[1]))
-
- def django_command_proxy(self, command):
- '''Call a django command'''
- if command == 'sqlall':
- management.call_command(command, 'Reporting')
- else:
- management.call_command(command)
-
- @printStats
- def purge(self, client=None, maxdate=None, state=None):
- '''Purge historical data from the database'''
-
- filtered = False # indicates whether or not a client should be deleted
-
- if not client and not maxdate and not state:
- self.errExit("Reports.prune: Refusing to prune all data")
-
- ipurge = Interaction.objects
- if client:
- try:
- cobj = Client.objects.get(name=client)
- ipurge = ipurge.filter(client=cobj)
- except Client.DoesNotExist:
- self.errExit("Client %s not in database" % client)
- self.log.debug("Filtering by client: %s" % client)
-
- if maxdate:
- filtered = True
- if not isinstance(maxdate, datetime.datetime):
- raise TypeError("maxdate is not a DateTime object")
- self.log.debug("Filtering by maxdate: %s" % maxdate)
- ipurge = ipurge.filter(timestamp__lt=maxdate)
-
- if settings.DATABASES['default']['ENGINE'] == \
- 'django.db.backends.sqlite3':
- grp_limit = 100
- else:
- grp_limit = 1000
- if state:
- filtered = True
- if state not in ('dirty', 'clean', 'modified'):
- raise TypeError("state is not one of the following values: "
- "dirty, clean, modified")
- self.log.debug("Filtering by state: %s" % state)
- ipurge = ipurge.filter(state=state)
-
- count = ipurge.count()
- rnum = 0
- try:
- while rnum < count:
- grp = list(ipurge[:grp_limit].values("id"))
- # just in case...
- if not grp:
- break
- Interaction.objects.filter(id__in=[x['id']
- for x in grp]).delete()
- rnum += len(grp)
- self.log.debug("Deleted %s of %s" % (rnum, count))
- except:
- self.log.error("Failed to remove interactions")
- (a, b, c) = sys.exc_info()
- msg = traceback.format_exception(a, b, c, limit=2)[-1][:-1]
- del a, b, c
- self.log.error(msg)
-
- # Prune any orphaned ManyToMany relations
- for m2m in (ActionEntry, PackageEntry, PathEntry, ServiceEntry, \
- FailureEntry, Group, Bundle):
- self.log.debug("Pruning any orphaned %s objects" % \
- m2m().__class__.__name__)
- m2m.prune_orphans()
-
- if client and not filtered:
- # Delete the client, ping data is automatic
- try:
- self.log.debug("Purging client %s" % client)
- cobj.delete()
- except:
- self.log.error("Failed to delete client %s" % client)
- (a, b, c) = sys.exc_info()
- msg = traceback.format_exception(a, b, c, limit=2)[-1][:-1]
- del a, b, c
- self.log.error(msg)
-
- @printStats
- def purge_expired(self, maxdate=None):
- '''Purge expired clients from the database'''
-
- if maxdate:
- if not isinstance(maxdate, datetime.datetime):
- raise TypeError("maxdate is not a DateTime object")
- self.log.debug("Filtering by maxdate: %s" % maxdate)
- clients = Client.objects.filter(expiration__lt=maxdate)
- else:
- clients = Client.objects.filter(expiration__isnull=False)
-
- for client in clients:
- self.log.debug("Purging client %s" % client)
- Interaction.objects.filter(client=client).delete()
- client.delete()
-
- def stats(self):
- classes = (Client, Interaction, Performance, \
- FailureEntry, ActionEntry, PathEntry, PackageEntry, \
- ServiceEntry, Group, Bundle)
-
- for cls in classes:
- print("%s has %s records" % (cls().__class__.__name__,
- cls.objects.count()))
diff --git a/src/lib/Bcfg2/Server/Admin/Syncdb.py b/src/lib/Bcfg2/Server/Admin/Syncdb.py
deleted file mode 100644
index 2722364f7..000000000
--- a/src/lib/Bcfg2/Server/Admin/Syncdb.py
+++ /dev/null
@@ -1,31 +0,0 @@
-import sys
-import Bcfg2.settings
-import Bcfg2.Options
-import Bcfg2.Server.Admin
-import Bcfg2.Server.models
-from django.core.exceptions import ImproperlyConfigured
-from django.core.management import setup_environ, call_command
-
-
-class Syncdb(Bcfg2.Server.Admin.Mode):
- """ Sync the Django ORM with the configured database """
-
- def __call__(self, args):
- # Parse options
- setup = Bcfg2.Options.get_option_parser()
- setup.add_option("web_configfile", Bcfg2.Options.WEB_CFILE)
- opts = sys.argv[1:]
- opts.remove(self.__class__.__name__.lower())
- setup.reparse(argv=opts)
-
- setup_environ(Bcfg2.settings)
- Bcfg2.Server.models.load_models(cfile=setup['web_configfile'])
-
- try:
- call_command("syncdb", interactive=False, verbosity=0)
- self._database_available = True
- except ImproperlyConfigured:
- self.errExit("Django configuration problem: %s" %
- sys.exc_info()[1])
- except:
- self.errExit("Database update failed: %s" % sys.exc_info()[1])
diff --git a/src/lib/Bcfg2/Server/Admin/Viz.py b/src/lib/Bcfg2/Server/Admin/Viz.py
deleted file mode 100644
index a29fdaceb..000000000
--- a/src/lib/Bcfg2/Server/Admin/Viz.py
+++ /dev/null
@@ -1,104 +0,0 @@
-""" Produce graphviz diagrams of metadata structures """
-
-import getopt
-import Bcfg2.Server.Admin
-from Bcfg2.Utils import Executor
-
-
-class Viz(Bcfg2.Server.Admin.MetadataCore):
- """ Produce graphviz diagrams of metadata structures """
- __usage__ = ("[options]\n\n"
- " %-32s%s\n"
- " %-32s%s\n"
- " %-32s%s\n"
- " %-32s%s\n"
- " %-32s%s\n" %
- ("-H, --includehosts",
- "include hosts in the viz output",
- "-b, --includebundles",
- "include bundles in the viz output",
- "-k, --includekey",
- "show a key for different digraph shapes",
- "-c, --only-client <clientname>",
- "show only the groups, bundles for the named client",
- "-o, --outfile <file>",
- "write viz output to an output file"))
-
- colors = ['steelblue1', 'chartreuse', 'gold', 'magenta',
- 'indianred1', 'limegreen', 'orange1', 'lightblue2',
- 'green1', 'blue1', 'yellow1', 'darkturquoise', 'gray66']
-
- __plugin_blacklist__ = ['DBStats', 'Cfg', 'Pkgmgr',
- 'Packages', 'Rules', 'Decisions',
- 'Deps', 'Git', 'Svn', 'Fossil', 'Bzr', 'Bundler']
-
- def __call__(self, args):
- # First get options to the 'viz' subcommand
- try:
- opts, args = getopt.getopt(args, 'Hbkc:o:',
- ['includehosts', 'includebundles',
- 'includekey', 'only-client=',
- 'outfile='])
- except getopt.GetoptError:
- self.usage()
-
- hset = False
- bset = False
- kset = False
- only_client = None
- outputfile = False
- for opt, arg in opts:
- if opt in ("-H", "--includehosts"):
- hset = True
- elif opt in ("-b", "--includebundles"):
- bset = True
- elif opt in ("-k", "--includekey"):
- kset = True
- elif opt in ("-c", "--only-client"):
- only_client = arg
- elif opt in ("-o", "--outfile"):
- outputfile = arg
-
- data = self.Visualize(hset, bset, kset, only_client, outputfile)
- if data:
- print(data)
-
- def Visualize(self, hosts=False, bundles=False, key=False,
- only_client=None, output=None):
- """Build visualization of groups file."""
- if output:
- fmt = output.split('.')[-1]
- else:
- fmt = 'png'
-
- exc = Executor()
- cmd = ["dot", "-T", fmt]
- if output:
- cmd.extend(["-o", output])
- idata = ["digraph groups {",
- '\trankdir="LR";',
- self.metadata.viz(hosts, bundles,
- key, only_client, self.colors)]
- if key:
- idata.extend(
- ["\tsubgraph cluster_key {",
- '\tstyle="filled";',
- '\tcolor="lightblue";',
- '\tBundle [ shape="septagon" ];',
- '\tGroup [shape="ellipse"];',
- '\tProfile [style="bold", shape="ellipse"];',
- '\tHblock [label="Host1|Host2|Host3",shape="record"];',
- '\tlabel="Key";',
- "\t}"])
- idata.append("}")
- try:
- result = exc.run(cmd, inputdata=idata)
- except OSError:
- # on some systems (RHEL 6), you cannot run dot with
- # shell=True. on others (Gentoo with Python 2.7), you
- # must. In yet others (RHEL 5), either way works. I have
- # no idea what the difference is, but it's kind of a PITA.
- result = exc.run(cmd, shell=True, inputdata=idata)
- if not result.success:
- print("Error running %s: %s" % (cmd, result.error))
- raise SystemExit(result.retval)
diff --git a/src/lib/Bcfg2/Server/Admin/Xcmd.py b/src/lib/Bcfg2/Server/Admin/Xcmd.py
deleted file mode 100644
index 2613f74ac..000000000
--- a/src/lib/Bcfg2/Server/Admin/Xcmd.py
+++ /dev/null
@@ -1,54 +0,0 @@
-""" XML-RPC Command Interface for bcfg2-admin"""
-
-import sys
-import xmlrpclib
-import Bcfg2.Options
-import Bcfg2.Client.Proxy
-import Bcfg2.Server.Admin
-
-
-class Xcmd(Bcfg2.Server.Admin.Mode):
- """ XML-RPC Command Interface """
- __usage__ = "<command>"
-
- def __call__(self, args):
- setup = Bcfg2.Options.get_option_parser()
- setup.add_options(dict(ca=Bcfg2.Options.CLIENT_CA,
- certificate=Bcfg2.Options.CLIENT_CERT,
- key=Bcfg2.Options.SERVER_KEY,
- password=Bcfg2.Options.SERVER_PASSWORD,
- server=Bcfg2.Options.SERVER_LOCATION,
- user=Bcfg2.Options.CLIENT_USER,
- timeout=Bcfg2.Options.CLIENT_TIMEOUT))
- opts = sys.argv[1:]
- opts.remove(self.__class__.__name__.lower())
- setup.reparse(argv=opts)
- Bcfg2.Client.Proxy.RetryMethod.max_retries = 1
- proxy = Bcfg2.Client.Proxy.ComponentProxy(setup['server'],
- setup['user'],
- setup['password'],
- key=setup['key'],
- cert=setup['certificate'],
- ca=setup['ca'],
- timeout=setup['timeout'])
- if len(setup['args']) == 0:
- self.errExit("Usage: xcmd <xmlrpc method> <optional arguments>")
- cmd = args[0]
- try:
- data = getattr(proxy, cmd)(*args[1:])
- except xmlrpclib.Fault:
- flt = sys.exc_info()[1]
- if flt.faultCode == 7:
- print("Unknown method %s" % cmd)
- return
- elif flt.faultCode == 20:
- return
- else:
- raise
- except Bcfg2.Client.Proxy.ProxyError:
- err = sys.exc_info()[1]
- print("Proxy Error: %s" % err)
- return
-
- if data is not None:
- print(data)
diff --git a/src/lib/Bcfg2/Server/Admin/__init__.py b/src/lib/Bcfg2/Server/Admin/__init__.py
deleted file mode 100644
index 06a419354..000000000
--- a/src/lib/Bcfg2/Server/Admin/__init__.py
+++ /dev/null
@@ -1,142 +0,0 @@
-""" Base classes for admin modes """
-
-import re
-import sys
-import logging
-import lxml.etree
-import Bcfg2.Server.Core
-import Bcfg2.Options
-from Bcfg2.Compat import ConfigParser, walk_packages
-
-__all__ = [m[1] for m in walk_packages(path=__path__)]
-
-
-class Mode(object):
- """ Base object for admin modes. Docstrings are used as help
- messages, so if you are seeing this, a help message has not yet
- been added for this mode. """
- __usage__ = None
- __args__ = []
-
- def __init__(self):
- self.setup = Bcfg2.Options.get_option_parser()
- self.configfile = self.setup['configfile']
- self.__cfp = False
- self.log = logging.getLogger('Bcfg2.Server.Admin.Mode')
- usage = "bcfg2-admin %s" % self.__class__.__name__.lower()
- if self.__usage__ is not None:
- usage += " " + self.__usage__
- self.setup.hm = usage
-
- def getCFP(self):
- """ get a config parser for the Bcfg2 config file """
- if not self.__cfp:
- self.__cfp = ConfigParser.ConfigParser()
- self.__cfp.read(self.configfile)
- return self.__cfp
-
- cfp = property(getCFP)
-
- def __call__(self, args):
- raise NotImplementedError
-
- @classmethod
- def usage(cls, rv=1):
- """ Exit with a long usage message """
- print(re.sub(r'\s{2,}', ' ', cls.__doc__.strip()))
- print("")
- print("Usage:")
- usage = "bcfg2-admin %s" % cls.__name__.lower()
- if cls.__usage__ is not None:
- usage += " " + cls.__usage__
- print(" %s" % usage)
- raise SystemExit(rv)
-
- def shutdown(self):
- """ Perform any necessary shtudown tasks for this mode """
- pass
-
- def errExit(self, emsg):
- """ exit with an error """
- print(emsg)
- raise SystemExit(1)
-
- def load_stats(self, client):
- """ Load static statistics from the repository """
- stats = lxml.etree.parse("%s/etc/statistics.xml" % self.setup['repo'])
- hostent = stats.xpath('//Node[@name="%s"]' % client)
- if not hostent:
- self.errExit("Could not find stats for client %s" % (client))
- return hostent[0]
-
- def print_table(self, rows, justify='left', hdr=True, vdelim=" ",
- padding=1):
- """Pretty print a table
-
- rows - list of rows ([[row 1], [row 2], ..., [row n]])
- hdr - if True the first row is treated as a table header
- vdelim - vertical delimiter between columns
- padding - # of spaces around the longest element in the column
- justify - may be left,center,right
-
- """
- hdelim = "="
- justify = {'left': str.ljust,
- 'center': str.center,
- 'right': str.rjust}[justify.lower()]
-
- # Calculate column widths (longest item in each column
- # plus padding on both sides)
- cols = list(zip(*rows))
- col_widths = [max([len(str(item)) + 2 * padding
- for item in col]) for col in cols]
- borderline = vdelim.join([w * hdelim for w in col_widths])
-
- # Print out the table
- print(borderline)
- for row in rows:
- print(vdelim.join([justify(str(item), width)
- for (item, width) in zip(row, col_widths)]))
- if hdr:
- print(borderline)
- hdr = False
-
-
-# pylint wants MetadataCore and StructureMode to be concrete classes
-# and implement __call__, but they aren't and they don't, so we
-# disable that warning
-# pylint: disable=W0223
-
-class MetadataCore(Mode):
- """Base class for admin-modes that handle metadata."""
- __plugin_whitelist__ = None
- __plugin_blacklist__ = None
-
- def __init__(self):
- Mode.__init__(self)
- if self.__plugin_whitelist__ is not None:
- self.setup['plugins'] = [p for p in self.setup['plugins']
- if p in self.__plugin_whitelist__]
- elif self.__plugin_blacklist__ is not None:
- self.setup['plugins'] = [p for p in self.setup['plugins']
- if p not in self.__plugin_blacklist__]
-
- # admin modes don't need to watch for changes. one shot is fine here.
- self.setup['filemonitor'] = 'pseudo'
- try:
- self.bcore = Bcfg2.Server.Core.BaseCore()
- except Bcfg2.Server.Core.CoreInitError:
- msg = sys.exc_info()[1]
- self.errExit("Core load failed: %s" % msg)
- self.bcore.load_plugins()
- self.bcore.fam.handle_event_set()
- self.metadata = self.bcore.metadata
-
- def shutdown(self):
- if hasattr(self, 'bcore'):
- self.bcore.shutdown()
-
-
-class StructureMode(MetadataCore): # pylint: disable=W0223
- """ Base class for admin modes that handle structure plugins """
- pass
diff --git a/src/sbin/bcfg2-admin b/src/sbin/bcfg2-admin
index 0e1e34c60..d57cd8b35 100755
--- a/src/sbin/bcfg2-admin
+++ b/src/sbin/bcfg2-admin
@@ -2,97 +2,11 @@
""" bcfg2-admin is a script that helps to administer a Bcfg2
deployment. """
-import re
import sys
-import logging
-import Bcfg2.Logger
-import Bcfg2.Options
-import Bcfg2.Server.Admin
-from Bcfg2.Compat import StringIO
-
-
-def mode_import(modename):
- """Load Bcfg2.Server.Admin.<mode>."""
- modname = modename.capitalize()
- mod = getattr(__import__("Bcfg2.Server.Admin.%s" %
- (modname)).Server.Admin, modname)
- return getattr(mod, modname)
-
-
-def get_modes():
- """Get all available modes, except for the base mode."""
- return [x.lower() for x in Bcfg2.Server.Admin.__all__ if x != 'mode']
-
-
-def create_description():
- """Create the description string from the list of modes."""
- modes = get_modes()
- description = StringIO()
- description.write("Available modes are:\n\n")
- for mode in modes:
- try:
- doc = re.sub(r'\s{2,}', ' ', mode_import(mode).__doc__.strip())
- except (ImportError, SystemExit):
- continue
- description.write((" %-15s %s\n" % (mode, doc)))
- return description.getvalue()
-
-
-def main():
- optinfo = dict()
- optinfo.update(Bcfg2.Options.CLI_COMMON_OPTIONS)
- optinfo.update(Bcfg2.Options.SERVER_COMMON_OPTIONS)
- setup = Bcfg2.Options.load_option_parser(optinfo)
- # override default help message to include description of all modes
- setup.hm = "Usage:\n\n%s\n%s" % (setup.buildHelpMessage(),
- create_description())
- setup.parse(sys.argv[1:])
-
- if setup['debug']:
- level = logging.DEBUG
- elif setup['verbose']:
- level = logging.INFO
- else:
- level = logging.WARNING
- Bcfg2.Logger.setup_logging('bcfg2-admin', to_syslog=setup['syslog'],
- level=level)
-
- log = logging.getLogger('bcfg2-admin')
-
- # Provide help if requested or no args were specified
- if (not setup['args'] or len(setup['args']) < 1 or
- setup['args'][0] == 'help' or setup['help']):
- if len(setup['args']) > 1:
- # Get help for a specific mode by passing it the help argument
- setup['args'] = [setup['args'][1], setup['args'][0]]
- else:
- # Print short help for all modes
- print(setup.hm)
- raise SystemExit(0)
-
- if setup['args'][0] in get_modes():
- modname = setup['args'][0].capitalize()
- if len(setup['args']) > 1 and setup['args'][1] == 'help':
- mode_cls = mode_import(modname)
- mode_cls.usage(rv=0)
- try:
- mode_cls = mode_import(modname)
- except ImportError:
- err = sys.exc_info()[1]
- log.error("Failed to load admin mode %s: %s" % (modname, err))
- raise SystemExit(1)
- mode = mode_cls()
- try:
- return mode(setup['args'][1:])
- finally:
- mode.shutdown()
- else:
- log.error("Error: Unknown mode '%s'\n" % setup['args'][0])
- print(create_description())
- raise SystemExit(1)
+from Bcfg2.Server.Admin import CLI
if __name__ == '__main__':
try:
- sys.exit(main())
+ sys.exit(CLI().run())
except KeyboardInterrupt:
raise SystemExit(1)