diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e4db02a6d..2ab17a71e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ For details about compatibility between different releases, see the **Commitment ### Added -- Support for managed gateways through The Things Gateway Controller. +- Support for managed gateways and claiming through The Things Gateway Controller. - Support for The Things Industries gateway protocol. This is adds a new pair of ports to Gateway Server: `1889` for Envoy or Traefik terminated TLS mutual authentication, and `8889` for The Things Stack terminated TLS mutual authentication. ### Changed diff --git a/api/ttn/lorawan/v3/api.md b/api/ttn/lorawan/v3/api.md index 90ade78880..791c9d743e 100644 --- a/api/ttn/lorawan/v3/api.md +++ b/api/ttn/lorawan/v3/api.md @@ -3100,6 +3100,7 @@ DEPRECATED: This message is deprecated and will be removed in a future version o | ----- | ---- | ----- | ----------- | | `eui` | [`bytes`](#bytes) | | | | `supports_claiming` | [`bool`](#bool) | | | +| `is_managed` | [`bool`](#bool) | | Indicates whether the gateway is a managed gateway. If true, when the gateway is successfully claimed, it can be managed with ManagedGatewayConfigurationService. | #### Field Rules diff --git a/api/ttn/lorawan/v3/api.swagger.json b/api/ttn/lorawan/v3/api.swagger.json index f85fc23557..76c14eba7f 100644 --- a/api/ttn/lorawan/v3/api.swagger.json +++ b/api/ttn/lorawan/v3/api.swagger.json @@ -24696,6 +24696,10 @@ }, "supports_claiming": { "type": "boolean" + }, + "is_managed": { + "type": "boolean", + "description": "Indicates whether the gateway is a managed gateway.\nIf true, when the gateway is successfully claimed, it can be managed with ManagedGatewayConfigurationService." } } }, diff --git a/api/ttn/lorawan/v3/deviceclaimingserver.proto b/api/ttn/lorawan/v3/deviceclaimingserver.proto index e57a207541..50064baadb 100644 --- a/api/ttn/lorawan/v3/deviceclaimingserver.proto +++ b/api/ttn/lorawan/v3/deviceclaimingserver.proto @@ -419,6 +419,9 @@ message GetInfoByGatewayEUIResponse { } ]; bool supports_claiming = 2; + // Indicates whether the gateway is a managed gateway. + // If true, when the gateway is successfully claimed, it can be managed with ManagedGatewayConfigurationService. + bool is_managed = 3; } // The GatewayClaimingServer service support claiming and managing gateway claims. diff --git a/config/messages.json b/config/messages.json index a4abaf59db..2ba1261ee8 100644 --- a/config/messages.json +++ b/config/messages.json @@ -3923,13 +3923,22 @@ "file": "ttjs.go" } }, - "error:pkg/deviceclaimingserver/gateways/ttgc:not_implemented": { + "error:pkg/deviceclaimingserver/gateways/ttgc:dial_gateway_server": { "translations": { - "en": "not implemented" + "en": "dial Gateway Gerver" }, "description": { "package": "pkg/deviceclaimingserver/gateways/ttgc", - "file": "ttgc.go" + "file": "root_ca.go" + } + }, + "error:pkg/deviceclaimingserver/gateways/ttgc:gateway_server_tls": { + "translations": { + "en": "establish TLS connection with Gateway Server" + }, + "description": { + "package": "pkg/deviceclaimingserver/gateways/ttgc", + "file": "root_ca.go" } }, "error:pkg/deviceclaimingserver/gateways:invalid_upstream": { @@ -3941,6 +3950,15 @@ "file": "gateways.go" } }, + "error:pkg/deviceclaimingserver/gateways:ttgc_not_enabled": { + "translations": { + "en": "TTGC is not enabled" + }, + "description": { + "package": "pkg/deviceclaimingserver/gateways", + "file": "gateways.go" + } + }, "error:pkg/deviceclaimingserver:claim gateway": { "translations": { "en": "claim gateway" diff --git a/go.mod b/go.mod index d7286b9a35..b638341dc8 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 eff606a768..84479063b3 100644 --- a/go.sum +++ b/go.sum @@ -658,8 +658,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 e56087de99..c2ba2a660a 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 ec3b18f73d..2428e27dde 100644 --- a/pkg/deviceclaimingserver/gateways/gateways.go +++ b/pkg/deviceclaimingserver/gateways/gateways.go @@ -17,28 +17,39 @@ 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(config map[string][]string) (map[string][]dcstypes.EUI64Range, error) { - res := make(map[string][]dcstypes.EUI64Range, len(config)) - for host, ranges := range config { +func ParseGatewayEUIRanges(conf map[string][]string) (map[string][]dcstypes.EUI64Range, error) { + res := make(map[string][]dcstypes.EUI64Range, len(conf)) + for host, ranges := range conf { res[host] = make([]dcstypes.EUI64Range, 0, len(ranges)) for _, val := range ranges { var r dcstypes.EUI64Range @@ -76,7 +87,9 @@ 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 + // IsManagedGateway returns true if the gateway is a managed gateway. + IsManagedGateway(ctx context.Context, eui types.EUI64) (bool, error) } // rangeClaimer supports claiming a range of EUIs. @@ -93,6 +106,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 +121,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 +140,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 6ee5192f2a..bcbe541894 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 0000000000..f575927b35 --- /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 0000000000..15bfcf6004 --- /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 0000000000..b49a686ed0 --- /dev/null +++ b/pkg/deviceclaimingserver/gateways/ttgc/root_ca.go @@ -0,0 +1,54 @@ +// 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", "dial Gateway Gerver") + errGatewayServerTLS = errors.DefineAborted( + "gateway_server_tls", "establish TLS connection with Gateway Server", + ) +) + +func (u *Upstream) getRootCA(ctx context.Context, address string) (*x509.Certificate, error) { + d := new(net.Dialer) + netConn, err := d.DialContext(ctx, "tcp", address) + if err != nil { + return nil, errDialGatewayServer.WithCause(err) + } + defer netConn.Close() + + tlsConfig, err := u.GetTLSClientConfig(ctx) + if err != nil { + return nil, err + } + tlsConn := tls.Client(netConn, tlsConfig) + if err := tlsConn.HandshakeContext(ctx); err != nil { + return nil, errGatewayServerTLS.WithCause(err) + } + + state := tlsConn.ConnectionState() + verifiedChain := state.VerifiedChains[0] + return verifiedChain[len(verifiedChain)-1], nil +} diff --git a/pkg/deviceclaimingserver/gateways/ttgc/ttgc.go b/pkg/deviceclaimingserver/gateways/ttgc/ttgc.go index efcc204cd1..cb687eeca4 100644 --- a/pkg/deviceclaimingserver/gateways/ttgc/ttgc.go +++ b/pkg/deviceclaimingserver/gateways/ttgc/ttgc.go @@ -16,35 +16,198 @@ package ttgc import ( + "bytes" "context" + "crypto/tls" + "net" - "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/config/tlsconfig" + "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 +type component interface { + GetTLSClientConfig(context.Context, ...tlsconfig.Option) (*tls.Config, error) } -// NewClient returns a new TTGC client. -func (c Config) NewClient(context.Context) (*TTGC, error) { - return &TTGC{ - config: c, - }, nil +// Upstream is the client for The Things Gateway Controller. +type Upstream struct { + component + client *ttgc.Client } -var errUnimplemented = errors.DefineUnimplemented("not_implemented", "not implemented") +// 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{ + component: c, + client: client, + }, nil +} // 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. + host, _, err := net.SplitHostPort(clusterAddress) + if err != nil { + host = clusterAddress + } + clusterAddress = net.JoinHostPort(host, "8889") + rootCA, err := u.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: "on connect", + 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 +} + +// IsManagedGateway implements gateways.GatewayClaimer. +// This method always returns true. +func (*Upstream) IsManagedGateway(context.Context, types.EUI64) (bool, error) { + return true, nil } diff --git a/pkg/deviceclaimingserver/grpc_gateways.go b/pkg/deviceclaimingserver/grpc_gateways.go index 1db18606b4..0b29c4ead1 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 @@ -169,11 +169,24 @@ func (gcls gatewayClaimingServer) GetInfoByGatewayEUI( if err != nil { return nil, err } - eui := types.MustEUI64(in.Eui).OrZero() + var ( + eui = types.MustEUI64(in.Eui).OrZero() + claimer = gcls.upstream.Claimer(eui) + supportsClaiming = claimer != nil + isManaged bool + ) + if supportsClaiming { + var err error + isManaged, err = claimer.IsManagedGateway(ctx, eui) + if err != nil { + return nil, err + } + } return &ttnpb.GetInfoByGatewayEUIResponse{ Eui: in.Eui, - SupportsClaiming: gcls.upstream.Claimer(eui) != nil, + SupportsClaiming: supportsClaiming, + IsManaged: isManaged, }, nil } @@ -210,7 +223,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 2f19ea485e..5882eee7d4 100644 --- a/pkg/deviceclaimingserver/grpc_gateways_test.go +++ b/pkg/deviceclaimingserver/grpc_gateways_test.go @@ -71,9 +71,14 @@ func TestGatewayClaimingServer(t *testing.T) { // nolint:paralleltest }, }) - mockGatewayclaimer := &MockGatewayClaimer{} + mockGatewayclaimer := &MockGatewayClaimer{ + IsManagedGatewayFunc: func(_ context.Context, e types.EUI64) (bool, error) { + return e.Equal(supportedEUI), nil + }, + } mockUpstream, err := gateways.NewUpstream( ctx, + c, gateways.Config{}, gateways.WithClaimer( "mock", @@ -156,6 +161,7 @@ func TestGatewayClaimingServer(t *testing.T) { // nolint:paralleltest a.So(err, should.BeNil) a.So(resp.Eui, should.Resemble, unsupportedEUI.Bytes()) a.So(resp.SupportsClaiming, should.BeFalse) + a.So(resp.IsManaged, should.BeFalse) resp, err = gclsClient.GetInfoByGatewayEUI( ctx, @@ -167,6 +173,7 @@ func TestGatewayClaimingServer(t *testing.T) { // nolint:paralleltest a.So(err, should.BeNil) a.So(resp.Eui, should.Resemble, supportedEUI.Bytes()) a.So(resp.SupportsClaiming, should.BeTrue) + a.So(resp.IsManaged, should.BeTrue) // Test claiming for _, tc := range []struct { @@ -175,7 +182,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 +296,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 +324,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 +380,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 +467,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 +487,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 db7f875088..1c1dc94611 100644 --- a/pkg/deviceclaimingserver/util_test.go +++ b/pkg/deviceclaimingserver/util_test.go @@ -80,8 +80,9 @@ func (m MockEndDeviceClaimer) BatchUnclaim( type MockGatewayClaimer struct { EUIs []types.EUI64 - ClaimFunc func(context.Context, types.EUI64, string, string) error - UnclaimFunc func(context.Context, types.EUI64, string) error + ClaimFunc func(context.Context, types.EUI64, string, string) error + UnclaimFunc func(context.Context, types.EUI64) error + IsManagedGatewayFunc func(context.Context, types.EUI64) (bool, error) } // Claim implements gateways.Claimer. @@ -95,8 +96,13 @@ 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) +} + +// IsManagedGateway implements gateways.Claimer. +func (claimer MockGatewayClaimer) IsManagedGateway(ctx context.Context, eui types.EUI64) (bool, error) { + return claimer.IsManagedGatewayFunc(ctx, eui) } type mockGatewayRegistry struct { diff --git a/pkg/ttnpb/deviceclaimingserver.pb.go b/pkg/ttnpb/deviceclaimingserver.pb.go index c1d7f7552c..35131b9652 100644 --- a/pkg/ttnpb/deviceclaimingserver.pb.go +++ b/pkg/ttnpb/deviceclaimingserver.pb.go @@ -944,6 +944,9 @@ type GetInfoByGatewayEUIResponse struct { Eui []byte `protobuf:"bytes,1,opt,name=eui,proto3" json:"eui,omitempty"` SupportsClaiming bool `protobuf:"varint,2,opt,name=supports_claiming,json=supportsClaiming,proto3" json:"supports_claiming,omitempty"` + // Indicates whether the gateway is a managed gateway. + // If true, when the gateway is successfully claimed, it can be managed with ManagedGatewayConfigurationService. + IsManaged bool `protobuf:"varint,3,opt,name=is_managed,json=isManaged,proto3" json:"is_managed,omitempty"` } func (x *GetInfoByGatewayEUIResponse) Reset() { @@ -992,6 +995,13 @@ func (x *GetInfoByGatewayEUIResponse) GetSupportsClaiming() bool { return false } +func (x *GetInfoByGatewayEUIResponse) GetIsManaged() bool { + if x != nil { + return x.IsManaged + } + return false +} + type ClaimEndDeviceRequest_AuthenticatedIdentifiers struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1564,7 +1574,7 @@ var file_ttn_lorawan_v3_deviceclaimingserver_proto_rawDesc = []byte{ 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2d, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2f, 0x76, 0x33, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x55, 0x6e, 0x6d, 0x61, 0x72, 0x73, 0x68, 0x61, 0x6c, 0x38, 0x42, 0x79, 0x74, 0x65, 0x73, - 0x52, 0x03, 0x65, 0x75, 0x69, 0x22, 0x94, 0x02, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x66, + 0x52, 0x03, 0x65, 0x75, 0x69, 0x22, 0xb3, 0x02, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x42, 0x79, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x45, 0x55, 0x49, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0xc7, 0x01, 0x0a, 0x03, 0x65, 0x75, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x42, 0xb4, 0x01, 0x92, 0x41, 0x21, 0x4a, 0x12, 0x22, 0x37, 0x30, 0x42, 0x33, @@ -1581,142 +1591,143 @@ var file_ttn_lorawan_v3_deviceclaimingserver_proto_rawDesc = []byte{ 0x73, 0x68, 0x61, 0x6c, 0x38, 0x42, 0x79, 0x74, 0x65, 0x73, 0x52, 0x03, 0x65, 0x75, 0x69, 0x12, 0x2b, 0x0a, 0x11, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x5f, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x73, 0x75, 0x70, 0x70, - 0x6f, 0x72, 0x74, 0x73, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x32, 0xc2, 0x07, 0x0a, - 0x17, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x69, - 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x6c, 0x0a, 0x05, 0x43, 0x6c, 0x61, 0x69, - 0x6d, 0x12, 0x25, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, - 0x76, 0x33, 0x2e, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, - 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, - 0x69, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x22, 0x16, - 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x10, 0x3a, 0x01, 0x2a, 0x22, 0x0b, 0x2f, 0x65, 0x64, 0x63, 0x73, - 0x2f, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x12, 0x91, 0x01, 0x0a, 0x07, 0x55, 0x6e, 0x63, 0x6c, 0x61, - 0x69, 0x6d, 0x12, 0x24, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, - 0x2e, 0x76, 0x33, 0x2e, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x65, - 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x22, 0x48, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x42, 0x2a, 0x40, 0x2f, 0x65, 0x64, 0x63, 0x73, 0x2f, - 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x2f, 0x7b, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x73, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x7b, - 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x12, 0x82, 0x01, 0x0a, 0x10, 0x47, - 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x42, 0x79, 0x4a, 0x6f, 0x69, 0x6e, 0x45, 0x55, 0x49, 0x12, - 0x27, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, - 0x2e, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x42, 0x79, 0x4a, 0x6f, 0x69, 0x6e, 0x45, 0x55, - 0x49, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, - 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x66, - 0x6f, 0x42, 0x79, 0x4a, 0x6f, 0x69, 0x6e, 0x45, 0x55, 0x49, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x1b, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x15, 0x3a, 0x01, 0x2a, 0x22, 0x10, 0x2f, - 0x65, 0x64, 0x63, 0x73, 0x2f, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x2f, 0x69, 0x6e, 0x66, 0x6f, 0x12, - 0xa8, 0x01, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x53, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x12, 0x24, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, - 0x2e, 0x76, 0x33, 0x2e, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x65, - 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x1a, 0x26, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, - 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x61, - 0x69, 0x6d, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x48, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x42, 0x12, 0x40, 0x2f, 0x65, 0x64, 0x63, 0x73, 0x2f, - 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x2f, 0x7b, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x73, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x7b, - 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x12, 0xa5, 0x01, 0x0a, 0x14, 0x41, - 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x12, 0x2b, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, - 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x41, 0x70, - 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x48, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x42, - 0x3a, 0x01, 0x2a, 0x22, 0x3d, 0x2f, 0x65, 0x64, 0x63, 0x73, 0x2f, 0x61, 0x70, 0x70, 0x6c, 0x69, - 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x7b, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x73, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, - 0x7a, 0x65, 0x12, 0x8f, 0x01, 0x0a, 0x16, 0x55, 0x6e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, - 0x7a, 0x65, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x26, 0x2e, - 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x41, - 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, - 0x66, 0x69, 0x65, 0x72, 0x73, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x35, 0x82, - 0xd3, 0xe4, 0x93, 0x02, 0x2f, 0x2a, 0x2d, 0x2f, 0x65, 0x64, 0x63, 0x73, 0x2f, 0x61, 0x70, 0x70, - 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x7b, 0x61, 0x70, 0x70, 0x6c, 0x69, - 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x6f, - 0x72, 0x69, 0x7a, 0x65, 0x1a, 0x3b, 0x92, 0x41, 0x38, 0x12, 0x36, 0x43, 0x6c, 0x61, 0x69, 0x6d, - 0x20, 0x61, 0x6e, 0x64, 0x20, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x20, 0x65, 0x6e, 0x64, 0x20, - 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x20, 0x6f, 0x6e, 0x20, 0x65, 0x78, 0x74, 0x65, 0x72, - 0x6e, 0x61, 0x6c, 0x20, 0x4a, 0x6f, 0x69, 0x6e, 0x20, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, - 0x2e, 0x32, 0xa3, 0x03, 0x0a, 0x1c, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x42, - 0x61, 0x74, 0x63, 0x68, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, - 0x65, 0x72, 0x12, 0xac, 0x01, 0x0a, 0x07, 0x55, 0x6e, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x12, 0x2d, + 0x6f, 0x72, 0x74, 0x73, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x12, 0x1d, 0x0a, 0x0a, + 0x69, 0x73, 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x09, 0x69, 0x73, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x32, 0xc2, 0x07, 0x0a, 0x17, + 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x69, 0x6e, + 0x67, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x6c, 0x0a, 0x05, 0x43, 0x6c, 0x61, 0x69, 0x6d, + 0x12, 0x25, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, + 0x33, 0x2e, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, + 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, + 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x22, 0x16, 0x82, + 0xd3, 0xe4, 0x93, 0x02, 0x10, 0x3a, 0x01, 0x2a, 0x22, 0x0b, 0x2f, 0x65, 0x64, 0x63, 0x73, 0x2f, + 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x12, 0x91, 0x01, 0x0a, 0x07, 0x55, 0x6e, 0x63, 0x6c, 0x61, 0x69, + 0x6d, 0x12, 0x24, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, + 0x76, 0x33, 0x2e, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, + 0x48, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x42, 0x2a, 0x40, 0x2f, 0x65, 0x64, 0x63, 0x73, 0x2f, 0x63, + 0x6c, 0x61, 0x69, 0x6d, 0x2f, 0x7b, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x5f, 0x69, 0x64, 0x73, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x7b, 0x64, + 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x12, 0x82, 0x01, 0x0a, 0x10, 0x47, 0x65, + 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x42, 0x79, 0x4a, 0x6f, 0x69, 0x6e, 0x45, 0x55, 0x49, 0x12, 0x27, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, - 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x6e, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x45, 0x6e, 0x64, 0x44, - 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, + 0x47, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x42, 0x79, 0x4a, 0x6f, 0x69, 0x6e, 0x45, 0x55, 0x49, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, + 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, + 0x42, 0x79, 0x4a, 0x6f, 0x69, 0x6e, 0x45, 0x55, 0x49, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x1b, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x15, 0x3a, 0x01, 0x2a, 0x22, 0x10, 0x2f, 0x65, + 0x64, 0x63, 0x73, 0x2f, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x2f, 0x69, 0x6e, 0x66, 0x6f, 0x12, 0xa8, + 0x01, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x53, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x12, 0x24, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, + 0x76, 0x33, 0x2e, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x1a, 0x26, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, + 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x61, 0x69, + 0x6d, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x48, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x42, 0x12, 0x40, 0x2f, 0x65, 0x64, 0x63, 0x73, 0x2f, 0x63, + 0x6c, 0x61, 0x69, 0x6d, 0x2f, 0x7b, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x5f, 0x69, 0x64, 0x73, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x7b, 0x64, + 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x12, 0xa5, 0x01, 0x0a, 0x14, 0x41, 0x75, + 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x2b, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, + 0x2e, 0x76, 0x33, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x41, 0x70, 0x70, + 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x48, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x42, 0x3a, + 0x01, 0x2a, 0x22, 0x3d, 0x2f, 0x65, 0x64, 0x63, 0x73, 0x2f, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x7b, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x73, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, + 0x65, 0x12, 0x8f, 0x01, 0x0a, 0x16, 0x55, 0x6e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, + 0x65, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x26, 0x2e, 0x74, + 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x41, 0x70, + 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, + 0x69, 0x65, 0x72, 0x73, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x35, 0x82, 0xd3, + 0xe4, 0x93, 0x02, 0x2f, 0x2a, 0x2d, 0x2f, 0x65, 0x64, 0x63, 0x73, 0x2f, 0x61, 0x70, 0x70, 0x6c, + 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x7b, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, + 0x69, 0x7a, 0x65, 0x1a, 0x3b, 0x92, 0x41, 0x38, 0x12, 0x36, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x20, + 0x61, 0x6e, 0x64, 0x20, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x20, 0x65, 0x6e, 0x64, 0x20, 0x64, + 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x20, 0x6f, 0x6e, 0x20, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x20, 0x4a, 0x6f, 0x69, 0x6e, 0x20, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, + 0x32, 0xa3, 0x03, 0x0a, 0x1c, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, + 0x74, 0x63, 0x68, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x12, 0xac, 0x01, 0x0a, 0x07, 0x55, 0x6e, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x12, 0x2d, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x6e, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x45, 0x6e, 0x64, 0x44, 0x65, - 0x76, 0x69, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x42, 0x82, - 0xd3, 0xe4, 0x93, 0x02, 0x3c, 0x2a, 0x3a, 0x2f, 0x65, 0x64, 0x63, 0x73, 0x2f, 0x63, 0x6c, 0x61, - 0x69, 0x6d, 0x2f, 0x7b, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, - 0x69, 0x64, 0x73, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, - 0x69, 0x64, 0x7d, 0x2f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x62, 0x61, 0x74, 0x63, - 0x68, 0x12, 0x8b, 0x01, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x42, 0x79, 0x4a, - 0x6f, 0x69, 0x6e, 0x45, 0x55, 0x49, 0x73, 0x12, 0x28, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, + 0x76, 0x69, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x74, + 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x42, 0x61, + 0x74, 0x63, 0x68, 0x55, 0x6e, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, + 0x69, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x42, 0x82, 0xd3, + 0xe4, 0x93, 0x02, 0x3c, 0x2a, 0x3a, 0x2f, 0x65, 0x64, 0x63, 0x73, 0x2f, 0x63, 0x6c, 0x61, 0x69, + 0x6d, 0x2f, 0x7b, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, + 0x64, 0x73, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, + 0x64, 0x7d, 0x2f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x62, 0x61, 0x74, 0x63, 0x68, + 0x12, 0x8b, 0x01, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x42, 0x79, 0x4a, 0x6f, + 0x69, 0x6e, 0x45, 0x55, 0x49, 0x73, 0x12, 0x28, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, + 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x42, + 0x79, 0x4a, 0x6f, 0x69, 0x6e, 0x45, 0x55, 0x49, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x29, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, + 0x33, 0x2e, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x42, 0x79, 0x4a, 0x6f, 0x69, 0x6e, 0x45, + 0x55, 0x49, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x21, 0x82, 0xd3, 0xe4, + 0x93, 0x02, 0x1b, 0x3a, 0x01, 0x2a, 0x22, 0x16, 0x2f, 0x65, 0x64, 0x63, 0x73, 0x2f, 0x63, 0x6c, + 0x61, 0x69, 0x6d, 0x2f, 0x69, 0x6e, 0x66, 0x6f, 0x2f, 0x62, 0x61, 0x74, 0x63, 0x68, 0x1a, 0x46, + 0x92, 0x41, 0x43, 0x12, 0x41, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x20, 0x62, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x20, 0x6f, 0x66, + 0x20, 0x65, 0x6e, 0x64, 0x20, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x20, 0x6f, 0x6e, 0x20, + 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x20, 0x4a, 0x6f, 0x69, 0x6e, 0x20, 0x53, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x32, 0xbc, 0x05, 0x0a, 0x15, 0x47, 0x61, 0x74, 0x65, 0x77, + 0x61, 0x79, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x12, 0x68, 0x0a, 0x05, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x12, 0x23, 0x2e, 0x74, 0x74, 0x6e, 0x2e, + 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x43, 0x6c, 0x61, 0x69, 0x6d, + 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, + 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, + 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, + 0x72, 0x73, 0x22, 0x16, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x10, 0x3a, 0x01, 0x2a, 0x22, 0x0b, 0x2f, + 0x67, 0x63, 0x6c, 0x73, 0x2f, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x12, 0x67, 0x0a, 0x07, 0x55, 0x6e, + 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x12, 0x22, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, + 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x49, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, + 0x79, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x2a, 0x18, 0x2f, 0x67, 0x63, 0x6c, 0x73, + 0x2f, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x2f, 0x7b, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x5f, + 0x69, 0x64, 0x7d, 0x12, 0x8b, 0x01, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x42, + 0x79, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x45, 0x55, 0x49, 0x12, 0x2a, 0x2e, 0x74, 0x74, + 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x47, 0x65, 0x74, + 0x49, 0x6e, 0x66, 0x6f, 0x42, 0x79, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x45, 0x55, 0x49, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, - 0x42, 0x79, 0x4a, 0x6f, 0x69, 0x6e, 0x45, 0x55, 0x49, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x29, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, - 0x76, 0x33, 0x2e, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x42, 0x79, 0x4a, 0x6f, 0x69, 0x6e, - 0x45, 0x55, 0x49, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x21, 0x82, 0xd3, - 0xe4, 0x93, 0x02, 0x1b, 0x3a, 0x01, 0x2a, 0x22, 0x16, 0x2f, 0x65, 0x64, 0x63, 0x73, 0x2f, 0x63, - 0x6c, 0x61, 0x69, 0x6d, 0x2f, 0x69, 0x6e, 0x66, 0x6f, 0x2f, 0x62, 0x61, 0x74, 0x63, 0x68, 0x1a, - 0x46, 0x92, 0x41, 0x43, 0x12, 0x41, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x20, 0x61, 0x6e, 0x64, 0x20, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x20, 0x62, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x20, 0x6f, - 0x66, 0x20, 0x65, 0x6e, 0x64, 0x20, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x20, 0x6f, 0x6e, - 0x20, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x20, 0x4a, 0x6f, 0x69, 0x6e, 0x20, 0x53, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x32, 0xbc, 0x05, 0x0a, 0x15, 0x47, 0x61, 0x74, 0x65, - 0x77, 0x61, 0x79, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, 0x65, - 0x72, 0x12, 0x68, 0x0a, 0x05, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x12, 0x23, 0x2e, 0x74, 0x74, 0x6e, - 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x43, 0x6c, 0x61, 0x69, - 0x6d, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x42, 0x79, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x45, 0x55, 0x49, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1b, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x15, 0x3a, 0x01, 0x2a, 0x22, + 0x10, 0x2f, 0x67, 0x63, 0x6c, 0x73, 0x2f, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x2f, 0x69, 0x6e, 0x66, + 0x6f, 0x12, 0x94, 0x01, 0x0a, 0x10, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x47, + 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x12, 0x27, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, + 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, + 0x65, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x3f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x36, 0x3a, + 0x01, 0x2a, 0x22, 0x31, 0x2f, 0x67, 0x63, 0x6c, 0x73, 0x2f, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, + 0x79, 0x73, 0x2f, 0x7b, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x5f, 0x69, 0x64, 0x73, 0x2e, + 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x61, 0x75, 0x74, 0x68, + 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x88, 0x02, 0x01, 0x12, 0x82, 0x01, 0x0a, 0x12, 0x55, 0x6e, 0x61, + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x12, 0x22, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, - 0x65, 0x72, 0x73, 0x22, 0x16, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x10, 0x3a, 0x01, 0x2a, 0x22, 0x0b, - 0x2f, 0x67, 0x63, 0x6c, 0x73, 0x2f, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x12, 0x67, 0x0a, 0x07, 0x55, - 0x6e, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x12, 0x22, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, - 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x49, - 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, - 0x74, 0x79, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x2a, 0x18, 0x2f, 0x67, 0x63, 0x6c, - 0x73, 0x2f, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x2f, 0x7b, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, - 0x5f, 0x69, 0x64, 0x7d, 0x12, 0x8b, 0x01, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, - 0x42, 0x79, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x45, 0x55, 0x49, 0x12, 0x2a, 0x2e, 0x74, - 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x47, 0x65, - 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x42, 0x79, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x45, 0x55, - 0x49, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, - 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x66, - 0x6f, 0x42, 0x79, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x45, 0x55, 0x49, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1b, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x15, 0x3a, 0x01, 0x2a, - 0x22, 0x10, 0x2f, 0x67, 0x63, 0x6c, 0x73, 0x2f, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x2f, 0x69, 0x6e, - 0x66, 0x6f, 0x12, 0x94, 0x01, 0x0a, 0x10, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, - 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x12, 0x27, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, - 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, - 0x7a, 0x65, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x3f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x36, - 0x3a, 0x01, 0x2a, 0x22, 0x31, 0x2f, 0x67, 0x63, 0x6c, 0x73, 0x2f, 0x67, 0x61, 0x74, 0x65, 0x77, - 0x61, 0x79, 0x73, 0x2f, 0x7b, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x5f, 0x69, 0x64, 0x73, - 0x2e, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x61, 0x75, 0x74, - 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x88, 0x02, 0x01, 0x12, 0x82, 0x01, 0x0a, 0x12, 0x55, 0x6e, - 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, - 0x12, 0x22, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, - 0x33, 0x2e, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, - 0x69, 0x65, 0x72, 0x73, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x30, 0x82, 0xd3, - 0xe4, 0x93, 0x02, 0x27, 0x2a, 0x25, 0x2f, 0x67, 0x63, 0x6c, 0x73, 0x2f, 0x67, 0x61, 0x74, 0x65, - 0x77, 0x61, 0x79, 0x73, 0x2f, 0x7b, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x5f, 0x69, 0x64, - 0x7d, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x88, 0x02, 0x01, 0x1a, 0x26, - 0x92, 0x41, 0x23, 0x12, 0x21, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x20, 0x67, 0x61, 0x74, 0x65, 0x77, - 0x61, 0x79, 0x73, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x20, 0x63, - 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x2e, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x6f, 0x2e, 0x74, 0x68, 0x65, - 0x74, 0x68, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x6c, - 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2d, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2f, 0x76, 0x33, 0x2f, - 0x70, 0x6b, 0x67, 0x2f, 0x74, 0x74, 0x6e, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x33, + 0x65, 0x72, 0x73, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x30, 0x82, 0xd3, 0xe4, + 0x93, 0x02, 0x27, 0x2a, 0x25, 0x2f, 0x67, 0x63, 0x6c, 0x73, 0x2f, 0x67, 0x61, 0x74, 0x65, 0x77, + 0x61, 0x79, 0x73, 0x2f, 0x7b, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x5f, 0x69, 0x64, 0x7d, + 0x2f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x88, 0x02, 0x01, 0x1a, 0x26, 0x92, + 0x41, 0x23, 0x12, 0x21, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x20, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, + 0x79, 0x73, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x20, 0x63, 0x6c, + 0x61, 0x69, 0x6d, 0x73, 0x2e, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x6f, 0x2e, 0x74, 0x68, 0x65, 0x74, + 0x68, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x6c, 0x6f, + 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2d, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2f, 0x76, 0x33, 0x2f, 0x70, + 0x6b, 0x67, 0x2f, 0x74, 0x74, 0x6e, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/pkg/ttnpb/deviceclaimingserver.pb.paths.fm.go b/pkg/ttnpb/deviceclaimingserver.pb.paths.fm.go index a59e889385..82e7d590e6 100644 --- a/pkg/ttnpb/deviceclaimingserver.pb.paths.fm.go +++ b/pkg/ttnpb/deviceclaimingserver.pb.paths.fm.go @@ -174,11 +174,13 @@ var GetInfoByGatewayEUIRequestFieldPathsTopLevel = []string{ } var GetInfoByGatewayEUIResponseFieldPathsNested = []string{ "eui", + "is_managed", "supports_claiming", } var GetInfoByGatewayEUIResponseFieldPathsTopLevel = []string{ "eui", + "is_managed", "supports_claiming", } var ClaimEndDeviceRequest_AuthenticatedIdentifiersFieldPathsNested = []string{ diff --git a/pkg/ttnpb/deviceclaimingserver.pb.setters.fm.go b/pkg/ttnpb/deviceclaimingserver.pb.setters.fm.go index 68f5fc8d73..17c65f6c00 100644 --- a/pkg/ttnpb/deviceclaimingserver.pb.setters.fm.go +++ b/pkg/ttnpb/deviceclaimingserver.pb.setters.fm.go @@ -814,6 +814,16 @@ func (dst *GetInfoByGatewayEUIResponse) SetFields(src *GetInfoByGatewayEUIRespon var zero bool dst.SupportsClaiming = zero } + case "is_managed": + if len(subs) > 0 { + return fmt.Errorf("'is_managed' has no subfields, but %s were specified", subs) + } + if src != nil { + dst.IsManaged = src.IsManaged + } else { + var zero bool + dst.IsManaged = zero + } default: return fmt.Errorf("invalid field: '%s'", name) diff --git a/pkg/ttnpb/deviceclaimingserver.pb.validate.go b/pkg/ttnpb/deviceclaimingserver.pb.validate.go index 7fcbb6b7fb..2ef5fb29eb 100644 --- a/pkg/ttnpb/deviceclaimingserver.pb.validate.go +++ b/pkg/ttnpb/deviceclaimingserver.pb.validate.go @@ -1696,6 +1696,8 @@ func (m *GetInfoByGatewayEUIResponse) ValidateFields(paths ...string) error { case "supports_claiming": // no validation rules for SupportsClaiming + case "is_managed": + // no validation rules for IsManaged default: return GetInfoByGatewayEUIResponseValidationError{ field: name, diff --git a/pkg/ttnpb/deviceclaimingserver_json.pb.go b/pkg/ttnpb/deviceclaimingserver_json.pb.go index ab584e6795..921c0af8f9 100644 --- a/pkg/ttnpb/deviceclaimingserver_json.pb.go +++ b/pkg/ttnpb/deviceclaimingserver_json.pb.go @@ -742,6 +742,11 @@ func (x *GetInfoByGatewayEUIResponse) MarshalProtoJSON(s *jsonplugin.MarshalStat s.WriteObjectField("supports_claiming") s.WriteBool(x.SupportsClaiming) } + if x.IsManaged || s.HasField("is_managed") { + s.WriteMoreIf(&wroteField) + s.WriteObjectField("is_managed") + s.WriteBool(x.IsManaged) + } s.WriteObjectEnd() } @@ -765,6 +770,9 @@ func (x *GetInfoByGatewayEUIResponse) UnmarshalProtoJSON(s *jsonplugin.Unmarshal case "supports_claiming", "supportsClaiming": s.AddField("supports_claiming") x.SupportsClaiming = s.ReadBool() + case "is_managed", "isManaged": + s.AddField("is_managed") + x.IsManaged = s.ReadBool() } }) } diff --git a/pkg/webui/locales/ja.json b/pkg/webui/locales/ja.json index 2131dd6ed0..005275fc5b 100644 --- a/pkg/webui/locales/ja.json +++ b/pkg/webui/locales/ja.json @@ -1944,8 +1944,10 @@ "error:pkg/deviceclaimingserver/enddevices/ttjsv2:internal_error": "内部エラー", "error:pkg/deviceclaimingserver/enddevices/ttjsv2:unclaim_device": "EUI `{dev_eui}`のデバイスの主張を取り消す", "error:pkg/deviceclaimingserver/enddevices/ttjsv2:unclaim_devices": "デバイスの主張を取り消す", - "error:pkg/deviceclaimingserver/gateways/ttgc:not_implemented": "", + "error:pkg/deviceclaimingserver/gateways/ttgc:dial_gateway_server": "", + "error:pkg/deviceclaimingserver/gateways/ttgc:gateway_server_tls": "", "error:pkg/deviceclaimingserver/gateways:invalid_upstream": "", + "error:pkg/deviceclaimingserver/gateways:ttgc_not_enabled": "", "error:pkg/deviceclaimingserver:claim gateway": "", "error:pkg/deviceclaimingserver:claiming_not_supported": "JoinEUI `{eui}` に対してクレームはサポートされていません", "error:pkg/deviceclaimingserver:create_gateway": "", diff --git a/sdk/js/generated/api.json b/sdk/js/generated/api.json index eb0eb8aed5..059e8619b9 100644 --- a/sdk/js/generated/api.json +++ b/sdk/js/generated/api.json @@ -11626,6 +11626,18 @@ "isoneof": false, "oneofdecl": "", "defaultValue": "" + }, + { + "name": "is_managed", + "description": "Indicates whether the gateway is a managed gateway.\nIf true, when the gateway is successfully claimed, it can be managed with ManagedGatewayConfigurationService.", + "label": "", + "type": "bool", + "longType": "bool", + "fullType": "bool", + "ismap": false, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "" } ] }, diff --git a/tools/go.mod b/tools/go.mod index f83937ccfb..3b6ccd5604 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -218,7 +218,7 @@ require ( go.packetbroker.org/api/mapping/v2 v2.3.2 // indirect go.packetbroker.org/api/routing v1.9.2 // indirect go.packetbroker.org/api/v3 v3.17.1 // indirect - go.thethings.industries/pkg/api/gen/tti/gateway v0.0.0-20240723094213-b40a14f3b543 // indirect + go.thethings.industries/pkg/api/gen/tti/gateway v0.0.0-20240729145607-ea516688afbd // indirect go.thethings.industries/pkg/ca v0.0.0-20240723151912-b9bb4097ae6c // indirect go.thethings.network/lorawan-application-payload v0.0.0-20220125153912-1198ff1e403e // indirect go.thethings.network/lorawan-stack-legacy/v2 v2.1.0 // indirect diff --git a/tools/go.sum b/tools/go.sum index 6b02e88435..62cfaf1390 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -713,8 +713,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=