diff --git a/README.md b/README.md index fe9748e5..a412f297 100644 --- a/README.md +++ b/README.md @@ -40,3 +40,15 @@ Once a device has been created thorugh the provisioning procedure, it can be del Devices currently present on Arduino IoT Cloud can be retrieved by using this command: `$ iot-cloud-cli device list` + +## Thing commands + +Things can be created starting from a template or by cloning another thing. Additionally, a thing name should be specified. + +Create a thing from a template: + +`$ iot-cloud-cli thing create --name --template ` + +Create a thing by cloning another thing: + +`$ iot-cloud-cli thing create --name --clone-id ` diff --git a/cli/root.go b/cli/root.go index a999a1b8..ccc75821 100644 --- a/cli/root.go +++ b/cli/root.go @@ -6,6 +6,7 @@ import ( "github.com/arduino/iot-cloud-cli/cli/config" "github.com/arduino/iot-cloud-cli/cli/device" + "github.com/arduino/iot-cloud-cli/cli/thing" "github.com/spf13/cobra" ) @@ -13,6 +14,7 @@ func Execute() { rootCmd := &cobra.Command{} rootCmd.AddCommand(config.NewCommand()) rootCmd.AddCommand(device.NewCommand()) + rootCmd.AddCommand(thing.NewCommand()) if err := rootCmd.Execute(); err != nil { fmt.Fprintln(os.Stderr, err) diff --git a/cli/thing/create.go b/cli/thing/create.go new file mode 100644 index 00000000..1f48c44f --- /dev/null +++ b/cli/thing/create.go @@ -0,0 +1,49 @@ +package thing + +import ( + "fmt" + + "github.com/arduino/iot-cloud-cli/command/thing" + "github.com/spf13/cobra" +) + +var createFlags struct { + name string + deviceID string + template string + cloneID string +} + +func initCreateCommand() *cobra.Command { + createCommand := &cobra.Command{ + Use: "create", + Short: "Create a thing", + Long: "Create a thing for Arduino IoT Cloud", + RunE: runCreateCommand, + } + createCommand.Flags().StringVarP(&createFlags.name, "name", "n", "", "Thing name") + createCommand.Flags().StringVarP(&createFlags.deviceID, "device-id", "d", "", "ID of Device to bind to the new thing") + createCommand.Flags().StringVarP(&createFlags.cloneID, "clone-id", "c", "", "ID of Thing to be cloned") + createCommand.Flags().StringVarP(&createFlags.template, "template", "t", "", "File containing a thing template") + createCommand.MarkFlagRequired("name") + return createCommand +} + +func runCreateCommand(cmd *cobra.Command, args []string) error { + fmt.Printf("Creating thing with name %s\n", createFlags.name) + + params := &thing.CreateParams{ + Name: createFlags.name, + DeviceID: createFlags.deviceID, + Template: createFlags.template, + CloneID: createFlags.cloneID, + } + + thingID, err := thing.Create(params) + if err != nil { + return err + } + + fmt.Printf("IoT Cloud thing created with ID: %s\n", thingID) + return nil +} diff --git a/cli/thing/thing.go b/cli/thing/thing.go new file mode 100644 index 00000000..a1ccce9e --- /dev/null +++ b/cli/thing/thing.go @@ -0,0 +1,17 @@ +package thing + +import ( + "github.com/spf13/cobra" +) + +func NewCommand() *cobra.Command { + thingCommand := &cobra.Command{ + Use: "thing", + Short: "Thing commands.", + Long: "Thing commands.", + } + + thingCommand.AddCommand(initCreateCommand()) + + return thingCommand +} diff --git a/command/thing/create.go b/command/thing/create.go new file mode 100644 index 00000000..b4537154 --- /dev/null +++ b/command/thing/create.go @@ -0,0 +1,125 @@ +package thing + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + + "errors" + + iotclient "github.com/arduino/iot-client-go" + "github.com/arduino/iot-cloud-cli/internal/config" + "github.com/arduino/iot-cloud-cli/internal/iot" +) + +// CreateParams contains the parameters needed to create a new thing. +type CreateParams struct { + // Mandatory - contains the name of the thing + Name string + // Optional - contains the ID of the device to be bound to the thing + DeviceID string + // Mandatory if device is empty - contains the path of the template file + Template string + // Mandatory if template is empty- name of things to be cloned + CloneID string +} + +// Create allows to create a new thing +func Create(params *CreateParams) (string, error) { + if params.Template == "" && params.CloneID == "" { + return "", fmt.Errorf("%s", "provide either a thing(ID) to clone (--clone) or a thing template file (--template)\n") + } + + conf, err := config.Retrieve() + if err != nil { + return "", err + } + iotClient, err := iot.NewClient(conf.Client, conf.Secret) + if err != nil { + return "", err + } + + var thing *iotclient.Thing + + if params.CloneID != "" { + thing, err = clone(iotClient, params.CloneID) + if err != nil { + return "", err + } + + } else if params.Template != "" { + thing, err = loadTemplate(params.Template) + if err != nil { + return "", err + } + + } else { + return "", errors.New("provide either a thing(ID) to clone (--clone) or a thing template file (--template)") + } + + thing.Name = params.Name + force := true + if params.DeviceID != "" { + thing.DeviceId = params.DeviceID + } + thingID, err := iotClient.AddThing(thing, force) + if err != nil { + return "", err + } + + return thingID, nil +} + +func clone(client iot.Client, thingID string) (*iotclient.Thing, error) { + clone, err := client.GetThing(thingID) + if err != nil { + return nil, fmt.Errorf("%s: %w", "retrieving the thing to be cloned", err) + } + + thing := &iotclient.Thing{} + + // Copy device id + if clone.DeviceId != "" { + thing.DeviceId = clone.DeviceId + } + + // Copy properties + for _, p := range clone.Properties { + thing.Properties = append(thing.Properties, iotclient.Property{ + Name: p.Name, + MinValue: p.MinValue, + MaxValue: p.MaxValue, + Permission: p.Permission, + UpdateParameter: p.UpdateParameter, + UpdateStrategy: p.UpdateStrategy, + Type: p.Type, + VariableName: p.VariableName, + Persist: p.Persist, + Tag: p.Tag, + }) + } + + return thing, nil +} + +func loadTemplate(file string) (*iotclient.Thing, error) { + templateFile, err := os.Open(file) + if err != nil { + return nil, err + } + defer templateFile.Close() + + templateBytes, err := ioutil.ReadAll(templateFile) + if err != nil { + return nil, err + } + + thing := &iotclient.Thing{} + err = json.Unmarshal([]byte(templateBytes), thing) + if err != nil { + return nil, fmt.Errorf("%s: %w", "reading template file: template not valid: ", err) + } + + return thing, nil +} diff --git a/go.mod b/go.mod index 2c0205c1..87b09bd8 100644 --- a/go.mod +++ b/go.mod @@ -3,18 +3,21 @@ module github.com/arduino/iot-cloud-cli go 1.15 require ( + github.com/antihax/optional v1.0.0 // indirect github.com/arduino/arduino-cli v0.0.0-20210607095659-16f41352eac3 github.com/arduino/go-paths-helper v1.6.0 - github.com/arduino/iot-client-go v1.3.3 + github.com/arduino/iot-client-go v1.3.4-0.20210824101852-4a44149473c1 github.com/howeyc/crc16 v0.0.0-20171223171357-2b2a61e366a6 github.com/sirupsen/logrus v1.4.2 github.com/spf13/cobra v1.1.3 github.com/spf13/viper v1.7.0 github.com/stretchr/testify v1.6.1 go.bug.st/serial v1.3.0 - golang.org/x/net v0.0.0-20210505024714-0287a6fb4125 // indirect - golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914 + golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d // indirect + golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f golang.org/x/sys v0.0.0-20210503173754-0981d6026fa6 // indirect + google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20210504143626-3b2ad6ccc450 // indirect google.golang.org/grpc v1.39.0 + google.golang.org/protobuf v1.27.1 // indirect ) diff --git a/go.sum b/go.sum index 320ae417..89529be4 100644 --- a/go.sum +++ b/go.sum @@ -59,8 +59,8 @@ github.com/arduino/go-properties-orderedmap v1.3.0/go.mod h1:DKjD2VXY/NZmlingh4l github.com/arduino/go-timeutils v0.0.0-20171220113728-d1dd9e313b1b/go.mod h1:uwGy5PpN4lqW97FiLnbcx+xx8jly5YuPMJWfVwwjJiQ= github.com/arduino/go-win32-utils v0.0.0-20180330194947-ed041402e83b h1:3PjgYG5gVPA7cipp7vIR2lF96KkEJIFBJ+ANnuv6J20= github.com/arduino/go-win32-utils v0.0.0-20180330194947-ed041402e83b/go.mod h1:iIPnclBMYm1g32Q5kXoqng4jLhMStReIP7ZxaoUC2y8= -github.com/arduino/iot-client-go v1.3.3 h1:W+92osS+WcdVpePdPmj/BtupM+xV6DOJlI0HGpKrTX4= -github.com/arduino/iot-client-go v1.3.3/go.mod h1:gYvpMt7Qw+OSScTLyIlCnpbvy9y96ey/2zhB4w6FoK0= +github.com/arduino/iot-client-go v1.3.4-0.20210824101852-4a44149473c1 h1:tgVUBPbqkyd3KHTs+gweP5t9KAnkLbAsAMrHvu9jZSg= +github.com/arduino/iot-client-go v1.3.4-0.20210824101852-4a44149473c1/go.mod h1:gYvpMt7Qw+OSScTLyIlCnpbvy9y96ey/2zhB4w6FoK0= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= @@ -494,15 +494,15 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= -golang.org/x/net v0.0.0-20210505024714-0287a6fb4125 h1:Ugb8sMTWuWRC3+sz5WeN/4kejDx9BvIwnPUiJBjJE+8= -golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d h1:LO7XpTYMwTqxjLcGWPijK3vRXg1aWdlNOVOHRq45d7c= +golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914 h1:3B43BWw0xEBsLZ/NO1VALz6fppU3481pik+2Ksv45z8= -golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f h1:Qmd2pbz05z7z6lm0DrgQVVPuBm92jqujBKMHMOlOQEw= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -638,8 +638,9 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -701,8 +702,9 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20160105164936-4f90aeace3a2/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/iot/client.go b/internal/iot/client.go index a48a308f..c8ee311c 100644 --- a/internal/iot/client.go +++ b/internal/iot/client.go @@ -2,8 +2,10 @@ package iot import ( "context" + "encoding/json" "fmt" + "github.com/antihax/optional" iotclient "github.com/arduino/iot-client-go" ) @@ -13,6 +15,8 @@ type Client interface { DeleteDevice(id string) error ListDevices() ([]iotclient.ArduinoDevicev2, error) AddCertificate(id, csr string) (*iotclient.ArduinoCompressedv2, error) + AddThing(thing *iotclient.Thing, force bool) (string, error) + GetThing(id string) (*iotclient.ArduinoThing, error) } type client struct { @@ -89,6 +93,30 @@ func (cl *client) AddCertificate(id, csr string) (*iotclient.ArduinoCompressedv2 return &newCert.Compressed, nil } +// AddThing adds a new thing on Arduino IoT Cloud. +func (cl *client) AddThing(thing *iotclient.Thing, force bool) (string, error) { + opt := &iotclient.ThingsV2CreateOpts{Force: optional.NewBool(force)} + newThing, resp, err := cl.api.ThingsV2Api.ThingsV2Create(cl.ctx, *thing, opt) + if err != nil { + var respObj map[string]interface{} + json.NewDecoder(resp.Body).Decode(&respObj) + resp.Body.Close() + return "", fmt.Errorf("%s: %s: %v", "adding new thing", err, respObj) + } + return newThing.Id, nil +} + +// GetThing allows to retrieve a specific thing, given its id, +// from Arduino IoT Cloud. +func (cl *client) GetThing(id string) (*iotclient.ArduinoThing, error) { + thing, _, err := cl.api.ThingsV2Api.ThingsV2Show(cl.ctx, id, nil) + if err != nil { + err = fmt.Errorf("retrieving thing, %w", err) + return nil, err + } + return &thing, nil +} + func (cl *client) setup(client, secret string) error { // Get the access token in exchange of client_id and client_secret tok, err := token(client, secret)