Beispiel #1
0
    def mount_driver_config(self):
        if not self.volume_driver:
            return None

        return DriverConfig(
            name=self.volume_driver, options=self.volume_driver_options or None
        )
Beispiel #2
0
    def start(self):
        """Start the single-user server in a docker service.
        You can specify the params for the service through
        jupyterhub_config.py or using the user_options
        """
        self.log.debug("User: {}, start spawn".format(self.user.__dict__))

        # https://github.com/jupyterhub/jupyterhub
        # /blob/master/jupyterhub/user.py#L202
        # By default jupyterhub calls the spawner passing user_options
        if self.use_user_options:
            user_options = self.user_options
        else:
            user_options = {}

        service = yield self.get_service()
        if service is None:
            # Validate state
            if hasattr(self,
                       "container_spec") and self.container_spec is not None:
                container_spec = dict(**self.container_spec)
            elif user_options == {}:
                self.log.error("User: {} is trying to create a service"
                               " without a container_spec".format(self.user))
                raise Exception("That notebook is missing a specification"
                                "to launch it, contact the admin to resolve "
                                "this issue")

            # Setup service
            container_spec.update(user_options.get("container_spec", {}))

            # Which image to spawn
            if self.use_user_options and "user_selected_image" in user_options:
                self.log.debug(
                    "User options received: {}".format(user_options))
                image_name = user_options["user_selected_name"]
                image_value = user_options["user_selected_image"]
                selected_image = None
                for di in self.images:
                    if image_name == di["name"] and image_value == di["image"]:
                        selected_image = copy.deepcopy(di)
                if selected_image is None:
                    err_msg = "User selected image: {} couldn't be found".format(
                        image_value)
                    self.log.error(err_msg)
                    raise Exception(err_msg)
                self.log.info(
                    "Using the user selected image: {}".format(selected_image))
            else:
                # Default image
                selected_image = self.images[0]
                self.log.info(
                    "Using the default image: {}".format(selected_image))

            self.log.debug("Image info: {}".format(selected_image))
            # Does that image have restricted access
            if "access" in selected_image:
                # Check for static or db users
                allowed = False
                if self.service_owner in selected_image["access"]:
                    allowed = True
                else:
                    if os.path.exists(selected_image["access"]):
                        db_path = selected_image["access"]
                        try:
                            self.log.info("Checking db: {} for "
                                          "User: {}".format(
                                              db_path, self.service_owner))
                            with open(db_path, "r") as db:
                                users = [
                                    user.rstrip("\n").rstrip("\r\n")
                                    for user in db
                                ]
                                if self.service_owner in users:
                                    allowed = True
                        except IOError as err:
                            self.log.error("User: {} tried to open db file {},"
                                           "Failed {}".format(
                                               self.service_owner, db_path,
                                               err))
                if not allowed:
                    self.log.error(
                        "User: {} tried to launch {} without access".format(
                            self.service_owner, selected_image["image"]))
                    raise Exception(
                        "You don't have permission to launch that image")

            self.log.debug("Container spec: {}".format(container_spec))

            # Assign the image name as a label
            container_spec["labels"] = {"image_name": selected_image["name"]}

            # Setup mounts
            mounts = []
            # Global mounts
            if "mounts" in container_spec:
                mounts.extend(container_spec["mounts"])
            container_spec["mounts"] = []

            # Image mounts
            if "mounts" in selected_image:
                mounts.extend(selected_image["mounts"])

            for mount in mounts:
                if isinstance(mount, dict):
                    m = VolumeMounter(mount)
                    m = yield m.create(owner=self.service_owner)
                else:
                    # Expects a mount_class that supports 'create'
                    if hasattr(self.user, "data"):
                        m = yield mount.create(self.user.data,
                                               owner=self.service_owner)
                    else:
                        m = yield mount.create(owner=self.service_owner)
                container_spec["mounts"].append(m)

            # Some envs are required by the single-user-image
            if "env" in container_spec:
                container_spec["env"].update(self.get_env())
            else:
                container_spec["env"] = self.get_env()

            # Env of image
            if "env" in selected_image and isinstance(selected_image["env"],
                                                      dict):
                container_spec["env"].update(selected_image["env"])

            # Dynamic update of env values
            for env_key, env_value in container_spec["env"].items():
                stripped_value = env_value.lstrip("{").rstrip("}")
                if hasattr(self, stripped_value) and isinstance(
                        getattr(self, stripped_value), str):
                    container_spec["env"][env_key] = getattr(
                        self, stripped_value)
                if hasattr(self.user, stripped_value) and isinstance(
                        getattr(self.user, stripped_value), str):
                    container_spec["env"][env_key] = getattr(
                        self.user, stripped_value)
                if (hasattr(self.user, "data")
                        and hasattr(self.user.data, stripped_value)
                        and isinstance(getattr(self.user.data, stripped_value),
                                       str)):
                    container_spec["env"][env_key] = getattr(
                        self.user.data, stripped_value)

            # Args of image
            if "args" in selected_image and isinstance(selected_image["args"],
                                                       list):
                container_spec.update({"args": selected_image["args"]})

            if ("command" in selected_image
                    and isinstance(selected_image["command"], list)
                    or "command" in selected_image
                    and isinstance(selected_image["command"], str)):
                container_spec.update({"command": selected_image["command"]})

            # Log mounts config
            self.log.debug("User: {} container_spec mounts: {}".format(
                self.user, container_spec["mounts"]))

            # Global resource_spec
            resource_spec = {}
            if hasattr(self, "resource_spec"):
                resource_spec = self.resource_spec
            resource_spec.update(user_options.get("resource_spec", {}))

            networks = None
            if hasattr(self, "networks"):
                networks = self.networks
            if user_options.get("networks") is not None:
                networks = user_options.get("networks")

            # Global Log driver
            log_driver = None
            if hasattr(self, "log_driver"):
                log_driver = self.log_driver
            if user_options.get("log_driver") is not None:
                log_driver = user_options.get("log_driver")

            accelerators = []
            if hasattr(self, "accelerators"):
                accelerators = self.accelerators
            if user_options.get("accelerators") is not None:
                accelerators = user_options.get("accelerators")

            # Global placement
            placement = None
            if hasattr(self, "placement"):
                placement = self.placement
            if user_options.get("placement") is not None:
                placement = user_options.get("placement")

            # Image to spawn
            image = selected_image["image"]

            # Image resources
            if "resource_spec" in selected_image:
                resource_spec = selected_image["resource_spec"]

            # Accelerators attached to the image
            if "accelerators" in selected_image:
                accelerators = selected_image["accelerators"]

            # Placement of image
            if "placement" in selected_image:
                placement = selected_image["placement"]

            # Logdriver of image
            if "log_driver" in selected_image:
                log_driver = selected_image["log_driver"]

            # Configs attached to image
            if "configs" in selected_image and isinstance(
                    selected_image["configs"], list):
                for c in selected_image["configs"]:
                    if isinstance(c, dict):
                        self.configs.append(c)

            endpoint_spec = {}
            if "endpoint_spec" in selected_image:
                endpoint_spec = selected_image["endpoint_spec"]

            if self.configs:
                # Check that the supplied configs already exists
                current_configs = yield self.docker("configs")
                config_error_msg = (
                    "The server has a misconfigured config, "
                    "please contact an administrator to resolve this")

                for c in self.configs:
                    if "config_name" not in c:
                        self.log.error("Config: {} does not have a "
                                       "required config_name key".format(c))
                        raise Exception(config_error_msg)
                    if "config_id" not in c:
                        # Find the id from the supplied name
                        config_ids = [
                            cc["ID"] for cc in current_configs
                            if cc["Spec"]["Name"] == c["config_name"]
                        ]
                        if not config_ids:
                            self.log.error(
                                "A config with name {} could not be found")
                            raise Exception(config_error_msg)
                        c["config_id"] = config_ids[0]

                container_spec.update(
                    {"configs": [ConfigReference(**c) for c in self.configs]})

            # Prepare the accelerators and attach it to the environment
            if accelerators:
                for accelerator in accelerators:
                    accelerator_id = accelerator.aquire(self.user.name)
                    # NVIDIA_VISIBLE_DEVICES=0:0
                    container_spec["env"][
                        "NVIDIA_VISIBLE_DEVICES"] = "{}".format(accelerator_id)

            # Global container user
            uid_gid = None
            if "uid_gid" in container_spec:
                uid_gid = copy.deepcopy(container_spec["uid_gid"])
                del container_spec["uid_gid"]

            # Image user
            if "uid_gid" in selected_image:
                uid_gid = selected_image["uid_gid"]

            self.log.info("gid info {}".format(uid_gid))
            if isinstance(uid_gid, str):
                if ":" in uid_gid:
                    uid, gid = uid_gid.split(":")
                else:
                    uid, gid = uid_gid, None

                if (uid == "{uid}" and hasattr(self.user, "uid")
                        and self.user.uid is not None):
                    uid = self.user.uid

                if (gid is not None and gid == "{gid}"
                        and hasattr(self.user, "gid")
                        and self.user.gid is not None):
                    gid = self.user.gid

                if uid:
                    container_spec.update({"user": str(uid)})
                if uid and gid:
                    container_spec.update({"user": str(uid) + ":" + str(gid)})

            # Global container user
            if "user" in container_spec:
                container_spec["user"] = str(container_spec["user"])

            # Image user
            if "user" in selected_image:
                container_spec.update({"user": str(selected_image["user"])})

            dynamic_holders = [Spawner, self, self.user]
            if hasattr(self.user, "data"):
                dynamic_holders.append(self.user.data)

            # Expand container_spec before start
            for construct in dynamic_holders:
                try:
                    if not hasattr(construct, "__dict__"):
                        continue
                    recursive_format(container_spec, construct.__dict__)
                except TypeError:
                    pass

            # Log driver
            log_driver_name, log_driver_options = None, None
            if log_driver and isinstance(log_driver, dict):
                if "name" in log_driver:
                    log_driver_name = log_driver["name"]
                if "options" in log_driver:
                    log_driver_options = log_driver["options"]

            # Create the service
            container_spec = ContainerSpec(image, **container_spec)
            resources = Resources(**resource_spec)
            placement = Placement(**placement)

            task_log_driver = None
            if log_driver_name:
                task_log_driver = DriverConfig(log_driver_name,
                                               options=log_driver_options)

            task_spec = {
                "container_spec": container_spec,
                "resources": resources,
                "placement": placement,
            }

            if task_log_driver:
                task_spec.update({"log_driver": task_log_driver})

            task_tmpl = TaskTemplate(**task_spec)
            self.log.debug("task temp: {}".format(task_tmpl))
            # Set endpoint spec
            endpoint_spec = EndpointSpec(**endpoint_spec)

            resp = yield self.docker(
                "create_service",
                task_tmpl,
                name=self.service_name,
                networks=networks,
                endpoint_spec=endpoint_spec,
            )
            self.service_id = resp["ID"]
            self.log.info("Created Docker service {} (id: {}) from image {}"
                          " for user {}".format(self.service_name,
                                                self.service_id[:7], image,
                                                self.user))

            yield self.wait_for_running_tasks()

        else:
            self.log.info("Found existing Docker service '{}' (id: {})".format(
                self.service_name, self.service_id[:7]))
            # Handle re-using API token.
            # Get the API token from the environment variables
            # of the running service:
            envs = service["Spec"]["TaskTemplate"]["ContainerSpec"]["Env"]
            for line in envs:
                if line.startswith("JPY_API_TOKEN="):
                    self.api_token = line.split("=", 1)[1]
                    break

        ip = self.service_name
        port = self.service_port
        self.log.debug("Active service: '{}' with user '{}'".format(
            self.service_name, self.user))

        # we use service_name instead of ip
        # https://docs.docker.com/engine/swarm/networking/#use-swarm-mode-service-discovery
        # service_port is actually equal to 8888
        return ip, port
