%PDF- %PDF-
Direktori : /bin/ |
Current File : //bin/broctl |
#!/usr/bin/python # # The BroControl interactive shell. from __future__ import print_function import sys import time import logging # This is needed so that we can import BroControl. sys.path.insert(0, "/usr/lib/broctl") from BroControl.broctl import BroCtl from BroControl import brocmd from BroControl import util from BroControl import utilcurses from BroControl import version # Main command loop. class BroCtlCmdLoop(brocmd.ExitValueCmd): prompt = '[BroControl] > ' def __init__(self, broctl_class=BroCtl, interactive=False, cmd=""): brocmd.ExitValueCmd.__init__(self) self.broctl = broctl_class(ui=self) self.node_names = self.broctl.node_names() self.interactive = interactive # Warn user to do broctl install, if needed. Skip this check when # running cron to avoid receiving annoying emails. if cmd != "cron": self.broctl.warn_broctl_install(cmd in ("install", "deploy")) def finish(self): self.broctl.finish() def info(self, text): print(text) logging.info(text) def warn(self, text): self.info("Warning: %s" % text) def error(self, text): print("Error: %s" % text, file=sys.stderr) logging.info(text) self.exit_code = 1 def default(self, line): strlist = line.split() if not self.broctl.plugins.runCustomCommand(strlist[0], " ".join(strlist[1:]), self): if strlist[0] != "--help": self.error("unknown command '%s'" % strlist[0]) if not self.interactive: self.do_help(None) return True def emptyline(self): pass def precmd(self, line): logging.debug(line) return line def postcmd(self, stop, line): logging.debug("done") return stop def do_EOF(self, args): self._stopping = True return True def do_exit(self, args): """Terminates the shell.""" self._stopping = True return True def do_quit(self, args): """Terminates the shell.""" self._stopping = True return True def do_nodes(self, args): """Prints a list of all configured nodes.""" if args: raise SyntaxError("nodes does not take any arguments") for node in self.broctl.nodes(): self.info(node) return True def do_config(self, args): """Prints all configuration options with their current values.""" if args: raise SyntaxError("config does not take any arguments") for (key, val) in self.broctl.get_config(): self.info("%s = %s" % (key, val)) return True def do_install(self, args): """- [--local] Reinstalls on all nodes (unless the ``--local`` option is given, in which case nothing will be propagated to other nodes), including all configuration files and local policy scripts. Usually all nodes should be reinstalled at the same time, as any inconsistencies between them will lead to strange effects. This command must be executed after *all* changes to any part of the broctl configuration (and after upgrading to a new version of Bro or BroControl), otherwise the modifications will not take effect. Before executing ``install``, it is recommended to verify the configuration with check_.""" local = False for arg in args.split(): if arg == "--local": local = True else: raise SyntaxError("Invalid argument: %s" % arg) results = self.broctl.install(local) return results.ok def do_start(self, args): """- [<nodes>] Starts the given nodes, or all nodes if none are specified. Nodes already running are left untouched. """ results = self.broctl.start(node_list=args) return results.ok def do_stop(self, args): """- [<nodes>] Stops the given nodes, or all nodes if none are specified. Nodes not running are left untouched. """ results = self.broctl.stop(node_list=args) return results.ok def do_restart(self, args): """- [--clean] [<nodes>] Restarts the given nodes, or all nodes if none are specified. The effect is the same as first executing stop_ followed by a start_, giving the same nodes in both cases. If ``--clean`` is given, the installation is reset into a clean state before restarting. More precisely, a ``restart --clean`` turns into the command sequence stop_, cleanup_, check_, install_, and start_. """ clean = False if args.startswith("--clean"): args = args[7:] clean = True results = self.broctl.restart(clean=clean, node_list=args) return results.ok def do_deploy(self, args): """ Checks for errors in Bro policy scripts, then does an install followed by a restart on all nodes. This command should be run after any changes to Bro policy scripts or the broctl configuration, and after Bro is upgraded or even just recompiled. This command is equivalent to running the check_, install_, and restart_ commands, in that order. """ if args: raise SyntaxError("deploy does not take any arguments") results = self.broctl.deploy() return results.ok def do_status(self, args): """- [<nodes>] Prints the current status of the given nodes.""" success = True results = self.broctl.status(node_list=args) typewidth = 7 hostwidth = 16 data = results.get_node_data() if data[0][2]["type"] == "standalone": # In standalone mode, we need a wider "type" column. typewidth = 10 hostwidth = 13 showall = "peers" in data[0][2] if showall: colfmt = "{name:<12} {type:<{0}} {host:<{1}} {status:<9} {pid:<6} {peers:<6} {started}" else: colfmt = "{name:<12} {type:<{0}} {host:<{1}} {status:<9} {pid:<6} {started}" hdrlist = ["name", "type", "host", "status", "pid", "peers", "started"] header = dict((x, x.title()) for x in hdrlist) self.info(colfmt.format(typewidth, hostwidth, **header)) colfmtstopped = "{name:<12} {type:<{0}} {host:<{1}} {status}" for data in results.get_node_data(): node_info = data[2] if node_info["pid"]: mycolfmt = colfmt else: mycolfmt = colfmtstopped self.info(mycolfmt.format(typewidth, hostwidth, **node_info)) # Return status code of True only if all nodes are running if node_info["status"] != "running": success = False return success def _do_top_once(self, args): results = self.broctl.top(args) typewidth = 7 hostwidth = 16 data = results.get_node_data() proclist = data[0][2]["procs"] if proclist[0]["type"] == "standalone": # In standalone mode, we need a wider "type" column. typewidth = 10 hostwidth = 13 lines = ["%-12s %-*s %-*s %-7s %-7s %-6s %-4s %-5s %s" % ("Name", typewidth, "Type", hostwidth, "Host", "Pid", "Proc", "VSize", "Rss", "Cpu", "Cmd")] for data in results.get_node_data(): proclist = data[2]["procs"] for top_info in proclist: msg = ["%-12s" % top_info["name"]] msg.append("%-*s" % (typewidth, top_info["type"])) msg.append("%-*s" % (hostwidth, top_info["host"])) if top_info["error"]: msg.append("<%s>" % top_info["error"]) else: msg.append("%-7s" % top_info["pid"]) msg.append("%-7s" % top_info["proc"]) msg.append("%-6s" % util.number_unit_str(top_info["vsize"])) msg.append("%-4s" % util.number_unit_str(top_info["rss"])) msg.append("%3s%% " % top_info["cpu"]) msg.append("%s" % top_info["cmd"]) lines.append(" ".join(msg)) return (results.ok, lines) def do_top(self, args): """- [<nodes>] For each of the nodes, prints the status of the two Bro processes (parent process and child process) in a *top*-like format, including CPU usage and memory consumption. If executed interactively, the display is updated frequently until key ``q`` is pressed. If invoked non-interactively, the status is printed only once.""" if not self.interactive: success, lines = self._do_top_once(args) for line in lines: self.info(line) return success utilcurses.enterCurses() utilcurses.clearScreen() count = 0 while utilcurses.getCh() != "q": if count % 10 == 0: success, lines = self._do_top_once(args) utilcurses.clearScreen() utilcurses.printLines(lines) time.sleep(.1) count += 1 utilcurses.leaveCurses() return success def do_diag(self, args): """- [<nodes>] If a node has terminated unexpectedly, this command prints a (somewhat cryptic) summary of its final state including excerpts of any stdout/stderr output, resource usage, and also a stack backtrace if a core dump is found. The same information is sent out via mail when a node is found to have crashed (the "crash report"). While the information is mainly intended for debugging, it can also help to find misconfigurations (which are usually, but not always, caught by the check_ command).""" results = self.broctl.diag(node_list=args) for (node, success, output) in results.get_node_output(): self.info("[%s]" % node) for line in output: self.info(line) return results.ok def do_cron(self, args): """- [enable|disable|?] | [--no-watch] This command has two modes of operation. Without arguments (or just ``--no-watch``), it performs a set of maintenance tasks, including the logging of various statistical information, expiring old log files, checking for dead hosts, and restarting nodes which terminated unexpectedly (the latter can be suppressed with the ``--no-watch`` option if no auto-restart is desired). This mode is intended to be executed regularly via *cron*, as described in the installation instructions. While not intended for interactive use, no harm will be caused by executing the command manually: all the maintenance tasks will then just be performed one more time. The second mode is for interactive usage and determines if the regular tasks are indeed performed when ``broctl cron`` is executed. In other words, even with ``broctl cron`` in your crontab, you can still temporarily disable it by running ``cron disable``, and then later reenable with ``cron enable``. This can be helpful while working, e.g., on the BroControl configuration and ``cron`` would interfere with that. ``cron ?`` can be used to query the current state. """ watch = True if args == "--no-watch": watch = False elif args: if args == "enable": self.broctl.setcronenabled(True) elif args == "disable": self.broctl.setcronenabled(False) elif args == "?": results = self.broctl.cronenabled() self.info("cron " + (results and "enabled" or "disabled")) else: self.error("invalid cron argument") return False return True self.broctl.cron(watch) return True def do_check(self, args): """- [<nodes>] Verifies a modified configuration in terms of syntactical correctness (most importantly correct syntax in policy scripts). This command should be executed for each configuration change *before* install_ is used to put the change into place. The ``check`` command uses the policy files as found in SitePolicyPath_ to make sure they compile correctly. If they do, install_ will then copy them over to an internal place from where the nodes will read them at the next start_. This approach ensures that new errors in a policy script will not affect currently running nodes, even when one or more of them needs to be restarted.""" results = self.broctl.check(node_list=args) for (node, success, output) in results.get_node_output(): if success: self.info("%s scripts are ok." % node) else: self.info("%s scripts failed." % node) self.info("\n".join(output)) return results.ok def do_cleanup(self, args): """- [--all] [<nodes>] Clears the nodes' spool directories (if they are not running currently). This implies that their persistent state is flushed. Nodes that were crashed are reset into *stopped* state. If ``--all`` is specified, this command also removes the content of the node's TmpDir_, in particular deleteing any data potentially saved there for reference from previous crashes. Generally, if you want to reset the installation back into a clean state, you can first stop_ all nodes, then execute ``cleanup --all``, and finally start_ all nodes again.""" cleantmp = False if args.startswith("--all"): args = args[5:] cleantmp = True self.info("cleaning up nodes ...") results = self.broctl.cleanup(cleantmp=cleantmp, node_list=args) return results.ok def do_capstats(self, args): """- [<nodes>] [<interval>] Determines the current load on the network interfaces monitored by each of the given worker nodes. The load is measured over the specified interval (in seconds), or by default over 10 seconds. This command uses the :doc:`capstats<../../components/capstats/README>` tool, which is installed along with ``broctl``.""" interval = 10 args = args.split() if args: try: interval = max(1, int(args[-1])) args = args[0:-1] except ValueError: pass args = " ".join(args) def outputcapstats(tag, data): def output_one(tag, vals): return "%-21s %-10s %s" % (tag, vals.get("kpps", ""), vals.get("mbps", "")) self.info("\n%-21s %-10s %-10s (%ds average)\n%s" % (tag, "kpps", "mbps", interval, "-" * 40)) totals = None for (node, success, vals) in data: if not success: self.info(vals["output"]) continue if str(node) != "$total": hostnetif = "%s/%s" % (node.host, node.interface) self.info(output_one(hostnetif, vals)) else: totals = vals if totals: self.info("") self.info(output_one("Total", totals)) self.info("") results = self.broctl.capstats(interval=interval, node_list=args) outputcapstats("Interface", results.get_node_data()) return results.ok def do_update(self, args): """- [<nodes>] After a change to Bro policy scripts, this command updates the Bro processes on the given nodes *while they are running* (i.e., without requiring a restart_). However, such dynamic updates work only for a *subset* of Bro's full configuration. The following changes can be applied on the fly: The value of all const variables defined with the ``&redef`` attribute can be changed. More extensive script changes are not possible during runtime and always require a restart; if you change more than just the values of ``&redef``-able consts and still issue ``update``, the results are undefined and can lead to crashes. Also note that before running ``update``, you still need to do an install_ (preferably after check_), as otherwise ``update`` will not see the changes and it will resend the old configuration.""" results = self.broctl.update(node_list=args) return results.ok def do_df(self, args): """- [<nodes>] Reports the amount of disk space available on the nodes. Shows only paths relevant to the broctl installation.""" results = self.broctl.df(node_list=args) self.info("%27s %15s %-5s %-5s %-5s" % ("", "", "total", "avail", "capacity")) for (node, success, dfs) in results.get_node_data(): for key, diskinfo in dfs.items(): if key == "FAIL": self.info("df helper failed on %s: %s" % (node, diskinfo)) continue nodehost = "%s/%s" % (node.name, node.host) self.info("%27s %15s %-5s %-5s %-5.1f%%" % (nodehost, diskinfo.fs, util.number_unit_str(diskinfo.total), util.number_unit_str(diskinfo.available), diskinfo.percent)) return results.ok def do_print(self, args): """- <id> [<nodes>] Reports the *current* live value of the given Bro script ID on all of the specified nodes (which obviously must be running). This can for example be useful to (1) check that policy scripts are working as expected, or (2) confirm that configuration changes have in fact been applied. Note that IDs defined inside a Bro namespace must be prefixed with ``<namespace>::`` (e.g., ``print HTTP::mime_types_extensions`` to print the corresponding table from ``file-ident.bro``).""" args = args.split() try: id = args[0] args = " ".join(args[1:]) except IndexError: raise SyntaxError("no id given to print") results = self.broctl.print_id(id=id, node_list=args) for (node, success, args) in results.get_node_output(): if success: self.info("%12s %s = %s" % (node, args[0], args[1])) else: self.info("%12s <error: %s>" % (node, args)) return results.ok def do_peerstatus(self, args): """- [<nodes>] Primarily for debugging, ``peerstatus`` reports statistics about the network connections cluster nodes are using to communicate with other nodes.""" results = self.broctl.peerstatus(node_list=args) for (node, success, msg) in results.get_node_output(): if success: self.info("%11s\n%s" % (node, msg)) else: self.info("%11s <error: %s>" % (node, msg)) return results.ok def do_netstats(self, args): """- [<nodes>] Queries each of the nodes for their current counts of captured and dropped packets.""" results = self.broctl.netstats(node_list=args) for (node, success, msg) in results.get_node_output(): if success: self.info("%11s: %s" % (node, msg)) else: self.info("%11s: <error: %s>" % (node, msg)) return results.ok def do_exec(self, args): """- <command line> Executes the given Unix shell command line on all hosts configured to run at least one Bro instance. This is handy to quickly perform an action across all systems.""" results = self.broctl.execute(cmd=args) for node, success, output in results.get_node_output(): out = "\n> ".join(output) self.info("[%s/%s] %s\n> %s" % (node.name, node.host, (success and " " or "error"), out)) return results.ok def do_scripts(self, args): """- [-c] [<nodes>] Primarily for debugging Bro configurations, the ``scripts`` command lists all the Bro scripts loaded by each of the nodes in the order they will be parsed by the node at startup. If ``-c`` is given, the command operates as check_ does: it reads the policy files from their *original* location, not the copies installed by install_. The latter option is useful to check a not yet installed configuration.""" check = False args = args.split() try: while args[0].startswith("-"): opt = args[0] if opt == "-c": # Check non-installed policies. check = True else: raise SyntaxError("Unknown option: %s" % opt) args = args[1:] except IndexError: pass args = " ".join(args) results = self.broctl.scripts(check=check, node_list=args) for (node, success, output) in results.get_node_output(): if success: self.info("%s scripts are ok." % node) for line in output: self.info(" %s" % line) else: self.info("%s scripts failed." % node) self.info("\n".join(output)) return results.ok def do_process(self, args): """- <trace> [options] [-- <scripts>] Runs Bro offline on a given trace file using the same configuration as when running live. It does, however, use the potentially not-yet-installed policy files in SitePolicyPath_ and disables log rotation. Additional Bro command line flags and scripts can be given (each argument after a ``--`` argument is interpreted as a script). Upon completion, the command prints a path where the log files can be found. Subsequent runs of this command may delete these logs. In cluster mode, Bro is run with *both* manager and worker scripts loaded into a single instance. While that doesn't fully reproduce the live setup, it is often sufficient for debugging analysis scripts. """ options = [] scripts = [] trace = None in_scripts = False for arg in args.split(): if not trace: trace = arg continue if arg == "--": if in_scripts: raise SyntaxError("cannot parse arguments") in_scripts = True continue if not in_scripts: options += [arg] else: scripts += [arg] if not trace: raise SyntaxError("no trace file given") results = self.broctl.process(trace, options, scripts) return results.ok def completedefault(self, text, line, begidx, endidx): # Commands that take a "<nodes>" argument. nodes_cmds = ["capstats", "check", "cleanup", "df", "diag", "netstats", "print", "restart", "start", "status", "stop", "top", "update", "peerstatus", "scripts"] args = line.split() if not args or args[0] not in nodes_cmds: return [] nodes = ["manager", "workers", "proxies", "all"] + self.node_names return [n for n in nodes if n.startswith(text)] def do_help(self, args): """Prints a brief summary of all commands understood by the shell.""" plugin_help = "" for (cmd, args, descr) in self.broctl.plugins.allCustomCommands(): if not plugin_help: plugin_help += "\nCommands provided by plugins:\n\n" if args: cmd = "%s %s" % (cmd, args) plugin_help += " %-32s - %s\n" % (cmd, descr) self.info( """ BroControl Version %s capstats [<nodes>] [<secs>] - Report interface statistics with capstats check [<nodes>] - Check configuration before installing it cleanup [--all] [<nodes>] - Delete working dirs (flush state) on nodes config - Print broctl configuration cron [--no-watch] - Perform jobs intended to run from cron cron enable|disable|? - Enable/disable \"cron\" jobs deploy - Check, install, and restart df [<nodes>] - Print nodes' current disk usage diag [<nodes>] - Output diagnostics for nodes exec <shell cmd> - Execute shell command on all hosts exit - Exit shell install - Update broctl installation/configuration netstats [<nodes>] - Print nodes' current packet counters nodes - Print node configuration peerstatus [<nodes>] - Print status of nodes' remote connections print <id> [<nodes>] - Print values of script variable at nodes process <trace> [<op>] [-- <sc>] - Run Bro (with options and scripts) on trace quit - Exit shell restart [--clean] [<nodes>] - Stop and then restart processing scripts [-c] [<nodes>] - List the Bro scripts the nodes will load start [<nodes>] - Start processing status [<nodes>] - Summarize node status stop [<nodes>] - Stop processing top [<nodes>] - Show Bro processes ala top update [<nodes>] - Update configuration of nodes on the fly %s""" % (version.VERSION, plugin_help)) def main(): # Undocumented option to print the documentation. if len(sys.argv) == 3 and sys.argv[1] == "--print-doc": from BroControl import printdoc printdoc.print_broctl_docs(sys.argv[2], BroCtlCmdLoop) return 0 interactive = True if len(sys.argv) > 1: interactive = False cmd = "" if len(sys.argv) == 2: cmd = sys.argv[1] try: loop = BroCtlCmdLoop(BroCtl, interactive, cmd) except Exception as e: print("Error: %s" % e, file=sys.stderr) return 1 if len(sys.argv) > 1: cmdline = " ".join(sys.argv[1:]) loop.precmd(cmdline) try: cmdsuccess = loop.onecmd(cmdline) except Exception as e: cmdsuccess = False print("Error: %s" % e, file=sys.stderr) except KeyboardInterrupt: cmdsuccess = False loop.postcmd(False, cmdline) else: try: cmdsuccess = loop.cmdloop("\nWelcome to BroControl %s\n\nType \"help\" for help.\n" % version.VERSION) except KeyboardInterrupt: cmdsuccess = False loop.finish() return not cmdsuccess if __name__ == "__main__": sys.exit(main())