- fix an intend - add TODO: remove pkg - add .before_bott_at to backup of seastatus.tcl
474 lines
19 KiB
Python
474 lines
19 KiB
Python
# *****************************************************************************
|
|
#
|
|
# 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 itertools import zip_longest
|
|
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
|
|
|
|
|
|
MAIN = 1
|
|
STICK = 2
|
|
|
|
|
|
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
|
|
|
|
|
|
SEAEXT = {'main': '.config', 'stick': '.stick'}
|
|
|
|
|
|
class Reconnect(str):
|
|
"""a string tagged with the information that the frappy server is already running
|
|
|
|
isinstance(cfg, Reconnect) means: cfg may need reconnection, but not restart
|
|
"""
|
|
def __repr__(self):
|
|
return f'Reconnect({str(self)!r})'
|
|
|
|
|
|
class Keep(Reconnect):
|
|
"""a string tagged with the information to keep the connection as given
|
|
|
|
isinstance(cfg, Reconnect) means: the given cfg is already running, so no reconnection is needed
|
|
"""
|
|
def __repr__(self):
|
|
return f'Keep({str(self)!r})'
|
|
|
|
|
|
class FrappyManager(ServiceManager):
|
|
group = 'frappy'
|
|
services = ('main', 'stick', 'addons')
|
|
USAGE = """
|
|
Usage:
|
|
|
|
frappy list %(optional_ins)s
|
|
frappy start %(ins)s<service> <cfgfiles>
|
|
frappy restart %(ins)s[<service>] [<cfgfiles>] %(remark)s
|
|
frappy stop %(ins)s[<service>] %(remark)s
|
|
frappy listcfg %(ins)s[<service> | develop] # list available cfg files
|
|
|
|
<service> is one of main, stick, addons
|
|
%(legend)s
|
|
"""
|
|
# changed in all_info (see docstring there)
|
|
frappy2sea = None
|
|
sea2frappy = None
|
|
list_info = None
|
|
# changed in get_server_state:
|
|
frappy_cfgs = None
|
|
sea_cfgs = None
|
|
state = None
|
|
error = None
|
|
remarks = None
|
|
sea = None
|
|
|
|
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, opts=''):
|
|
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, opts=opts)
|
|
|
|
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()
|
|
|
|
@staticmethod
|
|
def get_cfg_details(namespace, cfgfile):
|
|
# get sea_cfg option from frappy cfg file
|
|
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, details=False):
|
|
"""get available cfg files
|
|
|
|
:param ins: instance
|
|
:param service: service nor None for all services
|
|
:param details: get details about relation to sea
|
|
:return: see param:`what`
|
|
|
|
implicit results:
|
|
self.frappy2sea: a dict <frappycfg> of <seacfg> (including extension .config/.stick/.addon)
|
|
self.sea2frappy: a dict <seacfg> of set of <frappycfg>
|
|
self.list_info: dict <cfgdir> of <cfg> of <description>
|
|
"""
|
|
all_cfg = set()
|
|
if not ins:
|
|
return {}
|
|
namespace = Namespace()
|
|
if details:
|
|
self.frappy2sea = f2s = {}
|
|
self.sea2frappy = s2f = {}
|
|
self.list_info = list_info = {}
|
|
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:
|
|
# sea_info.setdefault(sea_cfg, set()).add(cfg)
|
|
f2s[cfg] = sea_cfg
|
|
s2f.setdefault(sea_cfg, set()).add(cfg)
|
|
list_info.setdefault(cfgdir, {})[cfg] = desc.split('\n', 1)[0]
|
|
all_cfg.add(cfg)
|
|
# if service == 'main':
|
|
# sea_info['none.config'] = {''}
|
|
return all_cfg
|
|
|
|
def do_listcfg(self, ins='', service='', prt=print):
|
|
if not ins:
|
|
raise UsageError('missing instance')
|
|
self.all_cfg(ins, service, True)
|
|
seacfgpat = re.compile(r'(.*)(\.config|\.stick|\.addon)')
|
|
keylen = max((max(len(k) for k in cfgs) for cfgs in self.list_info.values()), default=1)
|
|
ambiguous = set()
|
|
for cfgdir, cfgs in self.list_info.items():
|
|
if cfgs:
|
|
prt('')
|
|
prt('--- %s:' % cfgdir)
|
|
for cfg, desc in sorted(cfgs.items(), key=lambda v: (v[0].lower(), v)):
|
|
seacfg = self.frappy2sea.get(cfg)
|
|
if seacfg:
|
|
name, ext = seacfgpat.match(seacfg).groups()
|
|
if name == cfg or name + 'stick' == cfg:
|
|
prefix = '* '
|
|
else:
|
|
prefix = f'* ({name}{ext}) '
|
|
if len(self.sea2frappy[seacfg]) > 1:
|
|
prefix = '!' + prefix[1:]
|
|
ambiguous.add(seacfg)
|
|
desc = prefix + desc
|
|
prt('%s %s' % (cfg.ljust(keylen), desc))
|
|
prt(' ')
|
|
gap = ' ' * keylen
|
|
prt(f'{gap} * need sea')
|
|
if ambiguous:
|
|
prt(f'{gap} ! {len(ambiguous)} ambiguous mappings sea -> frappy')
|
|
|
|
def treat_args(self, argdict, unknown=(), extra=()):
|
|
cfg = None
|
|
extra = list(extra)
|
|
for arg in unknown:
|
|
if arg.startswith('-'): # this is an option
|
|
extra.append(arg)
|
|
elif cfg is None:
|
|
cfg = arg
|
|
else:
|
|
cfg = ''
|
|
if cfg:
|
|
if cfg == 'develop':
|
|
argdict['service'] = cfg
|
|
return super().treat_args(argdict, (), extra)
|
|
if (',' in cfg or cfg.endswith('.cfg') or
|
|
self.is_cfg(argdict.get('ins'), argdict.get('service'), cfg)):
|
|
return super().treat_args(argdict, (), [cfg] + extra)
|
|
return super().treat_args(argdict, unknown, extra)
|
|
|
|
def check_cfg_file(self, ins, service, cfg, needsea=False):
|
|
if cfg == '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 get_server_state(self, ins, givencfgs):
|
|
"""get proposed configuration and an overview of the running servers
|
|
|
|
:param ins: the instance to be checked for
|
|
:param givencfgs: a dict <service> of cfg given by the ECS
|
|
:return: a dict <service> of cfg, where cfg is either:
|
|
- a bare string: this cfg is proposed to change to this value
|
|
- Reconnect(cfg): the frappy server is running as expected, but the given cfg does not match
|
|
- Keep(cfg): no change needed
|
|
remark: Reconnect amd Keep inherit from str, so Reconnect(cfg) == cfg is always True
|
|
|
|
implicit results:
|
|
self.remarks: a dict <service> of remark (why should this be changed?)
|
|
self.frappy_cfgs: a dict <service> of running cfgs
|
|
self.sea_cfgs: a dict <service> of sea cfgs (without ending .config/.stick/.addon)
|
|
self.state: a dict ('sea <service>' | 'frappy <service>') of cfg summarizing the state of all servers
|
|
a change of self.state indicates that the configuration may need to be reevaluated
|
|
self.error: there is an ambiguity for the mapping seacfg -> frappycfg
|
|
self.sea: a fresh SeaManager instance
|
|
"""
|
|
self.frappy_cfgs = self.get_cfg(ins, None) # dict <service> of running frappy servers
|
|
self.sea = SeaManager()
|
|
seaconfig = self.sea.get_cfg(ins, 'sea')
|
|
sealist = seaconfig.split('/') # <config>, <stick>, <addon1>, <addon2> ...
|
|
if len(sealist) < 2:
|
|
sealist.append('')
|
|
|
|
all_cfg = self.all_cfg(ins, None, True)
|
|
proposed_cfg = {}
|
|
self.sea_cfgs = {}
|
|
self.remarks = {}
|
|
self.error = False
|
|
self.state = {}
|
|
result = {}
|
|
for (service, ext), seacfg in zip_longest(SEAEXT.items(), sealist[0:2], fillvalue=(None, None)):
|
|
if service:
|
|
self.sea_cfgs[service] = seacfg
|
|
proposed = self.sea2frappy.get(seacfg + ext) or set()
|
|
if seaconfig:
|
|
proposed_cfg[service] = proposed
|
|
self.state[f'sea {service}'] = seacfg
|
|
|
|
running_addons = self.frappy_cfgs.get('addons')
|
|
running_addons = [v.strip() for v in running_addons.split(',') if v.strip()]
|
|
proposed_addons = [] # use list instead of set for keeping order
|
|
running_sea_addons = set()
|
|
for cfg in running_addons:
|
|
seacfg = self.frappy2sea.get(cfg.strip())
|
|
if seacfg is None:
|
|
proposed_addons.append(cfg) # addons with no sea cfg should be kept
|
|
elif seacfg.endswith('.addon') and seacfg[:-6] in sealist[2:]:
|
|
proposed_addons.append(cfg)
|
|
running_sea_addons.add(seacfg)
|
|
|
|
for scfg in sealist[2:]:
|
|
seacfg = scfg + '.addon'
|
|
if seacfg not in running_sea_addons:
|
|
proposed = self.sea2frappy.get(seacfg) or set()
|
|
if proposed:
|
|
if len(proposed) > 1:
|
|
self.error = f'ambiguous frappy cfg for {seacfg}.addon: {proposed}'
|
|
else:
|
|
proposed = list(proposed)[0]
|
|
if proposed not in proposed_addons:
|
|
proposed_addons.append(proposed)
|
|
self.sea_cfgs['addons'] = ','.join(sealist[2:])
|
|
if proposed_addons: # and set(proposed_addons) != set(running_addons):
|
|
proposed_cfg['addons'] = {','.join(proposed_addons)}
|
|
|
|
for service in FrappyManager.services:
|
|
given = givencfgs.get(service)
|
|
running = self.frappy_cfgs.get(service)
|
|
available = proposed_cfg.get(service)
|
|
|
|
if running:
|
|
self.state[f'frappy {service}'] = running
|
|
|
|
if seaconfig and (available or running in self.frappy2sea):
|
|
# we get here when the sea server is running and either at least one of:
|
|
# - the sea config is matching any frappy cfg
|
|
# - the running frappy cfg is matching a sea config
|
|
if not available:
|
|
self.remarks[service] = 'no frappy config'
|
|
continue
|
|
proposed = list(available)[0] if len(available) == 1 else None
|
|
if proposed is None:
|
|
running = None
|
|
for item in available:
|
|
if item == given:
|
|
proposed = item
|
|
if item == running:
|
|
running = item
|
|
if running and not proposed:
|
|
proposed = running
|
|
if proposed is None:
|
|
self.remarks[service] = 'ambiguous frappy cfg'
|
|
self.error = f'ambiguous frappy cfg for {seacfg}: {available}'
|
|
else:
|
|
self.remarks[service] = ''
|
|
if running == proposed:
|
|
if given == running:
|
|
result[service] = Keep(running)
|
|
elif running:
|
|
result[service] = proposed
|
|
self.remarks[service] = 'restart frappy'
|
|
else:
|
|
result[service] = proposed
|
|
else:
|
|
# we get here when sea is not running or all of:
|
|
# - the sea config has no matching frappy cfg
|
|
# - the frappy cfg has no matching sea config
|
|
if running:
|
|
if self.frappy2sea.get(running, '').endswith(ext):
|
|
result[service] = running
|
|
self.remarks[service] = 'restart frappy to start sea'
|
|
elif given != running:
|
|
result[service] = Reconnect(running)
|
|
self.remarks[service] = 'reconnect needed'
|
|
else:
|
|
result[service] = Keep(running)
|
|
self.remarks[service] = ''
|
|
elif given:
|
|
if self.frappy2sea.get(given, '').endswith(ext):
|
|
result[service] = running
|
|
self.remarks[service] = 'restart frappy and sea'
|
|
elif ':' in given:
|
|
self.remarks[service] = 'external device'
|
|
elif given in all_cfg:
|
|
result[service] = running
|
|
self.remarks[service] = 'restart frappy'
|
|
else:
|
|
result[service] = ''
|
|
self.remarks[service] = f'unknown frappy cfg {given}'
|
|
|
|
return result
|
|
|