diff --git a/internal/register/register.go b/internal/register/register.go new file mode 100644 index 0000000..3dc2d55 --- /dev/null +++ b/internal/register/register.go @@ -0,0 +1,98 @@ +package register + +import ( + "encoding/hex" + "fmt" + "net" + "strings" + + v0 "github.com/m-lab/autojoin/api/v0" + "github.com/m-lab/autojoin/iata" + v2 "github.com/m-lab/locate/api/v2" + "github.com/m-lab/uuid-annotator/annotator" + "github.com/oschwald/geoip2-golang" +) + +var ( + mlabDomain = "measurement-lab.org" +) + +// Params is used internally to collect multiple parameters. +type Params struct { + Project string + Service string + Org string + IPv4 string + IPv6 string + Geo *geoip2.City + Metro iata.Row + Network *annotator.Network +} + +// CreateRegisterResponse generates a RegisterResponse from the given +// parameters. As an internal package, the caller is required to validate all +// input parameters. +func CreateRegisterResponse(p *Params) v0.RegisterResponse { + // Calculate machine, site, and hostname. + machine := hex.EncodeToString(net.ParseIP(p.IPv4).To4()) + site := fmt.Sprintf("%s%d", p.Metro.IATA, p.Network.ASNumber) + hostname := fmt.Sprintf("%s-%s-%s.%s.%s.%s", p.Service, site, machine, p.Org, strings.TrimPrefix(p.Project, "mlab-"), mlabDomain) + + // Using these, create geo annotation. + geo := &annotator.Geolocation{ + ContinentCode: p.Geo.Continent.Code, + CountryCode: p.Geo.Country.IsoCode, + CountryName: p.Geo.Country.Names["en"], + MetroCode: int64(p.Geo.Location.MetroCode), + City: p.Geo.City.Names["en"], + PostalCode: p.Geo.Postal.Code, + // Use iata location as authoritative. + Latitude: p.Metro.Latitude, + Longitude: p.Metro.Longitude, + } + if len(p.Geo.Subdivisions) > 0 { + geo.Subdivision1ISOCode = p.Geo.Subdivisions[0].IsoCode + geo.Subdivision1Name = p.Geo.Subdivisions[0].Names["en"] + if len(p.Geo.Subdivisions) > 1 { + geo.Subdivision2ISOCode = p.Geo.Subdivisions[1].IsoCode + geo.Subdivision2Name = p.Geo.Subdivisions[1].Names["en"] + } + } + + // Put everything together into a RegisterResponse. + r := v0.RegisterResponse{ + Registration: &v0.Registration{ + Hostname: hostname, + Annotation: &v0.ServerAnnotation{ + Annotation: annotator.ServerAnnotations{ + Site: site, + Machine: machine, + Geo: geo, + Network: p.Network, + }, + Network: v0.Network{ + IPv4: p.IPv4, + IPv6: p.IPv6, + }, + Type: "unknown", // should be overridden by node. + }, + Heartbeat: &v2.Registration{ + City: geo.City, + CountryCode: geo.CountryCode, + ContinentCode: geo.ContinentCode, + Experiment: p.Service, + Hostname: hostname, + Latitude: geo.Latitude, + Longitude: geo.Longitude, + Machine: machine, + Metro: site[:3], + Project: p.Project, + Probability: 1, + Site: site, + Type: "unknown", // should be overridden by node. + Uplink: "unknown", // should be overridden by node. + }, + }, + } + return r +} diff --git a/internal/register/register_test.go b/internal/register/register_test.go new file mode 100644 index 0000000..98fbf6d --- /dev/null +++ b/internal/register/register_test.go @@ -0,0 +1,117 @@ +package register + +import ( + "strings" + "testing" + + "github.com/go-test/deep" + v0 "github.com/m-lab/autojoin/api/v0" + "github.com/m-lab/autojoin/iata" + v2 "github.com/m-lab/locate/api/v2" + "github.com/m-lab/uuid-annotator/annotator" + "github.com/oschwald/geoip2-golang" +) + +func TestCreateRegisterResponse(t *testing.T) { + tests := []struct { + name string + p *Params + want v0.RegisterResponse + }{ + { + name: "success", + p: &Params{ + Project: "mlab-sandbox", + Service: "ndt", + Org: "bar", + IPv4: "192.168.0.1", + IPv6: "", + Geo: &geoip2.City{ + Country: struct { + GeoNameID uint `maxminddb:"geoname_id"` + IsInEuropeanUnion bool `maxminddb:"is_in_european_union"` + IsoCode string `maxminddb:"iso_code"` + Names map[string]string `maxminddb:"names"` + }{ + IsoCode: "US", + }, + Subdivisions: []struct { + GeoNameID uint `maxminddb:"geoname_id"` + IsoCode string `maxminddb:"iso_code"` + Names map[string]string `maxminddb:"names"` + }{ + {IsoCode: "NY", Names: map[string]string{"en": "New York"}}, + {IsoCode: "ZZ", Names: map[string]string{"en": "fake thing"}}, + }, + Location: struct { + AccuracyRadius uint16 `maxminddb:"accuracy_radius"` + Latitude float64 `maxminddb:"latitude"` + Longitude float64 `maxminddb:"longitude"` + MetroCode uint `maxminddb:"metro_code"` + TimeZone string `maxminddb:"time_zone"` + }{ + Latitude: 41, + Longitude: -73, + }, + }, + Metro: iata.Row{ + IATA: "lga", + Latitude: -10, + Longitude: -10, + }, + Network: &annotator.Network{ + ASNumber: 12345, + }, + }, + want: v0.RegisterResponse{ + Registration: &v0.Registration{ + Hostname: "ndt-lga12345-c0a80001.bar.sandbox.measurement-lab.org", + Annotation: &v0.ServerAnnotation{ + Annotation: annotator.ServerAnnotations{ + Site: "lga12345", + Machine: "c0a80001", + Geo: &annotator.Geolocation{ + CountryCode: "US", + Subdivision1ISOCode: "NY", + Subdivision1Name: "New York", + Subdivision2ISOCode: "ZZ", + Subdivision2Name: "fake thing", + Latitude: -10, + Longitude: -10, + }, + Network: &annotator.Network{ + ASNumber: 12345, + }, + }, + Network: v0.Network{ + IPv4: "192.168.0.1", + }, + Type: "unknown", + }, + Heartbeat: &v2.Registration{ + CountryCode: "US", + Experiment: "ndt", + Hostname: "ndt-lga12345-c0a80001.bar.sandbox.measurement-lab.org", + Latitude: -10, + Longitude: -10, + Machine: "c0a80001", + Metro: "lga", + Project: "mlab-sandbox", + Probability: 1, + Site: "lga12345", + Type: "unknown", + Uplink: "unknown", + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := CreateRegisterResponse(tt.p) + if diff := deep.Equal(got, tt.want); diff != nil { + t.Errorf("CreateRegisterResponse() returned != expected: \n%s", strings.Join(diff, "\n")) + } + }) + } +}