K2LL33D SHELL

 Apache/2.4.7 (Ubuntu)
 Linux sman1baleendah 3.13.0-24-generic #46-Ubuntu SMP Thu Apr 10 19:11:08 UTC 2014 x86_64
 uid=33(www-data) gid=33(www-data) groups=33(www-data)
 safemode : OFF
 MySQL: ON | Perl: ON | cURL: OFF | WGet: ON
  >  / usr / lib / python2.7 / dist-packages / landscape / manager /
server ip : 104.21.89.46

your ip : 172.70.80.237

H O M E


Filename/usr/lib/python2.7/dist-packages/landscape/manager/scriptexecution.py
Size13.49 kb
Permissionrw-r--r--
Ownerroot : root
Create time27-Apr-2025 09:56
Last modified20-Feb-2014 23:01
Last accessed06-Jul-2025 19:45
Actionsedit | rename | delete | download (gzip)
Viewtext | code | image
"""
Functionality for running arbitrary shell scripts.

@var ALL_USERS: A token indicating all users should be allowed.
"""
import os
import pwd
import tempfile
import operator
import shutil

from twisted.internet.protocol import ProcessProtocol
from twisted.internet.defer import (
Deferred, fail, inlineCallbacks, returnValue, succeed)
from twisted.internet.error import ProcessDone

from landscape import VERSION
from landscape.constants import UBUNTU_PATH
from landscape.lib.scriptcontent import build_script
from landscape.lib.fetch import fetch_async, HTTPCodeError
from landscape.lib.persist import Persist
from landscape.lib.encoding import encode_if_needed
from landscape.manager.plugin import ManagerPlugin, SUCCEEDED, FAILED


ALL_USERS = object()
TIMEOUT_RESULT = 102
PROCESS_FAILED_RESULT = 103
FETCH_ATTACHMENTS_FAILED_RESULT = 104


class UnknownUserError(Exception):
pass


def get_user_info(username=None):
uid = None
gid = None
path = None
if username is not None:
username_str = encode_if_needed(username)
try:
info = pwd.getpwnam(username_str)
except KeyError:
raise UnknownUserError(u"Unknown user '%s'" % username)
uid = info.pw_uid
gid = info.pw_gid
path = info.pw_dir
if not os.path.exists(path):
path = "/"
return (uid, gid, path)


class ProcessTimeLimitReachedError(Exception):
"""
Raised when a process has been running for too long.

@ivar data: The data that the process printed before reaching the time
limit.
"""

def __init__(self, data):
self.data = data


class ProcessFailedError(Exception):
"""Raised when a process exits with a non-0 exit code.

@ivar data: The data that the process printed before reaching the time
limit.
"""

def __init__(self, data, exit_code):
self.data = data
self.exit_code = exit_code


class UnknownInterpreterError(Exception):
"""Raised when the interpreter specified to run a script is invalid.

@ivar interpreter: the interpreter specified for the script.
"""

def __init__(self, interpreter):
self.interpreter = interpreter
Exception.__init__(self, self._get_message())

def _get_message(self):
return "Unknown interpreter: '%s'" % self.interpreter


class ScriptRunnerMixin(object):
"""
@param process_factory: The L{IReactorProcess} provider to run the
process with.
"""

def __init__(self, process_factory=None):
if process_factory is None:
from twisted.internet import reactor as process_factory
self.process_factory = process_factory

def is_user_allowed(self, user):
allowed_users = self.registry.config.get_allowed_script_users()
return allowed_users == ALL_USERS or user in allowed_users

def write_script_file(self, script_file, filename, shell, code, uid, gid):
# Chown and chmod it before we write the data in it - the script may
# have sensitive content
# It would be nice to use fchown(2) and fchmod(2), but they're not
# available in python and using it with ctypes is pretty tedious, not
# to mention we can't get errno.
os.chmod(filename, 0700)
if uid is not None:
os.chown(filename, uid, gid)
script_file.write(build_script(shell, code))
script_file.close()

def _run_script(self, filename, uid, gid, path, env, time_limit):

if uid == os.getuid():
uid = None
if gid == os.getgid():
gid = None

