diff --git a/docs/resources/modelarts_notebook.md b/docs/resources/modelarts_notebook.md index 5894140f47..977d1838fb 100644 --- a/docs/resources/modelarts_notebook.md +++ b/docs/resources/modelarts_notebook.md @@ -11,21 +11,52 @@ Manages ModelArts notebook resource within HuaweiCloud. ## Example Usage +### Create a notebook with the EVS storage type + ```hcl variable "notebook_name" {} variable "key_pair_name" {} -variable "ip" {} +variable "image_id" {} +variable "allowed_ip_addresses" { + type = list(string) +} +variable "key_pair_name" {} -resource "huaweicloud_modelarts_notebook" "notebook" { +resource "huaweicloud_modelarts_notebook" "test" { name = var.notebook_name flavor_id = "modelarts.vm.cpu.2u" - image_id = "e1a07296-22a8-4f05-8bc8-e936c8e54090" + image_id = var.image_id - allowed_access_ips = [var.ip] + allowed_access_ips = var.allowed_ip_addresses key_pair = var.key_pair_name volume { - type = "EFS" + type = "EVS" + size = 5 + } +} +``` + +### Create a notebook with the EFS storage type + +```hcl +variable "notebook_name" {} +variable "image_id" {} +variable "resource_pool_id" {} +variable "sfs_export_location" {} +variable "sfs_turbo_id" {} + +resource "huaweicloud_modelarts_notebook" "test" { + name = var.notebook_name + flavor_id = "modelarts.vm.cpu.2u" + image_id = var.image_id + pool_id = var.resource_pool_id + + volume { + type = "EFS" + ownership = "DEDICATED" + uri = var.sfs_export_location + id = var.sfs_turbo_id } } ``` @@ -89,10 +120,14 @@ The `volume` block supports: Changing this parameter will create a new resource. -* `uri` - (Optional, String, ForceNew) Specifies the uri of dedicated storage disk, which is mandatory when the `type` +* `uri` - (Optional, String, ForceNew) Specifies the URL of dedicated storage disk, which is mandatory when the `type` is `EFS` and the `ownership` is `DEDICATED`. Example: `192.168.0.1:/user-9sfdsdgdfgh5ea4d56871e75d6966aa274/mount/`. Changing this parameter will create a new resource. +* `id` - (Optional, String, ForceNew) Specifies the ID of dedicated storage disk, which is mandatory when the `type` + is `EFS` and the `ownership` is `DEDICATED`. + Changing this parameter will create a new resource. + ## Attribute Reference In addition to all arguments above, the following attributes are exported: diff --git a/docs/resources/modelarts_resource_pool.md b/docs/resources/modelarts_resource_pool.md index 2ebb729148..a8fa22e662 100644 --- a/docs/resources/modelarts_resource_pool.md +++ b/docs/resources/modelarts_resource_pool.md @@ -104,7 +104,7 @@ The following arguments are supported: Including resource flavors and the number of resources of the corresponding flavors. The [resources](#ModelartsResourcePool_ResourceFlavor) structure is documented below. -* `scope` - (Optional, List) Specifies the list of job types supported by the resource pool. It is mandatory when +* `scope` - (Required, List) Specifies the list of job types supported by the resource pool. It is mandatory when `network_id` is specified and can not be specified when `vpc_id` is specified. The options are as follows: + **Train**: training job. + **Infer**: inference job. diff --git a/go.mod b/go.mod index f0ec98ebd8..75459b8bed 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.18 require ( github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 - github.com/chnsz/golangsdk v0.0.0-20240529073340-b68ab4ec7a36 + github.com/chnsz/golangsdk v0.0.0-20240531093804-71e344f541e8 github.com/hashicorp/go-cleanhttp v0.5.2 github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-uuid v1.0.3 diff --git a/go.sum b/go.sum index 15cea29a96..9f68c91282 100644 --- a/go.sum +++ b/go.sum @@ -22,8 +22,8 @@ github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6 github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/chnsz/golangsdk v0.0.0-20240529073340-b68ab4ec7a36 h1:MiPVVsb+UjJOJa1Jydc0QCPrX1RIVrqXI8nqj1Yz6X4= -github.com/chnsz/golangsdk v0.0.0-20240529073340-b68ab4ec7a36/go.mod h1:Erm4hDWxXgAdbkG3+hhJFgRzEL1TvvcroWzw2Gax4uI= +github.com/chnsz/golangsdk v0.0.0-20240531093804-71e344f541e8 h1:TFPjAfOsUpO6UZFfDoYImYpchjbPBjMgtAPu5UJ8Zpg= +github.com/chnsz/golangsdk v0.0.0-20240531093804-71e344f541e8/go.mod h1:Erm4hDWxXgAdbkG3+hhJFgRzEL1TvvcroWzw2Gax4uI= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= diff --git a/huaweicloud/services/acceptance/modelarts/resource_huaweicloud_modelarts_notebook_test.go b/huaweicloud/services/acceptance/modelarts/resource_huaweicloud_modelarts_notebook_test.go index 07b2ff0d15..bd7d88021b 100644 --- a/huaweicloud/services/acceptance/modelarts/resource_huaweicloud_modelarts_notebook_test.go +++ b/huaweicloud/services/acceptance/modelarts/resource_huaweicloud_modelarts_notebook_test.go @@ -11,6 +11,7 @@ import ( "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" ) func getNotebookResourceFunc(cfg *config.Config, state *terraform.ResourceState) (interface{}, error) { @@ -95,6 +96,105 @@ resource "huaweicloud_modelarts_notebook" "test" { `, rName) } +func TestAccResourceNotebook_dedicated(t *testing.T) { + var instance notebook.CreateOpts + resourceName := "huaweicloud_modelarts_notebook.test" + name := acceptance.RandomAccResourceNameWithDash() + + rc := acceptance.InitResourceCheck( + resourceName, + &instance, + getNotebookResourceFunc, + ) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + ProviderFactories: acceptance.TestAccProviderFactories, + CheckDestroy: rc.CheckResourceDestroy(), + Steps: []resource.TestStep{ + { + Config: testAccNotebook_dedicated(name), + Check: resource.ComposeTestCheckFunc( + rc.CheckResourceExists(), + resource.TestCheckResourceAttr(resourceName, "name", name), + resource.TestCheckResourceAttr(resourceName, "flavor_id", "modelarts.vm.cpu.2u"), + resource.TestCheckResourceAttr(resourceName, "volume.0.type", "EFS"), + resource.TestCheckResourceAttr(resourceName, "volume.0.ownership", "DEDICATED"), + resource.TestCheckResourceAttrPair(resourceName, "volume.0.uri", "huaweicloud_sfs_turbo.test", "export_location"), + resource.TestCheckResourceAttrPair(resourceName, "volume.0.id", "huaweicloud_sfs_turbo.test", "id"), + resource.TestCheckResourceAttr(resourceName, "auto_stop_enabled", "false"), + resource.TestCheckResourceAttrSet(resourceName, "image_name"), + resource.TestCheckResourceAttrSet(resourceName, "image_swr_path"), + resource.TestCheckResourceAttrSet(resourceName, "image_type"), + resource.TestCheckResourceAttrSet(resourceName, "created_at"), + resource.TestCheckResourceAttrSet(resourceName, "updated_at"), + resource.TestCheckResourceAttrSet(resourceName, "url"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccNotebook_dedicated(rName string) string { + return fmt.Sprintf(` +%[1]s + +data "huaweicloud_availability_zones" "test" {} + +resource "huaweicloud_sfs_turbo" "test" { + name = "%[2]s" + size = 1228 + share_proto = "NFS" + share_type = "HPC" + hpc_bandwidth = "40M" + vpc_id = huaweicloud_vpc.test.id + subnet_id = huaweicloud_vpc_subnet.test.id + security_group_id = huaweicloud_networking_secgroup.test.id + availability_zone = data.huaweicloud_availability_zones.test.names[0] +} + +resource "huaweicloud_modelarts_network" "test" { + name = "%[2]s" + cidr = "172.16.0.0/12" + + peer_connections { + vpc_id = huaweicloud_vpc.test.id + subnet_id = huaweicloud_vpc_subnet.test.id + } +} + +resource "huaweicloud_modelarts_resource_pool" "test" { + name = "%[2]s" + scope = ["Notebook", "Train", "Infer"] + network_id = huaweicloud_modelarts_network.test.id + + resources { + flavor_id = "modelarts.vm.cpu.8ud" + count = 1 + } +} + +resource "huaweicloud_modelarts_notebook" "test" { + name = "%[2]s" + flavor_id = "modelarts.vm.cpu.2u" + image_id = "e1a07296-22a8-4f05-8bc8-e936c8e54090" + pool_id = huaweicloud_modelarts_resource_pool.test.id + + volume { + type = "EFS" + ownership = "DEDICATED" + uri = huaweicloud_sfs_turbo.test.export_location + id = huaweicloud_sfs_turbo.test.id + } +} +`, common.TestBaseNetwork(rName), rName) +} + func TestAccResourceNotebook_all(t *testing.T) { var instance notebook.CreateOpts resourceName := "huaweicloud_modelarts_notebook.test" diff --git a/huaweicloud/services/acceptance/modelarts/resource_huaweicloud_modelarts_resource_pool_test.go b/huaweicloud/services/acceptance/modelarts/resource_huaweicloud_modelarts_resource_pool_test.go index 59f4252e11..bbc66d5084 100644 --- a/huaweicloud/services/acceptance/modelarts/resource_huaweicloud_modelarts_resource_pool_test.go +++ b/huaweicloud/services/acceptance/modelarts/resource_huaweicloud_modelarts_resource_pool_test.go @@ -270,7 +270,7 @@ func testModelartsResourcePool_basic_update(name string) string { resource "huaweicloud_modelarts_resource_pool" "test" { name = "%s" description = "This is a demo update" - scope = ["Train", "Infer"] + scope = ["Infer", "Train"] network_id = huaweicloud_modelarts_network.test.id resources { diff --git a/huaweicloud/services/modelarts/resource_huaweicloud_modelarts_notebook.go b/huaweicloud/services/modelarts/resource_huaweicloud_modelarts_notebook.go index 142a4a7032..c86900184c 100644 --- a/huaweicloud/services/modelarts/resource_huaweicloud_modelarts_notebook.go +++ b/huaweicloud/services/modelarts/resource_huaweicloud_modelarts_notebook.go @@ -48,9 +48,6 @@ func ResourceNotebook() *schema.Resource { "name": { Type: schema.TypeString, Required: true, - ValidateFunc: validation.StringMatch(regexp.MustCompile(`^[A-Za-z][\w]{1,64}$`), - "The name consists of 1 to 64 characters, starting with a letter. "+ - "Only letters, digits and underscores (_) are allowed."), }, "flavor_id": { Type: schema.TypeString, @@ -90,6 +87,11 @@ func ResourceNotebook() *schema.Resource { Optional: true, ForceNew: true, }, + "id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, "mount_path": { Type: schema.TypeString, Optional: true, @@ -378,9 +380,14 @@ func buildVolumeParamter(d *schema.ResourceData) (*notebook.VolumeReq, error) { if rst.Category == "EFS" && rst.Ownership == "DEDICATED" { v, ok := d.GetOk("volume.0.uri") if !ok { - return nil, fmt.Errorf("uri is mandatory if the storage type is EFS and ownership is DEDICATED") + return nil, fmt.Errorf("the parameter 'uri' is mandatory if the storage type is EFS and ownership is DEDICATED") } rst.Uri = v.(string) + v, ok = d.GetOk("volume.0.id") + if !ok { + return nil, fmt.Errorf("the parameter 'id' is mandatory if the storage type is EFS and ownership is DEDICATED") + } + rst.ID = v.(string) } if v, ok := d.GetOk("volume.0.size"); ok { @@ -408,6 +415,8 @@ func setVolumeToState(d *schema.ResourceData, volume notebook.VolumeRes) error { result["type"] = volume.Category result["ownership"] = volume.Ownership result["size"] = volume.Capacity + result["uri"] = volume.URI + result["id"] = volume.ID result["mount_path"] = volume.MountPath return d.Set("volume", []map[string]interface{}{result}) } diff --git a/huaweicloud/services/modelarts/resource_huaweicloud_modelarts_resource_pool.go b/huaweicloud/services/modelarts/resource_huaweicloud_modelarts_resource_pool.go index 2d361baed8..f4340cf54f 100644 --- a/huaweicloud/services/modelarts/resource_huaweicloud_modelarts_resource_pool.go +++ b/huaweicloud/services/modelarts/resource_huaweicloud_modelarts_resource_pool.go @@ -58,11 +58,16 @@ func ResourceModelartsResourcePool() *schema.Resource { Description: `The name of the resource pool.`, }, "scope": { - Type: schema.TypeList, - Elem: &schema.Schema{Type: schema.TypeString}, - Optional: true, - Computed: true, - Description: `List of job types supported by the resource pool.`, + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + Computed: true, + Description: utils.SchemaDesc( + `List of job types supported by the resource pool.`, + utils.SchemaDescInput{ + Required: true, + }, + ), }, "resources": { Type: schema.TypeList, @@ -305,6 +310,42 @@ func modelartsResourcePoolUserLoginSchema() *schema.Resource { return &sc } +func scopeStatusRefreshFunc(cfg *config.Config, region string, d *schema.ResourceData, scopes []interface{}) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + getResourcePoolRespBody, err := queryResourcePool(cfg, region, d) + if err != nil { + return getResourcePoolRespBody, "ERROR", err + } + + for _, scope := range scopes { + scopeStatus := fmt.Sprintf("status.scope[?scopeType=='%s']|[0].state", scope) + if utils.PathSearch(scopeStatus, getResourcePoolRespBody, "").(string) != "Enabled" { + return "No matches found", "PENDING", nil + } + } + return "Matched", "COMPLETED", nil + } +} + +func createResourcePoolWaitingForScopesCompleted(ctx context.Context, d *schema.ResourceData, meta interface{}, timeout time.Duration) error { + cfg := meta.(*config.Config) + region := cfg.GetRegion(d) + stateConf := &resource.StateChangeConf{ + Pending: []string{"PENDING"}, + Target: []string{"COMPLETED"}, + Refresh: scopeStatusRefreshFunc(cfg, region, d, d.Get("scope").(*schema.Set).List()), + Timeout: timeout, + // In most cases, the bind operation will be completed immediately, but in a few cases, it needs to wait + // for a short period of time, and the polling is performed by incrementing the time here. + PollInterval: 10 * time.Second, + } + _, err := stateConf.WaitForStateContext(ctx) + if err != nil { + return fmt.Errorf("error waiting for the scope statuses are both completed: %s", err) + } + return nil +} + func resourceModelartsResourcePoolCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { cfg := meta.(*config.Config) region := cfg.GetRegion(d) @@ -376,6 +417,11 @@ func resourceModelartsResourcePoolCreate(ctx context.Context, d *schema.Resource if err != nil { return diag.Errorf("error waiting for the Modelarts resource pool (%s) creation to complete: %s", d.Id(), err) } + + err = createResourcePoolWaitingForScopesCompleted(ctx, d, meta, d.Timeout(schema.TimeoutCreate)) + if err != nil { + return diag.Errorf("error waiting for the Modelarts resource pool (%s) creation to complete: %s", d.Id(), err) + } return resourceModelartsResourcePoolRead(ctx, d, meta) } @@ -429,7 +475,7 @@ func buildCreateResourcePoolMetaDataAnnotationsBodyParams(d *schema.ResourceData func buildCreateResourcePoolSpecBodyParams(d *schema.ResourceData) map[string]interface{} { params := map[string]interface{}{ "type": "Dedicate", - "scope": utils.ValueIngoreEmpty(d.Get("scope")), + "scope": utils.ValueIngoreEmpty(d.Get("scope").(*schema.Set).List()), "resources": buildResourcePoolSpecResources(d), "userLogin": buildCreateResourcePoolSpecUserLoginBodyParams(d), "network": buildCreateResourcePoolSpecNetworkBodyParams(d), @@ -842,7 +888,7 @@ func buildUpdateResourcePoolMetaDataAnnotationsBodyParams(d *schema.ResourceData func buildUpdateResourcePoolSpecBodyParams(d *schema.ResourceData) map[string]interface{} { params := map[string]interface{}{ - "scope": utils.ValueIngoreEmpty(d.Get("scope")), + "scope": utils.ValueIngoreEmpty(d.Get("scope").(*schema.Set).List()), "resources": buildResourcePoolSpecResources(d), } return params @@ -937,8 +983,8 @@ func updateResourcePoolWaitingForStateCompleted(ctx context.Context, d *schema.R } // check if the resource pool is in the process of changing scope - if rawArray, ok := d.Get("scope").([]string); ok { - for _, v := range rawArray { + if rawArray, ok := d.GetOk("scope"); ok { + for _, v := range rawArray.(*schema.Set).List() { scopeStatus := fmt.Sprintf("status.scope[?scopeType=='%s']|[0].state", v) log.Println("scopeStatus: ", scopeStatus) if utils.PathSearch(scopeStatus, getResourcePoolRespBody, "").(string) != "Enabled" { diff --git a/vendor/github.com/chnsz/golangsdk/openstack/modelarts/v1/notebook/requests.go b/vendor/github.com/chnsz/golangsdk/openstack/modelarts/v1/notebook/requests.go index 61c415cc21..fe89bc1a29 100644 --- a/vendor/github.com/chnsz/golangsdk/openstack/modelarts/v1/notebook/requests.go +++ b/vendor/github.com/chnsz/golangsdk/openstack/modelarts/v1/notebook/requests.go @@ -31,6 +31,7 @@ type VolumeReq struct { Ownership string `json:"ownership" required:"true"` Capacity *int `json:"capacity,omitempty"` Uri string `json:"uri,omitempty"` + ID string `json:"id,omitempty"` } type ListOpts struct { diff --git a/vendor/github.com/chnsz/golangsdk/openstack/modelarts/v1/notebook/results.go b/vendor/github.com/chnsz/golangsdk/openstack/modelarts/v1/notebook/results.go index 58da0fd7dc..a660fd97b2 100644 --- a/vendor/github.com/chnsz/golangsdk/openstack/modelarts/v1/notebook/results.go +++ b/vendor/github.com/chnsz/golangsdk/openstack/modelarts/v1/notebook/results.go @@ -83,6 +83,8 @@ type VolumeRes struct { MountPath string `json:"mount_path"` Ownership string `json:"ownership"` Status string `json:"status"` + URI string `json:"uri"` + ID string `json:"id"` } type ListNotebooks struct { diff --git a/vendor/modules.txt b/vendor/modules.txt index bae3f98da1..1be437c039 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -13,7 +13,7 @@ github.com/apparentlymart/go-cidr/cidr # github.com/apparentlymart/go-textseg/v13 v13.0.0 ## explicit; go 1.16 github.com/apparentlymart/go-textseg/v13/textseg -# github.com/chnsz/golangsdk v0.0.0-20240529073340-b68ab4ec7a36 +# github.com/chnsz/golangsdk v0.0.0-20240531093804-71e344f541e8 ## explicit; go 1.14 github.com/chnsz/golangsdk github.com/chnsz/golangsdk/auth