servicemanager/frappyman.py

261 lines
10 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
from glob import glob
from os.path import join, isdir, basename, expanduser, exists
from configparser import ConfigParser
from .base import ServiceManager, ServiceDown, UsageError
from .seaman import SeaManager
def dummy(*args, **kwds):
pass
class Node:
description = ''
def __call__(self, equipment_id, description, *args, **kwds):
self.description = description
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)
for key, value in env.items():
if '<SERV>' in value:
env[key] = value.replace('<SERV>', service)
os.environ[key] = env[key]
cfgpaths = self.config_dirs(ins, service)
if cfgpaths:
env['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['PYTHONPATH'] = os.environ['PYTHONPATH'] = ':'.join(pypathlist)
return start_dir, env
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 all_cfg(self, ins, service, details=False):
"""get available cfg files
:param ins: instance
:param service: service nor None for all services
:param details:
True: return a dict of <cfgdir> of <cfg> of <description>
False: return a set of <cfg>
"""
result = {}
all_cfg = set()
if not ins:
return {}
namespace = {k: dummy for k in ('Mod', 'Param', 'Command', 'Group')}
namespace['Node'] = node = Node()
for service in [service] if service else self.services:
for cfgdir in self.config_dirs(ins, service):
result.setdefault(cfgdir, {})
cfgs = result[cfgdir]
root = self.env[ins].get('FRAPPY_ROOT')
cfg_pattern ='*_cfg.py' if exists(join(root, 'frappy')) else '*.cfg'
for cfgfile in glob(join(cfgdir, cfg_pattern)):
desc = ''
if details:
try:
if cfgfile.endswith('.py'):
node.description = ''
try:
with open(cfgfile, encoding='utf-8') as f:
exec(f.read(), namespace)
except Exception as e:
node.description = repr(e)
desc = node.description
else:
parser = ConfigParser()
parser.read(cfgfile)
for s in parser.sections():
if s == 'NODE' or s.startswith('node '):
desc = parser[s].get('description', '').split('\n')[0]
break
except Exception:
pass
cfg = basename(cfgfile)[:1-len(cfg_pattern)]
if cfg not in all_cfg:
all_cfg.add(cfg)
cfgs[cfg] = desc
return result if details else all_cfg
def do_listcfg(self, ins='', service='', prt=print):
if not ins:
raise UsageError('missing instance')
if service:
all_cfg = self.all_cfg(ins, service, True)
else:
all_cfg = {}
for service in self.services:
all_cfg.update(self.all_cfg(ins, service, True))
for cfgdir, cfgs in all_cfg.items():
if cfgs:
prt('')
prt('--- %s:' % cfgdir)
keylen = max(len(k) for k in cfgs)
for cfg, desc in cfgs.items():
prt('%s %s' % (cfg.ljust(keylen), desc))
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
cfg in self.all_cfg(argdict.get('ins'), argdict.get('service'))):
return super().treat_args(argdict, (), unknown)
return super().treat_args(argdict, unknown, extra)
def cfg_from_sea(self, ins):
sea = SeaManager()
sea.get_info()
cfgs, confirmed = sea.get_cfg(ins, 'sea', True)
cfgs = cfgs.split('/')
result = {}
def check_cfg_file(cfg, allcfg):
if cfg is 'none':
return ''
return cfg if cfg in allcfg else cfg + '?'
allmain = self.all_cfg(ins, 'main')
allsticks = self.all_cfg(ins, 'stick')
if cfgs[0]:
result['main'] = check_cfg_file(cfgs[0], allmain)
if len(cfgs) > 1:
stick = cfgs[1]
if stick:
if stick not in allsticks and stick in allmain:
stick += 'stick'
result['stick'] = check_cfg_file(stick, allsticks)
addons = [check_cfg_file(a) for a in cfgs[2:]]
if addons:
result['addons'] = ','.join(addons)
result['confirmed'] = confirmed
return result