Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ tools/bin/mage js:translations # after adding/changing frontend react-intl m
tools/bin/mage go:eventData # after adding/changing events
```

Changing backend error definitions (`errors.Define*`) requires running **both** `tools/bin/mage go:messages` and `tools/bin/mage js:translations`: the locale files in `pkg/webui/locales/*.json` embed the backend error keys from `config/messages.json`, so CI fails if only the first is regenerated.

Go tests that need Redis **skip themselves silently** unless `TEST_REDIS=1` is set and the databases are running (`dev:dbStart`) — passing output may mean skipped, not tested. `REDIS_ADDRESS`/`REDIS_DB` override the target instance; `TEST_SLOWDOWN` scales test timeouts on slow machines.

Add `-v` to mage for verbose output when a target fails silently. If a build is in a broken state, rebuild the frontend from scratch: `tools/bin/mage jsSDK:cleanDeps jsSDK:clean js:cleanDeps js:clean js:build`.
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ For details about compatibility between different releases, see the **Commitment
### Added

- `gs_gateways_disconnected_total` metric, counting gateway disconnections by protocol and by the error the connection was disconnected with. This makes disconnection reasons (such as gateways disappearing without a close handshake, or missing too many pongs) observable as a rate, instead of only through logs.
- `ttgc.managed-gateway-euis` configuration option: Gateway EUI prefixes of managed gateways, defaulting to the EUI prefix of The Things Industries managed gateways. Gateways outside these prefixes are reported as not managed in the claiming info.
- `ttgc.lbscups.lns-port` configuration option: the LoRa Basics Station LNS port of the Gateway Server, defaulting to `8887`.

### Changed

Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
18 changes: 14 additions & 4 deletions cmd/internal/shared/config.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
// Copyright © 2019 The Things Network Foundation, The Things Industries B.V.
//
// Licensed under the Apache License, Version 2.0 (the "License");
Expand Down Expand Up @@ -178,19 +178,29 @@

// DefaultTTGCConfig is the default config for The Things Gateway Controller.
var DefaultTTGCConfig = ttgc.Config{
GatewayEUIs: []types.EUI64Prefix{
// The Things Industries gateways
{
GatewayEUIs: []types.EUI64Range{
// TTIGPro gateways.
types.EUI64Prefix{
EUI64: types.EUI64{0xEC, 0x65, 0x6E, 0xFF, 0xFE, 0x00, 0x00, 0x00},
Length: 40,
},
}.EUI64Range(),
},
ManagedGatewayEUIs: []types.EUI64Range{
// TTIGPro gateways.
types.EUI64Prefix{
EUI64: types.EUI64{0xEC, 0x65, 0x6E, 0xFF, 0xFE, 0x00, 0x00, 0x00},
Length: 40,
}.EUI64Range(),
},
Address: "gc.thethings.industries:443",
TLS: tlsconfig.ClientAuth{
Source: "file",
Certificate: "cert.pem",
Key: "key.pem",
},
LBSCUPS: ttgc.LBSCUPSConfig{
LNSPort: 8887,
},
}

// DefaultServiceBase is the default base config for a service.
Expand Down
45 changes: 45 additions & 0 deletions config/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -4076,6 +4076,15 @@
"file": "gateways.go"
}
},
"error:pkg/deviceclaimingserver/retry:operation_unsuccessful": {
"translations": {
"en": "operation `{name}` unsuccessful after `{number}` attempts"
},
"description": {
"package": "pkg/deviceclaimingserver/retry",
"file": "task.go"
}
},
"error:pkg/deviceclaimingserver:claim gateway": {
"translations": {
"en": "claim gateway"
Expand Down Expand Up @@ -4112,6 +4121,15 @@
"file": "grpc_end_devices.go"
}
},
"error:pkg/deviceclaimingserver:fetch_created_gateway": {
"translations": {
"en": "fetch gateway after creation"
},
"description": {
"package": "pkg/deviceclaimingserver",
"file": "grpc_gateways.go"
}
},
"error:pkg/deviceclaimingserver:gateway_already_exists": {
"translations": {
"en": "gateway with EUI `{eui}` already exists"
Expand Down Expand Up @@ -4193,6 +4211,15 @@
"file": "grpc_end_devices.go"
}
},
"error:pkg/deviceclaimingserver:update_gateway": {
"translations": {
"en": "update gateway"
},
"description": {
"package": "pkg/deviceclaimingserver",
"file": "grpc_gateways.go"
}
},
"error:pkg/devicerepository/store/bleve:cannot_open_index": {
"translations": {
"en": "cannot open index"
Expand Down Expand Up @@ -9800,6 +9827,24 @@
"file": "eui.go"
}
},
"error:pkg/types:invalid_eui64_range": {
"translations": {
"en": "invalid EUI64 range `{value}`"
},
"description": {
"package": "pkg/types",
"file": "eui.go"
}
},
"error:pkg/types:invalid_eui64_ranges": {
"translations": {
"en": "invalid EUI64 ranges of `{name}`"
},
"description": {
"package": "pkg/types",
"file": "eui.go"
}
},
"error:pkg/types:invalid_join_nonce": {
"translations": {
"en": "invalid JoinNonce"
Expand Down
49 changes: 4 additions & 45 deletions pkg/deviceclaimingserver/gateways/gateways.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package gateways
import (
"context"
"crypto/tls"
"strings"

"go.thethings.network/lorawan-stack/v3/pkg/cluster"
"go.thethings.network/lorawan-stack/v3/pkg/config"
Expand Down Expand Up @@ -52,42 +51,6 @@ var (
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) {
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
switch {
case strings.Contains(val, "/"):
var prefix types.EUI64Prefix
if err := prefix.UnmarshalText([]byte(val)); err != nil {
return nil, errInvalidUpstream.WithAttributes("name", host).WithCause(err)
}
r = dcstypes.RangeFromEUI64Prefix(prefix)
case strings.Contains(val, "-"):
parts := strings.Split(val, "-")
if len(parts) != 2 {
return nil, errInvalidUpstream.WithAttributes("name", host)
}
var start, end types.EUI64
if err := start.UnmarshalText([]byte(parts[0])); err != nil {
return nil, errInvalidUpstream.WithAttributes("name", host).WithCause(err)
}
if err := end.UnmarshalText([]byte(parts[1])); err != nil {
return nil, errInvalidUpstream.WithAttributes("name", host).WithCause(err)
}
r = dcstypes.RangeFromEUI64Range(start, end)
default:
return nil, errInvalidUpstream.WithAttributes("name", host)
}
res[host] = append(res[host], r)
}
}
return res, nil
}

// Claimer provides methods for claiming Gateways.
type Claimer interface {
// Claim claims a gateway.
Expand All @@ -102,7 +65,7 @@ type Claimer interface {

// rangeClaimer supports claiming a range of EUIs.
type rangeClaimer struct {
ranges []dcstypes.EUI64Range
ranges []types.EUI64Range
Claimer
}

Expand All @@ -125,19 +88,15 @@ func NewUpstream(
opt(upstream)
}

hosts, err := ParseGatewayEUIRanges(conf.Upstreams)
hosts, err := types.ParseEUI64RangesMap(conf.Upstreams)
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
hosts["ttgc"] = ttgcConf.GatewayEUIs
}

// Setup upstream table.
Expand Down Expand Up @@ -170,7 +129,7 @@ func NewUpstream(
type Option func(*Upstream)

// WithClaimer adds a claimer to Upstream.
func WithClaimer(name string, ranges []dcstypes.EUI64Range, claimer Claimer) Option {
func WithClaimer(name string, ranges []types.EUI64Range, claimer Claimer) Option {
return func(upstream *Upstream) {
upstream.claimers[name] = rangeClaimer{
Claimer: claimer,
Expand Down
29 changes: 14 additions & 15 deletions pkg/deviceclaimingserver/gateways/gateways_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import (
"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"
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"
Expand All @@ -39,11 +38,11 @@ func TestUpstream(t *testing.T) {
ServiceBase: config.ServiceBase{
TTGC: ttgc.Config{
Enabled: true,
GatewayEUIs: []types.EUI64Prefix{
{
GatewayEUIs: []types.EUI64Range{
types.EUI64Prefix{
EUI64: types.EUI64{0x58, 0xa0, 0xcb, 0xff, 0xfe, 0x80, 0x00, 0x00},
Length: 48,
},
}.EUI64Range(),
},
TLS: tlsconfig.ClientAuth{
Source: "file",
Expand All @@ -56,32 +55,32 @@ func TestUpstream(t *testing.T) {

// Invalid ranges.
ranges := map[string][]string{"ttgc": {"&S(FU*)"}}
euiPrefixes, err := gateways.ParseGatewayEUIRanges(ranges)
euiPrefixes, err := types.ParseEUI64RangesMap(ranges)
a.So(err, should.NotBeNil)
a.So(euiPrefixes, should.BeEmpty)

ranges = map[string][]string{"ttgc": {"58A0CBFFFE800000"}}
euiPrefixes, err = gateways.ParseGatewayEUIRanges(ranges)
euiPrefixes, err = types.ParseEUI64RangesMap(ranges)
a.So(err, should.NotBeNil)
a.So(euiPrefixes, should.BeEmpty)

ranges = map[string][]string{"ttgc": {"58A0CBFFFE800000/123456"}}
euiPrefixes, err = gateways.ParseGatewayEUIRanges(ranges)
euiPrefixes, err = types.ParseEUI64RangesMap(ranges)
a.So(err, should.NotBeNil)
a.So(euiPrefixes, should.BeEmpty)

ranges = map[string][]string{"ttgc": {"58A0CBFFFE800000-58A0CBFFFE800000-58A0CBFFFE800000"}}
euiPrefixes, err = gateways.ParseGatewayEUIRanges(ranges)
euiPrefixes, err = types.ParseEUI64RangesMap(ranges)
a.So(err, should.NotBeNil)
a.So(euiPrefixes, should.BeEmpty)

ranges = map[string][]string{"ttgc": {"001616FFFEWXUSD-001616FFFETGENDE"}}
euiPrefixes, err = gateways.ParseGatewayEUIRanges(ranges)
euiPrefixes, err = types.ParseEUI64RangesMap(ranges)
a.So(err, should.NotBeNil)
a.So(euiPrefixes, should.BeEmpty)

ranges = map[string][]string{"ttgc": {"001616FFFE42DFAD-001616FFFETGENDE"}}
euiPrefixes, err = gateways.ParseGatewayEUIRanges(ranges)
euiPrefixes, err = types.ParseEUI64RangesMap(ranges)
a.So(err, should.NotBeNil)
a.So(euiPrefixes, should.BeEmpty)

Expand All @@ -92,15 +91,15 @@ func TestUpstream(t *testing.T) {
"001616FFFE42DFAD-001616FFFE42E395",
},
}
euiPrefixes, err = gateways.ParseGatewayEUIRanges(ranges)
euiPrefixes, err = types.ParseEUI64RangesMap(ranges)
a.So(err, should.BeNil)
a.So(euiPrefixes, should.Resemble, map[string][]dcstypes.EUI64Range{
a.So(euiPrefixes, should.Resemble, map[string][]types.EUI64Range{
"ttgc": {
dcstypes.RangeFromEUI64Prefix(types.EUI64Prefix{
types.EUI64Prefix{
EUI64: types.EUI64{0x58, 0xa0, 0xcb, 0xff, 0xfe, 0x80, 0x00, 0x00},
Length: 48,
}),
dcstypes.RangeFromEUI64Range(
}.EUI64Range(),
types.EUI64RangeFromInterval(
types.EUI64{0x00, 0x16, 0x16, 0xff, 0xfe, 0x42, 0xdf, 0xad},
types.EUI64{0x00, 0x16, 0x16, 0xff, 0xfe, 0x42, 0xe3, 0x95},
),
Expand Down
Loading
Loading