diff --git a/.gitignore b/.gitignore index d2c5cfe..5789b35 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ values-customize.yaml cluster/load_balancer cluster/ebs_driver cluster/*.json + +run.env diff --git a/images/groups_notebook/Dockerfile b/images/groups_notebook/Dockerfile new file mode 100644 index 0000000..1319a62 --- /dev/null +++ b/images/groups_notebook/Dockerfile @@ -0,0 +1,137 @@ +FROM astronomycommons/public-hub-singleuser:0.6.3 + +USER root + +# Update cache +RUN apt-get -y update \ + && apt-get -y install apt-file \ + && apt-file update + +# Install LSST dependencies https://pipelines.lsst.io/v/DM-14099/install/prereqs/debian.html +RUN apt-get install --no-install-recommends -y \ + bison \ + ca-certificates \ + cmake \ + curl \ + # Java 11 already installed + # default-jre \ + flex \ + gettext \ + git \ + libbz2-dev \ + libcurl4-openssl-dev \ + libfontconfig1 \ + libglib2.0-dev \ + libncurses5-dev \ + libreadline6-dev \ + libx11-dev \ + libxrender1 \ + libxt-dev \ + m4 \ + make \ + perl-modules \ + zlib1g-dev + +# Install extra packages +RUN apt-get install --no-install-recommends -y \ + joe \ + telnet \ + bind9utils \ + traceroute \ + vim \ + emacs \ + nano \ + cron \ + git \ + sextractor \ + psfex \ + swarp \ + scamp \ + mariadb-client mariadb-server mycli \ + postgresql + +# Install Desktop +RUN apt-get -y install \ + dbus-x11 \ + xfce4 \ + xfce4-panel \ + xfce4-session \ + xfce4-settings \ + xorg \ + xubuntu-icon-theme \ + firefox \ + # Disable creation of Music, Documents, etc.. directories + # ref: https://unix.stackexchange.com/questions/268720/who-is-creating-documents-video-pictures-etc-in-home-directory + && apt-get remove -y xdg-user-dirs \ + # Disable the Applications|Log Out menu item in XFCE + # ref: https://github.com/yuvipanda/jupyter-desktop-server/issues/16 + && rm -f /usr/share/applications/xfce4-session-logout.desktop + +# Install ds9 +RUN cd /usr/local/bin \ + && curl -L http://ds9.si.edu/download/ubuntu18/ds9.ubuntu18.8.4.1.tar.gz | tar xzvf - + +# +# Install github API packages to system python (which will be safe to use by +# root) +# +RUN apt-get -y install python3-pip +RUN /usr/bin/pip3 install PyGithub + +# remove apt cache files +RUN apt-get -y clean \ + && rm -rf /var/lib/apt/lists/* + +# +# Work to be done by 'admin' (package installs, path adjustments) +# + +USER admin + +# Grab conda envs from the NFS server +RUN rmdir /opt/conda/envs && \ + ln -s /home/opt/conda/envs /opt/conda/envs + +# Link to the LSST stack +RUN ln -s /home/opt/lsst /opt/lsst + +# Pick up the LSST environment's kernel as the default +RUN rm -r /opt/conda/share/jupyter/kernels && \ + ln -s /opt/conda/envs/lsst/share/jupyter/kernels /opt/conda/share/jupyter/kernels + +# Add nb_conda_kernels +RUN mamba install nb_conda_kernels + +# Clean in-container conda install ... +# ... and work around a conda bug when pkgs dir is empty (see +# https://github.com/conda/conda/issues/7267#issuecomment-458661530) +RUN conda clean --all -f -y \ + && mkdir /opt/conda/pkgs && touch /opt/conda/pkgs/urls.txt + + +# +# More root work +# + +USER root +# Hide system kernels from nb_conda_kernels +# Place user-defined conda environments into the user's directory +RUN printf '\ +\n\ +c.CondaKernelSpecManager.env_filter = r"^/opt/.*$" \n\ +c.CondaKernelSpecManager.name_format = "'"{0}"' ({1})" \n'\ +>> /etc/jupyter/jupyter_notebook_config.py + +# Get rid of the jovyan user's (huge) home directory +RUN rm -rf /home/jovyan +# STEVEN add +RUN userdel jovyan + +# +# Startup scripts +# +COPY scripts/get-org-memberships.py /usr/local/bin/ +COPY scripts/startup.sh /usr/local/bin/ +COPY scripts/run-startup.sh /usr/local/bin/start-notebook.d/ + +USER root diff --git a/images/groups_notebook/README.md b/images/groups_notebook/README.md new file mode 100644 index 0000000..479c26a --- /dev/null +++ b/images/groups_notebook/README.md @@ -0,0 +1,28 @@ +1. Build the image +``` +$ docker build --platform linux/amd64 . -t astronomycommons/lincc-notebook:testing +``` + +2. Get a GitHub token from: https://github.com/settings/tokens +- Click "Generate new token" --> "Generate new token (classic)" +- Add "read:user" and "read:org" permissions +- Click "Generate token" +- Copy the token to "GH_TOKEN=..." in step (4) + +3. Create `run.env`: +``` +$ cp run.env.template run.env +``` + +4. Update `run.env` +``` +# file: run.env +GH_TOKEN= +NB_UID=12345 # or any number 1001+ +NB_USER=stevenstetzler # or any string +``` + +5. Run the notebook server: +``` +./run.sh +``` diff --git a/images/groups_notebook/run.env.template b/images/groups_notebook/run.env.template new file mode 100644 index 0000000..4d8854c --- /dev/null +++ b/images/groups_notebook/run.env.template @@ -0,0 +1,17 @@ +# +# Generate a personal access token on github +# +GH_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +# +# find your user id at https://caius.github.io/github_id/ +# +NB_UID=xxxxxx +NB_USER=xxxxxxxx +# +# These usually don't have to be changed +# +MEM_LIMIT=7030702080 +MEM_GUARANTEE=7030702080 +CPU_LIMIT=2.0 +CPU_GUARANTEE=1.5 +CHOWN_HOME=yes diff --git a/images/groups_notebook/run.sh b/images/groups_notebook/run.sh new file mode 100755 index 0000000..68b28e6 --- /dev/null +++ b/images/groups_notebook/run.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# +# A simple driver to test the container. You'll need to create +# run.env by filling out the missing info in run.env.template +# + +# +# About docker volumes: https://docs.docker.com/storage/volumes/ +# + +# docker volume rm lincc-homes + +RUN_ARGS=( + -it + --rm + -p 8888:8888 + --user=root + --name jupyter-testuser + --env-file run.env + --mount source=lincc-homes,target=/home + -v $PWD/scripts/startup.sh:/usr/local/bin/startup.sh + -v $PWD/scripts/get-org-memberships.py:/usr/local/bin/get-org-memberships.py +) +IMAGE=astronomycommons/lincc-notebook:testing +if [ $# == 0 ]; then + CMD_ARGS=( + jupyterhub-singleuser + --ip=0.0.0.0 + --port=8888 + --allow-root + ) +else + CMD_ARGS=("$@") +fi + +docker run ${RUN_ARGS[@]} ${IMAGE} ${CMD} ${CMD_ARGS[@]} diff --git a/images/groups_notebook/scripts/entrypoint.sh b/images/groups_notebook/scripts/entrypoint.sh new file mode 100755 index 0000000..1c862f2 --- /dev/null +++ b/images/groups_notebook/scripts/entrypoint.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +echo "Running command ${1}" + +# Check the command that is trying to be run in the container +# and pass that command to the start-spark script in case we are +# trying to launch a spark worker +case "$1" in + # We are in spark land + driver | driver-py | driver-r | executor) + source /usr/local/bin/pre-start-source.sh + export CONTAINER_TYPE="spark" + CMD=("/usr/local/bin/start.sh" + "/usr/local/bin/start-spark.sh" + "$@" + ) + ;; + start-ssh.sh) + export CONTAINER_TYPE="ssh" + CMD=("/usr/local/bin/start.sh" + "/usr/local/bin/start-ssh.sh" + "$@") + ;; + /bin/bash | bash | /bin/sh | sh) + export CONTAINER_TYPE="shell" + CMD=("/usr/local/bin/start.sh" + "$@") + ;; + # We are spawning a single user notebook session, a dask worker, or something else + # Just pass the command through + *) + source /usr/local/bin/pre-start-source.sh + export CONTAINER_TYPE="notebook" + CMD=("/usr/local/bin/start-notebook.sh" + "${@:2}") + ;; +esac + +# Run the command +echo "${CMD[@]}" +exec "${CMD[@]}" diff --git a/images/groups_notebook/scripts/get-org-memberships.py b/images/groups_notebook/scripts/get-org-memberships.py new file mode 100644 index 0000000..359730e --- /dev/null +++ b/images/groups_notebook/scripts/get-org-memberships.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python +# +# Grab organization membership information from GitHub, and +# print it out as " ", one per line. +# +# Must have GitHub auth token in $GH_TOKEN envvar +# + +import os, argparse +from github import Github + +parser = argparse.ArgumentParser(description='Fetch organization membership info. Authentication token must be in the GH_TOKEN environmental variable.') +parser.add_argument('what', choices=['member', 'owner'], help='which info to return') +args = parser.parse_args() + +# this script expects the access token to be given in an environment variable +token = os.environ["GH_TOKEN"] +g = Github(token) + +# find our identity, and our organizations +me = g.get_user() +orgs = me.get_orgs() + +if args.what == 'member': + for org in orgs: + print(org.login, org.id) +elif args.what == 'owner': + for org in orgs: + if any(member.login == me.login for member in org.get_members(role='admin')): + print(org.login, org.id) diff --git a/images/groups_notebook/scripts/pre-start-source.sh b/images/groups_notebook/scripts/pre-start-source.sh new file mode 100755 index 0000000..b78b5f9 --- /dev/null +++ b/images/groups_notebook/scripts/pre-start-source.sh @@ -0,0 +1,4 @@ +# Automatically create JAVA_HOME +export JAVA_HOME="$(dirname $(dirname $(readlink -f $(which java))))" +# Add py4j libraries to pythonpath +export PYTHONPATH=$SPARK_HOME/python:$(ls $SPARK_HOME/python/lib/py4j*.zip) diff --git a/images/groups_notebook/scripts/run-startup.sh b/images/groups_notebook/scripts/run-startup.sh new file mode 100644 index 0000000..1dde1ed --- /dev/null +++ b/images/groups_notebook/scripts/run-startup.sh @@ -0,0 +1,2 @@ +# this gets overridden by exactly the same script in the helm chart +. /usr/local/bin/startup.sh diff --git a/images/groups_notebook/scripts/start-spark.sh b/images/groups_notebook/scripts/start-spark.sh new file mode 100755 index 0000000..1e5f2f1 --- /dev/null +++ b/images/groups_notebook/scripts/start-spark.sh @@ -0,0 +1,147 @@ +#!/bin/bash +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# echo commands to the terminal output +set -ex + +# Check whether there is a passwd entry for the container UID +myuid=$(id -u) +mygid=$(id -g) +# turn off -e for getent because it will return error code in anonymous uid case +set +e +uidentry=$(getent passwd $myuid) +set -e + +if test -f "$SPARK_START_SCRIPT"; then + source "$SPARK_START_SCRIPT" +else + echo "WARNING: File $SPARK_START_SCRIPT not found! Is it mounted correctly?" +fi + +# If there is no passwd entry for the container UID, attempt to create one +if [ -z "$uidentry" ] ; then + if [ -w /etc/passwd ] ; then + echo "$myuid:x:$myuid:$mygid:anonymous uid:$SPARK_HOME:/bin/false" >> /etc/passwd + else + echo "Container ENTRYPOINT failed to add passwd entry for anonymous UID" + fi +fi + +SPARK_K8S_CMD="$1" +case "$SPARK_K8S_CMD" in + driver | driver-py | driver-r | executor) + shift 1 + ;; + "") + ;; + *) + echo "Non-spark-on-k8s command provided, proceeding in pass-through mode..." + exec tini -g -- "$@" + ;; +esac + +SPARK_CLASSPATH="$SPARK_CLASSPATH:${SPARK_HOME}/jars/*" +env | grep SPARK_JAVA_OPT_ | sort -t_ -k4 -n | sed 's/[^=]*=\(.*\)/\1/g' > /tmp/java_opts.txt +readarray -t SPARK_EXECUTOR_JAVA_OPTS < /tmp/java_opts.txt + +if [ -n "$SPARK_EXTRA_CLASSPATH" ]; then + SPARK_CLASSPATH="$SPARK_CLASSPATH:$SPARK_EXTRA_CLASSPATH" +fi + +if [ -n "$PYSPARK_FILES" ]; then + PYTHONPATH="$PYTHONPATH:$PYSPARK_FILES" +fi + +PYSPARK_ARGS="" +if [ -n "$PYSPARK_APP_ARGS" ]; then + PYSPARK_ARGS="$PYSPARK_APP_ARGS" +fi + +R_ARGS="" +if [ -n "$R_APP_ARGS" ]; then + R_ARGS="$R_APP_ARGS" +fi + +if [ "$PYSPARK_MAJOR_PYTHON_VERSION" == "2" ]; then + pyv="$(python -V 2>&1)" + export PYTHON_VERSION="${pyv:7}" + export PYSPARK_PYTHON="python" + export PYSPARK_DRIVER_PYTHON="python" +elif [ "$PYSPARK_MAJOR_PYTHON_VERSION" == "3" ]; then + pyv3="$(python3 -V 2>&1)" + export PYTHON_VERSION="${pyv3:7}" + export PYSPARK_PYTHON="python3" + export PYSPARK_DRIVER_PYTHON="python3" +fi + +case "$SPARK_K8S_CMD" in + driver) + CMD=( + "$SPARK_HOME/bin/spark-submit" + --conf "spark.driver.bindAddress=$SPARK_DRIVER_BIND_ADDRESS" + --deploy-mode client + "$@" + ) + ;; + driver-py) + CMD=( + "$SPARK_HOME/bin/spark-submit" + --conf "spark.driver.bindAddress=$SPARK_DRIVER_BIND_ADDRESS" + --deploy-mode client + "$@" $PYSPARK_PRIMARY $PYSPARK_ARGS + ) + ;; + driver-r) + CMD=( + "$SPARK_HOME/bin/spark-submit" + --conf "spark.driver.bindAddress=$SPARK_DRIVER_BIND_ADDRESS" + --deploy-mode client + "$@" $R_PRIMARY $R_ARGS + ) + ;; + executor) + CMD=( + ${JAVA_HOME}/bin/java + "${SPARK_EXECUTOR_JAVA_OPTS[@]}" + -Xms$SPARK_EXECUTOR_MEMORY + -Xmx$SPARK_EXECUTOR_MEMORY + -cp "$SPARK_CLASSPATH" + org.apache.spark.executor.CoarseGrainedExecutorBackend + --driver-url $SPARK_DRIVER_URL + --executor-id $SPARK_EXECUTOR_ID + --cores $SPARK_EXECUTOR_CORES + --app-id $SPARK_APPLICATION_ID + --hostname $SPARK_EXECUTOR_POD_IP + ) + ;; + + *) + echo "Unknown command: $SPARK_K8S_CMD" 1>&2 + exit 1 +esac + +if test "$SPARK_LOCAL_DIRS"; then + local_dirs=($(echo $SPARK_LOCAL_DIRS | tr "," "\n")) + cd ${local_dirs[0]} +else + if test "$SPARK_WORKER_DIR"; then + cd $SPARK_WORKER_DIR + fi +fi + +exec "${CMD[@]}" diff --git a/images/groups_notebook/scripts/start-ssh.sh b/images/groups_notebook/scripts/start-ssh.sh new file mode 100755 index 0000000..97e9717 --- /dev/null +++ b/images/groups_notebook/scripts/start-ssh.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +# Function to setup ssh and link to NFS +function ssh_setup { + server_config_dir="/home/admin/ssh/server" + # load ssh config from NFS + if [ ! -d $server_config_dir ] + then + # Back up host keys and config to NFS + mkdir -p $server_config_dir + cp -r /etc/ssh/* $server_config_dir/. + fi + # remove host keys and config from container + rm -rf /etc/ssh + # link to host keys and config on NFS + ln -s $server_config_dir /etc/ssh + + # add config specified via Helm chart + if [ -d /etc/_ssh ]; then + cat /etc/_ssh/ssh_config.d/chart.conf + cp /etc/_ssh/ssh_config.d/chart.conf /etc/ssh/ssh_config.d/. + cat /etc/_ssh/sshd_config.d/chart.conf + cp /etc/_ssh/sshd_config.d/chart.conf /etc/ssh/sshd_config.d/. + fi + # (re)generate missing host keys + ssh-keygen -A + # Add /run/sshd + mkdir -p /run/sshd +} + +echo "Starting SSH" +echo "ID: $(id -u)" +if [ "$(id -u)" -ne "0" ]; then + echo "Script hasn't been run as root, will try to run again as root" + if [[ "${GRANT_SUDO}" != "1" && "${GRANT_SUDO}" != 'yes' ]]; then + echo "Must run as root with GRANT_SUDO=1" + exit -1 + else + CMD="sudo -u root /usr/local/bin/start-ssh.sh" + echo "Executing: ${CMD}" + exec ${CMD} + fi +fi + +echo "Setting up SSH:" +ssh_setup +echo "Starting SSH service" +# $(which sshd) -ddd -D -p 22 +echo "Using configuration:" +sshd -T +service ssh start +sleep infinity diff --git a/images/groups_notebook/scripts/startup.sh b/images/groups_notebook/scripts/startup.sh new file mode 100644 index 0000000..2daca6c --- /dev/null +++ b/images/groups_notebook/scripts/startup.sh @@ -0,0 +1,296 @@ +# +# Startup script run as 'root' by start.sh +# +# This is run very early in the execution of start.sh, which allows us to hijack +# much of the (buggy) functionality that goes on there. +# +#set -x + +# +# Prepare the key directories +# + +SVC_HOMES="/home/owners" +GROUP_HOMES="/lincc/groups" +DATA_DIR="/lincc/data" +SHARED_DIR="/lincc/shared" +SECRET="/lincc/.system" + +# FIXME: this should be a direct mount +mkdir -p /home/_lincc +ln -s /home/_lincc /lincc + +# HACK: $SECRET is used to exchange user/group info with the NFS server +mkdir -p "$SECRET"; chmod 700 "$SECRET" +ls -l "$SECRET" + +# Make sure these exist, and that the ownership is correct. +mkdir -p "$SVC_HOMES" # owned by root.root +mkdir -p "$GROUP_HOMES" # owned by root.root + +[[ ! -e "$DATA_DIR" ]] && { mkdir -p "$DATA_DIR"; chown admin:admin "$DATA_DIR"; } # globally shared data; owned by admin.admin +[[ ! -e "$SHARED_DIR" ]] && { mkdir -p "$SHARED_DIR"; chown root.root "$SHARED_DIR"; chmod u+rwx,g+rwx,o+rwxt "$SHARED_DIR"; } # globally shared dir, set up with the sticky bit so anyone can write + +## +## Create our user +## +H="/home/$NB_USER" + +groupadd -g "$NB_UID" "$NB_USER" + +test -e "$H" && CREATE_HOME="-M" || CREATE_HOME="-m" # Don't create the home directory if it already exists +useradd $CREATE_HOME -s /bin/bash -u "$NB_UID" -g "$NB_UID" -G users "$NB_USER" +[[ "$CREATE_HOME" == "-m" ]] && chmod 770 "$H" # If we just created the home dir, set private default permissions + +cd "$H" + +# prevent start.sh from messing with groups +export NB_GID="$NB_UID" + +## +## Create groups for all organizations we're a member of +## + +info() +{ + # Use this to print informational messages. Will prepend the script + # and function name to any passed arguments. + + echo "${BASH_SOURCE[1]}::${FUNCNAME[1]}: ""$@" 1>&2 +} + +_safe_python() +{ + # Invoke the version of Python safe to be run by root (i.e., one + # which is not under other users' control (e.g. admins own + # /opt/conda, so malicious admin can elevate themselves to root if + # we accidentally run anything from there). + /usr/bin/python3 "$@" +} + +_normalize_username() +{ + # Default JupyterHub GitHub OAuthenticator turns all usernames to lowercase + # If you change this for your hub, you mush change this script as well. + + # Note: this only works with Bash 4+ + echo "${1,,}" +} + +create_org_groups() +{ + info entered + ( + # temporary file where we'll accumulate group information + mkdir -p "$SECRET"/{tmp,groups} + TMPFN="$SECRET/tmp/${NB_USER}.${NB_UID}" + rm -f "$TMPFN" + + groups=() + while IFS=" " read -r org_name org_id remainder; do + org_name=$(_normalize_username "${org_name}") + H="${SVC_HOMES}/${org_name}" + G="${GROUP_HOMES}/${org_name}" + E="${G}/envs" + + # create the corresponding group + groupadd -g "${org_id}" "${org_name}" + + # create and configure the group service account. + # This account owns all files that group members + # shouldn't accidentally delete. + # + # Rant: Ordinarily, this account's primary group would be just the ${org_name} group. + # We'd set the umask to 002, thus making any files/dirs created by the account unwritable + # by regular members of the group (i.e., files/dirs created by the service users would + # typically have rwxr-xr-x permissions). This would satisfy the goal of preventing accidental + # deletion of files by group members. + # + # Alas... Enter the bonkers decision at https://github.com/conda/conda-build/issues/1904 + # where conda folks decided to explicitly set g+w permissions on everything one installs, thus + # breaking security in systems configured with USERGROUPS=no (i.e., where users share the + # primary group). This means that the permissions are rwxrwxr-x, no matter the umask, meaning + # that any group member can delete these directories. That this type of decision could have been + # made me lost faith in Anaconda Inc's security proweness... $DEITY knows what else that group + # may be doing on the backend of anaconda.org (how safe are those files?). /Rant off + # + # To work around the insanity above, we create a special group for the service user. We give + # it a gid of 1Bn + ${org_id}. The number of IDs on GH seems to be growing at ~2M/month (1.66, + # to be exact; checked on Oct/19/2021). The largest current ID is around 91.8M. At that + # rate, for the IDs to reach 1Bn it will take ~37 years. Though I still feel the "solution" + # is yucky and should be fixed (w. a database of mappings), we should be fine for awhile. + # + svc_group_id=$((org_id + 1000000000)) + svc_group_name="${org_name}_svc" + groupadd -g "${svc_group_id}" "${svc_group_name}" + if [[ -e "$H" ]]; then + # Home exists; just create the user. + useradd -M -b "$SVC_HOMES" -s /bin/bash -u "${org_id}" -g "${svc_group_id}" -G users,"${org_name}" "${org_name}" + else + # Home doesn't exist; create the user, configure permissions + useradd -m -b "$SVC_HOMES" -s /bin/bash -u "${org_id}" -g "${svc_group_id}" -G users,"${org_name}" "${org_name}" + + chmod o-rwx "$H" + + # change the default environment location for the service account + cat > "$H/.condarc" <<-EOT + envs_dirs: + - $E + EOT + fi + echo "${org_name} ${org_id} ${svc_group_name} ${svc_group_id}" >> "$TMPFN" + + # now create the org directory if it doesn't exist and make it owned + # by the service account. this is for common data and software shared + # by the group. + # + # The group directory will have the sticky bit set. This will allow anyone + # in the group to create new files/folders, but only the owner of the file + # and the group service account to delete or rename them. This adds a layer + # of safety. It also explains why we can have the envs/ directory here, w/o + # worrying a group member may delete it. + if [[ ! -e "$G" ]]; then + mkdir -p "$G" + chown "${org_name}":"${org_name}" "$G" + chmod u=rwx,g=rwxs,o=,o+t "$G" + fi + + # create the environments directory, and make sure it's owned by the + # service group + if [[ ! -e "$E" ]]; then + mkdir -p "$E" + chown "${org_name}":"${svc_group_name}" "$E" + chmod 755 "$E" + fi + + info "added and configured group ${org_name} with service account ${org_name}:${svc_group_name}" + groups+=("${org_name}") + done < <(_safe_python -u /usr/local/bin/get-org-memberships.py member) # note: this construct ensures the while loop doesn't launch in a subshell (thus retaining $groups) + + # add the user to all groups + groups_comma=$(IFS=, ; echo "${groups[*]}") + usermod -a -G "${groups_comma}" "$NB_USER" + info "added ${NB_USER} to ${groups_comma}" + + # update the NFS-server group database (atomic move) + mv "$TMPFN" "$SECRET/groups/${NB_USER}.${NB_UID}" + du -a "$SECRET" 1>&2 + + # return the space-delimited list of groups + echo "${groups[@]}" + ) +} + +create_service_accounts() +{ + ( + _safe_python -u /usr/local/bin/get-org-memberships.py owner | + while IFS=" " read -r org_name org_id remainder; do + org_name=$(_normalize_username "${org_name}") + # allow our user to access it with sudo + S="/etc/sudoers.d/svc-${org_name}" + echo "$NB_USER ALL=(${org_name}) NOPASSWD: ALL" > "$S" + + info "service account access for ${org_name} added to sudoers in $S"; + done + ) +} + +# FIXME: we could use `conda config` commands +_echo_condarc() +{ + echo "$@" >> /opt/conda/.condarc +} + +set_conda_env_paths() +{ + [[ $# -eq 0 ]] && return + + # make sure the user's home dir is first, so it's picked up as + # the default place to install user environments + _echo_condarc "envs_dirs:" + _echo_condarc ' - $HOME/.conda/envs' + + # append all envs_dirs + local grp + for grp in "$@"; do + _echo_condarc " - /lincc/groups/$grp/envs" + done +} + +# Mirror the user's GH organizations into groups +groups=$(create_org_groups) + +# Allow sudo to service accunts for orgs where this user is an owner +# Do this in the background (as it will take awhile due to numerous GH API calls) +create_service_accounts & + +# set the user's default environment directories +set_conda_env_paths $groups + +## +## Delete jovyan user, to stop start.sh from trying to rename it to $NB_USER +## +# STEVEN comment +# userdel -f jovyan +# rm -rf /home/jovyan + +## +## Ensure their directory is correctly set up with symlinks to shared resources +## +{ test -L "$H/data" && echo "link to /home/data exists; skipping creation"; } || { sudo -i -u "$NB_USER" ln -s /home/data $H/data; } +{ test -L "$H/shared" && echo "link to /home/shared exists; skipping creation"; } || { sudo -i -u "$NB_USER" ln -s /home/shared $H/shared; } + + +# FIXME: we should fix this in kubespawner -- just mount the directories of the groups +# the user is a member of... +MY="/my" +mkdir "$MY" +ln -s "$DATA_DIR" "$MY/data" +ln -s "$SHARED_DIR" "$MY/shared" +mkdir "$MY/groups" +for grp in $groups; do + ln -s "$GROUP_HOMES/$grp" "$MY/groups/$grp" +done + +{ test -L "$H/lincc" && echo "$H/lincc exists; skipping creation"; } || { sudo -i -u "$NB_USER" ln -s /my $H/lincc; } + +## +## Spawn a daemon to create other users, based on home directory entries +## +adduserloop() +{ + # + # A daemon that loops in the background and creates UNIX users for any new + # entries that show up in /home + # + while true + do + HOMEDIR=/home + + for USR in $(ls $HOMEDIR); do + USRID=$(stat -c '%u' "$HOMEDIR/$USR") + GROUPID=$(stat -c '%g' "$HOMEDIR/$USR") + info checking $USR + + if grep -q $GROUPID /etc/group; then + echo "Group with $GROUPID exists. Skipping..." + else + echo "Group with $GROUPID does not exist. Adding..." + groupadd -g $GROUPID $USR + fi + + if [[ $(id -u $USRID) ]]; then + echo "User with $USRID exists. Skipping..." + else + echo "User with $USRID does not exist. Adding..." + useradd -M -s /bin/bash -u $USRID -g $GROUPID -G users $USR -s /bin/bash && cat /etc/passwd || echo "Failed to add user $USR" + fi + done + echo "sleeping 60 seconds..." + sleep 60 + done +} + +adduserloop &> /dev/null & +#adduserloop &