From 92d44f5c940c1f998e2e7a41fb2846b820dd2c31 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Sat, 23 Sep 2023 10:32:33 +0200 Subject: [PATCH 01/19] [WIP] overview in table form, test --- frappyman.py | 133 ++++++++++++++++++++++++++++++++++++----------- test_proposed.py | 24 +++++++++ 2 files changed, 126 insertions(+), 31 deletions(-) create mode 100644 test_proposed.py diff --git a/frappyman.py b/frappyman.py index 8837205..3dc1840 100644 --- a/frappyman.py +++ b/frappyman.py @@ -32,6 +32,10 @@ from .base import ServiceManager, ServiceDown, UsageError from .seaman import SeaManager +MAIN = 1 +STICK = 2 + + class Namespace(dict): def __init__(self): self['Node'] = self.node @@ -58,6 +62,83 @@ class Namespace(dict): __builtins__ = builtins +def make_proposed(givencfgs, ourcfgs, seacfgs, sea_info, strict=False): + overview = {} + for service in 'main', 'stick': + given = givencfgs.get(service) + if given: + overview.setdefault(service, {})['given'] = given + frappy = ourcfgs.get(service) + if frappy: + overview.setdefault(service, {})['frappy'] = frappy + for addon in givencfgs.get('addons', '').split(','): + addon = addon.strip() + if addon: + overview.setdefault(addon, {})['given'] = addon + for addon in ourcfgs.get('addons', '').split(','): + addon = addon.strip() + if addon: + overview.setdefault(addon, {})['frappy'] = addon + + error = False + result = {} + addons = set() + for i, seacfg in enumerate(seacfgs): + if i == 0: + key, ext = 'main', '.config' + elif i == 1: + key, ext = 'stick', '.stick' + else: + key, ext = None, '.addon' + available = sea_info.get(seacfg + ext, ()) + if available: + proposed = list(available)[0] if len(available) == 1 else None + if not proposed: + for item in available: + if item == overview.get(key or item, {}).get('given'): + proposed = item + if item == overview.get(key or item, {}).get('frappy'): + frappy = item + if frappy and not proposed: + proposed = frappy + if proposed: + itemdict = overview.setdefault(key or proposed, {}) + given = itemdict.get('given') + itemdict.update(name=(key or 'addon'), sea=seacfg, + remark='ok' if given == proposed else f'change to {proposed}') + if key: + result[key] = given + else: + addons.add(given) + else: + for item in available: + overview.setdefault(item, {}).update(name=item, remark='ambiguous', sea=seacfg) + error = True + else: + overview.setdefault(key or seacfg, {}).update(name=key or '', sea=seacfg, remark='missing frappy config') + error = True + + if addons: + result['addons'] = ','.join(addons) + + if error and strict: + result = {} + else: + for service, cfg in ourcfgs.items(): + key = cfg if service is 'addons' else service + if cfg: + prop = result.get(service, '') + if prop == cfg: + result[service] = True + + columns = ['name', 'given', 'frappy', 'sea', 'remark'] + rows = [columns[:-1] + [' ']] + [[d.get(c) or '' for c in columns] for d in overview.values()] + wid = [max(len(v) for v in column) for column in zip(*rows)] + # insert title lines + rows.insert(1, ['-' * w for w in wid]) + return result, [' '.join(v.ljust(w) for w, v in zip(wid, row)) for row in rows] + + class FrappyManager(ServiceManager): group = 'frappy' services = ('main', 'stick', 'addons') @@ -290,40 +371,30 @@ class FrappyManager(ServiceManager): except Exception: return cfg + '?' - def guess_cfgs(self, ins, cfgs): + def propose_cfgs(self, ins, givencfgs, strict=False): + """get proposed configuration and an overview of the running servers + + :param ins: the instance to be checked for + :param givencfgs: a dict of cfg given by the ECS + :param strict: in case of errors (ambiguities or missing frappy config files) + proposed will be return as None + + :return: a tuple (, ) + where is information from running servers: + a dict of dict with optional items 'given', 'frappy', 'sea', 'proposed', 'error' + + given: the values for the cfgs argument + frappy: the cfg from running frappy servers + sea: the sea config file running + proposed: proposed configuration for the ECS + error: information about missing or ambiguous cfg files + """ + ourcfgs = self.get_cfg(ins, None) sea = SeaManager() seacfgs = sea.get_cfg(ins, 'sea').split('/') - sea_info = {} if len(seacfgs) < 2: seacfgs.append('') + sea_info = {} self.all_cfg(ins, None, sea_info=sea_info) + return self.make_proposed(givencfgs, ourcfgs, seacfgs, sea_info, strict) - 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 diff --git a/test_proposed.py b/test_proposed.py new file mode 100644 index 0000000..2cf3c29 --- /dev/null +++ b/test_proposed.py @@ -0,0 +1,24 @@ +import pytest +from servicemanager.frappyman import make_proposed + +sea_info = { + 'ma10.config': {'ma10'}, + 'ma10.stick': {'ma10stick', 'ma10'}, + 'rt.addon': {'rt', 'roomt'}, + 'befilter.addon': {'befilter'}, +} + +given = dict(main='ma10', stick='ma10stick') +cfgs = dict(given) +seacfg = ['ma10', 'ma10'] + +@pytest.mark.parametrize('given, current, seacfg, output, proposed, proposedstrict', [ + + ]) +def test_proposed(given, current, seacfg, output, proposed, proposedstrict): + prop, overview = make_proposed(given, current, seacfg, sea_info) + assert overview == output + assert prop == proposed + props, overview = make_proposed(given, current, seacfg, sea_info, True) + assert overview == output + assert props == proposedstrict From 7ac25158eb4c2ca7918ba96ad8166510d8ccdfb4 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Fri, 6 Oct 2023 17:24:05 +0200 Subject: [PATCH 02/19] [WIP] improvements overview/test --- frappyman.py | 99 ++++++++++++++++++++++++++++++++---------------- test_proposed.py | 17 +++++++-- 2 files changed, 79 insertions(+), 37 deletions(-) diff --git a/frappyman.py b/frappyman.py index 3dc1840..c2f5e2f 100644 --- a/frappyman.py +++ b/frappyman.py @@ -25,7 +25,7 @@ import os import re import builtins from glob import glob -from collections import defaultdict +from itertools import zip_longest from os.path import join, isdir, basename, expanduser, exists from configparser import ConfigParser from .base import ServiceManager, ServiceDown, UsageError @@ -62,7 +62,14 @@ class Namespace(dict): __builtins__ = builtins -def make_proposed(givencfgs, ourcfgs, seacfgs, sea_info, strict=False): +SEAEXT = {'main': '.config', 'stick': '.stick'} + + +def get_service(key): + return key if key in ('main', 'stick') else 'addons' + + +def make_proposed(givencfgs, ourcfgs, seacfgs, sea_info, strict=False, giventitle='nicos'): overview = {} for service in 'main', 'stick': given = givencfgs.get(service) @@ -80,43 +87,67 @@ def make_proposed(givencfgs, ourcfgs, seacfgs, sea_info, strict=False): if addon: overview.setdefault(addon, {})['frappy'] = addon + seacfgfiles = [(c or '') + SEAEXT.get(s, '.addon') + for c, s in zip_longest(seacfgs, FrappyManager.services)] + seacfgset = set(seacfgfiles) + inverted_sea_info = {k: [] for k in seacfgfiles} + for cfg, seacfg in sea_info.items(): + if seacfg in seacfgset: + inverted_sea_info[seacfg].append(cfg) error = False result = {} addons = set() - for i, seacfg in enumerate(seacfgs): - if i == 0: - key, ext = 'main', '.config' - elif i == 1: - key, ext = 'stick', '.stick' - else: - key, ext = None, '.addon' - available = sea_info.get(seacfg + ext, ()) + for seacfg, seacfgfile, key in zip_longest(seacfgs, seacfgfiles, ('main', 'stick')): + service = key or 'addons' + if not seacfg: + continue + available = inverted_sea_info[seacfgfile] if available: - proposed = list(available)[0] if len(available) == 1 else None + proposed = available[0] if len(available) == 1 else None if not proposed: + running = None for item in available: if item == overview.get(key or item, {}).get('given'): proposed = item if item == overview.get(key or item, {}).get('frappy'): - frappy = item - if frappy and not proposed: - proposed = frappy + running = item + if running and not proposed: + proposed = running if proposed: itemdict = overview.setdefault(key or proposed, {}) given = itemdict.get('given') - itemdict.update(name=(key or 'addon'), sea=seacfg, - remark='ok' if given == proposed else f'change to {proposed}') - if key: - result[key] = given + running = overview.get(key or proposed, {}).get('frappy') + if running == proposed: + remark = '' if given == running else 'reconnect' else: - addons.add(given) + remark = f'restart' + + itemdict.update(name=service, sea=seacfg, remark=remark) + if key: + result[key] = proposed + else: + addons.add(proposed) else: for item in available: - overview.setdefault(item, {}).update(name=item, remark='ambiguous', sea=seacfg) + overview.setdefault(item, {}).update(name=service, frappy=item, remark='ambiguous', sea=seacfg) error = True else: - overview.setdefault(key or seacfg, {}).update(name=key or '', sea=seacfg, remark='missing frappy config') + overview.setdefault(key or seacfg, {}).update(name=service, sea=seacfg, + remark='missing frappy config') error = True + restart = set() + for key, itemdict in overview.items(): + running = itemdict.get('frappy') + if running: + service = get_service(key) + if service == 'addons': + addons.add(running) + if not itemdict.get('sea'): + if sea_info.get(running): + restart.add(service) + itemdict['remark'] = f'restart to start sea' + elif itemdict.get('given') != running: + itemdict['remark'] = f'reconnect' if addons: result['addons'] = ','.join(addons) @@ -125,18 +156,17 @@ def make_proposed(givencfgs, ourcfgs, seacfgs, sea_info, strict=False): result = {} else: for service, cfg in ourcfgs.items(): - key = cfg if service is 'addons' else service if cfg: prop = result.get(service, '') - if prop == cfg: + if prop == cfg and service not in restart: result[service] = True columns = ['name', 'given', 'frappy', 'sea', 'remark'] - rows = [columns[:-1] + [' ']] + [[d.get(c) or '' for c in columns] for d in overview.values()] + rows = [['', giventitle, 'frappy', 'sea', '']] + [[d.get(c) or '' for c in columns] for d in overview.values()] wid = [max(len(v) for v in column) for column in zip(*rows)] # insert title lines - rows.insert(1, ['-' * w for w in wid]) - return result, [' '.join(v.ljust(w) for w, v in zip(wid, row)) for row in rows] + rows.insert(1, [''] + ['-' * w for w in wid[1:-1]] + ['']) + return result, [' '.join(v.ljust(w) for w, v in zip(wid, row)) for row in rows] class FrappyManager(ServiceManager): @@ -284,7 +314,8 @@ class FrappyManager(ServiceManager): :param service: service nor None for all services :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 + :param sea_info: None or a dict of with info about sea configs + in frappy cfgs to be populated :return: set of available config """ all_cfg = set() @@ -305,6 +336,7 @@ class FrappyManager(ServiceManager): if cfg not in all_cfg: if sea_cfg and sea_info is not None: sea_info.setdefault(sea_cfg, set()).add(cfg) + sea_info[cfg] = sea_cfg all_cfg.add(cfg) if list_info is not None: list_info.setdefault(cfgdir, {})[cfg] = desc.split('\n', 1)[0] @@ -320,11 +352,10 @@ class FrappyManager(ServiceManager): 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}) + sea_ambig = {} # collect info about ambiguous sea info + for cfg, seacfg in sea_info.items(): + sea_ambig.setdefault(seacfg, set()).add(cfg) ambiguous = 0 keylen = max(max(len(k) for k in cfgs) for cfgs in list_info.values()) for cfgdir, cfgs in list_info.items(): @@ -332,12 +363,14 @@ class FrappyManager(ServiceManager): prt('') prt('--- %s:' % cfgdir) for cfg, desc in cfgs.items(): - if cfg in sea_ambig: - name, ext, n = sea_ambig[cfg] + seacfg = sea_info.get(cfg) + if seacfg: + name, ext = seacfgpat.match(seacfg).groups() if name == cfg or name + 'stick' == cfg: prefix = '* ' else: prefix = f'* ({name}{ext}) ' + n = len(sea_ambig.get(seacfg)) if n > 1: prefix = '!' + prefix[1:] ambiguous += 1 diff --git a/test_proposed.py b/test_proposed.py index 2cf3c29..4aafb58 100644 --- a/test_proposed.py +++ b/test_proposed.py @@ -2,10 +2,13 @@ import pytest from servicemanager.frappyman import make_proposed sea_info = { - 'ma10.config': {'ma10'}, - 'ma10.stick': {'ma10stick', 'ma10'}, - 'rt.addon': {'rt', 'roomt'}, - 'befilter.addon': {'befilter'}, + 'ma10': 'ma10.config', + 'ma10stick': 'ma10.stick', + 'ma11': 'ma11.config', + 'ma11stick': 'ma11.stick', + 'roomt': 'rt.addon', + 'rt': 'rt.addon', + 'befilter': 'befilter.addon', } given = dict(main='ma10', stick='ma10stick') @@ -22,3 +25,9 @@ def test_proposed(given, current, seacfg, output, proposed, proposedstrict): props, overview = make_proposed(given, current, seacfg, sea_info, True) assert overview == output assert props == proposedstrict + + +def do(): + prop, tab = make_proposed(given, cfgs, seacfg, sea_info) + print(prop) + print('\n'.join(tab)) \ No newline at end of file From 7cc025bef482552f9cbee78a69147d7340708704 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Tue, 17 Oct 2023 11:01:33 +0200 Subject: [PATCH 03/19] [WIP] introduce get_server_state propose_cfgs will be obsolete --- frappyman.py | 132 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 131 insertions(+), 1 deletion(-) diff --git a/frappyman.py b/frappyman.py index c2f5e2f..02a604c 100644 --- a/frappyman.py +++ b/frappyman.py @@ -65,6 +65,7 @@ class Namespace(dict): SEAEXT = {'main': '.config', 'stick': '.stick'} +# TODO: put inline def get_service(key): return key if key in ('main', 'stick') else 'addons' @@ -169,6 +170,112 @@ def make_proposed(givencfgs, ourcfgs, seacfgs, sea_info, strict=False, giventitl return result, [' '.join(v.ljust(w) for w, v in zip(wid, row)) for row in rows] +def summarize_server_state(givencfgs, ourcfgs, seacfgs, sea_info, strict=False): + """get a guess for the configuration from information about running services + + :param givencfgs: dict of given configuration (from nicos cache) + :param outcfgs: dict of running configuration (frappy) + :param seacfgs: dict of running configuration (sea) + :param sea_info: dict of with info about sea configs + :param strict: when True return empty cfg result on error + :return: tuple (, , (, ), (, ) where: + : proposed config not sure + : dict of proposed cfg + : dict of items runnning in frappy servers (addons are separated) + : dict of items running on the sea server + : dict of given items (addons are separated) + : dict of actions / remarks + """ + givencfgs = dict(givencfgs) + addons = givencfgs.pop('addons', '') + for addon in addons.split(','): + addon = addon.strip() + if addon: + givencfgs[addon] = addon + frappycfgs = dict(ourcfgs) + addons = givencfgs.pop('addons', '') + for addon in addons.split(','): + addon = addon.strip() + if addon: + frappycfgs[addon] = addon + + seacfgfiles = [(c or '') + SEAEXT.get(s, '.addon') + for c, s in zip_longest(seacfgs, FrappyManager.services)] + seacfgset = set(seacfgfiles) + inverted_sea_info = {k: [] for k in seacfgfiles} + for cfg, seacfg in sea_info.items(): + if seacfg in seacfgset: + inverted_sea_info[seacfg].append(cfg) + error = False + result = {} + addons = set() + seacfgs = {} + remarks = {} + for seacfg, seacfgfile, key in zip_longest(seacfgs, seacfgfiles, ('main', 'stick')): + if not seacfg: + continue + available = inverted_sea_info[seacfgfile] + if available: + proposed = available[0] if len(available) == 1 else None + if not proposed: + running = None + for item in available: + if item == givencfgs.get(key or item): + proposed = item + if item == frappycfgs.get(key or item): + running = item + if running and not proposed: + proposed = running + if proposed: + pkey = key or proposed + given = givencfgs.get(pkey) + running = frappycfgs.get(pkey) + if running == proposed: + remarks[pkey] = '' if given == running else 'reconnect' + else: + remarks[pkey] = 'restart' + + seacfgs[pkey] = seacfg + if key: + result[key] = proposed + else: + addons.add(proposed) + else: + for item in available: + remarks[item] = 'ambiguous' + seacfgs[item] = seacfg + error = True + else: + pkey = key or seacfg + seacfg[pkey] = seacfg + remarks[pkey] = 'missing frappy config' + error = True + restart = set() + for key, running in frappycfgs.items(): + if running: + service = get_service(key) + if service == 'addons': + addons.add(running) + if not seacfgs.get(key): + if sea_info.get(running): + restart.add(service) + remarks[key] = 'restart to start sea' + elif givencfgs.get(key) != running: + remarks[key] = 'reconnect' + elif remarks.get(key) is None: + remarks[key] = '' + if addons: + result['addons'] = ','.join(addons) + + if not error: + for service, cfg in ourcfgs.items(): + if cfg: + prop = result.get(service, '') + if prop == cfg and service not in restart: + result[service] = True + return error, result, (ourcfgs, seacfgs), (givencfgs, remarks) + + class FrappyManager(ServiceManager): group = 'frappy' services = ('main', 'stick', 'addons') @@ -429,5 +536,28 @@ class FrappyManager(ServiceManager): seacfgs.append('') sea_info = {} self.all_cfg(ins, None, sea_info=sea_info) - return self.make_proposed(givencfgs, ourcfgs, seacfgs, sea_info, strict) + return make_proposed(givencfgs, ourcfgs, seacfgs, sea_info, strict) + + 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 of cfg given by the ECS + + :return: tuple (, , (, ), (, ) where: + : proposed config not sure + : dict of proposed cfg + : dict of items runnning in frappy servers (addons are separated) + : dict of items running on the sea server + : dict of given items (addons are separated) + : dict of actions / remarks + """ + ourcfgs = self.get_cfg(ins, None) + sea = SeaManager() + seacfgs = sea.get_cfg(ins, 'sea').split('/') + if len(seacfgs) < 2: + seacfgs.append('') + sea_info = {} + self.all_cfg(ins, None, sea_info=sea_info) + return summarize_server_state(givencfgs, ourcfgs, seacfgs, sea_info) From 14c7100ca81aced8604eceb82dd9dbfeb9c68be2 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Tue, 17 Oct 2023 11:11:24 +0200 Subject: [PATCH 04/19] [WIP] bug fix: sealist / seacfgs are different --- frappyman.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frappyman.py b/frappyman.py index 02a604c..e0c901e 100644 --- a/frappyman.py +++ b/frappyman.py @@ -170,12 +170,12 @@ def make_proposed(givencfgs, ourcfgs, seacfgs, sea_info, strict=False, giventitl return result, [' '.join(v.ljust(w) for w, v in zip(wid, row)) for row in rows] -def summarize_server_state(givencfgs, ourcfgs, seacfgs, sea_info, strict=False): +def summarize_server_state(givencfgs, ourcfgs, sealist, sea_info, strict=False): """get a guess for the configuration from information about running services :param givencfgs: dict of given configuration (from nicos cache) :param outcfgs: dict of running configuration (frappy) - :param seacfgs: dict of running configuration (sea) + :param sealist: list of running sea configuration :param sea_info: dict of with info about sea configs :param strict: when True return empty cfg result on error :return: tuple (, , (, ), (, ) where: @@ -200,7 +200,7 @@ def summarize_server_state(givencfgs, ourcfgs, seacfgs, sea_info, strict=False): frappycfgs[addon] = addon seacfgfiles = [(c or '') + SEAEXT.get(s, '.addon') - for c, s in zip_longest(seacfgs, FrappyManager.services)] + for c, s in zip_longest(sealist, FrappyManager.services)] seacfgset = set(seacfgfiles) inverted_sea_info = {k: [] for k in seacfgfiles} for cfg, seacfg in sea_info.items(): @@ -211,7 +211,7 @@ def summarize_server_state(givencfgs, ourcfgs, seacfgs, sea_info, strict=False): addons = set() seacfgs = {} remarks = {} - for seacfg, seacfgfile, key in zip_longest(seacfgs, seacfgfiles, ('main', 'stick')): + for seacfg, seacfgfile, key in zip_longest(sealist, seacfgfiles, ('main', 'stick')): if not seacfg: continue available = inverted_sea_info[seacfgfile] @@ -247,7 +247,7 @@ def summarize_server_state(givencfgs, ourcfgs, seacfgs, sea_info, strict=False): error = True else: pkey = key or seacfg - seacfg[pkey] = seacfg + seacfgs[pkey] = seacfg remarks[pkey] = 'missing frappy config' error = True restart = set() From 2252fe7e86ca3907546de0c11e4886722146b8ec Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Tue, 17 Oct 2023 11:28:09 +0200 Subject: [PATCH 05/19] [WIP] better remark --- frappyman.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frappyman.py b/frappyman.py index e0c901e..c3c8487 100644 --- a/frappyman.py +++ b/frappyman.py @@ -232,8 +232,10 @@ def summarize_server_state(givencfgs, ourcfgs, sealist, sea_info, strict=False): running = frappycfgs.get(pkey) if running == proposed: remarks[pkey] = '' if given == running else 'reconnect' + elif running: + remarks[pkey] = 'frappy restart needed' else: - remarks[pkey] = 'restart' + remarks[pkey] = 'frappy start needed' seacfgs[pkey] = seacfg if key: From 7e34c6f6cd5c68146a08fa1a5ec42992f5e67016 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Tue, 17 Oct 2023 12:52:37 +0200 Subject: [PATCH 06/19] [WIP] do not skip handling proposed on error --- frappyman.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/frappyman.py b/frappyman.py index c3c8487..1e415b9 100644 --- a/frappyman.py +++ b/frappyman.py @@ -269,12 +269,11 @@ def summarize_server_state(givencfgs, ourcfgs, sealist, sea_info, strict=False): if addons: result['addons'] = ','.join(addons) - if not error: - for service, cfg in ourcfgs.items(): - if cfg: - prop = result.get(service, '') - if prop == cfg and service not in restart: - result[service] = True + for service, cfg in ourcfgs.items(): + if cfg: + prop = result.get(service, '') + if prop == cfg and service not in restart: + result[service] = True return error, result, (ourcfgs, seacfgs), (givencfgs, remarks) From ac4e6f851a86f32b1562b8e92c4cc3daf9999c2a Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Tue, 17 Oct 2023 13:06:40 +0200 Subject: [PATCH 07/19] [WIP] remove givencfgs from result --- frappyman.py | 10 ++++------ test_proposed.py | 18 ++++++------------ 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/frappyman.py b/frappyman.py index 1e415b9..7d63e86 100644 --- a/frappyman.py +++ b/frappyman.py @@ -178,12 +178,11 @@ def summarize_server_state(givencfgs, ourcfgs, sealist, sea_info, strict=False): :param sealist: list of running sea configuration :param sea_info: dict of with info about sea configs :param strict: when True return empty cfg result on error - :return: tuple (, , (, ), (, ) where: + :return: tuple (, , (, ), where: : proposed config not sure : dict of proposed cfg : dict of items runnning in frappy servers (addons are separated) : dict of items running on the sea server - : dict of given items (addons are separated) : dict of actions / remarks """ givencfgs = dict(givencfgs) @@ -274,7 +273,7 @@ def summarize_server_state(givencfgs, ourcfgs, sealist, sea_info, strict=False): prop = result.get(service, '') if prop == cfg and service not in restart: result[service] = True - return error, result, (ourcfgs, seacfgs), (givencfgs, remarks) + return error, result, (ourcfgs, seacfgs), remarks class FrappyManager(ServiceManager): @@ -545,13 +544,12 @@ class FrappyManager(ServiceManager): :param ins: the instance to be checked for :param givencfgs: a dict of cfg given by the ECS - :return: tuple (, , (, ), (, ) where: + :return: tuple (, , (, ), where: : proposed config not sure : dict of proposed cfg : dict of items runnning in frappy servers (addons are separated) : dict of items running on the sea server - : dict of given items (addons are separated) - : dict of actions / remarks + : dict of actions to do / remarks """ ourcfgs = self.get_cfg(ins, None) sea = SeaManager() diff --git a/test_proposed.py b/test_proposed.py index 4aafb58..3cc471c 100644 --- a/test_proposed.py +++ b/test_proposed.py @@ -1,5 +1,5 @@ import pytest -from servicemanager.frappyman import make_proposed +from servicemanager.frappyman import summarize_server_state sea_info = { 'ma10': 'ma10.config', @@ -15,19 +15,13 @@ given = dict(main='ma10', stick='ma10stick') cfgs = dict(given) seacfg = ['ma10', 'ma10'] -@pytest.mark.parametrize('given, current, seacfg, output, proposed, proposedstrict', [ +@pytest.mark.parametrize('given, current, seacfg, state, proposed', [ ]) -def test_proposed(given, current, seacfg, output, proposed, proposedstrict): - prop, overview = make_proposed(given, current, seacfg, sea_info) - assert overview == output - assert prop == proposed - props, overview = make_proposed(given, current, seacfg, sea_info, True) - assert overview == output - assert props == proposedstrict +def test_proposed(given, current, seacfg, result): + assert result == summarize_server_state(given, current, seacfg, sea_info) def do(): - prop, tab = make_proposed(given, cfgs, seacfg, sea_info) - print(prop) - print('\n'.join(tab)) \ No newline at end of file + result = summarize_server_state(given, cfgs, seacfg, sea_info) + print(result) From 20665ae6f6a1976022c7f345194173f4d656d329 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Tue, 17 Oct 2023 13:13:39 +0200 Subject: [PATCH 08/19] [WIP] fix: return frappycfgs instead fo ourcfgs --- frappyman.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappyman.py b/frappyman.py index 7d63e86..cd88ee5 100644 --- a/frappyman.py +++ b/frappyman.py @@ -273,7 +273,7 @@ def summarize_server_state(givencfgs, ourcfgs, sealist, sea_info, strict=False): prop = result.get(service, '') if prop == cfg and service not in restart: result[service] = True - return error, result, (ourcfgs, seacfgs), remarks + return error, result, (frappycfgs, seacfgs), remarks class FrappyManager(ServiceManager): From 085e493942d45059fd6fc10e94405f69001097a3 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Tue, 17 Oct 2023 13:18:32 +0200 Subject: [PATCH 09/19] [WIP] fix frappycfgs --- frappyman.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappyman.py b/frappyman.py index cd88ee5..3922521 100644 --- a/frappyman.py +++ b/frappyman.py @@ -192,7 +192,7 @@ def summarize_server_state(givencfgs, ourcfgs, sealist, sea_info, strict=False): if addon: givencfgs[addon] = addon frappycfgs = dict(ourcfgs) - addons = givencfgs.pop('addons', '') + addons = frappycfgs.pop('addons', '') for addon in addons.split(','): addon = addon.strip() if addon: From dc24f3585c52556c0ea178c1edc720cbd45f4158 Mon Sep 17 00:00:00 2001 From: l_samenv Date: Thu, 26 Oct 2023 10:18:51 +0200 Subject: [PATCH 10/19] frappyman: fix make_proposed --- frappyman.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/frappyman.py b/frappyman.py index 3922521..2890352 100644 --- a/frappyman.py +++ b/frappyman.py @@ -460,34 +460,34 @@ class FrappyManager(ServiceManager): for service in self.services: self.all_cfg(ins, service, list_info, sea_info) seacfgpat = re.compile(r'(.*)(\.config|\.stick|\.addon)') - sea_ambig = {} # collect info about ambiguous sea info - for cfg, seacfg in sea_info.items(): - sea_ambig.setdefault(seacfg, set()).add(cfg) + inverted_sea_info = {} + for seacfg, cfgset in sea_info.items(): + for cfg in cfgset: + inverted_sea_info[cfg] = seacfg 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(): - seacfg = sea_info.get(cfg) + for cfg, desc in sorted(cfgs.items(), key=lambda v: (v[0].lower(), v)): + seacfg = inverted_sea_info.get(cfg) if seacfg: name, ext = seacfgpat.match(seacfg).groups() if name == cfg or name + 'stick' == cfg: prefix = '* ' else: prefix = f'* ({name}{ext}) ' - n = len(sea_ambig.get(seacfg)) - if n > 1: - prefix = '!' + prefix[1:] - ambiguous += 1 + if len(sea_info[seacfg]) > 1: + prefix = '!' + prefix + ambiguous += 1 / len(sea_info[seacfg]) 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') + print(f'{gap} ! {round(ambiguous)} ambiguous mappings sea -> frappy') def treat_args(self, argdict, unknown=(), extra=()): if len(unknown) == 1: From 9ddb7eb3df70a33fc03f8bde39fdd0a82c54b404 Mon Sep 17 00:00:00 2001 From: l_samenv Date: Fri, 10 May 2024 11:09:04 +0200 Subject: [PATCH 11/19] [WIP] state as of 2024-05-10 on samenv --- __init__.py | 4 +- base.py | 65 +++++--- frappyman.py | 444 ++++++++++++++++++--------------------------------- nicosman.py | 15 +- seaman.py | 56 +++++-- 5 files changed, 250 insertions(+), 334 deletions(-) diff --git a/__init__.py b/__init__.py index cd847cc..c17b2b5 100644 --- a/__init__.py +++ b/__init__.py @@ -30,7 +30,7 @@ this code is currently used: from servicemanager.base import ServiceManager, ServiceDown, UsageError, get_config from servicemanager.nicosman import NicosManager from servicemanager.seaman import SeaManager -from servicemanager.frappyman import FrappyManager +from servicemanager.frappyman import FrappyManager, Reconnect, Keep class SewebManager(ServiceManager): @@ -93,7 +93,7 @@ def run(group, arglist): args.setdefault('action', 'gui') print('do you mean:\n %s %s %s %s %s' % (group, args.get('action', ''), args.get('ins', ''), args.get('service', ''), ' '.join(extra))) - else: + else: try: serv.action(args['action'], *serv.treat_args(args, extra + arglist)) except AttributeError: diff --git a/base.py b/base.py index e7da651..37a6430 100644 --- a/base.py +++ b/base.py @@ -146,7 +146,7 @@ class ServiceManager: nr = section.get(self.group) if nr is not None: nr = '%02d' % int(nr) - self.commands[ins] = command + self.commands[ins] = command.replace('~', expanduser('~')) services = self.get_services(section) env = {k: get_subs(section, k, ins, nr) for k in section if k.isupper()} result[ins] = services @@ -156,8 +156,12 @@ class ServiceManager: #def get_cmdpats(self, groups): # return self.cmdpats - def get_ins_info(self, ins): + def get_ins_info(self, ins=None): self.get_info() + if ins is None: + ins = os.environ.get('Instrument') + if ins is None: + return {} return self.info[ins] def get_cfg(self, ins, service): @@ -175,7 +179,7 @@ class ServiceManager: def get_procs(self, groups=None, cfginfo=None): """return processes - :param groups: group to look for or None for all groups + :param groups: group to look for or None for own groups :param cfginfo: cfginfo dict to be populated :result: a dict[ins] of dict[service] of list of tuples (process, cfg) """ @@ -211,6 +215,10 @@ class ServiceManager: return result def wildcard(self, ins): + """return a list of matching instruments + + or None, when no wildcard character in ins + """ if ins is None or ins == 'all': return list(self.info) pat = re.sub(r'(\.|\*)', '.*', ins) @@ -248,13 +256,16 @@ class ServiceManager: continue # already killed for i in range(10): # total 0.1 * 10 * 9 / 2 = 4.5 sec try: - p.wait(0.1 * i) - except psutil.TimeoutExpired: - if p.status() != psutil.STATUS_ZOMBIE: - if print_wait and i > 4: - print('wait for %s %s' % (ins, service)) - print_wait = False - continue + try: + p.wait(0.1 * i) + except psutil.TimeoutExpired: + if p.status() != psutil.STATUS_ZOMBIE: + if print_wait and i > 4: + print('wait for %s %s' % (ins, service)) + print_wait = False + continue + except psutil.NoSuchProcess: + pass # process stopped in the meantime self.stopped[ins][service] = ' '.join(p.info['cmdline']) break else: @@ -281,10 +292,12 @@ class ServiceManager: if ins not in self.env: self.get_info() gr = self.group.upper() + if not ins: + raise UsageError('need instance') env = self.env[ins] return env.get('%s_ROOT' % gr, ''), env - def do_start(self, ins, service=None, cfg='', restart=False, wait=False, logger=None): + def do_start(self, ins, service=None, cfg='', restart=False, wait=False, logger=None, opts=''): ins_list = self.wildcard(ins) if ins_list is not None: return ins_list @@ -323,7 +336,10 @@ class ServiceManager: services = to_start for service_i in services: port = service_ports[service_i] - cmd = self.commands[ins] % dict(ins=ins, serv=service_i, port=port, cfg=cfg, pkg=self.pkg) + cmd = self.commands[ins] % dict(ins=ins, serv=service_i, port=port, cfg=cfg, pkg=self.pkg or ins) + if opts: + cmd = f'{cmd} {opts}' + print(cmd) if '%(cfg)s' in self.commands[ins] and not cfg: cmd = self.stopped[ins].get(service_i) if not cmd: @@ -333,7 +349,7 @@ class ServiceManager: wd = os.getcwd() try: start_dir, env = self.prepare_start(ins, service_i, cfg) - env = dict(os.environ, **env) + env = dict(os.environ, **env, Instrument=ins) os.chdir(start_dir) if start_dir not in sys.path: sys.path.insert(0, start_dir) @@ -392,7 +408,7 @@ class ServiceManager: return ins_list self.do_start(ins, service, cfg, True, logger=logger) - def do_run(self, ins, service=None, cfg=None): + def do_run(self, ins, service=None, cfg=None, opts=''): """for tests: run and wait""" if self.wildcard(ins) is not None: raise UsageError('no wildcards allowed with %s run' % self.group) @@ -401,7 +417,7 @@ class ServiceManager: service, = self.services except ValueError: raise UsageError('need service to start (one of %s)' % ', '.join(self.services)) - self.do_start(ins, service, cfg, wait=True) + self.do_start(ins, service, cfg, wait=True, opts=opts) def do_list(self, ins=None, *args): """info about running services""" @@ -436,7 +452,7 @@ class ServiceManager: plist = procs_dict.get(serv) if plist: cfg = cfginfo.get((ins_i, serv), '') or sm.get_cfg(ins_i, serv) - if sm == self: + if sm == self or instances is None: show_ins = True gs = '%s %s' % (group, serv) port = str(port or '') @@ -483,12 +499,21 @@ class ServiceManager: print(str(e)) def do_help(self, *args): + options = {'ins': ' ', 'optional_ins': '[instance] ', 'remark': '*', 'legend': ''} + wildcards = True if self.main_ins: - usage = self.USAGE.replace('', '[instance]') % ( - '[instance] is empty or one of %s' % ', '.join(self.info)) + if len(self.info) == 1: + for key in options: + options[key] = '' + options['remark'] = ' ' + wildcards = False + else: + options['legend'] = ' [instance] is empty or one of %s\n' % ', '.join(self.info) else: - usage = self.USAGE % (' is one of %s' % ', '.join(self.info)) - print(usage) + options['legend'] = ' is one of %s\n' % ', '.join(self.info) + if wildcards: + options['legend'] += " * wildcards allowed, using '.' to replace 0 or more arbitrary characters in \n" + print(self.USAGE % options) def treat_args(self, argdict, unknown=(), extra=()): if unknown: diff --git a/frappyman.py b/frappyman.py index 2890352..acca2ff 100644 --- a/frappyman.py +++ b/frappyman.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # ***************************************************************************** # # This program is free software; you can redistribute it and/or modify it under @@ -26,6 +25,7 @@ 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 @@ -65,215 +65,22 @@ class Namespace(dict): SEAEXT = {'main': '.config', 'stick': '.stick'} -# TODO: put inline -def get_service(key): - return key if key in ('main', 'stick') else 'addons' +class Reconnect(str): + """a string tagged with the information that the frappy server is already running - -def make_proposed(givencfgs, ourcfgs, seacfgs, sea_info, strict=False, giventitle='nicos'): - overview = {} - for service in 'main', 'stick': - given = givencfgs.get(service) - if given: - overview.setdefault(service, {})['given'] = given - frappy = ourcfgs.get(service) - if frappy: - overview.setdefault(service, {})['frappy'] = frappy - for addon in givencfgs.get('addons', '').split(','): - addon = addon.strip() - if addon: - overview.setdefault(addon, {})['given'] = addon - for addon in ourcfgs.get('addons', '').split(','): - addon = addon.strip() - if addon: - overview.setdefault(addon, {})['frappy'] = addon - - seacfgfiles = [(c or '') + SEAEXT.get(s, '.addon') - for c, s in zip_longest(seacfgs, FrappyManager.services)] - seacfgset = set(seacfgfiles) - inverted_sea_info = {k: [] for k in seacfgfiles} - for cfg, seacfg in sea_info.items(): - if seacfg in seacfgset: - inverted_sea_info[seacfg].append(cfg) - error = False - result = {} - addons = set() - for seacfg, seacfgfile, key in zip_longest(seacfgs, seacfgfiles, ('main', 'stick')): - service = key or 'addons' - if not seacfg: - continue - available = inverted_sea_info[seacfgfile] - if available: - proposed = available[0] if len(available) == 1 else None - if not proposed: - running = None - for item in available: - if item == overview.get(key or item, {}).get('given'): - proposed = item - if item == overview.get(key or item, {}).get('frappy'): - running = item - if running and not proposed: - proposed = running - if proposed: - itemdict = overview.setdefault(key or proposed, {}) - given = itemdict.get('given') - running = overview.get(key or proposed, {}).get('frappy') - if running == proposed: - remark = '' if given == running else 'reconnect' - else: - remark = f'restart' - - itemdict.update(name=service, sea=seacfg, remark=remark) - if key: - result[key] = proposed - else: - addons.add(proposed) - else: - for item in available: - overview.setdefault(item, {}).update(name=service, frappy=item, remark='ambiguous', sea=seacfg) - error = True - else: - overview.setdefault(key or seacfg, {}).update(name=service, sea=seacfg, - remark='missing frappy config') - error = True - restart = set() - for key, itemdict in overview.items(): - running = itemdict.get('frappy') - if running: - service = get_service(key) - if service == 'addons': - addons.add(running) - if not itemdict.get('sea'): - if sea_info.get(running): - restart.add(service) - itemdict['remark'] = f'restart to start sea' - elif itemdict.get('given') != running: - itemdict['remark'] = f'reconnect' - - if addons: - result['addons'] = ','.join(addons) - - if error and strict: - result = {} - else: - for service, cfg in ourcfgs.items(): - if cfg: - prop = result.get(service, '') - if prop == cfg and service not in restart: - result[service] = True - - columns = ['name', 'given', 'frappy', 'sea', 'remark'] - rows = [['', giventitle, 'frappy', 'sea', '']] + [[d.get(c) or '' for c in columns] for d in overview.values()] - wid = [max(len(v) for v in column) for column in zip(*rows)] - # insert title lines - rows.insert(1, [''] + ['-' * w for w in wid[1:-1]] + ['']) - return result, [' '.join(v.ljust(w) for w, v in zip(wid, row)) for row in rows] - - -def summarize_server_state(givencfgs, ourcfgs, sealist, sea_info, strict=False): - """get a guess for the configuration from information about running services - - :param givencfgs: dict of given configuration (from nicos cache) - :param outcfgs: dict of running configuration (frappy) - :param sealist: list of running sea configuration - :param sea_info: dict of with info about sea configs - :param strict: when True return empty cfg result on error - :return: tuple (, , (, ), where: - : proposed config not sure - : dict of proposed cfg - : dict of items runnning in frappy servers (addons are separated) - : dict of items running on the sea server - : dict of actions / remarks + isinstance(cfg, Reconnect) means: cfg may need reconnection, but not restart """ - givencfgs = dict(givencfgs) - addons = givencfgs.pop('addons', '') - for addon in addons.split(','): - addon = addon.strip() - if addon: - givencfgs[addon] = addon - frappycfgs = dict(ourcfgs) - addons = frappycfgs.pop('addons', '') - for addon in addons.split(','): - addon = addon.strip() - if addon: - frappycfgs[addon] = addon + def __repr__(self): + return f'Reconnect({str(self)!r})' - seacfgfiles = [(c or '') + SEAEXT.get(s, '.addon') - for c, s in zip_longest(sealist, FrappyManager.services)] - seacfgset = set(seacfgfiles) - inverted_sea_info = {k: [] for k in seacfgfiles} - for cfg, seacfg in sea_info.items(): - if seacfg in seacfgset: - inverted_sea_info[seacfg].append(cfg) - error = False - result = {} - addons = set() - seacfgs = {} - remarks = {} - for seacfg, seacfgfile, key in zip_longest(sealist, seacfgfiles, ('main', 'stick')): - if not seacfg: - continue - available = inverted_sea_info[seacfgfile] - if available: - proposed = available[0] if len(available) == 1 else None - if not proposed: - running = None - for item in available: - if item == givencfgs.get(key or item): - proposed = item - if item == frappycfgs.get(key or item): - running = item - if running and not proposed: - proposed = running - if proposed: - pkey = key or proposed - given = givencfgs.get(pkey) - running = frappycfgs.get(pkey) - if running == proposed: - remarks[pkey] = '' if given == running else 'reconnect' - elif running: - remarks[pkey] = 'frappy restart needed' - else: - remarks[pkey] = 'frappy start needed' - seacfgs[pkey] = seacfg - if key: - result[key] = proposed - else: - addons.add(proposed) - else: - for item in available: - remarks[item] = 'ambiguous' - seacfgs[item] = seacfg - error = True - else: - pkey = key or seacfg - seacfgs[pkey] = seacfg - remarks[pkey] = 'missing frappy config' - error = True - restart = set() - for key, running in frappycfgs.items(): - if running: - service = get_service(key) - if service == 'addons': - addons.add(running) - if not seacfgs.get(key): - if sea_info.get(running): - restart.add(service) - remarks[key] = 'restart to start sea' - elif givencfgs.get(key) != running: - remarks[key] = 'reconnect' - elif remarks.get(key) is None: - remarks[key] = '' - if addons: - result['addons'] = ','.join(addons) +class Keep(Reconnect): + """a string tagged with the information to keep the connection as given - for service, cfg in ourcfgs.items(): - if cfg: - prop = result.get(service, '') - if prop == cfg and service not in restart: - result[service] = True - return error, result, (frappycfgs, seacfgs), remarks + 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): @@ -282,16 +89,16 @@ class FrappyManager(ServiceManager): USAGE = """ Usage: - frappy list [instance] * - frappy start - frappy restart [] [] * - frappy stop [] * - frappy listcfg [ | develop] # list available cfg files + frappy list %(optional_ins)s + frappy start %(ins)s + frappy restart %(ins)s[] [] %(remark)s + frappy stop %(ins)s[] %(remark)s + frappy listcfg %(ins)s[ | develop] # list available cfg files is one of main, stick, addons - %s - * wildcards allowed, using '.' to replace 0 or more arbitrary characters in +%(legend)s """ + sea_info = None def config_dirs(self, ins, service): cfgpaths = [] @@ -336,12 +143,12 @@ class FrappyManager(ServiceManager): 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): + 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) + 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) @@ -414,22 +221,25 @@ class FrappyManager(ServiceManager): except Exception: return False - def all_cfg(self, ins, service, list_info=None, sea_info=None): + def all_cfg(self, ins, service, details=False): """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 of of - :param sea_info: None or a dict of with info about sea configs - in frappy cfgs to be populated - :return: set of available config + :param details: get details about relation to sea + :return: see param:`what` + + sea_info: is a dict of (including extension .config/.stick/.addon) + list_info: dict of of """ all_cfg = set() if not ins: return {} namespace = Namespace() - details = sea_info is not None or list_info is not None + 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')): @@ -441,67 +251,67 @@ class FrappyManager(ServiceManager): 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) - sea_info[cfg] = sea_cfg - all_cfg.add(cfg) - if list_info is not None: - list_info.setdefault(cfgdir, {})[cfg] = desc.split('\n', 1)[0] + 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') - 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) + self.all_cfg(ins, service, True) seacfgpat = re.compile(r'(.*)(\.config|\.stick|\.addon)') - inverted_sea_info = {} - for seacfg, cfgset in sea_info.items(): - for cfg in cfgset: - inverted_sea_info[cfg] = seacfg - ambiguous = 0 - keylen = max(max(len(k) for k in cfgs) for cfgs in list_info.values()) - for cfgdir, cfgs in list_info.items(): + 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 = inverted_sea_info.get(cfg) + 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(sea_info[seacfg]) > 1: - prefix = '!' + prefix - ambiguous += 1 / len(sea_info[seacfg]) + 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: - print(f'{gap} ! {round(ambiguous)} ambiguous mappings sea -> frappy') + prt(f'{gap} ! {len(ambiguous)} ambiguous mappings sea -> frappy') def treat_args(self, argdict, unknown=(), extra=()): - if len(unknown) == 1: - cfg = unknown[0] + 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, (), ()) + 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, (), unknown) + 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 is 'none': + if cfg == 'none': return '' try: desc, sea_cfg = self.cfg_details(self, ins, service, cfg) @@ -511,52 +321,112 @@ class FrappyManager(ServiceManager): except Exception: return cfg + '?' - def propose_cfgs(self, ins, givencfgs, strict=False): - """get proposed configuration and an overview of the running servers - - :param ins: the instance to be checked for - :param givencfgs: a dict of cfg given by the ECS - :param strict: in case of errors (ambiguities or missing frappy config files) - proposed will be return as None - - :return: a tuple (, ) - where is information from running servers: - a dict of dict with optional items 'given', 'frappy', 'sea', 'proposed', 'error' - - given: the values for the cfgs argument - frappy: the cfg from running frappy servers - sea: the sea config file running - proposed: proposed configuration for the ECS - error: information about missing or ambiguous cfg files - """ - ourcfgs = self.get_cfg(ins, None) - sea = SeaManager() - seacfgs = sea.get_cfg(ins, 'sea').split('/') - if len(seacfgs) < 2: - seacfgs.append('') - sea_info = {} - self.all_cfg(ins, None, sea_info=sea_info) - return make_proposed(givencfgs, ourcfgs, seacfgs, sea_info, strict) - 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 of cfg given by the ECS + TODO: update this doc :return: tuple (, , (, ), where: : proposed config not sure : dict of proposed cfg - : dict of items runnning in frappy servers (addons are separated) + : dict of items running in frappy servers (addons are separated) : dict of items running on the sea server : dict of actions to do / remarks """ - ourcfgs = self.get_cfg(ins, None) - sea = SeaManager() - seacfgs = sea.get_cfg(ins, 'sea').split('/') - if len(seacfgs) < 2: - seacfgs.append('') - sea_info = {} - self.all_cfg(ins, None, sea_info=sea_info) - return summarize_server_state(givencfgs, ourcfgs, seacfgs, sea_info) + self.frappy_cfgs = self.get_cfg(ins, None) # dict of running frappy servers + + self.sea = SeaManager() + seaconfig = self.sea.get_cfg(ins, 'sea') + sealist = seaconfig.split('/') # , , , ... + 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 + + given_addons = givencfgs.get('addons', '') + running_addons = self.frappy_cfgs.get('addons', '') + if running_addons: + for cfg in running_addons.split(','): + seacfg = self.frappy2sea.get(cfg.strip()) + if seacfg.endswith('.addon') and seacfg[:-6] not in sealist[2:]: + proposed_cfg['addons'] = {running_addons} + break + elif given_addons: + proposed_cfg['addons'] = {given_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: # sea server is running + 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 = True + 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: + 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 diff --git a/nicosman.py b/nicosman.py index f984a78..ec3c6a7 100644 --- a/nicosman.py +++ b/nicosman.py @@ -55,17 +55,16 @@ class NicosManager(ServiceManager): Usage: nicos gui - nicos (the same as above) - nicos list [] * - nicos start [] * - nicos restart [] * - nicos stop [] * - nicos create * + nicos (the same as above) + nicos list [] + nicos start [] + nicos restart [] %(remark)s + nicos stop [] %(remark)s + nicos create nicos link (create links to nicos data and scripts) is one of cache, deamon, poller - %s - * wildcards allowed, using '.' to replace 0 or more arbitrary characters in +%(legend)s to be done after the experiment: nicos copy (copy data and scripts from link) diff --git a/seaman.py b/seaman.py index 7bdcfd1..c438166 100644 --- a/seaman.py +++ b/seaman.py @@ -24,6 +24,7 @@ import sys import time import termios import subprocess +import psutil import os import re from os.path import join, exists @@ -55,17 +56,16 @@ class SeaManager(ServiceManager): USAGE = """ Usage: - sea gui - sea # the same as sea gui - sea cli # the same as old seacmd - sea start [service] * # the same as old 'monit start sea' - sea restart [service] * - sea stop [service] * - sea list [instance] * + sea gui %(ins)s + sea %(ins)s # the same as sea gui + sea cli %(ins)s # the same as old seacmd + sea start %(ins)s[service] %(remark)s # the same as old 'monit start sea' + sea restart %(ins)s[service] %(remark)s + sea stop %(ins)s[service] %(remark)s + sea list %(optional_ins)s [service] is empty or one of sea, graph - %s - * wildcards allowed, using '.' to replace 0 or more arbitrary characters in +%(legend)s """ def do_cli(self, ins): @@ -107,10 +107,31 @@ class SeaManager(ServiceManager): if sea_server_src and exists(sea_server_src): if os.system('diff %s SeaServer' % sea_server_src): print('reload SeaServer') - os.rename('SeaServer', 'SeaServer0') + try: + os.rename('SeaServer', 'SeaServer0') + except Exception: + pass os.system('cp %s ./' % sea_server_src) + if service == 'sea': + # debugging: copy status file in case of a reboot + seastatus = self.get_status_filename(ins) + if seastatus: + boot_time = time.strftime("%Y-%m-%dT%H-%M-%S", time.localtime(psutil.boot_time())) + dst = seastatus.replace('.tcl', '') + '.' + boot_time + if not exists(dst): + os.system(f'cp {seastatus} {dst}') return start_dir, env + def get_status_filename(self, ins): + searoot = self.env[ins].get('SEA_ROOT', '') + seastatus = join(searoot, ins, 'status', 'seastatus.tcl') + if exists(seastatus): + return seastatus + seastatus = join(searoot, 'status', 'seastatus.tcl') + if exists(seastatus): + return seastatus + return None + def get_cfg(self, ins, service, addconfirmed=False): """return cfg info about running programs, if relevant @@ -121,12 +142,9 @@ class SeaManager(ServiceManager): if 'sea' not in self.get_procs().get(ins, ()): return '' try: - searoot = self.env[ins].get('SEA_ROOT', '') - seastatus = join(searoot, ins, 'status', 'seastatus.tcl') - if not exists(seastatus): - seastatus = join(searoot, 'status', 'seastatus.tcl') - if not exists(seastatus): - return '?' + seastatus = self.get_status_filename(ins) + if not seastatus: + return '?' result = ['', ''] confirmed = '' with open(seastatus, 'r', encoding='utf-8') as f: @@ -178,4 +196,8 @@ class SeaManager(ServiceManager): argdict['ins'] = arg else: raise UsageError('unknown argument: %s' % arg) - return [argdict.pop('ins', '')] + extra + result = [argdict.pop('ins', '')] + service = argdict.pop('service', '') + if service: + result.append(service) + return result + extra From bce1d50f37f34e4a48f67f96fcf1466f5fbe9837 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Fri, 10 May 2024 11:18:52 +0200 Subject: [PATCH 12/19] fix gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index bee8a64..083d732 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ __pycache__ +.idea From f23d3e3624796ca88aa9e17fd752747ff01e188f Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Fri, 10 May 2024 11:19:21 +0200 Subject: [PATCH 13/19] remove test-proposed (not used yet...) --- test_proposed.py | 27 --------------------------- 1 file changed, 27 deletions(-) delete mode 100644 test_proposed.py diff --git a/test_proposed.py b/test_proposed.py deleted file mode 100644 index 3cc471c..0000000 --- a/test_proposed.py +++ /dev/null @@ -1,27 +0,0 @@ -import pytest -from servicemanager.frappyman import summarize_server_state - -sea_info = { - 'ma10': 'ma10.config', - 'ma10stick': 'ma10.stick', - 'ma11': 'ma11.config', - 'ma11stick': 'ma11.stick', - 'roomt': 'rt.addon', - 'rt': 'rt.addon', - 'befilter': 'befilter.addon', -} - -given = dict(main='ma10', stick='ma10stick') -cfgs = dict(given) -seacfg = ['ma10', 'ma10'] - -@pytest.mark.parametrize('given, current, seacfg, state, proposed', [ - - ]) -def test_proposed(given, current, seacfg, result): - assert result == summarize_server_state(given, current, seacfg, sea_info) - - -def do(): - result = summarize_server_state(given, cfgs, seacfg, sea_info) - print(result) From b680563fcd4a6f99f1a1b927d81df258edbed408 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Fri, 10 May 2024 12:02:07 +0200 Subject: [PATCH 14/19] improve doc and cleanup code --- frappyman.py | 43 +++++++++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/frappyman.py b/frappyman.py index acca2ff..4457c6a 100644 --- a/frappyman.py +++ b/frappyman.py @@ -98,7 +98,17 @@ class FrappyManager(ServiceManager): is one of main, stick, addons %(legend)s """ - sea_info = None + # 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 = [] @@ -199,7 +209,9 @@ class FrappyManager(ServiceManager): init(*nodes) interact() - def get_cfg_details(self, namespace, cfgfile): + @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: @@ -229,8 +241,10 @@ class FrappyManager(ServiceManager): :param details: get details about relation to sea :return: see param:`what` - sea_info: is a dict of (including extension .config/.stick/.addon) - list_info: dict of of + implicit results: + self.frappy2sea: a dict of (including extension .config/.stick/.addon) + self.sea2frappy: a dict of set of + self.list_info: dict of of """ all_cfg = set() if not ins: @@ -326,17 +340,22 @@ class FrappyManager(ServiceManager): :param ins: the instance to be checked for :param givencfgs: a dict of cfg given by the ECS + :return: a dict 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 - TODO: update this doc - :return: tuple (, , (, ), where: - : proposed config not sure - : dict of proposed cfg - : dict of items running in frappy servers (addons are separated) - : dict of items running on the sea server - : dict of actions to do / remarks + implicit results: + self.remarks: a dict of remark (why should this be changed?) + self.frappy_cfgs: a dict of running cfgs + self.sea_cfgs: a dict of sea cfgs (without ending .config/.stick/.addon) + self.state: a dict ('sea ' | 'frappy ') 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 of running frappy servers - self.sea = SeaManager() seaconfig = self.sea.get_cfg(ins, 'sea') sealist = seaconfig.split('/') # , , , ... From 15abee1bb18e0e512ea2e62cd2ad978d96107ddc Mon Sep 17 00:00:00 2001 From: l_samenv Date: Mon, 13 May 2024 13:25:33 +0200 Subject: [PATCH 15/19] fix bug when seacfg is not known to frappy --- frappyman.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappyman.py b/frappyman.py index 4457c6a..104f3dd 100644 --- a/frappyman.py +++ b/frappyman.py @@ -382,7 +382,7 @@ class FrappyManager(ServiceManager): if running_addons: for cfg in running_addons.split(','): seacfg = self.frappy2sea.get(cfg.strip()) - if seacfg.endswith('.addon') and seacfg[:-6] not in sealist[2:]: + if seacfg is not None and seacfg.endswith('.addon') and seacfg[:-6] not in sealist[2:]: proposed_cfg['addons'] = {running_addons} break elif given_addons: From 02b24ed202125cad7eea83a94ac85af4b9ab844b Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Mon, 13 May 2024 14:30:44 +0200 Subject: [PATCH 16/19] fix handling of addons in get_server_state --- frappyman.py | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/frappyman.py b/frappyman.py index 104f3dd..4287ed7 100644 --- a/frappyman.py +++ b/frappyman.py @@ -377,16 +377,29 @@ class FrappyManager(ServiceManager): proposed_cfg[service] = proposed self.state[f'sea {service}'] = seacfg - given_addons = givencfgs.get('addons', '') - running_addons = self.frappy_cfgs.get('addons', '') - if running_addons: - for cfg in running_addons.split(','): - seacfg = self.frappy2sea.get(cfg.strip()) - if seacfg is not None and seacfg.endswith('.addon') and seacfg[:-6] not in sealist[2:]: - proposed_cfg['addons'] = {running_addons} - break - elif given_addons: - proposed_cfg['addons'] = {given_addons} + running_addons = self.frappy_cfgs.get('addons') + running_addons = set(','.split(running_addons)) if running_addons else () + proposed_addons = set() + running_sea_addons = set() + for cfg in running_addons: + seacfg = self.frappy2sea.get(cfg.strip()) + if seacfg is None: + proposed_addons.add(cfg) # addons with no sea cfg should be kept + elif seacfg.endswith('.addon') and seacfg[:-6] in sealist[2:]: + proposed_addons.add(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_addons |= proposed + if proposed_addons and set(proposed_addons) != set(running_addons): + proposed_cfg['addons'] = {','.join(sorted(proposed_addons))} for service in FrappyManager.services: given = givencfgs.get(service) @@ -412,7 +425,7 @@ class FrappyManager(ServiceManager): proposed = running if proposed is None: self.remarks[service] = 'ambiguous frappy cfg' - self.error = True + self.error = f'ambiguous frappy cfg for {seacfg}: {available}' else: self.remarks[service] = '' if running == proposed: From bb9eac432eb81c150fe2517ce73217c0572d29bc Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Mon, 13 May 2024 14:55:27 +0200 Subject: [PATCH 17/19] fix handling of addons, 2nd attempt --- frappyman.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/frappyman.py b/frappyman.py index 4287ed7..18f7558 100644 --- a/frappyman.py +++ b/frappyman.py @@ -378,15 +378,15 @@ class FrappyManager(ServiceManager): self.state[f'sea {service}'] = seacfg running_addons = self.frappy_cfgs.get('addons') - running_addons = set(','.split(running_addons)) if running_addons else () - proposed_addons = set() + 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.add(cfg) # addons with no sea cfg should be kept + proposed_addons.append(cfg) # addons with no sea cfg should be kept elif seacfg.endswith('.addon') and seacfg[:-6] in sealist[2:]: - proposed_addons.add(cfg) + proposed_addons.append(cfg) running_sea_addons.add(seacfg) for scfg in sealist[2:]: @@ -397,9 +397,12 @@ class FrappyManager(ServiceManager): if len(proposed) > 1: self.error = f'ambiguous frappy cfg for {seacfg}.addon: {proposed}' else: - proposed_addons |= proposed - if proposed_addons and set(proposed_addons) != set(running_addons): - proposed_cfg['addons'] = {','.join(sorted(proposed_addons))} + 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) From 67f5f46c7fde6b88e2be94e4210731c738921ec1 Mon Sep 17 00:00:00 2001 From: l_samenv Date: Tue, 14 May 2024 16:40:57 +0200 Subject: [PATCH 18/19] patch QApplication in order to distinguish settings of instances use ~/.config/nicos_ instead of common ~/.config/nicos --- nicosman.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/nicosman.py b/nicosman.py index ec3c6a7..7799cdc 100644 --- a/nicosman.py +++ b/nicosman.py @@ -261,6 +261,17 @@ class NicosManager(ServiceManager): def do_gui(self, ins): self.prepare_client(ins) + + # patch QApplication to use 'nicos_' instead of 'nicos' as organizationName + # for different settings and log files + import nicos.guisupport.qt + + class QApplication(nicos.guisupport.qt.QApplication): + def __init__(self, *args, organizationName=None, **kwds): + super().__init__(*args, organizationName=f'nicos_{ins}', **kwds) + + nicos.guisupport.qt.QApplication = QApplication + from nicos.clients.gui.main import main print('starting nicos gui %s' % ins) try: From 4d9e0c757fd3db28829aad8f00d55b67cf2e7511 Mon Sep 17 00:00:00 2001 From: l_samenv Date: Mon, 10 Jun 2024 07:48:01 +0200 Subject: [PATCH 19/19] improve FrappyManager.get_server_state look at frappy server cfg when sea and frappy cfg are both unrelated --- frappyman.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/frappyman.py b/frappyman.py index 18f7558..5fc2686 100644 --- a/frappyman.py +++ b/frappyman.py @@ -404,6 +404,7 @@ class FrappyManager(ServiceManager): if proposed_addons: # and set(proposed_addons) != set(running_addons): proposed_cfg['addons'] = {','.join(proposed_addons)} + self._debug = {} for service in FrappyManager.services: given = givencfgs.get(service) running = self.frappy_cfgs.get(service) @@ -412,7 +413,12 @@ class FrappyManager(ServiceManager): if running: self.state[f'frappy {service}'] = running - if seaconfig: # sea server is running + self._debug[service] = (seaconfig, available, running, running in self.frappy2sea) + + 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 @@ -440,6 +446,9 @@ class FrappyManager(ServiceManager): 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