diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dbfbd18 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +.vscode + diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..4c5b5e8 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,22 @@ +Copyright (c) 2016-2018 Ohio Supercomputer Center + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 2910fab..d9b7cef 100644 --- a/README.md +++ b/README.md @@ -5,16 +5,41 @@ A Batch Connect app that launches RStudio Server via a container inside of a Slu ## Creating containers Versioned RStudio containers from the Rocker project seem to work out-of-the-box: -https://rocker-project.org/images/versioned/rstudio.html +[https://rocker-project.org/images/versioned/rstudio.html](https://rocker-project.org/images/versioned/rstudio.html) -`apptainer pull rocker-rstudio-R_4.6.0-RStudio_TBD-20260512-00.sif docker://rocker/rstudio:4.6.0` +Pull down the container: +```bash +apptainer pull rocker-rstudio-R_4.6.0-RStudio_TBD-20260512-00.sif docker://rocker/rstudio:4.6.0 +``` -`apptainer shell` the container and find out what version of RStudio it includes. Then update the name to include that, i.e.`rocker-rstudio-R_4.6.0-RStudio_2026.04.0-526-20260512-00.sif`, i.e., `---R_-RStudio_-YYYYMMDD-##.sif` where YYYYMMDD and ## is some optional increment on the image. +Use `apptainer shell` to investigate the container and find out what version of RStudio it includes. Then update the name to include that, i.e., `--R_-RStudio_--YYYYMMDD-##.sif`, where YYYYMMDD is the date the image was downloaded and ## is some optional increment on the image, e.g., `rocker-rstudio-R_4.6.0-RStudio_2026.04.0-526-20260512-00.sif`. + +If the image is modified, extended, or built by NCSA, then use 'ncsa' as the project/org. + +The images can be given "pretty names" in the app interface, but naming the .sif files in a consistent and comprehensive manner is helpful to maintainers. ## Contributing -1. If necessary (no dev privs in ncsa/mg-OOD-RStudio), fork it -2. Create your feature branch (`git checkout -b my-new-feature`) -3. Commit your changes (`git commit -am 'Add some feature'`) -4. Push to the branch (`git push origin my-new-feature`) +1. Work on the MG cluster, developing and testing your changes using the developer sandbox mode +2. If necessary (no dev privs in ncsa/mg-OOD-RStudio), fork the repository +3. Create your feature/topic branch (`git checkout -b username/ticket_id/short_desc`) +4. Add and commit your changes (`git commit -am 'Add some feature'`) +4. Push to the feature/topic branch (`git push origin username/ticket_id/short_desc`) 5. Create a new Pull Request + +## Publishing containers on the MG cluster + +NCSA staff can share containers in `/usr/local/oodimages/rstudio/` for others to use. Make sure permissions are set to allow everyone to access the .sif file. + +In order for the app to show the container as an option it must be added in `form.yml.erb`. Be sure to verify that you can select the container and successfully launch the app. + +Then put in a PR as described above. After the PR is approved and the branch is merged to `main`, make sure to add an appropriate version tag to the repo. + +Finally, ask an admin to update the Open OnDemand server config to pull in this new release by the tag. + +## Acknowledgments and References + +[https://github.com/sol-eng/singularity-rstudio](https://github.com/sol-eng/singularity-rstudio)\ +[https://discourse.openondemand.org/t/rstudio-when-launched-without-singularity-is-having-strange-troubles-with-authentication/1213/60](https://discourse.openondemand.org/t/rstudio-when-launched-without-singularity-is-having-strange-troubles-with-authentication/1213/60)\ +[https://github.com/OSC/bc_osc_rstudio_server](https://github.com/OSC/bc_osc_rstudio_server) + diff --git a/form.yml.erb b/form.yml.erb new file mode 100644 index 0000000..68f9617 --- /dev/null +++ b/form.yml.erb @@ -0,0 +1,93 @@ +# Batch Connect app configuration file +--- + +cluster: "magnus" + +attributes: + + bc_image: + label: "Select from provided container images or custom" + widget: "select" + value: "R 4.6.0" + options: + - ["R 4.6.0","/usr/local/oodimages/rstudio/rocker-rstudio-R_4.6.0-RStudio_2026.04.0-526-20260512-00.sif",data-hide-bc-custom-image: true,data-set-bc-custom-image: ""] + - ["R 4.5.3","/usr/local/oodimages/rstudio/rocker-rstudio-R_4.5.3-RStudio_2026.04.0-526-20260608-00.sif",data-hide-bc-custom-image: true,data-set-bc-custom-image: ""] + - ["custom","custom"] + # cachable: false + + bc_custom_image: + label: "Select custom container image" + widget: "path_selector" + help: "Enter the path for your own Apptainer .sif image that contains and R and RStudio. Images from the Rocker project (https://rocker-project.org/images/), or extended from those containers, are likely to work." + + bc_reservation: + label: "Name of reservation (leave empty if none)" + widget: "text_field" + value: "" + +# bc_partition: +# label: "Partition" +# widget: "select" +# value: "magnus" +# options: +# - ["debug","debug"] +# - ["gpu","gpu"] +# - ["magnus","magnus"] + + bc_partition: + label: "Partition" + widget: select + value: "magnus" + options: +## <%- CustomPartitions.partitions.each do |a| %> +## - [ "<%= a.strip %>", "<%= a %>" ] +## <%- end -%> + <%- CustomPartitions.partitions.each do |a| %> + - [ "<%= a.strip %>", "<%= a %>" ] + <%- end -%> +# cacheable: false +### help: "The rstudio partition is available to launch small sessions without delay. Sessions there are restricted to 1 core and a maximum of 8 GB of RAM." + + bc_num_slots: + label: "Number of CPUs" + widget: "number_field" + min: "1" +### help: "The rstudio partition only allows single-core sessions." + + bc_num_memory: + label: "Amount of RAM" + widget: "text_field" +### help: "Total memory for the node/session (--mem in Slurm). Use Slurm format, e.g., 4096M, 10G. If left blank, 4505 MB will be allocated per CPU core requested. A minimum of 4 GB is required for each job and lower values will be substituted before submission. The rstudio partition allows a maximum of 8 GB." + help: "Total memory for the node/session (--mem in Slurm). Use Slurm format, e.g., 4096M, 10G. If left blank, 4505 MB will be allocated per CPU core requested. A minimum of 4 GB is required for each job and lower values will be substituted before submission." + + bc_num_gpus: + label: "Number of GPUs" +# widget: "number_field" + widget: "text_field" +# max: "4" +# min: "0" + + bc_num_hours: + label: "Number of hours" + widget: "number_field" + max: "720" + min: "1" + +# working_dir: +# label: "Working Directory" +# data-filepicker: true +# data-target-file-type: dirs # Valid values are: files, dirs, or both +# readonly: false +# help: "Select your project directory; defaults to $HOME" + +form: + - bc_image + - bc_custom_image + - bc_partition + - bc_num_hours + - bc_reservation + - bc_num_slots + - bc_num_memory + - bc_num_gpus +# - working_dir + - bc_email_on_started diff --git a/icon.png b/icon.png new file mode 100644 index 0000000..656d133 Binary files /dev/null and b/icon.png differ diff --git a/manifest.yml b/manifest.yml new file mode 100644 index 0000000..6f54f05 --- /dev/null +++ b/manifest.yml @@ -0,0 +1,7 @@ +--- +name: RStudio Server +category: Interactive Apps +#subcategory: Servers +role: batch_connect +description: | + This app will launch RStudio Server inside of a container running in a Slurm job. diff --git a/submit.yml.erb b/submit.yml.erb new file mode 100644 index 0000000..f3c168b --- /dev/null +++ b/submit.yml.erb @@ -0,0 +1,35 @@ +# Job submission configuration file +--- + +batch_connect: + template: "basic" + conn_params: + - csrftoken + native: +script: + queue_name: "<%= bc_partition %>" + email_on_started: <%= bc_email_on_started %> + native: + - "-N 1" + - "-n <%= bc_num_slots.blank? ? 1 : bc_num_slots.to_i %>" + <%- unless bc_num_memory.blank? %> + # make sure the user has requested at least 4 GB of RAM + <%- if ( bc_num_memory.downcase.end_with?('g') and bc_num_memory[/[0-9]+/].to_i < 4 ) or + ( bc_num_memory.downcase.end_with?('m') and bc_num_memory[/[0-9]+/].to_i < 4096 ) or + ( bc_num_memory.downcase.end_with?('k') and bc_num_memory[/[0-9]+/].to_i < 4194304 ) %> + - "--mem=4G" + <%- else %> + - "--mem=<%= bc_num_memory %>" + <%- end -%> + <%- end -%> + <%- unless bc_num_hours.blank? -%> + - "-t" + - "0-<%= bc_num_hours %>:00:00" + <%- end -%> + <%- unless bc_num_gpus.blank? or bc_num_gpus == 0 -%> + - "--gres=gpu:<%= bc_num_gpus %>" + <%- end -%> + <%- unless bc_reservation.blank? -%> + - "--reservation=<%= bc_reservation %>" + <%- end -%> + diff --git a/template/after.sh.erb b/template/after.sh.erb new file mode 100755 index 0000000..8b7b6a7 --- /dev/null +++ b/template/after.sh.erb @@ -0,0 +1,9 @@ +# Wait for the RStudio Server to start +echo "Waiting for RStudio Server to open port ${port}..." +if wait_until_port_used "${host}:${port}" 120; then + echo "Discovered RStudio Server listening on port ${port}!" +else + echo "Timed out waiting for RStudio Server to open port ${port}!" + clean_up 1 +fi +sleep 2 diff --git a/template/before.sh.erb b/template/before.sh.erb new file mode 100755 index 0000000..bbcaeb2 --- /dev/null +++ b/template/before.sh.erb @@ -0,0 +1,16 @@ +# Export the module function if it exists +[[ $(type -t module) == "function" ]] && export -f module + +# Find available port to run server on +port=$(find_port ${host}) + +# Define a password and export it for RStudio authentication +password="$(create_passwd 16)" +export RSTUDIO_PASSWORD="${password}" + +# create CSRF token +<%- + require 'securerandom' + csrftoken=SecureRandom.uuid +-%> +export csrftoken="<%= csrftoken %>" diff --git a/template/bin/auth b/template/bin/auth new file mode 100755 index 0000000..c3de0ae --- /dev/null +++ b/template/bin/auth @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +# Confirm username is supplied +if [[ $# -lt 1 ]]; then + echo "Usage: auth USERNAME" + exit 1 +fi +USERNAME="${1}" + +# Confirm password environment variable exists +if [[ -z ${RSTUDIO_PASSWORD} ]]; then + echo "The environment variable RSTUDIO_PASSWORD is not set" + exit 1 +fi + +# Read in the password from user +read -s -p "Password: " PASSWORD +echo "" + +if [[ ${USERNAME} == ${USER} && ${PASSWORD} == ${RSTUDIO_PASSWORD} ]]; then + echo "Successful authentication" + exit 0 +else + echo "Invalid authentication" + exit 1 +fi + diff --git a/template/script.sh.erb b/template/script.sh.erb new file mode 100755 index 0000000..a9d42cd --- /dev/null +++ b/template/script.sh.erb @@ -0,0 +1,107 @@ +#!/usr/bin/env bash + +# Load the required environment +setup_env () { + # Additional environment which could be moved into a module + # Change these to suit +export RSTUDIO_SERVER_IMAGE="<% if context.bc_custom_image != '' %><%= context.bc_custom_image %><% else %><%= context.bc_image %><% end %>" +# export APPTAINER_BINDPATH="/etc,/media,/mnt,/opt,/srv,/usr/lib,/lib,/lib64,/usr/apps,/var" + export PATH="$PATH:/usr/lib/rstudio-server/bin" + export APPTAINERENV_PATH="$PATH" + # In Singularity 3.5.x it became necessary to explicitly pass LD_LIBRARY_PATH + # to the singularity process + export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/opt/R/4.4.0/lib/R/lib" + # export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/apps/general/spack/sw/linux-rhel8-zen/gcc-11.3.0/r-4.2.0-2liuw4vmic27cmqhyyt6jmvwbezn6mlx/rlib/R/lib:/usr/apps/general/spack/sw/linux-rhel8-cascadelake/gcc-11.3.0/curl-8.0.1-u7dljxx4j2vkwnhrbxl5dnigktopsogc/lib:/usr/apps/general/spack/sw/linux-rhel8-cascadelake/gcc-11.3.0/expat-2.5.0-37la23gnxetfy3uytrvk47cmtlycoska/lib:/usr/apps/general/spack/sw/linux-rhel8-cascadelake/gcc-11.3.0/geos-3.11.2-ipmlasljmvtuoykqyycrtu5gy4zw47cm/lib64:/usr/apps/general/spack/sw/linux-rhel8-cascadelake/gcc-11.3.0/json-c-0.16-7lokifwb6lzpml5k63adfgzrbrnc3hi2/lib64:/usr/apps/general/spack/sw/linux-rhel8-cascadelake/gcc-11.3.0/libgeotiff-1.6.0-ql2lodtsyry5cw25ngozc7qtb2jhf6zo/lib:/usr/apps/general/spack/sw/linux-rhel8-cascadelake/gcc-11.3.0/libjpeg-turbo-2.1.4-cmeyyk3pssdcvl4i4ijmfeoqiubfvby3/lib64:/usr/apps/general/spack/sw/linux-rhel8-cascadelake/gcc-11.3.0/libpng-1.6.39-rpbqrtfdirzf3fojuav2it3lpeqmmnye/lib64:/usr/apps/general/spack/sw/linux-rhel8-cascadelake/gcc-11.3.0/libtiff-4.4.0-q2c2seunvg5ym3qhgdwe3d3mkr72z5lb/lib64:/usr/apps/general/spack/sw/linux-rhel8-cascadelake/gcc-11.3.0/proj-8.2.1-t3pclijfh275ljxzranxvzs22hlene5j/lib64:/usr/apps/general/spack/sw/linux-rhel8-cascadelake/gcc-11.3.0/sqlite-3.40.1-ux7pnanmrgn4dvcm6qwrbvesyfposm56/lib:/usr/apps/general/spack/sw/linux-rhel8-cascadelake/gcc-11.3.0/zlib-1.2.13-smcuvhowz7cwxhb5jcy647kjmrnx4tbc/lib:/usr/apps/general/spack/sw/linux-rhel8-zen/gcc-8.5.0/gcc-11.3.0-cwx43q6qt46zl5olgckurx67xtg4nuyd/lib64:/usr/apps/general/spack/sw/linux-rhel8-zen/gcc-8.5.0/glib-2.74.6-4wqjtnm3mebxlldhaxpbh4ptqtaxjmoa/lib:/usr/apps/general/spack/sw/linux-rhel8-zen/gcc-11.3.0/hdf5-1.14.0-5xrz3r365nib6fma73irar6fbrgyzhd4/lib:/usr/apps/general/spack/sw/linux-rhel8-cascadelake/gcc-11.3.0/gdal-3.6.3-tkiuujut7ibrj3ljcof2r7zonydhmqt6/lib64" + export APPTAINERENV_LD_LIBRARY_PATH="$LD_LIBRARY_PATH" +} +setup_env +#echo $APPTAINER_BINDPATH +#echo $APPTAINERENV_PATH +#echo $APPTAINERENV_LD_LIBRARY_PATH + +#export WORKING_DIR="< % = session_dir % >" +export WORKING_DIR=${PWD} + +# +# Start RStudio Server +# + +# PAM auth helper used by RStudio +export RSTUDIO_AUTH="${WORKING_DIR}/bin/auth" + +# Generate an `rsession` wrapper script +export RSESSION_WRAPPER_FILE="$WORKING_DIR/rsession.sh" +( +umask 077 +sed 's/^ \{2\}//' > "$WORKING_DIR/rsession.sh" << EOL + #!/usr/bin/env bash + + # Log all output from this script + export RSESSION_LOG_FILE="$WORKING_DIR/rsession.log" + + exec &>>"\${RSESSION_LOG_FILE}" + set -x + + # rsession.sh doesn't share the same env as the outside script, so these + # need to be set explicitly + export R_LIBS_SITE="${R_LIBS_SITE}" + export RSTUDIO_DATA_HOME="/scratch/rstudio_user_data/${USER}/session_data" + export TZ="US/Eastern" + export HOME="$HOME" + export MODULEPATH_ROOT="$MODULEPATH_ROOT" + export MODULEPATH="$MODULEPATH" + export LMOD_PKG="$LMOD_PKG" + + env + + # Launch the original command + echo "Launching rsession..." + echo "...with args..." + echo "\${@}" + exec rsession --r-libs-user "${R_LIBS_USER}" "\${@}" +EOL +) +chmod 700 "$WORKING_DIR/rsession.sh" + +# Create DB config file +( +umask 077 +sed 's/^ \{2\}//' > "$WORKING_DIR/database.conf" << EOL + provider=sqlite + directory="$WORKING_DIR" + exec rsession --r-libs-user "${R_LIBS_USER}" "\${@}" +EOL +) +chmod 700 "$WORKING_DIR/database.conf" + +# Set working directory to home directory +cd "${HOME}" + +# server log directory in this job's working directory +mkdir -p "${WORKING_DIR}/logs" +( +umask 077 +sed 's/^ \{2\}//' > "$WORKING_DIR/logging.conf" << EOL + [*] + log-dir="${WORKING_DIR}/logs" +EOL +) +chmod 700 "$WORKING_DIR/logging.conf" + +export TMPDIR="$(mktemp -d)" +#TMPDIR="${WORKING_DIR}/tmp" +#mkdir -p $TMPDIR + +mkdir -p "$TMPDIR/rstudio-server" +python3 -c 'from uuid import uuid4; print(uuid4())' > "$TMPDIR/rstudio-server/secure-cookie-key" +chmod 0600 "$TMPDIR/rstudio-server/secure-cookie-key" + +module purge + +set -x +# Launch the RStudio Server +echo "Starting up rserver..." + +apptainer run --no-mount /usr/apps -B "$TMPDIR:/tmp,${WORKING_DIR}/logging.conf:/etc/rstudio/logging.conf" "$RSTUDIO_SERVER_IMAGE" rserver --www-port ${port} --auth-none 0 --auth-pam-helper-path "${RSTUDIO_AUTH}" --auth-encrypt-password 0 --rsession-path "${RSESSION_WRAPPER_FILE}" --server-data-dir="${TMPDIR}/run" --secure-cookie-key-file "${TMPDIR}/rstudio-server/secure-cookie-key" --server-user=$(whoami) --database-config-file="$WORKING_DIR/database.conf" + +echo 'Singularity has exited...' diff --git a/view.html.erb b/view.html.erb new file mode 100644 index 0000000..b491615 --- /dev/null +++ b/view.html.erb @@ -0,0 +1,20 @@ + +
+ "> + + + + + +