You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
499 lines
19 KiB
Python
499 lines
19 KiB
Python
############################################################################
|
|
# Copyright (C) gempa GmbH #
|
|
# All rights reserved. #
|
|
# Contact: gempa GmbH (seiscomp-dev@gempa.de) #
|
|
# #
|
|
# GNU Affero General Public License Usage #
|
|
# This file may be used under the terms of the GNU Affero #
|
|
# Public License version 3.0 as published by the Free Software Foundation #
|
|
# and appearing in the file LICENSE included in the packaging of this #
|
|
# file. Please review the following information to ensure the GNU Affero #
|
|
# Public License version 3.0 requirements will be met: #
|
|
# https://www.gnu.org/licenses/agpl-3.0.html. #
|
|
# #
|
|
# Other Usage #
|
|
# Alternatively, this file may be used in accordance with the terms and #
|
|
# conditions contained in a signed written agreement between you and #
|
|
# gempa GmbH. #
|
|
############################################################################
|
|
|
|
import os, time, sys
|
|
import seiscomp.core, seiscomp.client, seiscomp.datamodel
|
|
import seiscomp.io, seiscomp.system
|
|
|
|
|
|
def collectParams(container):
|
|
params = {}
|
|
for i in range(container.groupCount()):
|
|
params.update(collectParams(container.group(i)))
|
|
for i in range(container.structureCount()):
|
|
params.update(collectParams(container.structure(i)))
|
|
for i in range(container.parameterCount()):
|
|
p = container.parameter(i)
|
|
if p.symbol.stage == seiscomp.system.Environment.CS_UNDEFINED:
|
|
continue
|
|
params[p.variableName] = ",".join(p.symbol.values)
|
|
|
|
return params
|
|
|
|
|
|
def collect(idset, paramSetID):
|
|
paramSet = seiscomp.datamodel.ParameterSet.Find(paramSetID)
|
|
if not paramSet:
|
|
return
|
|
idset[paramSet.publicID()] = 1
|
|
if not paramSet.baseID():
|
|
return
|
|
collect(idset, paramSet.baseID())
|
|
|
|
|
|
def sync(paramSet, params):
|
|
obsoleteParams = []
|
|
seenParams = {}
|
|
i = 0
|
|
while i < paramSet.parameterCount():
|
|
p = paramSet.parameter(i)
|
|
if p.name() in params:
|
|
if p.name() in seenParams:
|
|
# Multiple parameter definitions with same name
|
|
sys.stderr.write(
|
|
"- %s:%s / duplicate parameter name\n" % (p.publicID(), p.name()))
|
|
p.detach()
|
|
continue
|
|
seenParams[p.name()] = 1
|
|
val = params[p.name()]
|
|
if val != p.value():
|
|
p.setValue(val)
|
|
p.update()
|
|
else:
|
|
obsoleteParams.append(p)
|
|
i = i + 1
|
|
|
|
for p in obsoleteParams:
|
|
p.detach()
|
|
|
|
for key, val in list(params.items()):
|
|
if key in seenParams:
|
|
continue
|
|
p = seiscomp.datamodel.Parameter.Create()
|
|
p.setName(key)
|
|
p.setValue(val)
|
|
paramSet.add(p)
|
|
|
|
|
|
class ConfigDBUpdater(seiscomp.client.Application):
|
|
def __init__(self, argc, argv):
|
|
seiscomp.client.Application.__init__(self, argc, argv)
|
|
self.setLoggingToStdErr(True)
|
|
self.setMessagingEnabled(True)
|
|
self.setDatabaseEnabled(True, True)
|
|
self.setAutoApplyNotifierEnabled(False)
|
|
self.setInterpretNotifierEnabled(False)
|
|
self.setMessagingUsername("_sccfgupd_")
|
|
self.setLoadConfigModuleEnabled(True)
|
|
# Load all configuration modules
|
|
self.setConfigModuleName("")
|
|
self.setPrimaryMessagingGroup(
|
|
seiscomp.client.Protocol.LISTENER_GROUP)
|
|
|
|
self._outputFile = ""
|
|
self._keyDir = None
|
|
|
|
def createCommandLineDescription(self):
|
|
self.commandline().addGroup("Input")
|
|
self.commandline().addStringOption("Input", "key-dir",
|
|
"Overrides the location of the default key directory ($SEISCOMP_ROOT/etc/key)")
|
|
self.commandline().addGroup("Output")
|
|
self.commandline().addStringOption("Output", "output,o",
|
|
"If given, an output XML file is generated")
|
|
|
|
def validateParameters(self):
|
|
if not seiscomp.client.Application.validateParameters(self):
|
|
return False
|
|
|
|
try:
|
|
self._outputFile = self.commandline().optionString("output")
|
|
# Switch to offline mode
|
|
self.setMessagingEnabled(False)
|
|
self.setDatabaseEnabled(False, False)
|
|
self.setLoadConfigModuleEnabled(False)
|
|
except:
|
|
pass
|
|
|
|
try:
|
|
self._keyDir = self.commandline().optionString("key-dir")
|
|
except:
|
|
pass
|
|
|
|
return True
|
|
|
|
def init(self):
|
|
if not seiscomp.client.Application.init(self):
|
|
return False
|
|
|
|
# Initialize the basic directories
|
|
filebase = seiscomp.system.Environment.Instance().installDir()
|
|
descdir = os.path.join(filebase, "etc", "descriptions")
|
|
|
|
# Load definitions of the configuration schema
|
|
defs = seiscomp.system.SchemaDefinitions()
|
|
if not defs.load(descdir):
|
|
sys.stderr.write("Error: could not read descriptions\n")
|
|
return False
|
|
|
|
if defs.moduleCount() == 0:
|
|
sys.stderr.write("Warning: no modules defined, nothing to do\n")
|
|
return False
|
|
|
|
# Create a model from the schema and read its configuration including
|
|
# all bindings.
|
|
model = seiscomp.system.Model()
|
|
if self._keyDir:
|
|
model.keyDirOverride = self._keyDir
|
|
model.create(defs)
|
|
model.readConfig()
|
|
|
|
# Find all binding mods for trunk. Bindings of modules where standalone
|
|
# is set to true are ignored. They are supposed to handle their bindings
|
|
# on their own.
|
|
self.bindingMods = []
|
|
for i in range(defs.moduleCount()):
|
|
mod = defs.module(i)
|
|
# Ignore stand alone modules (eg seedlink, slarchive, ...) as they
|
|
# are not using the trunk libraries and don't need database
|
|
# configurations
|
|
if mod.isStandalone():
|
|
continue
|
|
self.bindingMods.append(mod.name)
|
|
|
|
if len(self.bindingMods) == 0:
|
|
sys.stderr.write(
|
|
"Warning: no usable modules found, nothing to do\n")
|
|
return False
|
|
|
|
self.stationSetups = {}
|
|
|
|
# Read bindings
|
|
for m in self.bindingMods:
|
|
mod = model.module(m)
|
|
if not mod:
|
|
sys.stderr.write("Warning: module %s not assigned\n" % m)
|
|
continue
|
|
if len(mod.bindings) == 0:
|
|
continue
|
|
|
|
# Rename global to default for being compatible with older
|
|
# releases
|
|
if m == "global":
|
|
m = "default"
|
|
|
|
sys.stderr.write("+ %s\n" % m)
|
|
for staid in list(mod.bindings.keys()):
|
|
binding = mod.getBinding(staid)
|
|
if not binding:
|
|
continue
|
|
# sys.stderr.write(" + %s.%s\n" % (staid.networkCode, staid.stationCode))
|
|
params = {}
|
|
for i in range(binding.sectionCount()):
|
|
params.update(collectParams(binding.section(i)))
|
|
key = (staid.networkCode, staid.stationCode)
|
|
if not key in self.stationSetups:
|
|
self.stationSetups[key] = {}
|
|
self.stationSetups[key][m] = params
|
|
sys.stderr.write(" + read %d stations\n" %
|
|
len(list(mod.bindings.keys())))
|
|
|
|
return True
|
|
|
|
def printUsage(self):
|
|
|
|
print('''Usage:
|
|
bindings2cfg [options]
|
|
|
|
Dump global and module bindings configurations''')
|
|
|
|
seiscomp.client.Application.printUsage(self)
|
|
|
|
print('''Examples:
|
|
Write bindings configuration from key directory to a configuration XML file:
|
|
bindings2cfg --key-dir ./etc/key -o config.xml
|
|
|
|
Write bindings configuration from key directory to the seiscomp local database
|
|
bindings2cfg --key-dir ./etc/key -d mysql://sysop:sysop@localhost/seiscomp
|
|
''')
|
|
|
|
return True
|
|
|
|
def send(self, *args):
|
|
'''
|
|
A simple wrapper that sends a message and tries to resend it in case of
|
|
an error.
|
|
'''
|
|
while not self.connection().send(*args):
|
|
sys.stderr.write("Warning: sending failed, retrying\n")
|
|
time.sleep(1)
|
|
|
|
def run(self):
|
|
'''
|
|
Reimplements the main loop of the application. This methods collects
|
|
all bindings and updates the database. It searches for already existing
|
|
objects and updates them or creates new objects. Objects that is didn't
|
|
touched are removed. This tool is the only one that should writes the
|
|
configuration into the database and thus manages the content.
|
|
'''
|
|
config = seiscomp.client.ConfigDB.Instance().config()
|
|
if config is None:
|
|
config = seiscomp.datamodel.Config()
|
|
|
|
configMod = None
|
|
obsoleteConfigMods = []
|
|
|
|
if not self._outputFile:
|
|
moduleName = self.name()
|
|
seiscomp.datamodel.Notifier.Enable()
|
|
else:
|
|
moduleName = "trunk"
|
|
|
|
configID = "Config/%s" % moduleName
|
|
|
|
for i in range(config.configModuleCount()):
|
|
if config.configModule(i).publicID() != configID:
|
|
obsoleteConfigMods.append(config.configModule(i))
|
|
else:
|
|
configMod = config.configModule(i)
|
|
|
|
# Remove obsolete config modules
|
|
for cm in obsoleteConfigMods:
|
|
sys.stderr.write(
|
|
"- %s / obsolete module configuration\n" % cm.name())
|
|
ps = seiscomp.datamodel.ParameterSet.Find(cm.parameterSetID())
|
|
if not ps is None:
|
|
ps.detach()
|
|
cm.detach()
|
|
del obsoleteConfigMods
|
|
|
|
if not configMod:
|
|
configMod = seiscomp.datamodel.ConfigModule.Find(configID)
|
|
if configMod is None:
|
|
configMod = seiscomp.datamodel.ConfigModule.Create(configID)
|
|
config.add(configMod)
|
|
else:
|
|
if configMod.name() != moduleName:
|
|
configMod.update()
|
|
if not configMod.enabled():
|
|
configMod.update()
|
|
|
|
configMod.setName(moduleName)
|
|
configMod.setEnabled(True)
|
|
else:
|
|
if configMod.name() != moduleName:
|
|
configMod.setName(moduleName)
|
|
configMod.update()
|
|
paramSet = seiscomp.datamodel.ParameterSet.Find(
|
|
configMod.parameterSetID())
|
|
if configMod.parameterSetID():
|
|
configMod.setParameterSetID("")
|
|
configMod.update()
|
|
|
|
if not paramSet is None:
|
|
paramSet.detach()
|
|
|
|
stationConfigs = {}
|
|
obsoleteStationConfigs = []
|
|
|
|
for i in range(configMod.configStationCount()):
|
|
cs = configMod.configStation(i)
|
|
if (cs.networkCode(), cs.stationCode()) in self.stationSetups:
|
|
stationConfigs[(cs.networkCode(), cs.stationCode())] = cs
|
|
else:
|
|
obsoleteStationConfigs.append(cs)
|
|
|
|
for cs in obsoleteStationConfigs:
|
|
sys.stderr.write("- %s/%s/%s / obsolete station configuration\n" %
|
|
(configMod.name(), cs.networkCode(), cs.stationCode()))
|
|
cs.detach()
|
|
del obsoleteStationConfigs
|
|
|
|
for staid, setups in list(self.stationSetups.items()):
|
|
try:
|
|
cs = stationConfigs[staid]
|
|
except:
|
|
cs = seiscomp.datamodel.ConfigStation.Find(
|
|
"Config/%s/%s/%s" % (configMod.name(), staid[0], staid[1]))
|
|
if not cs:
|
|
cs = seiscomp.datamodel.ConfigStation.Create(
|
|
"Config/%s/%s/%s" % (configMod.name(), staid[0], staid[1]))
|
|
configMod.add(cs)
|
|
cs.setNetworkCode(staid[0])
|
|
cs.setStationCode(staid[1])
|
|
cs.setEnabled(True)
|
|
|
|
ci = seiscomp.datamodel.CreationInfo()
|
|
ci.setCreationTime(seiscomp.core.Time.GMT())
|
|
ci.setAgencyID(self.agencyID())
|
|
ci.setAuthor(self.name())
|
|
cs.setCreationInfo(ci)
|
|
|
|
stationSetups = {}
|
|
obsoleteSetups = []
|
|
for i in range(cs.setupCount()):
|
|
setup = cs.setup(i)
|
|
if setup.name() in setups:
|
|
stationSetups[setup.name()] = setup
|
|
else:
|
|
obsoleteSetups.append(setup)
|
|
|
|
for s in obsoleteSetups:
|
|
sys.stderr.write("- %s/%s/%s/%s / obsolete station setup\n" %
|
|
(configMod.name(), cs.networkCode(), cs.stationCode(), setup.name()))
|
|
ps = seiscomp.datamodel.ParameterSet.Find(s.parameterSetID())
|
|
if ps:
|
|
ps.detach()
|
|
s.detach()
|
|
del obsoleteSetups
|
|
|
|
newParamSets = {}
|
|
globalSet = ""
|
|
for mod, params in list(setups.items()):
|
|
try:
|
|
setup = stationSetups[mod]
|
|
except:
|
|
setup = seiscomp.datamodel.Setup()
|
|
setup.setName(mod)
|
|
setup.setEnabled(True)
|
|
cs.add(setup)
|
|
|
|
paramSet = seiscomp.datamodel.ParameterSet.Find(
|
|
setup.parameterSetID())
|
|
if not paramSet:
|
|
paramSet = seiscomp.datamodel.ParameterSet.Find("ParameterSet/%s/Station/%s/%s/%s" % (
|
|
configMod.name(), cs.networkCode(), cs.stationCode(), setup.name()))
|
|
if not paramSet:
|
|
paramSet = seiscomp.datamodel.ParameterSet.Create(
|
|
"ParameterSet/%s/Station/%s/%s/%s" % (configMod.name(), cs.networkCode(), cs.stationCode(), setup.name()))
|
|
config.add(paramSet)
|
|
paramSet.setModuleID(configMod.publicID())
|
|
paramSet.setCreated(seiscomp.core.Time.GMT())
|
|
newParamSets[paramSet.publicID()] = 1
|
|
setup.setParameterSetID(paramSet.publicID())
|
|
if mod in stationSetups:
|
|
setup.update()
|
|
elif paramSet.moduleID() != configMod.publicID():
|
|
paramSet.setModuleID(configMod.publicID())
|
|
paramSet.update()
|
|
|
|
# Synchronize existing parameterset with the new parameters
|
|
sync(paramSet, params)
|
|
|
|
if setup.name() == "default":
|
|
globalSet = paramSet.publicID()
|
|
|
|
for i in range(cs.setupCount()):
|
|
setup = cs.setup(i)
|
|
paramSet = seiscomp.datamodel.ParameterSet.Find(
|
|
setup.parameterSetID())
|
|
if not paramSet:
|
|
continue
|
|
|
|
if paramSet.publicID() != globalSet and paramSet.baseID() != globalSet:
|
|
paramSet.setBaseID(globalSet)
|
|
if not paramSet.publicID() in newParamSets:
|
|
paramSet.update()
|
|
|
|
# Collect unused ParameterSets
|
|
usedSets = {}
|
|
for i in range(config.configModuleCount()):
|
|
configMod = config.configModule(i)
|
|
for j in range(configMod.configStationCount()):
|
|
cs = configMod.configStation(j)
|
|
for k in range(cs.setupCount()):
|
|
setup = cs.setup(k)
|
|
collect(usedSets, setup.parameterSetID())
|
|
|
|
# Delete unused ParameterSets
|
|
i = 0
|
|
while i < config.parameterSetCount():
|
|
paramSet = config.parameterSet(i)
|
|
if not paramSet.publicID() in usedSets:
|
|
sys.stderr.write("- %s / obsolete parameter set\n" %
|
|
paramSet.publicID())
|
|
paramSet.detach()
|
|
else:
|
|
i = i + 1
|
|
|
|
# Generate output file and exit if configured
|
|
if self._outputFile:
|
|
ar = seiscomp.io.XMLArchive()
|
|
if not ar.create(self._outputFile):
|
|
sys.stderr.write(
|
|
"Failed to created output file: %s" % self._outputFile)
|
|
return False
|
|
|
|
ar.setFormattedOutput(True)
|
|
ar.writeObject(config)
|
|
ar.close()
|
|
return True
|
|
|
|
ncount = seiscomp.datamodel.Notifier.Size()
|
|
if ncount > 0:
|
|
sys.stderr.write("+ synchronize %d change" % ncount)
|
|
if ncount > 1:
|
|
sys.stderr.write("s")
|
|
sys.stderr.write("\n")
|
|
else:
|
|
sys.stderr.write("- database is already up-to-date\n")
|
|
return True
|
|
|
|
cfgmsg = seiscomp.datamodel.ConfigSyncMessage(False)
|
|
cfgmsg.setCreationInfo(seiscomp.datamodel.CreationInfo())
|
|
cfgmsg.creationInfo().setCreationTime(seiscomp.core.Time.GMT())
|
|
cfgmsg.creationInfo().setAuthor(self.author())
|
|
cfgmsg.creationInfo().setAgencyID(self.agencyID())
|
|
self.send(seiscomp.client.Protocol.STATUS_GROUP, cfgmsg)
|
|
|
|
# Send messages in a batch of 100 notifiers to not exceed the
|
|
# maximum allowed message size of ~300kb.
|
|
msg = seiscomp.datamodel.NotifierMessage()
|
|
nmsg = seiscomp.datamodel.Notifier.GetMessage(False)
|
|
count = 0
|
|
sys.stderr.write("\r + sending notifiers: %d%%" %
|
|
(count * 100 / ncount))
|
|
sys.stderr.flush()
|
|
while nmsg:
|
|
for o in nmsg:
|
|
n = seiscomp.datamodel.Notifier.Cast(o)
|
|
if n:
|
|
msg.attach(n)
|
|
|
|
if msg.size() >= 100:
|
|
count += msg.size()
|
|
self.send("CONFIG", msg)
|
|
msg.clear()
|
|
sys.stderr.write("\r + sending notifiers: %d%%" %
|
|
(count * 100 / ncount))
|
|
sys.stderr.flush()
|
|
|
|
nmsg = seiscomp.datamodel.Notifier.GetMessage(False)
|
|
|
|
if msg.size() > 0:
|
|
count += msg.size()
|
|
self.send("CONFIG", msg)
|
|
msg.clear()
|
|
sys.stderr.write("\r + sending notifiers: %d%%" %
|
|
(count * 100 / ncount))
|
|
sys.stderr.flush()
|
|
|
|
sys.stderr.write("\n")
|
|
|
|
# Notify about end of synchronization
|
|
cfgmsg.creationInfo().setCreationTime(seiscomp.core.Time.GMT())
|
|
cfgmsg.isFinished = True
|
|
self.send(seiscomp.client.Protocol.STATUS_GROUP, cfgmsg)
|
|
|
|
return True
|
|
|
|
|
|
def main():
|
|
app = ConfigDBUpdater(len(sys.argv), sys.argv)
|
|
return app()
|