Commit f565c33d authored by Hunter Hansen's avatar Hunter Hansen Committed by Mayank Mittal

Converts container.sh into Python utilities (#539)

This PR replicates the capabilities of `container.sh` in Python, and
breaks its capabilities up into several files under a new directory
`/docker/isaaclab_container_utils`, as well as a superior interface in
`container.py`. The intention of this change is to make our
container-mediating code more easily readable, debuggable, and
modifiable. It is also done in the hopes that it can be more easily
distributed as we see a desire from users to [compose and modify our
setup](https://github.com/isaac-sim/IsaacLab/pull/455). It also has the
additional benefit of needing fewer sudo installs because of Python's
native yaml handling.

The central class, `IsaacLabContainerInterface`, contains a lot of the
original utility of the script, and several of the current
`container.py` scripts options simply configure it and call a method.
@pascal-roth `apptainer_utils.py` and the `./container.py job/push`
logic are separated out, I'm curious what you think of this delineation.
I also haven't been able to fully test that end of things as I don't
have a cluster to use, though I did verify that it worked to the extent
I could.

I will update the docs when I have received approval

<!-- As you go through the list, delete the ones that are not
applicable. -->

- Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- This change requires a documentation update

- [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with
`./isaaclab.sh --format`
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] I have run all the tests with `./isaaclab.sh --test` and they pass
- [ ] I have updated the changelog and the corresponding version in the
extension's `config/extension.toml` file
- [x] I have added my name to the `CONTRIBUTORS.md` or my name already
exists there

---------
Signed-off-by: 's avatarHunter Hansen <50837800+hhansen-bdai@users.noreply.github.com>
Signed-off-by: 's avatarJames Smith <142246516+jsmith-bdai@users.noreply.github.com>
Co-authored-by: 's avatarJames Smith <142246516+jsmith-bdai@users.noreply.github.com>
Co-authored-by: 's avatarDavid Hoeller <dhoeller@nvidia.com>
parent 1b8e2c0e
......@@ -26,6 +26,7 @@
**/*.sif
docker/exports/
docker/.container.yaml
docker/.container.cfg
# IDE
**/.idea/
......
......@@ -12,24 +12,3 @@ DOCKER_ISAACSIM_ROOT_PATH=/isaac-sim
DOCKER_ISAACLAB_PATH=/workspace/isaaclab
# Docker user directory - by default this is the root user's home directory
DOCKER_USER_HOME=/root
###
# Cluster specific settings
###
# Job scheduler used by cluster.
# Currently supports PBS and SLURM
CLUSTER_JOB_SCHEDULER=SLURM
# Docker cache dir for Isaac Sim (has to end on docker-isaac-sim)
# e.g. /cluster/scratch/$USER/docker-isaac-sim
CLUSTER_ISAAC_SIM_CACHE_DIR=/some/path/on/cluster/docker-isaac-sim
# Isaac Lab directory on the cluster (has to end on isaaclab)
# e.g. /cluster/home/$USER/isaaclab
CLUSTER_ISAACLAB_DIR=/some/path/on/cluster/isaaclab
# Cluster login
CLUSTER_LOGIN=username@cluster_ip
# Cluster scratch directory to store the SIF file
# e.g. /cluster/scratch/$USER
CLUSTER_SIF_PATH=/some/path/on/cluster/
# Python executable within Isaac Lab directory to run with the submitted job
CLUSTER_PYTHON_EXECUTABLE=source/standalone/workflows/rsl_rl/train.py
###
# Cluster specific settings
###
# Docker cache dir for Isaac Sim (has to end on docker-isaac-sim)
# e.g. /cluster/scratch/$USER/docker-isaac-sim
CLUSTER_ISAAC_SIM_CACHE_DIR=/some/path/on/cluster/docker-isaac-sim
# Isaac Lab directory on the cluster (has to end on isaaclab)
# e.g. /cluster/home/$USER/isaaclab
CLUSTER_ISAACLAB_DIR=/some/path/on/cluster/isaaclab
# Cluster login
CLUSTER_LOGIN=username@cluster_ip
# Cluster scratch directory to store the SIF file
# e.g. /cluster/scratch/$USER
CLUSTER_SIF_PATH=/some/path/on/cluster/
# Python executable within Isaac Lab directory to run with the submitted job
CLUSTER_PYTHON_EXECUTABLE=source/standalone/workflows/rsl_rl/train.py
#!/usr/bin/env bash
#==
# Configurations
#==
# Exits if error occurs
set -e
# Set tab-spaces
tabs 4
# get script directory
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
#==
# Functions
#==
# Function to check docker versions
# If docker version is more than 25, the script errors out.
check_docker_version() {
# check if docker is installed
if ! command -v docker &> /dev/null; then
echo "[Error] Docker is not installed! Please check the 'Docker Guide' for instruction." >&2;
exit 1
fi
# Retrieve Docker version
docker_version=$(docker --version | awk '{ print $3 }')
apptainer_version=$(apptainer --version | awk '{ print $3 }')
# Check if version is above 25.xx
if [ "$(echo "${docker_version}" | cut -d '.' -f 1)" -ge 25 ]; then
echo "[ERROR]: Docker version ${docker_version} is not compatible with Apptainer version ${apptainer_version}. Exiting."
exit 1
else
echo "[INFO]: Building singularity with docker version: ${docker_version} and Apptainer version: ${apptainer_version}."
fi
}
# Checks if a docker image exists, otherwise prints warning and exists
check_image_exists() {
image_name="$1"
if ! docker image inspect $image_name &> /dev/null; then
echo "[Error] The '$image_name' image does not exist!" >&2;
echo "[Error] You might be able to build it with /IsaacLab/docker/container.py." >&2;
exit 1
fi
}
# Check if the singularity image exists on the remote host, otherwise print warning and exit
check_singularity_image_exists() {
image_name="$1"
if ! ssh "$CLUSTER_LOGIN" "[ -f $CLUSTER_SIF_PATH/$image_name.tar ]"; then
echo "[Error] The '$image_name' image does not exist on the remote host $CLUSTER_LOGIN!" >&2;
exit 1
fi
}
submit_job() {
echo "[INFO] Arguments passed to job script ${@}"
case $CLUSTER_JOB_SCHEDULER in
"SLURM")
CMD=sbatch
job_script_file=submit_job_slurm.sh
;;
"PBS")
CMD=bash
job_script_file=submit_job_pbs.sh
;;
*)
echo "[ERROR] Unsupported job scheduler specified: '$CLUSTER_JOB_SCHEDULER'. Supported options are: ['SLURM', 'PBS']"
exit 1
;;
esac
ssh $CLUSTER_LOGIN "cd $CLUSTER_ISAACLAB_DIR && $CMD $CLUSTER_ISAACLAB_DIR/docker/cluster/$job_script_file \"$CLUSTER_ISAACLAB_DIR\" \"isaac-lab-$profile\" ${@}"
}
#==
# Main
#==
#!/bin/bash
help() {
echo -e "\nusage: $(basename "$0") [-h] <command> [<profile>] [<job_args>...] -- Utility for interfacing between IsaacLab and compute clusters."
echo -e "\noptions:"
echo -e " -h Display this help message."
echo -e "\ncommands:"
echo -e " push [<profile>] Push the docker image to the cluster."
echo -e " job [<profile>] [<job_args>] Submit a job to the cluster."
echo -e "\nwhere:"
echo -e " <profile> is the optional container profile specification. Defaults to 'base'."
echo -e " <job_args> are optional arguments specific to the job command."
echo -e "\n" >&2
}
# Parse options
while getopts ":h" opt; do
case ${opt} in
h )
help
exit 0
;;
\? )
echo "Invalid option: -$OPTARG" >&2
help
exit 1
;;
esac
done
shift $((OPTIND -1))
# Check for command
if [ $# -lt 1 ]; then
echo "Error: Command is required." >&2
help
exit 1
fi
command=$1
shift
profile="base"
case $command in
push)
if [ $# -gt 1 ]; then
echo "Error: Too many arguments for push command." >&2
help
exit 1
fi
[ $# -eq 1 ] && profile=$1
echo "Executing push command"
[ -n "$profile" ] && echo "Using profile: $profile"
if ! command -v apptainer &> /dev/null; then
echo "[INFO] Exiting because apptainer was not installed"
echo "[INFO] You may follow the installation procedure from here: https://apptainer.org/docs/admin/main/installation.html#install-ubuntu-packages"
exit
fi
# Check if Docker image exists
check_image_exists isaac-lab-$profile:latest
# Check if Docker version is greater than 25
check_docker_version
# source env file to get cluster login and path information
source $SCRIPT_DIR/.env.cluster
# make sure exports directory exists
mkdir -p /$SCRIPT_DIR/exports
# clear old exports for selected profile
rm -rf /$SCRIPT_DIR/exports/isaac-lab-$profile*
# create singularity image
# NOTE: we create the singularity image as non-root user to allow for more flexibility. If this causes
# issues, remove the --fakeroot flag and open an issue on the IsaacLab repository.
cd /$SCRIPT_DIR/exports
APPTAINER_NOHTTPS=1 apptainer build --sandbox --fakeroot isaac-lab-$profile.sif docker-daemon://isaac-lab-$profile:latest
# tar image (faster to send single file as opposed to directory with many files)
tar -cvf /$SCRIPT_DIR/exports/isaac-lab-$profile.tar isaac-lab-$profile.sif
# make sure target directory exists
ssh $CLUSTER_LOGIN "mkdir -p $CLUSTER_SIF_PATH"
# send image to cluster
scp $SCRIPT_DIR/exports/isaac-lab-$profile.tar $CLUSTER_LOGIN:$CLUSTER_SIF_PATH/isaac-lab-$profile.tar
;;
job)
[ $# -ge 1 ] && profile=$1 && shift
job_args="$@"
echo "Executing job command"
[ -n "$profile" ] && echo "Using profile: $profile"
[ -n "$job_args" ] && echo "Job arguments: $job_args"
source $SCRIPT_DIR/.env.cluster
# Check if singularity image exists on the remote host
check_singularity_image_exists isaac-lab-$profile
# make sure target directory exists
ssh $CLUSTER_LOGIN "mkdir -p $CLUSTER_ISAACLAB_DIR"
# Sync Isaac Lab code
echo "[INFO] Syncing Isaac Lab code..."
rsync -rh --exclude="*.git*" --filter=':- .dockerignore' /$SCRIPT_DIR/.. $CLUSTER_LOGIN:$CLUSTER_ISAACLAB_DIR
# execute job script
echo "[INFO] Executing job script..."
# check whether the second argument is a profile or a job argument
submit_job $job_args
;;
*)
echo "Error: Invalid command: $command" >&2
help
exit 1
;;
esac
......@@ -34,6 +34,7 @@ setup_directories() {
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
# load variables to set the Isaac Lab path on the cluster
source $SCRIPT_DIR/.env.cluster
source $SCRIPT_DIR/../.env.base
# make sure that all directories exists in cache directory
......
#!/usr/bin/env python3
# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
import argparse
import shutil
from pathlib import Path
from utils import x11_utils
from utils.isaaclab_container_interface import IsaacLabContainerInterface
def main():
parser = argparse.ArgumentParser(description="Utility for using Docker with Isaac Lab.")
subparsers = parser.add_subparsers(dest="command", required=True)
# We have to create a separate parent parser for common options to our subparsers
parent_parser = argparse.ArgumentParser(add_help=False)
parent_parser.add_argument("profile", nargs="?", default="base", help="Optional container profile specification.")
subparsers.add_parser(
"start", help="Build the docker image and create the container in detached mode.", parents=[parent_parser]
)
subparsers.add_parser(
"enter", help="Begin a new bash process within an existing Isaac Lab container.", parents=[parent_parser]
)
subparsers.add_parser(
"copy", help="Copy build and logs artifacts from the container to the host machine.", parents=[parent_parser]
)
subparsers.add_parser("stop", help="Stop the docker container and remove it.", parents=[parent_parser])
args = parser.parse_args()
if not shutil.which("docker"):
raise RuntimeError("Docker is not installed! Please check the 'Docker Guide' for instruction.")
# Creating container interface
ci = IsaacLabContainerInterface(context_dir=Path(__file__).resolve().parent, profile=args.profile)
print(f"[INFO] Using container profile: {ci.profile}")
if args.command == "start":
print(f"[INFO] Building the docker image and starting the container {ci.container_name} in the background...")
x11_outputs = x11_utils.x11_check(ci.statefile)
if x11_outputs is not None:
(x11_yaml, x11_envar) = x11_outputs
ci.add_yamls += x11_yaml
ci.environ.update(x11_envar)
ci.start()
elif args.command == "enter":
print(f"[INFO] Entering the existing {ci.container_name} container in a bash session...")
x11_utils.x11_refresh(ci.statefile)
ci.enter()
elif args.command == "copy":
print(f"[INFO] Copying artifacts from the 'isaac-lab-{ci.container_name}' container...")
ci.copy()
print("\n[INFO] Finished copying the artifacts from the container.")
elif args.command == "stop":
print(f"[INFO] Stopping the launched docker container {ci.container_name}...")
ci.stop()
x11_utils.x11_cleanup(ci.statefile)
else:
raise RuntimeError(f"Invalid command provided: {args.command}")
if __name__ == "__main__":
main()
#!/usr/bin/env bash
#==
# Configurations
#==
# Exits if error occurs
set -e
# Set tab-spaces
tabs 4
# get script directory
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
STATEFILE="${SCRIPT_DIR}/.container.yaml"
if ! [ -f "$STATEFILE" ]; then
touch $STATEFILE
fi
#==
# Functions
#==
# print the usage description
print_help () {
echo -e "\nusage: $(basename "$0") [-h] [...] -- Utility for handling Docker in Isaac Lab."
echo -e "\noptional arguments:"
echo -e "\t-h, --help Display the help content."
echo -e "\tstart [profile] Build the docker image and create the container in detached mode."
echo -e "\tenter [profile] Begin a new bash process within an existing Isaac Lab container."
echo -e "\tcopy [profile] Copy build and logs artifacts from the container to the host machine."
echo -e "\tstop [profile] Stop the docker container and remove it."
echo -e "\tpush [profile] Push the docker image to the cluster."
echo -e "\tjob [profile] [job_args] Submit a job to the cluster."
echo -e "\tconfig [profile] Parse, resolve and render compose file in canonical format."
echo -e "\n"
echo -e "where: "
echo -e "\t[profile] is the optional container profile specification. Example: 'isaaclab', 'base', 'ros2'."
echo -e "\t[job_args] are optional arguments specific to the executed script."
echo -e "\n" >&2
}
install_apptainer() {
# Installation procedure from here: https://apptainer.org/docs/admin/main/installation.html#install-ubuntu-packages
read -p "[INFO] Required 'apptainer' package could not be found. Would you like to install it via apt? (y/N)" app_answer
if [ "$app_answer" != "${app_answer#[Yy]}" ]; then
sudo apt update && sudo apt install -y software-properties-common
sudo add-apt-repository -y ppa:apptainer/ppa
sudo apt update && sudo apt install -y apptainer
else
echo "[INFO] Exiting because apptainer was not installed"
exit
fi
}
install_yq() {
# Installing yq to handle file parsing
# Installation procedure from here: https://github.com/mikefarah/yq?tab=readme-ov-file#linux-via-snap
read -p "[INFO] Required 'yq' package could not be found. Would you like to install it via snap? (y/N)" yq_answer
if [ "$yq_answer" != "${yq_answer#[Yy]}" ]; then
sudo snap install yq
else
echo "[INFO] Exiting because yq was not installed"
exit
fi
}
set_statefile_variable() {
# Check if yq is installed
if ! command -v yq &> /dev/null; then
install_yq
fi
# Stores key $1 with value $2 in yaml $STATEFILE
yq -i '.["'"$1"'"] = "'"$2"'"' $STATEFILE
}
load_statefile_variable() {
# Check if yq is installed
if ! command -v yq &> /dev/null; then
install_yq
fi
# Loads key $1 from yaml $STATEFILE as an envvar
# If key does not exist, the loaded var will equal "null"
eval $1="$(yq ".$1" $STATEFILE)"
}
delete_statefile_variable() {
# Check if yq is installed
if ! command -v yq &> /dev/null; then
install_yq
fi
# Deletes key $1 from yaml $STATEFILE
yq -i "del(.$1)" $STATEFILE
}
# Function to check docker versions
# If docker version is more than 25, the script errors out.
check_docker_version() {
# Retrieve Docker version
docker_version=$(docker --version | awk '{ print $3 }')
apptainer_version=$(apptainer --version | awk '{ print $3 }')
# Check if version is above 25.xx
if [ "$(echo "${docker_version}" | cut -d '.' -f 1)" -ge 25 ]; then
echo "[ERROR]: Docker version ${docker_version} is not compatible with Apptainer version ${apptainer_version}. Exiting."
exit 1
else
echo "[INFO]: Building singularity with docker version: ${docker_version} and Apptainer version: ${apptainer_version}."
fi
}
# Produces container_profile, add_profiles, and add_envs from the image_extension arg
resolve_image_extension() {
# If no profile was passed, we default to 'base'
container_profile=${1:-"base"}
# check if the second argument has to be a profile or can be a job argument instead
necessary_profile=${2:-true}
# We also default to 'base' if "isaaclab" is passed
if [ "$1" == "isaaclab" ]; then
container_profile="base"
fi
# check if a .env.$container_profile file exists
# if the argument is necessary a profile, then the file must exists otherwise an info is printed
if [ "$necessary_profile" = true ] && [ ! -f $SCRIPT_DIR/.env.$container_profile ]; then
echo "[Error] The profile '$container_profile' has no .env.$container_profile file!" >&2;
exit 1
elif [ ! -f $SCRIPT_DIR/.env.$container_profile ]; then
echo "[INFO] No .env.$container_profile found, assume second argument is no profile! Will use default container!" >&2;
container_profile="base"
fi
add_yamls="--file docker-compose.yaml"
add_profiles="--profile $container_profile"
# We will need .env.base regardless of profile
add_envs="--env-file .env.base"
# The second argument is interpreted as the profile to use.
# We will select the base profile by default.
# This will also determine the .env file that is loaded
if [ "$container_profile" != "base" ]; then
# We have to load multiple .env files here in order to combine
# them for the args from base required for extensions, (i.e. DOCKER_USER_HOME)
add_envs="$add_envs --env-file .env.$container_profile"
fi
}
# Prints a warning message and exits if the passed container is not running
is_container_running() {
container_name="$1"
if [ "$( docker container inspect -f '{{.State.Status}}' $container_name 2> /dev/null)" != "running" ]; then
echo "[Error] The '$container_name' container is not running!" >&2;
exit 1
fi
}
# Checks if a docker image exists, otherwise prints warning and exists
check_image_exists() {
image_name="$1"
if ! docker image inspect $image_name &> /dev/null; then
echo "[Error] The '$image_name' image does not exist!" >&2;
exit 1
fi
}
# Check if the singularity image exists on the remote host, otherwise print warning and exit
check_singularity_image_exists() {
image_name="$1"
if ! ssh "$CLUSTER_LOGIN" "[ -f $CLUSTER_SIF_PATH/$image_name.tar ]"; then
echo "[Error] The '$image_name' image does not exist on the remote host $CLUSTER_LOGIN!" >&2;
exit 1
fi
}
install_xauth() {
# check if xauth is installed
read -p "[INFO] xauth is not installed. Would you like to install it via apt? (y/N) " xauth_answer
if [ "$xauth_answer" != "${xauth_answer#[Yy]}" ]; then
sudo apt update && sudo apt install xauth
else
echo "[INFO] Did not install xauth. Full X11 forwarding not enabled."
fi
}
# This is modeled after Rocker's x11 forwarding extension
# https://github.com/osrf/rocker
configure_x11() {
if ! command -v xauth &> /dev/null; then
install_xauth
fi
load_statefile_variable __ISAACLAB_TMP_XAUTH
__ISAACLAB_TMP_DIR=/tmp/isaaclab_tmp_xauth/
# Create temp .xauth file to be mounted in the container
if [ "$__ISAACLAB_TMP_XAUTH" = "null" ] || [ ! -f "$__ISAACLAB_TMP_XAUTH" ]; then
mkdir -p "${__ISAACLAB_TMP_DIR}"
__ISAACLAB_TMP_XAUTH=$(mktemp --suffix=".xauth" --tmpdir="${__ISAACLAB_TMP_DIR}")
set_statefile_variable __ISAACLAB_TMP_XAUTH $__ISAACLAB_TMP_XAUTH
# Extract MIT-MAGIC-COOKIE for current display | Change the 'connection family' to FamilyWild (ffff) | merge into tmp .xauth file
# https://www.x.org/archive/X11R6.8.1/doc/Xsecurity.7.html#toc3
xauth nlist ${DISPLAY} | sed -e s/^..../ffff/ | xauth -f $__ISAACLAB_TMP_XAUTH nmerge -
fi
# Export here so it's an envvar for the called Docker commands
export __ISAACLAB_TMP_XAUTH
export __ISAACLAB_TMP_DIR
add_yamls="$add_yamls --file x11.yaml "
# TODO: Add check to make sure Xauth file is correct
}
x11_check() {
load_statefile_variable __ISAACLAB_X11_FORWARDING_ENABLED
if [ "$__ISAACLAB_X11_FORWARDING_ENABLED" = "null" ]; then
echo "[INFO] X11 forwarding from the Isaac Lab container is off by default."
echo "[INFO] It will fail if there is no display, or this script is being run via ssh without proper configuration."
read -p "Would you like to enable it? (y/N) " x11_answer
if [ "$x11_answer" != "${x11_answer#[Yy]}" ]; then
__ISAACLAB_X11_FORWARDING_ENABLED=1
set_statefile_variable __ISAACLAB_X11_FORWARDING_ENABLED 1
echo "[INFO] X11 forwarding is enabled from the container."
else
__ISAACLAB_X11_FORWARDING_ENABLED=0
set_statefile_variable __ISAACLAB_X11_FORWARDING_ENABLED 0
echo "[INFO] X11 forwarding is disabled from the container."
fi
else
echo "[INFO] X11 Forwarding is configured as $__ISAACLAB_X11_FORWARDING_ENABLED in .container.yaml"
if [ "$__ISAACLAB_X11_FORWARDING_ENABLED" = "1" ]; then
echo "[INFO] To disable X11 forwarding, set \`__ISAACLAB_X11_FORWARDING_ENABLED: 0\` in .container.yaml"
else
echo "[INFO] To enable X11 forwarding, set \`__ISAACLAB_X11_FORWARDING_ENABLED: 1\` in .container.yaml"
fi
fi
if [ "$__ISAACLAB_X11_FORWARDING_ENABLED" = "1" ]; then
configure_x11
fi
}
x11_update() {
# Check if the MIT-MAGIC-COOKIE-1 in __ISAACLAB_TMP_XAUTH
# is the same as the current DISPLAY's. If not, generate
# a new .xauth file with the current MIT-MAGIC-COOKIE-1,
# using the same filename so that the bind-mount and
# XAUTHORITY var from build-time still work
load_statefile_variable __ISAACLAB_TMP_XAUTH
if ! [ "$__ISAACLAB_TMP_XAUTH" = "null" ] && [ -f "$__ISAACLAB_TMP_XAUTH" ]; then
tmp_cookie=$(xauth -f "$__ISAACLAB_TMP_XAUTH" list | awk '$2 == "MIT-MAGIC-COOKIE-1" {print $3; exit}')
current_cookie=$(xauth list "${DISPLAY}" | awk '$2 == "MIT-MAGIC-COOKIE-1" {print $3; exit}')
if ! [ "${tmp_cookie}" = "{$current_cookie}" ]; then
rm "$__ISAACLAB_TMP_XAUTH"
touch "$__ISAACLAB_TMP_XAUTH"
xauth nlist ${DISPLAY} | sed -e s/^..../ffff/ | xauth -f $__ISAACLAB_TMP_XAUTH nmerge -
fi
fi
}
x11_cleanup() {
load_statefile_variable __ISAACLAB_TMP_XAUTH
if ! [ "$__ISAACLAB_TMP_XAUTH" = "null" ] && [ -f "$__ISAACLAB_TMP_XAUTH" ]; then
echo "[INFO] Removing temporary Isaac Lab .xauth file $__ISAACLAB_TMP_XAUTH."
rm $__ISAACLAB_TMP_XAUTH
delete_statefile_variable __ISAACLAB_TMP_XAUTH
fi
}
submit_job() {
echo "[INFO] Arguments passed to job script ${@}"
case $CLUSTER_JOB_SCHEDULER in
"SLURM")
CMD=sbatch
job_script_file=submit_job_slurm.sh
;;
"PBS")
CMD=bash
job_script_file=submit_job_pbs.sh
;;
*)
echo "[ERROR] Unsupported job scheduler specified: '$CLUSTER_JOB_SCHEDULER'. Supported options are: ['SLURM', 'PBS']"
exit 1
;;
esac
ssh $CLUSTER_LOGIN "cd $CLUSTER_ISAACLAB_DIR && $CMD $CLUSTER_ISAACLAB_DIR/docker/cluster/$job_script_file \"$CLUSTER_ISAACLAB_DIR\" \"isaac-lab-$container_profile\" ${@}"
}
#==
# Main
#==
# check argument provided
if [ -z "$*" ]; then
echo "[Error] No arguments provided." >&2;
print_help
exit 1
fi
# check if docker is installed
if ! command -v docker &> /dev/null; then
echo "[Error] Docker is not installed! Please check the 'Docker Guide' for instruction." >&2;
exit 1
fi
# parse arguments
mode="$1"
profile_arg="$2" # Capture the second argument as the potential profile argument
# Check mode argument and resolve the container profile
case $mode in
build|start|enter|copy|stop|push|config)
resolve_image_extension "$profile_arg" true
;;
job)
resolve_image_extension "$profile_arg" false
;;
*)
# Not recognized mode
echo "[Error] Invalid command provided: $mode"
print_help
exit 1
;;
esac
# Produces a nice print statement stating which container profile is being used
echo "[INFO] Using container profile: $container_profile"
# resolve mode
case $mode in
start)
echo "[INFO] Building the docker image and starting the container isaac-lab-$container_profile in the background..."
pushd ${SCRIPT_DIR} > /dev/null 2>&1
# Determine if we want x11 forwarding enabled
x11_check
# We have to build the base image as a separate step,
# in case we are building a profile which depends
# upon
docker compose --file docker-compose.yaml --env-file .env.base build isaac-lab-base
docker compose $add_yamls $add_profiles $add_envs up --detach --build --remove-orphans
popd > /dev/null 2>&1
;;
enter)
# Check that desired container is running, exit if it isn't
is_container_running isaac-lab-$container_profile
x11_update
echo "[INFO] Entering the existing 'isaac-lab-$container_profile' container in a bash session..."
pushd ${SCRIPT_DIR} > /dev/null 2>&1
docker exec --interactive --tty -e DISPLAY=$DISPLAY isaac-lab-$container_profile bash
popd > /dev/null 2>&1
;;
copy)
# Check that desired container is running, exit if it isn't
is_container_running isaac-lab-$container_profile
DOCKER_ISAACLAB_PATH=$(docker exec isaac-lab-$container_profile printenv DOCKER_ISAACLAB_PATH)
echo "[INFO] Copying artifacts from the 'isaac-lab-$container_profile' container..."
echo -e "\t - ${DOCKER_ISAACLAB_PATH}/logs -> ${SCRIPT_DIR}/artifacts/logs"
echo -e "\t - ${DOCKER_ISAACLAB_PATH}/docs/_build -> ${SCRIPT_DIR}/artifacts/docs/_build"
echo -e "\t - ${DOCKER_ISAACLAB_PATH}/data_storage -> ${SCRIPT_DIR}/artifacts/data_storage"
# enter the script directory
pushd ${SCRIPT_DIR} > /dev/null 2>&1
# We have to remove before copying because repeated copying without deletion
# causes strange errors such as nested _build directories
# warn the user
echo -e "[WARN] Removing the existing artifacts...\n"
rm -rf ./artifacts/logs ./artifacts/docs/_build ./artifacts/data_storage
# create the directories
mkdir -p ./artifacts/docs
# copy the artifacts
docker cp isaac-lab-$container_profile:${DOCKER_ISAACLAB_PATH}/logs ./artifacts/logs
docker cp isaac-lab-$container_profile:${DOCKER_ISAACLAB_PATH}/docs/_build ./artifacts/docs/_build
docker cp isaac-lab-$container_profile:${DOCKER_ISAACLAB_PATH}/data_storage ./artifacts/data_storage
echo -e "\n[INFO] Finished copying the artifacts from the container."
popd > /dev/null 2>&1
;;
stop)
# Check that desired container is running, exit if it isn't
is_container_running isaac-lab-$container_profile
echo "[INFO] Stopping the launched docker container isaac-lab-$container_profile..."
pushd ${SCRIPT_DIR} > /dev/null 2>&1
docker compose --file docker-compose.yaml $add_profiles $add_envs down
x11_cleanup
popd > /dev/null 2>&1
;;
push)
if ! command -v apptainer &> /dev/null; then
install_apptainer
fi
# Check if Docker image exists
check_image_exists isaac-lab-$container_profile:latest
# Check if Docker version is greater than 25
check_docker_version
# source env file to get cluster login and path information
source $SCRIPT_DIR/.env.base
# make sure exports directory exists
mkdir -p /$SCRIPT_DIR/exports
# clear old exports for selected profile
rm -rf /$SCRIPT_DIR/exports/isaac-lab-$container_profile*
# create singularity image
# NOTE: we create the singularity image as non-root user to allow for more flexibility. If this causes
# issues, remove the --fakeroot flag and open an issue on the IsaacLab repository.
cd /$SCRIPT_DIR/exports
APPTAINER_NOHTTPS=1 apptainer build --sandbox --fakeroot isaac-lab-$container_profile.sif docker-daemon://isaac-lab-$container_profile:latest
# tar image (faster to send single file as opposed to directory with many files)
tar -cvf /$SCRIPT_DIR/exports/isaac-lab-$container_profile.tar isaac-lab-$container_profile.sif
# make sure target directory exists
ssh $CLUSTER_LOGIN "mkdir -p $CLUSTER_SIF_PATH"
# send image to cluster
scp $SCRIPT_DIR/exports/isaac-lab-$container_profile.tar $CLUSTER_LOGIN:$CLUSTER_SIF_PATH/isaac-lab-$container_profile.tar
;;
job)
source $SCRIPT_DIR/.env.base
# Check if singularity image exists on the remote host
check_singularity_image_exists isaac-lab-$container_profile
# make sure target directory exists
ssh $CLUSTER_LOGIN "mkdir -p $CLUSTER_ISAACLAB_DIR"
# Sync Isaac Lab code
echo "[INFO] Syncing Isaac Lab code..."
rsync -rh --exclude="*.git*" --filter=':- .dockerignore' /$SCRIPT_DIR/.. $CLUSTER_LOGIN:$CLUSTER_ISAACLAB_DIR
# execute job script
echo "[INFO] Executing job script..."
# check whether the second argument is a profile or a job argument
if [ "$profile_arg" == "$container_profile" ] ; then
# if the second argument is a profile, we have to shift the arguments
submit_job "${@:3}"
else
# if the second argument is a job argument, we have to shift only one argument
submit_job "${@:2}"
fi
;;
config)
pushd ${SCRIPT_DIR} > /dev/null 2>&1
docker compose $add_yamls $add_envs $add_profiles config
;;
*)
# Not recognized mode
echo "[Error] Invalid command provided: $mode"
print_help
exit 1
;;
esac
# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
from __future__ import annotations
import os
import shutil
import subprocess
from pathlib import Path
from typing import Any
from utils.statefile import Statefile
class IsaacLabContainerInterface:
"""
Interface for managing Isaac Lab containers.
"""
def __init__(self, context_dir: Path, profile: str = "base", statefile: None | Statefile = None):
"""
Initialize the IsaacLabContainerInterface with the given parameters.
Args:
context_dir : The context directory for Docker operations.
statefile : An instance of the Statefile class to manage state variables. If not provided, initializes a Statefile(path=self.context_dir/.container.yaml).
profile : The profile name for the container. Defaults to "base".
"""
self.context_dir = context_dir
if statefile is None:
self.statefile = Statefile(path=self.context_dir / ".container.cfg")
else:
self.statefile = statefile
self.profile = profile
if self.profile == "isaaclab":
# Silently correct from isaaclab to base,
# because isaaclab is a commonly passed arg
# but not a real profile
self.profile = "base"
self.container_name = f"isaac-lab-{self.profile}"
self.image_name = f"isaac-lab-{self.profile}:latest"
self.environ = os.environ
self.resolve_image_extension()
self.load_dot_vars()
def resolve_image_extension(self):
"""
Resolve the image extension by setting up YAML files, profiles, and environment files for the Docker compose command.
"""
self.add_yamls = ["--file", "docker-compose.yaml"]
self.add_profiles = ["--profile", f"{self.profile}"]
self.add_env_files = ["--env-file", ".env.base"]
if self.profile != "base":
self.add_env_files += ["--env-file", f".env.{self.profile}"]
def load_dot_vars(self):
"""
Load environment variables from .env files into a dictionary.
The environment variables are read in order and overwritten if there are name conflicts,
mimicking the behavior of Docker compose.
"""
self.dot_vars: dict[str, Any] = {}
if len(self.add_env_files) % 2 != 0:
raise RuntimeError(
"The parameter self.add_env_files is configured incorrectly. There should be an even number of"
" arguments."
)
for i in range(1, len(self.add_env_files), 2):
with open(self.context_dir / self.add_env_files[i]) as f:
self.dot_vars.update(dict(line.strip().split("=", 1) for line in f if "=" in line))
def is_container_running(self) -> bool:
"""
Check if the container is running.
If the container is not running, return False.
Returns:
bool: True if the container is running, False otherwise.
"""
status = subprocess.run(
["docker", "container", "inspect", "-f", "{{.State.Status}}", self.container_name],
capture_output=True,
text=True,
check=False,
).stdout.strip()
return status == "running"
def does_image_exist(self) -> bool:
"""
Check if the Docker image exists.
If the image does not exist, return False.
Returns:
bool: True if the image exists, False otherwise.
"""
result = subprocess.run(["docker", "image", "inspect", self.image_name], capture_output=True, text=True)
return result.returncode == 0
def start(self):
"""
Build and start the Docker container using the Docker compose command.
"""
subprocess.run(
[
"docker",
"compose",
"--file",
"docker-compose.yaml",
"--env-file",
".env.base",
"build",
"isaac-lab-base",
],
check=False,
cwd=self.context_dir,
env=self.environ,
)
subprocess.run(
["docker", "compose"]
+ self.add_yamls
+ self.add_profiles
+ self.add_env_files
+ ["up", "--detach", "--build", "--remove-orphans"],
check=False,
cwd=self.context_dir,
env=self.environ,
)
def enter(self):
"""
Enter the running container by executing a bash shell.
Raises:
RuntimeError: If the container is not running.
"""
if self.is_container_running():
subprocess.run([
"docker",
"exec",
"--interactive",
"--tty",
"-e",
f"DISPLAY={os.environ['DISPLAY']}",
f"{self.container_name}",
"bash",
])
else:
raise RuntimeError(f"The container '{self.container_name}' is not running")
def stop(self):
"""
Stop the running container using the Docker compose command.
Raises:
RuntimeError: If the container is not running.
"""
if self.is_container_running():
subprocess.run(
["docker", "compose", "--file", "docker-compose.yaml"]
+ self.add_profiles
+ self.add_env_files
+ ["down"],
check=False,
cwd=self.context_dir,
env=self.environ,
)
else:
raise RuntimeError(f"Can't stop container '{self.container_name}' as it is not running.")
def copy(self, output_dir: Path | None = None):
"""
Copy artifacts from the running container to the host machine.
Args:
output_dir: The directory to copy the artifacts to. Defaults to self.context_dir.
Raises:
RuntimeError: If the container is not running.
"""
if self.is_container_running():
if output_dir is None:
output_dir = self.context_dir
output_dir = output_dir.joinpath("artifacts")
if not output_dir.exists():
output_dir.mkdir()
artifacts = {
"logs": output_dir.joinpath("logs"),
"docs/_build": output_dir.joinpath("docs"),
"data_storage": output_dir.joinpath("data_storage"),
}
for container_path, host_path in artifacts.items():
print(f"\t - /workspace/isaaclab/{container_path} -> {host_path}")
for path in artifacts.values():
shutil.rmtree(path, ignore_errors=True)
for container_path, host_path in artifacts.items():
subprocess.run(
[
"docker",
"cp",
f"isaac-lab-{self.profile}:/workspace/isaaclab/{container_path}/",
f"{host_path}",
],
check=False,
)
else:
raise RuntimeError(f"The container '{self.container_name}' is not running")
# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
from __future__ import annotations
import configparser
from configparser import ConfigParser
from pathlib import Path
from typing import Any
def load_cfg_file(path: Path) -> ConfigParser:
"""
Load the contents of a config file.
If the file exists, read its contents and return them as a dictionary.
If the file does not exist, create an empty file and return an empty dictionary.
Args:
statefile: The path to the config file.
Returns:
dict: The contents of the config file as a dictionary.
"""
cfg = ConfigParser()
cfg.read(path)
return cfg
def save_cfg_file(path: Path, cfg: ConfigParser):
"""
Save a dictionary to a config file.
Args:
path: The path to the config file.
data: The data to be saved to the config file.
"""
with open(path, "w+") as file:
cfg.write(file)
class Statefile:
"""
A class to manage state variables stored in a cfg file.
"""
def __init__(self, path: Path, namespace: str | None = None):
"""
Initialize the Statefile object with the path to the cfg file.
Args:
path: The path to the cfg file.
namespace: Namespace a section of the cfg.
Defaults to None, and all member functions will have
to specify section or else set Statefile.namespace directly.
"""
self.path = path
self.namespace = namespace
self.load_cfg()
def __del__(self):
"""
Save self.loaded_cfg to self.path upon deconstruction
"""
self.save_cfg()
def set_variable(self, key: str, value: Any, section: str | None = None):
"""
Set a variable in the cfg file.
Args:
key: The key of the variable to be set.
value: The value of the variable to be set.
section: section of the cfg. Defaults to the self.namespace
"""
if section is None:
if self.namespace is None:
raise configparser.Error("No section specified")
section = self.namespace
if section not in self.loaded_cfg.sections():
self.loaded_cfg.add_section(section)
self.loaded_cfg.set(section, key, value)
def load_variable(self, key: str, section: str | None = None) -> Any:
"""
Load a variable from the cfg file.
Args:
key: The key of the variable to be loaded.
section: section of the cfg. Defaults to the self.namespace
Returns:
any: The value of the variable, or None if the key does not exist.
"""
if section is None:
if self.namespace is None:
raise configparser.Error("No section specified")
section = self.namespace
return self.loaded_cfg.get(section, key, fallback=None)
def delete_variable(self, key: str, section: str | None = None):
"""
Delete a variable from the cfg file.
Args:
key: The key of the variable to be deleted.
section: section of the cfg. Defaults to self.namespace
"""
if section is None:
if self.namespace is None:
raise configparser.Error("No section specified")
section = self.namespace
if section not in self.loaded_cfg.sections():
raise configparser.NoSectionError(f"Section {section} does not exist in {self.path}")
if self.loaded_cfg.has_option(section, key):
self.loaded_cfg.remove_option(section, key)
else:
raise configparser.NoOptionError(option=key, section=section)
def load_cfg(self):
"""
Calls load_cfg_file() to populate self.loaded_cfg with the
data stored at self.path
"""
self.loaded_cfg = load_cfg_file(self.path)
def save_cfg(self):
"""
Calls save_cfg_file() to write the contents of self.loaded_cfg
to the file at self.path
"""
save_cfg_file(self.path, self.loaded_cfg)
# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
from __future__ import annotations
import os
import shutil
import subprocess
from pathlib import Path
from utils.statefile import Statefile
# This method of x11 enabling forwarding was inspired by osrf/rocker
# https://github.com/osrf/rocker
def configure_x11(statefile: Statefile) -> dict[str, str]:
"""
Configure X11 forwarding by creating and managing a temporary .xauth file.
If xauth is not installed, prompt the user to install it. If the .xauth file
does not exist, create it and configure it with the necessary xauth cookie.
Args:
statefile: An instance of the Statefile class to manage state variables.
Returns:
dict: A dictionary where the key is __ISAACLAB_TMP_XAUTH (referenced in x11.yaml)
and the value is the corresponding tmp file which has been created.
"""
if not shutil.which("xauth"):
print("[INFO] xauth is not installed.")
print("[INFO] Please install it with 'apt install xauth'")
exit(1)
statefile.namespace = "X11"
__ISAACLAB_TMP_XAUTH = statefile.load_variable("__ISAACLAB_TMP_XAUTH")
if __ISAACLAB_TMP_XAUTH is None or not Path(__ISAACLAB_TMP_XAUTH).exists():
__ISAACLAB_TMP_DIR = subprocess.run(["mktemp", "-d"], capture_output=True, text=True, check=True).stdout.strip()
__ISAACLAB_TMP_XAUTH = create_x11_tmpfile(tmpdir=Path(__ISAACLAB_TMP_DIR))
statefile.set_variable("__ISAACLAB_TMP_XAUTH", str(__ISAACLAB_TMP_XAUTH))
else:
__ISAACLAB_TMP_DIR = Path(__ISAACLAB_TMP_XAUTH).parent
return {"__ISAACLAB_TMP_XAUTH": str(__ISAACLAB_TMP_XAUTH), "__ISAACLAB_TMP_DIR": str(__ISAACLAB_TMP_DIR)}
def x11_check(statefile: Statefile) -> tuple[list[str], dict[str, str]] | None:
"""
Check and configure X11 forwarding based on user input and existing state.
Prompt the user to enable or disable X11 forwarding if not already configured.
Configure X11 forwarding if enabled.
Args:
statefile: An instance of the Statefile class to manage state variables.
Returns:
list or str: A list containing the x11.yaml file configuration option if X11 forwarding is enabled,
otherwise None
"""
statefile.namespace = "X11"
__ISAACLAB_X11_FORWARDING_ENABLED = statefile.load_variable("__ISAACLAB_X11_FORWARDING_ENABLED")
if __ISAACLAB_X11_FORWARDING_ENABLED is None:
print("[INFO] X11 forwarding from the Isaac Lab container is off by default.")
print(
"[INFO] It will fail if there is no display, or this script is being run via ssh without proper"
" configuration."
)
x11_answer = input("Would you like to enable it? (y/N) ")
if x11_answer.lower() == "y":
__ISAACLAB_X11_FORWARDING_ENABLED = "1"
statefile.set_variable("__ISAACLAB_X11_FORWARDING_ENABLED", "1")
print("[INFO] X11 forwarding is enabled from the container.")
else:
__ISAACLAB_X11_FORWARDING_ENABLED = "0"
statefile.set_variable("__ISAACLAB_X11_FORWARDING_ENABLED", "0")
print("[INFO] X11 forwarding is disabled from the container.")
else:
print(f"[INFO] X11 Forwarding is configured as {__ISAACLAB_X11_FORWARDING_ENABLED} in .container.cfg")
if __ISAACLAB_X11_FORWARDING_ENABLED == "1":
print("[INFO] To disable X11 forwarding, set __ISAACLAB_X11_FORWARDING_ENABLED=0 in .container.cfg")
else:
print("[INFO] To enable X11 forwarding, set __ISAACLAB_X11_FORWARDING_ENABLED=1 in .container.cfg")
if __ISAACLAB_X11_FORWARDING_ENABLED == "1":
x11_envar = configure_x11(statefile)
# If X11 forwarding is enabled, return the proper args to
# compose the x11.yaml file. Else, return an empty string.
return (["--file", "x11.yaml"], x11_envar)
return None
def x11_cleanup(statefile: Statefile):
"""
Clean up the temporary .xauth file used for X11 forwarding.
If the .xauth file exists, delete it and remove the corresponding state variable.
Args:
statefile: An instance of the Statefile class to manage state variables.
"""
statefile.namespace = "X11"
__ISAACLAB_TMP_XAUTH = statefile.load_variable("__ISAACLAB_TMP_XAUTH")
if __ISAACLAB_TMP_XAUTH is not None and Path(__ISAACLAB_TMP_XAUTH).exists():
print(f"[INFO] Removing temporary Isaac Lab .xauth file {__ISAACLAB_TMP_XAUTH}.")
Path(__ISAACLAB_TMP_XAUTH).unlink()
statefile.delete_variable("__ISAACLAB_TMP_XAUTH")
def create_x11_tmpfile(tmpfile: Path | None = None, tmpdir: Path | None = None) -> Path:
"""
Creates an .xauth file with an MIT-MAGIC-COOKIE derived from the current DISPLAY,
returns its location as a Path.
Args:
tmpfile: A Path to a file which will be filled with the correct .xauth info
tmpdir: A Path to the directory where a random tmp file will be made,
used as an --tmpdir arg to mktemp
"""
if tmpfile is None:
if tmpdir is None:
add_tmpdir = ""
else:
add_tmpdir = f"--tmpdir={tmpdir}"
# Create .tmp file with .xauth suffix
tmp_xauth = Path(
subprocess.run(
["mktemp", "--suffix=.xauth", f"{add_tmpdir}"], capture_output=True, text=True, check=True
).stdout.strip()
)
else:
tmpfile.touch()
tmp_xauth = tmpfile
# Derive current MIT-MAGIC-COOKIE and make it universally addressable
xauth_cookie = subprocess.run(
["xauth", "nlist", os.environ["DISPLAY"]], capture_output=True, text=True, check=True
).stdout.replace("ffff", "")
# Merge the new cookie into the create .tmp file
subprocess.run(["xauth", "-f", tmp_xauth, "nmerge", "-"], input=xauth_cookie, text=True, check=True)
return tmp_xauth
def x11_refresh(statefile: Statefile):
"""
If x11 is enabled, generates a new .xauth file with the current MIT-MAGIC-COOKIE-1,
using the same filename so that the bind-mount and
XAUTHORITY var from build-time still work. DISPLAY will also
need to be updated in the container environment command.
Args:
statefile: An instance of the Statefile class to manage state variables.
"""
statefile.namespace = "X11"
__ISAACLAB_TMP_XAUTH = Path(statefile.load_variable("__ISAACLAB_TMP_XAUTH"))
if __ISAACLAB_TMP_XAUTH is not None and __ISAACLAB_TMP_XAUTH.exists():
__ISAACLAB_TMP_XAUTH.unlink()
create_x11_tmpfile(tmpfile=__ISAACLAB_TMP_XAUTH)
statefile.set_variable("__ISAACLAB_TMP_XAUTH", str(__ISAACLAB_TMP_XAUTH))
......@@ -80,7 +80,8 @@ needed to run Isaac Lab inside a Docker container. A subset of these are summari
store frequently re-used resources compiled by Isaac Sim, such as shaders, and to retain logs, data, and documents.
* ``base.env``: Stores environment variables required for the ``base`` build process and the container itself. ``.env``
files which end with something else (i.e. ``.env.ros2``) define these for `image_extension <#isaac-lab-image-extensions>`_.
* ``container.sh``: A script that wraps the ``docker compose`` command to build the image and run the container.
* ``container.py``: A script that interfaces with tools in ``isaaclab_container_utils`` to configure and build the image,
and run and interact with the container.
Running the Container
---------------------
......@@ -89,7 +90,7 @@ Running the Container
The docker container copies all the files from the repository into the container at the
location ``/workspace/isaaclab`` at build time. This means that any changes made to the files in the container would not
normally be reflected in the repository after the image has been built, i.e. after ``./container.sh start`` is run.
normally be reflected in the repository after the image has been built, i.e. after ``./container.py start`` is run.
For a faster development cycle, we mount the following directories in the Isaac Lab repository into the container
so that you can edit their files from the host machine:
......@@ -99,15 +100,14 @@ Running the Container
for the ``_build`` subdirectory where build artifacts are stored.
The script ``container.sh`` wraps around three basic ``docker compose`` commands. Each can accept an `image_extension argument <#isaac-lab-image-extensions>`_,
The script ``container.py`` parallels three basic ``docker compose`` commands. Each can accept an `image_extension argument <#isaac-lab-image-extensions>`_,
or else they will default to image_extension ``base``:
1. ``start``: This builds the image and brings up the container in detached mode (i.e. in the background).
2. ``enter``: This begins a new bash process in an existing isaaclab container, and which can be exited
without bringing down the container.
3. ``copy``: This copies the ``logs``, ``data_storage`` and ``docs/_build`` artifacts, from the ``isaac-lab-logs``, ``isaac-lab-data`` and ``isaac-lab-docs``
volumes respectively, to the ``docker/artifacts`` directory. These artifacts persist between docker
container instances and are shared between image extensions.
volumes respectively, to the ``docker/artifacts`` directory. These artifacts persist between docker container instances and are shared between image extensions.
4. ``stop``: This brings down the container and removes it.
The following shows how to launch the container in a detached state and enter it:
......@@ -116,10 +116,10 @@ The following shows how to launch the container in a detached state and enter it
# Launch the container in detached mode
# We don't pass an image extension arg, so it defaults to 'base'
./docker/container.sh start
python docker/container.py start
# Enter the container
# We pass 'base' explicitly, but if we hadn't it would default to 'base'
./docker/container.sh enter base
python docker/container.py enter base
To copy files from the base container to the host machine, you can use the following command:
......@@ -128,13 +128,13 @@ To copy files from the base container to the host machine, you can use the follo
# Copy the file /workspace/isaaclab/logs to the current directory
docker cp isaac-lab-base:/workspace/isaaclab/logs .
The script ``container.sh`` provides a wrapper around this command to copy the ``logs`` , ``data_storage`` and ``docs/_build``
The script ``container.py`` provides a wrapper around this command to copy the ``logs`` , ``data_storage`` and ``docs/_build``
directories to the ``docker/artifacts`` directory. This is useful for copying the logs, data and documentation:
.. code::
# stop the container
./docker/container.sh stop
python docker/container.py stop
X11 forwarding
......@@ -194,7 +194,7 @@ To view the contents of these volumes, you can use the following command:
Isaac Lab Image Extensions
--------------------------
The produced image depends upon the arguments passed to ``./container.sh start`` and ``./container.sh stop``. These
The produced image depends upon the arguments passed to ``container.py start`` and ``container.py stop``. These
commands accept an ``image_extension`` as an additional argument. If no argument is passed, then these
commands default to ``base``. Currently, the only valid ``image_extension`` arguments are (``base``, ``ros2``).
Only one ``image_extension`` can be passed at a time, and the produced container will be named ``isaaclab``.
......@@ -202,13 +202,13 @@ Only one ``image_extension`` can be passed at a time, and the produced container
.. code:: bash
# start base by default
./container.sh start
python docker/container.py start
# stop base explicitly
./container.sh stop base
python docker/container.py stop base
# start ros2 container
./container.sh start ros2
python docker/container.py start ros2
# stop ros2 container
./container.sh stop ros2
python docker/container.py stop ros2
The passed ``image_extension`` argument will build the image defined in ``Dockerfile.${image_extension}``,
with the corresponding `profile`_ in the ``docker-compose.yaml`` and the envars from ``.env.${image_extension}``
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment