diff --git a/cli/device/device.go b/cli/device/device.go index 86e313a5..103c4c35 100644 --- a/cli/device/device.go +++ b/cli/device/device.go @@ -30,6 +30,7 @@ func NewCommand() *cobra.Command { } deviceCommand.AddCommand(initCreateCommand()) + deviceCommand.AddCommand(initMassCreateCommand()) deviceCommand.AddCommand(initListCommand()) deviceCommand.AddCommand(initDeleteCommand()) deviceCommand.AddCommand(tag.InitCreateTagsCommand()) diff --git a/cli/device/masscreate.go b/cli/device/masscreate.go new file mode 100644 index 00000000..02929446 --- /dev/null +++ b/cli/device/masscreate.go @@ -0,0 +1,125 @@ +// 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 . + +package device + +import ( + "context" + "fmt" + "os" + + "github.com/arduino/arduino-cli/cli/errorcodes" + "github.com/arduino/arduino-cli/cli/feedback" + "github.com/arduino/arduino-cloud-cli/command/device" + "github.com/arduino/arduino-cloud-cli/config" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "go.bug.st/cleanup" +) + +type massCreateFlags struct { + name string + fqbn string +} + +func initMassCreateCommand() *cobra.Command { + flags := &massCreateFlags{} + createCommand := &cobra.Command{ + Use: "mass-create", + Short: "Mass create a set of devices provisioning the onboard secure element with a valid certificate", + Long: "Mass create a set of devices for Arduino IoT Cloud provisioning the onboard secure element with a valid certificate", + Run: func(cmd *cobra.Command, args []string) { + if err := runMassCreateCommand(flags); err != nil { + feedback.Errorf("Error during device create: %v", err) + os.Exit(errorcodes.ErrGeneric) + } + }, + } + createCommand.Flags().StringVarP(&flags.name, "name", "n", "", "Base device name") + createCommand.Flags().StringVarP(&flags.fqbn, "fqbn", "b", "", "Device fqbn") + createCommand.MarkFlagRequired("name") + return createCommand +} + + +func runMassCreateCommand(flags *massCreateFlags) error { + logrus.Infof("Mass provisioning devices. Base name: %s", flags.name) + + cred, err := config.RetrieveCredentials() + if err != nil { + return fmt.Errorf("retrieving credentials: %w", err) + } + + ctx, cancel := cleanup.InterruptableContext(context.Background()) + defer cancel() + + boards, err := device.ListAllConnectedBoardsWithCrypto(&flags.fqbn) + if err != nil { + return err + } + if len(boards) == 0 { + return fmt.Errorf("no boards of type %s detected", flags.fqbn) + } + + var results []*device.DeviceInfo + for idx, board := range boards { + if len(board.Address) == 0{ + continue + } + logrus.Infof("Provisioning board on port: %s", board.Address) + bname := fmt.Sprintf("%s-%d", flags.name, idx) + params := &device.CreateParams{ + Name: bname, + Port: &board.Address, + FQBN: &board.Fqbn, + } + + dev, err := device.Create(ctx, params, cred) + if err != nil { + return err + } + + results = append(results, dev) + } + + feedback.PrintResult(massCreateResult{results}) + + return nil +} + +type massCreateResult struct { + devices []*device.DeviceInfo +} + +func (r massCreateResult) Data() interface{} { + return r.devices +} + +func (r massCreateResult) String() string { + var result string + for _, device := range r.devices { + result += fmt.Sprintf( + "name: %s\nid: %s\nboard: %s\nserial_number: %s\nfqbn: %s\n-------------\n", + device.Name, + device.ID, + device.Board, + device.Serial, + device.FQBN, + ) + } + return result +} diff --git a/command/device/board.go b/command/device/board.go index 568285c2..f2e3fe42 100644 --- a/command/device/board.go +++ b/command/device/board.go @@ -43,19 +43,19 @@ var ( ) // board contains details of a physical arduino board. -type board struct { - fqbn string - serial string - dType string - address string - protocol string +type Board struct { + Fqbn string + Serial string + DType string + Address string + Protocol string } // isCrypto checks if the board is a valid arduino board with a // supported crypto-chip. -func (b *board) isCrypto() bool { +func (b *Board) isCrypto() bool { for _, f := range cryptoFQBN { - if b.fqbn == f { + if b.Fqbn == f { return true } } @@ -63,9 +63,9 @@ func (b *board) isCrypto() bool { } // isLora checks if the board is a valid LoRa arduino board. -func (b *board) isLora() bool { +func (b *Board) isLora() bool { for _, f := range loraFQBN { - if b.fqbn == f { + if b.Fqbn == f { return true } } @@ -74,19 +74,19 @@ func (b *board) isLora() bool { // boardFromPorts returns a board that matches all the criteria // passed in. If no criteria are passed, it returns the first board found. -func boardFromPorts(ports []*rpc.DetectedPort, params *CreateParams) *board { +func boardFromPorts(ports []*rpc.DetectedPort, params *CreateParams) *Board { for _, port := range ports { if portFilter(port, params) { continue } boardFound := boardFilter(port.MatchingBoards, params) if boardFound != nil { - b := &board{ - fqbn: boardFound.Fqbn, - serial: port.Port.Properties["serialNumber"], - dType: strings.Split(boardFound.Fqbn, ":")[2], - address: port.Port.Address, - protocol: port.Port.Protocol, + b := &Board{ + Fqbn: boardFound.Fqbn, + Serial: port.Port.Properties["serialNumber"], + DType: strings.Split(boardFound.Fqbn, ":")[2], + Address: port.Port.Address, + Protocol: port.Port.Protocol, } return b } diff --git a/command/device/board_test.go b/command/device/board_test.go index e83baa4d..45b0d93d 100644 --- a/command/device/board_test.go +++ b/command/device/board_test.go @@ -61,21 +61,21 @@ func TestBoardFromPorts(t *testing.T) { name string filter *CreateParams ports []*rpc.DetectedPort - want *board + want *Board }{ { name: "port-filter", filter: &CreateParams{FQBN: nil, Port: stringPointer("ACM1")}, ports: portsTwoBoards, - want: &board{fqbn: "arduino:avr:uno", address: "ACM1"}, + want: &Board{Fqbn: "arduino:avr:uno", Address: "ACM1"}, }, { name: "fqbn-filter", filter: &CreateParams{FQBN: stringPointer("arduino:avr:uno"), Port: nil}, ports: portsTwoBoards, - want: &board{fqbn: "arduino:avr:uno", address: "ACM1"}, + want: &Board{Fqbn: "arduino:avr:uno", Address: "ACM1"}, }, { @@ -90,7 +90,7 @@ func TestBoardFromPorts(t *testing.T) { filter: &CreateParams{FQBN: nil, Port: nil}, ports: portsTwoBoards, // first board found is selected - want: &board{fqbn: "arduino:samd:nano_33_iot", address: "ACM0"}, + want: &Board{Fqbn: "arduino:samd:nano_33_iot", Address: "ACM0"}, }, { @@ -104,7 +104,7 @@ func TestBoardFromPorts(t *testing.T) { name: "both-filter-found", filter: &CreateParams{FQBN: stringPointer("arduino:avr:uno"), Port: stringPointer("ACM1")}, ports: portsTwoBoards, - want: &board{fqbn: "arduino:avr:uno", address: "ACM1"}, + want: &Board{Fqbn: "arduino:avr:uno", Address: "ACM1"}, }, { @@ -123,14 +123,14 @@ func TestBoardFromPorts(t *testing.T) { return } else if got != nil && tt.want == nil { - t.Errorf("Expected nil board, received not nil board with port %s and fqbn %s", got.address, got.fqbn) + t.Errorf("Expected nil board, received not nil board with port %s and fqbn %s", got.Address, got.Fqbn) } else if got == nil && tt.want != nil { - t.Errorf("Expected not nil board with port %s and fqbn %s, received a nil board", tt.want.address, tt.want.fqbn) + t.Errorf("Expected not nil board with port %s and fqbn %s, received a nil board", tt.want.Address, tt.want.Fqbn) - } else if got.address != tt.want.address || got.fqbn != tt.want.fqbn { + } else if got.Address != tt.want.Address || got.Fqbn != tt.want.Fqbn { t.Errorf("Expected board with port %s and fqbn %s, received board with port %s and fqbn %s", - tt.want.address, tt.want.fqbn, got.address, got.fqbn) + tt.want.Address, tt.want.Fqbn, got.Address, got.Fqbn) } }) } diff --git a/command/device/create.go b/command/device/create.go index 33d5cd30..296e24b0 100644 --- a/command/device/create.go +++ b/command/device/create.go @@ -60,8 +60,8 @@ func Create(ctx context.Context, params *CreateParams, cred *config.Credentials) "board with fqbn %s found at port %s is not a device with a supported crypto-chip.\n"+ "Try the 'create-lora' command instead if it's a LoRa device"+ " or 'create-generic' otherwise", - board.fqbn, - board.address, + board.Fqbn, + board.Address, ) } @@ -71,7 +71,7 @@ func Create(ctx context.Context, params *CreateParams, cred *config.Credentials) } logrus.Info("Creating a new device on the cloud") - dev, err := iotClient.DeviceCreate(ctx, board.fqbn, params.Name, board.serial, board.dType, params.ConnectionType) + dev, err := iotClient.DeviceCreate(ctx, board.Fqbn, params.Name, board.Serial, board.DType, params.ConnectionType) if err != nil { return nil, err } diff --git a/command/device/createlora.go b/command/device/createlora.go index 821d5018..e1ec49dc 100644 --- a/command/device/createlora.go +++ b/command/device/createlora.go @@ -84,12 +84,12 @@ func CreateLora(ctx context.Context, params *CreateLoraParams, cred *config.Cred "board with fqbn %s found at port %s is not a LoRa device."+ " Try the 'create' command instead if it's a device with a supported crypto-chip"+ " or 'create-generic' otherwise", - board.fqbn, - board.address, + board.Fqbn, + board.Address, ) } - bin, err := downloadProvisioningFile(ctx, board.fqbn) + bin, err := downloadProvisioningFile(ctx, board.Fqbn) if err != nil { return nil, err } @@ -97,13 +97,13 @@ func CreateLora(ctx context.Context, params *CreateLoraParams, cred *config.Cred logrus.Infof("%s", "Uploading deveui sketch on the LoRa board") errMsg := "Error while uploading the LoRa provisioning binary" err = retry(ctx, deveuiUploadAttempts, deveuiUploadWait*time.Millisecond, errMsg, func() error { - return comm.UploadBin(ctx, board.fqbn, bin, board.address, board.protocol) + return comm.UploadBin(ctx, board.Fqbn, bin, board.Address, board.Protocol) }) if err != nil { return nil, fmt.Errorf("failed to upload LoRa provisioning binary: %w", err) } - eui, err := extractEUI(ctx, board.address) + eui, err := extractEUI(ctx, board.Address) if err != nil { return nil, err } @@ -114,7 +114,7 @@ func CreateLora(ctx context.Context, params *CreateLoraParams, cred *config.Cred } logrus.Info("Creating a new device on the cloud") - dev, err := iotClient.DeviceLoraCreate(ctx, params.Name, board.serial, board.dType, eui, params.FrequencyPlan) + dev, err := iotClient.DeviceLoraCreate(ctx, params.Name, board.Serial, board.DType, eui, params.FrequencyPlan) if err != nil { return nil, err } diff --git a/command/device/masscreate.go b/command/device/masscreate.go new file mode 100644 index 00000000..1118cfbd --- /dev/null +++ b/command/device/masscreate.go @@ -0,0 +1,87 @@ +package device + +import ( + "context" + "errors" + "strings" + + rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1" + "github.com/arduino/arduino-cloud-cli/arduino/cli" + "go.bug.st/cleanup" +) + +func ListAllConnectedBoardsWithCrypto(fqbn *string) ([]*Board, error) { + comm, err := ListAllConnectedBoards() + if err != nil { + return nil, err + } + var withcrypto []*Board + for _, b := range comm { + if len(*fqbn) > 0 && b.Fqbn != *fqbn { + // Skipp not matching board + continue + } + if b.isCrypto() { + withcrypto = append(withcrypto, b) + } + } + return withcrypto, nil +} + +func ListAllConnectedBoards() ([]*Board, error) { + comm, err := cli.NewCommander() + if err != nil { + return nil, err + } + + ctx, cancel := cleanup.InterruptableContext(context.Background()) + defer cancel() + + ports, err := comm.BoardList(ctx) + if err != nil { + return nil, err + } + + board := boardsFromPorts(ports, nil) + if board == nil { + err = errors.New("no board found") + return nil, err + } + + return board, nil +} + +// boardsFromPorts returns boards that matches all the criteria +func boardsFromPorts(ports []*rpc.DetectedPort, fqbn *string) []*Board { + var boards []*Board + for _, port := range ports { + boardsFound := boardsFilter(port.MatchingBoards, fqbn) + if len(boardsFound) > 0 { + for _, boardFound := range boardsFound { + b := &Board{ + Fqbn: boardFound.Fqbn, + Serial: port.Port.Properties["serialNumber"], + DType: strings.Split(boardFound.Fqbn, ":")[2], + Address: port.Port.Address, + Protocol: port.Port.Protocol, + } + b.isCrypto() + boards = append(boards, b) + } + } + } + return boards +} + +func boardsFilter(boards []*rpc.BoardListItem, fqbn *string) (board []*rpc.BoardListItem) { + if fqbn == nil { + return boards + } + var filtered []*rpc.BoardListItem + for _, b := range boards { + if b.Fqbn == *fqbn { + filtered = append(filtered, b) + } + } + return filtered +} diff --git a/command/device/provision.go b/command/device/provision.go index 58781a33..7e918058 100644 --- a/command/device/provision.go +++ b/command/device/provision.go @@ -74,13 +74,13 @@ type provision struct { arduino.Commander cert certificateCreator ser *serial.Serial - board *board + board *Board id string } // run provisioning procedure for boards with crypto-chip. func (p provision) run(ctx context.Context) error { - bin, err := downloadProvisioningFile(ctx, p.board.fqbn) + bin, err := downloadProvisioningFile(ctx, p.board.Fqbn) if err != nil { return err } @@ -93,7 +93,7 @@ func (p provision) run(ctx context.Context) error { errMsg := "Error while uploading the provisioning sketch" err = retry(ctx, 5, time.Millisecond*1000, errMsg, func() error { //serialutils.Reset(dev.port, true, nil) - return p.UploadBin(ctx, p.board.fqbn, bin, p.board.address, p.board.protocol) + return p.UploadBin(ctx, p.board.Fqbn, bin, p.board.Address, p.board.Protocol) }) if err != nil { return err @@ -107,7 +107,7 @@ func (p provision) run(ctx context.Context) error { p.ser = serial.NewSerial() errMsg = "Error while connecting to the board" err = retry(ctx, 5, time.Millisecond*1000, errMsg, func() error { - return p.ser.Connect(p.board.address) + return p.ser.Connect(p.board.Address) }) if err != nil { return err