From 53efc1bb9961a805cc0eb423b492a2325d248b2b Mon Sep 17 00:00:00 2001 From: Johan Stokking Date: Mon, 29 Jul 2024 23:02:50 +0200 Subject: [PATCH] dcs: Implement claiming with Gateway Controller --- go.mod | 2 +- go.sum | 4 +- .../deviceclaimingserver.go | 2 +- pkg/deviceclaimingserver/gateways/gateways.go | 34 +++- .../gateways/gateways_test.go | 40 +++- .../gateways/testdata/client-key.pem | 28 +++ .../gateways/testdata/client.pem | 26 +++ .../gateways/ttgc/root_ca.go | 62 +++++++ .../gateways/ttgc/ttgc.go | 173 ++++++++++++++++-- pkg/deviceclaimingserver/grpc_gateways.go | 4 +- .../grpc_gateways_test.go | 13 +- pkg/deviceclaimingserver/util_test.go | 6 +- 12 files changed, 349 insertions(+), 45 deletions(-) create mode 100644 pkg/deviceclaimingserver/gateways/testdata/client-key.pem create mode 100644 pkg/deviceclaimingserver/gateways/testdata/client.pem create mode 100644 pkg/deviceclaimingserver/gateways/ttgc/root_ca.go diff --git a/go.mod b/go.mod index 57a625505c5..0448eee817a 100644 --- a/go.mod +++ b/go.mod @@ -88,7 +88,7 @@ require ( go.packetbroker.org/api/mapping/v2 v2.3.2 go.packetbroker.org/api/routing v1.9.2 go.packetbroker.org/api/v3 v3.17.1 - go.thethings.industries/pkg/api/gen/tti/gateway v0.0.0-20240723094213-b40a14f3b543 + go.thethings.industries/pkg/api/gen/tti/gateway v0.0.0-20240729145607-ea516688afbd go.thethings.industries/pkg/ca v0.0.0-20240723151912-b9bb4097ae6c go.thethings.network/lorawan-application-payload v0.0.0-20220125153912-1198ff1e403e go.thethings.network/lorawan-stack-legacy/v2 v2.1.0 diff --git a/go.sum b/go.sum index d2848d201e8..fb188ba6c06 100644 --- a/go.sum +++ b/go.sum @@ -654,8 +654,8 @@ go.packetbroker.org/api/routing v1.9.2 h1:J4+4vYZxa60UWC70Y9yy7sktU7DXaAp9Q13Bfq go.packetbroker.org/api/routing v1.9.2/go.mod h1:kd2K7gieDI35YfPA8/zDmLX3qiKPuXia/MA77BEAeUA= go.packetbroker.org/api/v3 v3.17.1 h1:LcyFPUGqVubGWMvQ16tZlQIKd+noGx7urzEYhSLiEQA= go.packetbroker.org/api/v3 v3.17.1/go.mod h1:6bVbdWAYLnvZ5kgXxA7GBQvZTN7vxI0DoF1Di1NoAT4= -go.thethings.industries/pkg/api/gen/tti/gateway v0.0.0-20240723094213-b40a14f3b543 h1:CpDA1J3O/krqQrPypf+ePIV5xiLyy9RIayLXRnxiDSI= -go.thethings.industries/pkg/api/gen/tti/gateway v0.0.0-20240723094213-b40a14f3b543/go.mod h1:2+WsMwIunNLh22oauBzGL56JazE3UY34W1fstqEbacw= +go.thethings.industries/pkg/api/gen/tti/gateway v0.0.0-20240729145607-ea516688afbd h1:FvD516hdD/iWqoS20SFdcoiUgwRJP3egNXNiN+Ux2d0= +go.thethings.industries/pkg/api/gen/tti/gateway v0.0.0-20240729145607-ea516688afbd/go.mod h1:2+WsMwIunNLh22oauBzGL56JazE3UY34W1fstqEbacw= go.thethings.industries/pkg/ca v0.0.0-20240723151912-b9bb4097ae6c h1:QkZ+O889SvaXAoJdIu2hyrAXvlIfuHDWOmlpOh97RHg= go.thethings.industries/pkg/ca v0.0.0-20240723151912-b9bb4097ae6c/go.mod h1:89OU623VYKW9i3W4CZgIGFmtgb/jsN8JV2PAuCsj+7w= go.thethings.network/lorawan-application-payload v0.0.0-20220125153912-1198ff1e403e h1:TWGQ3lh7gI2W5hnb6qPdpoAa0d7s/XPwvgf2VVCMJaY= diff --git a/pkg/deviceclaimingserver/deviceclaimingserver.go b/pkg/deviceclaimingserver/deviceclaimingserver.go index e56087de99e..c2ba2a660a8 100644 --- a/pkg/deviceclaimingserver/deviceclaimingserver.go +++ b/pkg/deviceclaimingserver/deviceclaimingserver.go @@ -74,7 +74,7 @@ func New(c *component.Component, conf *Config, opts ...Option) (*DeviceClaimingS } if dcs.grpc.gatewayClaimingServer == nil { - upstream, err := gateways.NewUpstream(ctx, conf.GatewayClaimingServerConfig) + upstream, err := gateways.NewUpstream(ctx, c, conf.GatewayClaimingServerConfig) if err != nil { return nil, err } diff --git a/pkg/deviceclaimingserver/gateways/gateways.go b/pkg/deviceclaimingserver/gateways/gateways.go index e2004daea6c..d7a2e439060 100644 --- a/pkg/deviceclaimingserver/gateways/gateways.go +++ b/pkg/deviceclaimingserver/gateways/gateways.go @@ -17,23 +17,34 @@ package gateways import ( "context" + "crypto/tls" "strings" + "go.thethings.network/lorawan-stack/v3/pkg/config" + "go.thethings.network/lorawan-stack/v3/pkg/config/tlsconfig" "go.thethings.network/lorawan-stack/v3/pkg/deviceclaimingserver/gateways/ttgc" dcstypes "go.thethings.network/lorawan-stack/v3/pkg/deviceclaimingserver/types" "go.thethings.network/lorawan-stack/v3/pkg/errors" "go.thethings.network/lorawan-stack/v3/pkg/types" ) +// Component is the interface to the component. +type Component interface { + GetBaseConfig(context.Context) config.ServiceBase + GetTLSClientConfig(context.Context, ...tlsconfig.Option) (*tls.Config, error) +} + // Config is the configuration for the Gateway Claiming Server. type Config struct { CreateOnNotFound bool `name:"create-on-not-found" description:"DEPRECATED"` // nolint:lll DefaultGatewayServerAddress string `name:"default-gateway-server-address" description:"The default Gateway Server Address"` // nolint:lll Upstreams map[string][]string `name:"upstreams" description:"Map of upstream type and the supported Gateway EUI ranges"` // nolint:lll - TTGC ttgc.Config `name:"ttgc"` } -var errInvalidUpstream = errors.DefineInvalidArgument("invalid_upstream", "upstream `{name}` is invalid") +var ( + errInvalidUpstream = errors.DefineInvalidArgument("invalid_upstream", "upstream `{name}` is invalid") + errTTGCNotEnabled = errors.DefineFailedPrecondition("ttgc_not_enabled", "TTGC is not enabled") +) // ParseGatewayEUIRanges parses the configured upstream map and returns map of ranges. func ParseGatewayEUIRanges(conf map[string][]string) (map[string][]dcstypes.EUI64Range, error) { @@ -76,7 +87,7 @@ type Claimer interface { // Claim claims a gateway. Claim(ctx context.Context, eui types.EUI64, ownerToken string, clusterAddress string) error // Unclaim unclaims a gateway. - Unclaim(context.Context, types.EUI64, string) error + Unclaim(ctx context.Context, eui types.EUI64) error } // rangeClaimer supports claiming a range of EUIs. @@ -93,6 +104,7 @@ type Upstream struct { // NewUpstream returns a new upstream based on the provided configuration. func NewUpstream( ctx context.Context, + c Component, conf Config, opts ...Option, ) (*Upstream, error) { @@ -107,6 +119,17 @@ func NewUpstream( if err != nil { return nil, err } + + // Implicitly add TTGC if it is enabled and not already configured. + ttgcConf := c.GetBaseConfig(ctx).TTGC + if _, ttgcAdded := hosts["ttgc"]; ttgcConf.Enabled && !ttgcAdded { + ttgcRanges := make([]dcstypes.EUI64Range, len(ttgcConf.GatewayEUIs)) + for i, prefix := range ttgcConf.GatewayEUIs { + ttgcRanges[i] = dcstypes.RangeFromEUI64Prefix(prefix) + } + hosts["ttgc"] = ttgcRanges + } + // Setup upstream table. for name, ranges := range hosts { if len(ranges) == 0 || name == "" { @@ -115,7 +138,10 @@ func NewUpstream( var claimer Claimer switch name { case "ttgc": - claimer, err = conf.TTGC.NewClient(ctx) + if !ttgcConf.Enabled { + return nil, errTTGCNotEnabled.New() + } + claimer, err = ttgc.New(ctx, c, ttgcConf) if err != nil { return nil, err } diff --git a/pkg/deviceclaimingserver/gateways/gateways_test.go b/pkg/deviceclaimingserver/gateways/gateways_test.go index 6ee5192f2a9..bcbe541894a 100644 --- a/pkg/deviceclaimingserver/gateways/gateways_test.go +++ b/pkg/deviceclaimingserver/gateways/gateways_test.go @@ -17,10 +17,14 @@ package gateways_test import ( "testing" + "go.thethings.network/lorawan-stack/v3/pkg/component" + componenttest "go.thethings.network/lorawan-stack/v3/pkg/component/test" + "go.thethings.network/lorawan-stack/v3/pkg/config" + "go.thethings.network/lorawan-stack/v3/pkg/config/tlsconfig" "go.thethings.network/lorawan-stack/v3/pkg/deviceclaimingserver/gateways" - "go.thethings.network/lorawan-stack/v3/pkg/deviceclaimingserver/gateways/ttgc" dcstypes "go.thethings.network/lorawan-stack/v3/pkg/deviceclaimingserver/types" "go.thethings.network/lorawan-stack/v3/pkg/errors" + "go.thethings.network/lorawan-stack/v3/pkg/ttgc" "go.thethings.network/lorawan-stack/v3/pkg/types" "go.thethings.network/lorawan-stack/v3/pkg/util/test" "go.thethings.network/lorawan-stack/v3/pkg/util/test/assertions/should" @@ -31,6 +35,25 @@ func TestUpstream(t *testing.T) { a, ctx := test.New(t) + c := componenttest.NewComponent(t, &component.Config{ + ServiceBase: config.ServiceBase{ + TTGC: ttgc.Config{ + Enabled: true, + GatewayEUIs: []types.EUI64Prefix{ + { + EUI64: types.EUI64{0x58, 0xa0, 0xcb, 0xff, 0xfe, 0x80, 0x00, 0x00}, + Length: 48, + }, + }, + TLS: tlsconfig.ClientAuth{ + Source: "file", + Certificate: "testdata/client.pem", + Key: "testdata/client-key.pem", + }, + }, + }, + }) + // Invalid ranges. ranges := map[string][]string{"ttgc": {"&S(FU*)"}} euiPrefixes, err := gateways.ParseGatewayEUIRanges(ranges) @@ -85,28 +108,25 @@ func TestUpstream(t *testing.T) { }) // Invalid configurations - config := gateways.Config{ + conf := gateways.Config{ Upstreams: map[string][]string{"ttgc": {"&S(FU*)"}}, - TTGC: ttgc.Config{}, } - upstream, err := gateways.NewUpstream(ctx, config) + upstream, err := gateways.NewUpstream(ctx, c, conf) a.So(errors.IsInvalidArgument(err), should.BeTrue) a.So(upstream, should.BeNil) - config = gateways.Config{ + conf = gateways.Config{ Upstreams: map[string][]string{"unsupported": {"58A0CBFFFE800000/48"}}, - TTGC: ttgc.Config{}, } - upstream, err = gateways.NewUpstream(ctx, config) + upstream, err = gateways.NewUpstream(ctx, c, conf) a.So(errors.IsInvalidArgument(err), should.BeTrue) a.So(upstream, should.BeNil) // Valid Configuration - config = gateways.Config{ + conf = gateways.Config{ Upstreams: map[string][]string{"ttgc": {"58A0CBFFFE800000/48"}}, - TTGC: ttgc.Config{}, } - upstream, err = gateways.NewUpstream(ctx, config) + upstream, err = gateways.NewUpstream(ctx, c, conf) a.So(err, should.BeNil) a.So(upstream, should.NotBeNil) diff --git a/pkg/deviceclaimingserver/gateways/testdata/client-key.pem b/pkg/deviceclaimingserver/gateways/testdata/client-key.pem new file mode 100644 index 00000000000..f575927b35e --- /dev/null +++ b/pkg/deviceclaimingserver/gateways/testdata/client-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDF+is3SbxVrjeO +yooAeJfjAD9GylMI5Z4cfHjPgeTf2bZlfVnmx+q5rdrN6B/cQo1BEIqFHZ9B6oqx +CZUgBBg9yZx12XjQk4kWvGhEnuHfJI3c188iZdUt6aJHITKaGGkbu6JJzaJ+eUl+ +pa4Ghae4gsP5qOgsp03LdSAZXByeP9E1nFu9MR1lJCfI2sBLoHLEIh3pA6YPhRvL +/aPBeCdKqnTVce61YnXxQQVHrA82smeICHRvx1KQWkvN7Y0icDSBh74vsRaaO+zP +suHTR5EmpmqNX5VZosoUrNCQHddAvQwBGZv/ULzykVPInKAzvHAnAKGyYmwdOrJM +fAVOQl3tAgMBAAECggEBALLy67kv1yKDNQjGnnLKjk/LW2a7Xs697mrFP9YhDSYh +fjLCWU63Cb4IHazc0l+fcFqNfwfPvLIyNGbNyJOF3/uJjvkfN4sgsFtytbTBAF1Y +hzpFf58R+N1lPx+YLEsJIYjF021uiCBVtU7apzCSAwZOfKHgQOyL1U/AcOE7V0rZ +v7efhQCSDJ76RAY1FuUDw8vk4kevWmWbjeVOoxnMjKfhOwL3Tfm8QXfc3USCv7Xk +85Od8mXxhrxRQLIk1RBYxhUj8uWSK0IvDE/jbyFEZQK10D8LnNWwbk1dg26nE3xu +oQbqUcF05ujlsDFGWCZ40d2mlu0Na/pskyjxu/9GQykCgYEA9lr8kVj+mTNamyPF +/ltc6l/+ofYDaMqZctZdYieZXg3J/6HN7y/xSDS5TBn4TVX7JvSw5rzoY3spzsO/ +49ZbCR4bTxM+ZdVZkkLrtNzVonVAiSSLWpK+1oCPOO+1ULIUMmJanJHKkQ5R33iH +2BZaDSC2J11uJc1E+moXnr+YMu8CgYEAzbpUBcB3bmsG14kHh2PEcO03Gm9v7wuF +nPEcrANVuwAxrow/keM+oyKaj3g5EUvlZDMwh6PvlhOo3Xtp5gS+RRfkMVLcQPXd +Apf7B6OEPL54rwrcoZbxEl2+Hf3UUl9SIKastMCdtHzvLmCYh+12eM/R9Lp1D6DL +3sWQRw62DOMCgYEA1o7L5fLyWo9lXDS93hfIRsAwTvKqaXv6RQ/56ODALDAqO5+6 +cZT5uX9h2qvLm99Ei9sUrwDcDLhZ4yCNYWtxgfFcq3QBJkO4bTAnhS/ISGOCP286 +hznDR6JUGqx657sQ6AjNDgvTtp4YJF8fQM3GxCQ3QPWYVwf+CXKY+8O2VLUCgYEA +i75mtqV/OvTeZ+f/wjrFxEOOK5nIueLktq+dX0bApE7EcKF5yPpIoP2vaYcrlJEu +V7rh2zFDXHksOo5LZ+CO8lYBPnPfgwy/PLTJ4u1ytORZC+Xf6q+iP2yH6M1zvSRc +oCs3o2w3c9NtkN4ynhpyYCwinQ9O1vfNpBwlHe9jQCsCgYB3KRWR4UGc2faQbBE9 +fMmbdTkA79gkwvr1q4pRQlCGnOxeIE4vfR4RCkfRkBJimMSI2sNDHFiDQZ8DJq6Y +3cwaL4uUSo9MEdY2ZZxaGgxWaEu4DH+ZV0lJv/RDgTbjD0xwomxXOn/+9UM17Nf9 +KYSfdCWcjW0kxJJ7PO4n8d65rQ== +-----END PRIVATE KEY----- diff --git a/pkg/deviceclaimingserver/gateways/testdata/client.pem b/pkg/deviceclaimingserver/gateways/testdata/client.pem new file mode 100644 index 00000000000..15bfcf60047 --- /dev/null +++ b/pkg/deviceclaimingserver/gateways/testdata/client.pem @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEWzCCAsOgAwIBAgIQNsTUWUKitSTLD+uN4zzpuzANBgkqhkiG9w0BAQsFADCB +hTEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMS0wKwYDVQQLDCRqb2hh +bkBKb2hhbi0yLmxvY2FsIChKb2hhbiBTdG9ra2luZykxNDAyBgNVBAMMK21rY2Vy +dCBqb2hhbkBKb2hhbi0yLmxvY2FsIChKb2hhbiBTdG9ra2luZykwHhcNMTkwNjAx +MDAwMDAwWhcNMzAwOTAzMTYyMDU0WjBYMScwJQYDVQQKEx5ta2NlcnQgZGV2ZWxv +cG1lbnQgY2VydGlmaWNhdGUxLTArBgNVBAsMJGpvaGFuQEpvaGFuLTIubG9jYWwg +KEpvaGFuIFN0b2traW5nKTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AMX6KzdJvFWuN47KigB4l+MAP0bKUwjlnhx8eM+B5N/ZtmV9WebH6rmt2s3oH9xC +jUEQioUdn0HqirEJlSAEGD3JnHXZeNCTiRa8aESe4d8kjdzXzyJl1S3pokchMpoY +aRu7oknNon55SX6lrgaFp7iCw/mo6CynTct1IBlcHJ4/0TWcW70xHWUkJ8jawEug +csQiHekDpg+FG8v9o8F4J0qqdNVx7rVidfFBBUesDzayZ4gIdG/HUpBaS83tjSJw +NIGHvi+xFpo77M+y4dNHkSamao1flVmiyhSs0JAd10C9DAEZm/9QvPKRU8icoDO8 +cCcAobJibB06skx8BU5CXe0CAwEAAaNzMHEwDgYDVR0PAQH/BAQDAgWgMB0GA1Ud +JQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMB8GA1UdIwQY +MBaAFDzAUZ9AtGTILuMWdD2VfCm0D72DMBEGA1UdEQQKMAiCBjAwMDAwMTANBgkq +hkiG9w0BAQsFAAOCAYEAI8mhLKPl4qUlZ2g1vmVrAGTGc0M2dzs4Xp2gyys+puup +sP4pRPQnIrEJaZcI2mk/4TTOpyF1tmLGaZ0V/hMzf07I5vB/Kz+jdj0+3AGVixwR ++KehvHVn6njfcZqa3l4Q7pFWyQtb199M1XPwGkQSEzxthU7dKH/447T8HwKp3xoK +hKb3pNd0h0MXgGGhawHFP5AHpI0x05cWT5zLXu5nXnjYt9UKpPCUH6Htcofg33GX +Y3rj3KvUz9WHYw/6LE+LlKjUKLkCG8bq2qaTRSrJQh5qTJRldzqjJ4qa7A7hUYJN +qm+Wb/FIMIA/sdfdMEwGAWhkZJjZlYoGv3LU7QLIkaCWjRsTs1gRw2EVCo0mFJ6x +LRm0UDBIIr30fEkGhSk38EYiX3HbxkCF61HPehhzL4noMkoDAuMq+8gFRVDBHOlv +YiNn1Y22LBsFdoKkbqI3nI62sFvI8lXpKRrL34lndSCevVXOEUOeupDdD1XvHYHQ +0bPVWAqh/KAO8JMDauCa +-----END CERTIFICATE----- diff --git a/pkg/deviceclaimingserver/gateways/ttgc/root_ca.go b/pkg/deviceclaimingserver/gateways/ttgc/root_ca.go new file mode 100644 index 00000000000..c366437c527 --- /dev/null +++ b/pkg/deviceclaimingserver/gateways/ttgc/root_ca.go @@ -0,0 +1,62 @@ +// Copyright © 2024 The Things Network Foundation, The Things Industries B.V. +// +// 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. + +// Package ttgc provides functions to use The Things Gateway Controller. +package ttgc + +import ( + "context" + "crypto/tls" + "crypto/x509" + "net" + + "go.thethings.network/lorawan-stack/v3/pkg/errors" +) + +var ( + errDialGatewayServer = errors.DefineAborted("dial_gateway_server", "failed to dial Gateway Gerver") + errGatewayServerTLS = errors.DefineAborted( + "gateway_server_tls", "failed to establish TLS connection with Gateway Server", + ) +) + +func getRootCA(ctx context.Context, address string) (*x509.Certificate, error) { + host, _, err := net.SplitHostPort(address) + if err != nil { + host = address + } + address = net.JoinHostPort(host, "8889") + + d := new(net.Dialer) + netConn, err := d.DialContext(ctx, "tcp", address) + if err != nil { + return nil, errDialGatewayServer.WithCause(err) + } + defer netConn.Close() + + tlsConn := tls.Client(netConn, &tls.Config{ + InsecureSkipVerify: true, //nolint:gosec + ServerName: host, + }) + if err := tlsConn.HandshakeContext(ctx); err != nil { + return nil, errGatewayServerTLS.WithCause(err) + } + + state := tlsConn.ConnectionState() + if len(state.PeerCertificates) == 0 { + return nil, errGatewayServerTLS.New() + } + + return state.PeerCertificates[len(state.PeerCertificates)-1], nil +} diff --git a/pkg/deviceclaimingserver/gateways/ttgc/ttgc.go b/pkg/deviceclaimingserver/gateways/ttgc/ttgc.go index efcc204cd1c..0669c0b299d 100644 --- a/pkg/deviceclaimingserver/gateways/ttgc/ttgc.go +++ b/pkg/deviceclaimingserver/gateways/ttgc/ttgc.go @@ -16,35 +16,176 @@ package ttgc import ( + "bytes" "context" - "go.thethings.network/lorawan-stack/v3/pkg/errors" + northboundv1 "go.thethings.industries/pkg/api/gen/tti/gateway/controller/northbound/v1" + "go.thethings.network/lorawan-stack/v3/pkg/log" + "go.thethings.network/lorawan-stack/v3/pkg/ttgc" "go.thethings.network/lorawan-stack/v3/pkg/types" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/durationpb" ) -// Config is the configuration for the client. -type Config struct{} +const profileGroup = "tts" -// TTGC is the client for The Things Gateway Controller. -type TTGC struct { - config Config +// Upstream is the client for The Things Gateway Controller. +type Upstream struct { + client *ttgc.Client } -// NewClient returns a new TTGC client. -func (c Config) NewClient(context.Context) (*TTGC, error) { - return &TTGC{ - config: c, +// New returns a new upstream client for The Things Gateway Controller. +func New(ctx context.Context, c ttgc.Component, config ttgc.Config) (*Upstream, error) { + client, err := ttgc.NewClient(ctx, c, config) + if err != nil { + return nil, err + } + return &Upstream{ + client, }, nil } -var errUnimplemented = errors.DefineUnimplemented("not_implemented", "not implemented") - // Claim implements gateways.GatewayClaimer. -func (TTGC) Claim(context.Context, types.EUI64, string, string) error { - return errUnimplemented.New() +// Claim does four things: +// 1. Claim the gateway +// 2. Upsert a LoRa Packet Forwarder profile with the root CA presented by the given Gateway Server +// 3. Upsert a Geolocation profile +// 4. Update the gateway with the profiles +func (u *Upstream) Claim(ctx context.Context, eui types.EUI64, ownerToken, clusterAddress string) error { + logger := log.FromContext(ctx) + + // Claim the gateway. + gtwClient := northboundv1.NewGatewayServiceClient(u.client) + _, err := gtwClient.Claim(ctx, &northboundv1.GatewayServiceClaimRequest{ + GatewayId: eui.MarshalNumber(), + Domain: u.client.Domain(ctx), + OwnerToken: ownerToken, + }) + if err != nil { + return err + } + + // Get the root CA from the Gateway Server and upsert the LoRa Packet Forwarder profile. + rootCA, err := getRootCA(ctx, clusterAddress) + if err != nil { + return err + } + var ( + loraPFProfileID []byte + loraPFProfile = &northboundv1.LoraPacketForwarderProfile{ + ProfileName: clusterAddress, + Shared: true, + Protocol: northboundv1.LoraPacketForwarderProtocol_LORA_PACKET_FORWARDER_PROTOCOL_TTI_V1, + Address: clusterAddress, + RootCa: rootCA.Raw, + } + loraPFProfileClient = northboundv1.NewLoraPacketForwarderProfileServiceClient(u.client) + ) + loraPFGetRes, err := loraPFProfileClient.GetByName( + ctx, + &northboundv1.LoraPacketForwarderProfileServiceGetByNameRequest{ + Domain: u.client.Domain(ctx), + Group: profileGroup, + ProfileName: clusterAddress, + }, + ) + if err != nil { + if status.Code(err) != codes.NotFound { + logger.WithError(err).Warn("Failed to get LoRa Packet Forwarder profile") + return err + } + res, err := loraPFProfileClient.Create(ctx, &northboundv1.LoraPacketForwarderProfileServiceCreateRequest{ + Domain: u.client.Domain(ctx), + Group: profileGroup, + LoraPacketForwarderProfile: loraPFProfile, + }) + if err != nil { + logger.WithError(err).Warn("Failed to create LoRa Packet Forwarder profile") + return err + } + loraPFProfileID = res.ProfileId + } else if profile := loraPFGetRes.LoraPacketForwarderProfile; profile.Shared != loraPFProfile.Shared || + profile.Protocol != loraPFProfile.Protocol || + !bytes.Equal(profile.RootCa, loraPFProfile.RootCa) { + _, err := loraPFProfileClient.Update(ctx, &northboundv1.LoraPacketForwarderProfileServiceUpdateRequest{ + Domain: u.client.Domain(ctx), + Group: profileGroup, + ProfileId: loraPFGetRes.ProfileId, + LoraPacketForwarderProfile: loraPFProfile, + }) + if err != nil { + logger.WithError(err).Warn("Failed to update LoRa Packet Forwarder profile") + return err + } + loraPFProfileID = loraPFGetRes.ProfileId + } + + // Upsert the Geolocation profile. + var ( + geolocationProfileID []byte + geolocationProfile = &northboundv1.GeolocationProfile{ + ProfileName: "hourly", + Shared: true, + DisconnectedFor: durationpb.New(0), + } + geolocationProfileClient = northboundv1.NewGeolocationProfileServiceClient(u.client) + ) + geolocationGetRes, err := geolocationProfileClient.GetByName( + ctx, + &northboundv1.GeolocationProfileServiceGetByNameRequest{ + Domain: u.client.Domain(ctx), + Group: profileGroup, + ProfileName: geolocationProfile.ProfileName, + }, + ) + if err != nil { + if status.Code(err) != codes.NotFound { + logger.WithError(err).Warn("Failed to get geolocation profile") + return err + } + res, err := geolocationProfileClient.Create(ctx, &northboundv1.GeolocationProfileServiceCreateRequest{ + Domain: u.client.Domain(ctx), + Group: profileGroup, + GeolocationProfile: geolocationProfile, + }) + if err != nil { + logger.WithError(err).Warn("Failed to create geolocation profile") + return err + } + geolocationProfileID = res.ProfileId + } else { + geolocationProfileID = geolocationGetRes.ProfileId + } + + // Update the gateway with the profiles. + _, err = gtwClient.Update(ctx, &northboundv1.GatewayServiceUpdateRequest{ + GatewayId: eui.MarshalNumber(), + Domain: u.client.Domain(ctx), + LoraPacketForwarderProfileId: &northboundv1.ProfileIDValue{ + Value: loraPFProfileID, + }, + GeolocationProfileId: &northboundv1.ProfileIDValue{ + Value: geolocationProfileID, + }, + }) + if err != nil { + logger.WithError(err).Warn("Failed to update gateway with profiles") + return err + } + + return nil } // Unclaim implements gateways.GatewayClaimer. -func (TTGC) Unclaim(context.Context, types.EUI64, string) error { - return errUnimplemented.New() +func (u *Upstream) Unclaim(ctx context.Context, eui types.EUI64) error { + gtwClient := northboundv1.NewGatewayServiceClient(u.client) + _, err := gtwClient.Unclaim(ctx, &northboundv1.GatewayServiceUnclaimRequest{ + GatewayId: eui.MarshalNumber(), + Domain: u.client.Domain(ctx), + }) + if err != nil { + return err + } + return nil } diff --git a/pkg/deviceclaimingserver/grpc_gateways.go b/pkg/deviceclaimingserver/grpc_gateways.go index 1db18606b43..56c4edb864c 100644 --- a/pkg/deviceclaimingserver/grpc_gateways.go +++ b/pkg/deviceclaimingserver/grpc_gateways.go @@ -132,7 +132,7 @@ func (gcls *gatewayClaimingServer) Claim( defer func(ids *ttnpb.GatewayIdentifiers) { if retErr != nil { observability.RegisterAbortClaim(ctx, ids.GetEntityIdentifiers(), retErr) - if err := claimer.Unclaim(ctx, gatewayEUI, string(authCode)); err != nil { + if err := claimer.Unclaim(ctx, gatewayEUI); err != nil { logger.WithError(err).Warn("Failed to unclaim gateway") } return @@ -210,7 +210,7 @@ func (gcls gatewayClaimingServer) Unclaim(ctx context.Context, req *ttnpb.Gatewa return nil, errGatewayClaimingNotSupported.WithAttributes("eui", gatewayEUI) } - if err := claimer.Unclaim(ctx, gatewayEUI, gtw.GatewayServerAddress); err != nil { + if err := claimer.Unclaim(ctx, gatewayEUI); err != nil { observability.RegisterFailUnclaim(ctx, gtw.GetEntityIdentifiers(), err) return nil, err } diff --git a/pkg/deviceclaimingserver/grpc_gateways_test.go b/pkg/deviceclaimingserver/grpc_gateways_test.go index 2f19ea485e9..1881b98b5b3 100644 --- a/pkg/deviceclaimingserver/grpc_gateways_test.go +++ b/pkg/deviceclaimingserver/grpc_gateways_test.go @@ -74,6 +74,7 @@ func TestGatewayClaimingServer(t *testing.T) { // nolint:paralleltest mockGatewayclaimer := &MockGatewayClaimer{} mockUpstream, err := gateways.NewUpstream( ctx, + c, gateways.Config{}, gateways.WithClaimer( "mock", @@ -175,7 +176,7 @@ func TestGatewayClaimingServer(t *testing.T) { // nolint:paralleltest CallOpt grpc.CallOption ClaimFunc func(context.Context, types.EUI64, string, string) error CreateFunc func(context.Context, *ttnpb.CreateGatewayRequest) (*ttnpb.Gateway, error) - UnclaimFunc func(context.Context, types.EUI64, string) error + UnclaimFunc func(context.Context, types.EUI64) error ErrorAssertion func(error) bool }{ { @@ -289,7 +290,7 @@ func TestGatewayClaimingServer(t *testing.T) { // nolint:paralleltest CreateFunc: func(context.Context, *ttnpb.CreateGatewayRequest) (*ttnpb.Gateway, error) { return nil, errCreate.New() }, - UnclaimFunc: func(_ context.Context, eui types.EUI64, _ string) error { + UnclaimFunc: func(_ context.Context, eui types.EUI64) error { if eui.Equal(supportedEUI) { return nil } @@ -317,7 +318,7 @@ func TestGatewayClaimingServer(t *testing.T) { // nolint:paralleltest CreateFunc: func(context.Context, *ttnpb.CreateGatewayRequest) (*ttnpb.Gateway, error) { return nil, errCreate.New() }, - UnclaimFunc: func(context.Context, types.EUI64, string) error { + UnclaimFunc: func(context.Context, types.EUI64) error { return errUnclaim.New() }, ErrorAssertion: errors.IsAborted, @@ -373,7 +374,7 @@ func TestGatewayClaimingServer(t *testing.T) { // nolint:paralleltest Req *ttnpb.GatewayIdentifiers CallOpt grpc.CallOption GetFunc func(context.Context, *ttnpb.GetGatewayRequest) (*ttnpb.Gateway, error) - UnclaimFunc func(context.Context, types.EUI64, string) error + UnclaimFunc func(context.Context, types.EUI64) error ErrorAssertion func(error) bool }{ { @@ -460,7 +461,7 @@ func TestGatewayClaimingServer(t *testing.T) { // nolint:paralleltest GatewayServerAddress: "test.example.com", }, nil }, - UnclaimFunc: func(context.Context, types.EUI64, string) error { + UnclaimFunc: func(context.Context, types.EUI64) error { return errUnclaim.New() }, CallOpt: authorizedCallOpt, @@ -480,7 +481,7 @@ func TestGatewayClaimingServer(t *testing.T) { // nolint:paralleltest GatewayServerAddress: "test.example.com", }, nil }, - UnclaimFunc: func(context.Context, types.EUI64, string) error { + UnclaimFunc: func(context.Context, types.EUI64) error { return nil }, CallOpt: authorizedCallOpt, diff --git a/pkg/deviceclaimingserver/util_test.go b/pkg/deviceclaimingserver/util_test.go index db7f8750881..6a3eaae742d 100644 --- a/pkg/deviceclaimingserver/util_test.go +++ b/pkg/deviceclaimingserver/util_test.go @@ -81,7 +81,7 @@ type MockGatewayClaimer struct { EUIs []types.EUI64 ClaimFunc func(context.Context, types.EUI64, string, string) error - UnclaimFunc func(context.Context, types.EUI64, string) error + UnclaimFunc func(context.Context, types.EUI64) error } // Claim implements gateways.Claimer. @@ -95,8 +95,8 @@ func (claimer MockGatewayClaimer) Claim( } // Unclaim implements gateways.Claimer. -func (claimer MockGatewayClaimer) Unclaim(ctx context.Context, eui types.EUI64, clusterAddress string) error { - return claimer.UnclaimFunc(ctx, eui, clusterAddress) +func (claimer MockGatewayClaimer) Unclaim(ctx context.Context, eui types.EUI64) error { + return claimer.UnclaimFunc(ctx, eui) } type mockGatewayRegistry struct {