Skip to content

Commit d9c9b39

Browse files
authored
Merge pull request #97 from TheThingsNetwork/feature/43-firefly
Support migration from the Firefly LNS
2 parents be008da + 012bb90 commit d9c9b39

File tree

24 files changed

+992
-102
lines changed

24 files changed

+992
-102
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
88

99
### Added
1010

11+
- Firefly source.
12+
1113
### Changed
1214

1315
### Deprecated

README.md

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Binaries are available on [GitHub](https://github.com/TheThingsNetwork/lorawan-s
1313
- [x] The Things Network Stack V2
1414
- [x] [ChirpStack Network Server](https://www.chirpstack.io/)
1515
- [x] [The Things Stack](https://www.github.com/TheThingsNetwork/lorawan-stack/)
16-
- [ ] [Firefly](https://fireflyiot.com/)
16+
- [x] [Firefly](https://fireflyiot.com/)
1717
- [ ] [LORIOT Network Server](https://www.loriot.io/)
1818

1919
Support for different sources is done by creating Source plugins. List available sources with:
@@ -238,6 +238,75 @@ $ ttn-lw-migrate tts application 'my-app-id' --dry-run --verbose > devices.json
238238
$ ttn-lw-migrate tts application 'my-app-id' > devices.json
239239
```
240240

241+
## Firefly
242+
243+
### Configuration
244+
245+
Configure with environment variables, or command-line arguments.
246+
247+
See `ttn-lw-migrate firefly {device|application} --help` for more details.
248+
249+
The following example shows how to set options via environment variables.
250+
251+
```bash
252+
$ export FIREFLY_HOST=example.com # Host of the Firefly API
253+
$ export FIREFLY_API_KEY=abcdefgh # Firefly API Key
254+
$ export APP_ID=my-test-app # Application ID for the exported devices
255+
$ export JOIN_EUI=1111111111111111 # JoinEUI for the exported devices
256+
$ export FREQUENCY_PLAN_ID=EU_863_870 # Frequency Plan ID for the exported devices
257+
$ export MAC_VERSION=1.0.2b # LoRaWAN MAC version for the exported devices
258+
```
259+
260+
### Notes
261+
262+
- The export process will halt if any error occurs.
263+
- Use the `--invalidate-keys` option to invalidate the root and/or session keys of the devices on the Firefly server. This is necessary to prevent both networks from communicating with the same device. The last byte of the keys will be incremented by 0x01. This enables an easy rollback if necessary. Setting this flag to false (default) would result in a "dry run", where the devices are exported but they will still be able to communicate with the Firefly server.
264+
265+
### Export Devices
266+
267+
To export a single device using its Device EUI (e.g. `1111111111111112`):
268+
269+
```bash
270+
# dry run first, verify that no errors occur
271+
$ ttn-lw-migrate firefly device 1111111111111112 --verbose > devices.json
272+
# export device
273+
$ ttn-lw-migrate firefly device 1111111111111112 --invalidate-keys > devices.json
274+
```
275+
276+
In order to export a large number of devices, create a file named `device_euis.txt` with one device EUI per line:
277+
278+
```txt
279+
1111111111111112
280+
FF11111111111134
281+
ABCD111111111100
282+
```
283+
284+
And then export with:
285+
286+
```bash
287+
# dry run first, verify that no errors occur
288+
$ ttn-lw-migrate firefly device --verbose < device_ids.txt > devices.json
289+
# export devices
290+
$ ttn-lw-migrate firefly device --invalidate-keys < device_ids.txt > devices.json
291+
```
292+
293+
### Export All Devices
294+
295+
The Firefly LNS does not strictly enforce device to application relationships.
296+
297+
Setting the `--all` flag will export **all devices that are accessible by the API key**. The `application` command without the `--all` flag does nothing.
298+
299+
> Note: Please be cautious while using this command as this might invalidate all the keys of all the devices.
300+
301+
To export all devices accessible by the API Key,
302+
303+
```bash
304+
# dry run first, verify that no errors occur
305+
$ ttn-lw-migrate firefly application --all --verbose > devices.json
306+
# export all devices
307+
$ ttn-lw-migrate firefly application --all --invalidate-keys > devices.json
308+
```
309+
241310
## Development Environment
242311

243312
Requires Go version 1.16 or higher. [Download Go](https://golang.org/dl/).

cmd/firefly/firefly.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright © 2023 The Things Network Foundation, The Things Industries B.V.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package firefly
16+
17+
import (
18+
"go.thethings.network/lorawan-stack-migrate/pkg/commands"
19+
_ "go.thethings.network/lorawan-stack-migrate/pkg/source/firefly"
20+
)
21+
22+
const sourceName = "firefly"
23+
24+
// FireflyCmd represents the firefly source.
25+
var FireflyCmd = commands.Source(sourceName,
26+
"Export devices from Digimondo's Firefly",
27+
)

cmd/root.go

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,14 @@ import (
2020

2121
"github.com/spf13/cobra"
2222
"go.thethings.network/lorawan-stack-migrate/cmd/chirpstack"
23+
"go.thethings.network/lorawan-stack-migrate/cmd/firefly"
2324
"go.thethings.network/lorawan-stack-migrate/cmd/ttnv2"
2425
"go.thethings.network/lorawan-stack-migrate/cmd/tts"
2526
"go.thethings.network/lorawan-stack-migrate/pkg/export"
2627
"go.thethings.network/lorawan-stack-migrate/pkg/source"
27-
"go.thethings.network/lorawan-stack/v3/pkg/log"
28-
"go.thethings.network/lorawan-stack/v3/pkg/rpcmiddleware/rpclog"
2928
)
3029

3130
var (
32-
logger *log.Logger
3331
ctx = context.Background()
3432
exportCfg = export.Config{}
3533
rootCfg = &source.RootConfig
@@ -39,26 +37,8 @@ var (
3937

4038
SilenceUsage: true,
4139
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
42-
logLevel := log.InfoLevel
43-
if verbose, _ := cmd.Flags().GetBool("verbose"); verbose {
44-
logLevel = log.DebugLevel
45-
}
46-
logHandler, err := log.NewZap("console")
47-
if err != nil {
48-
return err
49-
}
50-
logger = log.NewLogger(
51-
logHandler,
52-
log.WithLevel(logLevel),
53-
)
54-
rpclog.ReplaceGrpcLogger(logger)
55-
ctx = log.NewContext(ctx, logger)
56-
5740
exportCfg.DevIDPrefix, _ = cmd.Flags().GetString("dev-id-prefix")
58-
exportCfg.EUIForID, _ = cmd.Flags().GetBool("set-eui-as-id")
59-
ctx = export.NewContext(ctx, exportCfg)
60-
61-
cmd.SetContext(ctx)
41+
cmd.SetContext(export.NewContext(ctx, exportCfg))
6242
return nil
6343
},
6444
}
@@ -86,6 +66,11 @@ func init() {
8666
"frequency-plans-url",
8767
"https://raw.githubusercontent.com/TheThingsNetwork/lorawan-frequency-plans/master",
8868
"URL for fetching frequency plans")
69+
rootCmd.PersistentFlags().String(
70+
"dev-id-prefix",
71+
"",
72+
"(optional) value to be prefixed to the resulting device IDs",
73+
)
8974

9075
rootCmd.AddGroup(&cobra.Group{
9176
ID: "sources",
@@ -95,4 +80,5 @@ func init() {
9580
rootCmd.AddCommand(ttnv2.TTNv2Cmd)
9681
rootCmd.AddCommand(tts.TTSCmd)
9782
rootCmd.AddCommand(chirpstack.ChirpStackCmd)
83+
rootCmd.AddCommand(firefly.FireflyCmd)
9884
}

pkg/commands/export.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,15 @@ package commands
1616

1717
import (
1818
"io"
19-
"os"
2019

2120
"github.com/spf13/cobra"
2221
"go.thethings.network/lorawan-stack-migrate/pkg/export"
22+
"go.thethings.network/lorawan-stack-migrate/pkg/iterator"
2323
"go.thethings.network/lorawan-stack-migrate/pkg/source"
2424
"go.thethings.network/lorawan-stack/v3/pkg/log"
2525
)
2626

2727
func Export(cmd *cobra.Command, args []string, f func(s source.Source, item string) error) error {
28-
var iter Iterator
29-
switch len(args) {
30-
case 0:
31-
iter = NewReaderIterator(os.Stdin, '\n')
32-
default:
33-
iter = NewListIterator(args)
34-
}
35-
3628
s, err := source.NewSource(cmd.Context())
3729
if err != nil {
3830
return err
@@ -43,6 +35,14 @@ func Export(cmd *cobra.Command, args []string, f func(s source.Source, item stri
4335
}
4436
}()
4537

38+
var iter iterator.Iterator
39+
switch len(args) {
40+
case 0:
41+
iter = s.Iterator(cmd.Name() == "application")
42+
default:
43+
iter = iterator.NewListIterator(args)
44+
}
45+
4646
for {
4747
item, err := iter.Next()
4848
switch err {

pkg/commands/source.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,14 +127,14 @@ func ExecuteParentPersistentPreRun(cmd *cobra.Command, args []string) error {
127127
return nil
128128
}
129129
p := cmd.Parent()
130-
131130
if f := p.PersistentPreRunE; f != nil {
132131
if err := f(p, args); err != nil {
133132
return err
134133
}
135134
} else if f := p.PersistentPreRun; f != nil {
136135
f(p, args)
137136
}
137+
cmd.SetContext(p.Context())
138138
return nil
139139
}
140140

pkg/export/errors.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,5 @@ var (
2222
errInvalidFields = errors.DefineInvalidArgument("invalid_fields", "invalid fields for device `{device_id}`")
2323
errDevIDExceedsMaxLength = errors.Define("dev_id_exceeds_max_length", "device ID `{id}` exceeds max length")
2424
errAppIDExceedsMaxLength = errors.Define("app_id_exceeds_max_length", "application ID `{id}` exceeds max length")
25+
errNoExportedIDorEUI = errors.Define("no_exported_id_or_eui", "device `{device_id}` has no exported ID or EUI")
2526
)

pkg/export/export.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@
1515
package export
1616

1717
import (
18+
"encoding/hex"
1819
"fmt"
1920
"os"
2021
"strings"
2122

23+
"go.thethings.network/lorawan-stack-migrate/pkg/source"
2224
"go.thethings.network/lorawan-stack/v3/pkg/jsonpb"
2325
"go.thethings.network/lorawan-stack/v3/pkg/ttnpb"
24-
25-
"go.thethings.network/lorawan-stack-migrate/pkg/source"
2626
)
2727

2828
const (
@@ -36,7 +36,6 @@ func toJSON(dev *ttnpb.EndDevice) ([]byte, error) {
3636
}
3737

3838
type Config struct {
39-
EUIForID bool
4039
DevIDPrefix string
4140
}
4241

@@ -46,9 +45,13 @@ func (cfg Config) ExportDev(s source.Source, devID string) error {
4645
return errExport.WithAttributes("device_id", devID).WithCause(err)
4746
}
4847
oldID := dev.Ids.DeviceId
48+
eui := dev.Ids.DevEui
4949

50-
if eui := dev.Ids.DevEui; cfg.EUIForID && eui != nil {
51-
dev.Ids.DeviceId = strings.ToLower(string(eui))
50+
if oldID == "" {
51+
if eui == nil {
52+
return errNoExportedIDorEUI.WithAttributes("device_id", devID)
53+
}
54+
dev.Ids.DeviceId = strings.ToLower(hex.EncodeToString(eui))
5255
}
5356
if cfg.DevIDPrefix != "" {
5457
dev.Ids.DeviceId = fmt.Sprintf("%s-%s", cfg.DevIDPrefix, dev.Ids.DeviceId)
@@ -59,7 +62,7 @@ func (cfg Config) ExportDev(s source.Source, devID string) error {
5962
return errDevIDExceedsMaxLength.WithAttributes("id", id)
6063
}
6164

62-
if dev.Ids.DeviceId != oldID {
65+
if dev.Ids.DeviceId != oldID && oldID != "" {
6366
if dev.Attributes == nil {
6467
dev.Attributes = make(map[string]string)
6568
}
Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
package commands
15+
package iterator
1616

1717
import (
1818
"bufio"
@@ -61,3 +61,17 @@ func (r *readerIterator) Next() (string, error) {
6161
}
6262
return strings.TrimSpace(s), err
6363
}
64+
65+
// noopIterator is a no-op iterator.
66+
type noopIterator struct {
67+
}
68+
69+
// NewNoopIterator returns a new no-op iterator.
70+
func NewNoopIterator() Iterator {
71+
return &noopIterator{}
72+
}
73+
74+
// Next implements Iterator
75+
func (n *noopIterator) Next() (string, error) {
76+
return "", io.EOF
77+
}
Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,18 @@
1-
package commands_test
1+
// Copyright © 2023 The Things Network Foundation, The Things Industries B.V.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package iterator_test
216

317
import (
418
"bytes"
@@ -8,11 +22,11 @@ import (
822

923
"github.com/smartystreets/assertions"
1024
"github.com/smartystreets/assertions/should"
11-
"go.thethings.network/lorawan-stack-migrate/pkg/commands"
25+
"go.thethings.network/lorawan-stack-migrate/pkg/iterator"
1226
)
1327

1428
func TestListIterator(t *testing.T) {
15-
it := commands.NewListIterator([]string{"one", "two", "three"})
29+
it := iterator.NewListIterator([]string{"one", "two", "three"})
1630
a := assertions.New(t)
1731

1832
s, err := it.Next()
@@ -36,7 +50,7 @@ func TestListIterator(t *testing.T) {
3650
func TestReaderIterator(t *testing.T) {
3751
for _, sep := range []string{"\n", "\r\n"} {
3852
buf := []byte(strings.Join([]string{"one", "two", "three"}, sep))
39-
it := commands.NewReaderIterator(bytes.NewBuffer(buf), '\n')
53+
it := iterator.NewReaderIterator(bytes.NewBuffer(buf), '\n')
4054
a := assertions.New(t)
4155

4256
s, err := it.Next()

0 commit comments

Comments
 (0)