Skip to content

Commit

Permalink
k8s -> review (#94)
Browse files Browse the repository at this point in the history
Co-authored-by: Emerson Dove <[email protected]>
Co-authored-by: Edith Llontop <[email protected]>
Former-commit-id: ce3a1af
  • Loading branch information
3 people authored Nov 1, 2022
1 parent 458d215 commit ff212e2
Show file tree
Hide file tree
Showing 14 changed files with 598 additions and 24 deletions.
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.generic import KubeInstance
from .cloud_node import CloudNode # noqa: F401
from .launch_description import FogROSLaunchDescription # noqa: F401
Empty file modified fogros2/fogros2/aws_cloud_instance.py
100755 → 100644
Empty file.
22 changes: 16 additions & 6 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 @@ -102,15 +104,23 @@ def info(self, flush_to_disk=True):
with open(os.path.join(self._working_dir, "info"), "w+") as f:
json.dump(info_dict, f)
return info_dict

def force_start_vpn(self):
return True

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 +135,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 ros-humble-rmw-cyclonedds-cpp")
Expand Down Expand Up @@ -177,7 +187,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 @@ -229,17 +239,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
19 changes: 8 additions & 11 deletions fogros2/fogros2/dds_config_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,24 +53,18 @@ 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):
if ubuntu_release == "20.04":
interfaces = """
<NetworkInterfaceAddress>wg0</NetworkInterfaceAddress>
<AllowMulticast>false</AllowMulticast>
"""
else:
interfaces = """
def generate_config_file(self, extra_peers = []):
interfaces = """
<Interfaces>
<NetworkInterface name="wg0"/>
<NetworkInterface name="wg0" />
</Interfaces>
"""

Expand All @@ -82,6 +76,8 @@ def generate_config_file(self):
'cyclonedds/master/etc/cyclonedds.xsd"'
)

peer_xml = "".join(f"<Peer address=\"{peer}\"/>\n" for peer in extra_peers)

template = f"""
<?xml version="1.0" encoding="UTF-8" ?>
<CycloneDDS {xmlvals}>
Expand All @@ -91,6 +87,7 @@ def generate_config_file(self):
<Peers>
<Peer address="10.0.0.1"/>
<Peer address="10.0.0.2"/>
{peer_xml}
</Peers>
<ParticipantIndex>auto</ParticipantIndex>
</Discovery>
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').
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

0 comments on commit ff212e2

Please sign in to comment.