Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions fogros2/fogros2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,7 @@
# MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.

from .aws_cloud_instance import AWSCloudInstance # noqa: F401
from .gcp_cloud_instance import GCPCloudInstance
from .kubernetes.gcp_kubernetes import GCPKubeInstance
from .cloud_node import CloudNode # noqa: F401
from .launch_description import FogROSLaunchDescription # noqa: F401
26 changes: 18 additions & 8 deletions fogros2/fogros2/cloud_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ def __init__(
self.cyclone_builder = None
self.scp = None
self._ip = None
self._vpn_ip = None
self.ros_workspace = ros_workspace
self.ros_distro = os.getenv("ROS_DISTRO")
self.logger.debug(f"Using ROS workspace: {self.ros_workspace}")
Expand All @@ -84,6 +85,7 @@ def __init__(
self.cloud_service_provider = None
self.dockers = []
self.launch_foxglove = launch_foxglove
self._username = 'ubuntu'

@abc.abstractmethod
def create(self):
Expand All @@ -104,13 +106,18 @@ def info(self, flush_to_disk=True):
return info_dict

def connect(self):
self.scp = SCPClient(self._ip, self._ssh_key_path)
self.scp = SCPClient(self._ip, self._ssh_key_path, username=self._username)
self.scp.connect()

@property
def ip(self):
return self._ip

@property
def vpn_ip(self):
# Use this when the VPN IP is not None.
return self._vpn_ip

@property
def is_created(self):
return self._is_created
Expand All @@ -125,7 +132,7 @@ def apt_install(self, args):
)

def pip_install(self, args):
self.scp.execute_cmd(f"sudo pip3 install {args}")
self.scp.execute_cmd(f"python3 -m pip install {args}")

def install_cloud_dependencies(self):
self.apt_install("wireguard unzip docker.io python3-pip")
Expand Down Expand Up @@ -161,6 +168,9 @@ def install_ros(self):
# install ros2 packages
self.apt_install(f"ros-{self.ros_distro}-desktop")

# Installing all deps because cloud launch seems to rely on them
self.apt_install('python3-colcon-common-extensions')

# source environment
self.scp.execute_cmd(f"source /opt/ros/{self.ros_distro}/setup.bash")

Expand All @@ -173,7 +183,7 @@ def configure_rosbridge(self):
rosbridge_launch_script = (
"ssh -o StrictHostKeyChecking=no -i "
f"{self._ssh_key_path}"
" ubuntu@"
f" {self._username}@"
f"{self._ip}"
f' "source /opt/ros/{self.ros_distro}/setup.bash && '
'ros2 launch rosbridge_server rosbridge_websocket_launch.xml &"'
Expand Down Expand Up @@ -203,8 +213,8 @@ def push_ros_workspace(self):
make_zip_file(workspace_path, zip_dst)
self.scp.execute_cmd("echo removing old workspace")
self.scp.execute_cmd("rm -rf ros_workspace.zip ros2_ws fog_ws")
self.scp.send_file(f"{zip_dst}.zip", "/home/ubuntu/")
self.scp.execute_cmd("unzip -q /home/ubuntu/ros_workspace.zip")
self.scp.send_file(f"{zip_dst}.zip", f"/home/{self._username}/")
self.scp.execute_cmd(f"unzip -q /home/{self._username}/ros_workspace.zip")
self.scp.execute_cmd("echo successfully extracted new workspace")

def push_to_cloud_nodes(self):
Expand All @@ -223,17 +233,17 @@ def push_and_setup_vpn(self):

def configure_DDS(self):
# configure DDS
self.cyclone_builder = CycloneConfigBuilder(["10.0.0.1"])
self.cyclone_builder = CycloneConfigBuilder(["10.0.0.1"], username=self._username)
self.cyclone_builder.generate_config_file()
self.scp.send_file("/tmp/cyclonedds.xml", "~/cyclonedds.xml")

def launch_cloud_node(self):
cmd_builder = BashBuilder()
cmd_builder.append(f"source /opt/ros/{self.ros_distro}/setup.bash")
cmd_builder.append(
"cd /home/ubuntu/fog_ws && colcon build --cmake-clean-cache"
f"cd /home/{self._username}/fog_ws && colcon build --cmake-clean-cache"
)
cmd_builder.append(". /home/ubuntu/fog_ws/install/setup.bash")
cmd_builder.append(f". /home/{self._username}/fog_ws/install/setup.bash")
cmd_builder.append(self.cyclone_builder.env_cmd)
ros_domain_id = os.environ.get("ROS_DOMAIN_ID")
if not ros_domain_id:
Expand Down
4 changes: 2 additions & 2 deletions fogros2/fogros2/dds_config_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,12 @@ def generate_config_file(self):


class CycloneConfigBuilder(DDSConfigBuilder):
def __init__(self, ip_addresses):
def __init__(self, ip_addresses, username='ubuntu'):
super().__init__(ip_addresses)
self.config_save_path = "/tmp/cyclonedds.xml"
self.env_cmd = (
"export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp && "
"export CYCLONEDDS_URI=file:///home/ubuntu/cyclonedds.xml"
f"export CYCLONEDDS_URI=file:///home/{username}/cyclonedds.xml"
)

def generate_config_file(self):
Expand Down
150 changes: 150 additions & 0 deletions fogros2/fogros2/gcp_cloud_instance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# Copyright 2022 The Regents of the University of California (Regents)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Copyright ©2022. The Regents of the University of California (Regents).
# All Rights Reserved. Permission to use, copy, modify, and distribute this
# software and its documentation for educational, research, and not-for-profit
# purposes, without fee and without a signed licensing agreement, is hereby
# granted, provided that the above copyright notice, this paragraph and the
# following two paragraphs appear in all copies, modifications, and
# distributions. Contact The Office of Technology Licensing, UC Berkeley, 2150
# Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-7201,
# [email protected], http://ipira.berkeley.edu/industry-info for commercial
# licensing opportunities. IN NO EVEpNT SHALL REGENTS BE LIABLE TO ANY PARTY
# FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,
# INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS
# DOCUMENTATION, EVEN IF REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
# DAMAGE. REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
# PARTICULAR PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY,
# PROVIDED HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE
# MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.

import json
import os

import subprocess
import uuid

from .cloud_instance import CloudInstance

from .util import extract_bash_column


class GCPCloudInstance(CloudInstance):
"""GCP Implementation of CloudInstance."""

def __init__(
self,
project_id,
ami_image='projects/ubuntu-os-cloud/global/images/ubuntu-2204-jammy-v20220712a',
zone="us-central1-a",
machine_type="e2-medium",
disk_size=10,
**kwargs,
):
super().__init__(**kwargs)
self.cloud_service_provider = "GCP"

id_ = str(uuid.uuid4())[0:8]
self._name = f'fog-{id_}-{self._name}'

self.zone = zone
self.type = machine_type
self.compute_instance_disk_size = disk_size # GB
self.gcp_ami_image = ami_image

self._working_dir = os.path.join(self._working_dir_base, self._name)
os.makedirs(self._working_dir, exist_ok=True)

self._project_id = project_id

# after config
self._ssh_key = None

self.create()

def create(self):
self.logger.info(f"Creating new GCP Compute Engine instance with name {self._name}")
self.create_compute_engine_instance()
self.info(flush_to_disk=True)
self.connect()
self.install_ros()
self.install_colcon()
self.install_cloud_dependencies()
self.push_ros_workspace()
self.info(flush_to_disk=True)
self._is_created = True

def info(self, flush_to_disk=True):
info_dict = super().info(flush_to_disk)
info_dict["compute_region"] = self.zone
info_dict["compute_instance_type"] = self.type
info_dict["disk_size"] = self.compute_instance_disk_size
info_dict["compute_instance_id"] = self._name
if flush_to_disk:
with open(os.path.join(self._working_dir, "info"), "w+") as f:
json.dump(info_dict, f)
return info_dict

def create_compute_engine_instance(self):
os.system(f'gcloud config set project {self._project_id}')

result = subprocess.check_output(f'gcloud compute instances create {self._name} '
f'--project={self._project_id} --zone={self.zone} --machine-type={self.type} '
'--network-interface=network-tier=PREMIUM,subnet=default '
'--maintenance-policy=MIGRATE --provisioning-model=STANDARD '
'--scopes=https://www.googleapis.com/auth/devstorage.read_only,'
'https://www.googleapis.com/auth/logging.write,'
'https://www.googleapis.com/auth/monitoring.write,'
'https://www.googleapis.com/auth/servicecontrol,'
'https://www.googleapis.com/auth/service.management.readonly,'
'https://www.googleapis.com/auth/trace.append '
'--create-disk=auto-delete=yes,'
'boot=yes,'
f'device-name={self._name},'
f'image={self.gcp_ami_image},'
'mode=rw,'
f'size={self.compute_instance_disk_size},'
f'type=projects/{self._project_id}/zones/{self.zone}/diskTypes/pd-balanced '
'--no-shielded-secure-boot '
'--shielded-vtpm '
'--shielded-integrity-monitoring '
'--reservation-affinity=any', shell=True).decode()

# Grab external IP
ip = extract_bash_column(result, 'EXTERNAL_IP')

# Verifies the response was an ip
if len(ip.split('.')) != 4:
raise Exception(f'Error creating instance: {ip}')

self._ip = ip

# Generate SSH keys
os.system(f"printf '\n\n' | gcloud compute ssh {self._name} --zone {self.zone}")

user = subprocess.check_output('whoami', shell=True).decode().strip()

# Username
self._username = (open(f'/home/{user}/.ssh/google_compute_engine.pub').
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to break this up into more separate steps, as it is tough to parse what is going on currently.

read()).split(' ')[-1].strip().split('@')[0]

self._ssh_key_path = f'/home/{user}/.ssh/google_compute_engine'
self._is_created = True

self.logger.info(
f"Created {self.type} instance named {self._name} "
f"with id {self._name} and public IP address {self._ip}"
)
Empty file.
Loading