From fc364f0fc60b26d0a0d80186eaa78e2784182132 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Wed, 20 Sep 2023 17:01:06 +0200 Subject: [PATCH] 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 --- base.py | 8 +- frappyman.py | 211 +++++++++++++++++++++++++++++++++------------------ seaman.py | 9 ++- 3 files changed, 148 insertions(+), 80 deletions(-) diff --git a/base.py b/base.py index fe7d524..e7da651 100644 --- a/base.py +++ b/base.py @@ -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 of """ 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 diff --git a/frappyman.py b/frappyman.py index 5a65fc6..8837205 100644 --- a/frappyman.py +++ b/frappyman.py @@ -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 of of - False: return a set of + :param list_info: None or a dict to collect info in the form + dict of of + :param sea_info: None or a dict 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 diff --git a/seaman.py b/seaman.py index 31c06b8..7bdcfd1 100644 --- a/seaman.py +++ b/seaman.py @@ -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)