diff --git a/CHANGELOG.md b/CHANGELOG.md index aeb17ead..43a9bd8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## 0.1.2 (unreleased) - Fix issue with `catalystcenter_ip_pool_reservation` resource and multiple IP pools under a single site +- Add `catalystcenter_device_claim_site` resource ## 0.1.1 diff --git a/docs/guides/changelog.md b/docs/guides/changelog.md index 0b71e474..0f03d68a 100644 --- a/docs/guides/changelog.md +++ b/docs/guides/changelog.md @@ -10,6 +10,7 @@ description: |- ## 0.1.2 (unreleased) - Fix issue with `catalystcenter_ip_pool_reservation` resource and multiple IP pools under a single site +- Add `catalystcenter_device_claim_site` resource ## 0.1.1 diff --git a/docs/resources/device_claim_site.md b/docs/resources/device_claim_site.md new file mode 100644 index 00000000..745f1c2b --- /dev/null +++ b/docs/resources/device_claim_site.md @@ -0,0 +1,69 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "catalystcenter_device_claim_site Resource - terraform-provider-catalystcenter" +subcategory: "Plug and Play" +description: |- + This resource can manage a Device Claim Site. +--- + +# catalystcenter_device_claim_site (Resource) + +This resource can manage a Device Claim Site. + +## Example Usage + +```terraform +resource "catalystcenter_device_claim_site" "example" { + device_id = "12345678-1234-1234-1234-123456789012" + site_id = "12345678-1234-1234-1234-123456789012" + type = "Default" + image_id = "" + image_skip = true + config_id = "template1" + config_parameters = [ + { + name = "HOSTNAME" + value = "switch1" + } + ] +} +``` + + +## Schema + +### Required + +- `device_id` (String) The device ID +- `site_id` (String) The site ID +- `type` (String) The device type + - Choices: `Default`, `StackSwitch`, `AccessPoint`, `Sensor`, `CatalystWLC`, `MobilityExpress` + +### Optional + +- `config_id` (String) Config (temaplate) ID. Required if `type` is `Default` or `StackSwitch`. +- `config_parameters` (Attributes List) List of config (temaplate) parameters. (see [below for nested schema](#nestedatt--config_parameters)) +- `gateway` (String) Gateway IP. Required if `type` is `CatalystWLC` or `MobilityExpress`. +- `image_id` (String) Image ID. Required if `type` is `Default` or `StackSwitch`. +- `image_skip` (Boolean) Skip image provisioning. +- `ip_interface_name` (String) IP interface name. Required for Catalyst 9800 WLC. +- `rf_profile` (String) RF profile. Required if `type` is `AccessPoint`. +- `sensor_profile` (String) Sensor profile. Required if `type` is `Sensor`. +- `static_ip` (String) Static IP address. Required if `type` is `CatalystWLC` or `MobilityExpress`. +- `subnet_mask` (String) Subnet mask. Required if `type` is `CatalystWLC` or `MobilityExpress`. +- `vlan_id` (String) Vlan ID. Required for Catalyst 9800 WLC. + +### Read-Only + +- `id` (String) The id of the object + + +### Nested Schema for `config_parameters` + +Required: + +- `name` (String) Name of config parameter. + +Optional: + +- `value` (String) Value of config parameter. diff --git a/examples/resources/catalystcenter_device_claim_site/resource.tf b/examples/resources/catalystcenter_device_claim_site/resource.tf new file mode 100644 index 00000000..4f1161bb --- /dev/null +++ b/examples/resources/catalystcenter_device_claim_site/resource.tf @@ -0,0 +1,14 @@ +resource "catalystcenter_device_claim_site" "example" { + device_id = "12345678-1234-1234-1234-123456789012" + site_id = "12345678-1234-1234-1234-123456789012" + type = "Default" + image_id = "" + image_skip = true + config_id = "template1" + config_parameters = [ + { + name = "HOSTNAME" + value = "switch1" + } + ] +} diff --git a/gen/definitions/device_claim_site.yaml b/gen/definitions/device_claim_site.yaml new file mode 100644 index 00000000..cb1b2d52 --- /dev/null +++ b/gen/definitions/device_claim_site.yaml @@ -0,0 +1,96 @@ +--- +name: Device Claim Site +rest_endpoint: /dna/intent/api/v1/onboarding/pnp-device/site-claim +no_data_source: true +no_read: true +no_delete: true +no_import: true +post_update: true +skip_minimum_test: true +doc_category: Plug and Play +test_tags: [PNP] +attributes: + - model_name: deviceId + type: String + mandatory: true + id: true + description: The device ID + example: 12345678-1234-1234-1234-123456789012 + - model_name: siteId + type: String + mandatory: true + description: The site ID + example: 12345678-1234-1234-1234-123456789012 + - model_name: type + type: String + mandatory: true + enum_values: [Default, StackSwitch, AccessPoint, Sensor, CatalystWLC, MobilityExpress] + description: The device type + example: Default + - model_name: imageId + data_path: imageInfo + type: String + description: Image ID. Required if `type` is `Default` or `StackSwitch`. + example: "" + - model_name: skip + data_path: imageInfo + tf_name: image_skip + type: Bool + description: Skip image provisioning. + example: true + - model_name: configId + data_path: configInfo + type: String + description: Config (temaplate) ID. Required if `type` is `Default` or `StackSwitch`. + example: template1 + - model_name: configParameters + data_path: configInfo + type: List + description: List of config (temaplate) parameters. + attributes: + - model_name: key + tf_name: name + id: true + type: String + description: Name of config parameter. + example: HOSTNAME + - model_name: value + type: String + description: Value of config parameter. + example: switch1 + - model_name: rfProfile + type: String + description: RF profile. Required if `type` is `AccessPoint`. + example: profile1 + exclude_test: true + - model_name: staticIP + tf_name: static_ip + type: String + description: Static IP address. Required if `type` is `CatalystWLC` or `MobilityExpress`. + example: 1.2.3.4 + exclude_test: true + - model_name: subnetMask + type: String + description: Subnet mask. Required if `type` is `CatalystWLC` or `MobilityExpress`. + example: 255.255.255.0 + exclude_test: true + - model_name: gateway + type: String + description: Gateway IP. Required if `type` is `CatalystWLC` or `MobilityExpress`. + example: 1.2.3.1 + exclude_test: true + - model_name: vlanId + type: String + description: Vlan ID. Required for Catalyst 9800 WLC. + example: "100" + exclude_test: true + - model_name: ipInterfaceName + type: String + description: IP interface name. Required for Catalyst 9800 WLC. + example: GigabitEthernet1 + exclude_test: true + - model_name: sensorProfile + type: String + description: Sensor profile. Required if `type` is `Sensor`. + example: profile1 + exclude_test: true diff --git a/gen/doc_category.go b/gen/doc_category.go index 22d0cb85..db955b92 100644 --- a/gen/doc_category.go +++ b/gen/doc_category.go @@ -33,8 +33,10 @@ const ( ) type YamlConfig struct { - Name string `yaml:"name"` - DocCategory string `yaml:"doc_category"` + Name string `yaml:"name"` + DocCategory string `yaml:"doc_category"` + NoResource bool `yaml:"no_resource"` + NoDataSource bool `yaml:"no_data_source"` } var docPaths = []string{"./docs/data-sources/", "./docs/resources/"} @@ -74,6 +76,10 @@ func main() { // Update doc category for i := range configs { for _, path := range docPaths { + if (configs[i].NoDataSource && path == "./docs/data-sources/") || + (configs[i].NoResource && path == "./docs/resources/") { + continue + } filename := path + SnakeCase(configs[i].Name) + ".md" content, err := os.ReadFile(filename) if err != nil { diff --git a/gen/generator.go b/gen/generator.go index f9b20e10..251f193c 100644 --- a/gen/generator.go +++ b/gen/generator.go @@ -95,6 +95,8 @@ var templates = []t{ type YamlConfig struct { Name string `yaml:"name"` + NoResource bool `yaml:"no_resource"` + NoDataSource bool `yaml:"no_data_source"` RestEndpoint string `yaml:"rest_endpoint"` GetRestEndpoint string `yaml:"get_rest_endpoint"` PutRestEndpoint string `yaml:"put_rest_endpoint"` @@ -104,6 +106,8 @@ type YamlConfig struct { GetRequiresId bool `yaml:"get_requires_id"` GetExtraQueryParams string `yaml:"get_extra_query_params"` NoDelete bool `yaml:"no_delete"` + NoRead bool `yaml:"no_read"` + NoImport bool `yaml:"no_import"` PostUpdate bool `yaml:"post_update"` RootList bool `yaml:"root_list"` NoReadPrefix bool `yaml:"no_read_prefix"` @@ -410,8 +414,6 @@ func renderTemplate(templatePath, outputPath string, config interface{}) { } func main() { - providerConfig := make([]string, 0) - files, _ := os.ReadDir(definitionsPath) configs := make([]YamlConfig, len(files)) @@ -436,13 +438,22 @@ func main() { // Iterate over templates and render files for _, t := range templates { + if (configs[i].NoImport && t.path == "./gen/templates/import.sh") || + (configs[i].NoDataSource && t.path == "./gen/templates/data_source.go") || + (configs[i].NoDataSource && t.path == "./gen/templates/data_source_test.go") || + (configs[i].NoDataSource && t.path == "./gen/templates/data-source.tf") || + (configs[i].NoResource && t.path == "./gen/templates/resource.go") || + (configs[i].NoResource && t.path == "./gen/templates/resource_test.go") || + (configs[i].NoResource && t.path == "./gen/templates/resource.tf") || + (configs[i].NoResource && t.path == "./gen/templates/import.sh") { + continue + } renderTemplate(t.path, t.prefix+SnakeCase(configs[i].Name)+t.suffix, configs[i]) } - providerConfig = append(providerConfig, configs[i].Name) } // render provider.go - renderTemplate(providerTemplate, providerLocation, providerConfig) + renderTemplate(providerTemplate, providerLocation, configs) changelog, err := os.ReadFile(changelogOriginal) if err != nil { diff --git a/gen/schema/schema.yaml b/gen/schema/schema.yaml index 6427d280..78f199e0 100644 --- a/gen/schema/schema.yaml +++ b/gen/schema/schema.yaml @@ -1,5 +1,7 @@ --- name: str() # Name of the resource +no_resource: bool(required=False) # Set to true if no resource should be created +no_data_source: bool(required=False) # Set to true if no data source should be created rest_endpoint: str() # REST endpoint path get_rest_endpoint: str(required=False) # Override GET REST endpoint path put_rest_endpoint: str(required=False) # Override PUT REST endpoint path @@ -9,6 +11,8 @@ get_from_all: bool(required=False) # Set to true if GET does not support queryin get_requires_id: bool(required=False) # Set to true if the GET request requires an ID in the URL path get_extra_query_params: str(required=False) # Additional query parameters for GET request no_delete: bool(required=False) # Set to true if the DELETE request is not supported +no_read: bool(required=False) # Set to true if the GET request is not supported +no_import: bool(required=False) # Set to true if the resource does not support importing post_update: bool(required=False) # Set to true if the POST request is used for update root_list: bool(required=False) # Set to true if the root element of the data structure is a list no_read_prefix: bool(required=False) # Set to true if it is an Open API endpoint put the response is not embeeded into a "response" element diff --git a/gen/templates/provider.go b/gen/templates/provider.go index 591035eb..4e28505d 100644 --- a/gen/templates/provider.go +++ b/gen/templates/provider.go @@ -243,7 +243,9 @@ func (p *CcProvider) Configure(ctx context.Context, req provider.ConfigureReques func (p *CcProvider) Resources(ctx context.Context) []func() resource.Resource { return []func() resource.Resource{ {{- range .}} - New{{camelCase .}}Resource, + {{- if not .NoResource}} + New{{camelCase .Name}}Resource, + {{- end}} {{- end}} } } @@ -251,7 +253,9 @@ func (p *CcProvider) Resources(ctx context.Context) []func() resource.Resource { func (p *CcProvider) DataSources(ctx context.Context) []func() datasource.DataSource { return []func() datasource.DataSource{ {{- range .}} - New{{camelCase .}}DataSource, + {{- if not .NoDataSource}} + New{{camelCase .Name}}DataSource, + {{- end}} {{- end}} } } diff --git a/gen/templates/resource.go b/gen/templates/resource.go index 9e040ac8..122e26fd 100644 --- a/gen/templates/resource.go +++ b/gen/templates/resource.go @@ -49,7 +49,9 @@ import ( // Ensure provider defined types fully satisfy framework interfaces var _ resource.Resource = &{{camelCase .Name}}Resource{} +{{- if not .NoImport}} var _ resource.ResourceWithImportState = &{{camelCase .Name}}Resource{} +{{- end}} func New{{camelCase .Name}}Resource() resource.Resource { return &{{camelCase .Name}}Resource{} @@ -450,6 +452,7 @@ func (r *{{camelCase .Name}}Resource) Read(ctx context.Context, req resource.Rea } tflog.Debug(ctx, fmt.Sprintf("%s: Beginning Read", state.Id.String())) + {{- if not .NoRead}} params := "" {{- if .IdQueryParam}} @@ -482,6 +485,7 @@ func (r *{{camelCase .Name}}Resource) Read(ctx context.Context, req resource.Rea {{- end}} state.updateFromBody(ctx, res) + {{- end}} tflog.Debug(ctx, fmt.Sprintf("%s: Read finished successfully", state.Id.ValueString())) @@ -567,7 +571,9 @@ func (r *{{camelCase .Name}}Resource) Delete(ctx context.Context, req resource.D //template:end delete //template:begin import +{{- if not .NoImport}} func (r *{{camelCase .Name}}Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) } +{{- end}} //template:end import diff --git a/internal/provider/model_catalystcenter_device_claim_site.go b/internal/provider/model_catalystcenter_device_claim_site.go new file mode 100644 index 00000000..1accd85d --- /dev/null +++ b/internal/provider/model_catalystcenter_device_claim_site.go @@ -0,0 +1,318 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// Licensed under the Mozilla Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://mozilla.org/MPL/2.0/ +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: MPL-2.0 + +// Code generated by "gen/generator.go"; DO NOT EDIT. + +package provider + +//template:begin imports +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" +) + +//template:end imports + +//template:begin types +type DeviceClaimSite struct { + Id types.String `tfsdk:"id"` + DeviceId types.String `tfsdk:"device_id"` + SiteId types.String `tfsdk:"site_id"` + Type types.String `tfsdk:"type"` + ImageId types.String `tfsdk:"image_id"` + ImageSkip types.Bool `tfsdk:"image_skip"` + ConfigId types.String `tfsdk:"config_id"` + ConfigParameters []DeviceClaimSiteConfigParameters `tfsdk:"config_parameters"` + RfProfile types.String `tfsdk:"rf_profile"` + StaticIp types.String `tfsdk:"static_ip"` + SubnetMask types.String `tfsdk:"subnet_mask"` + Gateway types.String `tfsdk:"gateway"` + VlanId types.String `tfsdk:"vlan_id"` + IpInterfaceName types.String `tfsdk:"ip_interface_name"` + SensorProfile types.String `tfsdk:"sensor_profile"` +} + +type DeviceClaimSiteConfigParameters struct { + Name types.String `tfsdk:"name"` + Value types.String `tfsdk:"value"` +} + +//template:end types + +//template:begin getPath +func (data DeviceClaimSite) getPath() string { + return "/dna/intent/api/v1/onboarding/pnp-device/site-claim" +} + +//template:end getPath + +//template:begin toBody +func (data DeviceClaimSite) toBody(ctx context.Context, state DeviceClaimSite) string { + body := "" + if !data.DeviceId.IsNull() { + body, _ = sjson.Set(body, "deviceId", data.DeviceId.ValueString()) + } + if !data.SiteId.IsNull() { + body, _ = sjson.Set(body, "siteId", data.SiteId.ValueString()) + } + if !data.Type.IsNull() { + body, _ = sjson.Set(body, "type", data.Type.ValueString()) + } + if !data.ImageId.IsNull() { + body, _ = sjson.Set(body, "imageInfo.imageId", data.ImageId.ValueString()) + } + if !data.ImageSkip.IsNull() { + body, _ = sjson.Set(body, "imageInfo.skip", data.ImageSkip.ValueBool()) + } + if !data.ConfigId.IsNull() { + body, _ = sjson.Set(body, "configInfo.configId", data.ConfigId.ValueString()) + } + if len(data.ConfigParameters) > 0 { + body, _ = sjson.Set(body, "configInfo.configParameters", []interface{}{}) + for _, item := range data.ConfigParameters { + itemBody := "" + if !item.Name.IsNull() { + itemBody, _ = sjson.Set(itemBody, "key", item.Name.ValueString()) + } + if !item.Value.IsNull() { + itemBody, _ = sjson.Set(itemBody, "value", item.Value.ValueString()) + } + body, _ = sjson.SetRaw(body, "configInfo.configParameters.-1", itemBody) + } + } + if !data.RfProfile.IsNull() { + body, _ = sjson.Set(body, "rfProfile", data.RfProfile.ValueString()) + } + if !data.StaticIp.IsNull() { + body, _ = sjson.Set(body, "staticIP", data.StaticIp.ValueString()) + } + if !data.SubnetMask.IsNull() { + body, _ = sjson.Set(body, "subnetMask", data.SubnetMask.ValueString()) + } + if !data.Gateway.IsNull() { + body, _ = sjson.Set(body, "gateway", data.Gateway.ValueString()) + } + if !data.VlanId.IsNull() { + body, _ = sjson.Set(body, "vlanId", data.VlanId.ValueString()) + } + if !data.IpInterfaceName.IsNull() { + body, _ = sjson.Set(body, "ipInterfaceName", data.IpInterfaceName.ValueString()) + } + if !data.SensorProfile.IsNull() { + body, _ = sjson.Set(body, "sensorProfile", data.SensorProfile.ValueString()) + } + return body +} + +//template:end toBody + +//template:begin fromBody +func (data *DeviceClaimSite) fromBody(ctx context.Context, res gjson.Result) { + if value := res.Get("deviceId"); value.Exists() { + data.DeviceId = types.StringValue(value.String()) + } else { + data.DeviceId = types.StringNull() + } + if value := res.Get("siteId"); value.Exists() { + data.SiteId = types.StringValue(value.String()) + } else { + data.SiteId = types.StringNull() + } + if value := res.Get("type"); value.Exists() { + data.Type = types.StringValue(value.String()) + } else { + data.Type = types.StringNull() + } + if value := res.Get("imageInfo.imageId"); value.Exists() { + data.ImageId = types.StringValue(value.String()) + } else { + data.ImageId = types.StringNull() + } + if value := res.Get("imageInfo.skip"); value.Exists() { + data.ImageSkip = types.BoolValue(value.Bool()) + } else { + data.ImageSkip = types.BoolNull() + } + if value := res.Get("configInfo.configId"); value.Exists() { + data.ConfigId = types.StringValue(value.String()) + } else { + data.ConfigId = types.StringNull() + } + if value := res.Get("configInfo.configParameters"); value.Exists() { + data.ConfigParameters = make([]DeviceClaimSiteConfigParameters, 0) + value.ForEach(func(k, v gjson.Result) bool { + item := DeviceClaimSiteConfigParameters{} + if cValue := v.Get("key"); cValue.Exists() { + item.Name = types.StringValue(cValue.String()) + } else { + item.Name = types.StringNull() + } + if cValue := v.Get("value"); cValue.Exists() { + item.Value = types.StringValue(cValue.String()) + } else { + item.Value = types.StringNull() + } + data.ConfigParameters = append(data.ConfigParameters, item) + return true + }) + } + if value := res.Get("rfProfile"); value.Exists() { + data.RfProfile = types.StringValue(value.String()) + } else { + data.RfProfile = types.StringNull() + } + if value := res.Get("staticIP"); value.Exists() { + data.StaticIp = types.StringValue(value.String()) + } else { + data.StaticIp = types.StringNull() + } + if value := res.Get("subnetMask"); value.Exists() { + data.SubnetMask = types.StringValue(value.String()) + } else { + data.SubnetMask = types.StringNull() + } + if value := res.Get("gateway"); value.Exists() { + data.Gateway = types.StringValue(value.String()) + } else { + data.Gateway = types.StringNull() + } + if value := res.Get("vlanId"); value.Exists() { + data.VlanId = types.StringValue(value.String()) + } else { + data.VlanId = types.StringNull() + } + if value := res.Get("ipInterfaceName"); value.Exists() { + data.IpInterfaceName = types.StringValue(value.String()) + } else { + data.IpInterfaceName = types.StringNull() + } + if value := res.Get("sensorProfile"); value.Exists() { + data.SensorProfile = types.StringValue(value.String()) + } else { + data.SensorProfile = types.StringNull() + } +} + +//template:end fromBody + +//template:begin updateFromBody +func (data *DeviceClaimSite) updateFromBody(ctx context.Context, res gjson.Result) { + if value := res.Get("deviceId"); value.Exists() && !data.DeviceId.IsNull() { + data.DeviceId = types.StringValue(value.String()) + } else { + data.DeviceId = types.StringNull() + } + if value := res.Get("siteId"); value.Exists() && !data.SiteId.IsNull() { + data.SiteId = types.StringValue(value.String()) + } else { + data.SiteId = types.StringNull() + } + if value := res.Get("type"); value.Exists() && !data.Type.IsNull() { + data.Type = types.StringValue(value.String()) + } else { + data.Type = types.StringNull() + } + if value := res.Get("imageInfo.imageId"); value.Exists() && !data.ImageId.IsNull() { + data.ImageId = types.StringValue(value.String()) + } else { + data.ImageId = types.StringNull() + } + if value := res.Get("imageInfo.skip"); value.Exists() && !data.ImageSkip.IsNull() { + data.ImageSkip = types.BoolValue(value.Bool()) + } else { + data.ImageSkip = types.BoolNull() + } + if value := res.Get("configInfo.configId"); value.Exists() && !data.ConfigId.IsNull() { + data.ConfigId = types.StringValue(value.String()) + } else { + data.ConfigId = types.StringNull() + } + for i := range data.ConfigParameters { + keys := [...]string{"key"} + keyValues := [...]string{data.ConfigParameters[i].Name.ValueString()} + + var r gjson.Result + res.Get("configInfo.configParameters").ForEach( + func(_, v gjson.Result) bool { + found := false + for ik := range keys { + if v.Get(keys[ik]).String() == keyValues[ik] { + found = true + continue + } + found = false + break + } + if found { + r = v + return false + } + return true + }, + ) + if value := r.Get("key"); value.Exists() && !data.ConfigParameters[i].Name.IsNull() { + data.ConfigParameters[i].Name = types.StringValue(value.String()) + } else { + data.ConfigParameters[i].Name = types.StringNull() + } + if value := r.Get("value"); value.Exists() && !data.ConfigParameters[i].Value.IsNull() { + data.ConfigParameters[i].Value = types.StringValue(value.String()) + } else { + data.ConfigParameters[i].Value = types.StringNull() + } + } + if value := res.Get("rfProfile"); value.Exists() && !data.RfProfile.IsNull() { + data.RfProfile = types.StringValue(value.String()) + } else { + data.RfProfile = types.StringNull() + } + if value := res.Get("staticIP"); value.Exists() && !data.StaticIp.IsNull() { + data.StaticIp = types.StringValue(value.String()) + } else { + data.StaticIp = types.StringNull() + } + if value := res.Get("subnetMask"); value.Exists() && !data.SubnetMask.IsNull() { + data.SubnetMask = types.StringValue(value.String()) + } else { + data.SubnetMask = types.StringNull() + } + if value := res.Get("gateway"); value.Exists() && !data.Gateway.IsNull() { + data.Gateway = types.StringValue(value.String()) + } else { + data.Gateway = types.StringNull() + } + if value := res.Get("vlanId"); value.Exists() && !data.VlanId.IsNull() { + data.VlanId = types.StringValue(value.String()) + } else { + data.VlanId = types.StringNull() + } + if value := res.Get("ipInterfaceName"); value.Exists() && !data.IpInterfaceName.IsNull() { + data.IpInterfaceName = types.StringValue(value.String()) + } else { + data.IpInterfaceName = types.StringNull() + } + if value := res.Get("sensorProfile"); value.Exists() && !data.SensorProfile.IsNull() { + data.SensorProfile = types.StringValue(value.String()) + } else { + data.SensorProfile = types.StringNull() + } +} + +//template:end updateFromBody diff --git a/internal/provider/provider.go b/internal/provider/provider.go index df468dfa..a594690b 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -249,6 +249,7 @@ func (p *CcProvider) Resources(ctx context.Context) []func() resource.Resource { NewCredentialsSNMPv2ReadResource, NewCredentialsSNMPv2WriteResource, NewCredentialsSNMPv3Resource, + NewDeviceClaimSiteResource, NewFloorResource, NewIPPoolResource, NewIPPoolReservationResource, diff --git a/internal/provider/resource_catalystcenter_device_claim_site.go b/internal/provider/resource_catalystcenter_device_claim_site.go new file mode 100644 index 00000000..084e3274 --- /dev/null +++ b/internal/provider/resource_catalystcenter_device_claim_site.go @@ -0,0 +1,269 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// Licensed under the Mozilla Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://mozilla.org/MPL/2.0/ +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: MPL-2.0 + +// Code generated by "gen/generator.go"; DO NOT EDIT. + +package provider + +//template:begin imports +import ( + "context" + "fmt" + + "github.com/CiscoDevNet/terraform-provider-catalystcenter/internal/provider/helpers" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + cc "github.com/netascode/go-catalystcenter" +) + +//template:end imports + +//template:begin model + +// Ensure provider defined types fully satisfy framework interfaces +var _ resource.Resource = &DeviceClaimSiteResource{} + +func NewDeviceClaimSiteResource() resource.Resource { + return &DeviceClaimSiteResource{} +} + +type DeviceClaimSiteResource struct { + client *cc.Client +} + +func (r *DeviceClaimSiteResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_device_claim_site" +} + +func (r *DeviceClaimSiteResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + // This description is used by the documentation generator and the language server. + MarkdownDescription: helpers.NewAttributeDescription("This resource can manage a Device Claim Site.").String, + + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "The id of the object", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "device_id": schema.StringAttribute{ + MarkdownDescription: helpers.NewAttributeDescription("The device ID").String, + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "site_id": schema.StringAttribute{ + MarkdownDescription: helpers.NewAttributeDescription("The site ID").String, + Required: true, + }, + "type": schema.StringAttribute{ + MarkdownDescription: helpers.NewAttributeDescription("The device type").AddStringEnumDescription("Default", "StackSwitch", "AccessPoint", "Sensor", "CatalystWLC", "MobilityExpress").String, + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf("Default", "StackSwitch", "AccessPoint", "Sensor", "CatalystWLC", "MobilityExpress"), + }, + }, + "image_id": schema.StringAttribute{ + MarkdownDescription: helpers.NewAttributeDescription("Image ID. Required if `type` is `Default` or `StackSwitch`.").String, + Optional: true, + }, + "image_skip": schema.BoolAttribute{ + MarkdownDescription: helpers.NewAttributeDescription("Skip image provisioning.").String, + Optional: true, + }, + "config_id": schema.StringAttribute{ + MarkdownDescription: helpers.NewAttributeDescription("Config (temaplate) ID. Required if `type` is `Default` or `StackSwitch`.").String, + Optional: true, + }, + "config_parameters": schema.ListNestedAttribute{ + MarkdownDescription: helpers.NewAttributeDescription("List of config (temaplate) parameters.").String, + Optional: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + MarkdownDescription: helpers.NewAttributeDescription("Name of config parameter.").String, + Required: true, + }, + "value": schema.StringAttribute{ + MarkdownDescription: helpers.NewAttributeDescription("Value of config parameter.").String, + Optional: true, + }, + }, + }, + }, + "rf_profile": schema.StringAttribute{ + MarkdownDescription: helpers.NewAttributeDescription("RF profile. Required if `type` is `AccessPoint`.").String, + Optional: true, + }, + "static_ip": schema.StringAttribute{ + MarkdownDescription: helpers.NewAttributeDescription("Static IP address. Required if `type` is `CatalystWLC` or `MobilityExpress`.").String, + Optional: true, + }, + "subnet_mask": schema.StringAttribute{ + MarkdownDescription: helpers.NewAttributeDescription("Subnet mask. Required if `type` is `CatalystWLC` or `MobilityExpress`.").String, + Optional: true, + }, + "gateway": schema.StringAttribute{ + MarkdownDescription: helpers.NewAttributeDescription("Gateway IP. Required if `type` is `CatalystWLC` or `MobilityExpress`.").String, + Optional: true, + }, + "vlan_id": schema.StringAttribute{ + MarkdownDescription: helpers.NewAttributeDescription("Vlan ID. Required for Catalyst 9800 WLC.").String, + Optional: true, + }, + "ip_interface_name": schema.StringAttribute{ + MarkdownDescription: helpers.NewAttributeDescription("IP interface name. Required for Catalyst 9800 WLC.").String, + Optional: true, + }, + "sensor_profile": schema.StringAttribute{ + MarkdownDescription: helpers.NewAttributeDescription("Sensor profile. Required if `type` is `Sensor`.").String, + Optional: true, + }, + }, + } +} + +func (r *DeviceClaimSiteResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + r.client = req.ProviderData.(*CcProviderData).Client +} + +//template:end model + +//template:begin create +func (r *DeviceClaimSiteResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan DeviceClaimSite + + // Read plan + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, fmt.Sprintf("%s: Beginning Create", plan.Id.ValueString())) + + // Create object + body := plan.toBody(ctx, DeviceClaimSite{}) + + params := "" + res, err := r.client.Post(plan.getPath()+params, body) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to configure object (POST), got error: %s, %s", err, res.String())) + return + } + plan.Id = types.StringValue(fmt.Sprint(plan.DeviceId.ValueString())) + + tflog.Debug(ctx, fmt.Sprintf("%s: Create finished successfully", plan.Id.ValueString())) + + diags = resp.State.Set(ctx, &plan) + resp.Diagnostics.Append(diags...) +} + +//template:end create + +//template:begin read +func (r *DeviceClaimSiteResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state DeviceClaimSite + + // Read state + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, fmt.Sprintf("%s: Beginning Read", state.Id.String())) + + tflog.Debug(ctx, fmt.Sprintf("%s: Read finished successfully", state.Id.ValueString())) + + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) +} + +//template:end read + +//template:begin update +func (r *DeviceClaimSiteResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan, state DeviceClaimSite + + // Read plan + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + // Read state + diags = req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, fmt.Sprintf("%s: Beginning Update", plan.Id.ValueString())) + + body := plan.toBody(ctx, state) + params := "" + + res, err := r.client.Post(plan.getPath()+params, body) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to configure object (PUT), got error: %s, %s", err, res.String())) + return + } + + tflog.Debug(ctx, fmt.Sprintf("%s: Update finished successfully", plan.Id.ValueString())) + + diags = resp.State.Set(ctx, &plan) + resp.Diagnostics.Append(diags...) +} + +//template:end update + +//template:begin delete +func (r *DeviceClaimSiteResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state DeviceClaimSite + + // Read state + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, fmt.Sprintf("%s: Beginning Delete", state.Id.ValueString())) + + tflog.Debug(ctx, fmt.Sprintf("%s: Delete finished successfully", state.Id.ValueString())) + + resp.State.RemoveResource(ctx) +} + +//template:end delete + +//template:begin import +//template:end import diff --git a/internal/provider/resource_catalystcenter_device_claim_site_test.go b/internal/provider/resource_catalystcenter_device_claim_site_test.go new file mode 100644 index 00000000..9abd4e9f --- /dev/null +++ b/internal/provider/resource_catalystcenter_device_claim_site_test.go @@ -0,0 +1,98 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// Licensed under the Mozilla Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://mozilla.org/MPL/2.0/ +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: MPL-2.0 + +// Code generated by "gen/generator.go"; DO NOT EDIT. + +package provider + +//template:begin imports +import ( + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +//template:end imports + +//template:begin testAcc +func TestAccCcDeviceClaimSite(t *testing.T) { + if os.Getenv("PNP") == "" { + t.Skip("skipping test, set environment variable PNP") + } + var checks []resource.TestCheckFunc + checks = append(checks, resource.TestCheckResourceAttr("catalystcenter_device_claim_site.test", "device_id", "12345678-1234-1234-1234-123456789012")) + checks = append(checks, resource.TestCheckResourceAttr("catalystcenter_device_claim_site.test", "site_id", "12345678-1234-1234-1234-123456789012")) + checks = append(checks, resource.TestCheckResourceAttr("catalystcenter_device_claim_site.test", "type", "Default")) + checks = append(checks, resource.TestCheckResourceAttr("catalystcenter_device_claim_site.test", "image_id", "")) + checks = append(checks, resource.TestCheckResourceAttr("catalystcenter_device_claim_site.test", "image_skip", "true")) + checks = append(checks, resource.TestCheckResourceAttr("catalystcenter_device_claim_site.test", "config_id", "template1")) + checks = append(checks, resource.TestCheckResourceAttr("catalystcenter_device_claim_site.test", "config_parameters.0.name", "HOSTNAME")) + checks = append(checks, resource.TestCheckResourceAttr("catalystcenter_device_claim_site.test", "config_parameters.0.value", "switch1")) + + var steps []resource.TestStep + steps = append(steps, resource.TestStep{ + Config: testAccCcDeviceClaimSiteConfig_all(), + Check: resource.ComposeTestCheckFunc(checks...), + }) + steps = append(steps, resource.TestStep{ + ResourceName: "catalystcenter_device_claim_site.test", + ImportState: true, + }) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: steps, + }) +} + +//template:end testAcc + +//template:begin testPrerequisites +//template:end testPrerequisites + +//template:begin testAccConfigMinimal +func testAccCcDeviceClaimSiteConfig_minimum() string { + config := `resource "catalystcenter_device_claim_site" "test" {` + "\n" + config += ` device_id = "12345678-1234-1234-1234-123456789012"` + "\n" + config += ` site_id = "12345678-1234-1234-1234-123456789012"` + "\n" + config += ` type = "Default"` + "\n" + config += `}` + "\n" + return config +} + +//template:end testAccConfigMinimal + +//template:begin testAccConfigAll +func testAccCcDeviceClaimSiteConfig_all() string { + config := `resource "catalystcenter_device_claim_site" "test" {` + "\n" + config += ` device_id = "12345678-1234-1234-1234-123456789012"` + "\n" + config += ` site_id = "12345678-1234-1234-1234-123456789012"` + "\n" + config += ` type = "Default"` + "\n" + config += ` image_id = ""` + "\n" + config += ` image_skip = true` + "\n" + config += ` config_id = "template1"` + "\n" + config += ` config_parameters = [{` + "\n" + config += ` name = "HOSTNAME"` + "\n" + config += ` value = "switch1"` + "\n" + config += ` }]` + "\n" + config += `}` + "\n" + return config +} + +//template:end testAccConfigAll diff --git a/templates/guides/changelog.md.tmpl b/templates/guides/changelog.md.tmpl index 0b71e474..0f03d68a 100644 --- a/templates/guides/changelog.md.tmpl +++ b/templates/guides/changelog.md.tmpl @@ -10,6 +10,7 @@ description: |- ## 0.1.2 (unreleased) - Fix issue with `catalystcenter_ip_pool_reservation` resource and multiple IP pools under a single site +- Add `catalystcenter_device_claim_site` resource ## 0.1.1