├── .dockerignore ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── Dockerfile ├── LICENSE ├── QUICKSTART.md ├── README.md ├── SECURITY.md ├── fogros2 ├── CHANGELOG.rst ├── configs │ ├── cyclonedds.ubuntu.2004.xml │ └── cyclonedds.ubuntu.2204.xml ├── fogros2 │ ├── __init__.py │ ├── aws_cloud_instance.py │ ├── cloud_instance.py │ ├── cloud_node.py │ ├── command │ │ ├── __init__.py │ │ └── fog.py │ ├── command_builder.py │ ├── dds_config_builder.py │ ├── gcp_cloud_instance.py │ ├── kubernetes │ │ ├── __init__.py │ │ └── generic.py │ ├── launch_description.py │ ├── name_generator.py │ ├── scp.py │ ├── util.py │ ├── verb │ │ ├── __init__.py │ │ ├── delete.py │ │ ├── image.py │ │ ├── list.py │ │ └── ssh.py │ ├── vpn.py │ ├── wgconfig.py │ └── wgexec.py ├── get-docker.sh ├── image │ └── Dockerfile ├── launch │ └── cloud.launch.py ├── package.xml ├── requirements.txt ├── resource │ └── fogros2 ├── setup.cfg ├── setup.py ├── test │ ├── test_copyright.py │ ├── test_flake8.py │ └── test_pep257.py └── utils │ ├── __init__.py │ ├── ec2_instance_type_selection.py │ └── region_ami_selection.py ├── fogros2_examples ├── CHANGELOG.rst ├── fogros2_examples │ ├── __init__.py │ ├── listener.py │ └── talker.py ├── launch │ ├── talker.auto_aws.launch.py │ ├── talker.aws.launch.py │ ├── talker.gcp.launch.py │ ├── talker.kube.launch.py │ └── talker.local.launch.py ├── package.xml ├── resource │ └── fogros2_examples ├── setup.cfg ├── setup.py └── test │ ├── test_copyright.py │ ├── test_flake8.py │ └── test_pep257.py └── ros_entrypoint.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | .dockerignore 3 | .gitignore 4 | .github 5 | 6 | *.md -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | 4 | examples/log 5 | examples/build 6 | examples/install 7 | *~ 8 | 9 | # Byte-compiled / optimized / DLL files 10 | __pycache__/ 11 | *.py[cod] 12 | *$py.class 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 | share/python-wheels/ 32 | *.egg-info/ 33 | .installed.cfg 34 | *.egg 35 | MANIFEST 36 | 37 | # PyInstaller 38 | # Usually these files are written by a python script from a template 39 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 40 | *.manifest 41 | *.spec 42 | 43 | # Installer logs 44 | pip-log.txt 45 | pip-delete-this-directory.txt 46 | 47 | # Unit test / coverage reports 48 | htmlcov/ 49 | .tox/ 50 | .nox/ 51 | .coverage 52 | .coverage.* 53 | .cache 54 | nosetests.xml 55 | coverage.xml 56 | *.cover 57 | *.py,cover 58 | .hypothesis/ 59 | .pytest_cache/ 60 | cover/ 61 | 62 | # Translations 63 | *.mo 64 | *.pot 65 | 66 | # Django stuff: 67 | *.log 68 | local_settings.py 69 | db.sqlite3 70 | db.sqlite3-journal 71 | 72 | # Flask stuff: 73 | instance/ 74 | .webassets-cache 75 | 76 | # Scrapy stuff: 77 | .scrapy 78 | 79 | # Sphinx documentation 80 | docs/_build/ 81 | 82 | # PyBuilder 83 | .pybuilder/ 84 | target/ 85 | 86 | # Jupyter Notebook 87 | .ipynb_checkpoints 88 | 89 | # IPython 90 | profile_default/ 91 | ipython_config.py 92 | 93 | # pyenv 94 | # For a library or package, you might want to ignore these files since the code is 95 | # intended to run in multiple environments; otherwise, check them in: 96 | # .python-version 97 | 98 | # pipenv 99 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 100 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 101 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 102 | # install all needed dependencies. 103 | #Pipfile.lock 104 | 105 | # poetry 106 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 107 | # This is especially recommended for binary packages to ensure reproducibility, and is more 108 | # commonly ignored for libraries. 109 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 110 | #poetry.lock 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG UBUNTU_DISTRO=jammy 2 | #rolling is alternative 3 | ARG ROS_DISTRO=humble 4 | FROM ubuntu:${UBUNTU_DISTRO} 5 | 6 | # Set up install, set tzdata 7 | ARG UBUNTU_DISTRO 8 | ARG ROS_DISTRO 9 | ENV TZ=America/Vancouver 10 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone 11 | 12 | # Get ROS key 13 | RUN apt update && apt install -y curl gnupg2 lsb-release && \ 14 | curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg 15 | 16 | # Install apt deps 17 | RUN echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu ${UBUNTU_DISTRO} main" | tee /etc/apt/sources.list.d/ros2.list > /dev/null 18 | RUN apt update && DEBIAN_FRONTEND=noninteractive apt install -y \ 19 | ros-${ROS_DISTRO}-desktop \ 20 | ros-${ROS_DISTRO}-rmw-cyclonedds-cpp \ 21 | iproute2 \ 22 | net-tools \ 23 | python3-colcon-common-extensions \ 24 | python3-pip \ 25 | unzip \ 26 | wireguard 27 | RUN rm -rf /var/lib/apt/lists/* 28 | 29 | # Install AWS dep 30 | RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" 31 | RUN unzip awscliv2.zip && rm awscliv2.zip 32 | RUN ./aws/install 33 | 34 | # Install python deps 35 | RUN python3 -m pip install --no-cache-dir -U boto3 paramiko scp wgconfig 36 | 37 | # Create FogROS2 worspace and build it 38 | ENV ROS_WS=/home/root/fog_ws 39 | RUN mkdir -p ${ROS_WS}/src 40 | WORKDIR ${ROS_WS}/src 41 | COPY . ${ROS_WS}/src/ 42 | WORKDIR ${ROS_WS} 43 | RUN . /opt/ros/${ROS_DISTRO}/setup.sh && \ 44 | colcon build --cmake-clean-cache 45 | 46 | # setup entrypoint 47 | ENV ROS_DISTRO=${ROS_DISTRO} 48 | COPY ./ros_entrypoint.sh / 49 | RUN chmod +x /ros_entrypoint.sh 50 | 51 | ENTRYPOINT [ "/ros_entrypoint.sh" ] 52 | CMD ["bash"] 53 | -------------------------------------------------------------------------------- /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 | 204 | 205 | 206 | ---------------------------- 207 | 208 | 209 | 210 | Software License Agreement (BSD License) 211 | 212 | Redistribution and use in source and binary forms, with or without 213 | modification, are permitted provided that the following conditions 214 | are met: 215 | 216 | * Redistributions of source code must retain the above copyright 217 | notice, this list of conditions and the following disclaimer. 218 | * Redistributions in binary form must reproduce the above 219 | copyright notice, this list of conditions and the following 220 | disclaimer in the documentation and/or other materials provided 221 | with the distribution. 222 | * Neither the name of Willow Garage, Inc. nor the names of its 223 | contributors may be used to endorse or promote products derived 224 | from this software without specific prior written permission. 225 | 226 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 227 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 228 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 229 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 230 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 231 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 232 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 233 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 234 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 235 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 236 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 237 | POSSIBILITY OF SUCH DAMAGE. 238 | -------------------------------------------------------------------------------- /QUICKSTART.md: -------------------------------------------------------------------------------- 1 | Install FogROS 2 and ROS 2 from Scratch 2 | --- 3 | 4 | This is a quick start guide for installing FogROS 2 (and ROS 2) and its requisites from scratch (e.g., in a VM). New contributors to the project can start here. You can also watch our video tutorials here: [part 1](https://youtu.be/IfR0JjOytuE) and [part 2](https://youtu.be/tXH0kxx7LqU) 5 | 6 | 1. Install Ubuntu 20.04 or Ubuntu 22.04. See [here](https://ubuntu.com/tutorials/install-ubuntu-desktop#1-overview) for a tutorial. 7 | 8 | 2. Upgrade 9 | ```bash 10 | sudo apt update 11 | sudo apt upgrade 12 | ``` 13 | 14 | 3. Reboot 15 | ```bash 16 | reboot 17 | ``` 18 | 19 | 4. Get UTF-8 locale installed 20 | 21 | ``` 22 | sudo apt install locales 23 | sudo locale-gen en_US en_US.UTF-8 24 | sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 25 | export LANG=en_US.UTF-8 26 | ``` 27 | 28 | 5. Setup sources for ROS 2 (https://docs.ros.org/en/rolling/Installation/Ubuntu-Install-Debians.html) 29 | 30 | ``` 31 | sudo apt update 32 | sudo apt install curl gnupg2 lsb-release 33 | sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg 34 | echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(source /etc/os-release && echo $UBUNTU_CODENAME) main" | sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null 35 | ``` 36 | 37 | 6. Install ROS 2 Packages (https://docs.ros.org/en/rolling/Installation/Ubuntu-Install-Debians.html) 38 | 39 | ``` 40 | sudo apt update 41 | sudo apt install ros-rolling-desktop 42 | ``` 43 | 44 | 7. Add env to startup 45 | 46 | ``` 47 | echo "source /opt/ros/rolling/setup.bash" >> ~/.bashrc 48 | source /opt/ros/rolling/setup.bash 49 | ``` 50 | 51 | 8. Choose and set a `ROS_DOMAIN_ID` (in range 0 to 101) (https://docs.ros.org/en/rolling/Concepts/About-Domain-ID.html) 52 | 53 | ``` 54 | export ROS_DOMAIN_ID=99 55 | echo 'export ROS_DOMAIN_ID=99' >> ~/.bashrc 56 | ``` 57 | 9. Install colcon and git 58 | 59 | ``` 60 | sudo apt install python3-colcon-common-extensions 61 | sudo apt install git 62 | ``` 63 | 64 | 10. Create a workspace 65 | 66 | ``` 67 | mkdir -p ~/fog_ws/src 68 | ``` 69 | 70 | 11. Clone 71 | 72 | ``` 73 | cd ~/fog_ws/src 74 | git clone -b humble https://github.com/BerkeleyAutomation/FogROS2.git 75 | cp FogROS2/fogros2/configs/cyclonedds.ubuntu.$(lsb_release -rs | sed 's/\.//').xml ../cyclonedds.xml 76 | ``` 77 | 78 | 12. Build 79 | 80 | ``` 81 | #If you see a warning like this, you are fine “On Ubuntu 22.04 this may generate deprecation warnings. These may be ignored.” 82 | cd ~/fog_ws 83 | colcon build 84 | ``` 85 | 86 | 13. Install AWS CLI 87 | 88 | ``` 89 | sudo apt install awscli 90 | ``` 91 | 92 | 14. Configure AWS Basic Settings. To run the next command, you need to have your [security credentials, an output format and AWS Region.](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html) 93 | 94 | ``` 95 | aws configure 96 | ``` 97 | 98 | 15. Install additional dependencies 99 | 100 | ``` 101 | sudo apt install python3-pip wireguard 102 | pip install boto3 paramiko scp wgconfig 103 | ``` 104 | 105 | 16. If using Ubuntu 22.04 106 | 107 | ``` 108 | sudo apt install ros-rolling-rmw-cyclonedds-cpp 109 | ``` 110 | 111 | 17. Run basic example. Note that the last command may take some time to complete especially the first time it is run. If your setup is correct, you should see the talker node publishing. 112 | 113 | ``` 114 | cd ~/fog_ws 115 | source install/setup.bash 116 | export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp 117 | export CYCLONEDDS_URI=file://$(pwd)/install/fogros2/share/fogros2/configs/cyclonedds.ubuntu.$(lsb_release -rs | sed 's/\.//').xml 118 | 119 | ros2 launch fogros2_examples talker.aws.launch.py 120 | ``` 121 | 122 | 18. You are done. Refer to our [README](https://github.com/BerkeleyAutomation/FogROS2/blob/main/README.md) for additional information including [Command Line Interface commands](https://github.com/BerkeleyAutomation/FogROS2#command-line-interface), which allow you do a lot with your cloud instances from the command line, and [Docker installation](https://github.com/BerkeleyAutomation/FogROS2#docker). 123 | 124 | Next we’ll terminate the demo by typing CTRL-C twice. The first one terminates the robot node, the second one terminates the cloud node. 125 | 126 | We can see the cloud computer that FogROS 2 launched for us using the FogROS command-line interface or CLI. “Ros2 fog list” shows that we have one running instance with the name XXX. Finally, we terminate the instance so that we are no longer being charged for it by running “ros2 fog delete XXX”. You can verify that it is being deleted and by running “ros2 fog list” again. Observe the “status shutting down” line. After a short while, running ros2 fog list will show nothing, indicating that the instance is terminated and you are no longer being charged. 127 | 128 | ``` 129 | Typing CTRL-C kills the local instance (e.g., listener) the first time and then the cloud instance the second time 130 | 131 | #To see the name of a FogROS2 instance 132 | ros2 fog list 133 | 134 | #To delete a FogROS2 instance 135 | ros2 fog delete [name] 136 | 137 | ``` 138 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FogROS2 2 | 3 | [`FogROS2`](https://github.com/BerkeleyAutomation/FogROS2) extends ROS 2 [[1]](#1) for cloud deployment of computational graphs in a security-conscious manner. It allows researchers to easily and securely deploy ROS abstractions across cloud providers with minimal effort, thus gaining access to additional computing substrates including CPU cores, GPUs, FPGAs, or TPUs, as well as pre-deployed software made available by other researchers. To do so, `FogROS2` extends the ROS 2 launch system, introducing additional syntax to allow roboticists to specify at launch time which components of their architecture will be deployed to the cloud and which components will be deployed on the edge. 4 | 5 | 6 | - [FogROS2](#fogros2) 7 | - [Install](#install) 8 | - [Quickstart](#quickstart) 9 | - [Docker (Recommended)](#docker-recommended) 10 | - [Natively](#natively) 11 | - [Install Dependencies](#install-dependencies) 12 | - [Launch ROS 2 computational graphs in the cloud](#launch-ros-2-computational-graphs-in-the-cloud) 13 | - [Docker (Recommended)](#docker-recommended-1) 14 | - [Native](#native) 15 | - [Run your own robotics applications](#run-your-own-robotics-applications) 16 | - [Setting Up Automatic Image Transport](#setting-up-automatic-image-transport) 17 | - [Command Line Interface](#command-line-interface) 18 | - [Some Common Issues](#some-common-issues) 19 | - [Running Examples:](#running-examples) 20 | 21 | ## Install 22 | ### Quickstart 23 | If you are new to ROS and Ubuntu, and want to install FogROS 2 (and ROS 2) and its requisites from scratch, follow instructions [here](https://github.com/BerkeleyAutomation/FogROS2/blob/humble/QUICKSTART.md). 24 | ### Docker (Recommended) 25 | Alternatively, you can simplify reproduction using an OS virtualization environment with Docker. You can also watch our video tutorial [here](https://www.youtube.com/embed/oEnmZXojkcI?start=1&end=800). 26 | ```bash 27 | git clone -b humble https://github.com/BerkeleyAutomation/FogROS2 28 | cd FogROS2 29 | 30 | # Install AWS CLI 31 | sudo apt install awscli 32 | 33 | # Configure AWS Basic Settings. To run the next command, you need to have your security credentials, an output format and AWS Region. See https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html 34 | aws configure 35 | 36 | #Build Docker Image 37 | docker build -t fogros2 . 38 | ``` 39 | By default, this command will build a docker image for ROS Rolling and Ubuntu 22.04 (jammy). These defaults can be changed using the `--build-arg` flag (e.g., `docker build -t fogros2:focal-humble . --build-arg UBUNTU_DISTRO=focal --build-arg ROS_DISTRO=humble` will build a ROS Humble image with Ubuntu 20.04 (focal)). 40 | *Note: the Dockerfile is cooked for x86_64. If you're using a workstation with an Arm-based architecture (e.g. an M1), build the container with the `docker build --platform linux/amd64 -t fogros2 .`*. 41 | 42 | ### Natively 43 | `FogROS2` is actually a ROS meta-package, so you can just fetch it in your workspace, build it, source the workspace as an overlay and start using its capabilities. You can also watch our video tutorial [here](https://www.youtube.com/embed/JlV4DhArb8Q?start=1&end=402). 44 | 45 | #### Install Dependencies 46 | 47 | ROS 2 dependencies: 48 | ``` 49 | # If using Ubuntu 22.04 50 | sudo apt install ros-rolling-rmw-cyclonedds-cpp 51 | ``` 52 | 53 | FogROS 2 dependencies: 54 | ``` 55 | sudo apt install python3-pip wireguard unzip 56 | sudo pip3 install wgconfig boto3 paramiko scp 57 | 58 | # Install AWS CLI 59 | sudo apt install awscli 60 | 61 | # Configure AWS Basic Settings. To run the next command, you need to have your security credentials, an output format and AWS Region. See https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html 62 | aws configure 63 | ``` 64 | 65 | ```bash 66 | source /opt/ros//setup.bash 67 | mkdir -p ~/fog_ws/src 68 | cd ~/fog_ws/src 69 | git clone -b humble https://github.com/BerkeleyAutomation/FogROS2 70 | cd ../ 71 | colcon build # re-build the workspace 72 | source install/setup.bash 73 | ``` 74 | 75 | 76 | ## Launch ROS 2 computational graphs in the cloud 77 | 78 | ### Docker (Recommended) 79 | You can see this in our video tutorial [here](https://www.youtube.com/embed/oEnmZXojkcI?start=801) 80 | 81 | ```bash 82 | # launch fogros2 container 83 | docker run -it --rm --net=host -v $HOME/.aws:/root/.aws --cap-add=NET_ADMIN fogros2 84 | 85 | # launch talker node on the cloud 86 | ros2 launch fogros2_examples talker.aws.launch.py 87 | ``` 88 | 89 | (*Note: the Dockerfile is cooked for x86_64. If you're using a workstation with an Arm-based architecture (e.g. an M1), run the container with the `docker run -it --platform linux/amd64 --rm --net=host --cap-add=NET_ADMIN fogros2`*.) 90 | 91 | ### Native 92 | Note: These commands must be run from the root of your ROS workspace. You can see this in our video tutorial [here.](https://www.youtube.com/embed/JlV4DhArb8Q?start=403) 93 | ```bash 94 | source /opt/ros//setup.bash 95 | source install/setup.bash 96 | export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp 97 | export CYCLONEDDS_URI=file://$(pwd)/install/fogros2/share/fogros2/configs/cyclonedds.ubuntu.$(lsb_release -rs | sed 's/\.//').xml 98 | 99 | ros2 launch fogros2_examples talker.aws.launch.py 100 | ``` 101 | 102 | ## Run your own robotics applications 103 | If using, for example, Docker take the following steps: 104 | 105 | Step 1: Mount your robotics application to docker's folder. 106 | For example, 107 | ``` 108 | docker run -it --rm \ 109 | --net=host --cap-add=NET_ADMIN \ 110 | .... 111 | -v FOLDER_IN_YOUR_LOCAL_DIR:/home/root/fog_ws/src/YOUR_PKG_NAME \ 112 | ... 113 | keplerc/ros2:latest /bin/bash 114 | ``` 115 | You may also `git clone` your development repo to the docker container instead. 116 | 117 | 118 | Step 2: Write the FogROS2 launch file. Examples of launch files can be found in the talker*.launch.py [here](https://github.com/BerkeleyAutomation/FogROS2/tree/humble/fogros2_examples/launch). 119 | 120 | 121 | ## Setting Up Automatic Image Transport 122 | Step 1: Identify all topics that need to use a compressed transport. 123 | 124 | Step 2: In a `fogros2.CloudNode`, add the parameter `stream_topics=[]`, where `stream_topics` is a list of tuples 125 | where each tuple is just a pair of `(TOPIC_NAME, TRANSPORT_TYPE)` values. 126 | 127 | `TOPIC_NAME` is the string that represents the name of the topic that publishes `sensor_msgs/Image` 128 | 129 | Valid `TRANSPORT_TYPE` values are `compressed`, `theora`, and `raw` if only `image-transport` and `image-transport-plugins` are installed on the system. `h264` is another valid `TRANSPORT_TYPE` if step 3 is followed. 130 | 131 | Step 3 (Optional): If using H.264, please also clone the H.264 decoder found [here](https://github.com/clydemcqueen/h264_image_transport) into the workspace's src directory. The current repo only contains the encoder and the full image transport pipeline will not work without the decoder also. 132 | 133 | Example of `stream_topics` argument: 134 | 135 | `stream_topics=[('/camera/image_raw', 'h264'), ('/camera2/image_raw', 'compressed')]` 136 | 137 | Adding the above argument to a `fogros2.CloudNode` makes the topic `/camera/image_raw` publish using the `h264 image_transport`, and makes the topic `/camera2/image_raw` publish using the `compressed image_transport`. 138 | 139 | Please note that all cloud nodes that are expecting raw images will be remapped to `TOPIC_NAME/cloud` to remove any topic naming conflicts. (TODO: Automate remapping) 140 | 141 | ## Command Line Interface 142 | We currently support the following CLI commands for easier debugging and development. 143 | 144 | ```bash 145 | # List existing FogROS instances 146 | ros2 fog list 147 | 148 | # Connect via SSH to the corresponding instance (e.g., named "ascent-corona") 149 | # the instance name can be found by the list command above 150 | ros2 fog connect ascent-corona 151 | 152 | # delete the existing FogROS instance (e.g. named "ascent-corona") 153 | ros2 fog delete ascent-corona 154 | # or all of the existing instances 155 | ros2 fog delete all 156 | ``` 157 | 158 | ## Some Common Issues 159 | 1. Warning: _2 packages has stderr outputs: fogros2 fogros2_examples_ after running colcon build. This warning occurs in Ubuntu 22.04 (jammy) builds, but does not affect functionality. See https://github.com/BerkeleyAutomation/FogROS2/issues/45. Your installation should still work. 160 | 2. _[WARN] [1652044293.921367226] [fogros2.scp]: [Errno None] Unable to connect to port 22 on xx.xx.xx.xxx, retrying..._ . This warning occurs when AWS has not yet started the instance. This message should eventually be replaced by _SCP Connected!_ once the instance is started. 161 | 3. _WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behavior with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv_. This warning is often seen when installing packages on the cloud instance, but can be ignored. 162 | 4. _docker: permission denied..._ . Depending on your permissions, you may need to run docker commands with _sudo_. 163 | 164 | ## Running Examples: 165 | We have used FogROS for 3 example use-cases (motion planning, grasp planning, and SLAM map building). Please see our [examples repo](https://github.com/BerkeleyAutomation/fogros2-examples) for these and how to run them. 166 | 167 | 168 | ## References 169 | [1] 170 | Macenski Steven, Foote Tully, Gerkey Brian, Lalancette Chris, and Woodall William, 171 | “Robot Operating System 2: Design, architecture, and uses in the wild,” 172 | Science Robotics, vol. 7, no. 66, p. eabm6074, doi: 10.1126/scirobotics.abm6074. 173 | 174 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | This package conforms to the ROS 2 Vulnerability Disclosure Policy in [REP-2006](https://www.ros.org/reps/rep-2006.html). -------------------------------------------------------------------------------- /fogros2/CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2 | Changelog for package fogros2 3 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 4 | 0.1.7 (2022-07-11) 5 | ------------------ 6 | * temporarily removing awscli as dependency due to regression 7 | 8 | 0.1.6 (2022-06-29) 9 | ------------------ 10 | * added wgconfig and wireguard as dependencies 11 | 12 | 0.1.5 (2022-06-08) 13 | ------------------ 14 | * added dependencies to package.xml to reduce those that are manually downloaded 15 | 16 | 0.1.4 (2022-05-15) 17 | ------------------ 18 | * fixed QUICKSTART.md documentation and removed unnecessary dependencies 19 | * prepared package.xml with list of authors and maintainers for release 20 | * changed environment variable checks from asserts to exceptions 21 | * cleaned up README.md 22 | 23 | 0.1.3 (2022-05-09) 24 | ------------------ 25 | * readded human readable instance name generation 26 | * added checks and interterpretable feedback for client errors 27 | * added environment variable checks 28 | * updated documentation to include new potential issues and remove unnecessary instructions 29 | * fixed colcon warning message about arg parsing in the cloud instance 30 | 31 | 0.1.2 (2022-05-02) 32 | ------------------ 33 | * removed unique name generator and fixed package.xml 34 | 35 | 0.1.1 (2022-05-02) 36 | ------------------ 37 | * updates CLI to use AWS APIs to interface with running instances 38 | * adds support for Ubuntu 20.04 and 22.04 39 | * decouples the launch file and only include the inherited launch 40 | * adds local peer to fix the DDS node discovery 41 | * changes cloud containers to use wg interface 42 | * adds lock to protect cloud instance ready state 43 | * first public release for Humble -------------------------------------------------------------------------------- /fogros2/configs/cyclonedds.ubuntu.2004.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | wg0 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | auto 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /fogros2/configs/cyclonedds.ubuntu.2204.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | auto 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /fogros2/fogros2/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 The Regents of the University of California (Regents) 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 | # Copyright ©2022. The Regents of the University of California (Regents). 16 | # All Rights Reserved. Permission to use, copy, modify, and distribute this 17 | # software and its documentation for educational, research, and not-for-profit 18 | # purposes, without fee and without a signed licensing agreement, is hereby 19 | # granted, provided that the above copyright notice, this paragraph and the 20 | # following two paragraphs appear in all copies, modifications, and 21 | # distributions. Contact The Office of Technology Licensing, UC Berkeley, 2150 22 | # Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-7201, 23 | # otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial 24 | # licensing opportunities. IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY 25 | # FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, 26 | # INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS 27 | # DOCUMENTATION, EVEN IF REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 28 | # DAMAGE. REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT 29 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 30 | # PARTICULAR PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, 31 | # PROVIDED HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE 32 | # MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 33 | 34 | from .aws_cloud_instance import AWSCloudInstance # noqa: F401 35 | from .gcp_cloud_instance import GCPCloudInstance # noqa: F401 36 | from .kubernetes.generic import KubeInstance # noqa: F401 37 | from .cloud_node import CloudNode # noqa: F401 38 | from .launch_description import FogROSLaunchDescription # noqa: F401 39 | -------------------------------------------------------------------------------- /fogros2/fogros2/aws_cloud_instance.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 The Regents of the University of California (Regents) 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 | # Copyright ©2022. The Regents of the University of California (Regents). 16 | # All Rights Reserved. Permission to use, copy, modify, and distribute this 17 | # software and its documentation for educational, research, and not-for-profit 18 | # purposes, without fee and without a signed licensing agreement, is hereby 19 | # granted, provided that the above copyright notice, this paragraph and the 20 | # following two paragraphs appear in all copies, modifications, and 21 | # distributions. Contact The Office of Technology Licensing, UC Berkeley, 2150 22 | # Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-7201, 23 | # otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial 24 | # licensing opportunities. IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY 25 | # FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, 26 | # INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS 27 | # DOCUMENTATION, EVEN IF REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 28 | # DAMAGE. REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT 29 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 30 | # PARTICULAR PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, 31 | # PROVIDED HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE 32 | # MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 33 | 34 | import json 35 | import os 36 | 37 | import boto3 38 | from botocore.exceptions import ClientError 39 | 40 | from .cloud_instance import CloudInstance 41 | from .name_generator import get_unique_name 42 | 43 | 44 | class AWSCloudInstance(CloudInstance): 45 | """AWS Implementation of CloudInstance.""" 46 | 47 | def __init__( 48 | self, 49 | ami_image, 50 | region="us-west-1", 51 | ec2_instance_type="t2.micro", 52 | disk_size=30, 53 | **kwargs, 54 | ): 55 | super().__init__(**kwargs) 56 | self.cloud_service_provider = "AWS" 57 | 58 | self.region = region 59 | self.ec2_instance_type = ec2_instance_type 60 | self.ec2_instance_disk_size = disk_size # GB 61 | self.aws_ami_image = ami_image 62 | 63 | # aws objects 64 | self.ec2_instance = None 65 | self.ec2_resource_manager = boto3.resource("ec2", self.region) 66 | self.ec2_boto3_client = boto3.client("ec2", self.region) 67 | 68 | # Check for name collision among other AWS instances 69 | cur_instances = self.ec2_boto3_client.describe_instances( 70 | Filters=[ 71 | { 72 | "Name": "instance.group-name", 73 | "Values": ["FOGROS2_SECURITY_GROUP"], 74 | }, 75 | {"Name": "tag:FogROS2-Name", "Values": [self._name]}, 76 | ] 77 | ) 78 | while len(cur_instances["Reservations"]) > 0: 79 | self._name = get_unique_name() 80 | cur_instances = self.ec2_boto3_client.describe_instances( 81 | Filters=[ 82 | { 83 | "Name": "instance.group-name", 84 | "Values": ["FOGROS2_SECURITY_GROUP"], 85 | }, 86 | {"Name": "tag:FogROS2-Name", "Values": [self._name]}, 87 | ] 88 | ) 89 | self._working_dir = os.path.join(self._working_dir_base, self._name) 90 | os.makedirs(self._working_dir, exist_ok=True) 91 | 92 | # key & security group names 93 | self.ec2_security_group = "FOGROS2_SECURITY_GROUP" 94 | self.ec2_key_name = f"FogROS2KEY-{self._name}" 95 | self._ssh_key_path = os.path.join( 96 | self._working_dir, f"{self.ec2_key_name}.pem" 97 | ) 98 | 99 | # Auto-delete key pair collision (since any instance using this key 100 | # pair would be terminated already, otherwise it would be found in 101 | # the previous collision check) 102 | self.ec2_boto3_client.delete_key_pair(KeyName=self.ec2_key_name) 103 | 104 | # after config 105 | self._ssh_key = None 106 | self.ec2_security_group_ids = None 107 | 108 | self.create() 109 | 110 | def create(self): 111 | self.logger.info(f"Creating new EC2 instance with name {self._name}") 112 | self.create_security_group() 113 | self.generate_key_pair() 114 | self.create_ec2_instance() 115 | self.info(flush_to_disk=True) 116 | self.connect() 117 | # Uncomment out the next three lines if you are not using a custom AMI 118 | self.install_ros() 119 | self.install_cloud_dependencies() 120 | self.install_colcon() 121 | self.push_ros_workspace() 122 | self.info(flush_to_disk=True) 123 | self._is_created = True 124 | 125 | def info(self, flush_to_disk=True): 126 | info_dict = super().info(flush_to_disk) 127 | info_dict["ec2_region"] = self.region 128 | info_dict["ec2_instance_type"] = self.ec2_instance_type 129 | info_dict["disk_size"] = self.ec2_instance_disk_size 130 | info_dict["aws_ami_image"] = self.aws_ami_image 131 | info_dict["ec2_instance_id"] = self.ec2_instance.instance_id 132 | if flush_to_disk: 133 | with open(os.path.join(self._working_dir, "info"), "w+") as f: 134 | json.dump(info_dict, f) 135 | return info_dict 136 | 137 | def get_default_vpc(self): 138 | response = self.ec2_boto3_client.describe_vpcs( 139 | Filters=[{"Name": "is-default", "Values": ["true"]}] 140 | ) 141 | vpcs = response.get("Vpcs", []) 142 | 143 | if len(vpcs) == 0: 144 | self.logger.warn("No default VPC found. Creating one.") 145 | response = self.ec2_boto3_client.create_default_vpc() 146 | vpc_id = response["Vpc"]["VpcId"] 147 | self.logger.warn(f"Created new default VPC {vpc_id}") 148 | return vpc_id 149 | 150 | if len(vpcs) > 1: 151 | # This shouldn't happen, but just in case, warn. 152 | self.logger.warn( 153 | "Multiple default VPCs. This may lead to undefined behavior." 154 | ) 155 | 156 | vpc_id = vpcs[0].get("VpcId", "") 157 | self.logger.info(f"Using VPC: {vpc_id}") 158 | return vpc_id 159 | 160 | def create_security_group(self): 161 | vpc_id = self.get_default_vpc() 162 | try: 163 | response = self.ec2_boto3_client.describe_security_groups( 164 | GroupNames=[self.ec2_security_group] 165 | ) 166 | security_group_id = response["SecurityGroups"][0]["GroupId"] 167 | except ClientError as e: 168 | # check if the group does not exist. we'll create one in 169 | # that case. Any other error is unexpected and re-thrown. 170 | if e.response["Error"]["Code"] != "InvalidGroup.NotFound": 171 | raise e 172 | 173 | self.logger.warn("Security group does not exist, creating.") 174 | response = self.ec2_boto3_client.create_security_group( 175 | GroupName=self.ec2_security_group, 176 | Description=( 177 | "Security group used by FogROS 2 (safe to delete" 178 | " when FogROS 2 is not in use)" 179 | ), 180 | VpcId=vpc_id, 181 | ) 182 | security_group_id = response["GroupId"] 183 | self.logger.info( 184 | f"Security group {security_group_id} created in vpc {vpc_id}." 185 | ) 186 | 187 | data = self.ec2_boto3_client.authorize_security_group_ingress( 188 | GroupId=security_group_id, 189 | IpPermissions=[ 190 | { 191 | "IpProtocol": "-1", 192 | "FromPort": 0, 193 | "ToPort": 65535, 194 | "IpRanges": [{"CidrIp": "0.0.0.0/0"}], 195 | } 196 | ], 197 | ) 198 | self.logger.info(f"Ingress Successfully Set {data}") 199 | 200 | self.logger.info(f"Using security group id: {security_group_id}") 201 | self.ec2_security_group_ids = [security_group_id] 202 | 203 | def generate_key_pair(self): 204 | ec2_keypair = self.ec2_boto3_client.create_key_pair( 205 | KeyName=self.ec2_key_name 206 | ) 207 | self._ssh_key = ec2_keypair["KeyMaterial"] 208 | 209 | # Since we're writing an SSH key, make sure to write with 210 | # user-only permissions. 211 | with open( 212 | os.open(self._ssh_key_path, os.O_CREAT | os.O_WRONLY, 0o600), "w" 213 | ) as f: 214 | f.write(self._ssh_key) 215 | 216 | def create_ec2_instance(self): 217 | # start EC2 instance 218 | # note that we can start muliple instances at the same time 219 | instances = self.ec2_resource_manager.create_instances( 220 | ImageId=self.aws_ami_image, 221 | MinCount=1, 222 | MaxCount=1, 223 | InstanceType=self.ec2_instance_type, 224 | KeyName=self.ec2_key_name, 225 | SecurityGroupIds=self.ec2_security_group_ids, 226 | ClientToken=f"FogROS2-{self._name}", 227 | TagSpecifications=[ 228 | { 229 | "ResourceType": "instance", 230 | "Tags": [{"Key": "FogROS2-Name", "Value": self._name}], 231 | } 232 | ], 233 | BlockDeviceMappings=[ 234 | { 235 | "DeviceName": "/dev/sda1", 236 | "Ebs": { 237 | "VolumeSize": self.ec2_instance_disk_size, 238 | "VolumeType": "standard", 239 | }, 240 | } 241 | ], 242 | ) 243 | 244 | self.ec2_instance = instances[0] 245 | 246 | # use the boto3 waiter 247 | self.logger.info("Waiting for launching to finish") 248 | self.ec2_instance.wait_until_running() 249 | 250 | # reload instance object until IP 251 | self.ec2_instance.reload() 252 | self._ip = self.ec2_instance.public_ip_address 253 | while not self._ip: 254 | self.ec2_instance.reload() 255 | self.logger.info("Waiting for launching to finish") 256 | self._ip = self.ec2_instance.public_ip_address 257 | self.logger.info( 258 | f"Created {self.ec2_instance_type} instance named {self._name} " 259 | f"with id {self.ec2_instance.id} and public IP address {self._ip}" 260 | ) 261 | -------------------------------------------------------------------------------- /fogros2/fogros2/cloud_instance.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 The Regents of the University of California (Regents) 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 | # Copyright ©2022. The Regents of the University of California (Regents). 16 | # All Rights Reserved. Permission to use, copy, modify, and distribute this 17 | # software and its documentation for educational, research, and not-for-profit 18 | # purposes, without fee and without a signed licensing agreement, is hereby 19 | # granted, provided that the above copyright notice, this paragraph and the 20 | # following two paragraphs appear in all copies, modifications, and 21 | # distributions. Contact The Office of Technology Licensing, UC Berkeley, 2150 22 | # Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-7201, 23 | # otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial 24 | # licensing opportunities. IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY 25 | # FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, 26 | # INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS 27 | # DOCUMENTATION, EVEN IF REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 28 | # DAMAGE. REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT 29 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 30 | # PARTICULAR PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, 31 | # PROVIDED HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE 32 | # MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 33 | 34 | import abc 35 | import json 36 | import os 37 | import subprocess 38 | 39 | from rclpy import logging 40 | 41 | from .command_builder import BashBuilder 42 | from .dds_config_builder import CycloneConfigBuilder 43 | from .name_generator import get_unique_name 44 | from .scp import SCPClient 45 | from .util import ( 46 | MissingEnvironmentVariableException, 47 | instance_dir, 48 | make_zip_file, 49 | ) 50 | 51 | 52 | class CloudInstance(abc.ABC): 53 | """Abstract Base Class for Cloud Instances (e.g., AWS, GCP).""" 54 | 55 | def __init__( 56 | self, 57 | ros_workspace=os.path.dirname(os.getenv("COLCON_PREFIX_PATH")), 58 | working_dir_base=instance_dir(), 59 | launch_foxglove=False, 60 | ): 61 | if "RMW_IMPLEMENTATION" not in os.environ: 62 | raise MissingEnvironmentVariableException( 63 | "RMW_IMPLEMENTATION environment variable not set!" 64 | ) 65 | if "CYCLONEDDS_URI" not in os.environ: 66 | raise MissingEnvironmentVariableException( 67 | "CYCLONEDDS_URI environment variable not set!" 68 | ) 69 | 70 | # others 71 | self.logger = logging.get_logger(__name__) 72 | self.cyclone_builder = None 73 | self.scp = None 74 | self._ip = None 75 | self._vpn_ip = None 76 | self.ros_workspace = ros_workspace 77 | self.ros_distro = os.getenv("ROS_DISTRO") 78 | self.logger.debug(f"Using ROS workspace: {self.ros_workspace}") 79 | self._name = get_unique_name() 80 | self._working_dir_base = working_dir_base 81 | self._working_dir = os.path.join(self._working_dir_base, self._name) 82 | os.makedirs(self._working_dir, exist_ok=True) 83 | self._ssh_key_path = None 84 | self._is_created = False 85 | self.cloud_service_provider = None 86 | self.dockers = [] 87 | self.launch_foxglove = launch_foxglove 88 | self._username = 'ubuntu' 89 | 90 | @abc.abstractmethod 91 | def create(self): 92 | pass 93 | 94 | def info(self, flush_to_disk=True): 95 | info_dict = { 96 | "name": self._name, 97 | "cloud_service_provider": self.cloud_service_provider, 98 | "ros_workspace": self.ros_workspace, 99 | "working_dir": self._working_dir, 100 | "ssh_key_path": self._ssh_key_path, 101 | "public_ip": self._ip, 102 | } 103 | if flush_to_disk: 104 | with open(os.path.join(self._working_dir, "info"), "w+") as f: 105 | json.dump(info_dict, f) 106 | return info_dict 107 | 108 | def force_start_vpn(self): 109 | return True 110 | 111 | def connect(self): 112 | self.scp = SCPClient(self._ip, self._ssh_key_path, username=self._username) 113 | self.scp.connect() 114 | 115 | @property 116 | def ip(self): 117 | return self._ip 118 | 119 | @property 120 | def vpn_ip(self): 121 | # Use this when the VPN IP is not None. 122 | return self._vpn_ip 123 | 124 | @property 125 | def is_created(self): 126 | return self._is_created 127 | 128 | @property 129 | def name(self): 130 | return self._name 131 | 132 | def apt_install(self, args): 133 | self.scp.execute_cmd( 134 | f"sudo DEBIAN_FRONTEND=noninteractive apt-get install -y {args}" 135 | ) 136 | 137 | def pip_install(self, args): 138 | self.scp.execute_cmd(f"python3 -m pip install {args}") 139 | 140 | def install_cloud_dependencies(self): 141 | self.apt_install("wireguard unzip docker.io python3-pip ros-humble-rmw-cyclonedds-cpp") 142 | self.pip_install("boto3") 143 | self.pip_install("paramiko") 144 | self.pip_install("scp") 145 | self.pip_install("wgconfig") 146 | 147 | def install_ros(self): 148 | # setup sources 149 | self.apt_install("software-properties-common gnupg lsb-release") 150 | self.scp.execute_cmd("sudo add-apt-repository universe") 151 | self.scp.execute_cmd( 152 | "sudo curl -sSL " 153 | "https://raw.githubusercontent.com/ros/rosdistro/master/ros.key " 154 | "-o /usr/share/keyrings/ros-archive-keyring.gpg" 155 | ) 156 | self.scp.execute_cmd( 157 | 'echo "deb [arch=$(dpkg --print-architecture) ' 158 | "signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] " 159 | "http://packages.ros.org/ros2/ubuntu $(source /etc/os-release && " 160 | 'echo $UBUNTU_CODENAME) main" | ' 161 | "sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null" 162 | ) 163 | 164 | # Run apt-get update after adding universe and ROS2 repos. 165 | self.scp.execute_cmd("sudo apt-get update") 166 | 167 | # set locale 168 | self.apt_install("locales") 169 | self.scp.execute_cmd("sudo locale-gen en_US en_US.UTF-8") 170 | self.scp.execute_cmd( 171 | "sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8" 172 | ) 173 | self.scp.execute_cmd("export LANG=en_US.UTF-8") 174 | 175 | # install ros2 packages 176 | self.apt_install(f"ros-{self.ros_distro}-desktop") 177 | 178 | # source environment 179 | self.scp.execute_cmd(f"source /opt/ros/{self.ros_distro}/setup.bash") 180 | 181 | def configure_rosbridge(self): 182 | # install rosbridge 183 | self.apt_install(f"ros-{self.ros_distro}-rosbridge-suite") 184 | 185 | # source ros and launch rosbridge through ssh 186 | subprocess.call(f"chmod 400 {self._ssh_key_path}", shell=True) 187 | rosbridge_launch_script = ( 188 | "ssh -o StrictHostKeyChecking=no -i " 189 | f"{self._ssh_key_path}" 190 | f" {self._username}@" 191 | f"{self._ip}" 192 | f' "source /opt/ros/{self.ros_distro}/setup.bash && ' 193 | 'ros2 launch rosbridge_server rosbridge_websocket_launch.xml &"' 194 | ) 195 | self.logger.info(rosbridge_launch_script) 196 | subprocess.Popen(rosbridge_launch_script, shell=True) 197 | 198 | def install_colcon(self): 199 | # ros2 repository 200 | self.scp.execute_cmd( 201 | "sudo sh -c 'echo \"deb [arch=amd64,arm64] " 202 | 'http://repo.ros2.org/ubuntu/main `lsb_release -cs` main" > ' 203 | "/etc/apt/sources.list.d/ros2-latest.list'" 204 | ) 205 | self.scp.execute_cmd( 206 | "curl -s" 207 | " https://raw.githubusercontent.com/ros/rosdistro/master/ros.asc" 208 | " | sudo apt-key add -" 209 | ) 210 | 211 | self.pip_install("colcon-common-extensions") 212 | 213 | def push_ros_workspace(self): 214 | # configure ROS env 215 | workspace_path = self.ros_workspace # os.getenv("COLCON_PREFIX_PATH") 216 | zip_dst = "/tmp/ros_workspace" 217 | make_zip_file(workspace_path, zip_dst) 218 | self.scp.execute_cmd("echo removing old workspace") 219 | self.scp.execute_cmd("rm -rf ros_workspace.zip ros2_ws fog_ws") 220 | # self.scp.send_file(f"{zip_dst}.zip", "/home/ubuntu/") 221 | self.scp.send_file(f"{zip_dst}.tar", "/home/ubuntu/") 222 | # self.scp.execute_cmd("unzip -q /home/ubuntu/ros_workspace.zip") 223 | self.scp.execute_cmd("tar -xf /home/ubuntu/ros_workspace.tar") 224 | self.scp.execute_cmd("echo successfully extracted new workspace") 225 | 226 | def push_to_cloud_nodes(self): 227 | self.scp.send_file( 228 | f"/tmp/to_cloud_{self._name}", "/tmp/to_cloud_nodes" 229 | ) 230 | 231 | def push_and_setup_vpn(self): 232 | self.scp.send_file( 233 | f"/tmp/fogros-cloud.conf{self._name}", "/tmp/fogros-aws.conf" 234 | ) 235 | self.scp.execute_cmd( 236 | "sudo cp /tmp/fogros-aws.conf /etc/wireguard/wg0.conf && " 237 | "sudo chmod 600 /etc/wireguard/wg0.conf && sudo wg-quick up wg0" 238 | ) 239 | 240 | def configure_DDS(self): 241 | # configure DDS 242 | self.cyclone_builder = CycloneConfigBuilder(["10.0.0.1"], username=self._username) 243 | self.cyclone_builder.generate_config_file() 244 | self.scp.send_file("/tmp/cyclonedds.xml", "~/cyclonedds.xml") 245 | 246 | def launch_cloud_node(self): 247 | cmd_builder = BashBuilder() 248 | cmd_builder.append(f"source /opt/ros/{self.ros_distro}/setup.bash") 249 | cmd_builder.append( 250 | f"cd /home/{self._username}/fog_ws && /home/{self._username}/.local/bin/colcon build --cmake-clean-cache" 251 | ) 252 | cmd_builder.append(f". /home/{self._username}/fog_ws/install/setup.bash") 253 | cmd_builder.append(self.cyclone_builder.env_cmd) 254 | ros_domain_id = os.environ.get("ROS_DOMAIN_ID") 255 | if not ros_domain_id: 256 | ros_domain_id = 0 257 | cmd_builder.append( 258 | f"ROS_DOMAIN_ID={ros_domain_id} " 259 | "ros2 launch fogros2 cloud.launch.py" 260 | ) 261 | self.logger.info(cmd_builder.get()) 262 | self.scp.execute_cmd(cmd_builder.get()) 263 | 264 | def add_docker_container(self, cmd): 265 | self.dockers.append(cmd) 266 | 267 | def launch_cloud_dockers(self): 268 | # launch foxglove docker (if launch_foxglove specified) 269 | if self.launch_foxglove: 270 | self.configure_rosbridge() 271 | self.scp.execute_cmd( 272 | "sudo docker run -d --rm -p '8080:8080' " 273 | "ghcr.io/foxglove/studio:latest" 274 | ) 275 | 276 | # launch user specified dockers 277 | for docker_cmd in self.dockers: 278 | self.scp.execute_cmd(docker_cmd) 279 | -------------------------------------------------------------------------------- /fogros2/fogros2/cloud_node.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 The Regents of the University of California (Regents) 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 | # Copyright ©2022. The Regents of the University of California (Regents). 16 | # All Rights Reserved. Permission to use, copy, modify, and distribute this 17 | # software and its documentation for educational, research, and not-for-profit 18 | # purposes, without fee and without a signed licensing agreement, is hereby 19 | # granted, provided that the above copyright notice, this paragraph and the 20 | # following two paragraphs appear in all copies, modifications, and 21 | # distributions. Contact The Office of Technology Licensing, UC Berkeley, 2150 22 | # Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-7201, 23 | # otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial 24 | # licensing opportunities. IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY 25 | # FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, 26 | # INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS 27 | # DOCUMENTATION, EVEN IF REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 28 | # DAMAGE. REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT 29 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 30 | # PARTICULAR PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, 31 | # PROVIDED HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE 32 | # MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 33 | 34 | from launch_ros.actions import Node 35 | 36 | 37 | class CloudNode(Node): 38 | def __init__(self, machine, stream_topics=[], **kwargs): 39 | super().__init__(**kwargs) 40 | self.machine = machine 41 | self.stream_topics = stream_topics 42 | 43 | def __getstate__(self): 44 | # workaround to make pickle not serializing self.machine 45 | state = self.__dict__.copy() 46 | del state["machine"] 47 | return state 48 | 49 | @property 50 | def unique_id(self): 51 | return self.machine.name 52 | -------------------------------------------------------------------------------- /fogros2/fogros2/command/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BerkeleyAutomation/FogROS2/f32b6af197ac30847d86b3b899bca44a6ca56a25/fogros2/fogros2/command/__init__.py -------------------------------------------------------------------------------- /fogros2/fogros2/command/fog.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 The Regents of the University of California (Regents) 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 | # Copyright ©2022. The Regents of the University of California (Regents). 16 | # All Rights Reserved. Permission to use, copy, modify, and distribute this 17 | # software and its documentation for educational, research, and not-for-profit 18 | # purposes, without fee and without a signed licensing agreement, is hereby 19 | # granted, provided that the above copyright notice, this paragraph and the 20 | # following two paragraphs appear in all copies, modifications, and 21 | # distributions. Contact The Office of Technology Licensing, UC Berkeley, 2150 22 | # Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-7201, 23 | # otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial 24 | # licensing opportunities. IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY 25 | # FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, 26 | # INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS 27 | # DOCUMENTATION, EVEN IF REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 28 | # DAMAGE. REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT 29 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 30 | # PARTICULAR PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, 31 | # PROVIDED HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE 32 | # MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 33 | 34 | 35 | from ros2cli.command import CommandExtension, add_subparsers_on_demand 36 | 37 | 38 | class FogCommand(CommandExtension): 39 | """Base 'fog' command ROS 2 CLI extension.""" 40 | 41 | def add_arguments(self, parser, cli_name): 42 | """Add verb parsers.""" 43 | self._subparser = parser 44 | # add arguments and sub-commands of verbs 45 | add_subparsers_on_demand( 46 | parser, cli_name, "_verb", "fogros2.verb", required=False 47 | ) 48 | 49 | def main(self, *, parser, args): 50 | """ 51 | Handle fog command. 52 | 53 | Take in args from CLI, pass to verbs if specified, 54 | otherwise print help. 55 | """ 56 | # in case no verb was passed 57 | if not hasattr(args, "_verb"): 58 | self._subparser.print_help() 59 | return 0 60 | 61 | # call the verb's main method 62 | extension = getattr(args, "_verb") 63 | return extension.main(args=args) 64 | -------------------------------------------------------------------------------- /fogros2/fogros2/command_builder.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 The Regents of the University of California (Regents) 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 | # Copyright ©2022. The Regents of the University of California (Regents). 16 | # All Rights Reserved. Permission to use, copy, modify, and distribute this 17 | # software and its documentation for educational, research, and not-for-profit 18 | # purposes, without fee and without a signed licensing agreement, is hereby 19 | # granted, provided that the above copyright notice, this paragraph and the 20 | # following two paragraphs appear in all copies, modifications, and 21 | # distributions. Contact The Office of Technology Licensing, UC Berkeley, 2150 22 | # Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-7201, 23 | # otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial 24 | # licensing opportunities. IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY 25 | # FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, 26 | # INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS 27 | # DOCUMENTATION, EVEN IF REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 28 | # DAMAGE. REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT 29 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 30 | # PARTICULAR PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, 31 | # PROVIDED HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE 32 | # MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 33 | 34 | import logging 35 | 36 | 37 | class BashBuilder: 38 | def __init__(self, cmd_save_path="/opt/ros_ws/cmd.sh"): 39 | self.cmd_save_path = cmd_save_path 40 | self.command = "" 41 | self.logger = logging.getLogger(__name__) 42 | 43 | def save(self): 44 | with open(self.cmd_save_path, "w+") as f: 45 | f.write(self.command) 46 | self.logger.info(self.command) 47 | 48 | def get(self): 49 | return self.command 50 | 51 | def append(self, cmd): 52 | if self.command: 53 | self.command += f" && {cmd}" 54 | else: 55 | self.command = cmd 56 | -------------------------------------------------------------------------------- /fogros2/fogros2/dds_config_builder.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 The Regents of the University of California (Regents) 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 | # Copyright ©2022. The Regents of the University of California (Regents). 16 | # All Rights Reserved. Permission to use, copy, modify, and distribute this 17 | # software and its documentation for educational, research, and not-for-profit 18 | # purposes, without fee and without a signed licensing agreement, is hereby 19 | # granted, provided that the above copyright notice, this paragraph and the 20 | # following two paragraphs appear in all copies, modifications, and 21 | # distributions. Contact The Office of Technology Licensing, UC Berkeley, 2150 22 | # Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-7201, 23 | # otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial 24 | # licensing opportunities. IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY 25 | # FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, 26 | # INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS 27 | # DOCUMENTATION, EVEN IF REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 28 | # DAMAGE. REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT 29 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 30 | # PARTICULAR PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, 31 | # PROVIDED HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE 32 | # MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 33 | 34 | import lsb_release 35 | 36 | ubuntu_release = lsb_release.get_os_release()["RELEASE"] 37 | 38 | 39 | class DDSConfigBuilder: 40 | def __init__(self, ip_addresses): 41 | """ 42 | Construct new DDSConfigBuilder. 43 | 44 | @param: 45 | ip_addresses: a list of ip addresses of cloud instances/VPN peers 46 | """ 47 | self.ip_addresses = ip_addresses 48 | self.config_save_path = None 49 | self.env_cmd = None 50 | 51 | def generate_config_file(self): 52 | pass 53 | 54 | 55 | class CycloneConfigBuilder(DDSConfigBuilder): 56 | def __init__(self, ip_addresses, username='ubuntu'): 57 | super().__init__(ip_addresses) 58 | self.config_save_path = "/tmp/cyclonedds.xml" 59 | self.env_cmd = ( 60 | "export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp && " 61 | f"export CYCLONEDDS_URI=file:///home/{username}/cyclonedds.xml" 62 | ) 63 | 64 | def generate_config_file(self, extra_peers=[]): 65 | interfaces = """ 66 | 67 | 68 | 69 | """ 70 | 71 | xmlvals = ( 72 | 'xmlns="https://cdds.io/config" ' 73 | 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ' 74 | 'xsi:schemaLocation="https://cdds.io/config ' 75 | "https://raw.githubusercontent.com/eclipse-cyclonedds/" 76 | 'cyclonedds/master/etc/cyclonedds.xsd"' 77 | ) 78 | 79 | peer_xml = "".join(f"\n" for peer in extra_peers) 80 | 81 | template = f""" 82 | 83 | 84 | 85 | {interfaces} 86 | 87 | 88 | 89 | 90 | {peer_xml} 91 | 92 | auto 93 | 94 | 95 | 96 | """ 97 | 98 | with open(self.config_save_path, "w+") as f: 99 | f.write(template) 100 | -------------------------------------------------------------------------------- /fogros2/fogros2/gcp_cloud_instance.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 The Regents of the University of California (Regents) 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 | # Copyright ©2022. The Regents of the University of California (Regents). 16 | # All Rights Reserved. Permission to use, copy, modify, and distribute this 17 | # software and its documentation for educational, research, and not-for-profit 18 | # purposes, without fee and without a signed licensing agreement, is hereby 19 | # granted, provided that the above copyright notice, this paragraph and the 20 | # following two paragraphs appear in all copies, modifications, and 21 | # distributions. Contact The Office of Technology Licensing, UC Berkeley, 2150 22 | # Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-7201, 23 | # otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial 24 | # licensing opportunities. IN NO EVEpNT SHALL REGENTS BE LIABLE TO ANY PARTY 25 | # FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, 26 | # INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS 27 | # DOCUMENTATION, EVEN IF REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 28 | # DAMAGE. REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT 29 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 30 | # PARTICULAR PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, 31 | # PROVIDED HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE 32 | # MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 33 | 34 | import json 35 | import os 36 | 37 | import subprocess 38 | import uuid 39 | 40 | from .cloud_instance import CloudInstance 41 | 42 | from .util import extract_bash_column 43 | 44 | 45 | class GCPCloudInstance(CloudInstance): 46 | """GCP Implementation of CloudInstance.""" 47 | 48 | def __init__( 49 | self, 50 | project_id, 51 | ami_image='projects/ubuntu-os-cloud/global/images/ubuntu-2204-jammy-v20220712a', 52 | zone="us-central1-a", 53 | machine_type="e2-medium", 54 | disk_size=10, 55 | **kwargs, 56 | ): 57 | super().__init__(**kwargs) 58 | self.cloud_service_provider = "GCP" 59 | 60 | id_ = str(uuid.uuid4())[0:8] 61 | self._name = f'fog-{id_}-{self._name}' 62 | 63 | self.zone = zone 64 | self.type = machine_type 65 | self.compute_instance_disk_size = disk_size # GB 66 | self.gcp_ami_image = ami_image 67 | 68 | self._working_dir = os.path.join(self._working_dir_base, self._name) 69 | os.makedirs(self._working_dir, exist_ok=True) 70 | 71 | self._project_id = project_id 72 | 73 | # after config 74 | self._ssh_key = None 75 | 76 | self.create() 77 | 78 | def create(self): 79 | self.logger.info(f"Creating new GCP Compute Engine instance with name {self._name}") 80 | self.create_compute_engine_instance() 81 | self.info(flush_to_disk=True) 82 | self.connect() 83 | self.install_ros() 84 | self.install_colcon() 85 | self.install_cloud_dependencies() 86 | self.push_ros_workspace() 87 | self.info(flush_to_disk=True) 88 | self._is_created = True 89 | 90 | def info(self, flush_to_disk=True): 91 | info_dict = super().info(flush_to_disk) 92 | info_dict["compute_region"] = self.zone 93 | info_dict["compute_instance_type"] = self.type 94 | info_dict["disk_size"] = self.compute_instance_disk_size 95 | info_dict["compute_instance_id"] = self._name 96 | if flush_to_disk: 97 | with open(os.path.join(self._working_dir, "info"), "w+") as f: 98 | json.dump(info_dict, f) 99 | return info_dict 100 | 101 | def create_compute_engine_instance(self): 102 | os.system(f'gcloud config set project {self._project_id}') 103 | 104 | result = subprocess\ 105 | .check_output( 106 | f'gcloud compute instances create {self._name} ' 107 | f'--project={self._project_id} --zone={self.zone} --machine-type={self.type} ' 108 | '--network-interface=network-tier=PREMIUM,subnet=default ' 109 | '--maintenance-policy=MIGRATE --provisioning-model=STANDARD ' 110 | '--scopes=https://www.googleapis.com/auth/devstorage.read_only,' 111 | 'https://www.googleapis.com/auth/logging.write,' 112 | 'https://www.googleapis.com/auth/monitoring.write,' 113 | 'https://www.googleapis.com/auth/servicecontrol,' 114 | 'https://www.googleapis.com/auth/service.management.readonly,' 115 | 'https://www.googleapis.com/auth/trace.append ' 116 | '--create-disk=auto-delete=yes,' 117 | 'boot=yes,' 118 | f'device-name={self._name},' 119 | f'image={self.gcp_ami_image},' 120 | 'mode=rw,' 121 | f'size={self.compute_instance_disk_size},' 122 | f'type=projects/{self._project_id}/zones/{self.zone}/diskTypes/pd-balanced ' 123 | '--no-shielded-secure-boot ' 124 | '--shielded-vtpm ' 125 | '--shielded-integrity-monitoring ' 126 | '--reservation-affinity=any', shell=True).decode() 127 | 128 | # Grab external IP 129 | ip = extract_bash_column(result, 'EXTERNAL_IP') 130 | 131 | # Verifies the response was an ip 132 | if len(ip.split('.')) != 4: 133 | raise Exception(f'Error creating instance: {ip}') 134 | 135 | self._ip = ip 136 | 137 | # Generate SSH keys 138 | os.system(f"printf '\n\n' | gcloud compute ssh {self._name} --zone {self.zone}") 139 | 140 | user = subprocess.check_output('whoami', shell=True).decode().strip() 141 | 142 | # Username 143 | self._username = (open(f'/home/{user}/.ssh/google_compute_engine.pub'). 144 | read()).split(' ')[-1].strip().split('@')[0] 145 | 146 | self._ssh_key_path = f'/home/{user}/.ssh/google_compute_engine' 147 | self._is_created = True 148 | 149 | self.logger.info( 150 | f"Created {self.type} instance named {self._name} " 151 | f"with id {self._name} and public IP address {self._ip}" 152 | ) 153 | -------------------------------------------------------------------------------- /fogros2/fogros2/kubernetes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BerkeleyAutomation/FogROS2/f32b6af197ac30847d86b3b899bca44a6ca56a25/fogros2/fogros2/kubernetes/__init__.py -------------------------------------------------------------------------------- /fogros2/fogros2/kubernetes/generic.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 The Regents of the University of California (Regents) 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 | # Copyright ©2022. The Regents of the University of California (Regents). 16 | # All Rights Reserved. Permission to use, copy, modify, and distribute this 17 | # software and its documentation for educational, research, and not-for-profit 18 | # purposes, without fee and without a signed licensing agreement, is hereby 19 | # granted, provided that the above copyright notice, this paragraph and the 20 | # following two paragraphs appear in all copies, modifications, and 21 | # distributions. Contact The Office of Technology Licensing, UC Berkeley, 2150 22 | # Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-7201, 23 | # otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial 24 | # licensing opportunities. IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY 25 | # FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, 26 | # INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS 27 | # DOCUMENTATION, EVEN IF REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 28 | # DAMAGE. REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT 29 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 30 | # PARTICULAR PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, 31 | # PROVIDED HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE 32 | # MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 33 | 34 | import json 35 | import os 36 | 37 | import subprocess 38 | import time 39 | import uuid 40 | import tempfile 41 | import textwrap 42 | 43 | from ..util import extract_bash_column 44 | 45 | from ..cloud_instance import CloudInstance 46 | 47 | 48 | class KubeInstance(CloudInstance): 49 | """Generic Kubernetes CloudInstance.""" 50 | 51 | def __init__( 52 | self, 53 | container_image="docker.io/njha/fogros2_base", 54 | zone="us-central1-a", 55 | mcpu=0, 56 | mb=0, 57 | **kwargs, 58 | ): 59 | super().__init__(**kwargs) 60 | self.cloud_service_provider = "ONPREM" 61 | 62 | id_ = str(uuid.uuid4())[0:8] 63 | self._name = f"fog-{id_}-{self._name}" 64 | 65 | self.zone = zone 66 | self.type = f"{mcpu}mx{mb}Mb" 67 | self.container_image = container_image 68 | 69 | self._mcpu = mcpu 70 | self._mmb = mb 71 | 72 | self._working_dir = os.path.join(self._working_dir_base, self._name) 73 | os.makedirs(self._working_dir, exist_ok=True) 74 | 75 | # after config 76 | self._ssh_key = None 77 | 78 | self.create() 79 | 80 | def create(self): 81 | self.logger.info(f"Creating new ROS node on Kubernetes with name {self._name}") 82 | self.create_compute_engine_instance() 83 | self.info(flush_to_disk=True) 84 | self.connect() 85 | self.install_cloud_dependencies() 86 | self.push_ros_workspace() 87 | self.info(flush_to_disk=True) 88 | self._is_created = True 89 | 90 | def info(self, flush_to_disk=True): 91 | info_dict = super().info(flush_to_disk) 92 | info_dict["compute_region"] = self.zone 93 | info_dict["compute_instance_type"] = self.type 94 | info_dict["compute_instance_id"] = self._name 95 | if flush_to_disk: 96 | with open(os.path.join(self._working_dir, "info"), "w+") as f: 97 | json.dump(info_dict, f) 98 | return info_dict 99 | 100 | def force_start_vpn(self): 101 | return False 102 | 103 | def create_service_pair(self, pub_key_path: str): 104 | # Instance Selector 105 | selector = { 106 | "edu.berkeley.autolab.fogros/instance": self._name, 107 | } 108 | 109 | # SSH Service 110 | ssh_config: dict = { 111 | "apiVersion": "v1", 112 | "kind": "Service", 113 | "metadata": {"name": f"{self._name}-ssh"}, 114 | "spec": { 115 | "type": "LoadBalancer", 116 | "ports": [ 117 | { 118 | "port": 22, 119 | "targetPort": 22, 120 | "name": "ssh", 121 | "protocol": "TCP", 122 | } 123 | ], 124 | "selector": selector, 125 | }, 126 | } 127 | # VPN Service 128 | vpn_config: dict = { 129 | "apiVersion": "v1", 130 | "kind": "Service", 131 | "metadata": { 132 | "name": f"{self._name}-vpn", 133 | }, 134 | "spec": { 135 | "type": "LoadBalancer", 136 | "ports": [ 137 | { 138 | "port": 51820, 139 | "targetPort": 51820, 140 | "name": "wg", 141 | "protocol": "UDP", 142 | } 143 | ], 144 | "selector": selector, 145 | }, 146 | } 147 | 148 | # Runner Pod 149 | pod_resources = { 150 | "memory": f"{self._mmb}Mi", 151 | "cpu": f"{self._mcpu}m", 152 | } 153 | pod_config: dict = { 154 | "apiVersion": "v1", 155 | "kind": "Pod", 156 | "metadata": { 157 | "name": self._name, 158 | "labels": selector, 159 | }, 160 | "spec": { 161 | "restartPolicy": "Never", 162 | "containers": [ 163 | { 164 | "name": self._name, 165 | "image": self.container_image, 166 | "imagePullPolicy": "Always", 167 | "securityContext": { 168 | "capabilities": { 169 | "add": ["NET_ADMIN", "CAP_SYS_ADMIN"], 170 | }, 171 | "privileged": True, 172 | }, 173 | "resources": { 174 | "requests": pod_resources, 175 | "limits": pod_resources, 176 | }, 177 | "env": [ 178 | { 179 | "name": "SSH_PUBKEY", 180 | "value": open(pub_key_path).read().strip(), 181 | }, 182 | ], 183 | "command": ["/bin/bash"], 184 | "args": [ 185 | "-c", 186 | textwrap.dedent( 187 | """ 188 | echo $SSH_PUBKEY >> '/home/ubuntu/.ssh/authorized_keys' &&\\ 189 | chmod -R u=rwX '/home/ubuntu/.ssh' &&\\ 190 | chown -R 'ubuntu:ubuntu' '/home/ubuntu/.ssh' &&\\ 191 | service ssh restart &&\\ 192 | sleep infinity 193 | """, 194 | ), 195 | ], 196 | }, 197 | ], 198 | }, 199 | } 200 | 201 | # TODO: Use the Kubernetes API (pypy/kubernetes) instead of shelling out to kubectl. 202 | for config in [vpn_config, ssh_config, pod_config]: 203 | file = tempfile.NamedTemporaryFile() 204 | open(file.name, "w").write(json.dumps(config)) 205 | self.logger.debug( 206 | f"Creating {config['kind']}/{config['metadata']['name']}..." 207 | ) 208 | os.system(f"kubectl apply -f {file.name}") 209 | file.close() 210 | 211 | # Poll until services are live... 212 | while True: 213 | if ( 214 | "Running" not 215 | in subprocess.check_output( 216 | f"kubectl get pod {self._name}", shell=True 217 | ).decode() 218 | or "pending" 219 | in subprocess.check_output( 220 | f'kubectl get service {ssh_config["metadata"]["name"]}', shell=True 221 | ).decode() 222 | or "pending" 223 | in subprocess.check_output( 224 | f'kubectl get service {vpn_config["metadata"]["name"]}', shell=True 225 | ).decode() 226 | ): 227 | self.logger.info("Some services still creating...") 228 | time.sleep(5) 229 | else: 230 | break 231 | 232 | self.logger.debug("Extracting IPs") 233 | ssh_data = subprocess.check_output( 234 | f'kubectl get service {ssh_config["metadata"]["name"]}', shell=True 235 | ).decode() 236 | vpn_data = subprocess.check_output( 237 | f'kubectl get service {vpn_config["metadata"]["name"]}', shell=True 238 | ).decode() 239 | 240 | return extract_bash_column(ssh_data, "EXTERNAL-IP"),\ 241 | extract_bash_column(vpn_data, "EXTERNAL-IP") 242 | 243 | def create_compute_engine_instance(self): 244 | # Generate SSH keys 245 | self._ssh_key_path = os.path.expanduser(f"~/.ssh/{self._name}") 246 | os.system(f"ssh-keygen -f {self._ssh_key_path} -q -N ''") 247 | 248 | ssh_ip, vpn_ip = self.create_service_pair(f"{self._ssh_key_path}.pub") 249 | 250 | self._ip = ssh_ip 251 | self._vpn_ip = vpn_ip 252 | 253 | self._username = "ubuntu" 254 | self._is_created = True 255 | 256 | self.logger.info( 257 | f"Created {self.type} instance named {self._name} " 258 | f"with id {self._name} and public IP address {self._ip} with VPN IP {self._vpn_ip}" 259 | ) 260 | -------------------------------------------------------------------------------- /fogros2/fogros2/launch_description.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 The Regents of the University of California (Regents) 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 | # Copyright ©2022. The Regents of the University of California (Regents). 16 | # All Rights Reserved. Permission to use, copy, modify, and distribute this 17 | # software and its documentation for educational, research, and not-for-profit 18 | # purposes, without fee and without a signed licensing agreement, is hereby 19 | # granted, provided that the above copyright notice, this paragraph and the 20 | # following two paragraphs appear in all copies, modifications, and 21 | # distributions. Contact The Office of Technology Licensing, UC Berkeley, 2150 22 | # Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-7201, 23 | # otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial 24 | # licensing opportunities. IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY 25 | # FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, 26 | # INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS 27 | # DOCUMENTATION, EVEN IF REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 28 | # DAMAGE. REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT 29 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 30 | # PARTICULAR PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, 31 | # PROVIDED HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE 32 | # MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 33 | 34 | from typing import TYPE_CHECKING, Iterable, List, Optional, Text, Tuple 35 | 36 | import launch.logging 37 | from launch.action import Action 38 | from launch.actions import DeclareLaunchArgument 39 | from launch.launch_context import LaunchContext 40 | from launch.launch_description_entity import LaunchDescriptionEntity 41 | 42 | if TYPE_CHECKING: 43 | from launch.actions.include_launch_description import ( 44 | IncludeLaunchDescription, 45 | ) # noqa: F401 46 | 47 | import pickle 48 | from collections import defaultdict 49 | from threading import Thread 50 | from time import sleep 51 | 52 | from .vpn import VPN 53 | 54 | 55 | class FogROSLaunchDescription(LaunchDescriptionEntity): 56 | """ 57 | Description of a launch-able system. 58 | 59 | The description is expressed by a collection of entities which represent 60 | the system architect's intentions. 61 | 62 | The description may also have arguments, which are declared by 63 | :py:class:`launch.actions.DeclareLaunchArgument` actions within this 64 | launch description. 65 | 66 | Arguments for this description may be accessed via the 67 | :py:meth:`get_launch_arguments` method. 68 | The arguments are gathered by searching through the entities in this 69 | launch description and the descriptions of each entity (which may include 70 | entities yielded by those entities). 71 | """ 72 | 73 | def __init__( 74 | self, 75 | initial_entities: Optional[Iterable[LaunchDescriptionEntity]] = None, 76 | *, 77 | deprecated_reason: Optional[Text] = None, 78 | ) -> None: 79 | """Create a LaunchDescription.""" 80 | self.__entities = [] 81 | self.__to_cloud_entities = defaultdict(list) 82 | self.__streamed_topics = [] 83 | if initial_entities: 84 | for entity in initial_entities: 85 | self.add_entity_with_filter(entity) 86 | 87 | self.__deprecated_reason = deprecated_reason 88 | 89 | def visit( 90 | self, context: LaunchContext 91 | ) -> Optional[List[LaunchDescriptionEntity]]: 92 | """Override LaunchDescriptionEntity to visit contained entities.""" 93 | # dump the to cloud nodes into different files 94 | for key, value in self.__to_cloud_entities.items(): 95 | with open(f"/tmp/to_cloud_{key}", "wb+") as f: 96 | print(f"{key}: to be dumped") 97 | dumped_node_str = pickle.dumps(value) 98 | f.write(dumped_node_str) 99 | 100 | # create VPN credentials to all of the machines 101 | machines = [ 102 | self.__to_cloud_entities[n][0].machine 103 | for n in self.__to_cloud_entities 104 | ] 105 | vpn = VPN() 106 | vpn.generate_wg_config_files(machines) 107 | if any([machine.force_start_vpn() for machine in machines]): 108 | vpn.start_robot_vpn() 109 | 110 | # tell remote machine to push the to cloud nodes and 111 | # wait here until all the nodes are done 112 | for machine in machines: 113 | while not machine.is_created: 114 | print(f"Waiting for machine {machine.name}") 115 | sleep(1) 116 | # machine is ready, # push to_cloud and setup vpn 117 | machine.push_to_cloud_nodes() 118 | machine.push_and_setup_vpn() 119 | machine.configure_DDS() 120 | machine.launch_cloud_dockers() 121 | thread = Thread(target=machine.launch_cloud_node, args=[]) 122 | thread.start() 123 | 124 | if self.__deprecated_reason is not None: 125 | if "current_launch_file_path" in context.get_locals_as_dict(): 126 | message = "launch file [{}] is deprecated: {}".format( 127 | context.locals.current_launch_file_path, 128 | self.__deprecated_reason, 129 | ) 130 | else: 131 | message = "deprecated launch description: {}".format( 132 | self.__deprecated_reason 133 | ) 134 | launch.logging.get_logger().warning(message) 135 | return self.__entities 136 | 137 | def describe_sub_entities(self) -> List[LaunchDescriptionEntity]: 138 | """Override from LaunchDescriptionEntity to return sub entities.""" 139 | return self.__entities 140 | 141 | def get_launch_arguments( 142 | self, conditional_inclusion=False 143 | ) -> List[DeclareLaunchArgument]: 144 | """ 145 | Return list of :py:class:`launch.actions.DeclareLaunchArgument`. 146 | 147 | See 148 | :py:method:`get_launch_arguments_with_include_launch_description_actions()` 149 | for more details. 150 | """ 151 | return [ 152 | item[0] 153 | for item in 154 | self.get_launch_arguments_with_include_launch_description_actions( 155 | conditional_inclusion 156 | ) 157 | ] 158 | 159 | def get_launch_arguments_with_include_launch_description_actions( 160 | self, conditional_inclusion=False 161 | ) -> List[Tuple[DeclareLaunchArgument, List["IncludeLaunchDescription"]]]: 162 | """ 163 | Return list of launch args with associated actions. 164 | 165 | The first element of the tuple is a declare launch argument action. 166 | The second is `None` if the argument was declared at the top level of 167 | this launch description, if not it's a list with all the nested 168 | include launch description actions involved. 169 | 170 | This list is generated (never cached) by searching through this launch 171 | description for any instances of the action that declares launch 172 | arguments. 173 | 174 | It will use 175 | :py:meth:`launch.LaunchDescriptionEntity.describe_sub_entities` 176 | and 177 | :py:meth:`launch.LaunchDescriptionEntity.describe_conditional_sub_entities` 178 | in order to discover as many instances of the declare launch argument 179 | actions as is possible. 180 | Also, specifically in the case of the 181 | :py:class:`launch.actions.IncludeLaunchDescription` action, the method 182 | :py:meth:`launch.LaunchDescriptionSource.try_get_launch_description_without_context` 183 | is used to attempt to load launch descriptions without the "runtime" 184 | context available. 185 | This function may fail, e.g. if the path to the launch file to include 186 | uses the values of launch configurations that have not been set yet, 187 | and in that case the failure is ignored and the arugments defined in 188 | those launch files will not be seen either. 189 | 190 | Duplicate declarations of an argument are ignored, therefore the 191 | default value and description from the first instance of the argument 192 | declaration is used. 193 | """ 194 | from launch.actions import IncludeLaunchDescription # noqa: F811 195 | 196 | declared_launch_arguments: List[ 197 | Tuple[DeclareLaunchArgument, List[IncludeLaunchDescription]] 198 | ] = [] 199 | from launch.actions import ResetLaunchConfigurations 200 | 201 | def process_entities( 202 | entities, *, _conditional_inclusion, nested_ild_actions=None 203 | ): 204 | for entity in entities: 205 | if isinstance(entity, DeclareLaunchArgument): 206 | # Avoid duplicate entries with the same name. 207 | if entity.name in ( 208 | e.name for e, _ in declared_launch_arguments 209 | ): 210 | continue 211 | # Stuff this contextual information into the class for 212 | # potential use in command-line descriptions or errors. 213 | entity._conditionally_included = _conditional_inclusion 214 | entity._conditionally_included |= ( 215 | entity.condition is not None 216 | ) 217 | declared_launch_arguments.append( 218 | (entity, nested_ild_actions) 219 | ) 220 | if isinstance(entity, ResetLaunchConfigurations): 221 | # Launch arguments after this cannot be set directly 222 | # by top level arguments 223 | return 224 | else: 225 | next_nested_ild_actions = nested_ild_actions 226 | if isinstance(entity, IncludeLaunchDescription): 227 | if next_nested_ild_actions is None: 228 | next_nested_ild_actions = [] 229 | next_nested_ild_actions.append(entity) 230 | process_entities( 231 | entity.describe_sub_entities(), 232 | _conditional_inclusion=False, 233 | nested_ild_actions=next_nested_ild_actions, 234 | ) 235 | for ( 236 | conditional_sub_entity 237 | ) in entity.describe_conditional_sub_entities(): 238 | process_entities( 239 | conditional_sub_entity[1], 240 | _conditional_inclusion=True, 241 | nested_ild_actions=next_nested_ild_actions, 242 | ) 243 | 244 | process_entities( 245 | self.entities, _conditional_inclusion=conditional_inclusion 246 | ) 247 | 248 | return declared_launch_arguments 249 | 250 | @property 251 | def entities(self) -> List[LaunchDescriptionEntity]: 252 | """Getter for the entities.""" 253 | return self.__entities 254 | 255 | def add_entity(self, entity: LaunchDescriptionEntity) -> None: 256 | """Add an entity to the LaunchDescription.""" 257 | # self.__entities.append(entity) 258 | self.add_entity_with_filter(entity) 259 | 260 | def add_entity_with_filter(self, entity): 261 | if entity.__class__.__name__ == "CloudNode": 262 | self.__to_cloud_entities[entity.unique_id].append(entity) 263 | if entity.stream_topics: 264 | for stream_topic in entity.stream_topics: 265 | self.add_image_transport_entities( 266 | stream_topic[0], stream_topic[1], entity.machine 267 | ) 268 | else: 269 | self.__entities.append(entity) 270 | 271 | def add_image_transport_entities( 272 | self, topic_name, intermediate_transport, machine 273 | ): 274 | """Add image transport nodes to the cloud and robot.""" 275 | from launch_ros.actions import Node 276 | 277 | import fogros2 278 | 279 | self.__streamed_topics.append(topic_name) 280 | new_cloud_topic_name = f"{topic_name}/cloud" 281 | print( 282 | f"Added {intermediate_transport} transport decoder/subscriber " 283 | f"for topic {topic_name}" 284 | ) 285 | decoder_node = fogros2.CloudNode( 286 | machine=machine, 287 | package="image_transport", 288 | executable="republish", 289 | output="screen", 290 | name="republish_node", 291 | arguments=[ 292 | intermediate_transport, # Input 293 | "raw", # Output 294 | ], 295 | remappings=[ 296 | ( 297 | f"in/{intermediate_transport}", 298 | f"{topic_name}/{intermediate_transport}", 299 | ), 300 | ("out", new_cloud_topic_name), 301 | ], 302 | ) 303 | 304 | self.__to_cloud_entities[decoder_node.unique_id].append(decoder_node) 305 | 306 | print( 307 | f"Added {intermediate_transport} transport encoder/publisher " 308 | f"for topic {topic_name}" 309 | ) 310 | encoder_node = Node( 311 | package="image_transport", 312 | executable="republish", 313 | output="screen", 314 | name="republish_node2", 315 | arguments=[ 316 | "raw", # Input 317 | intermediate_transport, # Output 318 | ], 319 | remappings=[ 320 | ("in", topic_name), 321 | ( 322 | f"out/{intermediate_transport}", 323 | f"{topic_name}/{intermediate_transport}", 324 | ), 325 | ], 326 | ) 327 | self.__entities.append(encoder_node) 328 | 329 | def add_action(self, action: Action) -> None: 330 | """Add an action to the LaunchDescription.""" 331 | self.add_entity(action) 332 | 333 | @property 334 | def deprecated(self) -> bool: 335 | """Getter for deprecated.""" 336 | return self.__deprecated_reason is not None 337 | 338 | @property 339 | def deprecated_reason(self) -> Optional[Text]: 340 | """ 341 | Getter for deprecated. 342 | 343 | Returns `None` if the launch description is not deprecated. 344 | """ 345 | return self.__deprecated_reason 346 | -------------------------------------------------------------------------------- /fogros2/fogros2/scp.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 The Regents of the University of California (Regents) 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 | # Copyright ©2022. The Regents of the University of California (Regents). 16 | # All Rights Reserved. Permission to use, copy, modify, and distribute this 17 | # software and its documentation for educational, research, and not-for-profit 18 | # purposes, without fee and without a signed licensing agreement, is hereby 19 | # granted, provided that the above copyright notice, this paragraph and the 20 | # following two paragraphs appear in all copies, modifications, and 21 | # distributions. Contact The Office of Technology Licensing, UC Berkeley, 2150 22 | # Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-7201, 23 | # otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial 24 | # licensing opportunities. IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY 25 | # FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, 26 | # INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS 27 | # DOCUMENTATION, EVEN IF REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 28 | # DAMAGE. REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT 29 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 30 | # PARTICULAR PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, 31 | # PROVIDED HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE 32 | # MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 33 | 34 | import select 35 | import sys 36 | from time import sleep 37 | 38 | import paramiko 39 | from rclpy import logging 40 | from scp import SCPClient as SCPClientBase 41 | 42 | # ec2 console coloring 43 | CRED = "\033[91m" 44 | CEND = "\033[0m" 45 | 46 | 47 | class SCPClient: 48 | def __init__(self, ip, ssh_key_path, username=None): 49 | self.ip = ip 50 | self.ssh_key = paramiko.RSAKey.from_private_key_file(ssh_key_path) 51 | self.ssh_client = paramiko.SSHClient() 52 | self.logger = logging.get_logger(__name__) 53 | 54 | if username is None: 55 | self.username = 'ubuntu' 56 | else: 57 | self.username = username 58 | 59 | def connect(self): 60 | self.ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 61 | connected = False 62 | while not connected: 63 | try: 64 | self.ssh_client.connect( 65 | hostname=self.ip, 66 | username=self.username, 67 | pkey=self.ssh_key, 68 | look_for_keys=False, 69 | ) 70 | connected = True 71 | # TODO: Handle specific exceptions differently? 72 | # See https://docs.paramiko.org/en/stable/api/client.html 73 | except Exception as e: 74 | self.logger.warn(f"{e}, retrying...") 75 | sleep(5) 76 | self.logger.info("SCP connected!") 77 | 78 | def send_file(self, src_path, dst_path): 79 | with SCPClientBase(self.ssh_client.get_transport()) as scp: 80 | scp.put(src_path, dst_path) 81 | 82 | def execute_cmd(self, cmd): 83 | timeout = 300 84 | stdin, stdout, stderr = self.ssh_client.exec_command( 85 | cmd, get_pty=False 86 | ) 87 | 88 | # See https://stackoverflow.com/a/32758464 89 | ch = stdout.channel # channel shared by stdin, stdout, stderr 90 | stdin.close() # we don't need stdin 91 | ch.shutdown_write() # not going to write 92 | while not ch.closed: 93 | readq, _, _ = select.select([ch], [], [], timeout) 94 | for c in readq: 95 | if c.recv_ready(): 96 | sys.stdout.buffer.write(c.recv(len(c.in_buffer))) 97 | sys.stdout.buffer.flush() 98 | if c.recv_stderr_ready(): 99 | sys.stderr.buffer.write( 100 | c.recv_stderr(len(c.in_stderr_buffer)) 101 | ) 102 | sys.stderr.buffer.flush() 103 | stdout.close() 104 | stderr.close() 105 | ch.recv_exit_status() 106 | -------------------------------------------------------------------------------- /fogros2/fogros2/util.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 The Regents of the University of California (Regents) 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 | # Copyright ©2022. The Regents of the University of California (Regents). 16 | # All Rights Reserved. Permission to use, copy, modify, and distribute this 17 | # software and its documentation for educational, research, and not-for-profit 18 | # purposes, without fee and without a signed licensing agreement, is hereby 19 | # granted, provided that the above copyright notice, this paragraph and the 20 | # following two paragraphs appear in all copies, modifications, and 21 | # distributions. Contact The Office of Technology Licensing, UC Berkeley, 2150 22 | # Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-7201, 23 | # otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial 24 | # licensing opportunities. IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY 25 | # FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, 26 | # INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS 27 | # DOCUMENTATION, EVEN IF REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 28 | # DAMAGE. REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT 29 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 30 | # PARTICULAR PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, 31 | # PROVIDED HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE 32 | # MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 33 | 34 | import errno 35 | import os 36 | import tarfile 37 | 38 | 39 | _work_dir_cache = None 40 | _instance_dir_cache = None 41 | 42 | 43 | class MissingEnvironmentVariableException(Exception): 44 | pass 45 | 46 | 47 | def _mkdir(path, mode=0o700): 48 | try: 49 | os.mkdir(path, mode=mode) 50 | except OSError as e: 51 | if e.errno != errno.EEXIST: 52 | raise 53 | 54 | 55 | def work_dir(): 56 | global _work_dir_cache 57 | if _work_dir_cache is None: 58 | home = os.path.expanduser("~") 59 | path = os.path.join(home, ".fogros2") 60 | _mkdir(path) 61 | _work_dir_cache = path 62 | return _work_dir_cache 63 | 64 | 65 | def instance_dir(): 66 | global _instance_dir_cache 67 | if _instance_dir_cache is None: 68 | path = os.path.join(work_dir(), "instances") 69 | _mkdir(path) 70 | _instance_dir_cache = path 71 | return _instance_dir_cache 72 | 73 | 74 | # Using Tar not Zip 75 | def make_zip_file(dir_name, target_path): 76 | root_dir, workspace_name = os.path.split(dir_name) 77 | print(root_dir, workspace_name) 78 | base_name = os.path.abspath(target_path) 79 | os.chdir(root_dir) 80 | 81 | tar_compression = '' 82 | archive_name = base_name + '.tar' + '' 83 | archive_dir = os.path.dirname(archive_name) 84 | 85 | # https://stackoverflow.com/questions/16000794/python-tarfile-and-excludes 86 | EXCLUDE_FILES = ['.git'] 87 | 88 | if archive_dir and not os.path.exists(archive_dir): 89 | os.makedirs(archive_dir) 90 | tar = tarfile.open(archive_name, 'w|%s' % tar_compression) 91 | try: 92 | tar.add(workspace_name, filter=lambda x: None if x.name in EXCLUDE_FILES else x) 93 | finally: 94 | tar.close() 95 | return archive_name 96 | 97 | 98 | def extract_bash_column(subprocess_output: str, column_name: str, row_number: int = 0): 99 | """Find the value of any given column value - ex: CLUSTER-IP -> 10.0.015. 100 | 101 | :param subprocess_output: Direct output of subprocess.check_output().decode() 102 | :param column_name: The column name to search for ex: CLUSTER-IP 103 | :param row_number: Defaults to the first data row, row_number = 1 is second data row 104 | :return: String of output value 105 | """ 106 | lines = subprocess_output.split('\n') 107 | if column_name not in lines[0]: 108 | raise LookupError(f"Could not find column {column_name} in {lines[0].strip()}") 109 | column_index = lines[0].index(column_name) 110 | 111 | output_str = '' 112 | while column_index != len(lines[row_number+1]) and lines[row_number+1][column_index] != ' ': 113 | output_str += lines[row_number+1][column_index] 114 | column_index += 1 115 | 116 | return output_str 117 | -------------------------------------------------------------------------------- /fogros2/fogros2/verb/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BerkeleyAutomation/FogROS2/f32b6af197ac30847d86b3b899bca44a6ca56a25/fogros2/fogros2/verb/__init__.py -------------------------------------------------------------------------------- /fogros2/fogros2/verb/delete.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 The Regents of the University of California (Regents) 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 | # Copyright ©2022. The Regents of the University of California (Regents). 16 | # All Rights Reserved. Permission to use, copy, modify, and distribute this 17 | # software and its documentation for educational, research, and not-for-profit 18 | # purposes, without fee and without a signed licensing agreement, is hereby 19 | # granted, provided that the above copyright notice, this paragraph and the 20 | # following two paragraphs appear in all copies, modifications, and 21 | # distributions. Contact The Office of Technology Licensing, UC Berkeley, 2150 22 | # Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-7201, 23 | # otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial 24 | # licensing opportunities. IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY 25 | # FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, 26 | # INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS 27 | # DOCUMENTATION, EVEN IF REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 28 | # DAMAGE. REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT 29 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 30 | # PARTICULAR PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, 31 | # PROVIDED HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE 32 | # MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 33 | 34 | import os 35 | import shutil 36 | 37 | import boto3 38 | from botocore.exceptions import NoRegionError 39 | from ros2cli.verb import VerbExtension 40 | 41 | from ..util import instance_dir 42 | 43 | 44 | class DeleteVerb(VerbExtension): 45 | def add_arguments(self, parser, cli_name): 46 | parser.add_argument( 47 | "name", 48 | type=str, 49 | nargs=1, 50 | help="""FogROS instance name to delete, or 'all' to delete 51 | all FogROS instances""", 52 | ) 53 | parser.add_argument( 54 | "--region", 55 | nargs="*", 56 | help="Set AWS region (overrides config/env settings)", 57 | ) 58 | parser.add_argument( 59 | "--dry-run", 60 | action="store_true", 61 | help="Show what would happen, but do not execute", 62 | ) 63 | 64 | def query_region(self, region, args): 65 | try: 66 | client = boto3.client("ec2", region) 67 | except NoRegionError: 68 | raise RuntimeError( 69 | "AWS is not configured! Please run `aws configure` first." 70 | ) 71 | 72 | if "all" in args.name: 73 | # Any instance with a FogROS2-Name tag. 74 | tag_filter = {"Name": "tag-key", "Values": ["FogROS2-Name"]} 75 | else: 76 | # only instances with specific name tag. 77 | tag_filter = {"Name": "tag:FogROS2-Name", "Values": args.name} 78 | 79 | ec2_instances = client.describe_instances( 80 | Filters=[ 81 | { 82 | "Name": "instance.group-name", 83 | "Values": ["FOGROS2_SECURITY_GROUP"], 84 | }, 85 | tag_filter, 86 | ] 87 | ) 88 | 89 | if len(ec2_instances["Reservations"]) == 0: 90 | print( 91 | "No EC2 instances found with the specified name; " 92 | "check list to be sure name is correct!" 93 | ) 94 | 95 | return [client, ec2_instances] 96 | 97 | def delete_instances(self, client, ec2_instances, dry_run): 98 | delete_count = 0 99 | for res in ec2_instances["Reservations"]: 100 | for inst in res["Instances"]: 101 | tag_map = ( 102 | {t["Key"]: t["Value"] for t in inst["Tags"]} 103 | if "Tags" in inst 104 | else {} 105 | ) 106 | print( 107 | f"Deleting {tag_map.get('FogROS2-Name', '(unknown)')} " 108 | f"{inst['InstanceId']}" 109 | ) 110 | print(f" terminating instance {inst['InstanceId']}") 111 | if not dry_run: 112 | response = client.terminate_instances( 113 | InstanceIds=[inst["InstanceId"]] 114 | ) 115 | if "TerminatingInstances" not in response or inst[ 116 | "InstanceId" 117 | ] not in map( 118 | lambda x: x["InstanceId"], 119 | response["TerminatingInstances"], 120 | ): 121 | raise RuntimeError( 122 | "Could not terminate instance" 123 | f" {inst['InstanceId']}!" 124 | ) 125 | print(f" deleting key pair {inst['KeyName']}") 126 | if not dry_run: 127 | response = client.delete_key_pair(KeyName=inst["KeyName"]) 128 | if response["ResponseMetadata"]["HTTPStatusCode"] != 200: 129 | raise RuntimeError( 130 | f"Could not delete key pair {inst['KeyName']}!" 131 | ) 132 | if "FogROS2-Name" in tag_map: 133 | inst_dir = os.path.join( 134 | instance_dir(), tag_map["FogROS2-Name"] 135 | ) 136 | if os.path.exists(inst_dir): 137 | print(f" removing instance data {inst_dir}") 138 | if not dry_run: 139 | shutil.rmtree(inst_dir) 140 | print(" done.") 141 | delete_count += 1 142 | 143 | return delete_count 144 | 145 | def main(self, *, args): 146 | regions = args.region 147 | if regions is None or len(regions) == 0: 148 | regions = [None] 149 | elif "*" in regions or "all" in regions: 150 | client = boto3.client("ec2") 151 | response = client.describe_regions() 152 | regions = [r["RegionName"] for r in response["Regions"]] 153 | 154 | if len(regions) == 1: 155 | delete_count = self.delete_instances( 156 | *self.query_region(regions[0], args), args.dry_run 157 | ) 158 | else: 159 | from concurrent.futures import ThreadPoolExecutor 160 | 161 | with ThreadPoolExecutor(max_workers=len(regions)) as executor: 162 | futures = [ 163 | executor.submit(self.query_region, r, args) 164 | for r in regions 165 | ] 166 | delete_count = sum( 167 | [ 168 | self.delete_instances(*f.result(), args.dry_run) 169 | for f in futures 170 | ] 171 | ) 172 | 173 | if delete_count == 0: 174 | print("No instances deleted") 175 | -------------------------------------------------------------------------------- /fogros2/fogros2/verb/image.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 The Regents of the University of California (Regents) 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 | # Copyright ©2022. The Regents of the University of California (Regents). 16 | # All Rights Reserved. Permission to use, copy, modify, and distribute this 17 | # software and its documentation for educational, research, and not-for-profit 18 | # purposes, without fee and without a signed licensing agreement, is hereby 19 | # granted, provided that the above copyright notice, this paragraph and the 20 | # following two paragraphs appear in all copies, modifications, and 21 | # distributions. Contact The Office of Technology Licensing, UC Berkeley, 2150 22 | # Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-7201, 23 | # otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial 24 | # licensing opportunities. IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY 25 | # FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, 26 | # INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS 27 | # DOCUMENTATION, EVEN IF REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 28 | # DAMAGE. REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT 29 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 30 | # PARTICULAR PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, 31 | # PROVIDED HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE 32 | # MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 33 | 34 | import boto3 35 | 36 | from botocore.exceptions import NoRegionError 37 | from ros2cli.verb import VerbExtension 38 | 39 | 40 | class ImageVerb(VerbExtension): 41 | 42 | def add_arguments(self, parser, cli_name): 43 | parser.add_argument( 44 | "name", 45 | type=str, 46 | nargs="*", 47 | help="Set instance name. \ 48 | Can be found when running 'ros2 fog list' command next to ssh_key after " 49 | "'FogROS2KEY-'", 50 | ) 51 | parser.add_argument( 52 | "--region", 53 | nargs="*", 54 | help="Set AWS region (overrides config/env settings)", 55 | ) 56 | parser.add_argument( 57 | "--dry-run", 58 | action="store_true", 59 | help="Show what would happen, but do not execute", 60 | ) 61 | 62 | def query_region(self, region, name): 63 | try: 64 | client = boto3.client("ec2", region) 65 | except NoRegionError: 66 | raise RuntimeError( 67 | "AWS is not configured! Please run `aws configure` first." 68 | ) 69 | print("Instance name: ", name) 70 | ec2_instances = client.describe_instances( 71 | Filters=[ 72 | { 73 | "Name": "instance.group-name", 74 | "Values": ["FOGROS2_SECURITY_GROUP"], 75 | }, 76 | {"Name": "tag:FogROS2-Name", "Values": name}, 77 | ] 78 | ) 79 | 80 | if len(ec2_instances["Reservations"]) == 0: 81 | print( 82 | "No EC2 instances found with the specified name; " 83 | "check list to be sure name is correct!" 84 | ) 85 | 86 | return [client, ec2_instances] 87 | 88 | def create_ami(self, client, ec2_instances, dry_run): 89 | image_count = 0 90 | for res in ec2_instances["Reservations"]: 91 | for inst in res["Instances"]: 92 | tag_map = ( 93 | {t["Key"]: t["Value"] for t in inst["Tags"]} 94 | if "Tags" in inst 95 | else {} 96 | ) 97 | print( 98 | f"Converting {tag_map.get('FogROS2-Name', '(unknown)')} " 99 | f"{inst['InstanceId']} to AMI." 100 | ) 101 | name = tag_map["FogROS2-Name"] + "-image" 102 | inst_id = inst['InstanceId'] 103 | 104 | if not dry_run: 105 | response = client.create_image(InstanceId=inst_id, Name=name) 106 | if response["ResponseMetadata"]["HTTPStatusCode"] != 200: 107 | raise RuntimeError( 108 | f"Could not create image for {inst['KeyName']}!" 109 | ) 110 | 111 | print("done.") 112 | image_count += 1 113 | 114 | return image_count 115 | 116 | def main(self, *, args): 117 | regions = args.region 118 | if regions is None or len(regions) == 0: 119 | regions = [None] 120 | elif "*" in regions or "all" in regions: 121 | client = boto3.client("ec2") 122 | response = client.describe_regions() 123 | regions = [r["RegionName"] for r in response["Regions"]] 124 | 125 | if len(regions) == 1: 126 | image_count = self.create_ami( 127 | *self.query_region(regions[0], args.name), args.dry_run 128 | ) 129 | else: 130 | from concurrent.futures import ThreadPoolExecutor 131 | 132 | with ThreadPoolExecutor(max_workers=len(regions)) as executor: 133 | futures = [ 134 | executor.submit(self.query_region, r, args.name) 135 | for r in regions 136 | ] 137 | image_count = sum( 138 | [ 139 | self.create_ami(*f.result(), args.dry_run) 140 | for f in futures 141 | ] 142 | ) 143 | 144 | if image_count == 0: 145 | print("No image was created") 146 | -------------------------------------------------------------------------------- /fogros2/fogros2/verb/list.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 The Regents of the University of California (Regents) 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 | # Copyright ©2022. The Regents of the University of California (Regents). 16 | # All Rights Reserved. Permission to use, copy, modify, and distribute this 17 | # software and its documentation for educational, research, and not-for-profit 18 | # purposes, without fee and without a signed licensing agreement, is hereby 19 | # granted, provided that the above copyright notice, this paragraph and the 20 | # following two paragraphs appear in all copies, modifications, and 21 | # distributions. Contact The Office of Technology Licensing, UC Berkeley, 2150 22 | # Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-7201, 23 | # otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial 24 | # licensing opportunities. IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY 25 | # FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, 26 | # INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS 27 | # DOCUMENTATION, EVEN IF REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 28 | # DAMAGE. REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT 29 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 30 | # PARTICULAR PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, 31 | # PROVIDED HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE 32 | # MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 33 | 34 | import boto3 35 | from botocore.exceptions import NoRegionError 36 | from ros2cli.verb import VerbExtension 37 | 38 | 39 | class ListVerb(VerbExtension): 40 | def add_arguments(self, parser, cli_name): 41 | parser.add_argument( 42 | "--region", 43 | nargs="*", 44 | help="Set AWS region (overrides config/env settings)", 45 | ) 46 | 47 | def query_region(self, region): 48 | try: 49 | client = boto3.client("ec2", region) 50 | except NoRegionError: 51 | raise RuntimeError( 52 | "AWS is not configured! Please run `aws configure` first." 53 | ) 54 | 55 | ec2_instances = client.describe_instances( 56 | Filters=[ 57 | { 58 | "Name": "instance.group-name", 59 | "Values": ["FOGROS2_SECURITY_GROUP"], 60 | }, 61 | {"Name": "tag-key", "Values": ["FogROS2-Name"]}, 62 | ] 63 | ) 64 | 65 | # For each instance, we also look up the block device mapping 66 | # to get the disk size. 67 | for res in ec2_instances["Reservations"]: 68 | for inst in res["Instances"]: 69 | volumes = client.describe_volumes( 70 | VolumeIds=[ 71 | m["Ebs"]["VolumeId"] 72 | for m in inst["BlockDeviceMappings"] 73 | ] 74 | ) 75 | # Update the ec2_instances structure to include the 76 | # block device info. 77 | for i in range(len(volumes["Volumes"])): 78 | # Attachments duplicates info we already have, so 79 | # delete it. Not really necessary, but it cleans 80 | # things up when debugging. 81 | del volumes["Volumes"][i]["Attachments"] 82 | inst["BlockDeviceMappings"][i]["Ebs"][ 83 | "VolumeInfo" 84 | ] = volumes["Volumes"][i] 85 | 86 | return [region, ec2_instances] 87 | 88 | def print_region_info(self, region, ec2_instances): 89 | if len(ec2_instances["Reservations"]) == 0: 90 | print("No FogROS instances found") 91 | for res in ec2_instances["Reservations"]: 92 | for inst in res["Instances"]: 93 | tag_map = ( 94 | {t["Key"]: t["Value"] for t in inst["Tags"]} 95 | if "Tags" in inst 96 | else {} 97 | ) 98 | print( 99 | f"====== {tag_map.get('FogROS2-Name', '(unknown)')} ======" 100 | ) 101 | print("cloud_service_provider: AWS") 102 | print(f"ec2_region: {region}") 103 | print( 104 | "ec2_instance_type: " 105 | f"{inst.get('InstanceType', '(unknown)')}" 106 | ) 107 | print( 108 | f"ec2_instance_id: {inst.get('InstanceId', '(unknown)')}" 109 | ) 110 | print(f"public_ip: {inst.get('PublicIpAddress', '(none)')}") 111 | print(f"ssh_key: {inst.get('KeyName', '(unknown)')}") 112 | for bdm in inst["BlockDeviceMappings"]: 113 | if "Ebs" in bdm and "VolumeInfo" in bdm["Ebs"]: 114 | print(f"disk_size: {bdm['Ebs']['VolumeInfo']['Size']}") 115 | print(f"aws_ami_image: {inst.get('ImageId', '(unknown)')}") 116 | print( 117 | f"state: {inst.get('State', {'Name':'(unknown)'})['Name']}" 118 | ) 119 | 120 | def main(self, *, args): 121 | regions = args.region 122 | if regions is None or len(regions) == 0: 123 | regions = [None] # use default (should we default to "all") 124 | elif "*" in regions or "all" in regions: 125 | client = boto3.client("ec2") 126 | response = client.describe_regions() 127 | regions = [r["RegionName"] for r in response["Regions"]] 128 | 129 | if len(regions) == 1: 130 | self.print_region_info(*self.query_region(regions[0])) 131 | else: 132 | from concurrent.futures import ThreadPoolExecutor, as_completed 133 | 134 | with ThreadPoolExecutor(max_workers=len(regions)) as executor: 135 | futures = [ 136 | executor.submit(self.query_region, r) for r in regions 137 | ] 138 | for f in as_completed(futures): 139 | self.print_region_info(*f.result()) 140 | -------------------------------------------------------------------------------- /fogros2/fogros2/verb/ssh.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 The Regents of the University of California (Regents) 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 | # Copyright ©2022. The Regents of the University of California (Regents). 16 | # All Rights Reserved. Permission to use, copy, modify, and distribute this 17 | # software and its documentation for educational, research, and not-for-profit 18 | # purposes, without fee and without a signed licensing agreement, is hereby 19 | # granted, provided that the above copyright notice, this paragraph and the 20 | # following two paragraphs appear in all copies, modifications, and 21 | # distributions. Contact The Office of Technology Licensing, UC Berkeley, 2150 22 | # Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-7201, 23 | # otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial 24 | # licensing opportunities. IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY 25 | # FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, 26 | # INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS 27 | # DOCUMENTATION, EVEN IF REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 28 | # DAMAGE. REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT 29 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 30 | # PARTICULAR PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, 31 | # PROVIDED HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE 32 | # MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 33 | 34 | import os 35 | 36 | import boto3 37 | from botocore.exceptions import NoRegionError 38 | from ros2cli.verb import VerbExtension 39 | 40 | from ..util import instance_dir 41 | 42 | 43 | class SSHVerb(VerbExtension): 44 | def add_arguments(self, parser, cli_name): 45 | parser.add_argument( 46 | "name", 47 | type=str, 48 | nargs=1, 49 | help="FogROS instance name to connect to", 50 | ) 51 | parser.add_argument( 52 | "--region", 53 | nargs="*", 54 | help="Set AWS region (overrides config/env settings)", 55 | ) 56 | parser.add_argument( 57 | "--user", 58 | "-u", 59 | type=str, 60 | nargs="?", 61 | default="ubuntu", 62 | help="User name of the remote SSH instance", 63 | ) 64 | 65 | def query_region(self, region, name): 66 | try: 67 | client = boto3.client("ec2", region) 68 | except NoRegionError: 69 | raise RuntimeError( 70 | "AWS is not configured! Please run `aws configure` first." 71 | ) 72 | 73 | ec2_instances = client.describe_instances( 74 | Filters=[ 75 | { 76 | "Name": "instance.group-name", 77 | "Values": ["FOGROS2_SECURITY_GROUP"], 78 | }, 79 | {"Name": "tag:FogROS2-Name", "Values": name}, 80 | ] 81 | ) 82 | return ec2_instances 83 | 84 | def main(self, *, args): 85 | regions = args.region 86 | 87 | if regions is None or len(regions) == 0: 88 | regions = [None] 89 | elif "*" in regions or "all" in regions: 90 | client = boto3.client("ec2") 91 | response = client.describe_regions() 92 | regions = [r["RegionName"] for r in response["Regions"]] 93 | 94 | if len(regions) == 1: 95 | instances = [self.query_region(regions[0], args.name)] 96 | else: 97 | from concurrent.futures import ThreadPoolExecutor, as_completed 98 | 99 | with ThreadPoolExecutor(max_workers=len(regions)) as executor: 100 | futures = [ 101 | executor.submit(self.query_region, r, args.name) 102 | for r in regions 103 | ] 104 | instances = [f.result() for f in as_completed(futures)] 105 | 106 | for ec2_instances in instances: 107 | for res in ec2_instances["Reservations"]: 108 | for inst in res["Instances"]: 109 | tag_map = ( 110 | {t["Key"]: t["Value"] for t in inst["Tags"]} 111 | if "Tags" in inst 112 | else {} 113 | ) 114 | name = tag_map["FogROS2-Name"] 115 | key_name = inst["KeyName"] 116 | key_path = os.path.join( 117 | instance_dir(), name, f"{key_name}.pem" 118 | ) 119 | if "PublicIpAddress" not in inst: 120 | print( 121 | "Warning: matching instance does not have a " 122 | "public IP address" 123 | ) 124 | continue 125 | public_ip = inst["PublicIpAddress"] 126 | os.execvp( 127 | "ssh", 128 | ("ssh", "-i", key_path, f"{args.user}@{public_ip}"), 129 | ) 130 | 131 | # Since execvp replaces the current process, if here, we 132 | # haven't found a matching instance. 133 | print("No matching instance found") 134 | -------------------------------------------------------------------------------- /fogros2/fogros2/vpn.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 The Regents of the University of California (Regents) 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 | # Copyright ©2022. The Regents of the University of California (Regents). 16 | # All Rights Reserved. Permission to use, copy, modify, and distribute this 17 | # software and its documentation for educational, research, and not-for-profit 18 | # purposes, without fee and without a signed licensing agreement, is hereby 19 | # granted, provided that the above copyright notice, this paragraph and the 20 | # following two paragraphs appear in all copies, modifications, and 21 | # distributions. Contact The Office of Technology Licensing, UC Berkeley, 2150 22 | # Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-7201, 23 | # otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial 24 | # licensing opportunities. IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY 25 | # FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, 26 | # INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS 27 | # DOCUMENTATION, EVEN IF REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 28 | # DAMAGE. REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT 29 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 30 | # PARTICULAR PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, 31 | # PROVIDED HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE 32 | # MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 33 | 34 | import os 35 | 36 | from .wgconfig import WGConfig 37 | from .wgexec import get_publickey, generate_privatekey 38 | 39 | 40 | class VPN: 41 | def __init__( 42 | self, 43 | cloud_key_path="/tmp/fogros-cloud.conf", 44 | robot_key_path="/tmp/fogros-local.conf", 45 | ): 46 | self.cloud_key_path = cloud_key_path 47 | self.robot_key_path = robot_key_path 48 | 49 | self.cloud_name_to_pub_key_path = dict() 50 | self.cloud_name_to_priv_key_path = dict() 51 | 52 | self.robot_private_key = generate_privatekey() 53 | self.robot_public_key = get_publickey(self.robot_private_key) 54 | 55 | def generate_key_pairs(self, machines): 56 | """ 57 | Create key pair for each machine. 58 | 59 | @param machines: List 60 | """ 61 | for machine in machines: 62 | name = machine.name 63 | cloud_private_key = generate_privatekey() 64 | self.cloud_name_to_priv_key_path[name] = cloud_private_key 65 | cloud_public_key = get_publickey(cloud_private_key) 66 | self.cloud_name_to_pub_key_path[name] = cloud_public_key 67 | 68 | def generate_wg_config_files(self, machines): 69 | self.generate_key_pairs(machines) 70 | 71 | # generate cloud configs 72 | counter = 2 # start the static ip addr counter from 2 73 | for machine in machines: 74 | name = machine.name 75 | machine_config_pwd = self.cloud_key_path + name 76 | machine_priv_key = self.cloud_name_to_priv_key_path[name] 77 | aws_config = WGConfig(machine_config_pwd) 78 | aws_config.add_attr(None, "PrivateKey", machine_priv_key) 79 | aws_config.add_attr(None, "ListenPort", 51820) 80 | aws_config.add_attr(None, "Address", f"10.0.0.{counter:d}/24") 81 | aws_config.add_peer(self.robot_public_key, "# fogROS Robot") 82 | aws_config.add_attr( 83 | self.robot_public_key, "AllowedIPs", "10.0.0.1/32" 84 | ) 85 | aws_config.write_file() 86 | counter += 1 87 | 88 | # generate robot configs 89 | robot_config = WGConfig(self.robot_key_path) 90 | robot_config.add_attr(None, "PrivateKey", self.robot_private_key) 91 | robot_config.add_attr(None, "ListenPort", 51820) 92 | robot_config.add_attr(None, "Address", "10.0.0.1/24") 93 | for machine in machines: 94 | name = machine.name 95 | if hasattr(machine, 'vpn_ip') and machine.vpn_ip is not None: 96 | ip = machine.vpn_ip 97 | else: 98 | ip = machine.ip 99 | cloud_pub_key = self.cloud_name_to_pub_key_path[name] 100 | robot_config.add_peer(cloud_pub_key, f"# AWS{name}") 101 | robot_config.add_attr(cloud_pub_key, "AllowedIPs", "10.0.0.2/32") 102 | robot_config.add_attr(cloud_pub_key, "Endpoint", f"{ip}:51820") 103 | robot_config.add_attr(cloud_pub_key, "PersistentKeepalive", 3) 104 | robot_config.write_file() 105 | 106 | def start_robot_vpn(self): 107 | # Copy /tmp/fogros-local.conf to /etc/wireguard/wg0.conf locally. 108 | # Do NOT move this to a SUID executable because that will cause trivial LPE. 109 | os.system("sudo cp /tmp/fogros-local.conf /etc/wireguard/wg0.conf") 110 | os.system("sudo chmod 600 /etc/wireguard/wg0.conf") 111 | os.system("sudo wg-quick down wg0") 112 | os.system("sudo wg-quick up wg0") 113 | -------------------------------------------------------------------------------- /fogros2/fogros2/wgconfig.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 The Regents of the University of California (Regents) 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 | # Copyright ©2022. The Regents of the University of California (Regents). 16 | # All Rights Reserved. Permission to use, copy, modify, and distribute this 17 | # software and its documentation for educational, research, and not-for-profit 18 | # purposes, without fee and without a signed licensing agreement, is hereby 19 | # granted, provided that the above copyright notice, this paragraph and the 20 | # following two paragraphs appear in all copies, modifications, and 21 | # distributions. Contact The Office of Technology Licensing, UC Berkeley, 2150 22 | # Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-7201, 23 | # otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial 24 | # licensing opportunities. IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY 25 | # FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, 26 | # INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS 27 | # DOCUMENTATION, EVEN IF REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 28 | # DAMAGE. REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT 29 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 30 | # PARTICULAR PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, 31 | # PROVIDED HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE 32 | # MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 33 | 34 | 35 | from builtins import str 36 | from builtins import range 37 | from io import open 38 | import os 39 | 40 | 41 | class WGConfig(): 42 | SECTION_FIRSTLINE = '_index_firstline' 43 | SECTION_LASTLINE = '_index_lastline' 44 | SECTION_RAW = '_rawdata' 45 | _interface = None # interface attributes 46 | _peers = None # peer data 47 | 48 | def __init__(self, file, keyattr='PublicKey'): 49 | self.filename = self.file2filename(file) 50 | self.keyattr = keyattr 51 | self.lines = [] 52 | self.initialize_file() 53 | 54 | @staticmethod 55 | def file2filename(file): 56 | if os.path.basename(file) == file: 57 | if not file.endswith('.conf'): 58 | file += '.conf' 59 | file = os.path.join('/etc/wireguard', file) 60 | return file 61 | 62 | def invalidate_data(self): 63 | self._interface = None 64 | self._peers = None 65 | 66 | def read_file(self): 67 | with open(self.filename, 'r') as wgfile: 68 | self.lines = [line.rstrip() for line in wgfile.readlines()] 69 | self.invalidate_data() 70 | 71 | def write_file(self, file=None): 72 | if file is None: 73 | filename = self.filename 74 | else: 75 | filename = self.file2filename(file) 76 | with os.fdopen( 77 | os.open(filename, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o640), 'w') as wgfile: 78 | wgfile.writelines(line + '\n' for line in self.lines) 79 | 80 | @staticmethod 81 | def parse_line(line): 82 | attr, _, value = line.partition('=') 83 | attr = attr.strip() 84 | parts = value.partition('#') 85 | value = parts[0].strip() # strip comments and whitespace 86 | value = str(value) # this line is for Python2 support only 87 | comment = parts[1] + parts[2] 88 | if value.isnumeric(): 89 | value = [int(value)] 90 | else: 91 | # decompose into list based on commata as separator 92 | value = [item.strip() for item in value.split(',')] 93 | return attr, value, comment 94 | 95 | def parse_lines(self): 96 | # There will be two special attributes in the parsed data: 97 | # _index_firstline: Line (zero indexed) of the section header 98 | # (including any leading lines with comments) 99 | # _index_lastline: Line (zero indexed) of the last attribute line of the section 100 | # (including any directly following comments) 101 | 102 | def close_section(section, section_data): 103 | section_data = {k: (v if len(v) > 1 else v[0]) for k, v in section_data.items()} 104 | if section is None: # nothing to close on first section 105 | return 106 | elif section == 'interface': # close interface section 107 | self._interface = section_data 108 | else: # close peer section 109 | peername = section_data.get(self.keyattr) 110 | self._peers[peername] = section_data 111 | section_data[self.SECTION_RAW] = self.lines[section_data[self.SECTION_FIRSTLINE]: 112 | (section_data[self.SECTION_LASTLINE] + 1)] 113 | 114 | self._interface = dict() 115 | self._peers = dict() 116 | section = None 117 | section_data = dict() 118 | last_empty_line_in_section = -1 # virtual empty line before start of file 119 | for i, line in enumerate(self.lines): 120 | # Ignore leading whitespace and trailing whitespace 121 | line = line.strip() 122 | # Ignore empty lines and comments 123 | if len(line) == 0: 124 | last_empty_line_in_section = i 125 | continue 126 | if line.startswith('['): # section 127 | if last_empty_line_in_section is not None: 128 | section_data[self.SECTION_LASTLINE] = [last_empty_line_in_section - 1] 129 | close_section(section, section_data) 130 | section_data = dict() 131 | section = line[1:].partition(']')[0].lower() 132 | if last_empty_line_in_section is None: 133 | section_data[self.SECTION_FIRSTLINE] = [i] 134 | else: 135 | section_data[self.SECTION_FIRSTLINE] = [last_empty_line_in_section + 1] 136 | last_empty_line_in_section = None 137 | section_data[self.SECTION_LASTLINE] = [i] 138 | if section not in ['interface', 'peer']: 139 | raise ValueError('Unsupported section [{0}] in line {1}'.format(section, i)) 140 | elif line.startswith('#'): 141 | section_data[self.SECTION_LASTLINE] = [i] 142 | else: # regular line 143 | attr, value, _comment = self.parse_line(line) 144 | section_data[attr] = section_data.get(attr, []) 145 | section_data[attr].extend(value) 146 | section_data[self.SECTION_LASTLINE] = [i] 147 | close_section(section, section_data) 148 | 149 | def handle_leading_comment(self, leading_comment): 150 | if leading_comment is not None: 151 | if leading_comment.strip()[0] != '#': 152 | raise ValueError('A comment needs to start with a "#"') 153 | self.lines.append(leading_comment) 154 | 155 | def initialize_file(self, leading_comment=None): 156 | self.lines = list() 157 | self.handle_leading_comment(leading_comment) # add leading comment if needed 158 | self.lines.append('[Interface]') 159 | self.invalidate_data() 160 | 161 | def add_peer(self, key, leading_comment=None): 162 | if key in self.peers: 163 | raise KeyError('Peer to be added already exists') 164 | self.lines.append('') # append an empty line for separation 165 | self.handle_leading_comment(leading_comment) # add leading comment if needed 166 | # Append peer with key attribute 167 | self.lines.append('[Peer]') 168 | self.lines.append('{0} = {1}'.format(self.keyattr, key)) 169 | # Invalidate data cache 170 | self.invalidate_data() 171 | 172 | def del_peer(self, key): 173 | if key not in self.peers: 174 | raise KeyError('The peer to be deleted does not exist') 175 | section_firstline = self.peers[key][self.SECTION_FIRSTLINE] 176 | section_lastline = self.peers[key][self.SECTION_LASTLINE] 177 | # Remove a blank line directly before the peer section 178 | if section_firstline > 0: 179 | if len(self.lines[section_firstline - 1]) == 0: 180 | section_firstline -= 1 181 | # Only keep needed lines 182 | result = [] 183 | if section_firstline > 0: 184 | result.extend(self.lines[0:section_firstline]) 185 | result.extend(self.lines[(section_lastline + 1):]) 186 | self.lines = result 187 | # Invalidate data cache 188 | self.invalidate_data() 189 | 190 | def get_sectioninfo(self, key): 191 | if key is None: # interface 192 | section_firstline = self.interface[self.SECTION_FIRSTLINE] 193 | section_lastline = self.interface[self.SECTION_LASTLINE] 194 | else: # peer 195 | if key not in self.peers: 196 | raise KeyError('The specified peer does not exist') 197 | section_firstline = self.peers[key][self.SECTION_FIRSTLINE] 198 | section_lastline = self.peers[key][self.SECTION_LASTLINE] 199 | return section_firstline, section_lastline 200 | 201 | def add_attr(self, key, attr, value, leading_comment=None, append_as_line=False): 202 | section_firstline, section_lastline = self.get_sectioninfo(key) 203 | if leading_comment is not None: 204 | if leading_comment.strip()[0] != '#': 205 | raise ValueError('A comment needs to start with a "#"') 206 | # Look for line with the attribute 207 | line_found = None 208 | for i in range(section_firstline + 1, section_lastline + 1): 209 | line_attr, line_value, line_comment = self.parse_line(self.lines[i]) 210 | if attr == line_attr: 211 | line_found = i 212 | # Add the attribute at the right place 213 | if (line_found is None) or append_as_line: 214 | line_found = section_lastline if (line_found is None) else line_found 215 | line_found += 1 216 | self.lines.insert(line_found, '{0} = {1}'.format(attr, value)) 217 | else: 218 | line_attr, line_value, line_comment = self.parse_line(self.lines[line_found]) 219 | line_value.append(value) 220 | if len(line_comment) > 0: 221 | line_comment = ' ' + line_comment 222 | line_value = [str(item) for item in line_value] 223 | self.lines[line_found] = line_attr + ' = ' + ', '.join(line_value) + line_comment 224 | # Handle leading comments 225 | if leading_comment is not None: 226 | self.lines.insert(line_found, leading_comment) 227 | # Invalidate data cache 228 | self.invalidate_data() 229 | 230 | def del_attr(self, key, attr, value=None, remove_leading_comments=True): 231 | section_firstline, section_lastline = self.get_sectioninfo(key) 232 | # Find all lines with matching attribute name and (if requested) value 233 | line_found = [] 234 | for i in range(section_firstline + 1, section_lastline + 1): 235 | line_attr, line_value, line_comment = self.parse_line(self.lines[i]) 236 | if attr == line_attr: 237 | if (value is None) or (value in line_value): 238 | line_found.append(i) 239 | if len(line_found) == 0: 240 | raise ValueError('The attribute/value to be deleted is not present') 241 | # Process all relevant lines 242 | for i in reversed(line_found): # reversed so that non-processed indices stay valid 243 | if value is None: 244 | del(self.lines[i]) 245 | else: 246 | line_attr, line_value, line_comment = self.parse_line(self.lines[i]) 247 | line_value.remove(value) 248 | if len(line_value) > 0: # keep remaining values in that line 249 | self.lines[i] = line_attr + ' = ' + ', '.join(line_value) + line_comment 250 | else: # otherwise line is no longer needed 251 | del(self.lines[i]) 252 | # Handle leading comments 253 | if remove_leading_comments: 254 | i = line_found[0] - 1 255 | while i > 0: 256 | if len(self.lines[i]) and (self.lines[i][0] == '#'): 257 | del(self.lines[i]) 258 | i -= 1 259 | else: 260 | break 261 | # Invalidate data cache 262 | self.invalidate_data() 263 | 264 | @property 265 | def interface(self): 266 | if self._interface is None: 267 | self.parse_lines() 268 | return self._interface 269 | 270 | @property 271 | def peers(self): 272 | if self._peers is None: 273 | self.parse_lines() 274 | return self._peers 275 | -------------------------------------------------------------------------------- /fogros2/fogros2/wgexec.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 The Regents of the University of California (Regents) 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 | # Copyright ©2022. The Regents of the University of California (Regents). 16 | # All Rights Reserved. Permission to use, copy, modify, and distribute this 17 | # software and its documentation for educational, research, and not-for-profit 18 | # purposes, without fee and without a signed licensing agreement, is hereby 19 | # granted, provided that the above copyright notice, this paragraph and the 20 | # following two paragraphs appear in all copies, modifications, and 21 | # distributions. Contact The Office of Technology Licensing, UC Berkeley, 2150 22 | # Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-7201, 23 | # otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial 24 | # licensing opportunities. IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY 25 | # FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, 26 | # INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS 27 | # DOCUMENTATION, EVEN IF REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 28 | # DAMAGE. REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT 29 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 30 | # PARTICULAR PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, 31 | # PROVIDED HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE 32 | # MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 33 | 34 | 35 | import logging 36 | import shlex 37 | import subprocess 38 | 39 | 40 | logger = logging.getLogger(__name__) 41 | 42 | 43 | def execute(command, input=None, suppressoutput=False, suppresserrors=False): 44 | args = shlex.split(command) 45 | stdin = None if input is None else subprocess.PIPE 46 | input = None if input is None else input.encode('utf-8') 47 | nsp = subprocess.Popen(args, stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 48 | out, err = nsp.communicate(input=input) 49 | if err is not None: 50 | err = err.decode('utf8') 51 | if not suppresserrors and (len(err) > 0): 52 | logger.error(err) 53 | out = out.decode('utf8') 54 | if not suppressoutput and (len(out) > 0): 55 | print(out) 56 | nsp.wait() 57 | return out, err, nsp.returncode 58 | 59 | 60 | def generate_privatekey(): 61 | out, err, returncode = execute('wg genkey', suppressoutput=True) 62 | if (returncode != 0) or (len(err) > 0): 63 | return None 64 | out = out.strip() # remove trailing newline 65 | return out 66 | 67 | 68 | def get_publickey(wg_private): 69 | if wg_private is None: 70 | return None 71 | out, err, returncode = execute('wg pubkey', input=wg_private, suppressoutput=True) 72 | if (returncode != 0) or (len(err) > 0): 73 | return None 74 | out = out.strip() # remove trailing newline 75 | return out 76 | 77 | 78 | def generate_keypair(): 79 | wg_private = generate_privatekey() 80 | wg_public = get_publickey(wg_private) 81 | return wg_private, wg_public 82 | 83 | 84 | def generate_presharedkey(): 85 | out, err, returncode = execute('wg genpsk', suppressoutput=True) 86 | if (returncode != 0) or (len(err) > 0): 87 | return None 88 | out = out.strip() # remove trailing newline 89 | return out 90 | -------------------------------------------------------------------------------- /fogros2/image/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ros:humble 2 | 3 | # TODO: every ssh server using this will have the same key :rofl: 4 | RUN apt update && apt install -y vim software-properties-common gnupg lsb-release locales ros-humble-rmw-cyclonedds-cpp openssh-server sudo curl python3-colcon-common-extensions wireguard unzip python3-pip iproute2 5 | 6 | # Shouldn't be needed but oh well... 7 | RUN python3 -m pip install boto3 paramiko scp wgconfig kubernetes 8 | 9 | RUN useradd 'ubuntu' -m -s /bin/bash && mkdir '/home/ubuntu/.ssh' && echo 'ubuntu ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers 10 | 11 | CMD sleep infinity 12 | -------------------------------------------------------------------------------- /fogros2/launch/cloud.launch.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 The Regents of the University of California (Regents) 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 | # Copyright ©2022. The Regents of the University of California (Regents). 16 | # All Rights Reserved. Permission to use, copy, modify, and distribute this 17 | # software and its documentation for educational, research, and not-for-profit 18 | # purposes, without fee and without a signed licensing agreement, is hereby 19 | # granted, provided that the above copyright notice, this paragraph and the 20 | # following two paragraphs appear in all copies, modifications, and 21 | # distributions. Contact The Office of Technology Licensing, UC Berkeley, 2150 22 | # Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-7201, 23 | # otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial 24 | # licensing opportunities. IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY 25 | # FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, 26 | # INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS 27 | # DOCUMENTATION, EVEN IF REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 28 | # DAMAGE. REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT 29 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 30 | # PARTICULAR PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, 31 | # PROVIDED HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE 32 | # MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 33 | 34 | import pickle 35 | 36 | from launch import LaunchDescription 37 | 38 | 39 | def generate_launch_description(): 40 | ld = LaunchDescription() 41 | node_path = "/tmp/to_cloud_nodes" 42 | with open(node_path, "rb") as f: 43 | nodes_in_str = f.read() 44 | nodes = pickle.loads(nodes_in_str) 45 | for node in nodes: 46 | ld.add_action(node) 47 | print("action added") 48 | return ld 49 | -------------------------------------------------------------------------------- /fogros2/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | fogros2 5 | 0.1.7 6 | 7 | A ROS 2 extension for the cloud deployment of computational graphs in a cloud-provider 8 | agnostic and security-conscious manner. 9 | 10 | Jeff Ichnowski 11 | Kaiyuan (Eric) Chen 12 | Karthik Dharmarajan 13 | Simeon Adebola 14 | Jeff Ichnowski 15 | Kaiyuan (Eric) Chen 16 | Karthik Dharmarajan 17 | Simeon Adebola 18 | Michael Danielczuk 19 | Víctor Mayoral-Vilches 20 | Hugo Zhan 21 | Derek Xu 22 | Ramtin Ghassemi 23 | Nikhil Jha 24 | Apache License 2.0 25 | 26 | python3-boto3 27 | python3-scp 28 | python3-paramiko 29 | ros2cli 30 | rmw_cyclonedds_cpp 31 | wireguard 32 | 33 | ament_copyright 34 | ament_flake8 35 | ament_pep257 36 | python3-pytest 37 | 38 | 39 | ament_python 40 | 41 | ../README.md 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /fogros2/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3 2 | scp 3 | wgconfig 4 | paramiko 5 | -------------------------------------------------------------------------------- /fogros2/resource/fogros2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BerkeleyAutomation/FogROS2/f32b6af197ac30847d86b3b899bca44a6ca56a25/fogros2/resource/fogros2 -------------------------------------------------------------------------------- /fogros2/setup.cfg: -------------------------------------------------------------------------------- 1 | [develop] 2 | script_dir=$base/lib/fogros2 3 | [install] 4 | install_scripts=$base/lib/fogros2 5 | -------------------------------------------------------------------------------- /fogros2/setup.py: -------------------------------------------------------------------------------- 1 | # Copyright ©2022. The Regents of the University of California (Regents). 2 | # All Rights Reserved. Permission to use, copy, modify, and distribute this 3 | # software and its documentation for educational, research, and not-for-profit 4 | # purposes, without fee and without a signed licensing agreement, is hereby 5 | # granted, provided that the above copyright notice, this paragraph and the 6 | # following two paragraphs appear in all copies, modifications, and 7 | # distributions. Contact The Office of Technology Licensing, UC Berkeley, 2150 8 | # Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-7201, 9 | # otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial 10 | # licensing opportunities. IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY 11 | # FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, 12 | # INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS 13 | # DOCUMENTATION, EVEN IF REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 14 | # DAMAGE. REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT 15 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 16 | # PARTICULAR PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, 17 | # PROVIDED HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE 18 | # MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 19 | 20 | import os 21 | from glob import glob 22 | 23 | from setuptools import find_packages, setup 24 | 25 | package_name = "fogros2" 26 | setup( 27 | name=package_name, 28 | version="0.1.0", 29 | packages=find_packages(exclude=["test"]), 30 | data_files=[ 31 | ( 32 | "share/ament_index/resource_index/packages", 33 | [os.path.join("resource", package_name)], 34 | ), 35 | (os.path.join("share", package_name), ["package.xml"]), 36 | ( 37 | os.path.join("share", package_name, "launch"), 38 | glob("launch/*.launch.py"), 39 | ), 40 | ( 41 | os.path.join("share", package_name, "configs"), 42 | glob("configs/*.xml"), 43 | ), 44 | ], 45 | install_requires=["setuptools"], 46 | zip_safe=True, 47 | author="Kaiyuan (Eric) Chen, Víctor Mayoral-Vilches", 48 | author_email="kych@berkeley.edu, v.mayoralv@gmail.com", 49 | maintainer="Kaiyuan (Eric) Chen", 50 | maintainer_email="kych@berkeley.edu", 51 | description="ROS 2 extension for cloud computational graph deployment", 52 | long_description="""A ROS 2 extension for the cloud deployment 53 | of computational graphs in a cloud-provider agnostic 54 | and security-conscious manner.""", 55 | license="Apache License, Version 2.0", 56 | tests_require=["pytest"], 57 | entry_points={ 58 | "ros2cli.command": [ 59 | "fog = fogros2.command.fog:FogCommand", 60 | ], 61 | "fogros2.verb": [ 62 | "list = fogros2.verb.list:ListVerb", 63 | "delete = fogros2.verb.delete:DeleteVerb", 64 | "connect = fogros2.verb.ssh:SSHVerb", 65 | "image = fogros2.verb.image:ImageVerb", 66 | ], 67 | }, 68 | ) 69 | -------------------------------------------------------------------------------- /fogros2/test/test_copyright.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 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 | import pytest 16 | from ament_copyright.main import main 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 | -------------------------------------------------------------------------------- /fogros2/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 | import pytest 16 | from ament_flake8.main import main 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 | -------------------------------------------------------------------------------- /fogros2/test/test_pep257.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 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 | import pytest 16 | from ament_pep257.main import main 17 | 18 | 19 | @pytest.mark.linter 20 | @pytest.mark.pep257 21 | def test_pep257(): 22 | rc = main(argv=[".", "test", "--add-ignore", "D213"]) 23 | assert rc == 0, "Found code style errors / warnings" 24 | -------------------------------------------------------------------------------- /fogros2/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BerkeleyAutomation/FogROS2/f32b6af197ac30847d86b3b899bca44a6ca56a25/fogros2/utils/__init__.py -------------------------------------------------------------------------------- /fogros2/utils/ec2_instance_type_selection.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 The Regents of the University of California (Regents) 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 | # Copyright ©2022. The Regents of the University of California (Regents). 16 | # All Rights Reserved. Permission to use, copy, modify, and distribute this 17 | # software and its documentation for educational, research, and not-for-profit 18 | # purposes, without fee and without a signed licensing agreement, is hereby 19 | # granted, provided that the above copyright notice, this paragraph and the 20 | # following two paragraphs appear in all copies, modifications, and 21 | # distributions. Contact The Office of Technology Licensing, UC Berkeley, 2150 22 | # Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-7201, 23 | # otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial 24 | # licensing opportunities. IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY 25 | # FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, 26 | # INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS 27 | # DOCUMENTATION, EVEN IF REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 28 | # DAMAGE. REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT 29 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 30 | # PARTICULAR PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, 31 | # PROVIDED HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE 32 | # MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 33 | 34 | import boto3 35 | import json 36 | from pkg_resources import resource_filename 37 | from .region_ami_selection import haversine, aws_regions 38 | 39 | FLT = '[{{"Field": "tenancy", "Value": "shared", "Type": "TERM_MATCH"}},'\ 40 | '{{"Field": "operatingSystem", "Value": "{o}", "Type": "TERM_MATCH"}},'\ 41 | '{{"Field": "preInstalledSw", "Value": "NA", "Type": "TERM_MATCH"}},'\ 42 | '{{"Field": "instanceType", "Value": "{t}", "Type": "TERM_MATCH"}},'\ 43 | '{{"Field": "location", "Value": "{r}", "Type": "TERM_MATCH"}},'\ 44 | '{{"Field": "capacitystatus", "Value": "Used", "Type": "TERM_MATCH"}}]' 45 | 46 | 47 | def get_price(region_name, instance, os): 48 | f = FLT.format(r=get_region(region_name), t=instance, o=os) 49 | pricing_client = boto3.client('pricing', region_name=region_name) 50 | 51 | data = pricing_client.get_products(ServiceCode='AmazonEC2', 52 | Filters=json.loads(f)) 53 | od = json.loads(data['PriceList'][0])['terms']['OnDemand'] 54 | id1 = list(od)[0] 55 | id2 = list(od[id1]['priceDimensions'])[0] 56 | price = od[id1]['priceDimensions'][id2]['pricePerUnit']['USD'] 57 | return price 58 | 59 | 60 | def get_region(region_name): 61 | default_region = 'US East (N. Virginia)' 62 | endpoint_file = resource_filename('botocore', 'data/endpoints.json') 63 | try: 64 | with open(endpoint_file, 'r') as f: 65 | data = json.load(f) 66 | return data['partitions'][0]['regions'][region_name]['description']\ 67 | .replace('Europe', 'EU') 68 | except IOError: 69 | return default_region 70 | 71 | 72 | def ec2_instance_types(region_name, cpu_architecture="x86_64", default_cores=2, 73 | default_threads_per_core=1, gpu=True): 74 | """Yield all available EC2 instance types in region .""" 75 | ec2 = boto3.client('ec2', region_name=region_name) 76 | 77 | describe_args = { 78 | "Filters": [ 79 | { 80 | 'Name': 'processor-info.supported-architecture', 81 | 'Values': [ 82 | cpu_architecture, 83 | ] 84 | }, 85 | { 86 | 'Name': 'vcpu-info.default-cores', 87 | 'Values': [ 88 | str(default_cores), 89 | ] 90 | }, 91 | { 92 | 'Name': 'vcpu-info.default-threads-per-core', 93 | 'Values': [ 94 | str(default_threads_per_core), 95 | ] 96 | }, 97 | ], 98 | } 99 | 100 | while True: 101 | describe_result = ec2.describe_instance_types(**describe_args) 102 | yield from [i["InstanceType"] for i in describe_result['InstanceTypes'] 103 | if not gpu or "GpuInfo" in i] 104 | if 'NextToken' not in describe_result: 105 | break 106 | describe_args['NextToken'] = describe_result['NextToken'] 107 | 108 | 109 | def find_cheapest_ec2_instance_type(region_name, cpu_architecture="x86_64", default_cores=2, 110 | default_threads_per_core=1, gpu=False): 111 | (lat, lon) = aws_regions[region_name] 112 | region_name = min(['us-east-1', 'ap-south-1'], key=lambda region: haversine(region, lat, lon)) 113 | return min(ec2_instance_types(region_name, cpu_architecture, default_cores, 114 | default_threads_per_core, gpu), 115 | key=lambda instance_type: get_price(region_name, instance_type, 'Linux')) 116 | -------------------------------------------------------------------------------- /fogros2/utils/region_ami_selection.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 The Regents of the University of California (Regents) 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 | # Copyright ©2022. The Regents of the University of California (Regents). 16 | # All Rights Reserved. Permission to use, copy, modify, and distribute this 17 | # software and its documentation for educational, research, and not-for-profit 18 | # purposes, without fee and without a signed licensing agreement, is hereby 19 | # granted, provided that the above copyright notice, this paragraph and the 20 | # following two paragraphs appear in all copies, modifications, and 21 | # distributions. Contact The Office of Technology Licensing, UC Berkeley, 2150 22 | # Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-7201, 23 | # otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial 24 | # licensing opportunities. IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY 25 | # FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, 26 | # INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS 27 | # DOCUMENTATION, EVEN IF REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 28 | # DAMAGE. REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT 29 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 30 | # PARTICULAR PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, 31 | # PROVIDED HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE 32 | # MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 33 | 34 | import json 35 | import requests 36 | from math import radians, cos, sin, asin, sqrt 37 | 38 | 39 | aws_regions = { 40 | "us-east-2": (40.4167, -82.9167), 41 | "us-east-1": (38.0339, -78.4860), 42 | "us-west-1": (37.7749, -122.4194), 43 | "us-west-2": (45.5200, -122.6819), 44 | "af-south-1": (-33.9249, 18.4241), 45 | "ap-east-1": (22.2800, 114.1588), 46 | "ap-southeast-3": (-6.2315, 106.8275), 47 | "ap-south-1": (19.0760, 72.8777), 48 | "ap-northeast-3": (34.6723, 135.4848), 49 | "ap-northeast-2": (37.5665, 126.9780), 50 | "ap-southeast-1": (1.3521, 103.8198), 51 | "ap-southeast-2": (-33.8688, 151.2093), 52 | "ap-northeast-1": (35.6895, 139.6917), 53 | "ca-central-1": (43.6532, -79.3832), 54 | "eu-central-1": (50.1147, 8.6821), 55 | "eu-west-1": (53.4129, -8.2439), 56 | "eu-west-2": (51.5074, -0.1278), 57 | "eu-south-1": (45.4642, 9.1900), 58 | "eu-west-3": (48.8566, 2.3522), 59 | "eu-north-1": (59.3293, 18.0686), 60 | "me-south-1": (26.0667, 50.5577), 61 | "me-central-1": (23.4241, 53.8478), 62 | "sa-east-1": (-23.5505, -46.6333), 63 | } 64 | 65 | 66 | def find_nearest_region_and_ami(regions): 67 | ip = json.loads(requests.get("https://ip.seeip.org/jsonip?").text)["ip"] 68 | response = requests.get("http://ip-api.com/json/" + ip).json() 69 | lat = response["lat"] 70 | long = response["lon"] 71 | closest_region = min(regions, key=lambda region: haversine(region, lat, long)) 72 | return closest_region, regions[closest_region]["ami_image"] 73 | 74 | 75 | def haversine(region, lat, lon): 76 | lon1, lat1, lon2, lat2 = map(radians, 77 | [aws_regions[region][1], aws_regions[region][0], lon, lat]) 78 | dlon = lon2 - lon1 79 | dlat = lat2 - lat1 80 | a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2 81 | c = 2 * asin(sqrt(a)) 82 | km = 6371 * c 83 | return km 84 | -------------------------------------------------------------------------------- /fogros2_examples/CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2 | Changelog for package fogros2_examples 3 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 4 | 0.1.5 (2022-06-08) 5 | ------------------ 6 | * added dependencies to package.xml to reduce those that are manually downloaded 7 | 8 | 0.1.4 (2022-05-15) 9 | ------------------ 10 | * prepared package.xml with list of authors and maintainers for release 11 | * removed non-talker examples 12 | 13 | 0.1.3 (2022-05-09) 14 | ------------------ 15 | * fixed talker and listener example naming 16 | 17 | 0.1.1 (2022-05-02) 18 | ------------------ 19 | * updated examples to detect Ubuntu release and use the correct AMI and cyclonedds.xml -------------------------------------------------------------------------------- /fogros2_examples/fogros2_examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BerkeleyAutomation/FogROS2/f32b6af197ac30847d86b3b899bca44a6ca56a25/fogros2_examples/fogros2_examples/__init__.py -------------------------------------------------------------------------------- /fogros2_examples/fogros2_examples/listener.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 The Regents of the University of California (Regents) 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 | # Copyright ©2022. The Regents of the University of California (Regents). 16 | # All Rights Reserved. Permission to use, copy, modify, and distribute this 17 | # software and its documentation for educational, research, and not-for-profit 18 | # purposes, without fee and without a signed licensing agreement, is hereby 19 | # granted, provided that the above copyright notice, this paragraph and the 20 | # following two paragraphs appear in all copies, modifications, and 21 | # distributions. Contact The Office of Technology Licensing, UC Berkeley, 2150 22 | # Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-7201, 23 | # otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial 24 | # licensing opportunities. IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY 25 | # FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, 26 | # INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS 27 | # DOCUMENTATION, EVEN IF REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 28 | # DAMAGE. REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT 29 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 30 | # PARTICULAR PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, 31 | # PROVIDED HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE 32 | # MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 33 | 34 | import socket 35 | 36 | import rclpy 37 | from rclpy.node import Node 38 | from std_msgs.msg import String 39 | 40 | 41 | class MinimalSubscriber(Node): 42 | def __init__(self): 43 | super().__init__("minimal_subscriber") 44 | self.host_name = socket.gethostname() 45 | self.host_ip = socket.gethostbyname(self.host_name) 46 | 47 | self.subscription = self.create_subscription( 48 | String, "topic", self.listener_callback, 10 49 | ) 50 | self.subscription # prevent unused variable warning 51 | 52 | def listener_callback(self, msg): 53 | self.get_logger().warning( 54 | 'I am %s (%s). I heard: "%s"' 55 | % (self.host_name, self.host_ip, msg.data) 56 | ) 57 | 58 | 59 | def main(args=None): 60 | rclpy.init(args=args) 61 | 62 | minimal_subscriber = MinimalSubscriber() 63 | 64 | rclpy.spin(minimal_subscriber) 65 | 66 | # Destroy the node explicitly 67 | # (optional - otherwise it will be done automatically 68 | # when the garbage collector destroys the node object) 69 | minimal_subscriber.destroy_node() 70 | rclpy.shutdown() 71 | 72 | 73 | if __name__ == "__main__": 74 | main() 75 | -------------------------------------------------------------------------------- /fogros2_examples/fogros2_examples/talker.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 The Regents of the University of California (Regents) 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 | # Copyright ©2022. The Regents of the University of California (Regents). 16 | # All Rights Reserved. Permission to use, copy, modify, and distribute this 17 | # software and its documentation for educational, research, and not-for-profit 18 | # purposes, without fee and without a signed licensing agreement, is hereby 19 | # granted, provided that the above copyright notice, this paragraph and the 20 | # following two paragraphs appear in all copies, modifications, and 21 | # distributions. Contact The Office of Technology Licensing, UC Berkeley, 2150 22 | # Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-7201, 23 | # otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial 24 | # licensing opportunities. IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY 25 | # FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, 26 | # INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS 27 | # DOCUMENTATION, EVEN IF REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 28 | # DAMAGE. REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT 29 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 30 | # PARTICULAR PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, 31 | # PROVIDED HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE 32 | # MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 33 | 34 | import socket 35 | 36 | import rclpy 37 | from std_msgs.msg import String 38 | 39 | 40 | def main(args=None): 41 | rclpy.init(args=args) 42 | 43 | node = rclpy.create_node("minimal_publisher") 44 | publisher = node.create_publisher(String, "topic", 10) 45 | host_name = socket.gethostname() 46 | host_ip = socket.gethostbyname(host_name) 47 | 48 | msg = String() 49 | i = 0 50 | 51 | def timer_callback(): 52 | nonlocal i 53 | msg.data = "Hello World from %s (%s): %d" % (host_name, host_ip, i) 54 | i += 1 55 | node.get_logger().warning('Publishing: "%s"' % msg.data) 56 | publisher.publish(msg) 57 | 58 | timer_period = 0.5 # seconds 59 | timer = node.create_timer(timer_period, timer_callback) 60 | 61 | rclpy.spin(node) 62 | 63 | # Destroy the timer attached to the node explicitly 64 | # (optional - otherwise it will be done automatically 65 | # when the garbage collector destroys the node object) 66 | node.destroy_timer(timer) 67 | node.destroy_node() 68 | rclpy.shutdown() 69 | 70 | 71 | if __name__ == "__main__": 72 | main() 73 | -------------------------------------------------------------------------------- /fogros2_examples/launch/talker.auto_aws.launch.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 The Regents of the University of California (Regents) 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 | # Copyright ©2022. The Regents of the University of California (Regents). 16 | # All Rights Reserved. Permission to use, copy, modify, and distribute this 17 | # software and its documentation for educational, research, and not-for-profit 18 | # purposes, without fee and without a signed licensing agreement, is hereby 19 | # granted, provided that the above copyright notice, this paragraph and the 20 | # following two paragraphs appear in all copies, modifications, and 21 | # distributions. Contact The Office of Technology Licensing, UC Berkeley, 2150 22 | # Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-7201, 23 | # otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial 24 | # licensing opportunities. IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY 25 | # FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, 26 | # INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS 27 | # DOCUMENTATION, EVEN IF REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 28 | # DAMAGE. REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT 29 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 30 | # PARTICULAR PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, 31 | # PROVIDED HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE 32 | # MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 33 | 34 | from launch_ros.actions import Node 35 | import fogros2 36 | from utils import region_ami_selection, ec2_instance_type_selection 37 | 38 | 39 | def generic_ubuntu_ami(): 40 | return { 41 | "us-west-1": {"ami_image": "ami-02ea247e531eb3ce6"}, 42 | "us-west-2": {"ami_image": "ami-017fecd1353bcc96e"}, 43 | "us-east-1": {"ami_image": "ami-08c40ec9ead489470"}, 44 | "us-east-2": {"ami_image": "ami-097a2df4ac947655f"}, 45 | "ap-northeast-1": {"ami_image": "ami-03f4fa076d2981b45"}, 46 | "ap-northeast-2": {"ami_image": "ami-0e9bfdb247cc8de84"}, 47 | "ap-northeast-3": {"ami_image": "ami-08c2ee02329b72f26"}, 48 | } 49 | 50 | 51 | def generate_launch_description(): 52 | """Talker example that launches the listener on AWS.""" 53 | ld = fogros2.FogROSLaunchDescription() 54 | 55 | region, ami = region_ami_selection.find_nearest_region_and_ami(generic_ubuntu_ami()) 56 | 57 | ec2_instance_type = ec2_instance_type_selection.find_cheapest_ec2_instance_type(region) 58 | 59 | print(region, ami, ec2_instance_type) 60 | machine1 = fogros2.AWSCloudInstance( 61 | region=region, ec2_instance_type=ec2_instance_type, ami_image=ami 62 | ) 63 | 64 | listener_node = Node( 65 | package="fogros2_examples", executable="listener", output="screen" 66 | ) 67 | 68 | talker_node = fogros2.CloudNode( 69 | package="fogros2_examples", 70 | executable="talker", 71 | output="screen", 72 | machine=machine1, 73 | ) 74 | ld.add_action(talker_node) 75 | ld.add_action(listener_node) 76 | return ld 77 | -------------------------------------------------------------------------------- /fogros2_examples/launch/talker.aws.launch.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 The Regents of the University of California (Regents) 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 | # Copyright ©2022. The Regents of the University of California (Regents). 16 | # All Rights Reserved. Permission to use, copy, modify, and distribute this 17 | # software and its documentation for educational, research, and not-for-profit 18 | # purposes, without fee and without a signed licensing agreement, is hereby 19 | # granted, provided that the above copyright notice, this paragraph and the 20 | # following two paragraphs appear in all copies, modifications, and 21 | # distributions. Contact The Office of Technology Licensing, UC Berkeley, 2150 22 | # Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-7201, 23 | # otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial 24 | # licensing opportunities. IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY 25 | # FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, 26 | # INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS 27 | # DOCUMENTATION, EVEN IF REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 28 | # DAMAGE. REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT 29 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 30 | # PARTICULAR PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, 31 | # PROVIDED HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE 32 | # MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 33 | 34 | from launch_ros.actions import Node 35 | 36 | import fogros2 37 | 38 | 39 | def ami_image(): 40 | # An AMI is an Amazon Web Services virtual machine image with a 41 | # pre-installed OS and dependencies. We match the AMI in the 42 | # cloud to have the same OS release as the robot. Currently we 43 | # support Ubuntu 20.04 and 22.04. 44 | 45 | import lsb_release 46 | 47 | ubuntu_release = lsb_release.get_os_release()["RELEASE"] 48 | 49 | if ubuntu_release == "20.04": 50 | return "ami-00f25057ddc9b310b" 51 | if ubuntu_release == "22.04": 52 | # "ami-034160df82745c454" is custom AMI 53 | return "ami-036cafe742923b3d9" 54 | 55 | raise ValueError(f"No AMI for {ubuntu_release}") 56 | 57 | 58 | def generate_launch_description(): 59 | """Talker example that launches the listener on AWS.""" 60 | ld = fogros2.FogROSLaunchDescription() 61 | machine1 = fogros2.AWSCloudInstance( 62 | region="us-west-1", ec2_instance_type="t2.xlarge", ami_image=ami_image() 63 | ) 64 | 65 | listener_node = Node( 66 | package="fogros2_examples", executable="listener", output="screen" 67 | ) 68 | 69 | talker_node = fogros2.CloudNode( 70 | package="fogros2_examples", 71 | executable="talker", 72 | output="screen", 73 | machine=machine1, 74 | ) 75 | ld.add_action(talker_node) 76 | ld.add_action(listener_node) 77 | return ld 78 | -------------------------------------------------------------------------------- /fogros2_examples/launch/talker.gcp.launch.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 The Regents of the University of California (Regents) 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 | # Copyright ©2022. The Regents of the University of California (Regents). 16 | # All Rights Reserved. Permission to use, copy, modify, and distribute this 17 | # software and its documentation for educational, research, and not-for-profit 18 | # purposes, without fee and without a signed licensing agreement, is hereby 19 | # granted, provided that the above copyright notice, this paragraph and the 20 | # following two paragraphs appear in all copies, modifications, and 21 | # distributions. Contact The Office of Technology Licensing, UC Berkeley, 2150 22 | # Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-7201, 23 | # otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial 24 | # licensing opportunities. IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY 25 | # FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, 26 | # INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS 27 | # DOCUMENTATION, EVEN IF REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 28 | # DAMAGE. REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT 29 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 30 | # PARTICULAR PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, 31 | # PROVIDED HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE 32 | # MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 33 | 34 | from launch_ros.actions import Node 35 | 36 | import fogros2 37 | 38 | 39 | def generate_launch_description(): 40 | """Talker example that launches the listener on Google Compute Engine.""" 41 | ld = fogros2.FogROSLaunchDescription() 42 | machine1 = fogros2.GCPCloudInstance( 43 | project_id='shade-prod' 44 | ) 45 | 46 | listener_node = Node( 47 | package="fogros2_examples", executable="listener", output="screen" 48 | ) 49 | 50 | talker_node = fogros2.CloudNode( 51 | package="fogros2_examples", 52 | executable="talker", 53 | output="screen", 54 | machine=machine1, 55 | ) 56 | ld.add_action(talker_node) 57 | ld.add_action(listener_node) 58 | return ld 59 | -------------------------------------------------------------------------------- /fogros2_examples/launch/talker.kube.launch.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 The Regents of the University of California (Regents) 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 | # Copyright ©2022. The Regents of the University of California (Regents). 16 | # All Rights Reserved. Permission to use, copy, modify, and distribute this 17 | # software and its documentation for educational, research, and not-for-profit 18 | # purposes, without fee and without a signed licensing agreement, is hereby 19 | # granted, provided that the above copyright notice, this paragraph and the 20 | # following two paragraphs appear in all copies, modifications, and 21 | # distributions. Contact The Office of Technology Licensing, UC Berkeley, 2150 22 | # Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-7201, 23 | # otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial 24 | # licensing opportunities. IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY 25 | # FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, 26 | # INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS 27 | # DOCUMENTATION, EVEN IF REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 28 | # DAMAGE. REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT 29 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 30 | # PARTICULAR PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, 31 | # PROVIDED HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE 32 | # MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 33 | 34 | from launch_ros.actions import Node 35 | 36 | import fogros2 37 | 38 | 39 | def generate_launch_description(): 40 | """Talker example that launches the listener on GCP Kube.""" 41 | ld = fogros2.FogROSLaunchDescription() 42 | machine1 = fogros2.KubeInstance() 43 | 44 | listener_node = Node( 45 | package="fogros2_examples", executable="listener", output="screen" 46 | ) 47 | 48 | talker_node = fogros2.CloudNode( 49 | package="fogros2_examples", 50 | executable="talker", 51 | output="screen", 52 | machine=machine1, 53 | ) 54 | ld.add_action(talker_node) 55 | ld.add_action(listener_node) 56 | return ld 57 | -------------------------------------------------------------------------------- /fogros2_examples/launch/talker.local.launch.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 The Regents of the University of California (Regents) 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 | # Copyright ©2022. The Regents of the University of California (Regents). 16 | # All Rights Reserved. Permission to use, copy, modify, and distribute this 17 | # software and its documentation for educational, research, and not-for-profit 18 | # purposes, without fee and without a signed licensing agreement, is hereby 19 | # granted, provided that the above copyright notice, this paragraph and the 20 | # following two paragraphs appear in all copies, modifications, and 21 | # distributions. Contact The Office of Technology Licensing, UC Berkeley, 2150 22 | # Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-7201, 23 | # otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial 24 | # licensing opportunities. IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY 25 | # FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, 26 | # INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS 27 | # DOCUMENTATION, EVEN IF REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 28 | # DAMAGE. REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT 29 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 30 | # PARTICULAR PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, 31 | # PROVIDED HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE 32 | # MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 33 | 34 | from launch import LaunchDescription 35 | from launch_ros.actions import Node 36 | 37 | 38 | def generate_launch_description(): 39 | """Talker example that launches everything locally.""" 40 | ld = LaunchDescription() 41 | 42 | listener_node = Node( 43 | package="fogros2_examples", executable="listener", output="screen" 44 | ) 45 | talker_node = Node( 46 | package="fogros2_examples", executable="talker", output="screen" 47 | ) 48 | ld.add_action(talker_node) 49 | ld.add_action(listener_node) 50 | return ld 51 | -------------------------------------------------------------------------------- /fogros2_examples/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | fogros2_examples 5 | 0.1.7 6 | Examples using FogROS2 7 | Jeff Ichnowski 8 | Kaiyuan (Eric) Chen 9 | Karthik Dharmarajan 10 | Simeon Adebola 11 | Jeff Ichnowski 12 | Kaiyuan (Eric) Chen 13 | Karthik Dharmarajan 14 | Simeon Adebola 15 | Michael Danielczuk 16 | Víctor Mayoral-Vilches 17 | Hugo Zhan 18 | Derek Xu 19 | Ramtin Ghassemi 20 | Nikhil Jha 21 | Apache License 2.0 22 | 23 | fogros2 24 | 25 | ament_copyright 26 | ament_flake8 27 | ament_pep257 28 | python3-pytest 29 | 30 | 31 | ament_python 32 | 33 | ../README.md 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /fogros2_examples/resource/fogros2_examples: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BerkeleyAutomation/FogROS2/f32b6af197ac30847d86b3b899bca44a6ca56a25/fogros2_examples/resource/fogros2_examples -------------------------------------------------------------------------------- /fogros2_examples/setup.cfg: -------------------------------------------------------------------------------- 1 | [develop] 2 | script_dir=$base/lib/fogros2_examples 3 | [install] 4 | install_scripts=$base/lib/fogros2_examples 5 | -------------------------------------------------------------------------------- /fogros2_examples/setup.py: -------------------------------------------------------------------------------- 1 | # Copyright ©2022. The Regents of the University of California (Regents). 2 | # All Rights Reserved. Permission to use, copy, modify, and distribute this 3 | # software and its documentation for educational, research, and not-for-profit 4 | # purposes, without fee and without a signed licensing agreement, is hereby 5 | # granted, provided that the above copyright notice, this paragraph and the 6 | # following two paragraphs appear in all copies, modifications, and 7 | # distributions. Contact The Office of Technology Licensing, UC Berkeley, 2150 8 | # Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-7201, 9 | # otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial 10 | # licensing opportunities. IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY 11 | # FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, 12 | # INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS 13 | # DOCUMENTATION, EVEN IF REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 14 | # DAMAGE. REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT 15 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 16 | # PARTICULAR PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, 17 | # PROVIDED HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE 18 | # MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 19 | 20 | import os 21 | from glob import glob 22 | 23 | from setuptools import setup 24 | 25 | package_name = "fogros2_examples" 26 | 27 | setup( 28 | name=package_name, 29 | version="0.0.1", 30 | packages=[package_name], 31 | data_files=[ 32 | ( 33 | "share/ament_index/resource_index/packages", 34 | [os.path.join("resource", package_name)], 35 | ), 36 | (os.path.join("share", package_name), ["package.xml"]), 37 | (os.path.join("share", package_name), glob("launch/*.launch.py")), 38 | ], 39 | install_requires=["setuptools"], 40 | zip_safe=True, 41 | author="Kaiyuan (Eric) Chen, Víctor Mayoral-Vilches", 42 | author_email="kych@berkeley.edu, v.mayoralv@gmail.com", 43 | maintainer="Kaiyuan (Eric) Chen", 44 | maintainer_email="kych@berkeley.edu", 45 | description="TODO: Package description", 46 | license="TODO: License declaration", 47 | tests_require=["pytest"], 48 | entry_points={ 49 | "console_scripts": [ 50 | "talker = fogros2_examples.talker:main", 51 | "listener = fogros2_examples.listener:main", 52 | ], 53 | }, 54 | ) 55 | -------------------------------------------------------------------------------- /fogros2_examples/test/test_copyright.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 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 | import pytest 16 | from ament_copyright.main import main 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 | -------------------------------------------------------------------------------- /fogros2_examples/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 | import pytest 16 | from ament_flake8.main import main 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 | -------------------------------------------------------------------------------- /fogros2_examples/test/test_pep257.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 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 | import pytest 16 | from ament_pep257.main import main 17 | 18 | 19 | @pytest.mark.linter 20 | @pytest.mark.pep257 21 | def test_pep257(): 22 | rc = main(argv=[".", "test"]) 23 | assert rc == 0, "Found code style errors / warnings" 24 | -------------------------------------------------------------------------------- /ros_entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # setup ros2 environment 5 | source /opt/ros/$ROS_DISTRO/setup.bash 6 | source $ROS_WS/install/setup.bash 7 | echo "source /opt/ros/$ROS_DISTRO/setup.bash" >> ~/.bashrc 8 | echo "source $ROS_WS/install/setup.bash" >> ~/.bashrc 9 | 10 | # work with CycloneDDS DDS implementation 11 | ver=$(lsb_release -rs | sed 's/\.//') 12 | export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp 13 | export CYCLONEDDS_URI=file://$(pwd)/install/fogros2/share/fogros2/configs/cyclonedds.ubuntu.$ver.xml 14 | 15 | exec "$@" --------------------------------------------------------------------------------