You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
153 lines
5.4 KiB
153 lines
5.4 KiB
"""
|
|
The runscript module is responsible for running external scripts and processing
|
|
their output.
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
import pwd
|
|
import grp
|
|
import subprocess
|
|
import json
|
|
|
|
|
|
def from_file(fname):
|
|
"""
|
|
Read or execute `fname` and decode its contents as JSON. Used for reading
|
|
parts of forms from external files or scripts.
|
|
"""
|
|
log = logging.getLogger(__name__)
|
|
|
|
if not fname.startswith('/'):
|
|
path = os.path.join(os.path.realpath(os.curdir), fname)
|
|
else:
|
|
path = fname
|
|
|
|
if os.access(path, os.X_OK):
|
|
# Executable. Run and grab output
|
|
log.debug("Executing %s", path)
|
|
proc = subprocess.Popen(path,
|
|
shell=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
close_fds=True)
|
|
stdout, stderr = proc.communicate(input)
|
|
if proc.returncode != 0:
|
|
log.error("%s returned non-zero exit code %s",
|
|
path,
|
|
proc.returncode)
|
|
log.error(stderr)
|
|
raise subprocess.CalledProcessError(proc.returncode, path, stderr)
|
|
out = stdout
|
|
else:
|
|
# Normal file
|
|
with open(path, 'r') as filehandle:
|
|
out = filehandle.read()
|
|
return json.loads(out)
|
|
|
|
|
|
def run_as(uid, gid, groups):
|
|
"""
|
|
Closure that changes the current running user and groups. Called before
|
|
executing scripts by Subprocess.
|
|
"""
|
|
def set_acc():
|
|
"""
|
|
Change user and groups
|
|
"""
|
|
os.setgroups(groups)
|
|
os.setgid(gid)
|
|
os.setuid(uid)
|
|
return set_acc
|
|
|
|
|
|
def run_script(form_def, form_values, env, stdout=None, stderr=None):
|
|
"""
|
|
Perform a callback for the form `form_def`. This calls a script.
|
|
`form_values` is a dictionary of validated values as returned by
|
|
FormDefinition.validate(). If form_def.output is of type 'raw', `stdout`
|
|
and `stderr` have to be open filehandles where the output of the
|
|
callback should be written. The output of the script is hooked up to
|
|
the output, depending on the output type.
|
|
"""
|
|
log = logging.getLogger('RUNSCRIPT')
|
|
|
|
# Validate params
|
|
if form_def.output == 'raw' and (stdout is None or stderr is None):
|
|
msg = 'stdout and stderr cannot be none if script output ' \
|
|
'is \'raw\''
|
|
raise ValueError(msg)
|
|
|
|
# Pass form values to the script through the environment as strings.
|
|
for key, value in form_values.items():
|
|
env[key] = str(value)
|
|
|
|
# Get the user uid, gid and groups we should run as. If the current
|
|
# user is root, we run as the given user or 'nobody' if no user was
|
|
# specified. Otherwise, we run as the user we already are.
|
|
if os.getuid() == 0:
|
|
if form_def.run_as is not None:
|
|
runas_pw = pwd.getpwnam(form_def.run_as)
|
|
else:
|
|
# Run as nobody
|
|
runas_pw = pwd.getpwnam('nobody')
|
|
runas_gr = grp.getgrgid(runas_pw.pw_gid)
|
|
groups = [
|
|
g.gr_gid
|
|
for g in grp.getgrall()
|
|
if runas_pw.pw_name in g.gr_mem
|
|
]
|
|
msg = "Running script as user={0}, gid={1}, groups={2}"
|
|
run_as_fn = run_as(runas_pw.pw_uid, runas_pw.pw_gid, groups)
|
|
log.info("%s", msg.format(runas_pw.pw_name,
|
|
runas_gr.gr_name,
|
|
str(groups)))
|
|
else:
|
|
run_as_fn = None
|
|
if form_def.run_as is not None:
|
|
log.critical("Not running as root, so we can't run the "
|
|
"script as user '%s'", form_def.run_as)
|
|
|
|
# If the form output type is 'raw', we directly stream the output to
|
|
# the browser. Otherwise we store it for later displaying.
|
|
if form_def.output == 'raw':
|
|
try:
|
|
proc = subprocess.Popen(form_def.script,
|
|
shell=True,
|
|
stdout=stdout,
|
|
stderr=stderr,
|
|
env=env,
|
|
close_fds=True,
|
|
preexec_fn=run_as_fn)
|
|
stdout, stderr = proc.communicate(input)
|
|
log.info("Exit code: %s", proc.returncode)
|
|
return proc.returncode
|
|
except OSError as err:
|
|
log.exception(err)
|
|
stderr.write(str(err) + '. Please see the log file.')
|
|
return -1
|
|
else:
|
|
try:
|
|
proc = subprocess.Popen(form_def.script,
|
|
shell=True,
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
env=env,
|
|
close_fds=True,
|
|
preexec_fn=run_as_fn)
|
|
stdout, stderr = proc.communicate()
|
|
log.info("Exit code: %s", proc.returncode)
|
|
return {
|
|
'stdout': stdout,
|
|
'stderr': stderr,
|
|
'exitcode': proc.returncode
|
|
}
|
|
except OSError as err:
|
|
log.exception(err)
|
|
return {
|
|
'stdout': '',
|
|
'stderr': 'Internal error: {0}. Please see the log '
|
|
'file.'.format(str(err)),
|
|
'exitcode': -1
|
|
}
|
|
|