change cfg file format

config file format change:

The section names no longer contain a space, the are either
bare module names or 'NODE' or 'INTERFACE' (capitalized in order to
distingish from module names).

The present code still accepts the old form.

Moving to the 'toml' format was considered too, but this needs some
more investigations. The necessary code changes would be limited
to the method Server.loadCfgFile.

Change-Id: I6020058c9dcc4c1cbf38f5b9e8f67e9aad670183
Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/23031
Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
Tested-by: JenkinsCodeReview <bjoern_pedersen@frm2.tum.de>
This commit is contained in:
zolliker 2020-05-01 15:02:41 +02:00
parent bdb754976f
commit 64a3bf534b
4 changed files with 145 additions and 167 deletions

View File

@ -1,123 +1,122 @@
[node PPMS.psi.ch] [NODE]
id = PPMS.psi.ch
description = PPMS at PSI description = PPMS at PSI
[interface tcp] [INTERFACE]
type = tcp uri = tcp://5000
bindto = 0.0.0.0
bindport = 5000
[module tt] [tt]
class = secop_psi.ppms.Temp class = secop_psi.ppms.Temp
description = main temperature description = main temperature
iodev = ppms iodev = ppms
[module mf] [mf]
class = secop_psi.ppms.Field class = secop_psi.ppms.Field
target.min = -9 target.min = -9
target.max = 9 target.max = 9
.description = magnetic field .description = magnetic field
.iodev = ppms .iodev = ppms
[module pos] [pos]
class = secop_psi.ppms.Position class = secop_psi.ppms.Position
.description = sample rotator .description = sample rotator
.iodev = ppms .iodev = ppms
[module lev] [lev]
class = secop_psi.ppms.Level class = secop_psi.ppms.Level
.description = helium level .description = helium level
.iodev = ppms .iodev = ppms
[module chamber] [chamber]
class = secop_psi.ppms.Chamber class = secop_psi.ppms.Chamber
.description = chamber state .description = chamber state
.iodev = ppms .iodev = ppms
[module r1] [r1]
class = secop_psi.ppms.BridgeChannel class = secop_psi.ppms.BridgeChannel
.description = resistivity channel 1 .description = resistivity channel 1
.no = 1 .no = 1
value.unit = Ohm value.unit = Ohm
.iodev = ppms .iodev = ppms
[module r2] [r2]
class = secop_psi.ppms.BridgeChannel class = secop_psi.ppms.BridgeChannel
.description = resistivity channel 2 .description = resistivity channel 2
.no = 2 .no = 2
value.unit = Ohm value.unit = Ohm
.iodev = ppms .iodev = ppms
[module r3] [r3]
class = secop_psi.ppms.BridgeChannel class = secop_psi.ppms.BridgeChannel
.description = resistivity channel 3 .description = resistivity channel 3
.no = 3 .no = 3
value.unit = Ohm value.unit = Ohm
.iodev = ppms .iodev = ppms
[module r4] [r4]
class = secop_psi.ppms.BridgeChannel class = secop_psi.ppms.BridgeChannel
.description = resistivity channel 4 .description = resistivity channel 4
.no = 4 .no = 4
value.unit = Ohm value.unit = Ohm
.iodev = ppms .iodev = ppms
[module i1] [i1]
class = secop_psi.ppms.Channel class = secop_psi.ppms.Channel
.description = current channel 1 .description = current channel 1
.no = 1 .no = 1
value.unit = uA value.unit = uA
.iodev = ppms .iodev = ppms
[module i2] [i2]
class = secop_psi.ppms.Channel class = secop_psi.ppms.Channel
.description = current channel 2 .description = current channel 2
.no = 2 .no = 2
value.unit = uA value.unit = uA
.iodev = ppms .iodev = ppms
[module i3] [i3]
class = secop_psi.ppms.Channel class = secop_psi.ppms.Channel
.description = current channel 3 .description = current channel 3
.no = 3 .no = 3
value.unit = uA value.unit = uA
.iodev = ppms .iodev = ppms
[module i4] [i4]
class = secop_psi.ppms.Channel class = secop_psi.ppms.Channel
.description = current channel 4 .description = current channel 4
.no = 4 .no = 4
value.unit = uA value.unit = uA
.iodev = ppms .iodev = ppms
[module v1] [v1]
class = secop_psi.ppms.DriverChannel class = secop_psi.ppms.DriverChannel
.description = voltage channel 1 .description = voltage channel 1
.no = 1 .no = 1
value.unit = V value.unit = V
.iodev = ppms .iodev = ppms
[module v2] [v2]
class = secop_psi.ppms.DriverChannel class = secop_psi.ppms.DriverChannel
.description = voltage channel 2 .description = voltage channel 2
.no = 2 .no = 2
value.unit = V value.unit = V
.iodev = ppms .iodev = ppms
[module tv] [tv]
class = secop_psi.ppms.UserChannel class = secop_psi.ppms.UserChannel
.description = VTI temperature .description = VTI temperature
enabled = 1 enabled = 1
value.unit = K value.unit = K
.iodev = ppms .iodev = ppms
[module ts] [ts]
class = secop_psi.ppms.UserChannel class = secop_psi.ppms.UserChannel
.description = sample temperature .description = sample temperature
enabled = 1 enabled = 1
value.unit = K value.unit = K
.iodev = ppms .iodev = ppms
[module ppms] [ppms]
class = secop_psi.ppms.Main class = secop_psi.ppms.Main
.description = the main and poller module .description = the main and poller module
.class_id = QD.MULTIVU.PPMS.1 .class_id = QD.MULTIVU.PPMS.1