Beispiel #3
0
    def create_mount(self, data):
        self.log.debug("create_mount: {}".format(data))

        # validate required driver data is present
        err, err_msg = False, []
        if 'sshcmd' not in self.config['driver_options'] \
                or self.config['driver_options']['sshcmd'] == '':
            err_msg.append("create_mount requires that the 'sshcmd'"
                           "driver_options key is set to a nonempty value")
            err = True

        if 'id_rsa' not in self.config['driver_options'] \
                and 'password' not in self.config['driver_options'] \
                or 'id_rsa' in self.config['driver_options'] \
                and 'password' in self.config['driver_options']:
            err_msg.append("create_mount requires either a 'id_rsa'"
                           " or 'password' driver_options key")
            err = True

        if 'id_rsa' in self.config['driver_options'] \
                and self.config['driver_options']['id_rsa'] == '' \
                or 'password' in self.config['driver_options'] \
                and self.config['driver_options']['password'] == '':
            err_msg.append(
                "create_mount requires a nonempty value from either "
                "'id_rsa' or 'password'")
            err = True

        if err:
            self.log.error("create_mount failed: {}".format(','.join(err_msg)))
            raise Exception("An error occurred during mount creation")

        driver = {
            'driver_config': self.config['driver_config'],
            'driver_options': {}
        }
        driver['driver_options'].update(self.config['driver_options'])
        del self.config['driver_options']

        # Setup driver
        # look for values that should be dynamically assigned from data
        for option_key, option_value in driver['driver_options'].items():
            if option_value == "{" + option_key + "}":
                dyn_value = yield self.get_from(option_key, data)
                if dyn_value:
                    driver['driver_options'][option_key] = dyn_value

        # Legacy options that will be deprecated
        if driver['driver_options']['sshcmd'] == '{sshcmd}':
            # Validate that the proper values are present
            username = yield self.get_from('USERNAME', data)
            path = yield self.get_from('PATH', data)
            if username and path:
                driver['driver_options']['sshcmd'] = username + path

        if 'id_rsa' in driver['driver_options'] \
                and driver['driver_options']['id_rsa'] == '{id_rsa}':
            key = yield self.get_from('PRIVATEKEY', data)
            if key:
                driver['driver_options']['id_rsa'] = key

        if 'port' in driver['driver_options'] \
                and driver['driver_options']['port'] == '{port}':
            port = yield self.get_from('PORT', data)
            if port:
                driver['driver_options']['port'] = port
            else:
                driver['driver_options']['port'] = '22'

        mount = {}
        mount.update(self.config)
        mount['driver_config'] = DriverConfig(name=driver['driver_config'],
                                              options=driver['driver_options'])
        return Mount(**mount)
