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
This commit is contained in:
zolliker 2023-09-20 17:01:06 +02:00
parent 957a5f0b2c
commit fc364f0fc6
3 changed files with 148 additions and 80 deletions

View File

@ -164,13 +164,13 @@ class ServiceManager:
"""return cfg info about running programs, if relevant
example for sea: return samenv name
if service is None return a dict <service> of <cfg>
"""
cfginfo = {}
self.get_procs(self.group, cfginfo)
result = cfginfo.get((ins, service))
if result:
return result
return ''
if service is None:
return {s: cfginfo.get((ins, s)) or '' for s in self.services}
return cfginfo.get((ins, service)) or ''
def get_procs(self, groups=None, cfginfo=None):
"""return processes

View File

@ -22,23 +22,41 @@
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
def dummy(*args, **kwds):
pass
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
class Node:
description = ''
def __call__(self, equipment_id, description, *args, **kwds):
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'
@ -156,70 +174,99 @@ class FrappyManager(ServiceManager):
init(*nodes)
interact()
def all_cfg(self, ins, service, details=False):
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 details:
True: return a dict of <cfgdir> of <cfg> of <description>
False: return a set of <cfg>
: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
"""
result = {}
all_cfg = set()
if not ins:
return {}
namespace = {k: dummy for k in ('Mod', 'Param', 'Command', 'Group')}
namespace['Node'] = node = Node()
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):
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 = ''
for cfgfile in glob(join(cfgdir, '*_cfg.py')):
cfg = basename(cfgfile)[:-7]
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
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:
all_cfg = self.all_cfg(ins, service, True)
self.all_cfg(ins, service, list_info, sea_info)
else:
all_cfg = {}
for service in self.services:
all_cfg.update(self.all_cfg(ins, service, True))
for cfgdir, cfgs in all_cfg.items():
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)
keylen = max(len(k) for k in cfgs)
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:
@ -228,35 +275,55 @@ class FrappyManager(ServiceManager):
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'))):
self.is_cfg(argdict.get('ins'), argdict.get('service'), cfg)):
return super().treat_args(argdict, (), unknown)
return super().treat_args(argdict, unknown, extra)
def cfg_from_sea(self, ins):
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()
sea.get_info()
cfgs, confirmed = sea.get_cfg(ins, 'sea', True)
cfgs = cfgs.split('/')
result = {}
seacfgs = sea.get_cfg(ins, 'sea').split('/')
sea_info = {}
if len(seacfgs) < 2:
seacfgs.append('')
self.all_cfg(ins, None, sea_info=sea_info)
def check_cfg_file(cfg, allcfg):
if cfg is 'none':
return ''
return cfg if cfg in allcfg else cfg + '?'
guess = defaultdict(lambda: defaultdict(list))
allmain = self.all_cfg(ins, 'main')
if cfgs[0]:
result['main'] = check_cfg_file(cfgs[0], allmain)
if len(cfgs) > 1:
stick = cfgs[1]
allsticks = self.all_cfg(ins, 'stick')
if stick:
if stick not in allsticks and stick in allmain:
stick += 'stick'
result['stick'] = check_cfg_file(stick, allsticks)
alladdons = self.all_cfg(ins, 'addons')
addons = [check_cfg_file(a, alladdons) for a in cfgs[2:]]
if addons:
result['addons'] = ','.join(addons)
result['confirmed'] = confirmed
return result
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

View File

@ -118,7 +118,7 @@ class SeaManager(ServiceManager):
"""
if service != 'sea': # ignore when service == 'graph'
return ''
if 'sea' not in self.get_procs().get(ins):
if 'sea' not in self.get_procs().get(ins, ()):
return ''
try:
searoot = self.env[ins].get('SEA_ROOT', '')
@ -135,7 +135,7 @@ class SeaManager(ServiceManager):
if match:
key, dev, addon = match.groups()
if addon:
if addon != result[1]:
if addon != result[1]: # skip stick appearing also in addon_list
result.append(addon)
elif key == 'name':
result[0] = dev
@ -145,8 +145,9 @@ class SeaManager(ServiceManager):
confirmed = dev
if not result[-1]:
result.pop()
result = '/'.join(result)
return (result, confirmed) if addconfirmed else result
if addconfirmed:
result.insert(0, confirmed)
return '/'.join(result)
except Exception as e:
return repr(e)