├── .github ├── CODEOWNERS ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── autoapprove.yml │ ├── automerge.yml │ ├── lint.yml │ └── test.yml ├── .gitignore ├── .readthedocs.yml ├── .vale.ini ├── CHANGELOG.rst ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── codecov.yml ├── docs ├── Makefile ├── make.bat ├── requirements.txt ├── source │ ├── actions.rst │ ├── conf.py │ ├── descriptions.rst │ ├── index.md │ ├── launch_ros_sandbox.rst │ └── modules.rst └── sphinx_build_symlink │ └── setup.py ├── examples ├── bad_image.launch.py ├── cpu_limit_sandbox_docker.launch.py ├── demo_nodes_run_as.py ├── local_image.launch.py ├── mem_limit_sandbox_docker.launch.py ├── minimal_sandbox_docker.launch.py ├── minimal_sandboxed_node_container.launch.py ├── minimal_sandboxed_run_as.launch.py ├── run_as.py └── talker_listener_sandbox_docker.launch.py ├── launch_ros_sandbox.dashing.repos ├── launch_ros_sandbox.repos ├── launch_ros_sandbox ├── __init__.py ├── actions │ ├── __init__.py │ ├── load_docker_nodes.py │ ├── load_runas_nodes.py │ └── sandboxed_node_container.py └── descriptions │ ├── __init__.py │ ├── docker_policy.py │ ├── policy.py │ ├── sandboxed_node.py │ ├── user.py │ └── user_policy.py ├── package.xml ├── resource └── launch_ros_sandbox ├── setup.cfg ├── setup.py └── test ├── config └── mypy.ini ├── dummy-dashing.Dockerfile ├── launch_ros_sandbox ├── actions │ └── test_sandboxed_node_container.py └── descriptions │ ├── test_docker_policy.py │ ├── test_user.py │ └── test_user_policy.py ├── test_copyright.py ├── test_docker_policy.sh ├── test_docker_policy_bad_image.sh ├── test_docker_policy_local_image.sh ├── test_docker_policy_output.sh ├── test_docker_policy_run_args.sh ├── test_flake8.py ├── test_mypy.py ├── test_pep257.py ├── test_restrict_cpus.sh └── test_run_as.Dockerfile /.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 | -------------------------------------------------------------------------------- /.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/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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.vale.ini: -------------------------------------------------------------------------------- 1 | StylesPath = styles 2 | MinAlertLevel = suggestion 3 | 4 | [*.md] 5 | BasedOnStyles = Google, Vale, write-good 6 | 7 | Vale.Spelling = YES 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | launch_ros_sandbox 2 | Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | -------------------------------------------------------------------------------- /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 | ![License](https://img.shields.io/github/license/ros-security/launch_ros_sandbox) 8 | [![Documentation Status](https://readthedocs.org/projects/launch_ros_sandbox/badge/?version=latest)](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` | [![Test Pipeline Status](https://github.com/ros-security/launch_ros_sandbox/workflows/Test%20launch_ros_sandbox/badge.svg)](https://github.com/ros-security/launch_ros_sandbox/actions) | N/A | N/A | N/A | N/A | 140 | | Dashing | `dashing-devel` | [![Build Status](http://build.ros2.org/buildStatus/icon?job=Ddev__launch_ros_sandbox__ubuntu_bionic_amd64)](http://build.ros2.org/job/Ddev__launch_ros_sandbox__ubuntu_bionic_amd64) | [![Build Status](http://build.ros2.org/buildStatus/icon?job=Dsrc_uB__launch_ros_sandbox__ubuntu_bionic__source)](http://build.ros2.org/job/Dsrc_uB__launch_ros_sandbox__ubuntu_bionic__source) | [![Build Status](http://build.ros2.org/buildStatus/icon?job=Dbin_uB64__launch_ros_sandbox__ubuntu_bionic_amd64__binary)](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 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | fixes: 2 | - "ros2_ws/src/launch_ros_sandbox/::" 3 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/source/index.md: -------------------------------------------------------------------------------- 1 | # launch_ros_sandbox 2 | 3 | ![License](https://img.shields.io/github/license/ros-security/launch_ros_sandbox) 4 | [![Documentation Status](https://readthedocs.org/projects/launch_ros_sandbox/badge/?version=latest)](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` | [![Build Status](https://travis-ci.com/ros-security/launch_ros_sandbox.svg?branch=master)](https://travis-ci.com/ros-security/launch_ros_sandbox) | N/A | N/A | N/A | N/A | 131 | | Dashing | `dashing-devel` | [![Build Status](http://build.ros2.org/buildStatus/icon?job=Ddev__launch_ros_sandbox__ubuntu_bionic_amd64)](http://build.ros2.org/job/Ddev__launch_ros_sandbox__ubuntu_bionic_amd64) | [![Build Status](http://build.ros2.org/buildStatus/icon?job=Dsrc_uB__launch_ros_sandbox__ubuntu_bionic__source)](http://build.ros2.org/job/Dsrc_uB__launch_ros_sandbox__ubuntu_bionic__source) | [![Build Status](http://build.ros2.org/buildStatus/icon?job=Dbin_uB64__launch_ros_sandbox__ubuntu_bionic_amd64__binary)](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 | -------------------------------------------------------------------------------- /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/source/modules.rst: -------------------------------------------------------------------------------- 1 | launch_ros_sandbox 2 | ================== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | launch_ros_sandbox 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/__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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /resource/launch_ros_sandbox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ros-tooling/launch_ros_sandbox/0cbeea6b160267e119de95b15411ef78c1c13ddf/resource/launch_ros_sandbox -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [develop] 2 | script-dir=$base/lib/launch_ros_sandbox 3 | [install] 4 | install-scripts=$base/lib/launch_ros_sandbox 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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_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_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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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_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_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 -------------------------------------------------------------------------------- /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_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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 --------------------------------------------------------------------------------