Beispiel #4
0
 def mount_driver_config_for_volume(self, volume_name):
     return DriverConfig(
         name=self.volume_driver,
         options=self.volume_driver_options_for_volume_name(volume_name)
         or None)
Beispiel #5
0
    def create_mount(self, data):
        self.log.debug("create_mount: {}".format(data))

        # validate required driver data is present
        err, err_msg = False, []
        if ("sshcmd" not in self.config["driver_options"]
                or self.config["driver_options"]["sshcmd"] == ""):
            err_msg.append("create_mount requires that the 'sshcmd'"
                           "driver_options key is set to a nonempty value")
            err = True

        if ("id_rsa" not in self.config["driver_options"]
                and "password" not in self.config["driver_options"]
                or "id_rsa" in self.config["driver_options"]
                and "password" in self.config["driver_options"]):
            err_msg.append("create_mount requires either a 'id_rsa'"
                           " or 'password' driver_options key")
            err = True

        if ("id_rsa" in self.config["driver_options"]
                and self.config["driver_options"]["id_rsa"] == ""
                or "password" in self.config["driver_options"]
                and self.config["driver_options"]["password"] == ""):
            err_msg.append(
                "create_mount requires a nonempty value from either "
                "'id_rsa' or 'password'")
            err = True

        if err:
            self.log.error("create_mount failed: {}".format(",".join(err_msg)))
            raise Exception("An error occurred during mount creation")

        driver = {
            "driver_config": self.config["driver_config"],
            "driver_options": {}
        }
        driver["driver_options"].update(self.config["driver_options"])
        del self.config["driver_options"]

        # Setup driver
        # look for values that should be dynamically assigned from data
        for option_key, option_value in driver["driver_options"].items():
            if option_value == "{" + option_key + "}":
                dyn_value = yield self.get_from(option_key, data)
                if dyn_value:
                    driver["driver_options"][option_key] = dyn_value

        # Legacy options that will be deprecated
        if driver["driver_options"]["sshcmd"] == "{sshcmd}":
            # Validate that the proper values are present
            username = yield self.get_from("USERNAME", data)
            path = yield self.get_from("PATH", data)
            if username and path:
                driver["driver_options"]["sshcmd"] = username + path

        if ("id_rsa" in driver["driver_options"]
                and driver["driver_options"]["id_rsa"] == "{id_rsa}"):
            key = yield self.get_from("PRIVATEKEY", data)
            if key:
                driver["driver_options"]["id_rsa"] = key

        if ("port" in driver["driver_options"]
                and driver["driver_options"]["port"] == "{port}"):
            port = yield self.get_from("PORT", data)
            if port:
                driver["driver_options"]["port"] = port
            else:
                driver["driver_options"]["port"] = "22"

        mount = {}
        mount.update(self.config)
        mount["driver_config"] = DriverConfig(name=driver["driver_config"],
                                              options=driver["driver_options"])
        return Mount(**mount)