pp = ProcessAccumulationProtocol(
self.registry.reactor, self.size_limit)
self.process_factory.spawnProcess(
pp, filename, uid=uid, gid=gid, path=path, env=env)
if time_limit is not None:
pp.schedule_cancel(time_limit)
return pp.result_deferred


class ScriptExecutionPlugin(ManagerPlugin, ScriptRunnerMixin):
"""A plugin which allows execution of arbitrary shell scripts.

@ivar size_limit: The number of bytes at which to truncate process output.
"""

size_limit = 500000

def register(self, registry):
super(ScriptExecutionPlugin, self).register(registry)
registry.register_message(
"execute-script", self._handle_execute_script)

def _respond(self, status, data, opid, result_code=None):
if not isinstance(data, unicode):
# Let's decode result-text, replacing non-printable
# characters
data = data.decode("utf-8", "replace")
message = {"type": "operation-result",
"status": status,
"result-text": data,
"operation-id": opid}
if result_code:
message["result-code"] = result_code
return self.registry.broker.send_message(
message, self._session_id, True)

def _handle_execute_script(self, message):
opid = message["operation-id"]
try:
user = message["username"]
if not self.is_user_allowed(user):
return self._respond(
FAILED,
u"Scripts cannot be run as user %s." % (user,),
opid)
server_supplied_env = message.get("env", None)

d = self.run_script(message["interpreter"], message["code"],
time_limit=message["time-limit"], user=user,
attachments=message["attachments"],
server_supplied_env=server_supplied_env)
d.addCallback(self._respond_success, opid)
d.addErrback(self._respond_failure, opid)
return d
except Exception, e:
self._respond(FAILED, self._format_exception(e), opid)
raise

def _format_exception(self, e):
return u"%s: %s" % (e.__class__.__name__, e.args[0])

def _respond_success(self, data, opid):
return self._respond(SUCCEEDED, data, opid)

def _respond_failure(self, failure, opid):
code = None
if failure.check(ProcessTimeLimitReachedError):
code = TIMEOUT_RESULT
elif failure.check(ProcessFailedError):
code = PROCESS_FAILED_RESULT
elif failure.check(HTTPCodeError):
code = FETCH_ATTACHMENTS_FAILED_RESULT
return self._respond(
FAILED, str(failure.value), opid,
FETCH_ATTACHMENTS_FAILED_RESULT)

if code is not None:
return self._respond(FAILED, failure.value.data, opid, code)
else:
return self._respond(FAILED, str(failure), opid)

@inlineCallbacks
def _save_attachments(self, attachments, uid, gid, computer_id):
root_path = self.registry.config.url.rsplit("/", 1)[0] + "/attachment/"
attachment_dir = tempfile.mkdtemp()
headers = {"User-Agent": "landscape-client/%s" % VERSION,
"Content-Type": "application/octet-stream",
"X-Computer-ID": computer_id}
for filename, attachment_id in attachments.items():
if isinstance(attachment_id, str):
# Backward compatible behavior
data = attachment_id
yield succeed(None)
else:
data = yield fetch_async(
"%s%d" % (root_path, attachment_id),
cainfo=self.registry.config.ssl_public_key,
headers=headers)
full_filename = os.path.join(attachment_dir, filename)
attachment = file(full_filename, "wb")
os.chmod(full_filename, 0600)
if uid is not None:
os.chown(full_filename, uid, gid)
attachment.write(data)
attachment.close()
os.chmod(attachment_dir, 0700)
if uid is not None:
os.chown(attachment_dir, uid, gid)
returnValue(attachment_dir)

def run_script(self, shell, code, user=None, time_limit=None,
attachments=None, server_supplied_env=None):
"""
Run a script based on a shell and the code.

A file will be written with #!<shell> as the first line, as executable,
and run as the given user.

XXX: Handle the 'reboot' and 'killall landscape-client' commands
gracefully.

@param shell: The interpreter to use.
@param code: The code to run.
@param user: The username to run the process as.
@param time_limit: The number of seconds to allow the process to run
before killing it and failing the returned Deferred with a
L{ProcessTimeLimitReachedError}.
@param attachments: C{dict} of filename/data attached to the script.

@return: A deferred that will fire with the data printed by the process
or fail with a L{ProcessTimeLimitReachedError}.
"""
if not os.path.exists(shell.split()[0]):
return fail(
UnknownInterpreterError(shell))

