# HG changeset patch # User jfp <jf.pieronne@laposte.net> # Date 1684916340 -7200 # Wed May 24 10:19:00 2023 +0200 # Node ID 58e996ba25f8b3f904f2a8fe1cb0339b2b5f4292 # Parent b3bf2ad04df4b055d21c8bd1f8e22b3655ab70c7 Add supprt to kt_limit, quotas, privileges, nouaf diff --git a/supervisord.conf_template b/supervisord.conf_template --- a/supervisord.conf_template +++ b/supervisord.conf_template @@ -1,3 +1,7 @@ +[supervisord] +nodaemon = false +user = system +command = dev:[dir]supervisord.com [program:firstpgm] command=dev:[dir]PGM1.COM process_name=PGM1D @@ -8,6 +12,15 @@ startretries=3 stopwaitsecs=10 user=vmsuser +kt_limit=1 +# if privileges or quotas are specified nouaf is implicitly define to true +# else default false +# nouaf=true +# cpuplm express in seconds +# quotas= astlm=300, diolm=250,, cpulm=60, +# biolm=20, pgflquota=1000000 +# privileges= SYSNAM, PRMMBX, IMPERSONATE, +# WORLD autorestart=unexpected # SS$_NORMAL, SS$_FORCEX, SS$_EXITFORCED exitcodes=1,11228,11220 diff --git a/supervisord.py b/supervisord.py --- a/supervisord.py +++ b/supervisord.py @@ -1,5 +1,5 @@ # -# Required privileges: SYSNAM, PRMMBX, IMPERSONATE, WORLD +# Required privileges: ALTPRI, SYSNAM, PRMMBX, IMPERSONATE, SETPRV, WORLD # import argparse import configparser @@ -11,7 +11,7 @@ import queue from dataclasses import dataclass, field from enum import IntEnum -from typing import Any, Dict, List, Literal, Tuple, Type +from typing import Any, Dict, List, Literal, Tuple, Type, NewType from ovms import ( accdef, @@ -23,6 +23,8 @@ jpidef, lnmdef, mbxqio, + pqldef, + prcdef, prvdef, psldef, ssdef, @@ -48,6 +50,73 @@ timer_astctx = vmsast.AstContext(vmsast.M_WAKE | vmsast.M_QUEUE) +# Define a new type called "PrvMask", which is internally an `int` +PrvMask = NewType('PrvMask', int) +# Ditto for "PidType" +PidType = NewType('PidType', int) + +privileges: Dict[str, PrvMask] = { + 'NOSAME': PrvMask(0), + 'ACNT': PrvMask(prvdef.PRV_M_ACNT), + 'ALLSPOOL': PrvMask(prvdef.PRV_M_ALLSPOOL), + 'ALTPRI': PrvMask(prvdef.PRV_M_ALTPRI), + 'AUDIT': PrvMask(prvdef.PRV_M_AUDIT), + 'BUGCHK': PrvMask(prvdef.PRV_M_BUGCHK), + 'BYPASS': PrvMask(prvdef.PRV_M_BYPASS), + 'CMEXEC': PrvMask(prvdef.PRV_M_CMEXEC), + 'CMKRNL': PrvMask(prvdef.PRV_M_CMKRNL), + 'DIAGNOSE': PrvMask(prvdef.PRV_M_DIAGNOSE), + 'DOWNGRADE': PrvMask(prvdef.PRV_M_DOWNGRADE), + 'EXQUOTA': PrvMask(prvdef.PRV_M_EXQUOTA), + 'GROUP': PrvMask(prvdef.PRV_M_GROUP), + 'GRPNAM': PrvMask(prvdef.PRV_M_GRPNAM), + 'GRPPRV': PrvMask(prvdef.PRV_M_GRPPRV), + 'IMPERSONATE': PrvMask(prvdef.PRV_M_IMPERSONATE), + 'IMPORT': PrvMask(prvdef.PRV_M_IMPORT), + 'LOG_IO': PrvMask(prvdef.PRV_M_LOG_IO), + 'MOUNT': PrvMask(prvdef.PRV_M_MOUNT), + 'NETMBX': PrvMask(prvdef.PRV_M_NETMBX), + 'OPER': PrvMask(prvdef.PRV_M_OPER), + 'PFNMAP': PrvMask(prvdef.PRV_M_PFNMAP), + 'PHY_IO': PrvMask(prvdef.PRV_M_PHY_IO), + 'PRMCEB': PrvMask(prvdef.PRV_M_PRMCEB), + 'PRMGBL': PrvMask(prvdef.PRV_M_SYSGBL), + 'PRMMBX': PrvMask(prvdef.PRV_M_PRMMBX), + 'PSWAPM': PrvMask(prvdef.PRV_M_PSWAPM), + 'READALL': PrvMask(prvdef.PRV_M_READALL), + 'SECURITY': PrvMask(prvdef.PRV_M_SECURITY), + 'SETPRV': PrvMask(prvdef.PRV_M_SETPRV), + 'SHARE': PrvMask(prvdef.PRV_M_SHARE), + 'SHMEM': PrvMask(prvdef.PRV_M_SHMEM), + 'SYSGBL': PrvMask(prvdef.PRV_M_SYSGBL), + 'SYSLCK': PrvMask(prvdef.PRV_M_SYSLCK), + 'SYSNAM': PrvMask(prvdef.PRV_M_SYSNAM), + 'SYSPRV': PrvMask(prvdef.PRV_M_SYSPRV), + 'TMPMBX': PrvMask(prvdef.PRV_M_TMPMBX), + 'UPGRADE': PrvMask(prvdef.PRV_M_UPGRADE), + 'VOLPRO': PrvMask(prvdef.PRV_M_VOLPRO), + 'WORLD': PrvMask(prvdef.PRV_M_WORLD), +} + + +quotas: Dict[str, int] = { + 'ASTLM': pqldef.PQL__ASTLM, + 'BIOLM': pqldef.PQL__BIOLM, + 'BYTLM': pqldef.PQL__BYTLM, + 'CPULM': pqldef.PQL__CPULM, + 'DIOLM': pqldef.PQL__DIOLM, + 'ENQLM': pqldef.PQL__ENQLM, + 'FILLM': pqldef.PQL__FILLM, + 'JTQUOTA': pqldef.PQL__JTQUOTA, + 'PGFLQUOTA': pqldef.PQL__PGFLQUOTA, + 'PRCLM': pqldef.PQL__PRCLM, + 'TQELM': pqldef.PQL__TQELM, + 'WSDEFAULT': pqldef.PQL__WSDEFAULT, + 'WSEXTENT': pqldef.PQL__WSEXTENT, + 'WSQUOTA': pqldef.PQL__WSQUOTA, +} + + def start_timer(): global timer_delay, timer_astctx timer_astctx.reset() @@ -135,7 +204,7 @@ class ProcessInfo(object): - pid: int + pid: PidType state: ProcessStates finalsts: int | None start_time: datetime.datetime | None @@ -143,7 +212,7 @@ def __init__( self, - pid: int = 0, + pid: PidType = PidType(0), start_time: datetime.datetime | None = None, state: ProcessStates = ProcessStates.STOPPED, ) -> None: @@ -166,9 +235,13 @@ startsecs: int startretries: int stopwaitsecs: int + kt_limit: int + nouaf: bool + quotas: List[Tuple[int, int]] | None + prv: PrvMask | None user: bytes process: ProcessInfo - running_processes: Dict[int, 'Program'] + running_processes: Dict[PidType, 'Program'] running_processes = dict() programs: Dict[str, 'Program'] programs = dict() @@ -195,6 +268,10 @@ startsecs: int, startretries: int, stopwaitsecs: int, + nouaf: bool, + quotas: List[Tuple[int, int]] | None, + prv: PrvMask | None, + kt_limit: int, autorestart: ( Type[RestartUnconditionally] | Type[RestartWhenExitUnexpected] @@ -217,6 +294,10 @@ self.timer_item = None self.kill_request = False self.autorestart = autorestart + self.nouaf = nouaf + self.quotas = quotas + self.prv = prv + self.kt_limit = kt_limit self.exitcodes = exitcodes self.process = ProcessInfo() @@ -266,7 +347,7 @@ self.process_name + b'_PID', supervisord_table_name, )[1] - pid = int(v, 16) + pid = PidType(int(v, 16)) while True: try: itime, stime = lib.getjpi(jpidef.JPI__LOGINTIM, pid)[2:] @@ -276,7 +357,7 @@ raise itime = crtl.fix_time(itime) assert itime is not None - self.process.pid = pid + self.process.pid = PidType(pid) self.process.start_time = datetime.datetime.fromtimestamp(itime) self.set_running() @@ -305,23 +386,64 @@ self.timer_item = None self.process.finalsts = None + # If the image argument specifies the SYS$SYSTEM:LOGINOUT.EXE, + # the UIC of the created process will be the UIC of the caller of $CREPRC, + # and the UIC parameter is ignored. + # So, we need to use persona userpro = starlet.create_user_profile(usrnam=self.user)[1] persona_id = starlet.persona_create(usrpro=userpro)[1] persona_previous_id = starlet.persona_assume(persona_id)[1] - starlet.persona_assume(persona_previous_id) - uic = lib.getjpi(jpidef.JPI__UIC)[2] - starlet.persona_delete(persona_id) - # Create a detach process (uic is specified) - s, pid, d = starlet.creprc( - uic=uic, - image=b'SYS$SYSTEM:LOGINOUT.EXE', - input=self.command, - output=self.stdout_file, - error=self.stderr_file, - prcnam=self.process_name, - mbxunt=Program.mbxunt, - ) + # Create a detach process (stsflg=prvdef.PRV_M_IMPERSONATE) + # From John Gillings: + # (https://community.hpe.com/t5/operating-system-openvms/run-uic-vs-sys-creprc/td-p/5171510) + # General comment about PRC$M_NOUAF ... + # When you run LOGINOUT in a detached process *WITHOUT* specifying PRC$M_NOUAF + # for $CREPRC, or *WITH* /AUTHORIZE on the DCL RUN command, (note that the + # default is reversed between the two), many of the process attributes you have + # specified with qualifiers or parameters may be overridden in the resulting + # process. + # That includes UIC, process name, quotas and privileges. Behind the scenes, + # the process is created with whatever you specified, but LOGINOUT replaces + # them with values from the UAF. + # + # From https://docs.vmssoftware.com/vsi-openvms-programming-concepts-manual-volume-i/ + # The SYS$CREPRC system service also does not provide default equivalence names + # for the logical names SYS$LOGIN, SYS$LOGIN_DEVICE, and SYS$SCRATCH. + # These logical names are available to the created process only when the + # specified image is LOGINOUT, and when the PRC$M_NOUAF flag is not set. + try: + stsflg = ( + prcdef.PRC_M_IMPERSONATE + | prcdef.PRC_M_PARSE_EXTENDED + | prcdef.PRC_M_KT_LIMIT + ) + if self.nouaf: + stsflg |= prcdef.PRC_M_NOUAF + pid = PidType( + starlet.creprc( + image=b'SYS$SYSTEM:LOGINOUT.EXE', + input=self.command, + output=self.stdout_file, + error=self.stderr_file, + prcnam=self.process_name, + mbxunt=Program.mbxunt, + quota=self.quotas, + prv=self.prv, + kt_limit=self.kt_limit, + # prcdef.PRC_M_DETACH and prcdef.PRC_M_IMPERSONATE are synonyms + stsflg=stsflg, + )[1] + ) + except OSError as e: + logger.warning( + f"Can' create process {self.process_name}, error {e}" + ) + self.process.state = ProcessStates.FATAL + return + finally: + starlet.persona_assume(persona_previous_id) + starlet.persona_delete(persona_id) lib.set_logical( self.process_name + b'_PID', hex(pid)[2:].upper(), @@ -333,7 +455,7 @@ supervisord_table_name, ) - self.process.pid = pid + self.process.pid = PidType(pid) self.process.start_time = datetime.datetime.now() if self.startsecs == 0: self.set_running() @@ -343,7 +465,7 @@ timer_queue.put( PrioritizedItem(current_tick + self.startsecs, self.timer_item) ) - Program.running_processes[pid] = self + Program.running_processes[PidType(pid)] = self lib.set_logical( self.process_name + b'_BEG', str(self.process.start_time)[:19], @@ -659,7 +781,9 @@ t_astctxt = qio_procterm(fterm) # noqa: F841 c_astctxt = qio_cmd(fcmd) # noqa: F841 - for pgm in sorted(Program.programs.values(), key=lambda pgm: pgm.priority): + for pgm in sorted( + Program.programs.values(), key=lambda pgm: pgm.priority + ): if pgm.autostart: pgm.create_process(False) @@ -710,11 +834,13 @@ handler.setFormatter(formatter) main_log.addHandler(handler) - # Required privileges: SYSNAM, PRMMBX, IMPERSONATE, WORLD + # Required privileges: ALTPRI, SYSNAM, PRMMBX, IMPERSONATE, SETPRV, WORLD starlet.setprv( 1, prvdef.PRV_M_SYSNAM + | prvdef.PRV_M_ALTPRI | prvdef.PRV_M_PRMMBX + | prvdef.PRV_M_SETPRV | prvdef.PRV_M_IMPERSONATE | prvdef.PRV_M_WORLD, ) @@ -744,6 +870,40 @@ for sn in config.sections(): if sn.startswith('program:'): + quotaslst = [ + quota.strip().upper().split('=') + for quota in config[sn].get('quotas', '').split(',') + if quota != '' + ] + prcquotas = [] + for quotan, quotav in quotaslst: + if quotan not in quotas: + print(f'{sn}: {quotan}={quotav} not a valid quota') + crtl.vms_exit(ssdef.SS__INVARG) + if quotan == 'CPULM': + print('>>>', repr(quotav)) + quotav = int(quotav) * 100 + prcquotas.append((quotas[quotan], int(quotav))) + if prcquotas == []: + prcquotas = None + + prvnames = [ + name.strip().upper() + for name in config[sn].get('privileges', '').split(',') + if name != '' + ] + prv: PrvMask | None + prv = None + for prvnam in prvnames: + if prvnam not in privileges: + print(f'{sn}: {prvnam} not a valid privilege') + crtl.vms_exit(ssdef.SS__INVARG) + prv = ( + privileges[prvnam] + if prv is None + else PrvMask(prv | privileges[prvnam]) + ) + name = sn.split(':')[-1].upper() process_name = config[sn]['process_name'] autostart = config[sn].getboolean('autostart', False) @@ -751,6 +911,8 @@ stdout_file = config[sn].get('stdout_file', 'NLA0:') stderr_file = config[sn].get('stderr_file', 'NLA0:') priority = config[sn].getint('priority', 999) + nouaf = config[sn].getboolean('nouaf', False) + kt_limit = config[sn].getint('kt_limit', 0) startsecs = config[sn].getint('startsecs', 10) startretries = config[sn].getint('startretries', 3) stopwaitsecs = config[sn].getint('stopwaitsescs', 10) @@ -759,6 +921,8 @@ ) exitcodes = config[sn].get('exitcodes', '1').split(',') user = config[sn]['user'] + if prv is not None or quotaslst is not None: + nouaf = True p = Program( name=name, user=user, @@ -772,6 +936,10 @@ startretries=startretries, stopwaitsecs=stopwaitsecs, autorestart=autorestart, + nouaf=nouaf, + quotas=prcquotas, + prv=prv, + kt_limit=kt_limit, exitcodes=[ int(exitcode) for exitcode in exitcodes if exitcode != '' ],