def _get_delegation_tokens(self, username, delegation_token_dir): """ If operating against Kerberized Hadoop, we'll need to have obtained delegation tokens for the user we want to run the subprocess as. We have to do it here rather than in the subprocess because the subprocess does not have Kerberos credentials in that case. """ delegation_token_files = [] all_clusters = [] all_clusters += all_mrclusters().values() all_clusters += get_all_hdfs().values() LOG.debug("Clusters to potentially acquire tokens for: %s" % (repr(all_clusters),)) for cluster in all_clusters: if cluster.security_enabled: current_user = cluster.user try: cluster.setuser(username) token = cluster.get_delegation_token(KERBEROS.HUE_PRINCIPAL.get()) token_file_no, path = tempfile.mkstemp(dir=delegation_token_dir) os.write(token_file_no, token) os.close(token_file_no) delegation_token_files.append(path) finally: cluster.setuser(current_user) return delegation_token_files
def kill_children(self): for pid, child in self.children.items(): try: os.write(child.kill_pipe, 'k') child.active = False # all maintenance of children's membership happens in runloop() # as children die and os.wait() gets results except OSError, e: if e.errno != errno.EPIPE: raise
def collect_child_status(self, child): self.child_events[child.pid] = event.Event() try: try: # tell the child to POST its status to us, we handle it in the # wsgi application below eventlet.hubs.trampoline(child.kill_pipe, write=True) os.write(child.kill_pipe, 's') t = eventlet.Timeout(1) results = self.child_events[child.pid].wait() t.cancel() except (OSError, IOError), e: results = {'error': "%s %s" % (type(e), e)} except eventlet.Timeout: results = {'error':'Timed out'}
def collect_child_status(self, child): self.child_events[child.pid] = event.Event() try: try: # tell the child to POST its status to us, we handle it in the # wsgi application below eventlet.hubs.trampoline(child.kill_pipe, write=True) os.write(child.kill_pipe, 's') t = eventlet.Timeout(1) results = self.child_events[child.pid].wait() t.cancel() except (OSError, IOError), e: results = {'error': "%s %s" % (type(e), e)} except eventlet.Timeout: results = {'error': 'Timed out'}
class Shell(object): """ A class to encapsulate I/O with a shell subprocess. """ def __init__(self, shell_command, subprocess_env, shell_id, username, delegation_token_dir): try: user_info = pwd.getpwnam(username) except KeyError: LOG.error("Unix user account didn't exist at subprocess creation. Was it deleted?") raise parent, child = pty.openpty() try: tty.setraw(parent) except tty.error: LOG.debug("Could not set parent fd to raw mode, user will see duplicated input.") subprocess_env[constants.HOME] = user_info.pw_dir command_to_use = [_SETUID_PROG, str(user_info.pw_uid), str(user_info.pw_gid)] command_to_use.extend(shell_command) delegation_token_files = self._get_delegation_tokens(username, delegation_token_dir) if delegation_token_files: merged_token_file_path = self._merge_delegation_tokens(delegation_token_files, delegation_token_dir) for path in delegation_token_files: try: os.unlink(path) except: LOG.warning("Could not remove delegation token file %s" % path) delegation_token_files = [merged_token_file_path] subprocess_env[constants.HADOOP_TOKEN_FILE_LOCATION] = merged_token_file_path try: LOG.debug("Starting subprocess with command '%s' and environment '%s'" % (command_to_use, subprocess_env,)) p = subprocess.Popen(command_to_use, stdin=child, stdout=child, stderr=child, env=subprocess_env, close_fds=True) except (OSError, ValueError): os.close(parent) os.close(child) raise msg_format = "%s - shell_id:%s pid:%d - args:%s" msg_args = (username, shell_id, p.pid, ' '.join(command_to_use)) msg = msg_format % msg_args SHELL_OUTPUT_LOGGER.info(msg) SHELL_INPUT_LOGGER.info(msg) # State that shouldn't be touched by any other classes. self._output_buffer_length = 0 self._commands = [] self._fd = parent self._child_fd = child self.subprocess = p self.pid = p.pid self._write_buffer = cStringIO.StringIO() self._read_buffer = cStringIO.StringIO() self._delegation_token_files = delegation_token_files # State that's accessed by other classes. self.shell_id = shell_id self.username = username # Timestamp that is updated on shell creation and on every output request. Used so that we know # when to kill the shell. self.time_received = time.time() self.last_output_sent = False self.remove_at_next_iteration = False self.destroyed = False def _merge_delegation_tokens(self, delegation_token_files, delegation_token_dir): """ Use the Credentials Merger utility to combine the delegation token files into one delegation token file. Returns the NamedTemporaryFile that contains the combined delegation tokens. """ merged_token_file_no, merged_token_file_path = tempfile.mkstemp(dir=delegation_token_dir) os.close(merged_token_file_no) merge_tool_args = [hadoop.conf.HDFS_CLUSTERS['default'].HADOOP_BIN.get(), 'jar'] merge_tool_args += [hadoop.conf.CREDENTIALS_MERGER_JAR.get(), merged_token_file_path] merge_tool_args += delegation_token_files LOG.debug("Merging credentials files with command: '%s'" % (' '.join(merge_tool_args))) merge_process = subprocess.Popen(merge_tool_args, stderr=subprocess.PIPE, shell=False, close_fds=True) while merge_process.poll() is None: time.sleep(1) retcode = merge_process.wait() if retcode != 0: LOG.error("Failed to merge credentials :'%s'..." % (merge_process.stderr.readline(),)) raise MergeToolException(_("bin/hadoop return non-zero %(retcode)d while trying to merge credentials.") % dict(retcode=(retcode,))) return merged_token_file_path def _get_delegation_tokens(self, username, delegation_token_dir): """ If operating against Kerberized Hadoop, we'll need to have obtained delegation tokens for the user we want to run the subprocess as. We have to do it here rather than in the subprocess because the subprocess does not have Kerberos credentials in that case. """ delegation_token_files = [] all_clusters = [] all_clusters += all_mrclusters().values() all_clusters += get_all_hdfs().values() LOG.debug("Clusters to potentially acquire tokens for: %s" % (repr(all_clusters),)) for cluster in all_clusters: if cluster.security_enabled: current_user = cluster.user try: cluster.setuser(username) token = cluster.get_delegation_token(KERBEROS.HUE_PRINCIPAL.get()) token_file_no, path = tempfile.mkstemp(dir=delegation_token_dir) os.write(token_file_no, token) os.close(token_file_no) delegation_token_files.append(path) finally: cluster.setuser(current_user) return delegation_token_files def mark_for_cleanup(self): """ Flag this shell to be picked up at the next iteration of handle_periodic. """ self.remove_at_next_iteration = True def get_previous_output(self): """ Called when a Hue session is restored. Returns a tuple of ( all previous output, next offset). """ val = self._read_buffer.getvalue() return ( val, len(val)) def get_previous_commands(self): """ Return the list of previously entered commands. This is used for bash_history semantics when restoring Shells. """ return self._commands def get_cached_output(self, offset): """ The offset is not the latest one, so some output has already been generated and is stored in the read buffer. So let's fetch it from there. Returns (output, has_more, new_offset) or None. """ self._read_buffer.seek(offset) next_output = self._read_buffer.read() if not next_output: return None more_available = len(next_output) >= shell.conf.SHELL_OS_READ_AMOUNT.get() return (next_output, more_available, self._output_buffer_length) def process_command(self, command): """ Write the command to the end of the wite buffer, and spawn a greenlet to write it into the subprocess when the subprocess becomes writable. Returns a dictionary with {return_code: bool}. """ # TODO(bc): Track the buffer size to avoid calling getvalue() every time if len(self._write_buffer.getvalue()) >= shell.conf.SHELL_WRITE_BUFFER_LIMIT.get(): return { constants.BUFFER_EXCEEDED : True } else: self._append_to_write_buffer(command) eventlet.spawn_n(self._write_child_when_able) return { constants.SUCCESS : True } def _append_to_write_buffer(self, command): """ Append the received command to the write buffer. This buffer is used when the child becomes readable to send commands to the child subprocess. """ self._write_buffer.seek(len(self._write_buffer.getvalue())) self._write_buffer.write("%s" % (command,)) # We seek back to the beginning so that when the child becomes writable we # feed the commands to the child in the order they were received. self._commands.append(command) while len(self._commands) > 25: self._commands.pop(0) def _read_from_write_buffer(self): """ Read and return the contents of the write buffer. """ self._write_buffer.seek(0) contents = self._write_buffer.read() return contents def _write_child_when_able(self): """ Select on the child's input file descriptor becoming writable, and then write commands to it. If not successful in writing all the commands, spawn a new greenlet to retry. """ LOG.debug("write_child_when_able") buffer_contents = self._read_from_write_buffer() if not buffer_contents: return try: r, w, x = select.select([],[self._fd],[]) except Exception, e: # The next 9 lines are taken from Facebook's Tornado project, which is open-sourced under # the Apache license. # Depending on python version and poll implementation, # different exception types may be thrown and there are # two ways EINTR might be signaled: # * e.errno == errno.EINTR # * e.args is like (errno.EINTR, 'Interrupted system call') if (getattr(e, 'errno') == errno.EINTR or (isinstance(getattr(e, 'args'), tuple) and len(e.args) == 2 and e.args[0] == errno.EINTR)): LOG.warning("Interrupted system call", exc_info=1) eventlet.spawn_n(self._write_child_when_able) else: LOG.error("Unexpected error on select") self.mark_for_cleanup() return if not w: return try: bytes_written = os.write(self._fd, buffer_contents) self._advance_write_buffer(bytes_written) except OSError, e: if e.errno == errno.EINTR: eventlet.spawn_n(self._write_child_when_able) elif e.errno != errno.EAGAIN: error_str = "%s - shell_id:%s pid:%d - Error writing to subprocess:%s" %\ (self.username, self.shell_id, self.pid, e,) LOG.error(error_str) SHELL_INPUT_LOGGER.error(error_str) self.mark_for_cleanup()