uid, gid, path = get_user_info(user)

fd, filename = tempfile.mkstemp()
script_file = os.fdopen(fd, "w")
self.write_script_file(
script_file, filename, shell, code, uid, gid)

env = {"PATH": UBUNTU_PATH, "USER": user or "", "HOME": path or ""}
if server_supplied_env:
env.update(server_supplied_env)
old_umask = os.umask(0022)

if attachments:
persist = Persist(
filename=os.path.join(self.registry.config.data_path,
"broker.bpickle"))
persist = persist.root_at("registration")
computer_id = persist.get("secure-id")
d = self._save_attachments(attachments, uid, gid, computer_id)
else:
d = succeed(None)

def prepare_script(attachment_dir):

if attachment_dir is not None:
env["LANDSCAPE_ATTACHMENTS"] = attachment_dir

return self._run_script(
filename, uid, gid, path, env, time_limit)

d.addCallback(prepare_script)
return d.addBoth(self._cleanup, filename, env, old_umask)

def _cleanup(self, result, filename, env, old_umask):
try:
os.unlink(filename)
except:
pass
if "LANDSCAPE_ATTACHMENTS" in env:
try:
shutil.rmtree(env["LANDSCAPE_ATTACHMENTS"])
except:
pass
os.umask(old_umask)
return result


class ProcessAccumulationProtocol(ProcessProtocol):
"""A ProcessProtocol which accumulates output.

@ivar size_limit: The number of bytes at which to truncate output.
"""

def __init__(self, reactor, size_limit):
self.data = []
self.result_deferred = Deferred()
self._cancelled = False
self.size_limit = size_limit
self.reactor = reactor
self._scheduled_cancel = None

def schedule_cancel(self, time_limit):
self._scheduled_cancel = self.reactor.call_later(
time_limit, self._cancel)

def childDataReceived(self, fd, data):
"""Some data was received from the child.

Add it to our buffer, as long as it doesn't go over L{size_limit}
bytes.
"""
current_size = reduce(operator.add, map(len, self.data), 0)
self.data.append(data[:self.size_limit - current_size])

def processEnded(self, reason):
"""Fire back the deferred.

The deferred will be fired with the string of data received from the
subprocess, or if the subprocess was cancelled, a
L{ProcessTimeLimitReachedError} will be fired with data accumulated so
far.
"""
exit_code = reason.value.exitCode
data = "".join(self.data)
if self._cancelled:
self.result_deferred.errback(ProcessTimeLimitReachedError(data))
else:
if self._scheduled_cancel is not None:
scheduled = self._scheduled_cancel
self._scheduled_cancel = None
self.reactor.cancel_call(scheduled)

if reason.check(ProcessDone):
self.result_deferred.callback(data)
else:
self.result_deferred.errback(ProcessFailedError(data,
exit_code))

def _cancel(self):
"""
Close filedescriptors, kill the process, and indicate that a
L{ProcessTimeLimitReachedError} should be fired on the deferred.
"""
# Sometimes children of the shell we're killing won't die unless their
# file descriptors are closed! For example, if /bin/sh -c "cat" is the
# process, "cat" won't die when we kill its shell. I'm not sure if this
# is really sufficient: maybe there's a way we can walk over all
# children of the process we started and kill them all.
for i in (0, 1, 2):
self.transport.closeChildFD(i)
self.transport.signalProcess("KILL")
self._cancelled = True


class ScriptExecution(ManagerPlugin):
"""
Meta-plugin wrapping ScriptExecutionPlugin and CustomGraphPlugin.
"""

def __init__(self):
from landscape.manager.customgraph import CustomGraphPlugin
self._script_execution = ScriptExecutionPlugin()
self._custom_graph = CustomGraphPlugin()

def register(self, registry):
super(ScriptExecution, self).register(registry)
self._script_execution.register(registry)
self._custom_graph.register(registry)

def exchange(self, urgent=False):
self._custom_graph.exchange(urgent)