diff --git a/docs/resources/modelarts_devserver_action.md b/docs/resources/modelarts_devserver_action.md new file mode 100644 index 00000000000..e9066367ec7 --- /dev/null +++ b/docs/resources/modelarts_devserver_action.md @@ -0,0 +1,44 @@ +--- +subcategory: "AI Development Platform (ModelArts)" +layout: "huaweicloud" +page_title: "HuaweiCloud: huaweicloud_modelarts_devserver_action" +description: |- + Use this resource to operate ModelArts DevServer within HuaweiCloud. +--- +# huaweicloud_modelarts_devserver_action + +Use this resource to operate ModelArts DevServer within HuaweiCloud. + +## Example Usage + +```hcl +variable "devserver_id" {} + +resource "huaweicloud_modelarts_devserver_action" "test" { + devserver_id = var.devserver_id + action = "start" +} +``` + +## 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 creates a new resource. + +* `devserver_id` - (Required, String, ForceNew) Specifies the ID of the DevServer. + Changing this creates a new resource. + +* `action` - (Required, String, ForceNew) Specifies the action type of the DevServer. + Changing this creates a new resource. + The valid values are as follows: + + **start**: The DevServers can be started only when it is stopped, stop failure or start failure. + + **stop**: The DevServers can be stopped only when it is running or stop failure. + +## Attribute Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The resource ID. diff --git a/huaweicloud/provider.go b/huaweicloud/provider.go index de4d04dd582..bffdeb5de32 100644 --- a/huaweicloud/provider.go +++ b/huaweicloud/provider.go @@ -1909,6 +1909,7 @@ func Provider() *schema.Provider { "huaweicloud_modelarts_dataset": modelarts.ResourceDataset(), "huaweicloud_modelarts_dataset_version": modelarts.ResourceDatasetVersion(), + "huaweicloud_modelarts_devserver_action": modelarts.ResourceDevServerAction(), "huaweicloud_modelarts_notebook": modelarts.ResourceNotebook(), "huaweicloud_modelarts_notebook_mount_storage": modelarts.ResourceNotebookMountStorage(), "huaweicloud_modelarts_model": modelarts.ResourceModelartsModel(), diff --git a/huaweicloud/services/acceptance/modelarts/resource_huaweicloud_modelarts_devserver_test.go b/huaweicloud/services/acceptance/modelarts/resource_huaweicloud_modelarts_devserver_test.go new file mode 100644 index 00000000000..a0689eb8157 --- /dev/null +++ b/huaweicloud/services/acceptance/modelarts/resource_huaweicloud_modelarts_devserver_test.go @@ -0,0 +1,182 @@ +package modelarts + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/config" + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/services/acceptance" + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/services/acceptance/common" + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/services/modelarts" +) + +func getDevServerResourceFunc(cfg *config.Config, state *terraform.ResourceState) (interface{}, error) { + region := acceptance.HW_REGION_NAME + client, err := cfg.NewServiceClient("modelarts", region) + if err != nil { + return nil, fmt.Errorf("error creating ModelArts client: %s", err) + } + + return modelarts.GetDevServerById(client, state.Primary.ID) +} + +func TestAccDevServer_basic(t *testing.T) { + var ( + obj interface{} + resourceName = "huaweicloud_modelarts_devserver.test" + name = acceptance.RandomAccResourceName() + password = acceptance.RandomPassword("!@%-_=+[{}]:,./?") + rc = acceptance.InitResourceCheck( + resourceName, + &obj, + getDevServerResourceFunc, + ) + ) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acceptance.TestAccPreCheck(t) + acceptance.TestAccPreCheckModelartsDevServer(t) + }, + ProviderFactories: acceptance.TestAccProviderFactories, + CheckDestroy: rc.CheckResourceDestroy(), + Steps: []resource.TestStep{ + { + Config: testDevServer_basic_step1(name, password, true), + Check: resource.ComposeTestCheckFunc( + rc.CheckResourceExists(), + resource.TestCheckResourceAttr(resourceName, "name", name), + resource.TestCheckResourceAttr(resourceName, "flavor", acceptance.HW_MODELARTS_DEVSERVER_FLAVOR), + resource.TestCheckResourceAttrSet(resourceName, "architecture"), + resource.TestCheckResourceAttrPair(resourceName, "vpc_id", "huaweicloud_vpc.test", "id"), + resource.TestCheckResourceAttrPair(resourceName, "subnet_id", "huaweicloud_vpc_subnet.test", "id"), + resource.TestCheckResourceAttrPair(resourceName, "security_group_id", "huaweicloud_networking_secgroup.test", "id"), + resource.TestCheckResourceAttr(resourceName, "image_id", acceptance.HW_MODELARTS_DEVSERVER_IMAGE_ID), + resource.TestCheckResourceAttrSet(resourceName, "type"), + resource.TestCheckResourceAttr(resourceName, "charging_mode", "PRE_PAID"), + resource.TestCheckResourceAttr(resourceName, "auto_renew", "true"), + resource.TestMatchResourceAttr(resourceName, "created_at", + regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}?(Z|([+-]\d{2}:\d{2}))$`)), + ), + }, + { + Config: testDevServer_basic_step2(name, password, false), + Check: resource.ComposeTestCheckFunc( + rc.CheckResourceExists(), + resource.TestCheckResourceAttr(resourceName, "auto_renew", "false"), + ), + }, + // Stopping the DevServer. + { + Config: testDevServer_basic_step3(name, password, false), + }, + // Stopping the stopped DevServer. + { + Config: testDevServer_basic_step4(name, password, false), + ExpectError: regexp.MustCompile(`Resource.Server '[a-f0-9-]+' is not allowed STOP: STOPPED`), + }, + // Starting the DevServer. + { + Config: testDevServer_basic_step5(name, password, false), + }, + // Starting the running DevServer. + { + Config: testDevServer_basic_step6(name, password, false), + ExpectError: regexp.MustCompile(`Resource.Server '[a-f0-9-]+' is not allowed START: RUNNING`), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "subnet_id", + "security_group_id", + "admin_pass", + "root_volume", + "period_unit", + "period", + "auto_renew", + }, + }, + }, + }) +} + +func testDevServer_basic_step1(name, password string, autoRenew bool) string { + return fmt.Sprintf(` +%[1]s + +resource "huaweicloud_modelarts_devserver" "test" { + name = "%[2]s" + flavor = "%[3]s" + vpc_id = huaweicloud_vpc.test.id + subnet_id = huaweicloud_vpc_subnet.test.id + security_group_id = huaweicloud_networking_secgroup.test.id + image_id = "%[4]s" + admin_pass = "%[5]s" + + root_volume { + size = 100 + type = "SSD" + } + + charging_mode = "PRE_PAID" + period_unit = "MONTH" + period = 1 + auto_renew = "%[6]v" +} +`, common.TestBaseNetwork(name), name, + acceptance.HW_MODELARTS_DEVSERVER_FLAVOR, acceptance.HW_MODELARTS_DEVSERVER_IMAGE_ID, password, autoRenew) +} + +func testDevServer_basic_step2(name, password string, autoRenew bool) string { + return testDevServer_basic_step1(name, password, autoRenew) +} + +func testDevServer_basic_step3(name, password string, autoRenew bool) string { + return fmt.Sprintf(` +%[1]s + +resource "huaweicloud_modelarts_devserver_action" "stop" { + devserver_id = huaweicloud_modelarts_devserver.test.id + action = "stop" +} +`, testDevServer_basic_step2(name, password, autoRenew)) +} + +func testDevServer_basic_step4(name, password string, autoRenew bool) string { + return fmt.Sprintf(` +%[1]s + +resource "huaweicloud_modelarts_devserver_action" "expect_stop_err" { + devserver_id = huaweicloud_modelarts_devserver.test.id + action = "stop" +} +`, testDevServer_basic_step3(name, password, autoRenew)) +} + +func testDevServer_basic_step5(name, password string, autoRenew bool) string { + return fmt.Sprintf(` +%[1]s + +resource "huaweicloud_modelarts_devserver_action" "start" { + devserver_id = huaweicloud_modelarts_devserver.test.id + action = "start" +} +`, testDevServer_basic_step3(name, password, autoRenew)) +} + +func testDevServer_basic_step6(name, password string, autoRenew bool) string { + return fmt.Sprintf(` +%[1]s + +resource "huaweicloud_modelarts_devserver_action" "expect_start_err" { + devserver_id = huaweicloud_modelarts_devserver.test.id + action = "start" +} +`, testDevServer_basic_step5(name, password, autoRenew)) +} diff --git a/huaweicloud/services/modelarts/resource_huaweicloud_modelarts_devserver_action.go b/huaweicloud/services/modelarts/resource_huaweicloud_modelarts_devserver_action.go new file mode 100644 index 00000000000..4671a445511 --- /dev/null +++ b/huaweicloud/services/modelarts/resource_huaweicloud_modelarts_devserver_action.go @@ -0,0 +1,133 @@ +package modelarts + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/chnsz/golangsdk" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/config" + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/utils" +) + +// @API ModelArts PUT /v1/{project_id}/dev-servers/{id}/start +// @API ModelArts PUT /v1/{project_id}/dev-servers/{id}/stop +// @API ModelArts GET /v1/{project_id}/dev-servers/{id} +func ResourceDevServerAction() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceDevServerActionCreate, + ReadContext: resourceDevServerActionRead, + DeleteContext: resourceDevServerActionDelete, + + Schema: map[string]*schema.Schema{ + "region": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + "devserver_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `The ID of the DevServer.`, + }, + "action": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `The action type of the DevServer.`, + }, + }, + } +} + +func resourceDevServerActionCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var ( + cfg = meta.(*config.Config) + region = cfg.GetRegion(d) + httpUrl = "v1/{project_id}/dev-servers/{id}/{action}" + devServerId = d.Get("devserver_id").(string) + action = d.Get("action").(string) + actionCompletedMap = map[string]string{ + "start": "RUNNING", + "stop": "STOPPED", + } + ) + + client, err := cfg.NewServiceClient("modelarts", region) + if err != nil { + return diag.Errorf("error creating ModelArts client: %s", err) + } + + actionPath := client.Endpoint + httpUrl + actionPath = strings.ReplaceAll(actionPath, "{project_id}", client.ProjectID) + actionPath = strings.ReplaceAll(actionPath, "{id}", devServerId) + actionPath = strings.ReplaceAll(actionPath, "{action}", action) + actionOpt := golangsdk.RequestOpts{ + KeepResponseBody: true, + } + if action == "start" { + actionOpt.JSONBody = map[string]interface{}{} + } + + _, err = client.Request("PUT", actionPath, &actionOpt) + if err != nil { + return diag.Errorf("unable to %s DevServer (%s): %s", action, devServerId, err) + } + + stateConf := &resource.StateChangeConf{ + Pending: []string{"PENDING"}, + Target: []string{"COMPLETED"}, + Refresh: refreshDevServerActionStatusFunc(client, devServerId, actionCompletedMap[action]), + Timeout: d.Timeout(schema.TimeoutCreate), + Delay: 10 * time.Second, + PollInterval: 10 * time.Second, + } + _, err = stateConf.WaitForStateContext(ctx) + if err != nil { + return diag.Errorf("error waiting for DevServer (%s) to %s completed: %s", devServerId, action, err) + } + + d.SetId(devServerId) + + return nil +} + +func refreshDevServerActionStatusFunc(client *golangsdk.ServiceClient, devServerId string, target string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + respBody, err := GetDevServerById(client, devServerId) + if err != nil { + return nil, "ERROR", err + } + + status := utils.PathSearch("status", respBody, "").(string) + if utils.StrSliceContains([]string{"START_FAILED", "ERROR", "STOP_FAILED"}, status) { + return respBody, "ERROR", fmt.Errorf("unexpected status (%s)", status) + } + + if status == target { + return respBody, "COMPLETED", nil + } + return "continue", "PENDING", nil + } +} + +func resourceDevServerActionRead(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil +} + +func resourceDevServerActionDelete(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + errorMsg := `This resource is only a one-time action resource for operating the DevServer. Deleting this resource will +not clear the corresponding request record, but will only remove the resource information from the tfstate file.` + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Warning, + Summary: errorMsg, + }, + } +}