first commit
This commit is contained in:
0
__init__.py
Normal file
0
__init__.py
Normal file
211
base.py
Normal file
211
base.py
Normal file
@@ -0,0 +1,211 @@
|
||||
# *****************************************************************************
|
||||
# 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 time
|
||||
import socket
|
||||
from pathlib import Path
|
||||
from configparser import ConfigParser
|
||||
|
||||
|
||||
DEFAULT_CFG = """[this]
|
||||
instrument={instrument}
|
||||
frappy_ports = 15000-15009
|
||||
"""
|
||||
MARCHESRC = ['/home/software/marche']
|
||||
CFGDIRS = ['/home/linse/config', '/home/l_samenv/linse_config']
|
||||
|
||||
|
||||
def read_config(filename):
|
||||
parser = ConfigParser()
|
||||
parser.optxform = str
|
||||
parser.read([str(filename)])
|
||||
return dict(parser)
|
||||
|
||||
|
||||
def write_config(filename, newvalues):
|
||||
parser = ConfigParser()
|
||||
parser.optxform = str
|
||||
parser.read([str(filename)])
|
||||
parser.read_dict(newvalues)
|
||||
with open(filename, 'w') as f:
|
||||
parser.write(f)
|
||||
|
||||
|
||||
class ArgError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class Config:
|
||||
def __init__(self):
|
||||
configfile = Path('~/.config/linsetools.cfg').expanduser()
|
||||
self.sections = read_config(configfile)
|
||||
self.instruments = {}
|
||||
for key, section in self.sections.items():
|
||||
if key == 'this':
|
||||
host = socket.gethostname().split('.')[0]
|
||||
section = {'instrument': host, 'frappy_ports': '15000-15009'}
|
||||
mandatory = set(section)
|
||||
this = self.sections.get('this', {})
|
||||
section.update(this)
|
||||
self.sections['this'] = section
|
||||
if set(this) & mandatory != mandatory:
|
||||
write_config(configfile, self.sections)
|
||||
self.this = section['instrument']
|
||||
self.instruments[self.this] = section
|
||||
else:
|
||||
head, _, tail = key.partition('.')
|
||||
if tail and head == 'instrument':
|
||||
self.instruments[tail] = section
|
||||
|
||||
|
||||
class Logger:
|
||||
loggers = {}
|
||||
levels = ['error', 'warning', 'info', 'debug']
|
||||
|
||||
def __new__(cls, level):
|
||||
log = cls.loggers.get(level)
|
||||
if log:
|
||||
print('old')
|
||||
return log
|
||||
log = object.__new__(cls)
|
||||
cls.loggers[level] = log
|
||||
method = log.show
|
||||
for lev in log.levels:
|
||||
setattr(log, lev, method)
|
||||
print(lev, level)
|
||||
if lev == level:
|
||||
method = log.hide
|
||||
return log
|
||||
|
||||
def show(self, fmt, *args):
|
||||
print(fmt % args)
|
||||
|
||||
def hide(self, fmt, *args):
|
||||
pass
|
||||
|
||||
|
||||
for marchedir in MARCHESRC:
|
||||
if Path(marchedir).is_dir():
|
||||
sys.path.append(marchedir)
|
||||
import marche.jobs as mj
|
||||
from marche.client import Client
|
||||
break
|
||||
|
||||
|
||||
STATUS_MAP = { # values are (<busy: bool), <target state:bool>, name)
|
||||
mj.DEAD: (False, False, 'DEAD'),
|
||||
mj.NOT_RUNNING: (False, False, 'NOT RUNNING'),
|
||||
mj.STARTING: (True, True, 'STARTING'),
|
||||
mj.INITIALIZING: (True, True, 'INITIALIZING'),
|
||||
mj.RUNNING: (False, True, 'RUNNING'),
|
||||
mj.WARNING: (False, True, 'WARNING'),
|
||||
mj.STOPPING: (True, False, 'STOPPING'),
|
||||
mj.NOT_AVAILABLE: (False, False, 'NOT AVAILABLE'),
|
||||
}
|
||||
|
||||
|
||||
def wait_status(cl, service):
|
||||
delay = 0.2
|
||||
while True:
|
||||
sts = cl.getServiceStatus(service)
|
||||
if STATUS_MAP[sts][0]: # busy
|
||||
if delay > 1.5: # this happens after about 5 sec
|
||||
return False
|
||||
time.sleep(delay)
|
||||
delay *= 1.225
|
||||
continue
|
||||
return True
|
||||
|
||||
|
||||
class MarcheControl:
|
||||
port = 8124
|
||||
|
||||
def __init__(self, host, port=None, user=None, instrument=None):
|
||||
self.config = Config()
|
||||
self.host = host
|
||||
self.instance = instrument or 'this'
|
||||
self.instrument = self.config.this if self.instance == 'this' else self.instance
|
||||
self.ins_config = self.config.instruments[self.insttrument]
|
||||
self.user = user or self.instrument # SINQ instruments
|
||||
if port is not None:
|
||||
self.port = port
|
||||
self._client = None
|
||||
self.argmap = {k: 'action' for k in ('start', 'restart', 'stop', 'gui', 'cli', 'list')}
|
||||
self.argmap.update((k, 'instrument') for k in self.config.instruments)
|
||||
|
||||
def connect(self):
|
||||
if self._client is None:
|
||||
# TODO: may need more generic solution for last arg
|
||||
print(self.host, self.port, self.user)
|
||||
self._client = Client(self.host, self.port, self.user, self.user.upper() + 'LNS')
|
||||
|
||||
# TODO; do we need disconnect?
|
||||
|
||||
def get_service(self, instance):
|
||||
return instance
|
||||
|
||||
def start(self, instance):
|
||||
self.connect()
|
||||
self._client.startService(self.get_service(instance))
|
||||
|
||||
def restart(self, instance):
|
||||
self.connect()
|
||||
self._client.restartService(self.get_service(instance))
|
||||
|
||||
def stop(self, instance):
|
||||
self.connect()
|
||||
self._client.stopService(self.get_service(instance))
|
||||
|
||||
def status(self, service):
|
||||
"""returns a dict <service> of (<busy>, <running (now or soon)>, <state name>)"""
|
||||
self.connect()
|
||||
servdict = self._client.getAllServiceInfo().get(service)
|
||||
if not servdict:
|
||||
return {}
|
||||
statedict = servdict['instances']
|
||||
return {k: STATUS_MAP[v['state']] for k, v in statedict.items()}
|
||||
|
||||
def reload(self):
|
||||
self.connect()
|
||||
self._client.reloadJobs()
|
||||
|
||||
def _run(self, action, other):
|
||||
if action == 'start':
|
||||
self.start(other)
|
||||
elif action == 'restart':
|
||||
self.restart(other)
|
||||
elif action == 'stop':
|
||||
self.stop(other)
|
||||
else:
|
||||
raise ArgError(f'unknown action {action!r}')
|
||||
|
||||
def run(self, *args):
|
||||
self._run(self.parse_args(*args))
|
||||
|
||||
def parse_args(self, *args):
|
||||
result = {}
|
||||
for arg in args:
|
||||
if arg in self.argmap:
|
||||
kind = self.argmap.get(arg, 'other')
|
||||
if kind in result:
|
||||
raise ArgError(f'duplicate {kind}')
|
||||
result[kind] = arg
|
||||
return result
|
||||
|
||||
133
frappy.py
Normal file
133
frappy.py
Normal file
@@ -0,0 +1,133 @@
|
||||
import re
|
||||
from pathlib import Path
|
||||
from .base import MarcheControl, Logger, ArgError
|
||||
|
||||
|
||||
WRAPPER_CFG = """interface = '{port}'
|
||||
include({cfg!r})
|
||||
overrideNode(interface=interface)
|
||||
"""
|
||||
WRAPPER_PAT = re.compile(r"interface\s=\s*'(\d*)'\s*\n")
|
||||
|
||||
|
||||
class FrappyMarche(MarcheControl):
|
||||
services = 'main', 'stick', 'addons'
|
||||
|
||||
def __init__(self, instance, host='localhost', port=None, user=None):
|
||||
super().__init__(host, port, user, instance)
|
||||
superconfig = self.config.sections['superfrappy']
|
||||
self.instance = instance
|
||||
|
||||
self.wrapperdir = superconfig.pop('wrapperdir')
|
||||
self.cfgdirs = superconfig.pop('cfgdirs')
|
||||
frappy_ports = self.ins_config.get('frappy_ports')
|
||||
ports = frappy_ports.split('-')
|
||||
self.frappy_ports = list(range(int(ports[0]), int(ports[-1])))
|
||||
self.frappy_servers = [f'{host}:{p}' for p in self.frappy_ports]
|
||||
|
||||
if not Path(self.wrapperdir).is_dir():
|
||||
raise ValueError(f'{self.wrapperdir} does not exist')
|
||||
self.get_cfg_info() # do we need to update this from time to time?
|
||||
self.argmap.update((k, 'service') for k in self.services)
|
||||
|
||||
def get_service(self, instance):
|
||||
return f'frappy.{instance}' if self.instance == 'this' else f'frappy.{self.instrument}-{instance}'
|
||||
|
||||
def wrapper_file(self, cfg):
|
||||
return Path(self.wrapperdir) / f'{cfg}_cfg.py'
|
||||
|
||||
def cfg_file(self, cfgdirs, service, cfg):
|
||||
if '/' in cfg:
|
||||
return Path(cfg)
|
||||
cfgpy = f'{cfg}_cfg.py'
|
||||
tries = []
|
||||
|
||||
services = [service] if service else list(self.services)
|
||||
services.append('')
|
||||
|
||||
for servicedir in services:
|
||||
for cfgdir in cfgdirs.split(':'):
|
||||
cfgfile = Path(cfgdir) / servicedir / cfgpy
|
||||
tries.append(cfgfile)
|
||||
if cfgfile.is_file():
|
||||
return servicedir, cfgfile
|
||||
else:
|
||||
raise FileNotFoundError(f'can not find {cfgpy} in {tries}')
|
||||
|
||||
def get_std_port(self, service):
|
||||
if service == 'main':
|
||||
return self.frappy_ports[0]
|
||||
if service == 'stick':
|
||||
return self.frappy_ports[1]
|
||||
return self.frappy_ports[2:]
|
||||
|
||||
def get_local_ports(self):
|
||||
self.get_cfg_info()
|
||||
run_state = self.status('frappy')
|
||||
result = {}
|
||||
for host_port, cfg in self.cfg_info.items():
|
||||
host, port = host_port.split(':')
|
||||
if host == 'localhost':
|
||||
busy, on, _ = run_state.get(cfg, (0,0,0))
|
||||
if on or busy:
|
||||
result.setdefault(port, []).append((on, busy, cfg))
|
||||
return {sorted(v)[-1][-1]: k for k, v in result.items()}
|
||||
|
||||
def get_port(self, service):
|
||||
if service not in {'main', 'stick', 'addons', 'addon'}:
|
||||
raise ArgError('illegal service argument')
|
||||
ports = self.get_std_port(service)
|
||||
if isinstance(ports, int):
|
||||
return ports
|
||||
self.get_cfg_info()
|
||||
used_ports = self.get_local_ports()
|
||||
for port in ports:
|
||||
if port not in used_ports:
|
||||
return port
|
||||
raise ValueError('too many frappy servers')
|
||||
|
||||
def add_frappy_service(self, service, cfg, port, log=None):
|
||||
if log is None:
|
||||
log = Logger('info')
|
||||
log.info('add %r port=%r', cfg, port)
|
||||
service, cfgfile = self.cfg_file(self.cfgdirs, service, cfg)
|
||||
if not port:
|
||||
if not service:
|
||||
raise ArgError('service is not given and can not be determined from cfg file location')
|
||||
port = self.get_port(service)
|
||||
wrapper_content = WRAPPER_CFG.format(cfg=str(cfgfile), port=port)
|
||||
cfgname = cfgfile.stem.removesuffix('_cfg')
|
||||
self.wrapper_file(cfgname).write_text(wrapper_content)
|
||||
self.get_cfg_info()
|
||||
log.info('wrapper %r %r', self.wrapper_file(cfgname), wrapper_content)
|
||||
self.reload()
|
||||
log.info('registered %r', cfgname)
|
||||
|
||||
def get_cfg_info(self):
|
||||
"""get info from wrapper dir"""
|
||||
result = {}
|
||||
for cfgfile in Path(self.wrapperdir).glob('*_cfg.py'):
|
||||
cfg = cfgfile.stem[:-4]
|
||||
match = WRAPPER_PAT.match(cfgfile.read_text())
|
||||
if match:
|
||||
result[f'localhost:{match.group(1)}'] = cfg
|
||||
self.cfg_info = result
|
||||
|
||||
def delete_frappy_service(self, cfg):
|
||||
try:
|
||||
self.wrapper_file(cfg).unlink()
|
||||
self.reload()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
def _run(self, action, service=None, other=None):
|
||||
cfg= other
|
||||
if action == 'start':
|
||||
self.add_frappy_service(service, cfg, None)
|
||||
self.start(cfg)
|
||||
elif action == 'restart':
|
||||
self.restart(cfg)
|
||||
elif action == 'stop':
|
||||
self.stop(cfg)
|
||||
else:
|
||||
raise ArgError(f'unknown action {action!r}')
|
||||
Reference in New Issue
Block a user