Compare commits
2 Commits
main
...
andrew/clo
Author | SHA1 | Date |
---|---|---|
![]() |
1ac1ed41e6 | |
![]() |
9e7ab9665a |
|
@ -5,8 +5,10 @@
|
|||
package cloudenv
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
|
@ -181,3 +183,111 @@ func getCloud() Cloud {
|
|||
// TODO: more, as needed.
|
||||
return ""
|
||||
}
|
||||
|
||||
// ApproximateLocation returns the approximate geographic location of the
|
||||
// region that the current cloud host is running in.
|
||||
//
|
||||
// If the current host is not running in the cloud, if cloud provider is not
|
||||
// supported, or if the current region could not be determined, then (0, 0)
|
||||
// will be returned.
|
||||
func (c Cloud) ApproximateLocation() (lat, lon float64) {
|
||||
switch c {
|
||||
case AWS:
|
||||
loc := getApproximateLocationAWS()
|
||||
return loc.lat, loc.lon
|
||||
case GCP:
|
||||
// TODO
|
||||
case Azure:
|
||||
// TODO
|
||||
}
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
type location struct{ lat, lon float64 }
|
||||
|
||||
var noLocation location
|
||||
|
||||
var approximateAWSRegionLocation = map[string]location{
|
||||
"af-south-1": {-33.928992, 18.417396}, // "CPT" / Cape Town, South Africa
|
||||
"ap-east-1": {22.2793278, 114.1628131}, // "HKG" / Hong Kong
|
||||
"ap-northeast-1": {35.6812665, 139.757653}, // "NRT" / Tokyo, Japan
|
||||
"ap-northeast-2": {37.5666791, 126.9782914}, // "ICN" / Seoul, Korea
|
||||
"ap-northeast-3": {34.661629, 135.4999268}, // "KIX" / Osaka, Japan
|
||||
"ap-south-1": {19.0785451, 72.878176}, // "BOM" / Mumbai, India
|
||||
"ap-south-2": {17.38878595, 78.46106473}, // "HYD" / Hyderabad, India
|
||||
"ap-southeast-1": {1.357107, 103.8194992}, // "SIN" / Singapore
|
||||
"ap-southeast-2": {-33.8698439, 151.2082848}, // "SYD" / Sydney, Australia
|
||||
"ap-southeast-3": {-6.1753942, 106.827183}, // "CGK" / Jakarta, Indonesia
|
||||
"ap-southeast-4": {-37.8142176, 144.9631608}, // "MEL" / Melbourne, Australia
|
||||
"ca-central-1": {45.5031824, -73.5698065}, // "YUL" / Montreal, Canada
|
||||
"cn-north-1": {39.906217, 116.3912757}, // "BJS" / Beijing
|
||||
"cn-northwest-1": {37.4999947, 105.1928783}, // "ZHY" / Zhongwei
|
||||
"eu-central-1": {50.1106444, 8.6820917}, // "FRA" / Frankfurt, Germany
|
||||
"eu-central-2": {47.3744489, 8.5410422}, // "ZRH" / Zurich, Switzerland
|
||||
"eu-north-1": {59.3251172, 18.0710935}, // "ARN" / Stockholm, Sweden
|
||||
"eu-south-1": {45.4641943, 9.1896346}, // "MXP" / Milan, Italy
|
||||
"eu-south-2": {41.6521342, -0.8809428}, // "ZAZ" / Zaragoza, Spain
|
||||
"eu-west-1": {53.3498006, -6.2602964}, // "DUB" / Dublin, Ireland
|
||||
"eu-west-2": {51.5073359, -0.12765}, // "LHR" / London, England
|
||||
"eu-west-3": {48.8588897, 2.32004102}, // "CDG" / Paris, France
|
||||
"me-south-1": {26.1551249, 50.5344606}, // "BAH" / Bahrain
|
||||
"sa-east-1": {-23.5506507, -46.6333824}, // "GRU" / São Paulo, Brazil
|
||||
"us-east-1": {38.8950368, -77.0365427}, // "IAD" / Washington D.C., USA
|
||||
"us-east-2": {39.9622601, -83.0007065}, // "CMH" / Columbus, Ohio, USA
|
||||
"us-gov-east-1": {39.9622601, -83.0007065}, // "CMH" / Columbus, Ohio, USA
|
||||
"us-gov-west-1": {45.5202471, -122.674194}, // "PDX" / Portland, Oregon, USA
|
||||
"us-west-1": {37.7790262, -122.419906}, // "SFO" / San Francisco, California, USA
|
||||
"us-west-2": {45.5202471, -122.674194}, // "PDX" / Portland, Oregon, USA
|
||||
|
||||
// NOTE: it's not public where in Dubai this is
|
||||
"me-central-1": {25.07428234, 55.18853865}, // Dubai
|
||||
}
|
||||
|
||||
func getApproximateLocationAWS() location {
|
||||
const maxWait = 2 * time.Second
|
||||
tr := &http.Transport{
|
||||
DisableKeepAlives: true,
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: maxWait,
|
||||
}).Dial,
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), maxWait)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "PUT", "http://"+CommonNonRoutableMetadataIP+"/latest/api/token", nil)
|
||||
if err != nil {
|
||||
return noLocation
|
||||
}
|
||||
req.Header.Set("X-aws-ec2-metadata-token-ttl-seconds", "30")
|
||||
|
||||
res, err := tr.RoundTrip(req)
|
||||
if err != nil {
|
||||
return noLocation
|
||||
}
|
||||
token, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
return noLocation
|
||||
}
|
||||
|
||||
req, err = http.NewRequestWithContext(ctx, "GET", "http://"+CommonNonRoutableMetadataIP+"/latest/dynamic/instance-identity/document", nil)
|
||||
if err != nil {
|
||||
return noLocation
|
||||
}
|
||||
req.Header.Set("X-aws-ec2-metadata-token", string(bytes.TrimSpace(token)))
|
||||
|
||||
res, err = tr.RoundTrip(req)
|
||||
if err != nil {
|
||||
return noLocation
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
var identityDocument struct {
|
||||
Region string `json:"region"`
|
||||
}
|
||||
if err := json.NewDecoder(res.Body).Decode(&identityDocument); err != nil {
|
||||
return noLocation
|
||||
}
|
||||
|
||||
return approximateAWSRegionLocation[identityDocument.Region]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
// Package neighbour contains a basic non-optimized nearest-neighbour
|
||||
// algorithm implementation for terrestrial GPS coordinates.
|
||||
package neighbour
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// Location is a latitude/longitude pair representing a point on Earth.
|
||||
type Location struct {
|
||||
Latitude float64
|
||||
Longitude float64
|
||||
}
|
||||
|
||||
// Distance calculates the great-circle distance between two points using the
|
||||
// Haversine formula, in kilometers.
|
||||
//
|
||||
// This is also known as the "as-the-crow-flies" distance.
|
||||
func (l Location) Distance(other Location) float64 {
|
||||
// For the following variable definitions:
|
||||
// φ is latitude ("phi")
|
||||
// λ is longitude ("lambda")
|
||||
// R is earth’s radius (mean radius = 6,371km)
|
||||
//
|
||||
// We can calculate the distance using the haversine formula as such:
|
||||
// a = sin²((φB - φA)/2) + cos φA * cos φB * sin²((λB - λA)/2)
|
||||
// c = 2 * atan2( √a, √(1−a) )
|
||||
// d = R * c
|
||||
|
||||
// Convert our latitude/longitude to radians, since the various math
|
||||
// functions take radians but latitude/longitude are in degrees.
|
||||
lat1, lon1 := degreesToRadians(l.Latitude), degreesToRadians(l.Longitude)
|
||||
lat2, lon2 := degreesToRadians(other.Latitude), degreesToRadians(other.Longitude)
|
||||
|
||||
deltaPhi := lat2 - lat1
|
||||
deltaLambda := lon2 - lon1
|
||||
|
||||
// Haversine
|
||||
a := math.Pow(math.Sin(deltaPhi/2), 2) +
|
||||
math.Cos(lat1)*math.Cos(lat2)*math.Pow(math.Sin(deltaLambda/2), 2)
|
||||
c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
|
||||
|
||||
// Return the distance in km.
|
||||
const earthRadiusKm = 6371
|
||||
return c * earthRadiusKm
|
||||
}
|
||||
|
||||
func degreesToRadians(d float64) float64 {
|
||||
return d * math.Pi / 180
|
||||
}
|
||||
|
||||
// Neighbours returns the nearest n neighbours to the provided point from the
|
||||
// set of candidates.
|
||||
func Neighbours(point Location, n int, candidates []Location) []Location {
|
||||
// Calculate all distances up-front to avoid recalculating during the
|
||||
// sort below.
|
||||
distances := map[Location]float64{}
|
||||
for _, candidate := range candidates {
|
||||
distances[candidate] = candidate.Distance(point)
|
||||
}
|
||||
|
||||
// Sort the candidates slice by their distance to the provided point.
|
||||
candidates = slices.Clone(candidates)
|
||||
slices.SortFunc(candidates, func(a, b Location) bool {
|
||||
return distances[a] < distances[b]
|
||||
})
|
||||
|
||||
return candidates[:n]
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package neighbour
|
||||
|
||||
import (
|
||||
"math"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHaversine(t *testing.T) {
|
||||
one := Location{51.510357, -0.116773} // King's College, London
|
||||
two := Location{38.889931, -77.009003} // The White House
|
||||
|
||||
dist := one.Distance(two)
|
||||
want := 5897.658
|
||||
|
||||
if math.Abs(want-dist) > 0.001 {
|
||||
t.Fatalf("distance mismatch; got %v, want %v", dist, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNeighbours(t *testing.T) {
|
||||
// Provincial capitals
|
||||
capitals := []Location{
|
||||
{48.4283182, -123.3649533}, // Victoria, BC, Canada
|
||||
{60.721571, -135.054932}, // Whitehorse, YT, Canada
|
||||
{53.5462055, -113.491241}, // Edmonton, AB, Canada
|
||||
{62.4540807, -114.377385}, // Yellowknife, NT, Canada
|
||||
{50.44876, -104.61731}, // Regina, SK, Canada
|
||||
{49.8955367, -97.1384584}, // Winnipeg, MB, Canada
|
||||
{63.74944, -68.521857}, // Iqaluit, NU, Canada
|
||||
{43.6534817, -79.3839347}, // Toronto, ON, Canada
|
||||
{45.5031824, -73.5698065}, // Montreal, QC, Canada
|
||||
{45.94780155, -66.6534707}, // Fredericton, NB, Canada
|
||||
{44.648618, -63.5859487}, // Halifax, NS, Canada
|
||||
{46.234953, -63.132935}, // Charlottetown, PE, Canada
|
||||
{47.5614705, -52.7126162}, // St. John’s, NL, Canada
|
||||
}
|
||||
|
||||
// Thunder Bay, Ontario, Canada
|
||||
point := Location{48.382221, -89.246109}
|
||||
nearest := Neighbours(point, 4, capitals)
|
||||
want := []Location{
|
||||
{49.8955367, -97.1384584}, // Winnipeg, MB, Canada
|
||||
{43.6534817, -79.3839347}, // Toronto, ON, Canada
|
||||
{50.44876, -104.61731}, // Regina, SK, Canada
|
||||
{45.5031824, -73.5698065}, // Montreal, QC, Canada
|
||||
}
|
||||
if !reflect.DeepEqual(nearest, want) {
|
||||
t.Errorf("nearest points mismatch\ngot: %v\nwant: %v", nearest, want)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue