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 @@ ...@@ -26,6 +26,7 @@
**/*.sif **/*.sif
docker/exports/ docker/exports/
docker/.container.yaml docker/.container.yaml
docker/.container.cfg
# IDE # IDE
**/.idea/ **/.idea/
......
...@@ -12,24 +12,3 @@ DOCKER_ISAACSIM_ROOT_PATH=/isaac-sim ...@@ -12,24 +12,3 @@ DOCKER_ISAACSIM_ROOT_PATH=/isaac-sim
DOCKER_ISAACLAB_PATH=/workspace/isaaclab DOCKER_ISAACLAB_PATH=/workspace/isaaclab
# Docker user directory - by default this is the root user's home directory # Docker user directory - by default this is the root user's home directory
DOCKER_USER_HOME=/root 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() { ...@@ -34,6 +34,7 @@ setup_directories() {
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
# load variables to set the Isaac Lab path on the cluster # load variables to set the Isaac Lab path on the cluster
source $SCRIPT_DIR/.env.cluster
source $SCRIPT_DIR/../.env.base source $SCRIPT_DIR/../.env.base
# make sure that all directories exists in cache directory # 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()
This diff is collapsed.
# 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 ...@@ -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. 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`` * ``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>`_. 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 Running the Container
--------------------- ---------------------
...@@ -89,7 +90,7 @@ 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 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 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 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: so that you can edit their files from the host machine:
...@@ -99,15 +100,14 @@ Running the Container ...@@ -99,15 +100,14 @@ Running the Container
for the ``_build`` subdirectory where build artifacts are stored. 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``: 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). 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 2. ``enter``: This begins a new bash process in an existing isaaclab container, and which can be exited
without bringing down the container. 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`` 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 volumes respectively, to the ``docker/artifacts`` directory. These artifacts persist between docker container instances and are shared between image extensions.
container instances and are shared between image extensions.
4. ``stop``: This brings down the container and removes it. 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: 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 ...@@ -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 # Launch the container in detached mode
# We don't pass an image extension arg, so it defaults to 'base' # 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 # Enter the container
# We pass 'base' explicitly, but if we hadn't it would default to 'base' # 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: 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 ...@@ -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 # Copy the file /workspace/isaaclab/logs to the current directory
docker cp isaac-lab-base:/workspace/isaaclab/logs . 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: directories to the ``docker/artifacts`` directory. This is useful for copying the logs, data and documentation:
.. code:: .. code::
# stop the container # stop the container
./docker/container.sh stop python docker/container.py stop
X11 forwarding X11 forwarding
...@@ -194,7 +194,7 @@ To view the contents of these volumes, you can use the following command: ...@@ -194,7 +194,7 @@ To view the contents of these volumes, you can use the following command:
Isaac Lab Image Extensions 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 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``). 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``. 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 ...@@ -202,13 +202,13 @@ Only one ``image_extension`` can be passed at a time, and the produced container
.. code:: bash .. code:: bash
# start base by default # start base by default
./container.sh start python docker/container.py start
# stop base explicitly # stop base explicitly
./container.sh stop base python docker/container.py stop base
# start ros2 container # start ros2 container
./container.sh start ros2 python docker/container.py start ros2
# stop ros2 container # 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}``, 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}`` 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