View File

@ -64,7 +64,7 @@ class Dispatcher:
def __init__(self, name, logger, options, srv): def __init__(self, name, logger, options, srv):
# to avoid errors, we want to eat all options here # to avoid errors, we want to eat all options here
self.equipment_id = name self.equipment_id = options.pop('id', name)
self.nodeprops = {} self.nodeprops = {}
for k in list(options): for k in list(options):
self.nodeprops[k] = options.pop(k) self.nodeprops[k] = options.pop(k)

View File

@ -26,11 +26,11 @@ import socket
import collections import collections
import socketserver import socketserver
from secop.datatypes import StringType, IntRange, BoolType from secop.datatypes import StringType, BoolType
from secop.errors import SECoPError from secop.errors import SECoPError
from secop.lib import formatException, \ from secop.lib import formatException, \
formatExtendedStack, formatExtendedTraceback formatExtendedStack, formatExtendedTraceback
from secop.properties import HasProperties, Property from secop.properties import Property
from secop.protocol.interface import decode_msg, encode_msg_frame, get_msg from secop.protocol.interface import decode_msg, encode_msg_frame, get_msg
from secop.protocol.messages import ERRORPREFIX, \ from secop.protocol.messages import ERRORPREFIX, \
HELPREPLY, HELPREQUEST, HelpMessage HELPREPLY, HELPREQUEST, HelpMessage
@ -187,42 +187,27 @@ class TCPRequestHandler(socketserver.BaseRequestHandler):
self.request.close() self.request.close()
class TCPServer(HasProperties, socketserver.ThreadingTCPServer): class TCPServer(socketserver.ThreadingTCPServer):
daemon_threads = True daemon_threads = True
allow_reuse_address = True allow_reuse_address = True
properties = { # for cfg-editor
'bindto': Property('hostname or ip address for binding', StringType(), configurables = {
default='localhost:%d' % DEF_PORT, export=False), 'uri': Property('hostname or ip address for binding', StringType(),
'bindport': Property('port number to bind', IntRange(1, 65535), default='tcp://%d' % DEF_PORT, export=False),
default=DEF_PORT, export=False),
'detailed_errors': Property('Flag to enable detailed Errorreporting.', BoolType(), 'detailed_errors': Property('Flag to enable detailed Errorreporting.', BoolType(),
default=False, export=False), default=False, export=False),
} }
# XXX: create configurables from Metaclass!
configurables = properties
def __init__(self, name, logger, options, srv): # pylint: disable=super-init-not-called def __init__(self, name, logger, options, srv): # pylint: disable=super-init-not-called
self.dispatcher = srv.dispatcher self.dispatcher = srv.dispatcher
self.name = name self.name = name
self.log = logger self.log = logger
# do not call HasProperties.__init__, as this will supercall ThreadingTCPServer port = int(options.pop('uri').split('://', 1)[-1])
self.initProperties() self.detailed_errors = options.pop('detailed_errors', False)
bindto = options.pop('bindto', 'localhost')
bindport = int(options.pop('bindport', DEF_PORT))
detailed_errors = options.pop('detailed_errors', False)
if ':' in bindto:
bindto, _port = bindto.rsplit(':')
bindport = int(_port)
self.setProperty('bindto', bindto)
self.setProperty('bindport', bindport)
self.setProperty('detailed_errors', detailed_errors)
self.checkProperties()
self.allow_reuse_address = True self.allow_reuse_address = True
self.log.info("TCPServer %s binding to %s:%d" % (name, self.bindto, self.bindport)) self.log.info("TCPServer %s binding to port %d" % (name, port))
socketserver.ThreadingTCPServer.__init__( socketserver.ThreadingTCPServer.__init__(
self, (self.bindto, self.bindport), TCPRequestHandler, bind_and_activate=True) self, ('0.0.0.0', port), TCPRequestHandler, bind_and_activate=True)
self.log.info("TCPServer initiated") self.log.info("TCPServer initiated")

