servicemanager/frappyman.py
Markus Zolliker fc364f0fc6 improve frappy manager
- add functionality to guess combination of configurations from
  the running sea and frappy servers
- instead of the name only, the references to the config in
  the SeaClient modules are used
- make this information also visibible in frappy list
2023-09-20 17:01:06 +02:00

330 lines
13 KiB
Python

# -*- coding: utf-8 -*-
# *****************************************************************************
#
# 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>
#
# *****************************************************************************
import sys
import os
import re
import builtins
from glob import glob
from collections import defaultdict
from os.path import join, isdir, basename, expanduser, exists
from configparser import ConfigParser
from .base import ServiceManager, ServiceDown, UsageError
from .seaman import SeaManager
class Namespace(dict):
def __init__(self):
self['Node'] = self.node
self['Mod'] = self.mod
for fun in 'Param', 'Command', 'Group':
self[fun] = self.dummy
self.init()
def init(self):
self.description = ''
self.sea_cfg = None
def node(self, equipment_id, description, *args, **kwds):
self.description = description
def mod(self, name, cls, description, config=None, **kwds):
cls = getattr(cls, '__name__', cls)
if cls.endswith('SeaClient'):
self.sea_cfg = config
def dummy(self, *args, **kwds):
return None
__builtins__ = builtins
class FrappyManager(ServiceManager):
group = 'frappy'
services = ('main', 'stick', 'addons')
USAGE = """
Usage:
frappy list [instance] *
frappy start <instance> <service> <cfgfiles>
frappy restart <instance> [<service>] [<cfgfiles>] *
frappy stop <instance> [<service>] *
frappy listcfg <instance> [<service> | develop] # list available cfg files
<service> is one of main, stick, addons
%s
* wildcards allowed, using '.' to replace 0 or more arbitrary characters in <instance>
"""
def config_dirs(self, ins, service):
cfgpaths = []
cfgparser = ConfigParser()
cfgparser.optionxform = str
cfgfile = self.env[ins].get('FRAPPY_CONFIG_FILE')
confdir = self.env[ins].get('FRAPPY_CONFDIR')
if cfgfile:
cfgfile = self.env[ins]['FRAPPY_CONFIG_FILE'].replace('<SERV>', service)
cfgparser.read(cfgfile)
try:
section = cfgparser['FRAPPY']
except KeyError:
raise ValueError('%s does not exist or has no FRAPPY section' % cfgfile)
confdir = section.get('confdir', confdir)
for cfgpath in confdir.split(os.pathsep):
if cfgpath.endswith('<SERV>'):
cfgpaths.append(expanduser(cfgpath[:-6] + service))
else:
scfg = join(cfgpath, service)
if isdir(scfg):
cfgpaths.append(scfg)
cfgpaths.append(expanduser(cfgpath))
return cfgpaths
def prepare_start(self, ins, service, cfg=''):
start_dir, env = super().prepare_start(ins, service)
env_update = {}
for key, value in env.items():
if '<SERV>' in value:
env_update[key] = value.replace('<SERV>', service)
os.environ[key] = env[key]
cfgpaths = self.config_dirs(ins, service)
if cfgpaths:
env_update['FRAPPY_CONFDIR'] = os.pathsep.join(cfgpaths)
# merge PYTHONPATH from servicemanager.cfg with the one from environment
pypathlist = []
for pypath in env.get('PYTHONPATH'), os.environ.get('PYTHONPATH'):
if pypath is not None:
pypathlist.extend(p for p in pypath.split(':') if p not in pypathlist)
if pypathlist:
env_update['PYTHONPATH'] = os.environ['PYTHONPATH'] = ':'.join(pypathlist)
return start_dir, dict(env, **env_update)
def do_start(self, ins, service=None, cfg='', restart=False, wait=False, logger=None):
if self.wildcard(ins) is not None:
raise UsageError('no wildcards allowed with %s start' % self.group)
if cfg and not service and len(self.services) != 1:
raise UsageError('need service to start (one of %s)' % ', '.join(self.services))
super().do_start(ins, service, cfg, restart, wait, logger)
def do_restart(self, ins, service=None, cfg=None, logger=None):
ins_list = super().do_restart(ins, service, cfg, logger)
if ins_list: # wildcard used
# determine running processes with cfg
cfginfo = {}
self.get_procs(None, cfginfo)
cfgs = {i for i, s in cfginfo if s == service or service is None}
return [i for i in ins_list if i in cfgs]
def get_nodes(self, ins='', service=None):
start_dir = ServiceManager.prepare_start(self, ins, None)[0]
if start_dir not in sys.path:
sys.path.insert(0, start_dir)
nodes = []
services = self.services if service is None else [service]
for service in services:
try:
self.check_running(ins, service)
nodes.append('localhost:%d' % self.info[ins][service])
except ServiceDown:
if len(services) == 1:
raise UsageError('frappy %s %s is not running' % (ins, service))
except KeyError:
if ins:
raise UsageError('unknown instance %s' % ins)
raise UsageError('missing instance')
if not nodes:
raise UsageError(f"frappy {ins}: none of {'/'.join(services)} is running")
return nodes
def do_gui(self, ins='', service=None):
print(f'starting frappy gui {ins} {service or ""}')
nodes = self.get_nodes(ins, service)
import logging
from frappy.gui.qt import QApplication
from frappy.gui.mainwindow import MainWindow
app = QApplication([])
win = MainWindow(nodes, logging.getLogger('gui'))
win.show()
return app.exec_()
def do_cli(self, ins='', service=None):
nodes = self.get_nodes(ins, service)
from frappy.client.interactive import init, interact
init(*nodes)
interact()
def get_cfg_details(self, namespace, cfgfile):
namespace.init()
local = {}
with open(cfgfile, encoding='utf-8') as f:
exec(f.read(), namespace, local)
return namespace.description, local.get('sea_cfg', namespace.sea_cfg)
def cfg_details(self, ins, service, cfg):
namespace = Namespace()
for cfgdir in self.config_dirs(ins, service):
cfgfile = join(cfgdir, f'{cfg}_cfg.py')
if exists(cfgfile):
return self.get_cfg_details(namespace, cfgfile)
raise FileNotFoundError(f'{cfg} not found')
def is_cfg(self, ins, service, cfg):
try:
self.cfg_details(ins, service, cfg)
return True
except Exception:
return False
def all_cfg(self, ins, service, list_info=None, sea_info=None):
"""get available cfg files
:param ins: instance
:param service: service nor None for all services
:param list_info: None or a dict to collect info in the form
dict <cfgdir> of <cfg> of <description>
:param sea_info: None or a dict <sea config> of set() to be populated
:return: set of available config
"""
all_cfg = set()
if not ins:
return {}
namespace = Namespace()
details = sea_info is not None or list_info is not None
for service in [service] if service else self.services:
for cfgdir in self.config_dirs(ins, service):
for cfgfile in glob(join(cfgdir, '*_cfg.py')):
cfg = basename(cfgfile)[:-7]
if details:
try:
desc, sea_cfg = self.get_cfg_details(namespace, cfgfile)
except Exception as e:
sea_cfg = None
desc = repr(e)
if cfg not in all_cfg:
if sea_cfg and sea_info is not None:
sea_info.setdefault(sea_cfg, set()).add(cfg)
all_cfg.add(cfg)
if list_info is not None:
list_info.setdefault(cfgdir, {})[cfg] = desc.split('\n', 1)[0]
return all_cfg
def do_listcfg(self, ins='', service='', prt=print):
if not ins:
raise UsageError('missing instance')
list_info = {}
sea_info = {}
if service:
self.all_cfg(ins, service, list_info, sea_info)
else:
for service in self.services:
self.all_cfg(ins, service, list_info, sea_info)
sea_ambig = {} # collect info about ambiguous sea info
seacfgpat = re.compile(r'(.*)(\.config|\.stick|\.addon)')
for seacfg, cfgset in sea_info.items():
name, ext = seacfgpat.match(seacfg).groups()
sea_ambig.update({k: (name, ext, len(cfgset)) for k in cfgset})
ambiguous = 0
keylen = max(max(len(k) for k in cfgs) for cfgs in list_info.values())
for cfgdir, cfgs in list_info.items():
if cfgs:
prt('')
prt('--- %s:' % cfgdir)
for cfg, desc in cfgs.items():
if cfg in sea_ambig:
name, ext, n = sea_ambig[cfg]
if name == cfg or name + 'stick' == cfg:
prefix = '* '
else:
prefix = f'* ({name}{ext}) '
if n > 1:
prefix = '!' + prefix[1:]
ambiguous += 1
desc = prefix + desc
prt('%s %s' % (cfg.ljust(keylen), desc))
prt(' ')
gap = ' ' * keylen
prt(f'{gap} * need sea')
if ambiguous:
print(f'{gap} ! {ambiguous} ambiguous mappings sea -> frappy')
def treat_args(self, argdict, unknown=(), extra=()):
if len(unknown) == 1:
cfg = unknown[0]
if cfg == 'develop':
argdict['service'] = cfg
return super().treat_args(argdict, (), ())
if (',' in cfg or cfg.endswith('.cfg') or
self.is_cfg(argdict.get('ins'), argdict.get('service'), cfg)):
return super().treat_args(argdict, (), unknown)
return super().treat_args(argdict, unknown, extra)
def check_cfg_file(self, ins, service, cfg, needsea=False):
if cfg is 'none':
return ''
try:
desc, sea_cfg = self.cfg_details(self, ins, service, cfg)
if needsea and not sea_cfg:
return None
return cfg
except Exception:
return cfg + '?'
def guess_cfgs(self, ins, cfgs):
sea = SeaManager()
seacfgs = sea.get_cfg(ins, 'sea').split('/')
sea_info = {}
if len(seacfgs) < 2:
seacfgs.append('')
self.all_cfg(ins, None, sea_info=sea_info)
guess = defaultdict(lambda: defaultdict(list))
def check_cfg(service, ext, frappyset, seacfgs):
for seacfg in seacfgs:
available = sea_info.get(seacfg + ext, ())
if available:
for a in available:
if a in frappyset:
guess[service]['ok'].append(a)
break
else:
if len(available) == 1:
available = next(iter(available))
# available is either a string or a set of strings
guess[service]['proposed'].append(available)
else:
guess[service]['missing'].append(seacfg)
main = cfgs.get('main')
check_cfg('main', '.config', set() if main is None else {main}, {seacfgs[0]})
if len(seacfgs) > 1:
stick = cfgs.get('stick')
check_cfg('stick', '.stick', set() if stick is None else {stick}, {seacfgs[1]})
if len(seacfgs) > 2:
addons = set(cfgs.get('addons').split(','))
check_cfg('addons', '.addons', set(addons), set(seacfgs[2:]))
return guess