diff --git a/docs/resources/hss_host_group_v5.md b/docs/resources/hss_host_group_v5.md new file mode 100644 index 000000000..1f16dc8a6 --- /dev/null +++ b/docs/resources/hss_host_group_v5.md @@ -0,0 +1,70 @@ +--- +subcategory: "Host Security Service (HSS)" +layout: "opentelekomcloud" +page_title: "OpenTelekomCloud: opentelekomcloud_hss_host_group_v5" +sidebar_current: "docs-opentelekomcloud-resource-hss-host-group-v5" +description: |- + Manages an HSS host group Service resource within OpenTelekomCloud. +--- + +# opentelekomcloud_hss_host_group_v5 + +Manages an HSS host group resource within OpenTelekomCloud. + +## Example Usage + +### Create an HSS host group and bind ECS instances + +```hcl +variable "host_group_name" {} +variable "host_ids" { + type = list(string) +} + +resource "opentelekomcloud_hss_host_group_v5" "test" { + name = var.host_group_name + host_ids = var.host_ids +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required, String) Specifies the name of the host group. + The valid length is limited from `1` to `64`, only Chinese characters, English letters, digits, hyphens (-), + underscores (_), dots (.), pluses (+) and asterisks (*) are allowed. + The Chinese characters must be in `UTF-8` or `Unicode` format. + +* `host_ids` - (Required, List) Specifies the list of host IDs. + +## Attribute Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The resource ID in UUID format. + +* `host_num` - The total host number. + +* `region` - The region where the host group is located. + +* `risk_host_num` - The number of hosts at risk. + +* `unprotect_host_num` - The number of unprotect hosts. + +* `unprotect_host_ids` - The ID list of the unprotect hosts. + +## Timeouts + +This resource provides the following timeouts configuration options: + +* `create` - Default is 30 minutes. +* `update` - Default is 30 minutes. + +## Import + +The host group resource can be imported using `id`, e.g. + +```bash +$ terraform import opentelekomcloud_hss_host_group_v5.group +``` diff --git a/go.mod b/go.mod index 8fac7840f..de4d32a9a 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/jmespath/go-jmespath v0.4.0 github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4 github.com/mitchellh/go-homedir v1.1.0 - github.com/opentelekomcloud/gophertelekomcloud v0.9.4-0.20241104181956-db479a6d384d + github.com/opentelekomcloud/gophertelekomcloud v0.9.4-0.20241111133703-101a74fd5b4e github.com/unknwon/com v1.0.1 golang.org/x/crypto v0.21.0 golang.org/x/sync v0.1.0 diff --git a/go.sum b/go.sum index c63d2deee..0a070442a 100644 --- a/go.sum +++ b/go.sum @@ -158,6 +158,8 @@ github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/opentelekomcloud/gophertelekomcloud v0.9.4-0.20241104181956-db479a6d384d h1:6nr8FpvqTw30NPORd7XIKKUW0EtYEKzWbxEO5mF/00g= github.com/opentelekomcloud/gophertelekomcloud v0.9.4-0.20241104181956-db479a6d384d/go.mod h1:M1F6OfSRZRzAmAFKQqSLClX952at5hx5rHe4UTEykgg= +github.com/opentelekomcloud/gophertelekomcloud v0.9.4-0.20241111133703-101a74fd5b4e h1:/rdKqoWltx2CwxKQQ4hPxuxX6ip2JQ8lAazWTvtji3k= +github.com/opentelekomcloud/gophertelekomcloud v0.9.4-0.20241111133703-101a74fd5b4e/go.mod h1:M1F6OfSRZRzAmAFKQqSLClX952at5hx5rHe4UTEykgg= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/opentelekomcloud/acceptance/hss/resource_opentelekomcloud_hss_host_group_v5_test.go b/opentelekomcloud/acceptance/hss/resource_opentelekomcloud_hss_host_group_v5_test.go new file mode 100644 index 000000000..57bb172e9 --- /dev/null +++ b/opentelekomcloud/acceptance/hss/resource_opentelekomcloud_hss_host_group_v5_test.go @@ -0,0 +1,130 @@ +package hss + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + group "github.com/opentelekomcloud/gophertelekomcloud/openstack/hss/v5/host" + "github.com/opentelekomcloud/terraform-provider-opentelekomcloud/opentelekomcloud/acceptance/common" + "github.com/opentelekomcloud/terraform-provider-opentelekomcloud/opentelekomcloud/acceptance/env" + "github.com/opentelekomcloud/terraform-provider-opentelekomcloud/opentelekomcloud/common/cfg" + "github.com/opentelekomcloud/terraform-provider-opentelekomcloud/opentelekomcloud/services/hss" +) + +func getHostGroupFunc(conf *cfg.Config, state *terraform.ResourceState) (interface{}, error) { + client, err := conf.HssV5Client(env.OS_REGION_NAME) + if err != nil { + return nil, fmt.Errorf("error creating HSS v5 client: %s", err) + } + return hss.QueryHostGroupById(client, state.Primary.ID) +} + +func TestAccHostGroup_basic(t *testing.T) { + var ( + gr *group.HostGroupResp + + name = fmt.Sprintf("hss-acc-api%s", acctest.RandString(5)) + rName = "opentelekomcloud_hss_host_group_v5.group" + ) + + rc := common.InitResourceCheck( + rName, + &gr, + getHostGroupFunc, + ) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + common.TestAccPreCheck(t) + }, + ProviderFactories: common.TestAccProviderFactories, + CheckDestroy: rc.CheckResourceDestroy(), + Steps: []resource.TestStep{ + { + Config: testAccHostGroup_basic(name), + Check: resource.ComposeTestCheckFunc( + rc.CheckResourceExists(), + resource.TestCheckResourceAttr(rName, "name", name), + resource.TestCheckResourceAttr(rName, "host_ids.#", "1"), + resource.TestCheckResourceAttrSet(rName, "host_num"), + ), + }, + { + Config: testAccHostGroup_update(name), + Check: resource.ComposeTestCheckFunc( + rc.CheckResourceExists(), + resource.TestCheckResourceAttr(rName, "name", name+"-update"), + resource.TestCheckResourceAttr(rName, "host_ids.#", "2"), + resource.TestCheckResourceAttrSet(rName, "host_num"), + ), + }, + { + ResourceName: rName, + ImportState: true, + ImportStateVerify: true, + // The field `unprotect_host_ids` will be filled in during the creation and editing operations. + // We only need to add ignore to the test case and do not need to make special instructions in the document. + ImportStateVerifyIgnore: []string{ + "unprotect_host_ids", + }, + }, + }, + }) +} + +func testAccHostGroup_base(name string) string { + return fmt.Sprintf(` +%[1]s + +resource "opentelekomcloud_compute_instance_v2" "instance" { + count = 2 + + name = "%s" + description = "my_desc" + availability_zone = "%s" + + image_name = "Standard_Debian_11_latest" + flavor_id = "s3.large.2" + + metadata = { + foo = "bar" + } + network { + uuid = data.opentelekomcloud_vpc_subnet_v1.shared_subnet.network_id + } + + tags = { + muh = "value-create" + kuh = "value-create" + emp = "" + } + + stop_before_destroy = true +} +`, common.DataSourceSubnet, name, env.OS_AVAILABILITY_ZONE) +} + +func testAccHostGroup_basic(name string) string { + return fmt.Sprintf(` +%[1]s + +resource "opentelekomcloud_hss_host_group_v5" "group" { + name = "%[2]s" + host_ids = slice(opentelekomcloud_compute_instance_v2.instance[*].id, 0, 1) +} +`, testAccHostGroup_base(name), name) +} + +func testAccHostGroup_update(name string) string { + return fmt.Sprintf(` +%[1]s + +resource "opentelekomcloud_hss_host_group_v5" "group" { + name = "%[2]s-update" + host_ids = opentelekomcloud_compute_instance_v2.instance[*].id +} +`, testAccHostGroup_base(name), name) +} diff --git a/opentelekomcloud/common/cfg/config.go b/opentelekomcloud/common/cfg/config.go index d6ebfcfbf..323b11703 100644 --- a/opentelekomcloud/common/cfg/config.go +++ b/opentelekomcloud/common/cfg/config.go @@ -1217,6 +1217,13 @@ func (c *Config) EvpnV5Client(region string) (*golangsdk.ServiceClient, error) { }) } +func (c *Config) HssV5Client(region string) (*golangsdk.ServiceClient, error) { + return openstack.NewHssV5(c.HwClient, golangsdk.EndpointOpts{ + Region: region, + Availability: c.getEndpointType(), + }) +} + func reconfigProjectName(src Config, projectName ProjectName) (*Config, error) { config := &Config{} if err := copier.Copy(config, &src); err != nil { diff --git a/opentelekomcloud/provider.go b/opentelekomcloud/provider.go index 4eb302c10..4d6d371ef 100644 --- a/opentelekomcloud/provider.go +++ b/opentelekomcloud/provider.go @@ -35,6 +35,7 @@ import ( "github.com/opentelekomcloud/terraform-provider-opentelekomcloud/opentelekomcloud/services/fgs" "github.com/opentelekomcloud/terraform-provider-opentelekomcloud/opentelekomcloud/services/fw" "github.com/opentelekomcloud/terraform-provider-opentelekomcloud/opentelekomcloud/services/gaussdb" + "github.com/opentelekomcloud/terraform-provider-opentelekomcloud/opentelekomcloud/services/hss" "github.com/opentelekomcloud/terraform-provider-opentelekomcloud/opentelekomcloud/services/iam" "github.com/opentelekomcloud/terraform-provider-opentelekomcloud/opentelekomcloud/services/ims" "github.com/opentelekomcloud/terraform-provider-opentelekomcloud/opentelekomcloud/services/kms" @@ -460,6 +461,7 @@ func Provider() *schema.Provider { "opentelekomcloud_fw_policy_v2": fw.ResourceFWPolicyV2(), "opentelekomcloud_fw_rule_v2": fw.ResourceFWRuleV2(), "opentelekomcloud_gaussdb_mysql_instance_v3": gaussdb.ResourceGaussDBInstanceV3(), + "opentelekomcloud_hss_host_group_v5": hss.ResourceHostGroup(), "opentelekomcloud_identity_acl_v3": iam.ResourceIdentityAclV3(), "opentelekomcloud_identity_agency_v3": iam.ResourceIdentityAgencyV3(), "opentelekomcloud_identity_credential_v3": iam.ResourceIdentityCredentialV3(), diff --git a/opentelekomcloud/services/hss/common.go b/opentelekomcloud/services/hss/common.go new file mode 100644 index 000000000..27b43f0fa --- /dev/null +++ b/opentelekomcloud/services/hss/common.go @@ -0,0 +1,13 @@ +package hss + +const ( + errCreationV5Client = "error creating OpenTelekomCloud HSS v5 client: %w" + hssClientV5 = "hss-v5-client" +) + +type ProtectStatus string + +const ( + ProtectStatusClosed ProtectStatus = "closed" + ProtectStatusOpened ProtectStatus = "opened" +) diff --git a/opentelekomcloud/services/hss/resource_opentelekomcloud_hss_host_group_v5.go b/opentelekomcloud/services/hss/resource_opentelekomcloud_hss_host_group_v5.go new file mode 100644 index 000000000..8da5fc064 --- /dev/null +++ b/opentelekomcloud/services/hss/resource_opentelekomcloud_hss_host_group_v5.go @@ -0,0 +1,329 @@ +package hss + +import ( + "context" + "encoding/json" + "fmt" + "log" + "time" + + "github.com/hashicorp/go-multierror" + "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/jmespath/go-jmespath" + golangsdk "github.com/opentelekomcloud/gophertelekomcloud" + group "github.com/opentelekomcloud/gophertelekomcloud/openstack/hss/v5/host" + "github.com/opentelekomcloud/terraform-provider-opentelekomcloud/opentelekomcloud/common" + "github.com/opentelekomcloud/terraform-provider-opentelekomcloud/opentelekomcloud/common/cfg" + "github.com/opentelekomcloud/terraform-provider-opentelekomcloud/opentelekomcloud/common/fmterr" +) + +func ResourceHostGroup() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceHostGroupCreate, + ReadContext: resourceHostGroupRead, + UpdateContext: resourceHostGroupUpdate, + DeleteContext: resourceHostGroupDelete, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(30 * time.Minute), + Update: schema.DefaultTimeout(30 * time.Minute), + }, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "host_ids": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "host_num": { + Type: schema.TypeInt, + Computed: true, + }, + "region": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + "risk_host_num": { + Type: schema.TypeInt, + Computed: true, + }, + "unprotect_host_num": { + Type: schema.TypeInt, + Computed: true, + }, + "unprotect_host_ids": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + } +} + +func resourceHostGroupCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(*cfg.Config) + client, err := common.ClientFromCtx(ctx, hssClientV5, func() (*golangsdk.ServiceClient, error) { + return config.HssV5Client(config.GetRegion(d)) + }) + if err != nil { + return fmterr.Errorf(errCreationV5Client, err) + } + + groupName := d.Get("name").(string) + hostIds := common.ExpandToStringListBySet(d.Get("host_ids").(*schema.Set)) + + opts := group.CreateOpts{ + Name: groupName, + HostIds: hostIds, + } + + unprotected, err := checkAllHostsAvailable(ctx, client, hostIds, d.Timeout(schema.TimeoutCreate)) + if err != nil { + return diag.FromErr(err) + } + log.Printf("[DEBUG] All OpenTelekomCloud HSS hosts are availabile.") + if len(unprotected) > 1 { + err := d.Set("unprotect_host_ids", unprotected) + if err != nil { + log.Printf("[WARN] These OpenTelekomCloud HSS hosts are not protected: %#v", unprotected) + } + } + err = group.Create(client, opts) + if err != nil { + return diag.Errorf("error creating OpenTelekomCloud HSS host group: %s", err) + } + + allHostGroups, err := queryHostGroups(client, groupName) + if err != nil { + return diag.FromErr(err) + } + if len(allHostGroups) < 1 { + return common.CheckDeletedDiag(d, err, "OpenTelekomCloud HSS host group") + } + d.SetId(allHostGroups[0].ID) + + clientCtx := common.CtxWithClient(ctx, client, hssClientV5) + return resourceHostGroupRead(clientCtx, d, meta) +} + +func checkAllHostsAvailable(ctx context.Context, client *golangsdk.ServiceClient, hostIDs []string, timeout time.Duration) ([]string, error) { + unprotected := make([]string, 0) + for _, hostId := range hostIDs { + log.Printf("[DEBUG] Waiting for the OpenTelekomCloud HSS host (%s) status to become available.", hostId) + stateConf := &resource.StateChangeConf{ + Pending: []string{"PENDING"}, + Target: []string{"COMPLETED"}, + Refresh: hostStatusRefreshFunc(client, hostId), + Timeout: timeout, + Delay: 30 * time.Second, + PollInterval: 30 * time.Second, + } + unprotectedHostId, err := stateConf.WaitForStateContext(ctx) + if err != nil { + return nil, fmt.Errorf("error waiting for the OpenTelekomCloud HSS host (%s) status to become completed: %s", hostId, err) + } + if unprotectedHostId != nil && unprotectedHostId.(string) != "" { + unprotected = append(unprotected, unprotectedHostId.(string)) + } + } + return unprotected, nil +} + +func hostStatusRefreshFunc(client *golangsdk.ServiceClient, hostId string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + var unprotectedHostId string + hostList, err := group.ListHost(client, group.ListHostOpts{ + HostID: hostId, + }) + if err != nil { + return unprotectedHostId, "ERROR", err + } + if hostList == nil || len(hostList) < 1 { + return unprotectedHostId, "PENDING", nil + } + if hostList[0].ProtectStatus == string(ProtectStatusClosed) { + unprotectedHostId = hostList[0].ID + } + return unprotectedHostId, "COMPLETED", nil + } +} + +func queryHostGroups(client *golangsdk.ServiceClient, name string) ([]group.HostGroupResp, error) { + groups, err := group.List(client, group.ListOpts{ + Name: name, + }) + if err != nil { + return nil, fmt.Errorf("error fetching OpenTelekomCloud HSS host group: %s", err) + } + return groups, nil +} + +func QueryHostGroupById(client *golangsdk.ServiceClient, groupId string) (*group.HostGroupResp, error) { + allHostGroups, err := queryHostGroups(client, "") + if err != nil { + return nil, err + } + filter := map[string]interface{}{ + "ID": groupId, + } + result, err := common.FilterSliceWithField(allHostGroups, filter) + if err != nil { + return nil, fmt.Errorf("error filtering OpenTelekomCloud HSS host groups list: %s", err) + } + if len(result) < 1 { + return nil, golangsdk.ErrDefault404{ + ErrUnexpectedResponseCode: golangsdk.ErrUnexpectedResponseCode{ + Body: []byte(fmt.Sprintf("the OpenTelekomCloud HSS host group (%s) does not exist", groupId)), + }, + } + } + if item, ok := result[0].(group.HostGroupResp); ok { + return &item, nil + } + return nil, fmt.Errorf("invalid OpenTelekomCloud HSS host group list, want 'group.HostGroupResp', but '%T'", result[0]) +} + +func resourceHostGroupRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(*cfg.Config) + client, err := common.ClientFromCtx(ctx, hssClientV5, func() (*golangsdk.ServiceClient, error) { + return config.HssV5Client(config.GetRegion(d)) + }) + if err != nil { + return fmterr.Errorf(errCreationV5Client, err) + } + + g, err := QueryHostGroupById(client, d.Id()) + if err != nil { + return common.CheckDeletedDiag(d, err, "OpenTelekomCloud HSS host group") + } + log.Printf("[DEBUG] The response of OpenTelekomCloud HSS host group is: %#v", g) + + mErr := multierror.Append(nil, + d.Set("region", config.GetRegion(d)), + d.Set("name", g.Name), + d.Set("host_ids", g.HostIds), + d.Set("host_num", g.HostNum), + d.Set("risk_host_num", g.RiskHostNum), + d.Set("unprotect_host_num", g.UnprotectHostNum), + ) + + if len(d.Get("unprotect_host_ids").([]interface{})) == 0 { + // The reason for writing an empty array to `unprotect_host_ids` is to avoid unexpected changes + mErr = multierror.Append(mErr, d.Set("unprotect_host_ids", make([]string, 0))) + } + + if err = mErr.ErrorOrNil(); err != nil { + return diag.Errorf("error saving OpenTelekomCloud HSS host group fields: %s", err) + } + return nil +} + +func resourceHostGroupUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(*cfg.Config) + client, err := common.ClientFromCtx(ctx, hssClientV5, func() (*golangsdk.ServiceClient, error) { + return config.HssV5Client(config.GetRegion(d)) + }) + if err != nil { + return fmterr.Errorf(errCreationV5Client, err) + } + + hostIds := common.ExpandToStringListBySet(d.Get("host_ids").(*schema.Set)) + + opts := group.UpdateOpts{ + ID: d.Id(), + Name: d.Get("name").(string), + HostIds: hostIds, + } + + unprotected, err := checkAllHostsAvailable(ctx, client, hostIds, d.Timeout(schema.TimeoutUpdate)) + if err != nil { + return diag.FromErr(err) + } + log.Printf("[DEBUG] All OpenTelekomCloud HSS hosts are availabile.") + if len(unprotected) > 1 { + err := d.Set("unprotect_host_ids", unprotected) + if err != nil { + log.Printf("[WARN] These OpenTelekomCloud HSS hosts are not protected: %#v", unprotected) + } + } + err = group.Update(client, opts) + if err != nil { + return diag.Errorf("error updating OpenTelekomCloud HSS host group: %s", err) + } + + clientCtx := common.CtxWithClient(ctx, client, hssClientV5) + return resourceHostGroupRead(clientCtx, d, meta) +} + +func resourceHostGroupDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(*cfg.Config) + client, err := common.ClientFromCtx(ctx, hssClientV5, func() (*golangsdk.ServiceClient, error) { + return config.HssV5Client(config.GetRegion(d)) + }) + if err != nil { + return fmterr.Errorf(errCreationV5Client, err) + } + err = group.Delete(client, group.DeleteOpts{GroupID: d.Id()}) + if err != nil { + return common.CheckDeletedDiag(d, parseDeleteHostGroupResponseError(err), "error deleting OpenTelekomCloud HSS host group") + } + + return nil +} + +// When the host group does not exist, the response code for deleting the API is `400`, +// and the response body is as follows: +// {"status_code":400,"request_id":"f17e56c2e92584cfd4614ab467cd6a1b","error_code":"", +// "error_message":"{\"error_code\":\"00100090\",\"error_description\":\"Failed to load server groups.\"}", +// "encoded_authorization_message":""} +func parseDeleteHostGroupResponseError(err error) error { + var errObj map[string]interface{} + if jsonErr := json.Unmarshal([]byte(err.Error()), &errObj); jsonErr != nil { + log.Printf("[WARN] failed to unmarshal error object: %s", jsonErr) + return err + } + + statusCode, parseStatusCodeErr := jmespath.Search("status_code", errObj) + if parseStatusCodeErr != nil || statusCode == nil { + log.Printf("[WARN] failed to parse status_code from response body: %s", parseStatusCodeErr) + return err + } + + if statusCodeFloat, ok := statusCode.(float64); ok && int(statusCodeFloat) == 400 { + errorMessage, parseErrorMessageErr := jmespath.Search("error_message", errObj) + if parseErrorMessageErr != nil || errorMessage == nil { + log.Printf("[WARN] failed to parse error_message: %s", parseErrorMessageErr) + return err + } + + var errMsgObj map[string]interface{} + if errMsgJson := json.Unmarshal([]byte(errorMessage.(string)), &errMsgObj); errMsgJson != nil { + log.Printf("[WARN] failed to unmarshal error_message: %s", errMsgJson) + return err + } + + errorCode, errorCodeErr := jmespath.Search("error_code", errMsgObj) + if errorCodeErr != nil || errorCode == nil { + log.Printf("[WARN] failed to extract error_code: %s", errorCodeErr) + return err + } + + if errorCode == "00100090" { + return golangsdk.ErrDefault404{} + } + } + + return err +} diff --git a/releasenotes/notes/hss-host-group-ea845c935a382d43.yaml b/releasenotes/notes/hss-host-group-ea845c935a382d43.yaml new file mode 100644 index 000000000..a812a1bca --- /dev/null +++ b/releasenotes/notes/hss-host-group-ea845c935a382d43.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + **New Resource:** ``opentelekomcloud_hss_host_group_v5`` (`#2718 `_)