├── resource
└── launch_ros_sandbox
├── codecov.yml
├── test
├── config
│ └── mypy.ini
├── dummy-dashing.Dockerfile
├── test_restrict_cpus.sh
├── test_run_as.Dockerfile
├── test_docker_policy_output.sh
├── test_docker_policy_local_image.sh
├── test_flake8.py
├── test_copyright.py
├── test_pep257.py
├── test_docker_policy_bad_image.sh
├── launch_ros_sandbox
│ ├── descriptions
│ │ ├── test_user.py
│ │ ├── test_user_policy.py
│ │ └── test_docker_policy.py
│ └── actions
│ │ └── test_sandboxed_node_container.py
├── test_mypy.py
├── test_docker_policy.sh
└── test_docker_policy_run_args.sh
├── NOTICE
├── setup.cfg
├── docs
├── source
│ ├── modules.rst
│ ├── launch_ros_sandbox.rst
│ ├── actions.rst
│ ├── descriptions.rst
│ ├── index.md
│ └── conf.py
├── requirements.txt
├── Makefile
├── make.bat
└── sphinx_build_symlink
│ └── setup.py
├── .vale.ini
├── launch_ros_sandbox.repos
├── launch_ros_sandbox.dashing.repos
├── .github
├── dependabot.yml
├── PULL_REQUEST_TEMPLATE.md
├── CODEOWNERS
└── workflows
│ ├── lint.yml
│ ├── autoapprove.yml
│ ├── automerge.yml
│ └── test.yml
├── CODE_OF_CONDUCT.md
├── CHANGELOG.rst
├── .readthedocs.yml
├── launch_ros_sandbox
├── actions
│ ├── __init__.py
│ ├── load_runas_nodes.py
│ ├── sandboxed_node_container.py
│ └── load_docker_nodes.py
├── __init__.py
└── descriptions
│ ├── __init__.py
│ ├── user.py
│ ├── policy.py
│ ├── user_policy.py
│ ├── sandboxed_node.py
│ └── docker_policy.py
├── package.xml
├── setup.py
├── examples
├── run_as.py
├── demo_nodes_run_as.py
├── bad_image.launch.py
├── minimal_sandboxed_node_container.launch.py
├── local_image.launch.py
├── minimal_sandboxed_run_as.launch.py
├── minimal_sandbox_docker.launch.py
├── talker_listener_sandbox_docker.launch.py
├── mem_limit_sandbox_docker.launch.py
└── cpu_limit_sandbox_docker.launch.py
├── .gitignore
├── CONTRIBUTING.md
├── README.md
└── LICENSE
/resource/launch_ros_sandbox:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | fixes:
2 | - "ros2_ws/src/launch_ros_sandbox/::"
3 |
--------------------------------------------------------------------------------
/test/config/mypy.ini:
--------------------------------------------------------------------------------
1 | [mypy]
2 | ignore_missing_imports = True
3 |
--------------------------------------------------------------------------------
/test/dummy-dashing.Dockerfile:
--------------------------------------------------------------------------------
1 | FROM osrf/ros:dashing-desktop
2 |
3 | RUN touch dummy
4 |
--------------------------------------------------------------------------------
/NOTICE:
--------------------------------------------------------------------------------
1 | launch_ros_sandbox
2 | Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [develop]
2 | script-dir=$base/lib/launch_ros_sandbox
3 | [install]
4 | install-scripts=$base/lib/launch_ros_sandbox
5 |
--------------------------------------------------------------------------------
/docs/source/modules.rst:
--------------------------------------------------------------------------------
1 | launch_ros_sandbox
2 | ==================
3 |
4 | .. toctree::
5 | :maxdepth: 4
6 |
7 | launch_ros_sandbox
8 |
--------------------------------------------------------------------------------
/.vale.ini:
--------------------------------------------------------------------------------
1 | StylesPath = styles
2 | MinAlertLevel = suggestion
3 |
4 | [*.md]
5 | BasedOnStyles = Google, Vale, write-good
6 |
7 | Vale.Spelling = YES
8 |
--------------------------------------------------------------------------------
/launch_ros_sandbox.repos:
--------------------------------------------------------------------------------
1 | repositories:
2 | launch_ros_sandbox:
3 | type: git
4 | url: https://github.com/ros-security/launch_ros_sandbox.git
5 | version: master
6 |
--------------------------------------------------------------------------------
/launch_ros_sandbox.dashing.repos:
--------------------------------------------------------------------------------
1 | repositories:
2 | launch_ros_sandbox:
3 | type: git
4 | url: https://github.com/ros-security/launch_ros_sandbox.git
5 | version: dashing-devel
6 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: github-actions
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | time: "16:00"
8 | open-pull-requests-limit: 10
9 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | *Issue #, if available:*
2 |
3 | *Description of changes:*
4 |
5 |
6 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
7 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # Reviewers (and by extension approvers) will be the default owners for everything in the repo.
2 | # Unless a later match takes precedence, @reviewers will be requested for review when someone opens a pull request.
3 | * @ros-tooling/reviewers
4 |
--------------------------------------------------------------------------------
/test/test_restrict_cpus.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Launch a container with container name 'sandboxed-listener-node'
4 |
5 | # Retrieve the available CPUs for the Docker container's cgroup
6 | docker exec sandboxed-listener-node cat /sys/fs/cgroup/cpuset/cpuset.cpus
7 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | ## Code of Conduct
2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
4 | opensource-codeofconduct@amazon.com with any additional questions or comments.
5 |
--------------------------------------------------------------------------------
/CHANGELOG.rst:
--------------------------------------------------------------------------------
1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2 | Changelog for package launch_ros_sandbox
3 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
4 |
5 | 0.0.1 (2019-09-13)
6 | ------------------
7 | * Initial release. Provide Sandboxing functionality launch actions
8 | * Contributors: Anas Abou Allaban, Devin Bonnie, Emerson Knapp, Zachary Michaels
9 |
--------------------------------------------------------------------------------
/docs/source/launch_ros_sandbox.rst:
--------------------------------------------------------------------------------
1 | launch\_ros\_sandbox package
2 | ============================
3 |
4 | Subpackages
5 | -----------
6 |
7 | .. toctree::
8 |
9 | actions
10 | descriptions
11 |
12 | Module contents
13 | ---------------
14 |
15 | .. automodule:: launch_ros_sandbox
16 | :members:
17 | :undoc-members:
18 | :show-inheritance:
19 |
--------------------------------------------------------------------------------
/docs/requirements.txt:
--------------------------------------------------------------------------------
1 | # Install ROS 2 dependencies using pip to help sphinx generate documentation API
2 | # This should *not* be used with the intent of actually running the software, colcon should
3 | # be used in this case.
4 | git+https://github.com/ament/ament_index.git#egg=ament_index_python&subdirectory=ament_index_python
5 | git+https://github.com/ros2/launch.git#egg=launch&subdirectory=launch
6 | sphinx-markdown-tables
7 |
--------------------------------------------------------------------------------
/.readthedocs.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | sphinx:
4 | configuration: docs/source/conf.py
5 |
6 | python:
7 | version: 3.6
8 | install:
9 | # Install requirements as system packages using pip
10 | - requirements: docs/requirements.txt
11 | # Ensure sphinx-build is in PATH
12 | - method: setuptools
13 | path: docs/sphinx_build_symlink/
14 | # Install launch_ros_sandbox using setuptools
15 | - method: setuptools
16 | path: .
17 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: Lint launch_ros_sandbox
2 | on:
3 | pull_request:
4 |
5 | jobs:
6 | ament_lint:
7 | runs-on: ubuntu-latest
8 | container:
9 | image: rostooling/setup-ros-docker:ubuntu-focal-ros-rolling-ros-base-latest
10 | strategy:
11 | fail-fast: false
12 | matrix:
13 | linter: [copyright, flake8, pep257]
14 | steps:
15 | - uses: actions/checkout@v2.4.0
16 | - uses: ros-tooling/action-ros-lint@0.1.3
17 | with:
18 | linter: ${{ matrix.linter }}
19 | package-name: launch_ros_sandbox
20 |
--------------------------------------------------------------------------------
/.github/workflows/autoapprove.yml:
--------------------------------------------------------------------------------
1 | name: Auto approve
2 |
3 | on:
4 | pull_request_target:
5 | types: [labeled]
6 |
7 | jobs:
8 | # Auto-approve dependabot PRs since this repo requires at least one approving review.
9 | # Dependabot will automatically merge minor version upgrades
10 | # (see .dependabot/config.yml for more info).
11 | auto-approve-dependabot:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: hmarr/auto-approve-action@v2.1.0
15 | if: github.actor == 'dependabot[bot]' && contains(github.event.pull_request.labels.*.name, 'dependencies')
16 | with:
17 | github-token: "${{ secrets.GITHUB_TOKEN }}"
18 |
--------------------------------------------------------------------------------
/test/test_run_as.Dockerfile:
--------------------------------------------------------------------------------
1 | FROM osrf/ros:dashing-desktop
2 |
3 | # copy the python package
4 | COPY . /opt/launch_ros_sandbox
5 |
6 | # set shell to bash
7 | SHELL ["/bin/bash", "-c"]
8 |
9 | # compile and install the python package (as root)
10 | RUN source /opt/ros/dashing/setup.bash && \
11 | cd /opt/launch_ros_sandbox && \
12 | python3 setup.py install --user
13 |
14 | # create sandboxed ros user and execute "./examples/run_as.py" with that user.
15 | # the build will fail if run_as does not work.
16 | RUN useradd -m dashing && \
17 | source /opt/ros/dashing/setup.bash && \
18 | cd /opt/launch_ros_sandbox && \
19 | ./examples/run_as.py dashing
20 |
21 | WORKDIR /opt/launch_ros_sandbox
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | SPHINXPROJ = launch_ros_sandbox
8 | SOURCEDIR = source
9 | BUILDDIR = build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
--------------------------------------------------------------------------------
/docs/source/actions.rst:
--------------------------------------------------------------------------------
1 | Actions
2 | =======
3 |
4 | Submodules
5 | ----------
6 |
7 | sandboxed\_node\_container module
8 | ----------------------------------------------------------------
9 |
10 | .. automodule:: launch_ros_sandbox.actions.sandboxed_node_container
11 | :members:
12 | :undoc-members:
13 | :show-inheritance:
14 |
15 | load\_docker\_nodes module
16 | ---------------------------------------------------------
17 |
18 | .. automodule:: launch_ros_sandbox.actions.load_docker_nodes
19 | :members:
20 | :undoc-members:
21 | :show-inheritance:
22 |
23 | load\_runas\_nodes module
24 | --------------------------------------------------------
25 |
26 | .. automodule:: launch_ros_sandbox.actions.load_runas_nodes
27 | :members:
28 | :undoc-members:
29 | :show-inheritance:
30 |
--------------------------------------------------------------------------------
/test/test_docker_policy_output.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # pull the image now so that we don't have to guess/query when its done in the script
4 | docker pull osrf/ros:dashing-desktop
5 |
6 | echo "Running './examples/talker_listener_sandbox_docker.launch.py' for 10 seconds"
7 |
8 | output=$(timeout -s INT 10s ./examples/talker_listener_sandbox_docker.launch.py)
9 |
10 | if [[ $output == *"[talker]: Publishing: 'Hello World:"* ]]
11 | then
12 | echo "Talker was heard!"
13 | else
14 | echo "Talker did not output to stdout in the allocated time period."
15 | exit 1
16 | fi
17 |
18 | if [[ $output == *"[listener]: I heard: [Hello World:"* ]]
19 | then
20 | echo "Listener was heard!"
21 | else
22 | echo "Listener did not output to stdout in the allocated time period."
23 | exit 2
24 | fi
25 |
26 | exit 0
27 |
--------------------------------------------------------------------------------
/test/test_docker_policy_local_image.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # pull the image now so that we don't have to guess/query when its done in the script
4 | docker build -t ros-dashing-dummy -f test/dummy-dashing.Dockerfile .
5 |
6 | echo "Running './examples/local_image.launch.py' for 10 seconds"
7 |
8 | output=$(timeout -s INT 10s ./examples/local_image.launch.py)
9 |
10 | if [[ $output == *"[talker]: Publishing: 'Hello World:"* ]]
11 | then
12 | echo "Talker was heard!"
13 | else
14 | echo "Talker did not output to stdout in the allocated time period."
15 | exit 1
16 | fi
17 |
18 | if [[ $output == *"[listener]: I heard: [Hello World:"* ]]
19 | then
20 | echo "Listener was heard!"
21 | else
22 | echo "Listener did not output to stdout in the allocated time period."
23 | exit 2
24 | fi
25 |
26 | docker rmi ros-dashing-dummy:latest
27 |
28 | exit 0
29 |
--------------------------------------------------------------------------------
/test/test_flake8.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017 Open Source Robotics Foundation, Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from ament_flake8.main import main
16 | import pytest
17 |
18 |
19 | @pytest.mark.flake8
20 | @pytest.mark.linter
21 | def test_flake8():
22 | rc = main(argv=[])
23 | assert rc == 0, 'Found errors'
24 |
--------------------------------------------------------------------------------
/test/test_copyright.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017 Open Source Robotics Foundation, Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from ament_copyright.main import main
16 | import pytest
17 |
18 |
19 | @pytest.mark.copyright
20 | @pytest.mark.linter
21 | def test_copyright():
22 | rc = main(argv=['.', 'test'])
23 | assert rc == 0, 'Found errors'
24 |
--------------------------------------------------------------------------------
/test/test_pep257.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017 Open Source Robotics Foundation, Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from ament_pep257.main import main
16 | import pytest
17 |
18 |
19 | @pytest.mark.linter
20 | @pytest.mark.pep257
21 | def test_pep257():
22 | rc = main(argv=[])
23 | assert rc == 0, 'Found code style errors / warnings'
24 |
--------------------------------------------------------------------------------
/launch_ros_sandbox/actions/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Package for launch_ros_sandbox actions."""
16 |
17 | from launch_ros_sandbox.actions.sandboxed_node_container import SandboxedNodeContainer
18 |
19 | __all__ = [
20 | 'SandboxedNodeContainer',
21 | ]
22 |
--------------------------------------------------------------------------------
/test/test_docker_policy_bad_image.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | echo "Running './examples/bad_image.launch.py'"
4 |
5 | output=$(timeout -s INT 10s ./examples/bad_image.launch.py)
6 |
7 | if [[ $? -ne 0 ]]
8 | then
9 | echo "The example script did not exit automatically."
10 | exit 3
11 | fi
12 |
13 | if [[ $output == *"[WARNING]"* && $output == *"could not be pulled but may be found locally"* ]]
14 | then
15 | echo "The bad image was not found on DockerHub"
16 | else
17 | echo "The bad image was found. Either someone created it on DockerHub and the test needs to be fixed or the the test failed."
18 | exit 1
19 | fi
20 |
21 | if [[ $output == *"[ERROR]"* && $output == *"could not be found"* ]]
22 | then
23 | echo "The bad image could not be ran as a container. The test passed."
24 | else
25 | echo "The bad image was able to run. Either it exists locally and the test needs to be fixed or the test failed."
26 | exit 2
27 | fi
28 |
29 | exit 0
30 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=source
11 | set BUILDDIR=build
12 | set SPHINXPROJ=launch_ros_sandbox
13 |
14 | if "%1" == "" goto help
15 |
16 | %SPHINXBUILD% >NUL 2>NUL
17 | if errorlevel 9009 (
18 | echo.
19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
20 | echo.installed, then set the SPHINXBUILD environment variable to point
21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
22 | echo.may add the Sphinx directory to PATH.
23 | echo.
24 | echo.If you don't have Sphinx installed, grab it from
25 | echo.http://sphinx-doc.org/
26 | exit /b 1
27 | )
28 |
29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
30 | goto end
31 |
32 | :help
33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
34 |
35 | :end
36 | popd
37 |
--------------------------------------------------------------------------------
/package.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | launch_ros_sandbox
5 | 0.1.0
6 | Extension to launch_ros to provide the ability to run nodes in sandboxed environments.
7 | ROS Security Working Group
8 | Apache 2.0
9 |
10 | python3-docker
11 | launch
12 | launch_ros
13 |
14 | ament_copyright
15 | ament_flake8
16 | ament_pep257
17 |
18 |
19 | python3-pytest
20 |
21 |
22 | ament_python
23 |
24 |
25 |
--------------------------------------------------------------------------------
/.github/workflows/automerge.yml:
--------------------------------------------------------------------------------
1 | name: Auto merge
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | master
7 | pull_request_review:
8 | types:
9 | - submitted
10 | check_suite:
11 | types:
12 | - completed
13 | status: {}
14 | jobs:
15 | # Automatically merge approved and green dependabot PRs.
16 | auto-merge-dependabot:
17 | runs-on: ubuntu-latest
18 | steps:
19 | - uses: pascalgn/automerge-action@v0.14.3
20 | if: github.actor == 'dependabot[bot]' || github.actor == 'dependabot-preview[bot]'
21 | env:
22 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
23 | MERGE_LABELS: "dependencies"
24 | MERGE_METHOD: "squash" # Sqush and merge
25 | MERGE_COMMIT_MESSAGE: "pull-request-title-and-description"
26 | MERGE_RETRY_SLEEP: "1200000" # Retry after 20m, enough time for check suites to run
27 | UPDATE_RETRIES: "6"
28 | UPDATE_METHOD: "rebase" # Rebase PR on base branch
29 | UPDATE_RETRY_SLEEP: "300000"
30 |
31 |
--------------------------------------------------------------------------------
/launch_ros_sandbox/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 |
16 | """
17 | Launch extension for running ROS nodes in a sandboxed environment.
18 |
19 | launch_ros_sandbox defines Launch actions to delegate the launch of nodes
20 | to a sandboxed environment.
21 | """
22 |
23 |
24 | from launch_ros_sandbox import actions
25 | from launch_ros_sandbox import descriptions
26 |
27 | __all__ = [
28 | 'actions',
29 | 'descriptions'
30 | ]
31 |
--------------------------------------------------------------------------------
/docs/source/descriptions.rst:
--------------------------------------------------------------------------------
1 | Descriptions
2 | ==========================================
3 |
4 | Submodules
5 | ----------
6 |
7 | docker\_policy module
8 | ---------------------------------------------------------
9 |
10 | .. automodule:: launch_ros_sandbox.descriptions.docker_policy
11 | :members:
12 | :undoc-members:
13 | :show-inheritance:
14 |
15 | policy module
16 | -------------------------------------------------
17 |
18 | .. automodule:: launch_ros_sandbox.descriptions.policy
19 | :members:
20 | :undoc-members:
21 | :show-inheritance:
22 |
23 | sandboxed\_node module
24 | ----------------------------------------------------------
25 |
26 | .. automodule:: launch_ros_sandbox.descriptions.sandboxed_node
27 | :members:
28 | :undoc-members:
29 | :show-inheritance:
30 |
31 | user module
32 | -----------------------------------------------
33 |
34 | .. automodule:: launch_ros_sandbox.descriptions.user
35 | :members:
36 | :undoc-members:
37 | :show-inheritance:
38 |
39 | user\_policy module
40 | -------------------------------------------------------
41 |
42 | .. automodule:: launch_ros_sandbox.descriptions.user_policy
43 | :members:
44 | :undoc-members:
45 | :show-inheritance:
46 |
--------------------------------------------------------------------------------
/launch_ros_sandbox/descriptions/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 |
16 | """Package of launch_ros_sandbox descriptions."""
17 |
18 | from launch_ros_sandbox.descriptions.docker_policy import DockerPolicy
19 | from launch_ros_sandbox.descriptions.policy import Policy
20 | from launch_ros_sandbox.descriptions.sandboxed_node import SandboxedNode
21 | from launch_ros_sandbox.descriptions.user import User
22 | from launch_ros_sandbox.descriptions.user_policy import UserPolicy
23 |
24 |
25 | __all__ = [
26 | 'DockerPolicy',
27 | 'Policy',
28 | 'SandboxedNode',
29 | 'User',
30 | 'UserPolicy',
31 | ]
32 |
--------------------------------------------------------------------------------
/test/launch_ros_sandbox/descriptions/test_user.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Tests for User."""
16 |
17 | import getpass
18 | import os
19 |
20 | import unittest
21 |
22 | from launch_ros_sandbox.descriptions import User
23 |
24 |
25 | class TestUser(unittest.TestCase):
26 |
27 | def test_get_user_from_username(self):
28 | """Verify User.from_username returns the correct User."""
29 | uid = os.getuid()
30 | gid = os.getgid()
31 | username = getpass.getuser()
32 | user = User.from_username(username)
33 |
34 | assert uid == user.uid
35 | assert gid == user.gid
36 |
--------------------------------------------------------------------------------
/test/launch_ros_sandbox/descriptions/test_user_policy.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Tests for the UserPolicy description."""
16 |
17 | import os
18 |
19 | import unittest
20 |
21 | from launch_ros_sandbox.descriptions import UserPolicy
22 |
23 |
24 | class TestUserPolicy(unittest.TestCase):
25 |
26 | def test_defaults_to_current_user(self):
27 | """Verify UserPolicy.run_as defaults to current user."""
28 | current_uid = os.getuid()
29 | current_gid = os.getgid()
30 |
31 | user_policy = UserPolicy()
32 | assert current_uid == user_policy.run_as.uid
33 | assert current_gid == user_policy.run_as.gid
34 |
--------------------------------------------------------------------------------
/test/test_mypy.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Canonical Ltd
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from pathlib import Path
16 |
17 | import pytest
18 |
19 |
20 | @pytest.mark.mypy
21 | @pytest.mark.linter
22 | def test_mypy():
23 | # TODO: when ament_mypy is officially released, remove this check and move import to the top
24 | # of the file. Until then, we still use it internally as developers.
25 | try:
26 | from ament_mypy.main import main
27 | config_path = Path(__file__).parent / 'config' / 'mypy.ini'
28 | print(config_path.resolve())
29 | rc = main(argv=['launch_ros_sandbox', '--config', str(config_path.resolve())])
30 | assert rc == 0, 'Found code style errors / warnings'
31 | except ImportError:
32 | pass
33 |
--------------------------------------------------------------------------------
/launch_ros_sandbox/actions/load_runas_nodes.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 |
16 | """
17 | Internal module for the LoadRunAsNodes Action.
18 |
19 | LoadRunAsNodes is an Action that controls the lifecycle of a sandboxed environment running nodes as
20 | a separate user. This Action is not exported and should only be used internally.
21 | """
22 |
23 | from launch import Action
24 |
25 |
26 | class LoadRunAsNodes(Action):
27 | """
28 | LoadRunAsNodes is an Action that controls the sandbox environment spawned by `UserPolicy`.
29 |
30 | LoadRunAsNodes should only be constructed by `UserPolicy.apply`.
31 | FIXME: Move the logic for launching the sandbox environment into `LoadRunAsNodes.execute`
32 | """
33 |
34 | pass
35 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 |
16 | """Package setup for launch_ros_sandbox."""
17 |
18 | from setuptools import find_packages
19 | from setuptools import setup
20 |
21 | package_name = 'launch_ros_sandbox'
22 |
23 | setup(
24 | name=package_name,
25 | version='0.1.0',
26 | packages=find_packages(exclude=['test']),
27 | data_files=[
28 | ('share/' + package_name, ['package.xml']),
29 | ('share/ament_index/resource_index/packages',
30 | ['resource/' + package_name]),
31 | ],
32 | install_requires=[
33 | 'setuptools',
34 | 'launch',
35 | 'docker',
36 | ],
37 | zip_safe=True,
38 | description='Sandbox extension to ROS 2 Launch.',
39 | license='Apache License, Version 2.0',
40 | tests_require=[
41 | 'pytest',
42 | ],
43 | )
44 |
--------------------------------------------------------------------------------
/launch_ros_sandbox/descriptions/user.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Module for User."""
16 |
17 | import pwd
18 |
19 |
20 | class User:
21 | """User is a pair of Unix UID and GID."""
22 |
23 | def __init__(
24 | self,
25 | *,
26 | uid: int,
27 | gid: int
28 | ) -> None:
29 | """Construct the User."""
30 | self._uid = uid
31 | self._gid = gid
32 |
33 | @classmethod
34 | def from_username(cls, username: str) -> 'User':
35 | """Get a User object from a username string."""
36 | user = pwd.getpwnam(username)
37 |
38 | return cls(
39 | uid=user.pw_uid,
40 | gid=user.pw_gid)
41 |
42 | @property
43 | def uid(self) -> int:
44 | """Get the User's user id."""
45 | return self._uid
46 |
47 | @property
48 | def gid(self) -> int:
49 | """Get the User's group id."""
50 | return self._gid
51 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test launch_ros_sandbox
2 | on:
3 | pull_request:
4 | push:
5 | branches:
6 | - master
7 | schedule:
8 | # Run every hour, to help detect flakiness and
9 | # broken external dependencies.
10 | - cron: '0 * * * *'
11 |
12 | jobs:
13 | build_and_test_macOS:
14 | runs-on: macOS-latest
15 | steps:
16 | - uses: ros-tooling/setup-ros@0.2.1
17 | - uses: ros-tooling/action-ros-ci@v0.2
18 | with:
19 | package-name: launch_ros_sandbox
20 | target-ros2-distro: rolling
21 | vcs-repo-file-url: https://raw.githubusercontent.com/ros2/ros2/master/ros2.repos
22 | - uses: actions/upload-artifact@v2.3.1
23 | with:
24 | name: colcon-logs-macOS
25 | path: ros_ws/log
26 |
27 | build_and_test_ubuntu:
28 | runs-on: ubuntu-latest
29 | container:
30 | image: rostooling/setup-ros-docker:ubuntu-focal-latest
31 | steps:
32 | - uses: ros-tooling/action-ros-ci@v0.2
33 | with:
34 | package-name: launch_ros_sandbox
35 | target-ros2-distro: rolling
36 | - uses: codecov/codecov-action@v2.1.0
37 | with:
38 | token: ${{ secrets.CODECOV_TOKEN }}
39 | file: ros_ws/src/launch_ros_sandbox/coverage.xml
40 | flags: unittests
41 | name: codecov-umbrella
42 | # codecov sometimes fail to upload the report, this
43 | # leads to flaky build failures.
44 | # In the future, it may be interesting to have a separate
45 | # job for coverage reports with a higher tolerance for failure
46 | # and/or add retry logic to the action.
47 | fail_ci_if_error: false
48 | - uses: actions/upload-artifact@v2.3.1
49 | with:
50 | name: colcon-logs-ubuntu
51 | path: ros_ws/log
52 |
--------------------------------------------------------------------------------
/docs/sphinx_build_symlink/setup.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 |
16 | """
17 | Looks for sphinx-build in lib/ and creates a symlink in bin/.
18 |
19 | This workaround mitigates an issue in readthedocs with sphinx where the
20 | sphinx-build binary is installed in a directory which is not listed in PATH,
21 | causing the documentation generation to fail (failed to open sphinx-build).
22 | """
23 |
24 | import os
25 | import pathlib
26 |
27 | from setuptools import setup
28 |
29 |
30 | sphinx_build = next(pathlib.Path('/').glob('**/sphinx-build'))
31 |
32 | # Looking for the venv bin directory. The home directory is
33 | # chosen as a starting point, because it excludes system bin
34 | # packages, and the venv is a subdirectory of the home dir.
35 | bin_directory = next(pathlib.Path.home().glob('**/bin'))
36 |
37 | try:
38 | os.symlink(sphinx_build, bin_directory / 'sphinx-build')
39 | except FileExistsError:
40 | # readthedocs re-use venvs acrosss multiple builds, which means that,
41 | # sometimes, the sphinx-build will have been previously created.
42 | pass
43 |
44 | package_name = 'sphinx_build_symlink'
45 |
46 | setup(
47 | name=package_name,
48 | version='0.1.0',
49 | )
50 |
--------------------------------------------------------------------------------
/test/test_docker_policy.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # This test must be run from the root directory of the package
4 |
5 | RED='\033[0;31m'
6 | GREEN='\033[0;32m'
7 | NORM='\033[0m'
8 |
9 | # pull the image now so that we don't have to guess/query when its done in the script
10 | docker pull osrf/ros:dashing-desktop
11 |
12 | # run the example; loads listener in Docker
13 | ./examples/minimal_sandbox_docker.launch.py&
14 | task_id=$!
15 |
16 | echo "TaskID: $task_id"
17 |
18 | echo "Checking if sandboxed-listener-node is running..."
19 | docker inspect -f "{{.State.Running}}" sandboxed-listener-node
20 | is_running=$?
21 |
22 | # Wait until the docker container is running. docker inspect will return 0 when it does.
23 | while [[ $is_running -ne 0 ]]
24 | do
25 | sleep 1
26 | docker inspect -f "{{.State.Running}}" sandboxed-listener-node
27 | is_running=$?
28 | done
29 |
30 | # Run listener in the container spun up by DockerPolicy for 5 seconds
31 | echo "Executing listener in Docker container..."
32 | timeout 5 docker exec -t sandboxed-listener-node /ros_entrypoint.sh ros2 run demo_nodes_cpp listener
33 | echo "Stopping Docker container..."
34 |
35 | # Note: these sleep commands are here just so the following command executes after stdout appears on
36 | # the terminal.
37 | kill -INT $task_id
38 | sleep 2
39 |
40 | # SIGTERM is required also if launch is ran in the background, this might be a bug in launch.
41 | kill $task_id
42 | sleep 2
43 |
44 | echo "Checking if sandboxed-listener-node is running..."
45 | docker inspect -f "{{.State.Running}}" sandboxed-listener-node
46 |
47 | # Check the exit code of docker inspect. 0 is returned only if it is running.
48 | # This check will set the exit code to 0 only if the exit code is not 0.
49 | if [[ $? -ne 0 ]]; then
50 | printf "%b%s%b\n" "$GREEN" "PASS" "$NORM"
51 | exit 0
52 | else
53 | printf "%b%s%b\n" "$RED" "FAIL" "$NORM"
54 | exit 1
55 | fi
56 |
--------------------------------------------------------------------------------
/examples/run_as.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 |
17 | """
18 | Reference code for spawning a process ran as a different user.
19 |
20 | This script creates a User object from the supplied username and runs a subprocess as that user.
21 | The script must be ran as root.
22 | The user must also exist.
23 |
24 | Usage: ./run_as ros2_user
25 | Expected output: ros2_user
26 | """
27 |
28 | import os
29 | import pwd
30 | import subprocess
31 | import sys
32 |
33 | from launch_ros_sandbox.descriptions import User
34 |
35 |
36 | def run_as_user(user: User) -> None:
37 | """Parse User object and run 'whoami' as that user."""
38 | pw_record = pwd.getpwuid(user.uid)
39 |
40 | env = os.environ.copy()
41 | env['HOME'] = pw_record.pw_dir
42 | env['LOGNAME'] = pw_record.pw_name
43 | env['USER'] = pw_record.pw_name
44 |
45 | def set_user():
46 | """Set the current user."""
47 | os.setgid(user.gid)
48 | os.setuid(user.uid)
49 |
50 | # This should probably use asyncio, since ExecuteNode uses it internally.
51 | # asyncio uses Popen internally, so this feature should work on it.
52 |
53 | process = subprocess.Popen(
54 | ['whoami'],
55 | preexec_fn=set_user,
56 | env=env
57 | )
58 |
59 | assert 0 == process.wait()
60 |
61 |
62 | if __name__ == '__main__':
63 | if os.getgid() != 0 or os.getuid() != 0:
64 | raise Exception('Script must be run as root!')
65 |
66 | username = sys.argv[1]
67 | run_as_user(User.from_username(username))
68 |
--------------------------------------------------------------------------------
/examples/demo_nodes_run_as.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 |
17 | """
18 | Reference code for spawning a process ran as a different user.
19 |
20 | This script creates a User object from the supplied username and runs a subprocess as that user.
21 | The script must be ran as root.
22 | The user must also exist.
23 |
24 | Usage: ./run_as ros2_user
25 | """
26 |
27 | import os
28 | import pwd
29 | import subprocess
30 | import sys
31 |
32 | from launch_ros_sandbox.descriptions import User
33 |
34 |
35 | def run_as_user(user: User) -> None:
36 | """Parse User object and run 'whoami' as that user."""
37 | pw_record = pwd.getpwuid(user.uid)
38 |
39 | env = os.environ.copy()
40 | env['HOME'] = pw_record.pw_dir
41 | env['LOGNAME'] = pw_record.pw_name
42 | env['USER'] = pw_record.pw_name
43 |
44 | def set_user():
45 | """Set the current user."""
46 | os.setgid(user.gid)
47 | os.setuid(user.uid)
48 |
49 | # This should probably use asyncio, since ExecuteNode uses it internally.
50 | # asyncio uses Popen internally, so this feature should work on it.
51 |
52 | process = subprocess.Popen(
53 | ['ros2', 'launch', 'demo_nodes_cpp', 'talker_listener.launch.py'],
54 | preexec_fn=set_user,
55 | env=env
56 | )
57 |
58 | assert 0 == process.wait()
59 |
60 |
61 | if __name__ == '__main__':
62 | if os.getgid() != 0 or os.getuid() != 0:
63 | raise Exception('Script must be run as root!')
64 |
65 | username = sys.argv[1]
66 | run_as_user(User.from_username(username))
67 |
--------------------------------------------------------------------------------
/test/test_docker_policy_run_args.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # This test must be run from the root directory of the package
4 |
5 | RED='\033[0;31m'
6 | GREEN='\033[0;32m'
7 | NORM='\033[0m'
8 |
9 | # pull the image now so that we don't have to guess/query when its done in the script
10 | docker pull osrf/ros:dashing-desktop
11 |
12 | # run the example; loads listener in Docker
13 | ./examples/mem_limit_sandbox_docker.launch.py&
14 | task_id=$!
15 |
16 | echo "TaskID: $task_id"
17 |
18 | echo "Checking if sandboxed-listener-node is running..."
19 | docker inspect -f "{{.State.Running}}" sandboxed-listener-node
20 | is_running=$?
21 |
22 | # Wait until the docker container is running. docker inspect will return 0 when it does.
23 | while [[ $is_running -ne 0 ]]
24 | do
25 | sleep 1
26 | docker inspect -f "{{.State.Running}}" sandboxed-listener-node
27 | is_running=$?
28 | done
29 |
30 | # Run listener in the container spun up by DockerPolicy for 5 seconds
31 | echo "Checking memory limits in Docker container..."
32 | memory=$(docker inspect sandboxed-listener-node | jq '.[0].HostConfig.Memory')
33 | expected_memory_128m="134217728";
34 | if [[ "$memory" -eq $expected_memory_128m ]]; then
35 | printf "%bPASS: Memory limits correctly set to 128m!%b\n" "$GREEN" "$NORM";
36 | result=0
37 | else
38 | printf "%bFAIL: Memory limits not correctly set to 128m!%b\n" "$RED" "$NORM";
39 | result=1
40 | fi
41 |
42 | echo "Stopping Docker container..."
43 |
44 | # Note: these sleep commands are here just so the following command executes after stdout appears on
45 | # the terminal.
46 | kill -INT $task_id
47 | sleep 2
48 |
49 | # SIGTERM is required also if launch is ran in the background, this might be a bug in launch.
50 | kill $task_id
51 | sleep 2
52 |
53 | echo "Checking if sandboxed-listener-node is running..."
54 | docker inspect -f "{{.State.Running}}" sandboxed-listener-node
55 |
56 | # Check the exit code of docker inspect. 0 is returned only if it is running.
57 | # This check will set the exit code to 0 only if the Docker container is not running and the memory check passed.
58 | if [[ $? -ne 0 && result -eq 0 ]]; then
59 | printf "%b%s%b\n" "$GREEN" "PASS" "$NORM"
60 | exit 0
61 | else
62 | printf "%b%s%b\n" "$RED" "FAIL" "$NORM"
63 | exit 1
64 | fi
--------------------------------------------------------------------------------
/examples/bad_image.launch.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 |
17 | """
18 | Minimal example tries to run an image that doesn't exist locally or on DockerHub.
19 |
20 | This example tries to run a ROS 2 node in a Docker container that should not exist locally nor on
21 | DockerHub. The expected behavior is that launch logs to warn that the Image is not found and then
22 | logs to error that it is also not found.
23 | """
24 | import sys
25 |
26 | from launch import LaunchDescription, LaunchService
27 |
28 | from launch_ros_sandbox.actions import SandboxedNodeContainer
29 | from launch_ros_sandbox.descriptions import DockerPolicy
30 | from launch_ros_sandbox.descriptions import SandboxedNode
31 |
32 |
33 | def generate_launch_description():
34 | """
35 | Create launch description for starting a SandboxedNodeContainer with a bad image name.
36 |
37 | A SandboxedNode must be loaded into the container for any work to be done.
38 | """
39 | ld = LaunchDescription()
40 | ld.add_action(
41 | SandboxedNodeContainer(
42 | sandbox_name='my_sandbox',
43 | policy=DockerPolicy(
44 | tag='not-exist',
45 | repository='definitely-not-a-real-image',
46 | ),
47 | node_descriptions=[
48 | SandboxedNode(
49 | package='demo_nodes_cpp',
50 | node_executable='talker',
51 | ),
52 | ]
53 | )
54 | )
55 |
56 | return ld
57 |
58 |
59 | if __name__ == '__main__':
60 | ls = LaunchService(argv=sys.argv[1:], debug=True)
61 | ls.include_launch_description(generate_launch_description())
62 | sys.exit(ls.run())
63 |
--------------------------------------------------------------------------------
/launch_ros_sandbox/descriptions/policy.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """
16 | Module for Policy description.
17 |
18 | Policy represents a security policy that can be applied to a SandboxedNodeContainer. It provides a
19 | single method `apply` which performs the sandboxing on the ROS 2 nodes described by
20 | SandboxedNodeContainer. The `apply` method must be implemented by any security policy that extends
21 | Policy.
22 |
23 | """
24 |
25 | from abc import ABC
26 | from abc import abstractmethod
27 | from typing import List
28 |
29 | from launch import Action
30 | from launch import LaunchContext
31 |
32 | from launch_ros_sandbox.descriptions.sandboxed_node import SandboxedNode
33 |
34 |
35 | class Policy(ABC):
36 | """Policy is the base class used by any sandboxing Policy description."""
37 |
38 | @abstractmethod
39 | def apply(
40 | self,
41 | context: LaunchContext,
42 | node_descriptions: List[SandboxedNode]
43 | ) -> Action:
44 | """
45 | Apply the sandboxing policy and returns an Action for controlling the environment.
46 |
47 | This method is called by `SandboxedNodeContainer.execute` when the SandboxedNodeContainer
48 | Action is visited by the LaunchService. This function returns a single Action which will
49 | launch the nodes inside the sandboxed environment. LaunchService monitors the lifecycle of
50 | this Action.
51 |
52 | :param: context is the LaunchContext. This is forwarded by SandboxedNodeContainer and is
53 | used to resolve any substitutions.
54 | :param: node_descriptions is the List of ROS 2 nodes to run within the sandboxed
55 | environment. This policy should handle launching each of these nodes within the sandbox.
56 | """
57 | pass
58 |
--------------------------------------------------------------------------------
/examples/minimal_sandboxed_node_container.launch.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 |
17 |
18 | """
19 | Minimal example for using SandboxedNodeContainer.
20 |
21 | This example runs a ROS 2 node in a sandboxed environment by invoking SandboxedNodeContainer
22 | action. SandboxedNodeContainer delegates the launch parameters to an instance of launch_ros that is
23 | running in a sandboxed environment using the requested sandboxing policy.
24 | """
25 |
26 | import sys
27 |
28 | import launch
29 |
30 | import launch_ros_sandbox
31 |
32 |
33 | def generate_launch_description() -> launch.LaunchDescription:
34 | """
35 | Create launch description for starting SandboxedNodeContainer.
36 |
37 | Two nodes are loaded into the sandboxed container: talker and listener.
38 | No operation is performed since no sandboxing policy was defined.
39 | """
40 | ld = launch.LaunchDescription()
41 |
42 | ld.add_action(
43 | launch_ros_sandbox.actions.SandboxedNodeContainer(
44 | sandbox_name='my_sandbox',
45 | node_descriptions=[
46 | launch_ros_sandbox.descriptions.SandboxedNode(
47 | package='demo_nodes_cpp',
48 | node_executable='talker',
49 | ),
50 | launch_ros_sandbox.descriptions.SandboxedNode(
51 | package='demo_nodes_cpp',
52 | node_executable='listener'
53 | )
54 | ]
55 | )
56 | )
57 |
58 | return ld
59 |
60 |
61 | if __name__ == '__main__':
62 | """Starts the SandboxedNodeContainer example as a script."""
63 |
64 | ls = launch.LaunchService(argv=sys.argv[1:])
65 | ls.include_launch_description(generate_launch_description())
66 | sys.exit(ls.run())
67 |
--------------------------------------------------------------------------------
/examples/local_image.launch.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 |
17 | """
18 | Minimal example for demoing when an image found locally is ran in DockerPolicy.
19 |
20 | This example tries to run a ROS 2 node in a Docker container that exists only locally.
21 | The expected behavior is that launch logs to warn that the Image is not found and then runs the
22 | local image.
23 | """
24 | import sys
25 |
26 | from launch import LaunchDescription, LaunchService
27 |
28 | from launch_ros_sandbox.actions import SandboxedNodeContainer
29 | from launch_ros_sandbox.descriptions import DockerPolicy
30 | from launch_ros_sandbox.descriptions import SandboxedNode
31 |
32 |
33 | def generate_launch_description():
34 | """
35 | Create launch description for starting a SandboxedNodeContainer with a local image.
36 |
37 | A SandboxedNode must be loaded into the container for any work to be done.
38 | """
39 | ld = LaunchDescription()
40 | ld.add_action(
41 | SandboxedNodeContainer(
42 | sandbox_name='my_sandbox',
43 | policy=DockerPolicy(
44 | tag='latest',
45 | repository='ros-dashing-dummy',
46 | entrypoint='/ros_entrypoint.sh'
47 | ),
48 | node_descriptions=[
49 | SandboxedNode(
50 | package='demo_nodes_cpp',
51 | node_executable='talker',
52 | ),
53 | SandboxedNode(
54 | package='demo_nodes_cpp',
55 | node_executable='listener'
56 | ),
57 | ]
58 | )
59 | )
60 |
61 | return ld
62 |
63 |
64 | if __name__ == '__main__':
65 | ls = LaunchService(argv=sys.argv[1:], debug=True)
66 | ls.include_launch_description(generate_launch_description())
67 | sys.exit(ls.run())
68 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/python
3 | # Edit at https://www.gitignore.io/?templates=python
4 |
5 | ### Python ###
6 | # Byte-compiled / optimized / DLL files
7 | __pycache__/
8 | *.py[cod]
9 | *$py.class
10 |
11 | # JetBrains
12 | .idea/
13 |
14 | # C extensions
15 | *.so
16 |
17 | # Distribution / packaging
18 | .Python
19 | build/
20 | develop-eggs/
21 | dist/
22 | downloads/
23 | eggs/
24 | .eggs/
25 | lib/
26 | lib64/
27 | parts/
28 | sdist/
29 | var/
30 | wheels/
31 | pip-wheel-metadata/
32 | share/python-wheels/
33 | *.egg-info/
34 | .installed.cfg
35 | *.egg
36 | MANIFEST
37 |
38 | # PyInstaller
39 | # Usually these files are written by a python script from a template
40 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
41 | *.manifest
42 | *.spec
43 |
44 | # Installer logs
45 | pip-log.txt
46 | pip-delete-this-directory.txt
47 |
48 | # Unit test / coverage reports
49 | htmlcov/
50 | .tox/
51 | .nox/
52 | .coverage
53 | .coverage.*
54 | .cache
55 | nosetests.xml
56 | coverage.xml
57 | *.cover
58 | .hypothesis/
59 | .pytest_cache/
60 |
61 | # Translations
62 | *.mo
63 | *.pot
64 |
65 | # Django stuff:
66 | *.log
67 | local_settings.py
68 | db.sqlite3
69 | db.sqlite3-journal
70 |
71 | # Flask stuff:
72 | instance/
73 | .webassets-cache
74 |
75 | # Scrapy stuff:
76 | .scrapy
77 |
78 | # Sphinx documentation
79 | docs/_build/
80 |
81 | # PyBuilder
82 | target/
83 |
84 | # Jupyter Notebook
85 | .ipynb_checkpoints
86 |
87 | # IPython
88 | profile_default/
89 | ipython_config.py
90 |
91 | # pyenv
92 | .python-version
93 |
94 | # pipenv
95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
98 | # install all needed dependencies.
99 | #Pipfile.lock
100 |
101 | # celery beat schedule file
102 | celerybeat-schedule
103 |
104 | # SageMath parsed files
105 | *.sage.py
106 |
107 | # Environments
108 | .env
109 | .venv
110 | env/
111 | venv/
112 | ENV/
113 | env.bak/
114 | venv.bak/
115 |
116 | # Spyder project settings
117 | .spyderproject
118 | .spyproject
119 |
120 | # Rope project settings
121 | .ropeproject
122 |
123 | # mkdocs documentation
124 | /site
125 |
126 | # mypy
127 | .mypy_cache/
128 | .dmypy.json
129 | dmypy.json
130 |
131 | # Pyre type checker
132 | .pyre/
133 |
134 | # End of https://www.gitignore.io/api/python
135 |
136 | .flake8
137 | .DS_Store
138 | .vscode
139 |
--------------------------------------------------------------------------------
/examples/minimal_sandboxed_run_as.launch.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 |
17 |
18 | """
19 | Minimal example for using SandboxedNodeContainer.
20 |
21 | This example runs a ROS 2 node in a sandboxed environment by invoking SandboxedNodeContainer
22 | action. SandboxedNodeContainer delegates the launch parameters to an instance of launch_ros that is
23 | running in a sandboxed environment using the requested sandboxing policy.
24 | """
25 |
26 | import sys
27 |
28 | import launch
29 |
30 | import launch_ros_sandbox
31 | from launch_ros_sandbox.descriptions import User
32 | from launch_ros_sandbox.descriptions import UserPolicy
33 |
34 |
35 | def generate_launch_description() -> launch.LaunchDescription:
36 | """
37 | Create launch description for starting SandboxedNodeContainer.
38 |
39 | Two nodes are loaded into the sandboxed container: talker and listener. No operation is
40 | performed since no sandboxing policy was defined.
41 | """
42 | ld = launch.LaunchDescription()
43 |
44 | ld.add_action(
45 | launch_ros_sandbox.actions.SandboxedNodeContainer(
46 | sandbox_name='my_sandbox',
47 | policy=UserPolicy(
48 | run_as=User.from_username('dashing'),
49 | ),
50 | node_descriptions=[
51 | launch_ros_sandbox.descriptions.SandboxedNode(
52 | package='demo_nodes_cpp',
53 | node_executable='talker',
54 | ),
55 | launch_ros_sandbox.descriptions.SandboxedNode(
56 | package='demo_nodes_cpp',
57 | node_executable='listener'
58 | )
59 | ]
60 | )
61 | )
62 |
63 | return ld
64 |
65 |
66 | if __name__ == '__main__':
67 | """Starts the SandboxedNodeContainer example as a script."""
68 |
69 | ls = launch.LaunchService(
70 | argv=sys.argv[1:],
71 | debug=True
72 | )
73 | ls.include_launch_description(generate_launch_description())
74 | sys.exit(ls.run())
75 |
--------------------------------------------------------------------------------
/test/launch_ros_sandbox/actions/test_sandboxed_node_container.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Tests for the SandboxedNodeContainer action."""
16 |
17 | import unittest.mock
18 |
19 | from launch import LaunchDescription
20 | from launch import LaunchService
21 |
22 | from launch_ros_sandbox.actions import SandboxedNodeContainer
23 | from launch_ros_sandbox.descriptions import SandboxedNode
24 |
25 |
26 | class TestSandboxedNodeContainer(unittest.TestCase):
27 |
28 | def _assert_launch_no_errors(self, actions):
29 | ld = LaunchDescription(actions)
30 | ls = LaunchService()
31 | ls.include_launch_description(ld)
32 | assert 0 == ls.run()
33 |
34 | def _assert_launch_errors(self, actions):
35 | ld = LaunchDescription(actions)
36 | ls = LaunchService()
37 | ls.include_launch_description(ld)
38 | assert 0 != ls.run()
39 |
40 | def test_launch_nodes(self):
41 | """Test launching a node."""
42 | node_action = SandboxedNodeContainer(
43 | sandbox_name='my_sandbox',
44 | node_descriptions=[
45 | SandboxedNode(
46 | package='demo_nodes_cpp',
47 | node_executable='talker',
48 | ),
49 | SandboxedNode(
50 | package='demo_nodes_cpp',
51 | node_executable='listener',
52 | ),
53 | ],
54 | )
55 | self._assert_launch_no_errors([node_action])
56 |
57 | def test_launch_empty_nodes(self):
58 | """Test launching SandboxedNodeContainer without child nodes."""
59 | node_action = SandboxedNodeContainer(
60 | sandbox_name='my_sandbox',
61 | )
62 | self._assert_launch_no_errors([node_action])
63 |
64 | @unittest.mock.patch('launch_ros_sandbox.descriptions.DockerPolicy')
65 | def test_launch_docker_policy(self, mock_docker_policy) -> None:
66 | """Test launching SandboxedNodeContainer with DockerPolicy."""
67 | node_action = SandboxedNodeContainer(
68 | sandbox_name='my_sandbox',
69 | policy=mock_docker_policy,
70 | node_descriptions=[
71 | SandboxedNode(
72 | package='demo_nodes_cpp',
73 | node_executable='talker',
74 | ),
75 | SandboxedNode(
76 | package='demo_nodes_cpp',
77 | node_executable='listener'
78 | ),
79 | ],
80 | )
81 |
82 | self._assert_launch_no_errors([node_action])
83 | mock_docker_policy.apply.assert_called()
84 |
--------------------------------------------------------------------------------
/launch_ros_sandbox/actions/sandboxed_node_container.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 |
16 | """Module for SandboxedNodeContainer class."""
17 |
18 | from typing import List
19 | from typing import Optional
20 |
21 | from launch import Action
22 | from launch import LaunchContext
23 | from launch.some_substitutions_type import SomeSubstitutionsType
24 | from launch.substitution import Substitution
25 | from launch.utilities import normalize_to_list_of_substitutions
26 |
27 | from launch_ros_sandbox.descriptions import Policy
28 | from launch_ros_sandbox.descriptions import SandboxedNode
29 |
30 |
31 | class SandboxedNodeContainer(Action):
32 | """SandboxedNodeContainer is an action that launches nodes within a sandboxed environment."""
33 |
34 | def __init__(
35 | self,
36 | *,
37 | sandbox_name: Optional[SomeSubstitutionsType] = None,
38 | policy: Optional[Policy] = None,
39 | node_descriptions: Optional[List[SandboxedNode]] = None,
40 | **kwargs
41 | ) -> None:
42 | """
43 | Initialize the SandboxedNodeContainer.
44 |
45 | :param: sandbox_name is an optional name assigned to the sandbox environment.
46 | :param: policy defines the sandboxing strategy used by the sandbox environment.
47 | :param: node_descriptions are the list of nodes to launch inside the sandbox environment.
48 | """
49 | super().__init__(**kwargs)
50 |
51 | self.__sandbox_name = None
52 | if sandbox_name is not None:
53 | self.__sandbox_name = normalize_to_list_of_substitutions(
54 | sandbox_name
55 | )
56 |
57 | self.__node_descriptions = None
58 | if node_descriptions is not None:
59 | self.__node_descriptions = node_descriptions
60 |
61 | self.__policy = policy
62 |
63 | def execute(
64 | self,
65 | context: LaunchContext
66 | ) -> Optional[List[Action]]:
67 | """
68 | Execute the SandboxedNodeContainer.
69 |
70 | All node descriptions defined will be launched inside the sandbox defined by the policy.
71 | """
72 | if self.__node_descriptions is None:
73 | return None
74 |
75 | if self.__policy is not None:
76 | sandboxing_action = self.__policy.apply(
77 | context=context,
78 | node_descriptions=self.__node_descriptions
79 | )
80 |
81 | if sandboxing_action is not None:
82 | return [sandboxing_action]
83 |
84 | return None
85 |
86 | @property
87 | def sandbox_name(self) -> Optional[List[Substitution]]:
88 | """Get sandbox name as a sequence of substitutions to be performed."""
89 | return self.__sandbox_name
90 |
--------------------------------------------------------------------------------
/examples/minimal_sandbox_docker.launch.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 |
17 |
18 | """
19 | Minimal example for using SandboxedNodeContainer with DockerPolicy.
20 |
21 | This example runs a ROS 2 node in a sandboxed environment by invoking SandboxedNodeContainer
22 | action. SandboxedNodeContainer delegates the launch parameters to an instance of launch_ros that is
23 | running in a Docker container.
24 |
25 | Currently, this test will only launch the Talker demo node inside Docker. This node can be observed
26 | by launching a Listener node either within the same docker container or on the host machine. The
27 | Docker container must be stopped externally in order to free resources.
28 |
29 | "container_id" should be substituted for the container name logged by launch. It can also be found
30 | by running "docker container ls".
31 |
32 | How to stop the Docker container:
33 | - docker stop $container_id
34 |
35 | How to run listener inside the Docker container
36 | - docker exec -it $container_id /bin/bash
37 | - source /ros_entrypoint.sh
38 | - ros2 run demo_nodes_cpp listener
39 | """
40 |
41 | import sys
42 |
43 | from launch import LaunchDescription
44 | from launch import LaunchService
45 |
46 | from launch_ros_sandbox.actions import SandboxedNodeContainer
47 | from launch_ros_sandbox.descriptions import DockerPolicy
48 | from launch_ros_sandbox.descriptions import SandboxedNode
49 |
50 |
51 | def generate_launch_description() -> LaunchDescription:
52 | """
53 | Create launch description for starting SandboxedNodeContainer with DockerPolicy.
54 |
55 | Talker is loaded inside the SandboxedNodeContainer.
56 | When the sandboxed node is executed, it runs the ROS 2 node within the Docker container.
57 | The container continues to run until stopped externally.
58 | The talker node can be interacted with by launching a listener node.
59 | The listener node does not need to be launched from within the Docker container.
60 | """
61 | ld = LaunchDescription()
62 |
63 | ld.add_action(
64 | SandboxedNodeContainer(
65 | sandbox_name='my_sandbox',
66 | policy=DockerPolicy(
67 | tag='dashing-desktop',
68 | repository='osrf/ros',
69 | container_name='sandboxed-listener-node',
70 | ),
71 | node_descriptions=[
72 | SandboxedNode(
73 | package='demo_nodes_cpp',
74 | node_executable='talker',
75 | ),
76 | ]
77 | )
78 | )
79 |
80 | return ld
81 |
82 |
83 | if __name__ == '__main__':
84 | """Starts the SandboxedNodeContainer example as a script."""
85 |
86 | ls = LaunchService(argv=sys.argv[1:], debug=True)
87 | ls.include_launch_description(generate_launch_description())
88 | sys.exit(ls.run())
89 |
--------------------------------------------------------------------------------
/examples/talker_listener_sandbox_docker.launch.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 |
17 |
18 | """
19 | Minimal example for using SandboxedNodeContainer with DockerPolicy.
20 |
21 | This example runs a ROS 2 node in a sandboxed environment by invoking SandboxedNodeContainer
22 | action. SandboxedNodeContainer delegates the launch parameters to an instance of launch_ros that is
23 | running in a Docker container.
24 |
25 | Currently, this test will only launch the Talker demo node inside Docker. This node can be observed
26 | by launching a Listener node either within the same docker container or on the host machine. The
27 | Docker container must be stopped externally in order to free resources.
28 |
29 | "container_id" should be substituted for the container name logged by launch. It can also be found
30 | by running "docker container ls".
31 |
32 | How to stop the Docker container:
33 | - docker stop $container_id
34 |
35 | How to run listener inside the Docker container
36 | - docker exec -it $container_id /bin/bash
37 | - source /ros_entrypoint.sh
38 | - ros2 run demo_nodes_cpp listener
39 | """
40 |
41 | import sys
42 |
43 | from launch import LaunchDescription
44 | from launch import LaunchService
45 |
46 | from launch_ros_sandbox.actions import SandboxedNodeContainer
47 | from launch_ros_sandbox.descriptions import DockerPolicy
48 | from launch_ros_sandbox.descriptions import SandboxedNode
49 |
50 |
51 | def generate_launch_description() -> LaunchDescription:
52 | """
53 | Create launch description for starting SandboxedNodeContainer with DockerPolicy.
54 |
55 | Talker and Listener are both loaded inside the SandboxedNodeContainer.
56 | When the sandboxed node is executed, it runs the ROS 2 node within the Docker container.
57 | The container continues to run until stopped externally.
58 | The output of the talker and listener nodes should be logged to the host machine's stdout.
59 | """
60 | ld = LaunchDescription()
61 |
62 | ld.add_action(
63 | SandboxedNodeContainer(
64 | sandbox_name='my_sandbox',
65 | policy=DockerPolicy(
66 | tag='dashing-desktop',
67 | repository='osrf/ros',
68 | container_name='sandboxed-listener-node',
69 | ),
70 | node_descriptions=[
71 | SandboxedNode(
72 | package='demo_nodes_cpp',
73 | node_executable='talker',
74 | ),
75 | SandboxedNode(
76 | package='demo_nodes_cpp',
77 | node_executable='listener'
78 | )
79 | ]
80 | )
81 | )
82 |
83 | return ld
84 |
85 |
86 | if __name__ == '__main__':
87 | """Starts the SandboxedNodeContainer example as a script."""
88 |
89 | ls = LaunchService(argv=sys.argv[1:], debug=True)
90 | ls.include_launch_description(generate_launch_description())
91 | sys.exit(ls.run())
92 |
--------------------------------------------------------------------------------
/examples/mem_limit_sandbox_docker.launch.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 |
17 |
18 | """
19 | Minimal example for using SandboxedNodeContainer with DockerPolicy with limited memory.
20 |
21 | This example runs a ROS 2 node in a sandboxed environment by invoking SandboxedNodeContainer
22 | action. SandboxedNodeContainer delegates the launch parameters to an instance of launch_ros that is
23 | running in a Docker container.
24 |
25 | Currently, this test will only launch the Talker demo node inside Docker. This node can be observed
26 | by launching a Listener node either within the same docker container or on the host machine. The
27 | Docker container must be stopped externally in order to free resources.
28 |
29 | "container_id" should be substituted for the container name logged by launch. It can also be found
30 | by running "docker container ls".
31 |
32 | How to stop the Docker container:
33 | - Send SIGINT to the launch process. (ctrl+c or kill -SIGINT)
34 |
35 | How to run listener inside the Docker container
36 | - docker exec -it sandboxed-listener-node ros2 run demo_nodes_cpp listener
37 | """
38 |
39 | import sys
40 |
41 | from launch import LaunchDescription
42 | from launch import LaunchService
43 |
44 | from launch_ros_sandbox.actions import SandboxedNodeContainer
45 | from launch_ros_sandbox.descriptions import DockerPolicy
46 | from launch_ros_sandbox.descriptions import SandboxedNode
47 |
48 |
49 | def generate_launch_description() -> LaunchDescription:
50 | """
51 | Create launch description for starting SandboxedNodeContainer with DockerPolicy.
52 |
53 | In this example, the C++ demo talker node is loaded inside the SandboxedNodeContainer called
54 | 'sandboxed-listener-node'.
55 | he Docker policy uses a Docker image of ROS2 Dashing (Desktop) from 'osrf/ros'.
56 | When the sandboxed node is executed, it runs the ROS 2 node within the Docker container.
57 | The container continues to run until stopped externally.
58 | The talker node can be interacted with by launching a listener node.
59 | The listener node does not need to be launched from within the Docker container.
60 | The container will be restricted to 128MB of RAM.
61 | """
62 | ld = LaunchDescription()
63 |
64 | ld.add_action(
65 | SandboxedNodeContainer(
66 | sandbox_name='my_sandbox',
67 | policy=DockerPolicy(
68 | tag='dashing-desktop',
69 | repository='osrf/ros',
70 | container_name='sandboxed-listener-node',
71 | run_args={
72 | 'mem_limit': '128m'
73 | }
74 | ),
75 | node_descriptions=[
76 | SandboxedNode(
77 | package='demo_nodes_cpp',
78 | node_executable='talker',
79 | ),
80 | ]
81 | )
82 | )
83 |
84 | return ld
85 |
86 |
87 | if __name__ == '__main__':
88 | """Starts the SandboxedNodeContainer example as a script."""
89 |
90 | ls = LaunchService(argv=sys.argv[1:], debug=True)
91 | ls.include_launch_description(generate_launch_description())
92 | sys.exit(ls.run())
93 |
--------------------------------------------------------------------------------
/examples/cpu_limit_sandbox_docker.launch.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 |
17 |
18 | """
19 | Minimal example for using SandboxedNodeContainer with DockerPolicy with limited CPU.
20 |
21 | This example runs a ROS 2 node in a sandboxed environment by invoking SandboxedNodeContainer
22 | action. SandboxedNodeContainer delegates the launch parameters to an instance of launch_ros that is
23 | running in a Docker container.
24 |
25 | Currently, this test will only launch the Talker demo node inside Docker. This node can be observed
26 | by launching a Listener node either within the same docker container or on the host machine. The
27 | Docker container must be stopped externally in order to free resources.
28 |
29 | "container_id" should be substituted for the container name logged by launch. It can also be found
30 | by running "docker container ls".
31 |
32 | How to stop the Docker container:
33 | - docker stop $container_id
34 |
35 | How to run listener inside the Docker container
36 | - docker exec -it $container_id /bin/bash
37 | - source /ros_entrypoint.sh
38 | - ros2 run demo_nodes_cpp listener
39 | """
40 |
41 | import sys
42 |
43 | from launch import LaunchDescription
44 | from launch import LaunchService
45 |
46 | from launch_ros_sandbox.actions import SandboxedNodeContainer
47 | from launch_ros_sandbox.descriptions import DockerPolicy
48 | from launch_ros_sandbox.descriptions import SandboxedNode
49 |
50 |
51 | def generate_launch_description() -> LaunchDescription:
52 | """
53 | Create a launch description for starting SandboxedNodeContainer with DockerPolicy.
54 |
55 | In this example, the C++ demo talker node is loaded inside the SandboxedNodeContainer called
56 | 'sandboxed-listener-node'.
57 | The Docker policy uses a Docker image of ROS2 Dashing (Desktop) from 'osrf/ros'.
58 | When the sandboxed node is executed, it runs the ROS 2 node within the Docker container.
59 | The container continues to run until stopped externally.
60 | The talker node can be interacted with by launching a listener node.
61 | The listener node does not need to be launched from within the Docker container.
62 | Only CPU 0 will be available to the nodes.
63 | The number of CPUs can be queried by running nproc inside the container.
64 | """
65 | ld = LaunchDescription()
66 |
67 | ld.add_action(
68 | SandboxedNodeContainer(
69 | sandbox_name='my_sandbox',
70 | policy=DockerPolicy(
71 | tag='dashing-desktop',
72 | repository='osrf/ros',
73 | container_name='sandboxed-listener-node',
74 | run_args={
75 | 'cpuset_cpus': '0'
76 | }
77 | ),
78 | node_descriptions=[
79 | SandboxedNode(
80 | package='demo_nodes_cpp',
81 | node_executable='talker',
82 | ),
83 | ]
84 | )
85 | )
86 |
87 | return ld
88 |
89 |
90 | if __name__ == '__main__':
91 | """Starts the SandboxedNodeContainer example as a script."""
92 |
93 | ls = LaunchService(argv=sys.argv[1:], debug=True)
94 | ls.include_launch_description(generate_launch_description())
95 | sys.exit(ls.run())
96 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing Guidelines
2 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional
3 | documentation, we greatly value feedback and contributions from our community.
4 |
5 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary
6 | information to effectively respond to your bug report or contribution.
7 |
8 |
9 | ## Reporting Bugs/Feature Requests
10 | We welcome you to use the GitHub issue tracker to report bugs or suggest features.
11 |
12 | When filing an issue, please check [existing open](https://github.com/ros-security/launch_ros_sandbox/issues), or [recently closed](https://github.com/ros-security/launch_ros_sandbox/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already
13 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful:
14 |
15 | * A reproducible test case or series of steps
16 | * The version of our code being used
17 | * Any modifications you've made relevant to the bug
18 | * Anything unusual about your environment or deployment
19 |
20 |
21 | ## Contributing via Pull Requests
22 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that:
23 |
24 | 1. You are working against the latest source on the *master* branch.
25 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already.
26 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted.
27 |
28 | To send us a pull request, please:
29 |
30 | 1. Fork the repository.
31 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change.
32 | 3. Ensure local tests pass.
33 | 4. Commit to your fork using clear commit messages.
34 | 5. Send us a pull request, answering any default questions in the pull request interface.
35 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation.
36 |
37 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and
38 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/).
39 |
40 |
41 | ## Finding contributions to work on
42 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/ros-security/launch_ros_sandbox/labels/help%20wanted) issues is a great place to start.
43 |
44 |
45 | ## Code of Conduct
46 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
47 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
48 | opensource-codeofconduct@amazon.com with any additional questions or comments.
49 |
50 |
51 | ## Security issue notifications
52 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue.
53 |
54 |
55 | ## Licensing
56 | Any contribution that you make to this repository will
57 | be under the Apache 2 License, as dictated by that
58 | [license](http://www.apache.org/licenses/LICENSE-2.0.html):
59 |
60 | ~~~
61 | 5. Submission of Contributions. Unless You explicitly state otherwise,
62 | any Contribution intentionally submitted for inclusion in the Work
63 | by You to the Licensor shall be under the terms and conditions of
64 | this License, without any additional terms or conditions.
65 | Notwithstanding the above, nothing herein shall supersede or modify
66 | the terms of any separate license agreement you may have executed
67 | with Licensor regarding such Contributions.
68 | ~~~
69 |
70 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes.
71 |
--------------------------------------------------------------------------------
/launch_ros_sandbox/descriptions/user_policy.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Module for UserPolicy."""
16 |
17 | import os
18 | import pwd
19 | import subprocess
20 | from typing import List
21 | from typing import Optional
22 |
23 | import launch
24 | from launch import Action
25 | from launch import LaunchContext
26 | from launch.utilities import perform_substitutions
27 |
28 | from launch_ros.substitutions import ExecutableInPackage
29 |
30 | from launch_ros_sandbox.actions.load_runas_nodes import LoadRunAsNodes
31 | from launch_ros_sandbox.descriptions.policy import Policy
32 | from launch_ros_sandbox.descriptions.sandboxed_node import SandboxedNode
33 | from launch_ros_sandbox.descriptions.user import User
34 |
35 |
36 | class UserPolicy(Policy):
37 | """
38 | UserPolicy defines parameters for running a sandboxed node as a different user.
39 |
40 | UserPolicy extends Policy. All parameters passed into UserPolicy are immutable and are only
41 | processed once the SandboxedNodeContainer is executed.
42 | """
43 |
44 | def __init__(
45 | self,
46 | *,
47 | run_as: Optional[User] = None,
48 | ) -> None:
49 | """Construct the UserPolicy."""
50 | self.__logger = launch.logging.get_logger(__name__)
51 | # default to current user if `run_as` is undefined.
52 | if run_as is not None:
53 | self._run_as = run_as
54 | else:
55 | self._run_as = User(
56 | uid=os.getuid(),
57 | gid=os.getgid())
58 |
59 | @property
60 | def run_as(self) -> User:
61 | """Get the User to run as."""
62 | return self._run_as
63 |
64 | def apply(
65 | self,
66 | context: LaunchContext,
67 | node_descriptions: List[SandboxedNode]
68 | ) -> Action:
69 | """Apply the policy any launches the ROS2 nodes in the sandbox."""
70 | user = self.run_as
71 | pw_record = pwd.getpwuid(user.uid)
72 |
73 | env = os.environ.copy()
74 | env['HOME'] = pw_record.pw_dir
75 | env['LOGNAME'] = pw_record.pw_name
76 | env['USER'] = pw_record.pw_name
77 | self.__logger.debug('Running as: {}'.format(pw_record.pw_name))
78 | self.__logger.debug('\tuid: {}'.format(user.uid))
79 | self.__logger.debug('\tgid: {}'.format(user.gid))
80 | self.__logger.debug('\thome: {}'.format(pw_record.pw_dir))
81 |
82 | def set_user() -> None:
83 | """Set the current user."""
84 | os.setgid(user.gid)
85 | os.setuid(user.uid)
86 |
87 | for description in node_descriptions:
88 | package_name = perform_substitutions(
89 | context,
90 | description.package
91 | )
92 | executable_name = perform_substitutions(
93 | context,
94 | description.node_executable
95 | )
96 |
97 | # TODO: support node namespace and node name
98 | # TODO: support parameters
99 | # TODO: support remappings
100 |
101 | cmd = [ExecutableInPackage(
102 | package=package_name,
103 | executable=executable_name
104 | ).perform(context)]
105 |
106 | self.__logger.info('Running: {}'.format(cmd))
107 |
108 | subprocess.Popen(
109 | cmd,
110 | preexec_fn=set_user,
111 | env=env
112 | )
113 |
114 | # TODO: handle events for process
115 |
116 | # TODO: LaunchAsUser is currently NO-OP due to all sandboxing logic being handled here.
117 | return LoadRunAsNodes()
118 |
--------------------------------------------------------------------------------
/launch_ros_sandbox/descriptions/sandboxed_node.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Module for SandboxedNode."""
16 |
17 | from typing import Iterable
18 | from typing import List
19 | from typing import Optional
20 |
21 | from launch.some_substitutions_type import SomeSubstitutionsType
22 | from launch.substitution import Substitution
23 | from launch.utilities import normalize_to_list_of_substitutions
24 |
25 | # TODO: this adds dependency on launch_ros
26 | # determine if this feature justifies the dependency
27 | from launch_ros.parameters_type import Parameters
28 | from launch_ros.parameters_type import SomeParameters
29 | from launch_ros.remap_rule_type import RemapRules
30 | from launch_ros.remap_rule_type import SomeRemapRules
31 | from launch_ros.utilities import normalize_parameters
32 | from launch_ros.utilities import normalize_remap_rules
33 |
34 |
35 | class SandboxedNode:
36 | """SandboxedNode describes sandbox launch configurations."""
37 |
38 | def __init__(
39 | self,
40 | *,
41 | package: SomeSubstitutionsType,
42 | node_executable: SomeSubstitutionsType,
43 | node_name: Optional[SomeSubstitutionsType] = None,
44 | node_namespace: SomeSubstitutionsType = '',
45 | parameters: Optional[SomeParameters] = None,
46 | remappings: Optional[SomeRemapRules] = None,
47 | arguments: Optional[Iterable[SomeSubstitutionsType]] = None,
48 | ) -> None:
49 | """
50 | Construct a SandboxedNode description.
51 |
52 | The actual node execution is delegated to the sandboxing environment
53 | defined by the policy.
54 |
55 | :param: package is the name of the node's package and is required for
56 | resolving the node.
57 | :param: node_executable is the name of the node's executable and is
58 | required for resolving the node.
59 | :param: node_name is an optional name attached to the node when it is
60 | launched. Defaults to NONE.
61 | :param: node_namespace is an optional namespace attached to the node
62 | when it is launched. Defaults to empty string.
63 | :param: parameters are the optional runtime configurations for the
64 | node, read from a YAML file. Defaults to NONE.
65 | :param: remappings are the ordered list of 'to' and 'from' string
66 | pairs to be passed to a node as ROS remapping rules.
67 | """
68 | self.__package = \
69 | normalize_to_list_of_substitutions(package)
70 | self.__node_executable = \
71 | normalize_to_list_of_substitutions(node_executable)
72 |
73 | self.__node_name = None
74 | if node_name is not None:
75 | self.__node_name = normalize_to_list_of_substitutions(node_name)
76 |
77 | self.__node_namespace = None
78 | if node_namespace is not None:
79 | self.__node_namespace = \
80 | normalize_to_list_of_substitutions(node_namespace)
81 |
82 | self.__parameters = None
83 | if parameters is not None:
84 | self.__parameters = normalize_parameters(parameters)
85 |
86 | self.__remappings = None
87 | if remappings is not None:
88 | self.__remappings = normalize_remap_rules(remappings)
89 |
90 | @property
91 | def package(self) -> List[Substitution]:
92 | """Get node package name as a sequence of substitutions to be performed."""
93 | return self.__package
94 |
95 | @property
96 | def node_executable(self) -> List[Substitution]:
97 | """Get node executable name as a sequence of substitutions to be performed."""
98 | return self.__node_executable
99 |
100 | @property
101 | def node_name(self) -> Optional[List[Substitution]]:
102 | """Get node name as a sequence of substitutions to be performed."""
103 | return self.__node_name
104 |
105 | @property
106 | def parameters(self) -> Optional[Parameters]:
107 | """Get node parameter YAML files or dicts with substitutions to be performed."""
108 | return self.__parameters
109 |
110 | @property
111 | def remappings(self) -> Optional[RemapRules]:
112 | """Get node remapping rules as (from, to) tuples with substitutions to be performed."""
113 | return self.__remappings
114 |
--------------------------------------------------------------------------------
/docs/source/index.md:
--------------------------------------------------------------------------------
1 | # launch_ros_sandbox
2 |
3 | 
4 | [](https://launch_ros_sandbox.readthedocs.io/en/latest/?badge=latest)
5 |
6 | `launch_ros_sandbox` is a `roslaunch2` extension.
7 |
8 | Using `launch_ros_sandbox`, you can define launch files running nodes in
9 | restrained environments, such as Docker containers or separate user accounts
10 | with limited privileges.
11 |
12 | [Package documentation][launch_ros_sandbox_doc]
13 |
14 | ## Installing
15 |
16 | ### Prerequisites
17 |
18 | `launch_ros_sandbox` requires Docker to be installed on your machine and that
19 | your user can execute `docker` commands.
20 |
21 | Check that your current user account is a member of the `docker` group:
22 |
23 | ```bash
24 | groups | grep docker
25 | ```
26 |
27 | If `docker` is not listed, add yourself to the group using:
28 |
29 | ```bash
30 | sudo usermod -aG docker $USER
31 | ```
32 |
33 | ### Binary Packages
34 |
35 | `launch_ros_sandbox` is not yet available as a binary package using APT or
36 | any other method.
37 |
38 | ### Installing from source
39 |
40 | #### Dashing (`dashing-devel` branch)
41 |
42 | This is the recommended way to install this software.
43 |
44 | * Install ROS 2 Dashing on your machine following the
45 | [official instructions][ros2_dashing_setup]. We recommend you use the
46 | official binary packages for your platform, if available.
47 | * Checkout the code source and compile it as follow:
48 |
49 | ```bash
50 | # If you use bash or zsh, source setup.bash or setup.zsh, instead of setup.sh
51 | source /opt/ros/dashing/setup.sh
52 | mkdir -p ~/ros2_dashing_ros_launch_sandbox_ws/src
53 | cd ros2_dashing_ros_launch_sandbox_ws
54 | # Clone this package repository using vcs.
55 | curl https://raw.githubusercontent.com/ros-security/launch_ros_sandbox/master/launch_ros_sandbox.dashing.repos | vcs import src/
56 | # Install all required system dependencies
57 | rosdep update
58 | rosdep install --ignore-packages-from-source --from-paths src/
59 | # Use colcon to compile launch_ros_sandbox code and all its dependencies
60 | colcon build --packages-up-to launch_ros_sandbox
61 | ```
62 |
63 | #### Latest (unstable development - `master` branch)
64 |
65 | Please follow those instructions if you plan to contribute to this repository.
66 |
67 | * Install all software dependencies required for ROS 2 development by
68 | following the [ROS 2 documentation][ros2_latest_setup].
69 | * Checkout the code source and compile it as follow:
70 |
71 | ```bash
72 | mkdir -p ~/ros2_latest_ros_launch_sandbox_ws/src
73 | cd ros2_latest_ros_launch_sandbox_ws
74 | # Use vcs to clone all required repositories
75 | curl https://raw.githubusercontent.com/ros2/ros2/dashing/ros2.repos | vcs import src/
76 | curl https://raw.githubusercontent.com/ros-security/launch_ros_sandbox/master/launch_ros_sandbox.repos | vcs import src/
77 | # Install all required system dependencies
78 | # Some packages may fail to install, this is expected on an unstable branch,
79 | # and is generally OK.
80 | rosdep update
81 | rosdep install -r --rosdistro=eloquent --ignore-packages-from-source --from-paths src/
82 | # Use colcon to compile launch_ros_sandbox code and all its dependencies
83 | colcon build --packages-up-to launch_ros_sandbox
84 | ```
85 |
86 | ## Usage
87 |
88 | A working example is provided in
89 | [examples/minimal_sandboxed_node_container.launch.py][ex_minimal_sandboxed_node_container_launch]
90 |
91 | ```bash
92 | ./examples/minimal_sandboxed_node_container.py
93 | ```
94 |
95 | Creating a sandboxed node is very similar to creating a regular launch file.
96 |
97 | Add a `SandboxedNodeContainer()` action like you would with a regular launch
98 | file, but make sure to provide the `sandbox_name` and `policy`.
99 | Adding nodes is also similar to regular launch files, however, you should use
100 | `launch_ros_sandbox.descriptions.SandboxedNode()` instead.
101 |
102 | A launch file with nodes running as a certain user would look like:
103 |
104 | ```python
105 | def generate_launch_description() -> launch.LaunchDescription:
106 | ld = launch.LaunchDescription()
107 |
108 | ld.add_action(
109 | launch_ros_sandbox.actions.SandboxedNodeContainer(
110 | sandbox_name='my_sandbox',
111 | policy=UserPolicy(run_as=User.from_username('dashing')),
112 | node_descriptions=[
113 | launch_ros_sandbox.descriptions.SandboxedNode(
114 | package='demo_nodes_cpp', node_executable='talker'),
115 | launch_ros_sandbox.descriptions.SandboxedNode(
116 | package='demo_nodes_cpp', node_executable='listener')
117 | ]
118 | )
119 | )
120 | ```
121 |
122 | ## License
123 |
124 | This library is licensed under the Apache 2.0 License.
125 |
126 | ## Build Status
127 |
128 | | ROS 2 Release | Branch Name | Development | Source Debian Package | X86-64 Debian Package | ARM64 Debian Package | ARMHF Debian package |
129 | | ------------- | --------------- | ----------- | --------------------- | --------------------- | -------------------- | -------------------- |
130 | | Latest | `master` | [](https://travis-ci.com/ros-security/launch_ros_sandbox) | N/A | N/A | N/A | N/A |
131 | | Dashing | `dashing-devel` | [](http://build.ros2.org/job/Ddev__launch_ros_sandbox__ubuntu_bionic_amd64) | [](http://build.ros2.org/job/Dsrc_uB__launch_ros_sandbox__ubuntu_bionic__source) | [](http://build.ros2.org/job/Dbin_uB64__launch_ros_sandbox__ubuntu_bionic_amd64__binary) | N/A | N/A |
132 |
133 | [ex_minimal_sandboxed_node_container_launch]: examples/minimal_sandboxed_node_container.launch.py
134 | [launch_ros_sandbox_doc]: https://launch_ros_sandbox.readthedocs.io
135 | [ros2_dashing_setup]: https://index.ros.org/doc/ros2/Installation/Dashing/
136 | [ros2_latest_setup]: https://index.ros.org/doc/ros2/Installation/Latest-Development-Setup/
137 |
--------------------------------------------------------------------------------
/test/launch_ros_sandbox/descriptions/test_docker_policy.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """
16 | Tests for the UserPolicy description.
17 |
18 | DockerPolicy handles default constructor parameters by setting the image name to
19 | 'osrf/ros:dashing-desktop' if both 'repository' and 'tag' are set to the default (None). If only
20 | 'tag' is set to its default value, DockerPolicy will assume the tag 'latest'.
21 |
22 | The unit tests verify that tag and repository properly default. No actual processing occurs inside
23 | DockerPolicy's constructor, so there is no side effects on calling the constructors within these
24 | tests. These tests also do not require the Docker daemon since DockerPolicy does not interact
25 | with Docker until the policy is applied to the SandboxedNodeContainer.
26 | """
27 |
28 | import unittest
29 |
30 | from launch_ros_sandbox.descriptions import DockerPolicy
31 |
32 |
33 | class TestDockerPolicy(unittest.TestCase):
34 |
35 | def test_repository_and_tag_defaults_to_osrf_ros_dashing_desktop(self) -> None:
36 | """
37 | Verify DockerPolicy 'image_name' is properly resolved with default 'tag' and 'repository'.
38 |
39 | DockerPolicy should resolve 'repository' to 'osrf/ros' and 'tag' to 'desktop-dashing' only
40 | when both are set to their default (None).
41 | """
42 | docker_policy = DockerPolicy()
43 |
44 | assert docker_policy.image_name == 'osrf/ros:dashing-desktop'
45 | assert docker_policy.repository == 'osrf/ros'
46 | assert docker_policy.tag == 'dashing-desktop'
47 |
48 | def test_tag_defaults_to_latest_if_repository_is_defined(self) -> None:
49 | """Verify DockerPolicy tag defaults to 'latest' when only repository is specified."""
50 | docker_policy = DockerPolicy(repository='ubuntu')
51 |
52 | assert docker_policy.image_name == 'ubuntu:latest'
53 | assert docker_policy.repository == 'ubuntu'
54 | assert docker_policy.tag == 'latest'
55 |
56 | def test_repository_defaults_to_osrf_if_tag_is_defined(self) -> None:
57 | """Verify DockerPolicy repository defaults to 'osrf/ros' if tag is defined."""
58 | docker_policy = DockerPolicy(tag='crystal-desktop')
59 |
60 | assert docker_policy.image_name == 'osrf/ros:crystal-desktop'
61 | assert docker_policy.repository == 'osrf/ros'
62 | assert docker_policy.tag == 'crystal-desktop'
63 |
64 | def test_image_name_properly_set_if_tag_and_repository_are_defined(self) -> None:
65 | """Verify DockerPolicy image_name is 'repository:tag' if both are defined."""
66 | docker_policy = DockerPolicy(
67 | repository='ubuntu',
68 | tag='bionic'
69 | )
70 |
71 | assert docker_policy.image_name == 'ubuntu:bionic'
72 | assert docker_policy.repository == 'ubuntu'
73 | assert docker_policy.tag == 'bionic'
74 |
75 | def test_tag_defaults_to_dashing_desktop_if_repository_is_manually_set(self) -> None:
76 | """Verify DockerPolicy tag defaults to 'dashing-desktop' if repository is 'osrf/ros'."""
77 | docker_policy = DockerPolicy(
78 | repository='osrf/ros',
79 | )
80 |
81 | assert docker_policy.image_name == 'osrf/ros:dashing-desktop'
82 | assert docker_policy.repository == 'osrf/ros'
83 | assert docker_policy.tag == 'dashing-desktop'
84 |
85 | def test_entrypoint_assign_default_repo_default_tag(self) -> None:
86 | """Verify entrypoint can be set if repo and tag are defaults."""
87 | docker_policy = DockerPolicy(
88 | entrypoint='foo'
89 | )
90 |
91 | assert docker_policy.entrypoint == 'foo'
92 | assert docker_policy.repository == 'osrf/ros'
93 | assert docker_policy.tag == 'dashing-desktop'
94 |
95 | def test_entrypoint_assign_default_repo(self) -> None:
96 | """Verify entrypoint can be set if repo is default."""
97 | docker_policy = DockerPolicy(
98 | entrypoint='foo',
99 | tag='bar'
100 | )
101 |
102 | assert docker_policy.entrypoint == 'foo'
103 | assert docker_policy.repository == 'osrf/ros'
104 | assert docker_policy.tag == 'bar'
105 |
106 | def test_entrypoint_assign_default_tag(self) -> None:
107 | """Verify entrypoint can be set if tag is default."""
108 | docker_policy = DockerPolicy(
109 | entrypoint='foo',
110 | repository='bar'
111 | )
112 |
113 | assert docker_policy.entrypoint == 'foo'
114 | assert docker_policy.repository == 'bar'
115 | assert docker_policy.tag == 'latest'
116 |
117 | def test_entrypoint_default_osrf_repo(self) -> None:
118 | """Verify entrypoint is 'ros_entrypoint' if repo is set to 'osrf/ros'."""
119 | docker_policy = DockerPolicy(
120 | repository='osrf/ros'
121 | )
122 |
123 | assert docker_policy.repository == 'osrf/ros'
124 | assert docker_policy.entrypoint == '/ros_entrypoint.sh'
125 |
126 | def test_entrypoint_default_not_osrf_repo(self) -> None:
127 | """Verify entrypoint is '/bin/bash -c' if repo is set to not 'osrf/ros'."""
128 | docker_policy = DockerPolicy(
129 | repository='foo'
130 | )
131 |
132 | assert docker_policy.repository == 'foo'
133 | assert docker_policy.entrypoint == '/bin/bash -c'
134 |
135 | def test_run_args_set_correctly(self) -> None:
136 | """Verify the DockerPolicy run arguments match for the Docker Image."""
137 | run_args = {
138 | 'cpuset_cpus': 0,
139 | 'mem_limit': '128m'
140 | }
141 | docker_policy = DockerPolicy(
142 | run_args=run_args
143 | )
144 |
145 | assert docker_policy.run_args == run_args
146 |
147 | def test_empty_run_args_set_correctly(self) -> None:
148 | """Verify the DockerPolicy has no run args if not set."""
149 | docker_policy = DockerPolicy()
150 |
151 | assert docker_policy.run_args is None
152 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DEPRECATED package
2 |
3 | This package is no longer under development. It was built for a 2019 Roscon workshop and has no future planned usage or maintenance. Repository not deleted for historical reasons (so as not to break existing indexes that contain links to it)
4 |
5 | # launch_ros_sandbox
6 |
7 | 
8 | [](https://launch_ros_sandbox.readthedocs.io/en/latest/?badge=latest)
9 |
10 | `launch_ros_sandbox` is a `roslaunch2` extension.
11 |
12 | Using `launch_ros_sandbox`, you can define launch files running nodes in
13 | restrained environments, such as Docker containers or separate user accounts
14 | with limited privileges.
15 |
16 | [Package documentation][launch_ros_sandbox_doc]
17 |
18 | ## Installing
19 |
20 | ### Prerequisites
21 |
22 | `launch_ros_sandbox` requires Docker to be installed on your machine and that
23 | your user can execute `docker` commands.
24 |
25 | Check that your current user account is a member of the `docker` group:
26 |
27 | ```bash
28 | groups | grep docker
29 | ```
30 |
31 | If `docker` is not listed, add yourself to the group using:
32 |
33 | ```bash
34 | sudo usermod -aG docker $USER
35 | ```
36 |
37 | ### Binary Packages
38 |
39 | #### Dashing
40 |
41 | On Ubuntu 18.04, you can install `launch_ros_sandbox` by running:
42 |
43 | ```sh
44 | sudo apt install ros-dashing-launch-ros-sandbox
45 | ```
46 |
47 | ### Installing from source
48 |
49 | #### Dashing (`dashing-devel` branch)
50 |
51 | This is the recommended way to install this software.
52 |
53 | * Install ROS 2 Dashing on your machine following the
54 | [official instructions][ros2_dashing_setup]. We recommend you use the
55 | official binary packages for your platform, if available.
56 | * Checkout the code source and compile it as follow:
57 |
58 | ```bash
59 | # If you use bash or zsh, source setup.bash or setup.zsh, instead of setup.sh
60 | source /opt/ros/dashing/setup.sh
61 | mkdir -p ~/ros2_dashing_ros_launch_sandbox_ws/src
62 | cd ros2_dashing_ros_launch_sandbox_ws
63 | # Clone this package repository using vcs.
64 | curl https://raw.githubusercontent.com/ros-security/launch_ros_sandbox/master/launch_ros_sandbox.dashing.repos | vcs import src/
65 | # Install all required system dependencies
66 | rosdep update
67 | rosdep install --ignore-packages-from-source --from-paths src/
68 | # Use colcon to compile launch_ros_sandbox code and all its dependencies
69 | colcon build --packages-up-to launch_ros_sandbox
70 | ```
71 |
72 | #### Latest (unstable development - `master` branch)
73 |
74 | Please follow those instructions if you plan to contribute to this repository.
75 |
76 | * Install all software dependencies required for ROS 2 development by
77 | following the [ROS 2 documentation][ros2_latest_setup].
78 | * Checkout the code source and compile it as follow:
79 |
80 | ```bash
81 | mkdir -p ~/ros2_latest_ros_launch_sandbox_ws/src
82 | cd ros2_latest_ros_launch_sandbox_ws
83 | # Use vcs to clone all required repositories
84 | curl https://raw.githubusercontent.com/ros2/ros2/dashing/ros2.repos | vcs import src/
85 | curl https://raw.githubusercontent.com/ros-security/launch_ros_sandbox/master/launch_ros_sandbox.repos | vcs import src/
86 | # Install all required system dependencies
87 | # Some packages may fail to install, this is expected on an unstable branch,
88 | # and is generally OK.
89 | rosdep update
90 | rosdep install -r --rosdistro=eloquent --ignore-packages-from-source --from-paths src/
91 | # Use colcon to compile launch_ros_sandbox code and all its dependencies
92 | colcon build --packages-up-to launch_ros_sandbox
93 | ```
94 |
95 | ## Usage
96 |
97 | A working example is provided in
98 | [examples/minimal_sandboxed_node_container.launch.py][ex_minimal_sandboxed_node_container_launch]
99 |
100 | ```bash
101 | ./examples/minimal_sandboxed_node_container.py
102 | ```
103 |
104 | Creating a sandboxed node is very similar to creating a regular launch file.
105 |
106 | Add a `SandboxedNodeContainer()` action like you would with a regular launch
107 | file, but make sure to provide the `sandbox_name` and `policy`.
108 | Adding nodes is also similar to regular launch files, however, you should use
109 | `launch_ros_sandbox.descriptions.SandboxedNode()` instead.
110 |
111 | A launch file with nodes running as a certain user would look like:
112 |
113 | ```python
114 | def generate_launch_description() -> launch.LaunchDescription:
115 | ld = launch.LaunchDescription()
116 |
117 | ld.add_action(
118 | launch_ros_sandbox.actions.SandboxedNodeContainer(
119 | sandbox_name='my_sandbox',
120 | policy=UserPolicy(run_as=User.from_username('dashing')),
121 | node_descriptions=[
122 | launch_ros_sandbox.descriptions.SandboxedNode(
123 | package='demo_nodes_cpp', node_executable='talker'),
124 | launch_ros_sandbox.descriptions.SandboxedNode(
125 | package='demo_nodes_cpp', node_executable='listener')
126 | ]
127 | )
128 | )
129 | ```
130 |
131 | ## License
132 |
133 | This library is licensed under the Apache 2.0 License.
134 |
135 | ## Build Status
136 |
137 | | ROS 2 Release | Branch Name | Development | Source Debian Package | X86-64 Debian Package | ARM64 Debian Package | ARMHF Debian package |
138 | | ------------- | --------------- | ----------- | --------------------- | --------------------- | -------------------- | -------------------- |
139 | | Latest | `master` | [](https://github.com/ros-security/launch_ros_sandbox/actions) | N/A | N/A | N/A | N/A |
140 | | Dashing | `dashing-devel` | [](http://build.ros2.org/job/Ddev__launch_ros_sandbox__ubuntu_bionic_amd64) | [](http://build.ros2.org/job/Dsrc_uB__launch_ros_sandbox__ubuntu_bionic__source) | [](http://build.ros2.org/job/Dbin_uB64__launch_ros_sandbox__ubuntu_bionic_amd64__binary) | N/A | N/A |
141 |
142 | [ex_minimal_sandboxed_node_container_launch]: examples/minimal_sandboxed_node_container.launch.py
143 | [launch_ros_sandbox_doc]: https://launch_ros_sandbox.readthedocs.io
144 | [ros2_dashing_setup]: https://index.ros.org/doc/ros2/Installation/Dashing/
145 | [ros2_latest_setup]: https://index.ros.org/doc/ros2/Installation/Latest-Development-Setup/
146 |
--------------------------------------------------------------------------------
/docs/source/conf.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 | # -*- coding: utf-8 -*-
16 | #
17 | # launch_ros_sandbox documentation build configuration file, created by
18 | # sphinx-quickstart on Thu Sep 19 08:53:28 2019.
19 | #
20 | # This file is execfile()d with the current directory set to its
21 | # containing dir.
22 | #
23 | # Note that not all possible configuration values are present in this
24 | # autogenerated file.
25 | #
26 | # All configuration values have a default; values that are commented out
27 | # serve to show the default.
28 |
29 | # If extensions (or modules to document with autodoc) are in another directory,
30 | # add these directories to sys.path here. If the directory is relative to the
31 | # documentation root, use os.path.abspath to make it absolute, like shown here.
32 | #
33 | import os
34 | import sys
35 | sys.path.insert(0, os.path.abspath('../..'))
36 |
37 |
38 | # -- General configuration ------------------------------------------------
39 |
40 | # If your documentation needs a minimal Sphinx version, state it here.
41 | #
42 | # needs_sphinx = '1.0'
43 |
44 | # Add any Sphinx extension module names here, as strings. They can be
45 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
46 | # ones.
47 | extensions = [
48 | 'recommonmark',
49 | 'sphinx.ext.autodoc',
50 | 'sphinx.ext.coverage',
51 | 'sphinx.ext.doctest',
52 | 'sphinx.ext.githubpages',
53 | 'sphinx.ext.todo',
54 | 'sphinx.ext.viewcode',
55 | 'sphinx_markdown_tables',
56 | ]
57 |
58 | # Add any paths that contain templates here, relative to this directory.
59 | templates_path = ['_templates']
60 |
61 | # The suffix(es) of source filenames.
62 | # You can specify multiple suffix as a list of string:
63 | #
64 | source_suffix = ['.rst', '.md']
65 | # source_suffix = '.rst'
66 |
67 | # The master toctree document.
68 | master_doc = 'index'
69 |
70 | # General information about the project.
71 | project = 'Launch ROS Sandbox'
72 | copyright = '2019, AWS Robomaker' # NOQA
73 | author = 'AWS Robomaker'
74 |
75 | # The version info for the project you're documenting, acts as replacement for
76 | # |version| and |release|, also used in various other places throughout the
77 | # built documents.
78 | #
79 | # The short X.Y version.
80 | version = '0.0.1'
81 | # The full version, including alpha/beta/rc tags.
82 | release = '0.0.1'
83 |
84 | # The language for content autogenerated by Sphinx. Refer to documentation
85 | # for a list of supported languages.
86 | #
87 | # This is also used if you do content translation via gettext catalogs.
88 | # Usually you set "language" from the command line for these cases.
89 | language = None
90 |
91 | # List of patterns, relative to source directory, that match files and
92 | # directories to ignore when looking for source files.
93 | # This patterns also effect to html_static_path and html_extra_path
94 | exclude_patterns = []
95 |
96 | # The name of the Pygments (syntax highlighting) style to use.
97 | pygments_style = 'sphinx'
98 |
99 | # If true, `todo` and `todoList` produce output, else they produce nothing.
100 | todo_include_todos = True
101 |
102 |
103 | # -- Options for HTML output ----------------------------------------------
104 |
105 | # The theme to use for HTML and HTML Help pages. See the documentation for
106 | # a list of builtin themes.
107 | #
108 | html_theme = 'alabaster'
109 |
110 | # Theme options are theme-specific and customize the look and feel of a theme
111 | # further. For a list of options available for each theme, see the
112 | # documentation.
113 | #
114 | html_theme_options = {
115 | 'description': 'A sandboxing plugin for launch_ros',
116 | 'fixed_sidebar': True
117 | }
118 |
119 | # Add any paths that contain custom static files (such as style sheets) here,
120 | # relative to this directory. They are copied after the builtin static files,
121 | # so a file named "default.css" will overwrite the builtin "default.css".
122 | # html_static_path = ['_static']
123 |
124 | # Custom sidebar templates, must be a dictionary that maps document names
125 | # to template names.
126 | #
127 | # This is required for the alabaster theme
128 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
129 | html_sidebars = {
130 | '**': [
131 | 'about.html',
132 | 'searchbox.html',
133 | 'navigation.html',
134 | ]
135 | }
136 |
137 |
138 | # -- Options for HTMLHelp output ------------------------------------------
139 |
140 | # Output file base name for HTML help builder.
141 | htmlhelp_basename = 'launch_ros_sandboxdoc'
142 |
143 |
144 | # -- Options for LaTeX output ---------------------------------------------
145 |
146 | latex_elements = {
147 | # The paper size ('letterpaper' or 'a4paper').
148 | #
149 | # 'papersize': 'letterpaper',
150 |
151 | # The font size ('10pt', '11pt' or '12pt').
152 | #
153 | # 'pointsize': '10pt',
154 |
155 | # Additional stuff for the LaTeX preamble.
156 | #
157 | # 'preamble': '',
158 |
159 | # Latex figure (float) alignment
160 | #
161 | # 'figure_align': 'htbp',
162 | }
163 |
164 | # Grouping the document tree into LaTeX files. List of tuples
165 | # (source start file, target name, title,
166 | # author, documentclass [howto, manual, or own class]).
167 | latex_documents = [
168 | (master_doc, 'launch_ros_sandbox.tex', 'launch_ros_sandbox Documentation',
169 | 'AWS Robomaker', 'manual'),
170 | ]
171 |
172 |
173 | # -- Options for manual page output ---------------------------------------
174 |
175 | # One entry per manual page. List of tuples
176 | # (source start file, name, description, authors, manual section).
177 | man_pages = [
178 | (master_doc, 'launch_ros_sandbox', 'launch_ros_sandbox Documentation',
179 | [author], 1)
180 | ]
181 |
182 |
183 | # -- Options for Texinfo output -------------------------------------------
184 |
185 | # Grouping the document tree into Texinfo files. List of tuples
186 | # (source start file, target name, title, author,
187 | # dir menu entry, description, category)
188 | texinfo_documents = [
189 | (master_doc, 'launch_ros_sandbox', 'launch_ros_sandbox Documentation',
190 | author, 'launch_ros_sandbox', 'One line description of project.',
191 | 'Miscellaneous'),
192 | ]
193 |
--------------------------------------------------------------------------------
/launch_ros_sandbox/descriptions/docker_policy.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """
16 | Module for the DockerPolicy description.
17 |
18 | Using DockerPolicy, users can load one or more nodes into a particular Docker container. Using
19 | DockerPolicy requires that Docker 18+ and docker-py 4.0+ is installed.
20 |
21 | Example:
22 | -------
23 | .. code-block:: python
24 |
25 | ld = launch.LaunchDescription()
26 |
27 | ld.add_action(
28 | launch_ros_sandbox.actions.SandboxedNodeContainer(
29 | sandbox_name='my_sandbox',
30 | policy=launch_ros_sandbox.descriptions.DockerPolicy(),
31 | node_descriptions=[
32 | launch_ros_sandbox.descriptions.SandboxedNode(
33 | package='demo_nodes_cpp',
34 | node_executable='talker',
35 | ),
36 | launch_ros_sandbox.descriptions.SandboxedNode(
37 | package='demo_nodes_cpp',
38 | node_executable='listener'
39 | )
40 | ]
41 | )
42 | )
43 |
44 | This will launch the talker and listener nodes within a Docker container running
45 | 'osrf/ros:dashing-desktop' image.
46 |
47 | Currently persistence is not supported, however it is planned to support forwarding all run
48 | parameters to docker-py.
49 |
50 | """
51 |
52 | import time
53 | from typing import Any
54 | from typing import Dict
55 | from typing import List
56 | from typing import Optional
57 |
58 | import launch
59 | from launch import Action
60 | from launch import LaunchContext
61 |
62 | from launch_ros_sandbox.descriptions.policy import Policy
63 | from launch_ros_sandbox.descriptions.sandboxed_node import SandboxedNode
64 |
65 | _DEFAULT_DOCKER_REPO = 'osrf/ros'
66 | _DEFAULT_DOCKER_TAG = 'dashing-desktop'
67 | _DEFAULT_EXEC_ENTRYPOINT = '/ros_entrypoint.sh'
68 |
69 |
70 | def _generate_container_name() -> str:
71 | """Generate a Docker container name for use in DockerPolicy."""
72 | return 'ros2launch-sandboxed-node-{}'.format(time.strftime('%H%M%S'))
73 |
74 |
75 | class DockerPolicy(Policy):
76 | """
77 | DockerPolicy defines parameters for running a sandboxed node in a Docker container.
78 |
79 | DockerPolicy extends Policy. All of the parameters passed into DockerPolicy are immutable and
80 | only processed once the SandboxedNodeContainer is executed.
81 | """
82 |
83 | def __init__(
84 | self,
85 | *,
86 | repository: Optional[str] = None,
87 | tag: Optional[str] = None,
88 | entrypoint: Optional[str] = None,
89 | container_name: Optional[str] = None,
90 | run_args: Optional[Dict[str, Any]] = None,
91 | ) -> None:
92 | """
93 | Construct the DockerPolicy.
94 |
95 | The constructor sets the repository, tag, and entrypoint for the Docker container based on
96 | the provided parameters. The repository and tag parameters are optional and will default
97 | to OSRF's latest ROS distribution if not set. A container name can also be provided, but
98 | will default to a generic name. The container is not started until the policy is applied.
99 |
100 | :param: repository is the Docker repository to pull the image from. 'repository' defaults
101 | to 'osrf/ros'.
102 | :param: tag is the Docker image tag. 'tag' defaults to 'dashing-desktop' if 'repository'
103 | evaluates to 'osrf/ros'; this includes if 'repository' defaults to 'osrf/ros'. Otherwise
104 | 'tag' defaults to 'latest'.
105 | :param: entrypoint is the absolute path of the script to run within the Docker container
106 | for launching internal ROS 2 nodes. Defaults to '/ros_entrypoint.sh' if repository
107 | evaluates to 'osrf/ros'. Otherwise 'entrypoint' defaults to '/bin/bash -c'.
108 | :param: container_name is the name of the container passed to Docker to make it easier to
109 | identify when listing all the containers. Defaults to
110 | ros2launch-sandboxed-node- where the time is when the DockerPolicy
111 | was constructed.
112 | :param: run_args is a dictionary of arguments (str to Any) passed into the 'run' command
113 | for the Docker container. See [1] for supported arguments.
114 | 'image', 'tty', 'detach', 'auto_remove', and 'name' are not valid keywords for 'run_args'
115 | due to being defined by LoadDockerNodes.
116 |
117 | [1]: https://docker-py.readthedocs.io/en/stable/containers.html#docker.models.containers.ContainerCollection.run # noqa
118 | """
119 | self.__logger = launch.logging.get_logger(__name__)
120 |
121 | # Evaluate repository first since the evaluation of tag and entrypoint depend upon it.
122 | self._repository = repository or _DEFAULT_DOCKER_REPO
123 |
124 | if self._repository == _DEFAULT_DOCKER_REPO:
125 | self._tag = tag or _DEFAULT_DOCKER_TAG
126 | self._entrypoint = entrypoint or _DEFAULT_EXEC_ENTRYPOINT
127 | else:
128 | # Repository is not the default repo, so assume we're not using an osrf image. This
129 | # changes the default tag to be the conventional 'latest' and default entrypoint to be
130 | # bash.
131 | self._tag = tag or 'latest'
132 | self._entrypoint = entrypoint or '/bin/bash -c'
133 |
134 | self._image_name = '{}:{}'.format(self._repository, self._tag)
135 | self._container_name = container_name or _generate_container_name()
136 | self._run_args = run_args
137 |
138 | @property
139 | def entrypoint(self) -> str:
140 | """Return the Docker container entrypoint."""
141 | return self._entrypoint
142 |
143 | @property
144 | def container_name(self) -> str:
145 | """Return the Docker container name."""
146 | return self._container_name
147 |
148 | @property
149 | def repository(self) -> str:
150 | """Return the Docker image repository."""
151 | return self._repository
152 |
153 | @property
154 | def tag(self) -> str:
155 | """Return the Docker image tag."""
156 | return self._tag
157 |
158 | @property
159 | def image_name(self) -> str:
160 | """
161 | Return the Docker image name.
162 |
163 | The image name is defined as 'repository:tag'.
164 | """
165 | return '{}:{}'.format(self.repository, self.tag)
166 |
167 | @property
168 | def run_args(self) -> Optional[Dict[str, Any]]:
169 | """Return the dictionary of Docker container run arguments."""
170 | return self._run_args
171 |
172 | def apply(
173 | self,
174 | context: LaunchContext,
175 | node_descriptions: List[SandboxedNode]
176 | ) -> Action:
177 | """
178 | Apply the policy and load each node inside the Docker sandbox.
179 |
180 | Applying the policy involves iterating over the list of nodes to execute and using the
181 | `ros2 run` CLI within the container. The node and package names are resolved using
182 | substitutions, a utility from Launch.
183 | """
184 | from launch_ros_sandbox.actions.load_docker_nodes import LoadDockerNodes
185 |
186 | return LoadDockerNodes(
187 | policy=self,
188 | node_descriptions=node_descriptions
189 | )
190 |
--------------------------------------------------------------------------------
/launch_ros_sandbox/actions/load_docker_nodes.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 |
16 | """
17 | Internal module for the LoadDockerNodes Action.
18 |
19 | LoadDockerNodes is an Action that controls the lifecycle of a sandboxed environment running nodes
20 | as a Docker container. This Action is not exported and should only be used internally.
21 | """
22 |
23 | import asyncio
24 | from concurrent.futures import ThreadPoolExecutor
25 | import shlex
26 | from threading import Lock
27 | from types import GeneratorType
28 | from typing import List, Optional
29 |
30 | import docker
31 | from docker.errors import ImageNotFound
32 |
33 | import launch
34 | from launch import Action, LaunchContext
35 | from launch.event import Event
36 | from launch.event_handlers import OnShutdown
37 | from launch.some_actions_type import SomeActionsType
38 | from launch.utilities import create_future, perform_substitutions
39 |
40 | from launch_ros_sandbox.descriptions.docker_policy import DockerPolicy
41 | from launch_ros_sandbox.descriptions.sandboxed_node import SandboxedNode
42 |
43 |
44 | def _containerized_cmd(entrypoint: str, package: str, executable: str) -> List[str]:
45 | """Prepare the command for executing within the Docker container."""
46 | # Use ros2 CLI command to find the executable
47 | return shlex.split(entrypoint) + ['ros2', 'run', package, executable]
48 |
49 |
50 | class LoadDockerNodes(Action):
51 | """
52 | LoadDockerNodes is an Action that controls the sandbox environment spawned by `DockerPolicy`.
53 |
54 | LoadDockerNodes should only be constructed by `DockerPolicy.apply`.
55 | """
56 |
57 | def __init__(
58 | self,
59 | policy: DockerPolicy,
60 | node_descriptions: List[SandboxedNode],
61 | **kwargs
62 | ) -> None:
63 | """
64 | Construct the LoadDockerNodes Action.
65 |
66 | Parameters regarding initialization are copied here.
67 | Most of the arguments are forwarded to Action.
68 | """
69 | super().__init__(**kwargs)
70 | self._policy = policy
71 | self._node_descriptions = node_descriptions
72 | self._completed_future = None # type: Optional[asyncio.Future]
73 | self._started_task = None # type: Optional[asyncio.Task]
74 | self._container = None # type: Optional[docker.models.containers.Container]
75 | self._shutdown_lock = Lock()
76 | self._docker_client = docker.from_env()
77 | self.__logger = launch.logging.get_logger(__name__)
78 | self._executor = ThreadPoolExecutor(max_workers=len(node_descriptions))
79 |
80 | def _pull_docker_image(self) -> None:
81 | """
82 | Pull the docker image.
83 |
84 | This will download the Docker image if it is not currently cached and will update it if its
85 | out of date.
86 |
87 | :raises ImageNotFound if Docker cannot find the remote repo for the image to pull
88 | """
89 | self.__logger.info('Pulling image {}'.format(self._policy.image_name))
90 |
91 | # This method may throw an ImageNotFound exception. Let the exception propogate upwards
92 | self._docker_client.images.pull(
93 | self._policy.repository,
94 | tag=self._policy.tag
95 | )
96 |
97 | def _start_docker_container(self) -> None:
98 | """
99 | Start Docker container.
100 |
101 | Run arguments will be forwarded to the containers run command if they exist.
102 | """
103 | tmp_run_args = self._policy.run_args or {}
104 |
105 | # This method may throw an ImageNotFound exception. Let the exception propogate upwards
106 | self._container = self._docker_client.containers.run(
107 | self._policy.image_name,
108 | detach=True,
109 | auto_remove=True,
110 | tty=True,
111 | name=self._policy.container_name,
112 | **tmp_run_args
113 | )
114 |
115 | self.__logger.info('Running Docker container: \"{}\"'.format(self._policy.container_name))
116 |
117 | def _load_nodes_in_docker(
118 | self,
119 | context: LaunchContext
120 | ) -> None:
121 | """Load all nodes into Docker container."""
122 | if self._container is None:
123 | self.__logger.error('Unable to load nodes into Docker container: '
124 | 'no active Docker container!')
125 | return
126 |
127 | for description in self._node_descriptions:
128 | package_name = perform_substitutions(
129 | context=context,
130 | subs=description.package
131 | )
132 |
133 | executable_name = perform_substitutions(
134 | context=context,
135 | subs=description.node_executable
136 | )
137 |
138 | cmd = _containerized_cmd(
139 | entrypoint=self._policy.entrypoint,
140 | package=package_name,
141 | executable=executable_name
142 | )
143 |
144 | log_generator = self._container.exec_run(
145 | cmd=cmd,
146 | tty=True,
147 | stream=True,
148 | )
149 |
150 | context.asyncio_loop.run_in_executor(self._executor, self._handle_logs, log_generator)
151 |
152 | self.__logger.debug('Running \"{}\" in container: \"{}\"'
153 | .format(cmd, self._policy.container_name))
154 |
155 | def _handle_logs(
156 | self,
157 | log_generator: GeneratorType
158 | ) -> None:
159 | """
160 | Process the logs from a container and print to the logger.
161 |
162 | Expects the `log generator` returned from Docker-py's container.exec_run.
163 | The generator blocks until a new log chunk is available.
164 | The log chunk is of type `bytes`, so it must be decoded before its sent to the logger.
165 | """
166 | for log in log_generator:
167 | if not log:
168 | pass # Sometimes we receive None
169 | elif isinstance(log, GeneratorType):
170 | for text in log:
171 | self.__logger.info(text.decode('utf-8').strip())
172 | else:
173 | try:
174 | self.__logger.info(log.decode('utf-8').strip())
175 | except (UnicodeDecodeError, AttributeError):
176 | self.__logger.exception('Unable to print log of type {}'.format(type(log)))
177 |
178 | async def _start_docker_nodes(
179 | self,
180 | context: LaunchContext
181 | ) -> None:
182 | """
183 | Start the Docker container and load all nodes into it.
184 |
185 | This will first attempt to pull the docker image, start the docker container, and then load
186 | all of the nodes.
187 |
188 | """
189 | # Try to pull the image and warn if it cannot be found.
190 | try:
191 | self._pull_docker_image()
192 | except ImageNotFound as ex:
193 | self.__logger.warn('Image "{}" could not be pulled but may be found locally.'
194 | .format(self._policy.image_name))
195 | self.__logger.debug(ex)
196 |
197 | # Try to run the image (even if it can't be pulled.) It might be available locally
198 | # Log an error if it cannot be found and cancel the future to signal that there is no work.
199 | try:
200 | self._start_docker_container()
201 | except ImageNotFound as ex:
202 | self.__logger.error(
203 | 'Image "{}" could not be found; execution of container "{}" failed.'
204 | .format(self._policy.image_name, self._policy.container_name))
205 | self.__logger.debug(ex)
206 |
207 | with self._shutdown_lock:
208 | if self._completed_future is not None:
209 | self._completed_future.cancel()
210 | self._completed_future = None
211 |
212 | return
213 |
214 | self._load_nodes_in_docker(context)
215 |
216 | def get_asyncio_future(self) -> Optional[asyncio.Future]:
217 | """Return the asyncio Future that represents the lifecycle of the Docker container."""
218 | return self._completed_future
219 |
220 | def execute(
221 | self,
222 | context: LaunchContext
223 | ) -> Optional[List[Action]]:
224 | """
225 | Execute the ROS 2 sandbox inside Docker.
226 |
227 | This will start the Docker container and run each ROS 2 node from inside that container.
228 | There is no additional work required, so this function always returns None.
229 | """
230 | context.register_event_handler(
231 | OnShutdown(
232 | on_shutdown=self.__on_shutdown
233 | )
234 | )
235 |
236 | self._completed_future = create_future(context.asyncio_loop)
237 |
238 | self._started_task = context.asyncio_loop.create_task(
239 | self._start_docker_nodes(context)
240 | )
241 |
242 | return None
243 |
244 | def __on_shutdown(
245 | self,
246 | event: Event,
247 | context: LaunchContext
248 | ) -> Optional[SomeActionsType]:
249 | """
250 | Run when the shutdown signal has been received.
251 |
252 | This will cancel the started task, if running, call cancel
253 | on the completed future, and stop the container.
254 |
255 | """
256 | with self._shutdown_lock:
257 |
258 | # if still starting cancel
259 | if self._started_task is not None:
260 | try:
261 | self._started_task.cancel()
262 | except asyncio.CancelledError:
263 | self._started_task = None
264 |
265 | if self._completed_future is not None:
266 | self._executor.shutdown(wait=False)
267 | self._completed_future.cancel()
268 | self._completed_future = None
269 |
270 | if self._container is not None:
271 | self._container.stop()
272 | self._container = None
273 |
274 | return None
275 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------