Source code for remoteappmanager.jupyterhub.spawners

import os
import escapism
import string

from traitlets import Any, Unicode, default
from tornado import gen

from jupyterhub.spawner import LocalProcessSpawner

ADMIN_CMD = "remoteappadmin"
USER_CMD = "remoteappmanager"


class BaseSpawner(LocalProcessSpawner):
    """Base class that provides common infrastructure to
    the actual spawners
    """

    #: The instance of the JupyterHub Proxy.
    #: We use Any in agreement with base class practice.
    proxy = Any()

    #: The path of the configuration file for the cmd executable
    config_file_path = Unicode(config=True)

    @property
    def cmd(self):
        """Overrides the base class traitlet so that we take full control
        of the spawned command according to user admin status"""
        return self.user_options.get('cmd', self._default_cmd())

    def __init__(self, **kwargs):
        super(BaseSpawner, self).__init__(**kwargs)
        # We can obtain a reference to the JupyterHub.proxy object
        # through the tornado settings passed onto the User
        self.proxy = self.user.settings.get('proxy')

    @default("options_form")
    def _options_form_default(self):
        """ Gives admins the option of spawning either RemoteAppManager
        admin or user sessions
        """
        if self.user.admin:
            return """
            <div>
                <legend>Choose RemoteAppManager Session:</legend>
                <select id="session_form" name="session" size="2">
                    <option value="admin" selected>Admin</option>
                    <option value="user">User</option>
                </select>
            </div>
            """
        return ""

    def _default_cmd(self):
        return [ADMIN_CMD] if self.user.admin else [USER_CMD]

    def options_from_form(self, form_data):
        """ Attempt to extract session selection from HTML form and
        return default session if not available
        """
        cmd = self._default_cmd()
        if "session" in form_data:
            selected = form_data.pop("session")[0]
            cmd = [ADMIN_CMD] if selected == "admin" else [USER_CMD]
        return {'cmd': cmd}

    def get_args(self):
        args = super().get_args()

        # Handle no default IP value (removed in jupyterhub v0.8.0)
        if not self.ip:
            args.append('--ip="127.0.0.1"')

        args.append('--user="{}"'.format(
            self.user.name))

        args.append('--base-urlpath="{}"'.format(
            self.server.base_url))

        args.append('--hub-prefix={}'.format(
            self.hub.base_url))

        args.append("--cookie-name={}".format(
            self.hub.cookie_name))

        args.append("--proxy-api-url={}".format(
            self.proxy.api_url))

        args.append("--logout_url={}".format(
            self.authenticator.logout_url(
                self.hub.base_url)))

        if self.config_file_path:
            args.append("--config-file={}".format(self.config_file_path))

        args.append("--login_service={}".format(
            self.authenticator.login_service))

        return args

    def get_env(self):
        env = super().get_env()
        env["PROXY_API_TOKEN"] = self.proxy.auth_token
        env.update(_docker_envvars())
        return env


class SystemUserSpawner(BaseSpawner):
    """
    Start remoteappmanager as a local process for a system user.

    The user identifier of the process is set to be the system user.
    The current directory is set to the system user's home directory.
    """


class VirtualUserSpawner(BaseSpawner):
    ''' Start remoteappmanager as a local process for a virtual user.

    A virtual user is not recognised as a system user, even if the
    user's name conincide with an existing system user.  As a result,
    the user does not need to be a system user for this spawner.

    The user identifier and the current work directory of the spawned
    local process are the same as the one that is running jupyterhub.
    '''

    #: Directory in which temporary home directory for the virtual
    #: user is created.  No directory is created if this is not
    #: defined and HOME directory would not be available.
    workspace_dir = Unicode(config=True)

    #: The path to the temporary workspace directory
    _virtual_workspace = Unicode()

    def make_preexec_fn(self, name):
        # We don't set user uid for virtual user
        # Nor do we try to start the process in the user's
        # home directory (it does not exist)
        pass

    def load_state(self, state):
        super().load_state(state)
        virtual_workspace = state.get('virtual_workspace')
        if virtual_workspace:
            if os.path.isdir(virtual_workspace):
                self._virtual_workspace = virtual_workspace
            else:
                self.log.warn('Previous virtual workspace is gone.')

    def get_state(self):
        state = super().get_state()
        if self._virtual_workspace:
            state['virtual_workspace'] = self._virtual_workspace
        return state

    def clear_state(self):
        super().clear_state()
        self._virtual_workspace = ''

    def user_env(self, env):
        env['USER'] = self.user.name

        if self._virtual_workspace:
            env['HOME'] = self._virtual_workspace

        return env

    @gen.coroutine
    def start(self):
        """ Start the process and create the virtual user's
        temporary home directory if `workspace_dir` is set
        """

        # Create the temporary directory as the user's workspace
        if self.workspace_dir and not self._virtual_workspace:
            try:
                workspace = _user_workspace(self.workspace_dir, self.user.name)
                os.makedirs(workspace, 0o755, exist_ok=True)
                self._virtual_workspace = workspace
            except Exception as exception:
                # A whole lot of reasons why temporary directory cannot
                # be created. e.g. workspace_dir does not exist
                # the owner of the process has no write permission
                # for the directory, etc.
                msg = ("Failed to create temporary directory for '{user}' in "
                       "'{tempdir}'.  Temporary workspace would not be "
                       "available. Please assign the spawner's `workspace_dir`"
                       " to a directory path where it has write permission. "
                       "Error: {error}")
                # log as error to avoid ugly traceback
                self.log.error(
                    msg.format(user=self.user.name,
                               tempdir=self.workspace_dir,
                               error=str(exception)))
            else:
                self.log.info("Created %s's temporary workspace in: %s",
                              self.user.name, self._virtual_workspace)
        return (yield super().start())

    @gen.coroutine
    def stop(self, now=False):
        """ Stop the process

        If virtual user has a temporary home directory,
        clean up the directory.
        """
        yield super().stop(now=now)


def _docker_envvars():
    """Returns a dictionary containing the docker environment variables, if
    present. If not present, returns an empty dictionary"""
    env = {envvar: os.environ[envvar]
           for envvar in ["DOCKER_HOST",
                          "DOCKER_CERT_PATH",
                          "DOCKER_MACHINE_NAME",
                          "DOCKER_TLS_VERIFY"]
           if envvar in os.environ}

    return env


# Used by escape
_ESCAPE_SAFE_CHARS = set(string.ascii_letters + string.digits + '-.')
_ESCAPE_CHAR = '_'


# Note: copied from container_manager.py, but we want to keep the
# spawners module completely separated from the remoteappmanager.
[docs]def escape(s): """Trivial escaping wrapper for well established stuff. Works for containers, file names. Note that it is not destructive, so it won't generate collisions.""" return escapism.escape(s, _ESCAPE_SAFE_CHARS, _ESCAPE_CHAR)
def _user_workspace(base_dir, user_name): """Returns the appropriate user workspace for the given username. Raises ValueError if the user_name is only spaces after basenaming. """ name = os.path.basename(user_name).strip() if len(name) == 0: raise ValueError("User name is invalid") return os.path.join(base_dir, escape(name))