diff --git a/docs/resources/iotda_device_async_command.md b/docs/resources/iotda_device_async_command.md new file mode 100644 index 0000000000..329541542b --- /dev/null +++ b/docs/resources/iotda_device_async_command.md @@ -0,0 +1,114 @@ +--- +subcategory: "IoT Device Access (IoTDA)" +layout: "huaweicloud" +page_title: "HuaweiCloud: huaweicloud_iotda_device_async_command" +description: |- + Manages a device asynchronous command delivery resource within HuaweiCloud. +--- + +# huaweicloud_iotda_device_async_command + +Manages a device asynchronous command delivery resource within HuaweiCloud. + +-> 1.This resource is only a one-time action resource for doing API action. Deleting this resource will not clear + the corresponding request record, but will only remove the resource information from the tfstate file. +
2.Currently, this resource is only supported deliver commands asynchronously to NB-IoT devices. +
3.After the resource is created, please pay attention to the command executed result through `status`, + you can execute the **terraform plan** command at regular intervals to monitor `status` changes. + +-> When accessing an IoTDA **standard** or **enterprise** edition instance, you need to specify the IoTDA service + endpoint in `provider` block. + You can login to the IoTDA console, choose the instance **Overview** and click **Access Details** + to view the HTTPS application access address. An example of the access address might be + **9bc34xxxxx.st1.iotda-app.ap-southeast-1.myhuaweicloud.com**, then you need to configure the + `provider` block as follows: + + ```hcl + provider "huaweicloud" { + endpoints = { + iotda = "https://9bc34xxxxx.st1.iotda-app.ap-southeast-1.myhuaweicloud.com" + } + } + ``` + +## Example Usage + +```hcl +variable "device_id"{} +variable "send_strategy"{} + +resource "huaweicloud_iotda_device_async_command" "test" { + device_id = var.device_id + send_strategy = var.send_strategy +} +``` + +## Argument Reference + +The following arguments are supported: + +* `region` - (Optional, String, ForceNew) Specifies the region in which to create the resource. + If omitted, the provider-level region will be used. + Changing this parameter will create a new resource. + +* `device_id` - (Required, String, ForceNew) Specifies the ID of the device to which the command is delivered. + Changing this parameter will create a new resource. + +* `send_strategy` - (Required, String, ForceNew) Specifies the delivery policy. + The valid values are as follows: + + **immediately**: The command is delivered immediately. + + **delay**: The command is cached and delivered after the device reports data or goes online. + + Changing this parameter will create a new resource. + +* `service_id` - (Optional, String, ForceNew) Specifies the ID of the device service to which the device command belongs, + which is defined in the product model associated with the device. + This parameter is mandatory if the device requires codecs to parse commands. + Changing this parameter will create a new resource. + +* `name` - (Optional, String, ForceNew) Specifies the command name, which is defined in the product model + associated with the device. + This parameter is mandatory if the device requires codecs to parse commands. + Changing this parameter will create a new resource. + +* `paras` - (Optional, Map, ForceNew) Specifies the command executed by the device. + If `service_id` is specified, each key is the parameter in commands in the product model. + If `service_id` is left empty, the key can be customized. + The maximum size of the request object is `256` KB. + Changing this parameter will create a new resource. + +* `expire_time` - (Optional, Int, ForceNew) Specifies the duration of caching commands on the IoT platform. + This parameter is valid only when `send_strategy` is set to **delay**. The unit is second. + If `expire_time` is set to **0** or not specified, the command is cached for `24` hours (`86,400` seconds) by default, + and the maximum cache duration is `2` days. + Changing this parameter will create a new resource. + +## Attribute Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The resource ID. + +* `status` - The status of the command. + The valid values are as follows: + + **PENDING**: The command is not delivered and is cached on the platform. + + **EXPIRED**: The command has expired, the cache time exceeds the value of `expire_time`. + + **SENT**: The command is being delivered. + + **DELIVERED**: The command has been delivered. + + **SUCCESSFUL**: The command has been executed. + + **FAILED**: The command fails to be executed. + + **TIMEOUT**: After the command is delivered, no response is received from the device or the response times out. + +* `result` - The command execution result. + +* `sent_time` - The time of the platform sent the command. + The format is **yyyyMMdd'T'HHmmss'Z'**, e.g. **20151212T121212Z**. + +* `delivered_time` - The time of the device received the command. + The format is **yyyyMMdd'T'HHmmss'Z'**, e.g. **20151212T121212Z**. + +* `response_time` - The time of the device responded to the command. + The format is **yyyyMMdd'T'HHmmss'Z'**, e.g. **20151212T121212Z**. + +* `created_at` - The creation time of the device command. + The format is **yyyyMMdd'T'HHmmss'Z'**, e.g. **20151212T121212Z**. diff --git a/huaweicloud/provider.go b/huaweicloud/provider.go index 3f8da57e65..b75c488d57 100644 --- a/huaweicloud/provider.go +++ b/huaweicloud/provider.go @@ -1685,18 +1685,19 @@ func Provider() *schema.Provider { "huaweicloud_images_image_share": ims.ResourceImsImageShare(), "huaweicloud_images_image_share_accepter": ims.ResourceImsImageShareAccepter(), - "huaweicloud_iotda_access_credential": iotda.ResourceAccessCredential(), - "huaweicloud_iotda_amqp": iotda.ResourceAmqp(), - "huaweicloud_iotda_batchtask": iotda.ResourceBatchTask(), - "huaweicloud_iotda_dataforwarding_rule": iotda.ResourceDataForwardingRule(), - "huaweicloud_iotda_device": iotda.ResourceDevice(), - "huaweicloud_iotda_device_certificate": iotda.ResourceDeviceCertificate(), - "huaweicloud_iotda_device_group": iotda.ResourceDeviceGroup(), - "huaweicloud_iotda_device_linkage_rule": iotda.ResourceDeviceLinkageRule(), - "huaweicloud_iotda_device_proxy": iotda.ResourceDeviceProxy(), - "huaweicloud_iotda_product": iotda.ResourceProduct(), - "huaweicloud_iotda_space": iotda.ResourceSpace(), - "huaweicloud_iotda_upgrade_package": iotda.ResourceUpgradePackage(), + "huaweicloud_iotda_access_credential": iotda.ResourceAccessCredential(), + "huaweicloud_iotda_amqp": iotda.ResourceAmqp(), + "huaweicloud_iotda_batchtask": iotda.ResourceBatchTask(), + "huaweicloud_iotda_dataforwarding_rule": iotda.ResourceDataForwardingRule(), + "huaweicloud_iotda_device": iotda.ResourceDevice(), + "huaweicloud_iotda_device_async_command": iotda.ResourceDeviceAsyncCommand(), + "huaweicloud_iotda_device_certificate": iotda.ResourceDeviceCertificate(), + "huaweicloud_iotda_device_group": iotda.ResourceDeviceGroup(), + "huaweicloud_iotda_device_linkage_rule": iotda.ResourceDeviceLinkageRule(), + "huaweicloud_iotda_device_proxy": iotda.ResourceDeviceProxy(), + "huaweicloud_iotda_product": iotda.ResourceProduct(), + "huaweicloud_iotda_space": iotda.ResourceSpace(), + "huaweicloud_iotda_upgrade_package": iotda.ResourceUpgradePackage(), "huaweicloud_kms_data_encrypt_decrypt": dew.ResourceKmsDataEncryptDecrypt(), "huaweicloud_kms_key": dew.ResourceKmsKey(), diff --git a/huaweicloud/services/acceptance/iotda/resource_huaweicloud_iotda_device_async_command_test.go b/huaweicloud/services/acceptance/iotda/resource_huaweicloud_iotda_device_async_command_test.go new file mode 100644 index 0000000000..8e220281d1 --- /dev/null +++ b/huaweicloud/services/acceptance/iotda/resource_huaweicloud_iotda_device_async_command_test.go @@ -0,0 +1,157 @@ +package iotda + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + + "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iotda/v5/model" + + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/config" + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/services/acceptance" +) + +func getDeviceAsyncCommandFunc(conf *config.Config, state *terraform.ResourceState) (interface{}, error) { + client, err := conf.HcIoTdaV5Client(acceptance.HW_REGION_NAME, WithDerivedAuth()) + if err != nil { + return nil, fmt.Errorf("error creating IoTDA v5 client: %s", err) + } + + return client.ShowAsyncDeviceCommand(&model.ShowAsyncDeviceCommandRequest{DeviceId: state.Primary.Attributes["device_id"], + CommandId: state.Primary.ID}) +} + +func TestAccDeviceAsyncCommand_basic(t *testing.T) { + var obj model.ShowDeviceResponse + + name := acceptance.RandomAccResourceName() + rName := "huaweicloud_iotda_device_async_command.test" + rc := acceptance.InitResourceCheck( + rName, + &obj, + getDeviceAsyncCommandFunc, + ) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + ProviderFactories: acceptance.TestAccProviderFactories, + CheckDestroy: rc.CheckResourceDestroy(), + Steps: []resource.TestStep{ + { + Config: testAccDeviceAsyncCommand_basic(name), + Check: resource.ComposeTestCheckFunc( + rc.CheckResourceExists(), + resource.TestCheckResourceAttrPair(rName, "device_id", "huaweicloud_iotda_device.test", "id"), + resource.TestCheckResourceAttrPair(rName, "service_id", "huaweicloud_iotda_product.test", "services.0.id"), + resource.TestCheckResourceAttrPair(rName, "name", "huaweicloud_iotda_product.test", "services.0.commands.0.name"), + resource.TestCheckResourceAttr(rName, "send_strategy", "delay"), + resource.TestCheckResourceAttr(rName, "expire_time", "80000"), + resource.TestCheckResourceAttrSet(rName, "status"), + resource.TestCheckResourceAttrSet(rName, "created_at"), + ), + }, + }, + }) +} + +func TestAccDeviceAsyncCommand_derived(t *testing.T) { + var obj model.ShowDeviceResponse + + name := acceptance.RandomAccResourceName() + rName := "huaweicloud_iotda_device_async_command.test" + rc := acceptance.InitResourceCheck( + rName, + &obj, + getDeviceAsyncCommandFunc, + ) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acceptance.TestAccPreCheck(t) + acceptance.TestAccPreCheckHWIOTDAAccessAddress(t) + }, + ProviderFactories: acceptance.TestAccProviderFactories, + CheckDestroy: rc.CheckResourceDestroy(), + Steps: []resource.TestStep{ + { + Config: testAccDeviceAsyncCommand_basic(name), + Check: resource.ComposeTestCheckFunc( + rc.CheckResourceExists(), + resource.TestCheckResourceAttrPair(rName, "device_id", "huaweicloud_iotda_device.test", "id"), + resource.TestCheckResourceAttrPair(rName, "service_id", "huaweicloud_iotda_product.test", "services.0.id"), + resource.TestCheckResourceAttrPair(rName, "name", "huaweicloud_iotda_product.test", "services.0.commands.0.name"), + resource.TestCheckResourceAttr(rName, "send_strategy", "delay"), + resource.TestCheckResourceAttr(rName, "expire_time", "80000"), + resource.TestCheckResourceAttrSet(rName, "status"), + resource.TestCheckResourceAttrSet(rName, "created_at"), + ), + }, + }, + }) +} + +func testAccDeviceAsyncCommand_base(name string) string { + return fmt.Sprintf(` +resource "huaweicloud_iotda_space" "test" { + name = "%[1]s" +} + +resource "huaweicloud_iotda_product" "test" { + name = "%[1]s" + device_type = "AI" + protocol = "CoAP" + space_id = huaweicloud_iotda_space.test.id + data_type = "json" + industry = "smart-home" + + services { + id = "001211985996" + type = "001002" + + commands { + name = "cmd-test" + + paras { + name = "cmd-req" + type = "string" + max_length = 20 + } + + responses { + name = "cmd-resp" + type = "string" + max_length = 20 + } + } + } +} + +resource "huaweicloud_iotda_device" "test" { + node_id = "101112026" + name = "%[1]s" + space_id = huaweicloud_iotda_space.test.id + product_id = huaweicloud_iotda_product.test.id + description = "demo" +} +`, name) +} + +func testAccDeviceAsyncCommand_basic(name string) string { + return fmt.Sprintf(` +%[1]s + +resource "huaweicloud_iotda_device_async_command" "test" { + device_id = huaweicloud_iotda_device.test.id + service_id = huaweicloud_iotda_product.test.services.0.id + name = huaweicloud_iotda_product.test.services.0.commands.0.name + send_strategy = "delay" + expire_time = "80000" + + paras = { + (huaweicloud_iotda_product.test.services.0.commands.0.paras.0.name) = "tf-acc" + } +} +`, testAccDeviceAsyncCommand_base(name)) +} diff --git a/huaweicloud/services/iotda/resource_huaweicloud_iotda_device_async_command.go b/huaweicloud/services/iotda/resource_huaweicloud_iotda_device_async_command.go new file mode 100644 index 0000000000..e52a2739c6 --- /dev/null +++ b/huaweicloud/services/iotda/resource_huaweicloud_iotda_device_async_command.go @@ -0,0 +1,175 @@ +package iotda + +import ( + "context" + + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iotda/v5/model" + + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/common" + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/config" + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/utils" +) + +// @API IoTDA POST /v5/iot/{project_id}/devices/{device_id}/async-commands +// @API IoTDA GET /v5/iot/{project_id}/devices/{device_id}/async-commands/{command_id} +func ResourceDeviceAsyncCommand() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceDeviceAsyncCommandCreate, + ReadContext: resourceDeviceAsyncCommandRead, + DeleteContext: resourceDeviceAsyncCommandDelete, + + Schema: map[string]*schema.Schema{ + "region": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + "device_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "send_strategy": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "service_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + "name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + "paras": { + Type: schema.TypeMap, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "expire_time": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + ForceNew: true, + }, + "status": { + Type: schema.TypeString, + Computed: true, + }, + "result": { + Type: schema.TypeMap, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "created_at": { + Type: schema.TypeString, + Computed: true, + }, + "sent_time": { + Type: schema.TypeString, + Computed: true, + }, + "delivered_time": { + Type: schema.TypeString, + Computed: true, + }, + "response_time": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func buildDeviceAsyncCommandBodyParams(d *schema.ResourceData) *model.CreateAsyncCommandRequest { + commandParas := utils.ValueIgnoreEmpty(d.Get("paras")) + bodyParams := model.CreateAsyncCommandRequest{ + DeviceId: d.Get("device_id").(string), + Body: &model.AsyncDeviceCommandRequest{ + ServiceId: utils.StringIgnoreEmpty(d.Get("service_id").(string)), + CommandName: utils.StringIgnoreEmpty(d.Get("name").(string)), + SendStrategy: d.Get("send_strategy").(string), + //nolint:gosec + ExpireTime: utils.Int32IgnoreEmpty(int32(d.Get("expire_time").(int))), + }, + } + if commandParas != nil { + bodyParams.Body.Paras = &commandParas + } + + return &bodyParams +} + +func resourceDeviceAsyncCommandCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + cfg := meta.(*config.Config) + region := cfg.GetRegion(d) + isDerived := WithDerivedAuth(cfg, region) + client, err := cfg.HcIoTdaV5Client(region, isDerived) + if err != nil { + return diag.Errorf("error creating IoTDA v5 client: %s", err) + } + + createOpts := buildDeviceAsyncCommandBodyParams(d) + resp, err := client.CreateAsyncCommand(createOpts) + if err != nil { + return diag.Errorf("error creating device asynchronous command : %s", err) + } + + if resp == nil || resp.CommandId == nil { + return diag.Errorf("error creating device asynchronous command: ID is not found in API response") + } + + d.SetId(*resp.CommandId) + + return resourceDeviceAsyncCommandRead(ctx, d, meta) +} + +func resourceDeviceAsyncCommandRead(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + c := meta.(*config.Config) + region := c.GetRegion(d) + isDerived := WithDerivedAuth(c, region) + client, err := c.HcIoTdaV5Client(region, isDerived) + if err != nil { + return diag.Errorf("error creating IoTDA v5 client: %s", err) + } + + deviceId := d.Get("device_id").(string) + + response, err := client.ShowAsyncDeviceCommand(&model.ShowAsyncDeviceCommandRequest{DeviceId: deviceId, CommandId: d.Id()}) + if err != nil { + return common.CheckDeletedDiag(d, err, "error retrieving device asynchronous command") + } + + mErr := multierror.Append( + d.Set("region", region), + d.Set("device_id", response.DeviceId), + d.Set("service_id", response.ServiceId), + d.Set("name", response.CommandName), + d.Set("status", response.Status), + d.Set("result", response.Result), + d.Set("send_strategy", response.SendStrategy), + d.Set("expire_time", response.ExpireTime), + d.Set("sent_time", response.SentTime), + d.Set("delivered_time", response.DeliveredTime), + d.Set("response_time", response.ResponseTime), + d.Set("created_at", response.CreatedTime), + ) + + return diag.FromErr(mErr.ErrorOrNil()) +} + +func resourceDeviceAsyncCommandDelete(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + // No processing is performed in the 'Delete()' method because the resource is a action resource. + return nil +}