mirror of
https://github.com/zeek/zeek.git
synced 2025-10-16 21:48:21 +00:00
513 lines
16 KiB
Python
513 lines
16 KiB
Python
#! /usr/bin/env python
|
|
#
|
|
# $Id: config.py 6948 2009-12-03 20:59:41Z robin $
|
|
#
|
|
# Functions to read and access the broctl configuration.
|
|
|
|
import os
|
|
import sys
|
|
import socket
|
|
import imp
|
|
import re
|
|
|
|
import ConfigParser
|
|
|
|
import options
|
|
import execute
|
|
import util
|
|
|
|
# One broctl node.
|
|
class Node:
|
|
|
|
# Valid tags in nodes file. The values will be stored
|
|
# in attributes of the same name.
|
|
_tags = { "type": 1, "host": 1, "interface": 1, "aux_scripts": 1, "brobase": 1, "ether": 1 }
|
|
|
|
def __init__(self, tag):
|
|
self.tag = tag
|
|
|
|
def __str__(self):
|
|
def fmt(v):
|
|
if type(v) == type([]):
|
|
v = ",".join(v)
|
|
return v
|
|
|
|
return ("%15s - " % self.tag) + " ".join(["%s=%s" % (k, fmt(self.__dict__[k])) for k in sorted(self.__dict__.keys())])
|
|
|
|
# Returns the working directory for this node.
|
|
def cwd(self):
|
|
return os.path.join(Config.spooldir, self.tag)
|
|
|
|
# Stores the nodes process ID.
|
|
def setPID(self, pid):
|
|
Config._setState("%s-pid" % self.tag, str(pid))
|
|
|
|
# Returns the stored process ID.
|
|
def getPID(self):
|
|
t = "%s-pid" % self.tag
|
|
if t in Config.state:
|
|
return Config.state[t]
|
|
return None
|
|
|
|
# Unsets the stored process ID.
|
|
def clearPID(self):
|
|
Config._setState("%s-pid" % self.tag, "")
|
|
|
|
# Mark node as having terminated unexpectedly.
|
|
def setCrashed(self):
|
|
Config._setState("%s-crashed" % self.tag, "1")
|
|
|
|
# Unsets the flag for unexpected termination.
|
|
def clearCrashed(self):
|
|
Config._setState("%s-crashed" % self.tag, "0")
|
|
|
|
# Returns true if node has terminated unexpectedly.
|
|
def hasCrashed(self):
|
|
t = "%s-crashed" % self.tag
|
|
return t in Config.state and Config.state[t] == "1"
|
|
|
|
# Set the Bro port this node is using.
|
|
def setPort(self, port):
|
|
Config._setState("%s-port" % self.tag, str(port))
|
|
|
|
# Get the Bro port this node is using.
|
|
def getPort(self):
|
|
t = "%s-port" % self.tag
|
|
return t in Config.state and int(Config.state[t]) or -1
|
|
|
|
# Class managing types of analysis.
|
|
class Analysis:
|
|
def __init__(self, cfgfile):
|
|
|
|
self.types = {}
|
|
cnt = 0
|
|
|
|
if not os.path.exists(cfgfile):
|
|
if Installing:
|
|
return
|
|
|
|
util.error("analysis configuration %s does not exist" % cfgfile)
|
|
|
|
for line in open(cfgfile):
|
|
cnt += 1
|
|
line = line.strip()
|
|
if not line or line.startswith("#"):
|
|
continue
|
|
|
|
f = line.split()
|
|
if len(f) < 2:
|
|
util.warn("cannot parse line %d in %s" % (cnt, cfgfile))
|
|
continue
|
|
|
|
type = f[0]
|
|
mechanism = f[1]
|
|
descr = ""
|
|
if len(f) > 2:
|
|
descr = " ".join(f[2:])
|
|
|
|
self.types[type] = (mechanism, descr)
|
|
|
|
# Returns true if we know this kind of analysis.
|
|
def isValid(self, type):
|
|
return type in self.types
|
|
|
|
# Returns true if type is enabled.
|
|
# Default is yes if we haven't disabled it.
|
|
def isEnabled(self, type):
|
|
tag = "analysis-%s" % type
|
|
try:
|
|
return int(Config.state[tag]) != 0
|
|
except KeyError:
|
|
return True
|
|
|
|
# Enable/disable type.
|
|
def toggle(self, type, enable=True):
|
|
tag = "analysis-%s" % type
|
|
if enable:
|
|
try:
|
|
del Config.state[tag]
|
|
except KeyError:
|
|
pass
|
|
else:
|
|
Config.state[tag] = 0
|
|
|
|
# Returns tuples (type, status, mechanism, descr) of all known analysis types.
|
|
# 'type' is tag for analysis.
|
|
# 'status' is True if analysis is activated.
|
|
# 'mechanism' gives the method how to control the analysis within Bro (see etc/analysis.dat).
|
|
# 'descr' is textual dscription for the kind of analysis.
|
|
def all(self):
|
|
result = []
|
|
keys = self.types.keys()
|
|
keys.sort()
|
|
for type in keys:
|
|
(mechanism, descr) = self.types[type]
|
|
result += [(type, self.isEnabled(type), mechanism, descr)]
|
|
return result
|
|
|
|
# Class storing the broctl configuration.
|
|
#
|
|
# This class provides access to four types of configuration/state:
|
|
#
|
|
# - the global broctl configuration from broctl.cfg
|
|
# - the node configuration from nodes.cfg
|
|
# - dynamic state variables which are kept across restarts in spool/broctl.dat
|
|
# - types of analysis which can be toggled via the shell
|
|
|
|
Config = None # Globally accessible instance of Configuration.
|
|
Installing = False
|
|
BroBase = None
|
|
MakeDestDir = None
|
|
|
|
class Configuration:
|
|
def __init__(self, config, basedir, distdir, version, standalone):
|
|
global Config
|
|
global Installing
|
|
|
|
Config = self
|
|
|
|
if "BROCTL_INSTALL" in os.environ:
|
|
Installing = True
|
|
|
|
global BroBase
|
|
BroBase = basedir
|
|
|
|
if "MAKE_DESTDIR" in os.environ:
|
|
global MakeDestDir
|
|
MakeDestDir = os.environ["MAKE_DESTDIR"]
|
|
|
|
self.config = {}
|
|
self.state = {}
|
|
|
|
# Read broctl.cfg.
|
|
self.config = self._readConfig(os.path.join(basedir, config))
|
|
|
|
# Set defaults for options we get passed in.
|
|
self._setOption("brobase", basedir)
|
|
self._setOption("distdir", distdir)
|
|
self._setOption("version", version)
|
|
self._setOption("standalone", standalone and "1" or "0")
|
|
|
|
# Initialize options.
|
|
for opt in options.options:
|
|
if not opt.dontinit:
|
|
self._setOption(opt.name.lower(), opt.default)
|
|
|
|
# Set defaults for options we derive dynamically.
|
|
self._setOption("mailto", "%s" % os.getenv("USER"))
|
|
self._setOption("mailfrom", "Big Brother <bro@%s>" % socket.gethostname())
|
|
self._setOption("home", os.getenv("HOME"))
|
|
self._setOption("mailalarmsto", self.config["mailto"])
|
|
|
|
# Determine operating system.
|
|
(success, output) = execute.captureCmd("uname")
|
|
if not success:
|
|
util.error("cannot run uname")
|
|
self._setOption("os", output[0].lower().strip())
|
|
|
|
# Find the time command (should be a GNU time for best results).
|
|
(success, output) = execute.captureCmd("which time")
|
|
self._setOption("time", output[0].lower().strip())
|
|
|
|
# Read nodes.cfg and broctl.dat.
|
|
self._readNodes()
|
|
self.readState()
|
|
|
|
# Setup the kinds of analyses which we support.
|
|
self._analysis = Analysis(self.analysiscfg)
|
|
|
|
# Make sure cron flag is cleared.
|
|
self.config["cron"] = "0"
|
|
|
|
# Provides access to the configuration options via the dereference operator.
|
|
# Lookups the attribute in broctl.cfg first, then in the dynamic variables from broctl.dat.
|
|
# Defaults to empty string for unknown options.
|
|
def __getattr__(self, attr):
|
|
if attr in self.config:
|
|
return self.config[attr]
|
|
if attr in self.state:
|
|
return self.state[attr]
|
|
return ""
|
|
|
|
# Returns True if attribute is defined.
|
|
def hasAttr(self, attr):
|
|
if attr in self.config:
|
|
return True
|
|
if attr in self.state:
|
|
return True
|
|
return False
|
|
|
|
# Returns a list of all broctl.cfg entries.
|
|
# Includes dynamic variables if dynamic is true.
|
|
def options(self, dynamic=True):
|
|
if dynamic:
|
|
return self.config.items() + self.state.items()
|
|
else:
|
|
return self.config.items()
|
|
|
|
# Returns a list of Nodes.
|
|
# - If tag is "global" or "all", all Nodes are returned if "expand_all" is true.
|
|
# If "expand_all" is false, returns an empty list in this case.
|
|
# - If tag is "proxies" or "proxy", all proxy Nodes are returned.
|
|
# - If tag is "workers" or "worker", all worker Nodes are returned.
|
|
# - If tag is "manager", the manager Node is returned.
|
|
def nodes(self, tag=None, expand_all=True):
|
|
nodes = []
|
|
type = None
|
|
|
|
if tag == "cluster" or tag == "all":
|
|
if not expand_all:
|
|
return []
|
|
|
|
tag = None
|
|
|
|
if tag == "proxies":
|
|
tag = "proxy"
|
|
|
|
if tag == "workers":
|
|
tag = "worker"
|
|
|
|
if ("scripts-%s" % tag) in self.config:
|
|
type = tag
|
|
|
|
for n in self.nodelist.values():
|
|
|
|
if type:
|
|
if type == n.type:
|
|
nodes += [n]
|
|
|
|
elif tag == n.tag or not tag:
|
|
nodes += [n]
|
|
|
|
nodes.sort(key=lambda n: (n.type, n.tag))
|
|
|
|
if not nodes and tag == "manager":
|
|
nodes = self.nodes("standalone")
|
|
|
|
return nodes
|
|
|
|
# Returns the manager Node.
|
|
def manager(self):
|
|
n = self.nodes("manager")
|
|
if n:
|
|
return n[0]
|
|
n = self.nodes("standalone")
|
|
if n:
|
|
return n[0]
|
|
return None
|
|
|
|
# Returns a list of nodes which is a subset of the result a similar call to
|
|
# nodes() would yield but within which each host appears only once.
|
|
def hosts(self, tag = None):
|
|
hosts = {}
|
|
for node in self.nodes(tag):
|
|
if not node.host in hosts:
|
|
hosts[node.host] = node
|
|
|
|
return hosts.values()
|
|
|
|
# Replace all occurences of "${option}", with option being either
|
|
# broctl.cfg option or a dynamic variable, with the corresponding value.
|
|
# Defaults to replacement with the empty string for unknown options.
|
|
def subst(self, str, make_dest=True):
|
|
while True:
|
|
m = re.search(r"(\$\{([A-Za-z]+)(:([^}]+))?\})", str)
|
|
if not m:
|
|
|
|
# This is a hack to support make's DESTDIR: if the env variable
|
|
# MAKE_DESTDIR is set, and the string we return starts with our
|
|
# installation prefix, we prepend the var's content. This it not
|
|
# totally perfect but should do the trick.
|
|
if Installing and MakeDestDir and MakeDestDir != "":
|
|
|
|
if make_dest and str.startswith(BroBase):
|
|
str = MakeDestDir + str
|
|
|
|
if not make_dest:
|
|
str = str.replace(MakeDestDir, "")
|
|
|
|
return str
|
|
|
|
key = m.group(2).lower()
|
|
if self.hasAttr(key):
|
|
value = self.__getattr__(key)
|
|
else:
|
|
value = m.group(4)
|
|
|
|
if not value:
|
|
value = ""
|
|
|
|
str = str[0:m.start(1)] + value + str[m.end(1):]
|
|
|
|
# Returns instance of class Analysis.
|
|
def analysis(self):
|
|
return self._analysis
|
|
|
|
# Parse nodes.cfg.
|
|
def _readNodes(self):
|
|
self.nodelist = {}
|
|
config = ConfigParser.SafeConfigParser()
|
|
if not config.read(self.nodecfg) and not Installing:
|
|
util.error("cannot read '%s'" % self.nodecfg)
|
|
|
|
manager = False
|
|
proxy = False
|
|
standalone = False
|
|
|
|
file = self.nodecfg
|
|
|
|
counts = {}
|
|
for sec in config.sections():
|
|
|
|
node = Node(sec)
|
|
self.nodelist[sec] = node
|
|
|
|
for (key, val) in config.items(sec):
|
|
if not key in Node._tags:
|
|
util.warn("%s: unknown key '%s' in section '%s'" % (file, key, sec))
|
|
continue
|
|
|
|
if key == "type":
|
|
# We determine which types are valid by checking for having an
|
|
# option specifying which scripts to use for it.
|
|
cfg = "scripts-%s" % val
|
|
if not cfg in self.config:
|
|
util.error("%s: unknown type '%s' in section '%s'" % (file, val, sec))
|
|
|
|
self.nodelist[sec].scripts = self.config[cfg].split()
|
|
|
|
if val == "manager":
|
|
if manager:
|
|
util.error("only one manager can be defined")
|
|
manager = True
|
|
|
|
if val == "proxy":
|
|
proxy = True
|
|
|
|
if val == "standalone":
|
|
standalone = True
|
|
|
|
node.__dict__[key] = val
|
|
|
|
try:
|
|
node.addr = socket.gethostbyname(node.host)
|
|
except AttributeError:
|
|
util.error("%s: no host given in section '%s'" % (file, sec))
|
|
except socket.gaierror, e:
|
|
util.error("%s: unknown host '%s' in section '%s' [%s]" % (file, node.host, sec, e.args[1]))
|
|
|
|
# Each node gets a number unique across its type.
|
|
type = self.nodelist[sec].type
|
|
try:
|
|
counts[type] += 1
|
|
except KeyError:
|
|
counts[type] = 1
|
|
|
|
node.count = counts[type]
|
|
|
|
if self.nodelist:
|
|
|
|
if not standalone:
|
|
if not manager:
|
|
util.error("%s: no manager defined" % file)
|
|
|
|
if not proxy:
|
|
util.error("%s: no proxy defined" % file)
|
|
|
|
else:
|
|
if len(self.nodelist) > 1:
|
|
util.error("%s: more than one node defined in stand-alone setup" % file)
|
|
|
|
for n in self.nodelist.values():
|
|
if n.type == "manager":
|
|
if not execute.isLocal(n):
|
|
util.error("script must be run on manager node")
|
|
|
|
if n.addr == "127.0.0.1" and n.type != "standalone":
|
|
util.error("cannot use localhost/127.0.0.1 for manager host in nodes configuration")
|
|
|
|
|
|
# Parses broctl.cfg and returns a dictionary of all entries.
|
|
def _readConfig(self, file):
|
|
config = {}
|
|
try:
|
|
for line in open(file):
|
|
|
|
line = line.strip()
|
|
if not line or line.startswith("#"):
|
|
continue
|
|
|
|
args = line.split("=")
|
|
if len(args) != 2:
|
|
util.error("%s: syntax error '%s'" % (file, line))
|
|
|
|
(key, val) = args
|
|
key = key.strip().lower()
|
|
val = val.strip()
|
|
|
|
config[key] = val
|
|
|
|
except IOError, e:
|
|
if not Installing:
|
|
util.error("cannot read '%s'" % file)
|
|
|
|
return config
|
|
|
|
# Initialize a global option if not already set.
|
|
def _setOption(self, val, key):
|
|
if not val in self.config:
|
|
self.config[val] = self.subst(key)
|
|
|
|
# Set a dynamic state variable.
|
|
def _setState(self, val, key):
|
|
self.state[val] = key
|
|
|
|
# Read dynamic state variables from {$spooldir}/broctl.dat .
|
|
def readState(self):
|
|
self.state = self._readConfig(self.statefile)
|
|
|
|
# Write the dynamic state variables into {$spooldir}/broctl.dat .
|
|
def writeState(self):
|
|
try:
|
|
out = open(self.statefile, "w")
|
|
except IOError:
|
|
if not Installing:
|
|
util.warn("can't write '%s'" % self.statefile)
|
|
return
|
|
|
|
print >>out, "# Automatically generated. Do not edit.\n"
|
|
|
|
for (key, val) in self.state.items():
|
|
print >>out, "%s = %s" % (key, self.subst(str(val), make_dest=False))
|
|
|
|
# Runs Bro to get its version numbers.
|
|
def determineBroVersion(self):
|
|
version = None
|
|
bro = os.path.join(self.distdir, "src/bro")
|
|
if execute.exists(None, bro):
|
|
(success, output) = execute.captureCmd("%s -v 2>&1" % bro)
|
|
if success:
|
|
version = output[0]
|
|
|
|
if not version:
|
|
# Ok if it's already set.
|
|
if "broversion" in self.state:
|
|
return
|
|
|
|
util.error("cannot find Bro binary to determine version")
|
|
|
|
m = re.search(".* version ([^ ]*).*$", version)
|
|
if not m:
|
|
util.error("cannot determine Bro version [%s]" % version.strip())
|
|
|
|
version = m.group(1)
|
|
if version.endswith("-debug"):
|
|
version = version[:-6]
|
|
|
|
self.state["broversion"] = version
|
|
self.state["bro"] = self.subst("${bindir}/bro")
|
|
|
|
# Returns true if we're running via "make install"
|
|
def installing(self):
|
|
return Installing
|
|
|