Skip to content

Commit

Permalink
[WIRE-422] Custom templates support (#157)
Browse files Browse the repository at this point in the history
  • Loading branch information
rjtokenring authored Jul 29, 2024
1 parent a17c934 commit 5ca90df
Show file tree
Hide file tree
Showing 26 changed files with 1,769 additions and 18 deletions.
2 changes: 1 addition & 1 deletion .licenses/go/github.com/arduino/iot-client-go/v2.dep.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: github.com/arduino/iot-client-go/v2
version: v2.0.2
version: v2.0.3
type: go
summary:
homepage: https://pkg.go.dev/github.com/arduino/iot-client-go/v2
Expand Down
49 changes: 47 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ Here are the FQBNs of the Arduino boards that can be provisioned with this comma
* `arduino:samd:mkrnb1500`
* `arduino:mbed_opta:opta`
* `arduino:mbed_giga:giga`
* `arduino:esp32:nano_nora`
* `arduino:renesas_uno:unor4wifi`

If the device supports more than one connectivity type (Eg: WiFi and Ethernet) the --connection flag can be used to set the desired connectivity

Expand Down Expand Up @@ -327,10 +329,17 @@ Note that the binary file (`.bin`) should be compiled using an arduino core that
arduino-cloud-cli ota upload --device-id <deviceID> --file <sketch-file.ino.bin>
```

The default OTA upload should complete in 10 minutes. Use `--deferred` flag to extend this time to one week (see an example sketch [here](https://github.com/arduino-libraries/ArduinoIoTCloud/blob/ab0af75a5666f875929029ac6df59e04789269c5/examples/ArduinoIoTCloud-DeferredOTA/ArduinoIoTCloud-DeferredOTA.ino)):
This schedule a new OTA. Its ID is printed as output.
It is possible to check status for scheduled/executed OTAs using status command.

```bash
arduino-cloud-cli ota upload --device-id <deviceID> --file <sketch-file.ino.bin> --deferred
arduino-cloud-cli ota status --ota-id <otaID>
```

or by device

```bash
arduino-cloud-cli ota status --device-id <deviceID>
```

### Mass upload
Expand Down Expand Up @@ -383,3 +392,39 @@ Create a dashboard: dashboards can be created only starting from a template. Sup
```bash
arduino-cloud-cli dashboard create --name <dashboardName> --template <template.(json|yaml)> --override <thing-0>=<actualThingID>,<thing-1>=<otherActualThingID>
```

## Custom templates

### List custom templates

Use following command to list available custom templates

```bash
arduino-cloud-cli template list
```

### Export custom template

Given list command, it is possible to get custom template ID. Given its ID, use following command to export it as '.tino' archive:

```bash
arduino-cloud-cli template export -t <templateID>
```

it is possible to specify output directory with '-d' flag

### Import custom template

To import a custom template, use command:

```bash
arduino-cloud-cli template import -f <template .tino archive>
```

### Template apply

It is possible to apply a given template to a device. Apply will generate required resources. Configure device connectivity using '-n' option (see --help for further details).

```bash
arduino-cloud-cli template apply -d <deviceID> -t <templateID> -p "<name prefix>" -n SECRET_SSID=<ssid>,SECRET_OPTIONAL_PASS=<pwd>
```
2 changes: 2 additions & 0 deletions cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/arduino/arduino-cloud-cli/cli/dashboard"
"github.com/arduino/arduino-cloud-cli/cli/device"
"github.com/arduino/arduino-cloud-cli/cli/ota"
"github.com/arduino/arduino-cloud-cli/cli/template"
"github.com/arduino/arduino-cloud-cli/cli/thing"
"github.com/arduino/arduino-cloud-cli/cli/version"
"github.com/sirupsen/logrus"
Expand Down Expand Up @@ -65,6 +66,7 @@ func Execute() {
cli.AddCommand(thing.NewCommand())
cli.AddCommand(dashboard.NewCommand())
cli.AddCommand(ota.NewCommand())
cli.AddCommand(template.NewCommand())

if err := cli.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
Expand Down
1 change: 1 addition & 0 deletions cli/device/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func NewCommand() *cobra.Command {

deviceCommand.AddCommand(initCreateCommand())
deviceCommand.AddCommand(initListCommand())
deviceCommand.AddCommand(initShowCommand())
deviceCommand.AddCommand(initDeleteCommand())
deviceCommand.AddCommand(tag.InitCreateTagsCommand())
deviceCommand.AddCommand(tag.InitDeleteTagsCommand())
Expand Down
102 changes: 102 additions & 0 deletions cli/device/show.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// This file is part of arduino-cloud-cli.
//
// Copyright (C) 2024 ARDUINO SA (http://www.arduino.cc/)
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

package device

import (
"context"
"fmt"
"os"
"strings"

"github.com/arduino/arduino-cli/cli/errorcodes"
"github.com/arduino/arduino-cli/cli/feedback"
"github.com/arduino/arduino-cli/table"
"github.com/arduino/arduino-cloud-cli/command/device"
"github.com/arduino/arduino-cloud-cli/config"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

type showFlags struct {
deviceId string
}

func initShowCommand() *cobra.Command {
flags := &showFlags{}
showCommand := &cobra.Command{
Use: "show",
Short: "Show device properties",
Long: "Show device properties on Arduino IoT Cloud",
Run: func(cmd *cobra.Command, args []string) {
if err := runShowCommand(flags); err != nil {
feedback.Errorf("Error during device show: %v", err)
os.Exit(errorcodes.ErrGeneric)
}
},
}
showCommand.Flags().StringVarP(&flags.deviceId, "device-id", "d", "", "device ID")

showCommand.MarkFlagRequired("device-id")

return showCommand
}

func runShowCommand(flags *showFlags) error {
logrus.Info("Show device")

cred, err := config.RetrieveCredentials()
if err != nil {
return fmt.Errorf("retrieving credentials: %w", err)
}

dev, _, err := device.Show(context.TODO(), flags.deviceId, cred)
if err != nil {
return err
}

feedback.PrintResult(showResult{dev})
return nil
}

type showResult struct {
device *device.DeviceInfo
}

func (r showResult) Data() interface{} {
return r.device
}

func (r showResult) String() string {
if r.device == nil {
return "No device found."
}
t := table.New()
t.SetHeader("Name", "ID", "Board", "FQBN", "SerialNumber", "Status", "Connection type", "Thing", "Tags")
t.AddRow(
r.device.Name,
r.device.ID,
r.device.Board,
r.device.FQBN,
r.device.Serial,
dereferenceString(r.device.Status),
dereferenceString(r.device.ConnectionType),
dereferenceString(r.device.ThingID),
strings.Join(r.device.Tags, ","),
)
return t.Render()
}
96 changes: 96 additions & 0 deletions cli/template/apply.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// This file is part of arduino-cloud-cli.
//
// Copyright (C) 2024 ARDUINO SA (http://www.arduino.cc/)
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

package template

import (
"fmt"
"os"
"strings"

"github.com/arduino/arduino-cli/cli/errorcodes"
"github.com/arduino/arduino-cli/cli/feedback"
"github.com/arduino/arduino-cloud-cli/command/template"
"github.com/arduino/arduino-cloud-cli/config"
"github.com/spf13/cobra"
)

type applyFlags struct {
templateId string
templatePrefix string
deviceId string
netCredentials string
applyOta bool
}

func initTemplateApplyCommand() *cobra.Command {
flags := &applyFlags{}
applyCommand := &cobra.Command{
Use: "apply",
Short: "Apply custom template",
Long: "Given a template, apply it and create all the resources defined in it",
Run: func(cmd *cobra.Command, args []string) {
if err := runTemplateApplyCommand(flags); err != nil {
feedback.Errorf("Error during template apply: %v", err)
os.Exit(errorcodes.ErrGeneric)
}
},
}

applyCommand.Flags().StringVarP(&flags.templateId, "template-id", "t", "", "Template ID")
applyCommand.Flags().StringVarP(&flags.templatePrefix, "prefix", "p", "", "Prefix to apply to the name of created resources")
applyCommand.Flags().StringVarP(&flags.deviceId, "device-id", "d", "", "Device ID")
applyCommand.Flags().StringVarP(&flags.netCredentials, "network-credentials", "n", "", "Comma separated network credentials used to configure device with format <key>=<value>. Supported values: SECRET_SSID | SECRET_OPTIONAL_PASS | SECRET_DEVICE_KEY")

applyCommand.MarkFlagRequired("template-id")
applyCommand.MarkFlagRequired("prefix")
applyCommand.MarkFlagRequired("device-id")

flags.applyOta = false

return applyCommand
}

func runTemplateApplyCommand(flags *applyFlags) error {
cred, err := config.RetrieveCredentials()
if err != nil {
return fmt.Errorf("retrieving credentials: %w", err)
}

deviceNetCredentials, err := parseCredentials(flags.netCredentials)
if err != nil {
return fmt.Errorf("parsing network credentials: %w", err)
}

return template.ApplyCustomTemplates(cred, flags.templateId, flags.deviceId, flags.templatePrefix, deviceNetCredentials, flags.applyOta)
}

func parseCredentials(credentials string) (map[string]string, error) {
credentialsMap := make(map[string]string)
if credentials == "" {
return credentialsMap, nil
}
credentialsArray := strings.Split(credentials, ",")
for _, credential := range credentialsArray {
credentialArray := strings.Split(credential, "=")
if len(credentialArray) != 2 {
return nil, fmt.Errorf("invalid network credential: %s", credential)
}
credentialsMap[credentialArray[0]] = credentialArray[1]
}
return credentialsMap, nil
}
64 changes: 64 additions & 0 deletions cli/template/export.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// This file is part of arduino-cloud-cli.
//
// Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/)
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

package template

import (
"fmt"
"os"

"github.com/arduino/arduino-cli/cli/errorcodes"
"github.com/arduino/arduino-cli/cli/feedback"
"github.com/arduino/arduino-cloud-cli/command/template"
"github.com/arduino/arduino-cloud-cli/config"
"github.com/spf13/cobra"
)

type exportFlags struct {
templateId string
path string
}

func initTemplateExportCommand() *cobra.Command {
flags := &exportFlags{}
uploadCommand := &cobra.Command{
Use: "export",
Short: "Export template",
Long: "Export template to a file",
Run: func(cmd *cobra.Command, args []string) {
if err := runTemplateExportCommand(flags); err != nil {
feedback.Errorf("Error during template export: %v", err)
os.Exit(errorcodes.ErrGeneric)
}
},
}

uploadCommand.Flags().StringVarP(&flags.templateId, "template-id", "t", "", "Template id")
uploadCommand.Flags().StringVarP(&flags.path, "directory", "d", "", "Output directory")

uploadCommand.MarkFlagRequired("template-id")

return uploadCommand
}

func runTemplateExportCommand(flags *exportFlags) error {
cred, err := config.RetrieveCredentials()
if err != nil {
return fmt.Errorf("retrieving credentials: %w", err)
}
return template.ExportCustomTemplate(cred, flags.templateId, flags.path)
}
Loading

0 comments on commit 5ca90df

Please sign in to comment.