Skip to content

Commit 2dd5426

Browse files
authored
Merge pull request #93 from BerkeleyAutomation/datacenter_selection
Datacenter selection Former-commit-id: a8fb166
2 parents db593c8 + af5f1b7 commit 2dd5426

File tree

6 files changed

+195
-1
lines changed

6 files changed

+195
-1
lines changed

fogros2/fogros2/aws_cloud_instance.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ def __init__(
107107

108108
self.create()
109109

110+
110111
def create(self):
111112
self.logger.info(f"Creating new EC2 instance with name {self._name}")
112113
self.create_security_group()

fogros2/fogros2/cloud_instance.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ def install_ros(self):
159159
self.scp.execute_cmd("export LANG=en_US.UTF-8")
160160

161161
# install ros2 packages
162-
self.apt_install(f"ros-{self.ros_distro}-desktop")
162+
# self.apt_install(f"ros-{self.ros_distro}-desktop")
163163

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

fogros2/utils/__init__.py

Whitespace-only changes.
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import boto3
2+
import json
3+
from pkg_resources import resource_filename
4+
from .region_ami_selection import haversine, aws_regions
5+
6+
FLT = '[{{"Field": "tenancy", "Value": "shared", "Type": "TERM_MATCH"}},'\
7+
'{{"Field": "operatingSystem", "Value": "{o}", "Type": "TERM_MATCH"}},'\
8+
'{{"Field": "preInstalledSw", "Value": "NA", "Type": "TERM_MATCH"}},'\
9+
'{{"Field": "instanceType", "Value": "{t}", "Type": "TERM_MATCH"}},'\
10+
'{{"Field": "location", "Value": "{r}", "Type": "TERM_MATCH"}},'\
11+
'{{"Field": "capacitystatus", "Value": "Used", "Type": "TERM_MATCH"}}]'
12+
13+
14+
def get_price(region_name, instance, os):
15+
f = FLT.format(r=get_region(region_name), t=instance, o=os)
16+
pricing_client = boto3.client('pricing',
17+
region_name=region_name,
18+
)
19+
data = pricing_client.get_products(ServiceCode='AmazonEC2', Filters=json.loads(f))
20+
od = json.loads(data['PriceList'][0])['terms']['OnDemand']
21+
id1 = list(od)[0]
22+
id2 = list(od[id1]['priceDimensions'])[0]
23+
price = od[id1]['priceDimensions'][id2]['pricePerUnit']['USD']
24+
return price
25+
26+
def get_region(region_name):
27+
default_region = 'US East (N. Virginia)'
28+
endpoint_file = resource_filename('botocore', 'data/endpoints.json')
29+
try:
30+
with open(endpoint_file, 'r') as f:
31+
data = json.load(f)
32+
return data['partitions'][0]['regions'][region_name]['description'].replace('Europe', 'EU')
33+
except IOError:
34+
return default_region
35+
36+
def ec2_instance_types(region_name, cpu_architecture="x86_64", default_cores=2, default_threads_per_core=1, gpu=True):
37+
'''Yield all available EC2 instance types in region <region_name>'''
38+
ec2 = boto3.client('ec2',
39+
region_name=region_name,
40+
)
41+
42+
describe_args = {
43+
"Filters": [
44+
{
45+
'Name': 'processor-info.supported-architecture',
46+
'Values': [
47+
cpu_architecture,
48+
]
49+
},
50+
{
51+
'Name': 'vcpu-info.default-cores',
52+
'Values': [
53+
str(default_cores),
54+
]
55+
},
56+
{
57+
'Name': 'vcpu-info.default-threads-per-core',
58+
'Values': [
59+
str(default_threads_per_core),
60+
]
61+
},
62+
],
63+
}
64+
65+
while True:
66+
describe_result = ec2.describe_instance_types(**describe_args)
67+
yield from [i["InstanceType"] for i in describe_result['InstanceTypes'] if not gpu or "GpuInfo" in i]
68+
if 'NextToken' not in describe_result:
69+
break
70+
describe_args['NextToken'] = describe_result['NextToken']
71+
72+
def find_cheapest_ec2_instance_type(region_name, cpu_architecture="x86_64", default_cores=2, default_threads_per_core=1, gpu=False):
73+
(lat, lon) = aws_regions[region_name]
74+
region_name = min(['us-east-1', 'ap-south-1'], key= lambda region: haversine(region, lat, lon))
75+
return min(ec2_instance_types(region_name, cpu_architecture, default_cores, default_threads_per_core, gpu), key=lambda instance_type: get_price(region_name, instance_type, 'Linux'))
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import json
2+
import requests
3+
from math import radians, cos, sin, asin, sqrt
4+
5+
aws_regions = {
6+
"us-east-2": (40.4167, -82.9167),
7+
"us-east-1": (38.0339, -78.4860),
8+
"us-west-1": (37.7749, -122.4194),
9+
"us-west-2": (45.5200, -122.6819),
10+
"af-south-1": (-33.9249, 18.4241),
11+
"ap-east-1": (22.2800, 114.1588),
12+
"ap-southeast-3": (-6.2315, 106.8275),
13+
"ap-south-1": (19.0760, 72.8777),
14+
"ap-northeast-3": (34.6723, 135.4848),
15+
"ap-northeast-2": (37.5665, 126.9780),
16+
"ap-southeast-1": (1.3521, 103.8198),
17+
"ap-southeast-2": (-33.8688, 151.2093),
18+
"ap-northeast-1": (35.6895, 139.6917),
19+
"ca-central-1": (43.6532, -79.3832),
20+
"eu-central-1": (50.1147, 8.6821),
21+
"eu-west-1": (53.4129, -8.2439),
22+
"eu-west-2": (51.5074, -0.1278),
23+
"eu-south-1": (45.4642, 9.1900),
24+
"eu-west-3": (48.8566, 2.3522),
25+
"eu-north-1": (59.3293, 18.0686),
26+
"me-south-1": (26.0667, 50.5577),
27+
"me-central-1": (23.4241, 53.8478),
28+
"sa-east-1": (-23.5505, -46.6333),
29+
}
30+
31+
def find_nearest_region_and_ami(regions):
32+
ip = json.loads(requests.get("https://ip.seeip.org/jsonip?").text)["ip"]
33+
response = requests.get("http://ip-api.com/json/" + ip).json()
34+
lat = response["lat"]
35+
long = response["lon"]
36+
closest_region = min(regions, key=lambda region: haversine(region, lat, long))
37+
return closest_region, regions[closest_region]["ami_image"]
38+
39+
def haversine(region, lat, lon):
40+
lon1, lat1, lon2, lat2 = map(radians, [aws_regions[region][1], aws_regions[region][0], lon, lat])
41+
dlon = lon2 - lon1
42+
dlat = lat2 - lat1
43+
a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2
44+
c = 2 * asin(sqrt(a))
45+
km = 6371 * c
46+
return km
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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+
# [email protected], 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-01154c8b2e9a14885" },
42+
"us-west-2": { "ami_image": "ami-0ddf424f81ddb0720" },
43+
"us-east-1": { "ami_image": "ami-08d4ac5b634553e16" },
44+
"us-east-2": { "ami_image": "ami-0960ab670c8bb45f3" },
45+
}
46+
47+
def generate_launch_description():
48+
"""Talker example that launches the listener on AWS."""
49+
ld = fogros2.FogROSLaunchDescription()
50+
51+
region, ami = region_ami_selection.find_nearest_region_and_ami(generic_ubuntu_ami())
52+
53+
ec2_instance_type = ec2_instance_type_selection.find_cheapest_ec2_instance_type(region)
54+
55+
print(region, ami, ec2_instance_type)
56+
machine1 = fogros2.AWSCloudInstance(
57+
region=region, ec2_instance_type=ec2_instance_type, ami_image=ami
58+
)
59+
60+
listener_node = Node(
61+
package="fogros2_examples", executable="listener", output="screen"
62+
)
63+
64+
talker_node = fogros2.CloudNode(
65+
package="fogros2_examples",
66+
executable="talker",
67+
output="screen",
68+
machine=machine1,
69+
)
70+
ld.add_action(talker_node)
71+
ld.add_action(listener_node)
72+
return ld

0 commit comments

Comments
 (0)