File: //proc/self/root/usr/local/lsws/lsns/bin/lssetup
#!/usr/bin/python3
import argparse, json, logging, os, re, shutil, signal, subprocess, tempfile
import xml.etree.ElementTree as ET
from stat import *
from subprocess import PIPE
from xml.dom import minidom
import common
def run_program(args, fail_reason = ""):
logging.debug('run: ' + str(args))
result = subprocess.run(args, stdout=PIPE, stderr=PIPE)
if fail_reason != "" and result.returncode == 0:
common.fatal_error('Expected: ' + args[0] + ' to fail: ' + fail_reason)
if fail_reason == "" and result.returncode != 0:
common.fatal_error('Error in running: ' + args[0] + ' errors: ' + result.stdout.decode('utf-8') + ' ' + result.stderr.decode('utf-8'))
return result.stdout.decode('utf-8')
def run_shell_program(cmd, fail_reason = ""):
logging.debug('run: ' + cmd)
result = subprocess.run(cmd, shell=True, stdout=PIPE, stderr=PIPE)
if fail_reason != "" and result.returncode == 0:
common.fatal_error('Expected: ' + cmd + ' to fail: ' + fail_reason)
if fail_reason == "" and result.returncode != 0:
common.fatal_error('Error in running: ' + cmd + ' errors: ' + result.stdout.decode('utf-8') + ' ' + result.stderr.decode('utf-8'))
return result.stdout.decode('utf-8')
def systemctl_daemon_reload():
logging.debug("Do daemon-reload")
run_program(['systemctl', 'daemon-reload'])
def validate_environment(args):
if not args.namespace_only and not args.disable and not os.path.exists('/sys/fs/cgroup/cgroup.controllers'):
common.fatal_error("cgroups is not v2 on this machine")
if os.getuid() != 0:
common.fatal_error("this program must be run as root")
if not args.namespace_only and not args.disable and not os.path.isfile('/etc/systemd/system.control/user.slice.d/50-IOAccounting.conf'):
logging.debug('Activate accounting')
run_program(['systemctl', 'set-property', 'user.slice', 'IOAccounting=yes', 'MemoryAccounting=yes', 'TasksAccounting=yes'])
systemctl_daemon_reload()
def init_pgm():
common.init_logging()
parser = argparse.ArgumentParser(prog="setup",
description='LiteSpeed Containers Setup Program')
parser.add_argument('-l', '--log', type=int, help='set logging level, 10=Debug, 20=Info, 30=Warning, 40=Error. Default is Info')
parser.add_argument('-q', '--quiet', action='store_true', help='turns off all logging and only outputs what is requested.')
parser.add_argument('-s', '--server_root', default=common.server_root(), help='the LiteSpeed SERVER_ROOT')
parser.add_argument('-c', '--cgroups', type=int, default=2, help='The minimum value for cgroups')
parser.add_argument('-i', '--cgroups-init', type=int, default=2, help='The cgroups value if not set currently')
parser.add_argument('-n', '--namespace', type=int, default=2, help='The minimum value for namespace')
parser.add_argument('-m', '--namespace-init', type=int, default=2, help='The namespace value if not set currently')
parser.add_argument('-g', '--no-config', action='store_true', help='Skips checking of LiteSpeed Config')
parser.add_argument('-t', '--no-subtree_control', action='store_true', help='Skips checking of system cgroup.subtree_control file')
parser.add_argument('-u', '--no-upgrade', action='store_true', help='Does not check the version of LiteSpeed')
parser.add_argument('-r', '--revert-config', action='store_true', help='Reverts modified LiteSpeed config file')
parser.add_argument('-o', '--namespace-only', action='store_true', help='No cgroups, namespaces only')
parser.add_argument('-d', '--disable', action='store_true', help='LiteSpeed Containers Disable')
args = parser.parse_args()
if not args.quiet or args.log != None:
if args.log != None:
logging.getLogger().setLevel(args.log)
else:
logging.getLogger().setLevel(logging.INFO)
logging.debug("Entering setup")
common.set_server_root(args.server_root)
validate_environment(args)
return args
def config_filename(lsws):
if lsws:
return common.server_root() + "/conf/httpd_config.xml"
return common.server_root() + "/conf/httpd_config.conf"
def config_filename_revert(lsws):
return config_filename(lsws) + '_lssetup'
def is_lsws():
if (os.path.isfile(config_filename(True))):
return True
if (os.path.isfile(config_filename(False))):
return False
common.fatal_error("Can not identify the LiteSpeed system type")
def copy_file_details(source, dest):
s = os.stat(source)
shutil.copystat(source, dest)
os.chown(dest, s.st_uid, s.st_gid)
def _append_subelement(parent, tag, text):
"""ET.SubElement-like append that preserves the parent's existing
indentation so the new element sits on its own line rather than being
jammed against the parent's closing tag.
In ElementTree, the whitespace that appears in the serialized XML between
elements lives in `.text` (before the first child) and `.tail` (after each
child). When we append a new child we have to redistribute these so:
- the element that used to be the last child keeps a sibling-indent in
its tail (so the new child sits one level in), and
- the new child gets the closing-tag-indent (so the parent's close stays
where it was)."""
if len(parent) == 0:
# No existing children: synthesize a reasonable indent.
sibling_indent = (parent.text or '\n') + ' '
closing_indent = parent.text or '\n'
parent.text = sibling_indent
new = ET.SubElement(parent, tag)
new.text = text
new.tail = closing_indent
return new
# The previous-last child's tail currently sits between it and the parent's
# closing tag — that's the closing-tag indent. We need to repurpose it as
# the new child's tail (so the parent's close stays put) and give the
# previous-last child a true sibling-indent.
closing_indent = parent[-1].tail or '\n'
if len(parent) >= 2:
# The element BEFORE the previous-last child has the canonical
# sibling-indent in its tail.
sibling_indent = parent[-2].tail or closing_indent
else:
# Only one prior child — use parent.text (which is the indent before
# the first child, also a sibling-indent).
sibling_indent = parent.text or closing_indent
parent[-1].tail = sibling_indent
new = ET.SubElement(parent, tag)
new.text = text
new.tail = closing_indent
return new
def set_cgroups(root, disable, val, init):
valSet = False
findLimit = root.find('./security/CGIRLimit')
if findLimit == None:
common.fatal_error('Unexpected missing element CGIRLimit in configuration file')
cgroups = findLimit.find('cgroups')
if disable:
if cgroups != None and int(cgroups.text) > 0:
logging.debug('cgroups needs disabling')
cgroups.text = str(0)
valSet = True
elif cgroups == None:
logging.debug('cgroups not in LSWS file')
cgroups = _append_subelement(findLimit, 'cgroups', str(init))
valSet = True
elif disable and int(cgroups.text) > 0:
logging.debug('cgroups needs disabling')
cgroups.text = str(0)
valSet = True
elif int(cgroups.text) < val:
logging.debug('cgroups bad value in LSWS file')
cgroups.text = str(val)
valSet = True
if valSet:
logging.debug('cgroups set')
else:
logging.debug('cgroups ok')
return valSet
def set_namespace(root, disable, val, init):
valSet = False
findSecurity = root.find('./security')
if findSecurity == None:
common.fatal_error('Unexpected missing element Security in configuration file')
namespace = findSecurity.find('namespace')
if disable:
if namespace != None and int(namespace.text) > 0:
logging.debug('namespace needs disabling')
namespace.text = str(0)
valSet = True
elif namespace == None:
logging.debug('namespace not in LSWS file')
namespace = _append_subelement(findSecurity, 'namespace', str(init))
valSet = True
elif int(namespace.text) < val:
logging.debug('namespace bad value in LSWS file')
namespace.text = str(val)
valSet = True
if valSet:
logging.debug('namespace set')
else:
logging.debug('namespace ok')
return valSet
def lsws_restart():
# Canonical LiteSpeed PID file. If it's missing, LiteSpeed isn't running.
pidfile = '/tmp/lshttpd/lshttpd.pid'
if not os.path.isfile(pidfile):
logging.debug("LiteSpeed not running")
return
try:
with open(pidfile, 'r') as pf:
pid_str = pf.readline().strip()
except OSError as err:
common.fatal_error("Cannot read LiteSpeed pid file %s: %s" % (pidfile, err))
if not pid_str:
logging.debug("LiteSpeed not running")
return
try:
pid = int(pid_str)
except ValueError:
common.fatal_error("Bad LiteSpeed pid value: %r" % pid_str)
# Sanity-check: the pid we're about to SIGUSR1 must actually be lshttpd,
# owned by root. Prevents racing into a recycled pid that belongs to a
# non-LiteSpeed process (e.g. an unprivileged user's process).
try:
exe = os.readlink('/proc/%d/exe' % pid)
st = os.stat('/proc/%d' % pid)
except OSError as err:
common.fatal_error("Cannot verify LiteSpeed pid %d: %s" % (pid, err))
if st.st_uid != 0 or 'lshttpd' not in os.path.basename(exe):
common.fatal_error(
"Refusing to signal pid %d: exe=%r uid=%d (not a root-owned lshttpd)"
% (pid, exe, st.st_uid))
try:
os.kill(pid, signal.SIGUSR1)
except OSError as err:
common.fatal_error("Error sending graceful restart to ListSpeed: %s" % err)
logging.debug("LiteSpeed restarted")
def lsws_config(args):
file = config_filename(True)
try:
tree = ET.parse(file)
except Exception as err:
common.fatal_error('Error parsing configuration: %s' % err)
revert = config_filename_revert(True)
shutil.copy2(file, revert)
copy_file_details(file, revert)
if not args.namespace_only:
valSet = set_cgroups(tree.getroot(), args.disable, args.cgroups, args.cgroups_init)
if set_namespace(tree.getroot(), args.disable, args.namespace, args.namespace_init):
valSet = True
if valSet:
try:
#ET.indent(tree, space=' ')
# Atomic, symlink-safe write: write to a randomized temp file in the
# same directory, then rename. Defeats a planted symlink at `file`.
tmp_fd, tmp_path = tempfile.mkstemp(
prefix='.httpd_config.', suffix='.xml',
dir=os.path.dirname(file))
try:
with os.fdopen(tmp_fd, 'wb') as tmp_w:
tree.write(tmp_w, encoding="UTF-8", xml_declaration=True, short_empty_elements=False)
copy_file_details(file, tmp_path)
os.replace(tmp_path, file)
except Exception:
try:
os.unlink(tmp_path)
except OSError:
pass
raise
except Exception as err:
common.fatal_error('Error updating configuration: %s' % err)
lsws_restart()
def lsws_validate(args):
lsws_config(args)
def rewrite_ols_config(args, cgroupsAt, pastCGIRLimit, cgroupsVal, namespaceAt, lineNo, namespaceVal):
stop1 = 0
replace1 = False
if not args.namespace_only:
if cgroupsAt != 0:
if (args.disable and int(cgroupsVal) > 0) or (not args.disable and int(cgroupsVal) <= args.cgroups):
stop1 = cgroupsAt
replace1 = True
if args.disable:
setCgroups = 0
else:
setCgroups = args.cgroups
elif not args.disable:
stop1 = pastCGIRLimit
setCgroups = args.cgroups_init
stop2 = 0
replace2 = False
if namespaceAt != 0:
if (args.disable and int(namespaceVal) > 0) or (not args.disable and int(namespaceVal) <= args.namespace):
stop2 = namespaceAt
replace2 = True
if args.disable:
setNS = 0
else:
setNS = args.namespace
elif not args.disable:
stop2 = lineNo
setNS = args.namespace_init
lineNo = 0
file = config_filename(False)
try:
f = open(file, 'r')
except Exception as err:
common.fatal_error('Error opening %s: %s' % (file, err))
# Atomic, symlink-safe rewrite: mkstemp in same dir, write, fsync-replace.
# Previous predictable `httpd_config.out` path was vulnerable to a planted
# symlink redirecting the write to an arbitrary file.
try:
fileout_fd, fileout = tempfile.mkstemp(
prefix='.httpd_config.', suffix='.out',
dir=os.path.dirname(file))
w = os.fdopen(fileout_fd, 'w')
except Exception as err:
common.fatal_error('Error opening temp file in %s: %s' % (os.path.dirname(file), err))
try:
for line in f:
replace = False
if lineNo > 0:
if stop1 != 0 and lineNo == stop1:
w.write(' cgroups %d\n' % setCgroups)
replace = replace1
logging.debug('writing cgroups %d' % setCgroups)
elif lineNo == stop2:
w.write('namespace %d\n'% setNS)
replace = replace2
logging.debug('writing namespace %d' % setNS)
if not replace:
w.write(line)
lineNo += 1
f.close()
w.close()
copy_file_details(file, fileout)
os.replace(fileout, file)
except Exception as err:
try:
os.unlink(fileout)
except OSError:
pass
common.fatal_error('Error writing OLS config file: %s' % err)
def ols_config(args):
logging.debug('OLS config file')
file = config_filename(False)
try:
f = open(file, 'r')
except Exception as err:
common.fatal_error('Error opening %s: %s' % (file, err))
revert = config_filename_revert(False)
shutil.copy2(file, revert)
copy_file_details(file, revert)
# Optimize for the do nothing case
inCGIRLimit = False
pastCGIRLimit = 0
cgroupsAt = 0
cgroupsLine = ''
namespaceAt = 0
namespaceLine = ''
lineNo = 0
cgroupsVal = 0
namespaceVal = 0
for line in f:
if pastCGIRLimit == 0:
if not inCGIRLimit:
if line.startswith('CGIRLimit'):
inCGIRLimit = True
elif line.find('}') != -1:
pastCGIRLimit = lineNo
elif not args.namespace_only and line.startswith(' cgroups'):
cgroupsLine = line
cgroupsAt = lineNo
elif line.startswith('namespace'):
namespaceLine = line
namespaceAt = lineNo
break
elif line.find('{') != -1:
break
lineNo += 1
f.close()
if not args.namespace_only and cgroupsLine != '':
cgroupsVal = re.search(r'[\d]+', cgroupsLine).group()
logging.debug('OLS cgroups val: %d' % int(cgroupsVal))
if namespaceLine != '':
namespaceVal = re.search(r'[\d]+', namespaceLine).group()
logging.debug('OLS namespace val: %d' % int(namespaceVal))
if args.disable:
if (args.namespace_only or cgroupsLine == '' or int(cgroupsVal) == 0) and (namespaceLine == '' or int(namespaceVal) == 0):
logging.debug('OLS config file needs no mod')
return
elif (args.namespace_only or (cgroupsLine != '' and int(cgroupsVal) >= args.cgroups)) and namespaceLine != '' and int(namespaceVal) >= args.namespace:
logging.debug('OLS config file needs no mod')
return
logging.debug('OLS config file needs mod')
rewrite_ols_config(args, cgroupsAt, pastCGIRLimit, cgroupsVal, namespaceAt, lineNo, namespaceVal)
lsws_restart()
def ols_validate(args):
ols_config(args)
def read_oneline_file(file):
try:
f = open(file, 'r')
except Exception as err:
common.fatal_error('Error opening %s: %s' % (file, err))
ln = f.readline()
line = ln.rstrip()
f.close()
return line
def read_subtree_control():
line = read_oneline_file('/sys/fs/cgroup/cgroup.subtree_control')
pieces = line.split(' ')
logging.debug('Read line: %s got %s' % (line, ' '.join(pieces)))
return pieces
def build_delegate(controllers):
dirname = '/etc/systemd/system/user@.service.d'
filename = dirname + '/delegate.conf'
desired = "[Service]\nDelegate=%s\n" % ' '.join(controllers)
# Idempotent: if the file already exists with the exact contents we'd
# write, this is a re-run of a successful Setup — return silently and
# don't bother systemd with a daemon-reload. If it exists with
# different contents, overwrite (the controller set may have changed,
# or a prior run left a partial/stale file). Only the create path
# needs the mkdir attempt.
if os.path.exists(filename):
try:
with open(filename, 'r') as r:
existing = r.read()
except Exception as err:
common.fatal_error('Error reading %s: %s' % (filename, err))
if existing == desired:
logging.debug(
'%s already matches desired controller set; nothing to do'
% filename
)
return
logging.debug(
'%s exists with stale contents; overwriting' % filename
)
else:
try:
os.mkdir(dirname)
except Exception as err:
logging.debug("Error creating service directory: %s" % err)
# Atomic, symlink-safe write of delegate.conf in systemd drop-in dir.
try:
tmp_fd, tmp_path = tempfile.mkstemp(
prefix='.delegate.', suffix='.conf', dir=dirname)
try:
with os.fdopen(tmp_fd, 'w') as w:
w.write(desired)
os.chmod(tmp_path, 0o644)
os.replace(tmp_path, filename)
except Exception:
try:
os.unlink(tmp_path)
except OSError:
pass
raise
except Exception as err:
common.fatal_error('Error opening %s: %s' % (filename, err))
systemctl_daemon_reload()
def subtree_control():
pieces = read_subtree_control()
controllers = ['cpu', 'cpuset', 'io', 'memory', 'pids']
missing = []
for controller in controllers:
if not controller in pieces:
missing.append(controller)
if len(missing) == 0 or (len(missing) == 1 and missing[0] == 'cpuset'):
logging.debug('No controller missing')
return
logging.debug('Missing the %s controllers' % ' '.join(missing))
build_delegate(controllers)
def compare_versions(verstr, minver):
verlist = verstr.split('.')
minlist = minver.split('.')
for index, min in enumerate(minlist):
if index > len(verlist):
logging.debug('Old version %s < %s (short)' % (verstr, minver))
return True
dash_index = verlist[index].find('-')
if dash_index != -1:
verlist[index] = verlist[index][0:dash_index]
if int(min) > int(verlist[index]):
logging.debug('Old version %s < %s' % (verstr, minver))
return True
if int(min) < int(verlist[index]):
logging.debug('Newer version %s > %s' % (verstr, minver))
return False
logging.debug('Equal version %s == %s' % (verstr, minver))
return False
def get_minver():
if is_lsws():
return '6.3'
return '1.7'
def needs_upgrade():
verstr = read_oneline_file(common.server_root() + '/VERSION')
return compare_versions(verstr, get_minver())
def upgrade():
autostr = read_oneline_file(common.server_root() + '/autoupdate/release')
upgrade_pgm = common.server_root() + '/admin/misc/lsup.sh'
logging.info('Doing LiteSpeed upgrade')
if not compare_versions(autostr, get_minver()):
run_program([upgrade_pgm, '-f'])
else:
run_program([upgrade_pgm, '-d', '-f', '-v', get_minver()])
def revert_config():
lsws = is_lsws()
if not os.path.isfile(config_filename_revert(lsws)):
common.fatal_error("You do not have a config file to revert")
logging.info('Doing LiteSpeed config file revert')
os.replace(config_filename_revert(lsws), config_filename(lsws))
lsws_restart()
def do_pgm(args):
logging.debug("Entering setup")
if args.revert_config:
revert_config()
return;
if not args.no_upgrade:
if needs_upgrade():
upgrade()
if not args.no_config:
if is_lsws():
lsws_validate(args)
else:
ols_validate(args)
else:
logging.debug('no-config specified')
if not args.disable and not args.no_subtree_control and not args.namespace_only:
subtree_control()
else:
logging.debug('no-subtree_control specified')
def main():
args = init_pgm()
return do_pgm(args)
if __name__ == "__main__":
main()