instead of removing setup files, they are changed to lowlevel, so no longer visible, but not creating any errors when loaded
624 lines
24 KiB
Python
624 lines
24 KiB
Python
# *****************************************************************************
|
|
# NICOS, the Networked Instrument Control System of the MLZ
|
|
# Copyright (c) 2009-2018 by the NICOS contributors (see AUTHORS)
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify it under
|
|
# the terms of the GNU General Public License as published by the Free Software
|
|
# Foundation; either version 2 of the License, or (at your option) any later
|
|
# version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
|
# details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License along with
|
|
# this program; if not, write to the Free Software Foundation, Inc.,
|
|
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
#
|
|
# Module authors:
|
|
# Markus Zolliker <markus.zolliker@psi.ch>
|
|
#
|
|
# *****************************************************************************
|
|
"""managing SECoP server and connections
|
|
|
|
SEC Node with added functionality for starting and stopping frappy servers
|
|
connected to a SEC node
|
|
"""
|
|
|
|
import sys
|
|
linsepath = '/sq_sw/linse/frappy'
|
|
if linsepath not in sys.path:
|
|
sys.path.insert(0, linsepath)
|
|
import re
|
|
from pathlib import Path
|
|
from nicos import config, session
|
|
from nicos.core import Override, Param, Moveable, status, POLLER, SIMULATION, DeviceAlias, \
|
|
Device, Readable, anytype, listof, MASTER, Attach
|
|
from frappy_sinq.secop.devices import SecNodeDevice, SecopDevice, DefunctDevice, SecopWritable, NicosSecopClient
|
|
from nicos.core.utils import createThread
|
|
from nicos.utils.comparestrings import compare
|
|
from nicos.devices.secop.devices import get_attaching_devices
|
|
from nicos.utils import loggers, printTable
|
|
from nicos.services.daemon.script import ScriptRequest
|
|
from nicos.commands import helparglist, usercommand
|
|
from nicos.commands.basic import RemoveSetup, AddSetup
|
|
from linsetools.frappy import FrappyControl
|
|
from linsetools.base import write_content
|
|
|
|
|
|
SETUP_TEMPLATE = """description = '%(desc)s'
|
|
group = 'optional'
|
|
display_order = 50.5
|
|
devices = {
|
|
'secnode_%(nodename)s':
|
|
device('frappy_sinq.new.FrappyNode',
|
|
description='SECoP connection to %(nodename)s', unit='', async_only=True,
|
|
prefix='%(prefix)s', auto_create=True,
|
|
uri=%(uri)s, visibility_level=2,
|
|
general_stop_whitelist=['om', 'stickrot'],
|
|
%(nodeargs)s),
|
|
}
|
|
%(additions)s
|
|
startupcode = '''
|
|
try:
|
|
frappy.adjustEnvironment()
|
|
except NameError:
|
|
printinfo("please load also the 'frappy' setup")
|
|
'''
|
|
"""
|
|
MEANINGS = {
|
|
'temperature': 'Ts',
|
|
'temperature_regulation': 'T',
|
|
'magneticfield': 'B',
|
|
'pressure': 'p',
|
|
'rotation_z': 'a3',
|
|
'stick_rotation': 'dom',
|
|
}
|
|
SKIP_ENV = 'rotation_z', 'dom'
|
|
SETUPDIR = '/home/linse/setups'
|
|
IDSUB = re.compile('\W+|^(?=\d)')
|
|
NOT_USED = 'not_used'
|
|
NAME_URI = re.compile(r'([^=]+)=(.+)')
|
|
|
|
|
|
def shorten(secnode_list):
|
|
result = ','.join(('' if v == '0' else v.split('=')[0]) for v in secnode_list)
|
|
return result[:-1] if result.endswith(',') else result
|
|
|
|
|
|
class Service(str):
|
|
pass
|
|
|
|
|
|
class Main(Service):
|
|
name = 'main'
|
|
|
|
|
|
class Stick(Service):
|
|
name = 'stick'
|
|
|
|
|
|
class Addons(Service):
|
|
name = 'addons'
|
|
|
|
|
|
def uri_with_kind(kind, uri):
|
|
if kind >= 2:
|
|
return Addons(uri)
|
|
return Stick(uri) if kind else Main(uri)
|
|
|
|
|
|
def secnodes_from_string(value):
|
|
result = {}
|
|
for kind, item in enumerate(value.split()):
|
|
match = NAME_URI.match(item)
|
|
if match:
|
|
name, uri = match.groups()
|
|
result[name] = uri_with_kind(kind, uri)
|
|
return result
|
|
|
|
|
|
def secnodes_to_string(secnodes):
|
|
secnode_list = ['0', '0']
|
|
for name, uri in secnodes.items():
|
|
item = f'{name}={uri}'
|
|
if isinstance(uri, Main):
|
|
secnode_list[0] = item
|
|
elif isinstance(uri, Stick):
|
|
secnode_list[1] = item
|
|
else:
|
|
secnode_list.append(item)
|
|
return ' '.join(secnode_list)
|
|
|
|
|
|
def shorten(value):
|
|
result = ','.join('' if v == '0' else v.split('=')[0]for v in value.split())
|
|
return result[:-1] if result.endswith(',') else result
|
|
|
|
|
|
def get_meanings(modname, meaning, hastarget):
|
|
result = {}
|
|
if meaning:
|
|
meaning_name, importance = meaning
|
|
if meaning_name not in MEANINGS:
|
|
session.log.warning('%s: meaning %r is unknown', modname, meaning_name)
|
|
return result
|
|
result.setdefault(meaning_name, []).append((importance, modname))
|
|
if meaning_name == 'temperature_regulation':
|
|
# add temperature_regulation to temperature list, with very low importance
|
|
result.setdefault('temperature', []).append((importance - 100, modname))
|
|
elif meaning_name == 'temperature' and hastarget:
|
|
result.setdefault('temperature_regulation', []).append((importance, modname))
|
|
return result
|
|
|
|
|
|
class FrappyManager(Readable):
|
|
"""start/stops to frappy servers or connects to SEC nodes"""
|
|
|
|
# TODO: make prefix a parameter
|
|
parameters = {
|
|
'prefix': Param("Prefix for the generated devices",
|
|
type=str, default='se_', settable=True),
|
|
'secnodes': Param('name=uri of secnodes, space separated',
|
|
type=str, default='', settable=True),
|
|
}
|
|
parameter_overrides = {
|
|
'unit': Override(unit='', mandatory=False),
|
|
}
|
|
|
|
valuetype = str # the main value is a comma separated list
|
|
_running = None
|
|
_frappy_control = None
|
|
_box2uri = None
|
|
_uri2box = None
|
|
_secnodes = None # a dict like Secnodes instance
|
|
_do_cleanup = False
|
|
|
|
def doInit(self, mode):
|
|
if SETUPDIR not in session._setup_paths:
|
|
session.log.error('can not use frappy as %r is not in the setup_subdirs', SETUPDIR)
|
|
expt = session.experiment
|
|
if 'persistent_environment' in expt.parameters and mode != SIMULATION:
|
|
expt.persistent_environment = [v for k, v in MEANINGS.items() if k not in SKIP_ENV]
|
|
else:
|
|
cls = type(expt)
|
|
session.log.warning(
|
|
'experiment (%s.%s) does not support persistent environment',
|
|
cls.__module__, cls.__qualname__)
|
|
self._check_secnodes(True)
|
|
|
|
def doUpdateSecnodes(self, value):
|
|
self._secnodes = secnodes_from_string(value)
|
|
self._value = shorten(value)
|
|
|
|
def _update_secnodes(self, secnodes):
|
|
self._secnodes = secnodes
|
|
self.secnodes = secnodes_to_string(self._secnodes)
|
|
|
|
def doRead(self, maxage=0):
|
|
return self._value
|
|
|
|
def _check_secnodes(self, check_connection):
|
|
fc = self._frappy_control = FrappyControl('this')
|
|
self._uri2box = {}
|
|
self._box2uri = {}
|
|
for nodename, uri in fc.config.sections.get('boxes', {}).items():
|
|
self._box2uri[nodename] = uri
|
|
self._uri2box[uri] = nodename
|
|
fc.get_cfg_info()
|
|
self._running = fc.running()
|
|
if self._secnodes is None:
|
|
secnodes = secnodes_from_string(self.secnodes)
|
|
else:
|
|
secnodes = {k: v for k, v in self._secnodes.items() if k in self._running or not v.startswith('localhost:')}
|
|
if check_connection:
|
|
for name, uri in list(secnodes.items()):
|
|
if not uri.startswith('localhost:'):
|
|
secopclient = self.connect_secnode(uri)
|
|
if not secopclient:
|
|
secnodes.pop(name)
|
|
continue
|
|
secopclient.disconnect()
|
|
self._box2uri[name] = uri
|
|
self._uri2box[uri] = name
|
|
self._update_secnodes(secnodes)
|
|
|
|
def doStatus(self, maxage=0):
|
|
return status.OK, ''
|
|
|
|
def get_uri_name(self, arg, service=None):
|
|
"""return (<uri>, <name>) from arg"""
|
|
if ':' in arg:
|
|
return arg, self._uri2box.get(arg) or arg.replace(':', '_')
|
|
uri = self._box2uri.get(arg)
|
|
if uri is None:
|
|
if service:
|
|
uri = service(f'localhost:{self._frappy_control.get_port(service.name)}')
|
|
else:
|
|
uri = self._frappy_control.cfg_info.get(arg)
|
|
if service:
|
|
return service(uri), arg
|
|
return uri, arg
|
|
|
|
def connect_secnode(self, uri):
|
|
"""connect to secnode
|
|
|
|
do not forget to disconnect if no longer used
|
|
"""
|
|
secopclient = NicosSecopClient(uri)
|
|
try:
|
|
secopclient.connect(30)
|
|
return secopclient
|
|
except Exception:
|
|
return None
|
|
|
|
def __call__(self, main=None, stick=None, addons=None, *extra, force=False, update=True):
|
|
self._check_secnodes(False)
|
|
fc = self._frappy_control
|
|
|
|
# determine which nodes to add (and start) or remove (and stop)
|
|
to_remove = {}
|
|
to_add = {}
|
|
main_item = None
|
|
stick_item = None
|
|
for name, uri in self._secnodes.items():
|
|
if isinstance(uri, Main):
|
|
main_item = name
|
|
elif isinstance(uri, Stick):
|
|
stick_item = name
|
|
elif not isinstance(uri, Addons):
|
|
self.log.warning('%r is no service', uri)
|
|
if main is not None:
|
|
if main_item:
|
|
to_remove[main_item] = self._secnodes.pop(main_item)
|
|
if main:
|
|
to_add[main] = Main
|
|
if main != main_item:
|
|
if main in self._secnodes:
|
|
to_remove[main] = self._secnodes.pop(main)
|
|
# auto stick:
|
|
if stick is None:
|
|
try:
|
|
cfg = f'{main}stick'
|
|
service, cfgfile = fc.cfg_file(None, 'stick', cfg)
|
|
if service == 'stick':
|
|
stick = cfg
|
|
except FileNotFoundError:
|
|
pass
|
|
if stick is not None:
|
|
if stick_item:
|
|
to_remove[stick_item] = self._secnodes.pop(stick_item)
|
|
if stick:
|
|
to_add[stick] = Stick
|
|
if stick in self._secnodes:
|
|
to_remove[stick] = self._secnodes.pop(stick)
|
|
for arglist in (addons,) + extra:
|
|
if arglist == '':
|
|
to_remove.update(self._secnodes)
|
|
self._secnodes.clear()
|
|
elif arglist is not None:
|
|
# allow legacy '<cfg1>,<cfg2>'
|
|
for arg in arglist.split(','):
|
|
if arg:
|
|
to_add[arg] = Addons
|
|
prev = self._secnodes.get(arg)
|
|
if isinstance(prev, (Main, Stick)):
|
|
to_remove[arg] = self._secnodes.pop(arg)
|
|
|
|
# remove_setups = {self.get_setup_name(cfg) for cfg in to_remove} & set(session.loaded_setups)
|
|
# if remove_setups:
|
|
# RemoveSetup(*remove_setups)
|
|
for cfg in to_remove:
|
|
secnode_dev = session.devices.get(f'secnode_{cfg}')
|
|
if secnode_dev:
|
|
secnode_dev.doShutdown()
|
|
for cfg, uri in to_remove.items():
|
|
if uri.startswith('localhost:'):
|
|
fc.stop(cfg)
|
|
fc.delete_frappy_service(cfg)
|
|
|
|
add_setups = []
|
|
# add and start new servers
|
|
for arg, servicecls in to_add.items():
|
|
uri, name = self.get_uri_name(arg, servicecls)
|
|
cfginfo = ''
|
|
if uri.startswith('localhost:'): # isinstance(uri, Service):
|
|
port = fc.get_port(uri.name)
|
|
uri = servicecls(fc.add_frappy_service(servicecls.name, name, port, session.log))
|
|
cfginfo = f' ({name})'
|
|
fc.start(name)
|
|
session.log.info('wait for startup of %r%s', uri, cfginfo)
|
|
else:
|
|
session.log.info('connect to %r%s', uri, cfginfo)
|
|
secopclient = self.connect_secnode(uri)
|
|
if secopclient:
|
|
self.write_setup_file(secopclient, uri, name, arg)
|
|
secopclient.disconnect()
|
|
add_setups.append(self.get_setup_name(name))
|
|
self._secnodes[name] = uri
|
|
else:
|
|
self._secnodes.pop(name, None)
|
|
session.log.exception('cannot connect to %r%s', uri, cfginfo)
|
|
self._update_secnodes(self._secnodes)
|
|
remove_setups = {self.get_setup_name(cfg) for cfg in to_remove} & set(session.loaded_setups)
|
|
noadd_set = session.loaded_setups - set(remove_setups)
|
|
add_setups = [v for v in add_setups if v not in noadd_set]
|
|
|
|
if remove_setups:
|
|
RemoveSetup(*remove_setups)
|
|
if add_setups:
|
|
AddSetup(*add_setups)
|
|
session.readSetups()
|
|
unused_setups = {f for f in Path(SETUPDIR).glob('se_*.py')
|
|
if f.stem not in session.loaded_setups and f.stem[3:] not in self._secnodes}
|
|
if unused_setups:
|
|
for setup_file in unused_setups:
|
|
content = setup_file.read_text()
|
|
newcontent = re.sub("\ngroup = '(optional|plugplay)'", "\ngroup = 'lowlevel'", content)
|
|
if newcontent != content:
|
|
self.log.info('hide unused setup %s', setup_file.stem)
|
|
write_content(setup_file, newcontent, group='+rw')
|
|
session.readSetups()
|
|
return self.read(0)
|
|
|
|
def get_setup_name(self, cfg):
|
|
cfg = cfg.replace(':', '_')
|
|
return f'se_{cfg}'
|
|
|
|
def _get_device_name(self, device_mapping, module):
|
|
"""get a modules mapped name according to device_mapping or None,
|
|
if unmapped
|
|
"""
|
|
if module in device_mapping and 'name' in device_mapping[module]:
|
|
return self.device_mapping[module]['name']
|
|
return self.prefix + module
|
|
|
|
def node_setup_info(self, secopclient):
|
|
"""determine aliases and envlist for SECoP devices
|
|
|
|
depending on their meaning
|
|
"""
|
|
modules = secopclient.modules
|
|
meaning_info = {} # dict <meaning name> of list of (<importance>, <target>)
|
|
device_mapping = {}
|
|
reserved_names = {v.lower() for v in MEANINGS.values()}
|
|
for modname, moddesc in modules.items():
|
|
if self.prefix == '' and modname.lower() in reserved_names:
|
|
device_mapping[modname] = {'name': f'{modname}_'}
|
|
meaning_info.update(get_meanings(modname, moddesc['properties'].get('meaning'),
|
|
'target' in moddesc['parameters']))
|
|
envlist = []
|
|
alias_config = {}
|
|
for meaning_name, info in meaning_info.items():
|
|
importance, modname = sorted(info)[-1]
|
|
devname = self._get_device_name(device_mapping, modname)
|
|
target = MEANINGS.get(meaning_name)
|
|
alias_config[target] = {devname: importance, NOT_USED: -100}
|
|
if target == 'a3' and meaning_name == 'rotation_z':
|
|
alias_config['om'] = {devname: importance}
|
|
if target not in SKIP_ENV:
|
|
envlist.append(target)
|
|
return envlist, alias_config, device_mapping
|
|
|
|
def write_setup_file(self, secopclient, uri, name, origname):
|
|
setup = self.get_setup_name(name)
|
|
if name != origname:
|
|
# sanitize name: replace sequences of non-alphanumeric characters by a single '_'
|
|
# also prefix '_' when starting with a digit
|
|
# and remove trailing .psi.ch
|
|
name = IDSUB.sub(secopclient.nodename.replace('.psi.ch', ''), '_')
|
|
self._box2uri[name] = uri
|
|
envlist, alias_config, devmap = self.node_setup_info(secopclient)
|
|
nodeargs = f'device_mapping={devmap!r}' if devmap else ''
|
|
additions = []
|
|
if alias_config:
|
|
additions.append(f'alias_config = {alias_config!r}')
|
|
desc = secopclient.properties.get('description') or name
|
|
setup_content = SETUP_TEMPLATE % {
|
|
'nodename': name,
|
|
'prefix': self.prefix,
|
|
'desc': desc.split('\n')[0],
|
|
'uri': repr(uri),
|
|
'nodeargs': nodeargs,
|
|
'additions': '\n'.join(additions),}
|
|
write_content(Path(SETUPDIR) / f'{setup}.py', setup_content, group='+rw')
|
|
return envlist
|
|
|
|
def adjustEnvironment(self):
|
|
if self._mode == SIMULATION:
|
|
return
|
|
self._do_cleanup = True
|
|
session.readSetups()
|
|
# user = type('User', (), {'name': 'ghost'})()
|
|
# session.daemon_device._controller.new_request(ScriptRequest('frappy.cleanup_setups()', '', user))
|
|
samenvlist = []
|
|
for meaning, devname in MEANINGS.items():
|
|
dev = session.devices.get(devname)
|
|
if dev:
|
|
try:
|
|
dev.read(0)
|
|
except Exception as e:
|
|
self.log.info('error %r when reading %s', e, dev)
|
|
if meaning in SKIP_ENV:
|
|
continue
|
|
if dev:
|
|
if dev.alias != NOT_USED:
|
|
samenvlist.append(devname)
|
|
elif devname in session.alias_config:
|
|
self.log.warning('can not find alias %r', devname)
|
|
|
|
expt = session.experiment
|
|
try:
|
|
expt.persistent_environment = samenvlist
|
|
except Exception:
|
|
pass
|
|
envlist = expt.sampleenv
|
|
previous = set(envlist)
|
|
for devname in samenvlist:
|
|
dev = session.devices.get(devname)
|
|
if dev is None:
|
|
if devname in previous:
|
|
envlist.remove(devname)
|
|
elif devname not in previous:
|
|
envlist.append(devname)
|
|
expt.setEnvironment(envlist)
|
|
self.read(0)
|
|
|
|
|
|
def cleanup_defunct():
|
|
for devname, setupname in list(session.dynamic_devices.items()):
|
|
dev = session.devices.get(devname)
|
|
if dev and dev._defunct:
|
|
devnames = [d.name for d, _ in get_attaching_devices(dev)]
|
|
if devnames:
|
|
session.log.warning('can not remove device %r due to dependencies on %s'
|
|
% (devname, ', '.join(devnames)))
|
|
else:
|
|
session.destroyDevice(devname)
|
|
session.dynamic_devices.pop(devname, None)
|
|
|
|
|
|
class FrappyNode(SecNodeDevice):
|
|
"""SEC node device, works together with superfrappy"""
|
|
parameters = {
|
|
'param_category': Param("category of parameters\n\n"
|
|
"set to 'general' if all parameters should appear in the datafile header",
|
|
type=str, default='', settable=True),
|
|
'quiet_init': Param('flag to set loglevel to error while initializing',
|
|
type=bool, default=False, settable=True),
|
|
# duplicate from SecNodeDevice. needed for the case where the code for
|
|
# the SecNodeDevcice is not up to date, but the setup is already new
|
|
# does not yet have the i
|
|
'general_stop_whitelist': Param('module names to accept general stop',
|
|
type=listof(str), prefercache=False,
|
|
default=[], userparam=False),
|
|
}
|
|
|
|
_lastcfg = None
|
|
# _marche = None
|
|
|
|
def doInit(self, mode):
|
|
# self._marche = MarcheControl()
|
|
if mode != SIMULATION and session.sessiontype != POLLER:
|
|
pass
|
|
# TODO:
|
|
# host_port = self.uri.rsplit('://')[-1]
|
|
# status = self._marche.status(config.instrument)
|
|
# running = self._attached_superfrappy.check_running(host_port)
|
|
# if self.frappycfg and running != self.frappycfg:
|
|
# self.superfrappy.add_server(host_port)
|
|
super().doInit(mode)
|
|
|
|
def createDevices(self):
|
|
super().createDevices()
|
|
self.log.info('--- create devices ---')
|
|
for devname, (_, devcfg) in self.setup_info.items():
|
|
params_cfg = devcfg['params_cfg']
|
|
meanings = get_meanings(
|
|
devcfg['secop_module'],
|
|
devcfg['secop_properties'].get('meaning'),
|
|
'target' in params_cfg)
|
|
for meaning in meanings:
|
|
alias = MEANINGS.get(meaning)
|
|
if alias and alias not in session.devices:
|
|
# alias is not yet created
|
|
devcfg = ('nicos.core.device.DeviceAlias', {'descripton': meaning})
|
|
session.configured_devices[alias] = devcfg
|
|
session.dynamic_devices[alias] = 'frappy'
|
|
session.createDevice(alias, recreate=True, explicit=True)
|
|
elif not alias:
|
|
self.log.info('do not know meaning %s', meaning)
|
|
if self.param_category:
|
|
dev = session.devices[devname]
|
|
for pname, pargs in params_cfg.items():
|
|
pinfo = dev.parameters[pname]
|
|
if not pinfo.category:
|
|
pinfo.category = self.param_category
|
|
|
|
def makeDynamicDevices(self, setup_info):
|
|
patched_loggers = {}
|
|
if self.quiet_init:
|
|
for devname, (_, devcfg) in setup_info.items():
|
|
log = session.getLogger(devname)
|
|
if log not in patched_loggers:
|
|
result = [loggers.INFO] # default level
|
|
patched_loggers[log] = result
|
|
log.setLevel(loggers.ERROR)
|
|
# avoid level change when the loglevel parameter is treated
|
|
# store level instead in result
|
|
log.__dict__['setLevel'] = result.append
|
|
try:
|
|
super().makeDynamicDevices(setup_info)
|
|
finally:
|
|
for log, result in patched_loggers.items():
|
|
log.__dict__.pop('setLevel', None) # re-enable setLevel
|
|
log.setLevel(result[-1]) # set to stored or default value
|
|
|
|
def disable(self):
|
|
seaconn = session.devices.get('sea_%s' % self.service)
|
|
if seaconn and seaconn._attached_secnode:
|
|
seaconn.communicate('frappy_remove %s' % self.service)
|
|
self._set_status(*self._status)
|
|
|
|
def _set_status(self, code, text):
|
|
if self.uri == '':
|
|
code, text = status.DISABLED, 'disabled'
|
|
SecNodeDevice._set_status(self, code, text)
|
|
|
|
# def restart(self):
|
|
# """restart frappy server"""
|
|
# host_port = self.uri.rsplit('://')[-1]
|
|
# self._marche.restart(config.instrument, self.frappycfg)
|
|
|
|
def get_info(self):
|
|
result = self.doRead() or ''
|
|
code, text = self.status()
|
|
if not result:
|
|
return '<disconnected>'
|
|
if code == status.OK or result == '':
|
|
return result
|
|
if (code, text) == (status.ERROR, 'reconnecting'):
|
|
return '%s (frappy not running)' % result
|
|
return '%s (%s)' % (result, text)
|
|
|
|
|
|
class NullDevice(Moveable):
|
|
"""dummy target for sample environment aliases"""
|
|
parameter_overrides = {
|
|
'unit': Override(unit='', mandatory=False),
|
|
}
|
|
|
|
def doRead(self, maxage=0):
|
|
return 0
|
|
|
|
def doStatus(self, maxage=0):
|
|
return status.DISABLED, 'disabled'
|
|
|
|
def doStart(self, target):
|
|
self.log.warning('disabled, cannot not move')
|
|
|
|
|
|
@usercommand
|
|
@helparglist('')
|
|
def frappy_list(service=None):
|
|
"""list available configuration files"""
|
|
fc = FrappyControl('this')
|
|
content = []
|
|
|
|
def prt(line):
|
|
content.append(line)
|
|
|
|
if service is None:
|
|
prt('Available configuration files')
|
|
prt('')
|
|
prt('Hint: if no config file can be found which matches your needs exactly')
|
|
prt('make a copy of an existing one, and change the description accordingly')
|
|
prt('')
|
|
prt('Usage (default argument "main"):')
|
|
prt('')
|
|
printTable(['command'], [['frappy_list(%r)' % s] for s in fc.services], prt)
|
|
|
|
fc.listcfg(service or 'main', prt)
|
|
session.log.info('\n%s', '\n'.join(content))
|