"""
*
* This file is part of rasdaman community.
*
* Rasdaman community 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 3 of the License, or
* (at your option) any later version.
*
* Rasdaman community 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 rasdaman community. If not, see .
*
* Copyright 2003 - 2016 Peter Baumann / rasdaman GmbH.
*
* For more information please see
* or contact Peter Baumann via .
*
"""
# make sure all string literals are unicode
from __future__ import unicode_literals
import os
import pwd
import subprocess
import time
from threading import Timer
from config_manager import ConfigManager
from util.log import log
from util.string_util import get_timestamp, binary_to_string
class UserInfo:
def __init__(self, name):
"""
Creates a user information record from the name
:param str name: the username
"""
pw_record = pwd.getpwnam(name)
self.user_name = pw_record.pw_name
self.user_home_dir = pw_record.pw_dir
self.user_uid = pw_record.pw_uid
self.user_gid = pw_record.pw_gid
def get_env_for_user(userinfo, cwd, env=None):
"""
Gets the environment for a specific user
:param UserInfo userinfo:the user information
:param str cwd: the cwd
:param dict env: an existing environment or none
"""
if env is None:
env = os.environ.copy()
env['HOME'] = userinfo.user_home_dir
env['LOGNAME'] = userinfo.user_name
env['PWD'] = cwd
env['USER'] = userinfo.user_name
return env
def demote_user_callback(user_info):
"""
Generates a callback to be used once the process forks
:param UserInfo user_info: the user information
:rtype: function
"""
def result():
os.setgid(user_info.user_gid)
os.setuid(user_info.user_uid)
return result
def kill_proc(proc, timed_out):
timed_out["value"] = True
proc.kill()
class ExecutionError(Exception):
def __init__(self, command, stdout, stderr, return_code):
"""
Class to describe an execution error
:param str command: the command that was tried
:param str stdout: the std output
:param str stderr: the std error
:param int return_code: the return code
"""
self.command = binary_to_string(command)
self.return_code = return_code
self.stdout = binary_to_string(stdout)
self.stderr = binary_to_string(stderr)
def __str__(self):
return "command: {}\nexit code: {}\nstdout: {}\nstderr: {}".format(
self.command, self.return_code, self.stdout, self.stderr)
class Executor:
def __init__(self, historic):
"""
Class to execute shell commands
:param History historic: the history object to save the commands
"""
self.historic = historic
def execute(self, command, working_dir=None, stdin=None, user=None, env=None,
throw_on_error=True, no_wait=False, shell=False, retry=0,
timeout_sec=-1, warn_on_error=False):
"""
Executes the command in the given working directory
:param list[str] command: the command as a list of words, e.g.
"make install" => ["make", "install"]
:param str working_dir: the path to the working dir, defaults to cwd
:param str stdin: a string to be given as the stdin input
:param str user: a username to execute the command. This is only
possible if the executing user has sudo access; defaults to
ConfigManager.default_user user
:param dict env: a environment dict as returned by os.environ.copy()
:param bool throw_on_error: set to false to prevent the executor from
throwing exceptions when the return code is different than 0
:param bool no_wait: if set to true it will not wait for the return code
and stdout. This is especially important when executing pg_ctl CMD for
example, which for whatever reason blocks on Docker when trying to get
the stdout of it.
:param bool shell: to run the command through a shell
:param int retry: number of times to retry a command before throwing an
exception on error
:param int timeout_sec: timeout in seconds before terminating the command
:param bool warn_on_error: log a warning in case the return code of
command is not 0.
:return: returns stdout, stderr, exit code
:rtype: (str, str, int)
"""
start = time.time()
start_time = get_timestamp(start)
if command is None or command == []:
return "", "", 0
if user is None:
user = ConfigManager.default_user
stdout, stderr, return_code = "", "", -1
cmd_string = " ".join(command) if type(command) == list else command
cmd_string = binary_to_string(cmd_string)
try:
# log.debug("Executing command with user " + user + ": " + cmd_string)
userinfo = UserInfo(user)
if working_dir is None:
working_dir = os.getcwd()
environment = get_env_for_user(userinfo, working_dir, env)
preexec_fn = demote_user_callback(userinfo)
while retry >= 0 and return_code != 0:
proc = subprocess.Popen(command,
preexec_fn=preexec_fn,
cwd=working_dir,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=environment,
shell=shell)
if no_wait:
break
# execute command, with or without a timeout
if timeout_sec > 0:
# will be set to True if the process timed out
timed_out = {"value": False}
timer = Timer(timeout_sec, kill_proc, [proc, timed_out])
try:
# start a timer that will kill the process if it isn't
# cancelled below before the process returns
timer.start()
stdout, stderr = proc.communicate(input=stdin)
stdout = binary_to_string(stdout)
stderr = binary_to_string(stderr)
finally:
timer.cancel()
if timed_out["value"]:
stderr += f"\nCommand execution timed out after {timeout_sec} seconds."
else:
return_code = proc.returncode
else:
stdout, stderr = proc.communicate(input=stdin)
stdout = binary_to_string(stdout)
stderr = binary_to_string(stderr)
return_code = proc.returncode
if return_code != 0:
if throw_on_error and retry == 0:
raise ExecutionError(cmd_string, stdout, stderr, return_code)
retry = retry - 1
if retry >= 0:
log.info("retrying command...")
else:
break
except OSError as e:
if e.errno == 1 and e.strerror == "Operation not permitted":
log.error("OS error: " + str(e) + "; most likely you need to execute with sudo/root user.")
else:
log.error("OS error: " + str(e))
if throw_on_error:
raise ExecutionError(cmd_string, stdout, e.strerror, e.errno)
except ExecutionError as e:
raise e
except Exception as e:
log.error(f"Failed executing command ({cmd_string}): {e}")
if throw_on_error:
raise ExecutionError(cmd_string, stdout, str(e), return_code)
finally:
end = time.time()
self.historic.add_to_history(cmd_string, stdout, stderr, return_code,
user, start_time, end - start)
if warn_on_error and return_code != 0:
log.warn(f"command failed: {cmd_string}\n"
f"exit code: {return_code}\n"
f"stdout: {stdout}\n"
f"stderr: {stderr}")
return stdout, stderr, return_code
def executeSudo(self, command, working_dir=None, stdin=None, env=None,
throw_on_error=True, no_wait=False, shell=False, retry=0,
timeout_sec=-1, warn_on_error=False):
"""
Executes the command as the root user. See the execute method for
parameter and result documentation.
"""
return self.execute(command, working_dir, stdin, "root", env,
throw_on_error, no_wait, shell, retry, timeout_sec,
warn_on_error)
def executeWithNewShell(self, command, working_dir=None, stdin=None, user=None,
env=None, throw_on_error=True, no_wait=False,
shell=False, retry=0, timeout_sec=-1, warn_on_error=False):
"""
Executes the command in a reloaded shell. You will have to escape your
arguments manually. This is useful for the moments where you need to
simulate a logout / login, e.g. after adding a new group to some user.
:param list[str] command: the command as a list of words, e.g.
"make install" => ["make", "install"]
:param str working_dir: the path to the working dir, defaults to cwd
:param str stdin: a string to be given as the stdin input
:param str user: a username to execute the command. This is only
possible if the executing user has sudo access; defaults to
ConfigManager.default_user user
:param dict env: a environment dict as returned by os.environ.copy()
:param bool throw_on_error: set to false to prevent the executor from
throwing exceptions when the return code is different than 0
:param bool no_wait: if set to true it will not wait for the return code
and stdout
:param bool shell: to run the command through a shell
:param int retry: number of times to retry a command before throwing an
exception on error
:return: returns stdout and stderr
:rtype: (str,str, int)
"""
if user is None:
user = ConfigManager.default_user
command = ["sudo", "su", user, "-c", " ".join(command)]
return self.executeSudo(command, working_dir, stdin, env, throw_on_error,
no_wait, shell, retry, timeout_sec, warn_on_error)