View File

@ -39,7 +39,7 @@ except ImportError:
DaemonContext = None DaemonContext = None
from secop.errors import ConfigError from secop.errors import ConfigError
from secop.lib import formatException, get_class, getGeneralConfig, mkthread from secop.lib import formatException, get_class, getGeneralConfig
from secop.modules import Attached from secop.modules import Attached
try: try:
@ -48,87 +48,106 @@ except ImportError:
systemd = None systemd = None
class Server: class Server:
# list allowed section prefixes INTERFACES = {
# if mapped dict does not exist -> section need a 'class' option 'tcp': 'protocol.interface.tcp.TCPServer',
# otherwise a 'type' option is evaluated and the class from the mapping dict used }
#
# IMPORTANT: keep the order! (node MUST be first, as the others are referencing it!)
CFGSECTIONS = [
# section_prefix, default type, mapping of selectable classes
('node', 'std', {'std': "protocol.dispatcher.Dispatcher",
'router': 'protocol.router.Router'}),
('module', None, None),
('interface', "tcp", {"tcp": "protocol.interface.tcp.TCPServer"}),
]
_restart = True _restart = True
def __init__(self, name, parent_logger=None, cfgfiles=None, interface=None, testonly=False): def __init__(self, name, parent_logger, cfgfiles=None, interface=None, testonly=False):
"""initialize server """initialize server
the configuration is taken either from <name>.cfg or from cfgfiles Arguments:
if cfgfiles is given, also the serverport has to be given. - name: the node name
interface is either an uri or a bare serverport number (with tcp as default) - parent_logger: the logger to inherit from
- cfgfiles: if not given, defaults to name
may be a comma separated list of cfg files
items ending with .cfg are taken as paths, else .cfg is appended and
files are looked up in the config path retrieved from the general config
- interface: an uri of the from tcp://<port> or a bare port number for tcp
if not given, the interface is taken from the config file. In case of
multiple cfg files, the interface is taken from the first cfg file
- testonly: test mode. tries to build all modules, but the server is not started
Format of cfg file (for now, both forms are accepted):
old form: new form:
[node <equipment id>] [NODE]
description=<descr> id=<equipment id>
description=<descr>
[interface tcp] [INTERFACE]
bindport=10769 uri=tcp://10769
bindto=0.0.0.0
[module temp] [temp]
ramp=12 ramp=12
...
""" """
self._testonly = testonly self._testonly = testonly
cfg = getGeneralConfig() cfg = getGeneralConfig()
self.log = parent_logger.getChild(name, True) self.log = parent_logger.getChild(name, True)
configuration = {k: OrderedDict() for k, _, _ in self.CFGSECTIONS}
if interface:
try:
typ, interface = str(interface).split('://', 1)
except ValueError:
typ = 'tcp'
try:
host, port = interface.split(':', 1)
except ValueError:
host, port = '0.0.0.0', interface
options = {'type': typ, 'bindto': host, 'bindport': port}
configuration['interface %s' % options['type']] = options
if not cfgfiles: if not cfgfiles:
cfgfiles = name cfgfiles = name
merged_cfg = OrderedDict()
ambiguous_sections = set()
for cfgfile in cfgfiles.split(','): for cfgfile in cfgfiles.split(','):
if cfgfile.endswith('.cfg') and os.path.exists(cfgfile): if cfgfile.endswith('.cfg') and os.path.exists(cfgfile):
filename = cfgfile filename = cfgfile
else: else:
filename = os.path.join(cfg['confdir'], cfgfile + '.cfg') filename = os.path.join(cfg['confdir'], cfgfile + '.cfg')
self.mergeCfgFile(configuration, filename) cfgdict = self.loadCfgFile(filename)
if len(configuration['node']) > 1: ambiguous_sections |= set(merged_cfg) & set(cfgdict)
description = ['merged node\n'] merged_cfg.update(cfgdict)
for section, opt in configuration['node']: self.node_cfg = merged_cfg.pop('NODE')
description.append("--- %s:\n%s\n" % (section[5:], opt['description'])) self.interface_cfg = merged_cfg.pop('INTERFACE')
configuration['node'] = {cfgfiles: {'description': '\n'.join(description)}} self.module_cfg = merged_cfg
self._configuration = configuration if interface:
self._cfgfile = cfgfiles # used for reference in error messages only ambiguous_sections.discard('interface')
ambiguous_sections.discard('node')
self.node_cfg['name'] = name
self.node_cfg['id'] = cfgfiles
self.interface_cfg['uri'] = str(interface)
if ambiguous_sections:
self.log.warning('ambiguous sections in %s: %r' % (cfgfiles, tuple(ambiguous_sections)))
self._cfgfiles = cfgfiles
self._pidfile = os.path.join(cfg['piddir'], name + '.pid') self._pidfile = os.path.join(cfg['piddir'], name + '.pid')
def mergeCfgFile(self, configuration, filename): def loadCfgFile(self, filename):
self.log.debug('Parse config file %s ...' % filename) self.log.debug('Parse config file %s ...' % filename)
result = OrderedDict()
parser = configparser.ConfigParser() parser = configparser.ConfigParser()
parser.optionxform = str parser.optionxform = str
if not parser.read([filename]): if not parser.read([filename]):
self.log.error("Couldn't read cfg file %r!" % filename)
raise ConfigError("Couldn't read cfg file %r" % filename) raise ConfigError("Couldn't read cfg file %r" % filename)
for section, options in parser.items(): for section, options in parser.items():
try: if section == 'DEFAULT':
kind, name = section.split(' ', 1)
kind = kind.lower()
cfgdict = configuration[kind]
except (ValueError, KeyError):
if section != 'DEFAULT':
self.log.warning('skip unknown section %s' % section)
continue continue
opt = dict(options) opts = {}
if name in cfgdict: for k, v in options.items():
if kind == 'interface': # is the following really needed? - ConfigParser supports multiple lines!
opt = dict(type='tcp', bindto='0.0.0.0') while '\n.\n' in v:
opt.update(options) v = v.replace('\n.\n', '\n\n')
if opt != cfgdict[name]: try:
self.log.warning('omit conflicting section %r in %s' % (section, filename)) opts[k] = ast.literal_eval(v)
else: except Exception:
cfgdict[name] = dict(options) opts[k] = v
# convert old form
name, _, arg = section.partition(' ')
if arg:
if name == 'node':
name = 'NODE'
opts['id'] = arg
elif name == 'interface':
name = 'INTERFACE'
if 'bindport' in opts:
opts.pop('bindto', None)
opts['uri'] = '%s://%s' % (opts.pop('type', arg), opts.pop('bindport'))
elif name == 'module':
name = arg
result[name] = opts
return result
def start(self): def start(self):
if not DaemonContext: if not DaemonContext:
@ -146,6 +165,10 @@ class Server:
files_preserve=self.log.getLogfileStreams()): files_preserve=self.log.getLogfileStreams()):
self.run() self.run()
def unknown_options(self, cls, options):
raise ConfigError("%s class don't know how to handle option(s): %s" %
(cls.__name__, ', '.join(options)))
def run(self): def run(self):
while self._restart: while self._restart:
self._restart = False self._restart = False
@ -159,73 +182,44 @@ class Server:
print(formatException(verbose=True)) print(formatException(verbose=True))
raise raise
opts = dict(self.interface_cfg)
scheme, _, _ = opts['uri'].rpartition('://')
scheme = scheme or 'tcp'
cls = get_class(self.INTERFACES[scheme])
with cls(scheme, self.log.getChild(scheme), opts, self) as self.interface:
if opts:
self.unknown_options(cls, opts)
self.log.info('startup done, handling transport messages') self.log.info('startup done, handling transport messages')
threads = []
for ifname, ifobj in self.interfaces.items():
self.log.debug('starting thread for interface %r' % ifname)
threads.append((ifname, mkthread(ifobj.serve_forever)))
if systemd: if systemd:
systemd.daemon.notify("READY=1\nSTATUS=accepting requests") systemd.daemon.notify("READY=1\nSTATUS=accepting requests")
for ifname, t in threads: self.interface.serve_forever()
t.join() self.interface.server_close()
self.log.debug('thread for %r died' % ifname) if self._restart:
self.restart_hook()
self.log.info('restart')
else:
self.log.info('shut down')
def restart(self): def restart(self):
if not self._restart: if not self._restart:
self._restart = True self._restart = True
for ifobj in self.interfaces.values(): self.interface.shutdown()
ifobj.shutdown()
ifobj.server_close()
def _processCfg(self): def _processCfg(self):
self.log.debug('Parse config file %s ...' % self._cfgfile) opts = dict(self.node_cfg)
cls = get_class(opts.pop('class', 'protocol.dispatcher.Dispatcher'))
for kind, default_type, classmapping in self.CFGSECTIONS: self.dispatcher = cls(opts.pop('name', self._cfgfiles), self.log.getChild('dispatcher'), opts, self)
objs = OrderedDict()
self.__dict__['%ss' % kind] = objs
for name, options in self._configuration[kind].items():
opts = dict(options)
if 'class' in opts:
cls = opts.pop('class')
else:
if not classmapping:
self.log.error('%s %s needs a class option!' % (kind.title(), name))
raise ConfigError('cfgfile %r: %s %s needs a class option!' %
(self._cfgfile, kind.title(), name))
type_ = opts.pop('type', default_type)
cls = classmapping.get(type_, None)
if not cls:
self.log.error('%s %s needs a type option (select one of %s)!' %
(kind.title(), name, ', '.join(repr(r) for r in classmapping)))
raise ConfigError('cfgfile %r: %s %s needs a type option (select one of %s)!' %
(self._cfgfile, kind.title(), name, ', '.join(repr(r) for r in classmapping)))
# MAGIC: transform \n.\n into \n\n which are normally stripped
# by the ini parser
for k in opts:
v = opts[k]
while '\n.\n' in v:
v = v.replace('\n.\n', '\n\n')
try:
opts[k] = ast.literal_eval(v)
except Exception:
opts[k] = v
# try to import the class, raise if this fails
self.log.debug('Creating %s %s ...' % (kind.title(), name))
# cls.__init__ should pop all used args from options!
logname = 'dispatcher' if kind == 'node' else '%s_%s' % (kind, name.lower())
obj = get_class(cls)(name, self.log.getChild(logname), opts, self)
if opts: if opts:
raise ConfigError('%s %s: class %s: don\'t know how to handle option(s): %s' % self.unknown_options(cls, opts)
(kind, name, cls, ', '.join(opts))) self.modules = OrderedDict()
for modname, options in self.module_cfg.items():
# all went well so far opts = dict(options)
objs[name] = obj cls = get_class(opts.pop('class'))
modobj = cls(modname, self.log.getChild(modname), opts, self)
# following line is the reason for 'node' beeing the first entry in CFGSECTIONS # all used args should be popped from opts!
if len(self.nodes) != 1: if opts:
raise ConfigError('cfgfile %r: needs exactly one node section!' % self._cfgfile) self.unknown_options(cls, opts)
self.dispatcher, = tuple(self.nodes.values()) self.modules[modname] = modobj
poll_table = dict() poll_table = dict()
# all objs created, now start them up and interconnect # all objs created, now start them up and interconnect