diff --git a/docs/data-sources/switch_stack_routing_interface.md b/docs/data-sources/switch_stack_routing_interface.md new file mode 100644 index 0000000..ec9dd89 --- /dev/null +++ b/docs/data-sources/switch_stack_routing_interface.md @@ -0,0 +1,49 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "meraki_switch_stack_routing_interface Data Source - terraform-provider-meraki" +subcategory: "Switches" +description: |- + This data source can read the Switch Stack Routing Interface configuration. +--- + +# meraki_switch_stack_routing_interface (Data Source) + +This data source can read the `Switch Stack Routing Interface` configuration. + +## Example Usage + +```terraform +data "meraki_switch_stack_routing_interface" "example" { + id = "12345678" + network_id = "L_123456" + switch_stack_id = "1234" +} +``` + + +## Schema + +### Required + +- `network_id` (String) Network ID +- `switch_stack_id` (String) Switch stack ID + +### Optional + +- `id` (String) The id of the object +- `name` (String) A friendly name or description for the interface or VLAN. + +### Read-Only + +- `default_gateway` (String) The next hop for any traffic that isn`t going to a directly connected subnet or over a static route. This IP address must exist in a subnet with a routed interface. +- `interface_ip` (String) The IP address this switch stack will use for layer 3 routing on this VLAN or subnet. This cannot be the same as the switch`s management IP. +- `ipv6_address` (String) The IPv6 address of the interface. Required if assignmentMode is `static`. Must not be included if assignmentMode is `eui-64`. +- `ipv6_assignment_mode` (String) The IPv6 assignment mode for the interface. Can be either `eui-64` or `static`. +- `ipv6_gateway` (String) The IPv6 default gateway of the interface. Required if prefix is defined and this is the first interface with IPv6 configured for the stack. +- `ipv6_prefix` (String) The IPv6 prefix of the interface. Required if IPv6 object is included. +- `multicast_routing` (String) Enable multicast support if, multicast routing between VLANs is required. Options are, `disabled`, `enabled` or `IGMP snooping querier`. Default is `disabled`. +- `ospf_settings_area` (String) The OSPF area to which this interface should belong. Can be either `disabled` or the identifier of an existing OSPF area. Defaults to `disabled`. +- `ospf_settings_cost` (Number) The path cost for this interface. Defaults to 1, but can be increased up to 65535 to give lower priority. +- `ospf_settings_is_passive_enabled` (Boolean) When enabled, OSPF will not run on the interface, but the subnet will still be advertised. +- `subnet` (String) The network that this routed interface is on, in CIDR notation (ex. 10.1.1.0/24). +- `vlan_id` (Number) The VLAN this routed interface is on. VLAN must be between 1 and 4094. diff --git a/docs/resources/switch_stack_routing_interface.md b/docs/resources/switch_stack_routing_interface.md new file mode 100644 index 0000000..4675a3a --- /dev/null +++ b/docs/resources/switch_stack_routing_interface.md @@ -0,0 +1,70 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "meraki_switch_stack_routing_interface Resource - terraform-provider-meraki" +subcategory: "Switches" +description: |- + This resource can manage the Switch Stack Routing Interface configuration. +--- + +# meraki_switch_stack_routing_interface (Resource) + +This resource can manage the `Switch Stack Routing Interface` configuration. + +## Example Usage + +```terraform +resource "meraki_switch_stack_routing_interface" "example" { + network_id = "L_123456" + switch_stack_id = "1234" + default_gateway = "192.168.1.1" + interface_ip = "192.168.1.2" + multicast_routing = "disabled" + name = "L3 interface" + subnet = "192.168.1.0/24" + vlan_id = 100 + ipv6_address = "1:2:3:4::1" + ipv6_assignment_mode = "static" + ipv6_gateway = "1:2:3:4::2" + ipv6_prefix = "1:2:3:4::/64" + ospf_settings_area = "0" + ospf_settings_cost = 1 + ospf_settings_is_passive_enabled = true +} +``` + + +## Schema + +### Required + +- `name` (String) A friendly name or description for the interface or VLAN. +- `network_id` (String) Network ID +- `switch_stack_id` (String) Switch stack ID +- `vlan_id` (Number) The VLAN this routed interface is on. VLAN must be between 1 and 4094. + +### Optional + +- `default_gateway` (String) The next hop for any traffic that isn`t going to a directly connected subnet or over a static route. This IP address must exist in a subnet with a routed interface. +- `interface_ip` (String) The IP address this switch stack will use for layer 3 routing on this VLAN or subnet. This cannot be the same as the switch`s management IP. +- `ipv6_address` (String) The IPv6 address of the interface. Required if assignmentMode is `static`. Must not be included if assignmentMode is `eui-64`. +- `ipv6_assignment_mode` (String) The IPv6 assignment mode for the interface. Can be either `eui-64` or `static`. +- `ipv6_gateway` (String) The IPv6 default gateway of the interface. Required if prefix is defined and this is the first interface with IPv6 configured for the stack. +- `ipv6_prefix` (String) The IPv6 prefix of the interface. Required if IPv6 object is included. +- `multicast_routing` (String) Enable multicast support if, multicast routing between VLANs is required. Options are, `disabled`, `enabled` or `IGMP snooping querier`. Default is `disabled`. + - Choices: `IGMP snooping querier`, `disabled`, `enabled` +- `ospf_settings_area` (String) The OSPF area to which this interface should belong. Can be either `disabled` or the identifier of an existing OSPF area. Defaults to `disabled`. +- `ospf_settings_cost` (Number) The path cost for this interface. Defaults to 1, but can be increased up to 65535 to give lower priority. +- `ospf_settings_is_passive_enabled` (Boolean) When enabled, OSPF will not run on the interface, but the subnet will still be advertised. +- `subnet` (String) The network that this routed interface is on, in CIDR notation (ex. 10.1.1.0/24). + +### Read-Only + +- `id` (String) The id of the object + +## Import + +Import is supported using the following syntax: + +```shell +terraform import meraki_switch_stack_routing_interface.example ",," +``` diff --git a/examples/data-sources/meraki_switch_stack_routing_interface/data-source.tf b/examples/data-sources/meraki_switch_stack_routing_interface/data-source.tf new file mode 100644 index 0000000..db40ece --- /dev/null +++ b/examples/data-sources/meraki_switch_stack_routing_interface/data-source.tf @@ -0,0 +1,5 @@ +data "meraki_switch_stack_routing_interface" "example" { + id = "12345678" + network_id = "L_123456" + switch_stack_id = "1234" +} diff --git a/examples/resources/meraki_switch_stack_routing_interface/import.sh b/examples/resources/meraki_switch_stack_routing_interface/import.sh new file mode 100644 index 0000000..08ae863 --- /dev/null +++ b/examples/resources/meraki_switch_stack_routing_interface/import.sh @@ -0,0 +1 @@ +terraform import meraki_switch_stack_routing_interface.example ",," diff --git a/examples/resources/meraki_switch_stack_routing_interface/resource.tf b/examples/resources/meraki_switch_stack_routing_interface/resource.tf new file mode 100644 index 0000000..dab1243 --- /dev/null +++ b/examples/resources/meraki_switch_stack_routing_interface/resource.tf @@ -0,0 +1,17 @@ +resource "meraki_switch_stack_routing_interface" "example" { + network_id = "L_123456" + switch_stack_id = "1234" + default_gateway = "192.168.1.1" + interface_ip = "192.168.1.2" + multicast_routing = "disabled" + name = "L3 interface" + subnet = "192.168.1.0/24" + vlan_id = 100 + ipv6_address = "1:2:3:4::1" + ipv6_assignment_mode = "static" + ipv6_gateway = "1:2:3:4::2" + ipv6_prefix = "1:2:3:4::/64" + ospf_settings_area = "0" + ospf_settings_cost = 1 + ospf_settings_is_passive_enabled = true +} diff --git a/gen/definitions/switch_stack_routing_interface.yaml b/gen/definitions/switch_stack_routing_interface.yaml new file mode 100644 index 0000000..170a225 --- /dev/null +++ b/gen/definitions/switch_stack_routing_interface.yaml @@ -0,0 +1,105 @@ +name: Switch Stack Routing Interface +rest_endpoint: /networks/%v/switch/stacks/%v/routing/interfaces +id_name: interfaceId +data_source_name_query: true +doc_category: Switches +attributes: + - tf_name: network_id + type: String + reference: true + description: Network ID + example: L_123456 + test_value: meraki_network.test.id + - tf_name: switch_stack_id + type: String + reference: true + description: Switch stack ID + example: "1234" + test_value: meraki_switch_stack.test.id + - model_name: defaultGateway + type: String + write_changes_only: true + description: The next hop for any traffic that isn`t going to a directly connected subnet or over a static route. This IP address must exist in a subnet with a routed interface. + example: 192.168.1.1 + minimum_test_value: '"192.168.1.1"' + - model_name: interfaceIp + type: String + description: The IP address this switch stack will use for layer 3 routing on this VLAN or subnet. This cannot be the same as the switch`s management IP. + example: 192.168.1.2 + minimum_test_value: '"192.168.1.2"' + - model_name: multicastRouting + type: String + description: Enable multicast support if, multicast routing between VLANs is required. Options are, `disabled`, `enabled` or `IGMP snooping querier`. Default is `disabled`. + example: disabled + enum_values: [IGMP snooping querier, disabled, enabled] + - model_name: name + type: String + mandatory: true + description: A friendly name or description for the interface or VLAN. + example: L3 interface + - model_name: subnet + type: String + description: The network that this routed interface is on, in CIDR notation (ex. 10.1.1.0/24). + example: 192.168.1.0/24 + minimum_test_value: '"192.168.1.0/24"' + - model_name: vlanId + type: Int64 + mandatory: true + description: The VLAN this routed interface is on. VLAN must be between 1 and 4094. + example: "100" + - model_name: address + type: String + data_path: [ipv6] + description: The IPv6 address of the interface. Required if assignmentMode is `static`. Must not be included if assignmentMode is `eui-64`. + example: 1:2:3:4::1 + - model_name: assignmentMode + type: String + data_path: [ipv6] + description: The IPv6 assignment mode for the interface. Can be either `eui-64` or `static`. + example: static + - model_name: gateway + type: String + data_path: [ipv6] + description: The IPv6 default gateway of the interface. Required if prefix is defined and this is the first interface with IPv6 configured for the stack. + example: 1:2:3:4::2 + - model_name: prefix + type: String + data_path: [ipv6] + description: The IPv6 prefix of the interface. Required if IPv6 object is included. + example: 1:2:3:4::/64 + - model_name: area + type: String + data_path: [ospfSettings] + exclude_test: true + description: The OSPF area to which this interface should belong. Can be either `disabled` or the identifier of an existing OSPF area. Defaults to `disabled`. + example: "0" + - model_name: cost + type: Int64 + data_path: [ospfSettings] + exclude_test: true + description: The path cost for this interface. Defaults to 1, but can be increased up to 65535 to give lower priority. + example: "1" + - model_name: isPassiveEnabled + type: Bool + data_path: [ospfSettings] + exclude_test: true + description: When enabled, OSPF will not run on the interface, but the subnet will still be advertised. + example: "true" +test_prerequisites: | + data "meraki_organization" "test" { + name = "Dev" + } + resource "meraki_network" "test" { + organization_id = data.meraki_organization.test.id + name = "Network1" + product_types = ["switch", "wireless"] + } + resource "meraki_network_device_claim" "test" { + network_id = meraki_network.test.id + serials = ["Q5KD-PCG4-HB8R", "Q5KD-CU8N-DEDR"] + } + resource "meraki_switch_stack" "test" { + network_id = meraki_network.test.id + name = "A cool stack" + serials = meraki_network_device_claim.test.serials + } diff --git a/internal/provider/data_source_meraki_switch_stack_routing_interface.go b/internal/provider/data_source_meraki_switch_stack_routing_interface.go new file mode 100644 index 0000000..c62451b --- /dev/null +++ b/internal/provider/data_source_meraki_switch_stack_routing_interface.go @@ -0,0 +1,208 @@ +// Copyright © 2024 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 + +package provider + +// Section below is generated&owned by "gen/generator.go". //template:begin imports +import ( + "context" + "fmt" + "net/url" + + "github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/netascode/go-meraki" + "github.com/tidwall/gjson" +) + +// End of section. //template:end imports + +// Section below is generated&owned by "gen/generator.go". //template:begin model + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &SwitchStackRoutingInterfaceDataSource{} + _ datasource.DataSourceWithConfigure = &SwitchStackRoutingInterfaceDataSource{} +) + +func NewSwitchStackRoutingInterfaceDataSource() datasource.DataSource { + return &SwitchStackRoutingInterfaceDataSource{} +} + +type SwitchStackRoutingInterfaceDataSource struct { + client *meraki.Client +} + +func (d *SwitchStackRoutingInterfaceDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_switch_stack_routing_interface" +} + +func (d *SwitchStackRoutingInterfaceDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + // This description is used by the documentation generator and the language server. + MarkdownDescription: "This data source can read the `Switch Stack Routing Interface` configuration.", + + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "The id of the object", + Optional: true, + Computed: true, + }, + "network_id": schema.StringAttribute{ + MarkdownDescription: "Network ID", + Required: true, + }, + "switch_stack_id": schema.StringAttribute{ + MarkdownDescription: "Switch stack ID", + Required: true, + }, + "default_gateway": schema.StringAttribute{ + MarkdownDescription: "The next hop for any traffic that isn`t going to a directly connected subnet or over a static route. This IP address must exist in a subnet with a routed interface.", + Computed: true, + }, + "interface_ip": schema.StringAttribute{ + MarkdownDescription: "The IP address this switch stack will use for layer 3 routing on this VLAN or subnet. This cannot be the same as the switch`s management IP.", + Computed: true, + }, + "multicast_routing": schema.StringAttribute{ + MarkdownDescription: "Enable multicast support if, multicast routing between VLANs is required. Options are, `disabled`, `enabled` or `IGMP snooping querier`. Default is `disabled`.", + Computed: true, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "A friendly name or description for the interface or VLAN.", + Optional: true, + Computed: true, + }, + "subnet": schema.StringAttribute{ + MarkdownDescription: "The network that this routed interface is on, in CIDR notation (ex. 10.1.1.0/24).", + Computed: true, + }, + "vlan_id": schema.Int64Attribute{ + MarkdownDescription: "The VLAN this routed interface is on. VLAN must be between 1 and 4094.", + Computed: true, + }, + "ipv6_address": schema.StringAttribute{ + MarkdownDescription: "The IPv6 address of the interface. Required if assignmentMode is `static`. Must not be included if assignmentMode is `eui-64`.", + Computed: true, + }, + "ipv6_assignment_mode": schema.StringAttribute{ + MarkdownDescription: "The IPv6 assignment mode for the interface. Can be either `eui-64` or `static`.", + Computed: true, + }, + "ipv6_gateway": schema.StringAttribute{ + MarkdownDescription: "The IPv6 default gateway of the interface. Required if prefix is defined and this is the first interface with IPv6 configured for the stack.", + Computed: true, + }, + "ipv6_prefix": schema.StringAttribute{ + MarkdownDescription: "The IPv6 prefix of the interface. Required if IPv6 object is included.", + Computed: true, + }, + "ospf_settings_area": schema.StringAttribute{ + MarkdownDescription: "The OSPF area to which this interface should belong. Can be either `disabled` or the identifier of an existing OSPF area. Defaults to `disabled`.", + Computed: true, + }, + "ospf_settings_cost": schema.Int64Attribute{ + MarkdownDescription: "The path cost for this interface. Defaults to 1, but can be increased up to 65535 to give lower priority.", + Computed: true, + }, + "ospf_settings_is_passive_enabled": schema.BoolAttribute{ + MarkdownDescription: "When enabled, OSPF will not run on the interface, but the subnet will still be advertised.", + Computed: true, + }, + }, + } +} +func (d *SwitchStackRoutingInterfaceDataSource) ConfigValidators(ctx context.Context) []datasource.ConfigValidator { + return []datasource.ConfigValidator{ + datasourcevalidator.ExactlyOneOf( + path.MatchRoot("id"), + path.MatchRoot("name"), + ), + } +} + +func (d *SwitchStackRoutingInterfaceDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, _ *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + d.client = req.ProviderData.(*MerakiProviderData).Client +} + +// End of section. //template:end model + +// Section below is generated&owned by "gen/generator.go". //template:begin read + +func (d *SwitchStackRoutingInterfaceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var config SwitchStackRoutingInterface + + // Read config + diags := req.Config.Get(ctx, &config) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, fmt.Sprintf("%s: Beginning Read", config.Id.String())) + + var res gjson.Result + var err error + if config.Id.IsNull() && !config.Name.IsNull() { + res, err = d.client.Get(config.getPath()) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to retrieve objects, got error: %s", err)) + return + } + if len(res.Array()) > 0 { + res.ForEach(func(k, v gjson.Result) bool { + if config.Name.ValueString() == v.Get("name").String() { + config.Id = types.StringValue(v.Get("interfaceId").String()) + tflog.Debug(ctx, fmt.Sprintf("%s: Found object with name '%v', id: %v", config.Id.String(), config.Name.ValueString(), config.Id.String())) + res = v + return false + } + return true + }) + } + + if config.Id.IsNull() { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to find object with name: %s", config.Name.ValueString())) + return + } + } + + if !res.Exists() { + res, err = d.client.Get(config.getPath() + "/" + url.QueryEscape(config.Id.ValueString())) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to retrieve object, got error: %s", err)) + return + } + } + + config.fromBody(ctx, res) + + tflog.Debug(ctx, fmt.Sprintf("%s: Read finished successfully", config.Id.ValueString())) + + diags = resp.State.Set(ctx, &config) + resp.Diagnostics.Append(diags...) +} + +// End of section. //template:end read diff --git a/internal/provider/data_source_meraki_switch_stack_routing_interface_test.go b/internal/provider/data_source_meraki_switch_stack_routing_interface_test.go new file mode 100644 index 0000000..b5de331 --- /dev/null +++ b/internal/provider/data_source_meraki_switch_stack_routing_interface_test.go @@ -0,0 +1,140 @@ +// Copyright © 2024 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 + +package provider + +// Section below is generated&owned by "gen/generator.go". //template:begin imports +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +// End of section. //template:end imports + +// Section below is generated&owned by "gen/generator.go". //template:begin testAccDataSource + +func TestAccDataSourceMerakiSwitchStackRoutingInterface(t *testing.T) { + var checks []resource.TestCheckFunc + checks = append(checks, resource.TestCheckResourceAttr("data.meraki_switch_stack_routing_interface.test", "default_gateway", "192.168.1.1")) + checks = append(checks, resource.TestCheckResourceAttr("data.meraki_switch_stack_routing_interface.test", "interface_ip", "192.168.1.2")) + checks = append(checks, resource.TestCheckResourceAttr("data.meraki_switch_stack_routing_interface.test", "multicast_routing", "disabled")) + checks = append(checks, resource.TestCheckResourceAttr("data.meraki_switch_stack_routing_interface.test", "name", "L3 interface")) + checks = append(checks, resource.TestCheckResourceAttr("data.meraki_switch_stack_routing_interface.test", "subnet", "192.168.1.0/24")) + checks = append(checks, resource.TestCheckResourceAttr("data.meraki_switch_stack_routing_interface.test", "vlan_id", "100")) + checks = append(checks, resource.TestCheckResourceAttr("data.meraki_switch_stack_routing_interface.test", "ipv6_address", "1:2:3:4::1")) + checks = append(checks, resource.TestCheckResourceAttr("data.meraki_switch_stack_routing_interface.test", "ipv6_assignment_mode", "static")) + checks = append(checks, resource.TestCheckResourceAttr("data.meraki_switch_stack_routing_interface.test", "ipv6_gateway", "1:2:3:4::2")) + checks = append(checks, resource.TestCheckResourceAttr("data.meraki_switch_stack_routing_interface.test", "ipv6_prefix", "1:2:3:4::/64")) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceMerakiSwitchStackRoutingInterfacePrerequisitesConfig + testAccDataSourceMerakiSwitchStackRoutingInterfaceConfig(), + Check: resource.ComposeTestCheckFunc(checks...), + }, + { + Config: testAccDataSourceMerakiSwitchStackRoutingInterfacePrerequisitesConfig + testAccNamedDataSourceMerakiSwitchStackRoutingInterfaceConfig(), + Check: resource.ComposeTestCheckFunc(checks...), + }, + }, + }) +} + +// End of section. //template:end testAccDataSource + +// Section below is generated&owned by "gen/generator.go". //template:begin testPrerequisites + +const testAccDataSourceMerakiSwitchStackRoutingInterfacePrerequisitesConfig = ` +data "meraki_organization" "test" { + name = "Dev" +} +resource "meraki_network" "test" { + organization_id = data.meraki_organization.test.id + name = "Network1" + product_types = ["switch", "wireless"] +} +resource "meraki_network_device_claim" "test" { + network_id = meraki_network.test.id + serials = ["Q5KD-PCG4-HB8R", "Q5KD-CU8N-DEDR"] +} +resource "meraki_switch_stack" "test" { + network_id = meraki_network.test.id + name = "A cool stack" + serials = meraki_network_device_claim.test.serials +} + +` + +// End of section. //template:end testPrerequisites + +// Section below is generated&owned by "gen/generator.go". //template:begin testAccDataSourceConfig + +func testAccDataSourceMerakiSwitchStackRoutingInterfaceConfig() string { + config := `resource "meraki_switch_stack_routing_interface" "test" {` + "\n" + config += ` network_id = meraki_network.test.id` + "\n" + config += ` switch_stack_id = meraki_switch_stack.test.id` + "\n" + config += ` default_gateway = "192.168.1.1"` + "\n" + config += ` interface_ip = "192.168.1.2"` + "\n" + config += ` multicast_routing = "disabled"` + "\n" + config += ` name = "L3 interface"` + "\n" + config += ` subnet = "192.168.1.0/24"` + "\n" + config += ` vlan_id = 100` + "\n" + config += ` ipv6_address = "1:2:3:4::1"` + "\n" + config += ` ipv6_assignment_mode = "static"` + "\n" + config += ` ipv6_gateway = "1:2:3:4::2"` + "\n" + config += ` ipv6_prefix = "1:2:3:4::/64"` + "\n" + config += `}` + "\n" + + config += ` + data "meraki_switch_stack_routing_interface" "test" { + id = meraki_switch_stack_routing_interface.test.id + network_id = meraki_network.test.id + switch_stack_id = meraki_switch_stack.test.id + } + ` + return config +} + +func testAccNamedDataSourceMerakiSwitchStackRoutingInterfaceConfig() string { + config := `resource "meraki_switch_stack_routing_interface" "test" {` + "\n" + config += ` network_id = meraki_network.test.id` + "\n" + config += ` switch_stack_id = meraki_switch_stack.test.id` + "\n" + config += ` default_gateway = "192.168.1.1"` + "\n" + config += ` interface_ip = "192.168.1.2"` + "\n" + config += ` multicast_routing = "disabled"` + "\n" + config += ` name = "L3 interface"` + "\n" + config += ` subnet = "192.168.1.0/24"` + "\n" + config += ` vlan_id = 100` + "\n" + config += ` ipv6_address = "1:2:3:4::1"` + "\n" + config += ` ipv6_assignment_mode = "static"` + "\n" + config += ` ipv6_gateway = "1:2:3:4::2"` + "\n" + config += ` ipv6_prefix = "1:2:3:4::/64"` + "\n" + config += `}` + "\n" + + config += ` + data "meraki_switch_stack_routing_interface" "test" { + name = meraki_switch_stack_routing_interface.test.name + network_id = meraki_network.test.id + switch_stack_id = meraki_switch_stack.test.id + } + ` + return config +} + +// End of section. //template:end testAccDataSourceConfig diff --git a/internal/provider/model_meraki_switch_stack_routing_interface.go b/internal/provider/model_meraki_switch_stack_routing_interface.go new file mode 100644 index 0000000..dbebd86 --- /dev/null +++ b/internal/provider/model_meraki_switch_stack_routing_interface.go @@ -0,0 +1,258 @@ +// Copyright © 2024 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 + +package provider + +// Section below is generated&owned by "gen/generator.go". //template:begin imports +import ( + "context" + "fmt" + "net/url" + + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" +) + +// End of section. //template:end imports + +// Section below is generated&owned by "gen/generator.go". //template:begin types + +type SwitchStackRoutingInterface struct { + Id types.String `tfsdk:"id"` + NetworkId types.String `tfsdk:"network_id"` + SwitchStackId types.String `tfsdk:"switch_stack_id"` + DefaultGateway types.String `tfsdk:"default_gateway"` + InterfaceIp types.String `tfsdk:"interface_ip"` + MulticastRouting types.String `tfsdk:"multicast_routing"` + Name types.String `tfsdk:"name"` + Subnet types.String `tfsdk:"subnet"` + VlanId types.Int64 `tfsdk:"vlan_id"` + Ipv6Address types.String `tfsdk:"ipv6_address"` + Ipv6AssignmentMode types.String `tfsdk:"ipv6_assignment_mode"` + Ipv6Gateway types.String `tfsdk:"ipv6_gateway"` + Ipv6Prefix types.String `tfsdk:"ipv6_prefix"` + OspfSettingsArea types.String `tfsdk:"ospf_settings_area"` + OspfSettingsCost types.Int64 `tfsdk:"ospf_settings_cost"` + OspfSettingsIsPassiveEnabled types.Bool `tfsdk:"ospf_settings_is_passive_enabled"` +} + +// End of section. //template:end types + +// Section below is generated&owned by "gen/generator.go". //template:begin getPath + +func (data SwitchStackRoutingInterface) getPath() string { + return fmt.Sprintf("/networks/%v/switch/stacks/%v/routing/interfaces", url.QueryEscape(data.NetworkId.ValueString()), url.QueryEscape(data.SwitchStackId.ValueString())) +} + +// End of section. //template:end getPath + +// Section below is generated&owned by "gen/generator.go". //template:begin toBody + +func (data SwitchStackRoutingInterface) toBody(ctx context.Context, state SwitchStackRoutingInterface) string { + body := "" + if !data.DefaultGateway.IsNull() && data.DefaultGateway != state.DefaultGateway { + body, _ = sjson.Set(body, "defaultGateway", data.DefaultGateway.ValueString()) + } + if !data.InterfaceIp.IsNull() { + body, _ = sjson.Set(body, "interfaceIp", data.InterfaceIp.ValueString()) + } + if !data.MulticastRouting.IsNull() { + body, _ = sjson.Set(body, "multicastRouting", data.MulticastRouting.ValueString()) + } + if !data.Name.IsNull() { + body, _ = sjson.Set(body, "name", data.Name.ValueString()) + } + if !data.Subnet.IsNull() { + body, _ = sjson.Set(body, "subnet", data.Subnet.ValueString()) + } + if !data.VlanId.IsNull() { + body, _ = sjson.Set(body, "vlanId", data.VlanId.ValueInt64()) + } + if !data.Ipv6Address.IsNull() { + body, _ = sjson.Set(body, "ipv6.address", data.Ipv6Address.ValueString()) + } + if !data.Ipv6AssignmentMode.IsNull() { + body, _ = sjson.Set(body, "ipv6.assignmentMode", data.Ipv6AssignmentMode.ValueString()) + } + if !data.Ipv6Gateway.IsNull() { + body, _ = sjson.Set(body, "ipv6.gateway", data.Ipv6Gateway.ValueString()) + } + if !data.Ipv6Prefix.IsNull() { + body, _ = sjson.Set(body, "ipv6.prefix", data.Ipv6Prefix.ValueString()) + } + if !data.OspfSettingsArea.IsNull() { + body, _ = sjson.Set(body, "ospfSettings.area", data.OspfSettingsArea.ValueString()) + } + if !data.OspfSettingsCost.IsNull() { + body, _ = sjson.Set(body, "ospfSettings.cost", data.OspfSettingsCost.ValueInt64()) + } + if !data.OspfSettingsIsPassiveEnabled.IsNull() { + body, _ = sjson.Set(body, "ospfSettings.isPassiveEnabled", data.OspfSettingsIsPassiveEnabled.ValueBool()) + } + return body +} + +// End of section. //template:end toBody + +// Section below is generated&owned by "gen/generator.go". //template:begin fromBody + +func (data *SwitchStackRoutingInterface) fromBody(ctx context.Context, res gjson.Result) { + if value := res.Get("defaultGateway"); value.Exists() { + data.DefaultGateway = types.StringValue(value.String()) + } else { + data.DefaultGateway = types.StringNull() + } + if value := res.Get("interfaceIp"); value.Exists() { + data.InterfaceIp = types.StringValue(value.String()) + } else { + data.InterfaceIp = types.StringNull() + } + if value := res.Get("multicastRouting"); value.Exists() { + data.MulticastRouting = types.StringValue(value.String()) + } else { + data.MulticastRouting = types.StringNull() + } + if value := res.Get("name"); value.Exists() { + data.Name = types.StringValue(value.String()) + } else { + data.Name = types.StringNull() + } + if value := res.Get("subnet"); value.Exists() { + data.Subnet = types.StringValue(value.String()) + } else { + data.Subnet = types.StringNull() + } + if value := res.Get("vlanId"); value.Exists() { + data.VlanId = types.Int64Value(value.Int()) + } else { + data.VlanId = types.Int64Null() + } + if value := res.Get("ipv6.address"); value.Exists() { + data.Ipv6Address = types.StringValue(value.String()) + } else { + data.Ipv6Address = types.StringNull() + } + if value := res.Get("ipv6.assignmentMode"); value.Exists() { + data.Ipv6AssignmentMode = types.StringValue(value.String()) + } else { + data.Ipv6AssignmentMode = types.StringNull() + } + if value := res.Get("ipv6.gateway"); value.Exists() { + data.Ipv6Gateway = types.StringValue(value.String()) + } else { + data.Ipv6Gateway = types.StringNull() + } + if value := res.Get("ipv6.prefix"); value.Exists() { + data.Ipv6Prefix = types.StringValue(value.String()) + } else { + data.Ipv6Prefix = types.StringNull() + } + if value := res.Get("ospfSettings.area"); value.Exists() { + data.OspfSettingsArea = types.StringValue(value.String()) + } else { + data.OspfSettingsArea = types.StringNull() + } + if value := res.Get("ospfSettings.cost"); value.Exists() { + data.OspfSettingsCost = types.Int64Value(value.Int()) + } else { + data.OspfSettingsCost = types.Int64Null() + } + if value := res.Get("ospfSettings.isPassiveEnabled"); value.Exists() { + data.OspfSettingsIsPassiveEnabled = types.BoolValue(value.Bool()) + } else { + data.OspfSettingsIsPassiveEnabled = types.BoolNull() + } +} + +// End of section. //template:end fromBody + +// Section below is generated&owned by "gen/generator.go". //template:begin fromBodyPartial + +// fromBodyPartial reads values from a gjson.Result into a tfstate model. It ignores null attributes in order to +// uncouple the provider from the exact values that the backend API might summon to replace nulls. (Such behavior might +// easily change across versions of the backend API.) For List/Set/Map attributes, the func only updates the +// "managed" elements, instead of all elements. +func (data *SwitchStackRoutingInterface) fromBodyPartial(ctx context.Context, res gjson.Result) { + if value := res.Get("defaultGateway"); value.Exists() && !data.DefaultGateway.IsNull() { + data.DefaultGateway = types.StringValue(value.String()) + } else { + data.DefaultGateway = types.StringNull() + } + if value := res.Get("interfaceIp"); value.Exists() && !data.InterfaceIp.IsNull() { + data.InterfaceIp = types.StringValue(value.String()) + } else { + data.InterfaceIp = types.StringNull() + } + if value := res.Get("multicastRouting"); value.Exists() && !data.MulticastRouting.IsNull() { + data.MulticastRouting = types.StringValue(value.String()) + } else { + data.MulticastRouting = types.StringNull() + } + if value := res.Get("name"); value.Exists() && !data.Name.IsNull() { + data.Name = types.StringValue(value.String()) + } else { + data.Name = types.StringNull() + } + if value := res.Get("subnet"); value.Exists() && !data.Subnet.IsNull() { + data.Subnet = types.StringValue(value.String()) + } else { + data.Subnet = types.StringNull() + } + if value := res.Get("vlanId"); value.Exists() && !data.VlanId.IsNull() { + data.VlanId = types.Int64Value(value.Int()) + } else { + data.VlanId = types.Int64Null() + } + if value := res.Get("ipv6.address"); value.Exists() && !data.Ipv6Address.IsNull() { + data.Ipv6Address = types.StringValue(value.String()) + } else { + data.Ipv6Address = types.StringNull() + } + if value := res.Get("ipv6.assignmentMode"); value.Exists() && !data.Ipv6AssignmentMode.IsNull() { + data.Ipv6AssignmentMode = types.StringValue(value.String()) + } else { + data.Ipv6AssignmentMode = types.StringNull() + } + if value := res.Get("ipv6.gateway"); value.Exists() && !data.Ipv6Gateway.IsNull() { + data.Ipv6Gateway = types.StringValue(value.String()) + } else { + data.Ipv6Gateway = types.StringNull() + } + if value := res.Get("ipv6.prefix"); value.Exists() && !data.Ipv6Prefix.IsNull() { + data.Ipv6Prefix = types.StringValue(value.String()) + } else { + data.Ipv6Prefix = types.StringNull() + } + if value := res.Get("ospfSettings.area"); value.Exists() && !data.OspfSettingsArea.IsNull() { + data.OspfSettingsArea = types.StringValue(value.String()) + } else { + data.OspfSettingsArea = types.StringNull() + } + if value := res.Get("ospfSettings.cost"); value.Exists() && !data.OspfSettingsCost.IsNull() { + data.OspfSettingsCost = types.Int64Value(value.Int()) + } else { + data.OspfSettingsCost = types.Int64Null() + } + if value := res.Get("ospfSettings.isPassiveEnabled"); value.Exists() && !data.OspfSettingsIsPassiveEnabled.IsNull() { + data.OspfSettingsIsPassiveEnabled = types.BoolValue(value.Bool()) + } else { + data.OspfSettingsIsPassiveEnabled = types.BoolNull() + } +} + +// End of section. //template:end fromBodyPartial diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 4b82646..10f09b1 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -245,6 +245,7 @@ func (p *MerakiProvider) Resources(ctx context.Context) []func() resource.Resour NewSwitchRoutingOSPFResource, NewSwitchSettingsResource, NewSwitchStackResource, + NewSwitchStackRoutingInterfaceResource, NewSwitchStormControlResource, NewSwitchSTPResource, } @@ -278,6 +279,7 @@ func (p *MerakiProvider) DataSources(ctx context.Context) []func() datasource.Da NewSwitchRoutingOSPFDataSource, NewSwitchSettingsDataSource, NewSwitchStackDataSource, + NewSwitchStackRoutingInterfaceDataSource, NewSwitchStormControlDataSource, NewSwitchSTPDataSource, } diff --git a/internal/provider/resource_meraki_switch_stack_routing_interface.go b/internal/provider/resource_meraki_switch_stack_routing_interface.go new file mode 100644 index 0000000..1051b16 --- /dev/null +++ b/internal/provider/resource_meraki_switch_stack_routing_interface.go @@ -0,0 +1,310 @@ +// Copyright © 2024 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 + +package provider + +// Section below is generated&owned by "gen/generator.go". //template:begin imports +import ( + "context" + "fmt" + "net/url" + "strings" + + "github.com/CiscoDevNet/terraform-provider-meraki/internal/provider/helpers" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "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" + "github.com/netascode/go-meraki" +) + +// End of section. //template:end imports + +// Section below is generated&owned by "gen/generator.go". //template:begin model + +// Ensure provider defined types fully satisfy framework interfaces +var ( + _ resource.Resource = &SwitchStackRoutingInterfaceResource{} + _ resource.ResourceWithImportState = &SwitchStackRoutingInterfaceResource{} +) + +func NewSwitchStackRoutingInterfaceResource() resource.Resource { + return &SwitchStackRoutingInterfaceResource{} +} + +type SwitchStackRoutingInterfaceResource struct { + client *meraki.Client +} + +func (r *SwitchStackRoutingInterfaceResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_switch_stack_routing_interface" +} + +func (r *SwitchStackRoutingInterfaceResource) 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 the `Switch Stack Routing Interface` configuration.").String, + + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "The id of the object", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "network_id": schema.StringAttribute{ + MarkdownDescription: helpers.NewAttributeDescription("Network ID").String, + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "switch_stack_id": schema.StringAttribute{ + MarkdownDescription: helpers.NewAttributeDescription("Switch stack ID").String, + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "default_gateway": schema.StringAttribute{ + MarkdownDescription: helpers.NewAttributeDescription("The next hop for any traffic that isn`t going to a directly connected subnet or over a static route. This IP address must exist in a subnet with a routed interface.").String, + Optional: true, + }, + "interface_ip": schema.StringAttribute{ + MarkdownDescription: helpers.NewAttributeDescription("The IP address this switch stack will use for layer 3 routing on this VLAN or subnet. This cannot be the same as the switch`s management IP.").String, + Optional: true, + }, + "multicast_routing": schema.StringAttribute{ + MarkdownDescription: helpers.NewAttributeDescription("Enable multicast support if, multicast routing between VLANs is required. Options are, `disabled`, `enabled` or `IGMP snooping querier`. Default is `disabled`.").AddStringEnumDescription("IGMP snooping querier", "disabled", "enabled").String, + Optional: true, + Validators: []validator.String{ + stringvalidator.OneOf("IGMP snooping querier", "disabled", "enabled"), + }, + }, + "name": schema.StringAttribute{ + MarkdownDescription: helpers.NewAttributeDescription("A friendly name or description for the interface or VLAN.").String, + Required: true, + }, + "subnet": schema.StringAttribute{ + MarkdownDescription: helpers.NewAttributeDescription("The network that this routed interface is on, in CIDR notation (ex. 10.1.1.0/24).").String, + Optional: true, + }, + "vlan_id": schema.Int64Attribute{ + MarkdownDescription: helpers.NewAttributeDescription("The VLAN this routed interface is on. VLAN must be between 1 and 4094.").String, + Required: true, + }, + "ipv6_address": schema.StringAttribute{ + MarkdownDescription: helpers.NewAttributeDescription("The IPv6 address of the interface. Required if assignmentMode is `static`. Must not be included if assignmentMode is `eui-64`.").String, + Optional: true, + }, + "ipv6_assignment_mode": schema.StringAttribute{ + MarkdownDescription: helpers.NewAttributeDescription("The IPv6 assignment mode for the interface. Can be either `eui-64` or `static`.").String, + Optional: true, + }, + "ipv6_gateway": schema.StringAttribute{ + MarkdownDescription: helpers.NewAttributeDescription("The IPv6 default gateway of the interface. Required if prefix is defined and this is the first interface with IPv6 configured for the stack.").String, + Optional: true, + }, + "ipv6_prefix": schema.StringAttribute{ + MarkdownDescription: helpers.NewAttributeDescription("The IPv6 prefix of the interface. Required if IPv6 object is included.").String, + Optional: true, + }, + "ospf_settings_area": schema.StringAttribute{ + MarkdownDescription: helpers.NewAttributeDescription("The OSPF area to which this interface should belong. Can be either `disabled` or the identifier of an existing OSPF area. Defaults to `disabled`.").String, + Optional: true, + }, + "ospf_settings_cost": schema.Int64Attribute{ + MarkdownDescription: helpers.NewAttributeDescription("The path cost for this interface. Defaults to 1, but can be increased up to 65535 to give lower priority.").String, + Optional: true, + }, + "ospf_settings_is_passive_enabled": schema.BoolAttribute{ + MarkdownDescription: helpers.NewAttributeDescription("When enabled, OSPF will not run on the interface, but the subnet will still be advertised.").String, + Optional: true, + }, + }, + } +} + +func (r *SwitchStackRoutingInterfaceResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + r.client = req.ProviderData.(*MerakiProviderData).Client +} + +// End of section. //template:end model + +// Section below is generated&owned by "gen/generator.go". //template:begin create + +func (r *SwitchStackRoutingInterfaceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan SwitchStackRoutingInterface + + // Read plan + diags := req.Plan.Get(ctx, &plan) + if resp.Diagnostics.Append(diags...); resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, fmt.Sprintf("%s: Beginning Create", plan.Id.ValueString())) + + // Create object + body := plan.toBody(ctx, SwitchStackRoutingInterface{}) + res, err := r.client.Post(plan.getPath(), body) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to configure object (POST/PUT), got error: %s, %s", err, res.String())) + return + } + plan.Id = types.StringValue(res.Get("interfaceId").String()) + + tflog.Debug(ctx, fmt.Sprintf("%s: Create finished successfully", plan.Id.ValueString())) + + diags = resp.State.Set(ctx, &plan) + resp.Diagnostics.Append(diags...) + + helpers.SetFlagImporting(ctx, false, resp.Private, &resp.Diagnostics) +} + +// End of section. //template:end create + +// Section below is generated&owned by "gen/generator.go". //template:begin read + +func (r *SwitchStackRoutingInterfaceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state SwitchStackRoutingInterface + + // Read state + diags := req.State.Get(ctx, &state) + if resp.Diagnostics.Append(diags...); resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, fmt.Sprintf("%s: Beginning Read", state.Id.String())) + res, err := r.client.Get(state.getPath() + "/" + url.QueryEscape(state.Id.ValueString())) + if err != nil && strings.Contains(err.Error(), "StatusCode 404") { + resp.State.RemoveResource(ctx) + return + } else if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to retrieve object (GET), got error: %s, %s", err, res.String())) + return + } + + imp, diags := helpers.IsFlagImporting(ctx, req) + if resp.Diagnostics.Append(diags...); resp.Diagnostics.HasError() { + return + } + + // After `terraform import` we switch to a full read. + if imp { + state.fromBody(ctx, res) + } else { + state.fromBodyPartial(ctx, res) + } + + tflog.Debug(ctx, fmt.Sprintf("%s: Read finished successfully", state.Id.ValueString())) + + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + + helpers.SetFlagImporting(ctx, false, resp.Private, &resp.Diagnostics) +} + +// End of section. //template:end read + +// Section below is generated&owned by "gen/generator.go". //template:begin update + +func (r *SwitchStackRoutingInterfaceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan, state SwitchStackRoutingInterface + + // Read plan + diags := req.Plan.Get(ctx, &plan) + if resp.Diagnostics.Append(diags...); resp.Diagnostics.HasError() { + return + } + + // Read state + diags = req.State.Get(ctx, &state) + if resp.Diagnostics.Append(diags...); resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, fmt.Sprintf("%s: Beginning Update", plan.Id.ValueString())) + + body := plan.toBody(ctx, state) + res, err := r.client.Put(plan.getPath()+"/"+url.QueryEscape(plan.Id.ValueString()), 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...) +} + +// End of section. //template:end update + +// Section below is generated&owned by "gen/generator.go". //template:begin delete + +func (r *SwitchStackRoutingInterfaceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state SwitchStackRoutingInterface + + // Read state + diags := req.State.Get(ctx, &state) + if resp.Diagnostics.Append(diags...); resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, fmt.Sprintf("%s: Beginning Delete", state.Id.ValueString())) + res, err := r.client.Delete(state.getPath() + "/" + url.QueryEscape(state.Id.ValueString())) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to delete object (DELETE), got error: %s, %s", err, res.String())) + return + } + + tflog.Debug(ctx, fmt.Sprintf("%s: Delete finished successfully", state.Id.ValueString())) + + resp.State.RemoveResource(ctx) +} + +// End of section. //template:end delete + +// Section below is generated&owned by "gen/generator.go". //template:begin import +func (r *SwitchStackRoutingInterfaceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + idParts := strings.Split(req.ID, ",") + + if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { + resp.Diagnostics.AddError( + "Unexpected Import Identifier", + fmt.Sprintf("Expected import identifier with format: ,,. Got: %q", req.ID), + ) + return + } + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("network_id"), idParts[0])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("switch_stack_id"), idParts[1])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), idParts[2])...) + + helpers.SetFlagImporting(ctx, true, resp.Private, &resp.Diagnostics) +} + +// End of section. //template:end import diff --git a/internal/provider/resource_meraki_switch_stack_routing_interface_test.go b/internal/provider/resource_meraki_switch_stack_routing_interface_test.go new file mode 100644 index 0000000..128765f --- /dev/null +++ b/internal/provider/resource_meraki_switch_stack_routing_interface_test.go @@ -0,0 +1,127 @@ +// Copyright © 2024 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 + +package provider + +// Section below is generated&owned by "gen/generator.go". //template:begin imports +import ( + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +// End of section. //template:end imports + +// Section below is generated&owned by "gen/generator.go". //template:begin testAcc + +func TestAccMerakiSwitchStackRoutingInterface(t *testing.T) { + var checks []resource.TestCheckFunc + checks = append(checks, resource.TestCheckResourceAttr("meraki_switch_stack_routing_interface.test", "default_gateway", "192.168.1.1")) + checks = append(checks, resource.TestCheckResourceAttr("meraki_switch_stack_routing_interface.test", "interface_ip", "192.168.1.2")) + checks = append(checks, resource.TestCheckResourceAttr("meraki_switch_stack_routing_interface.test", "multicast_routing", "disabled")) + checks = append(checks, resource.TestCheckResourceAttr("meraki_switch_stack_routing_interface.test", "name", "L3 interface")) + checks = append(checks, resource.TestCheckResourceAttr("meraki_switch_stack_routing_interface.test", "subnet", "192.168.1.0/24")) + checks = append(checks, resource.TestCheckResourceAttr("meraki_switch_stack_routing_interface.test", "vlan_id", "100")) + checks = append(checks, resource.TestCheckResourceAttr("meraki_switch_stack_routing_interface.test", "ipv6_address", "1:2:3:4::1")) + checks = append(checks, resource.TestCheckResourceAttr("meraki_switch_stack_routing_interface.test", "ipv6_assignment_mode", "static")) + checks = append(checks, resource.TestCheckResourceAttr("meraki_switch_stack_routing_interface.test", "ipv6_gateway", "1:2:3:4::2")) + checks = append(checks, resource.TestCheckResourceAttr("meraki_switch_stack_routing_interface.test", "ipv6_prefix", "1:2:3:4::/64")) + + var steps []resource.TestStep + if os.Getenv("SKIP_MINIMUM_TEST") == "" { + steps = append(steps, resource.TestStep{ + Config: testAccMerakiSwitchStackRoutingInterfacePrerequisitesConfig + testAccMerakiSwitchStackRoutingInterfaceConfig_minimum(), + }) + } + steps = append(steps, resource.TestStep{ + Config: testAccMerakiSwitchStackRoutingInterfacePrerequisitesConfig + testAccMerakiSwitchStackRoutingInterfaceConfig_all(), + Check: resource.ComposeTestCheckFunc(checks...), + }) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: steps, + }) +} + +// End of section. //template:end testAcc + +// Section below is generated&owned by "gen/generator.go". //template:begin testPrerequisites + +const testAccMerakiSwitchStackRoutingInterfacePrerequisitesConfig = ` +data "meraki_organization" "test" { + name = "Dev" +} +resource "meraki_network" "test" { + organization_id = data.meraki_organization.test.id + name = "Network1" + product_types = ["switch", "wireless"] +} +resource "meraki_network_device_claim" "test" { + network_id = meraki_network.test.id + serials = ["Q5KD-PCG4-HB8R", "Q5KD-CU8N-DEDR"] +} +resource "meraki_switch_stack" "test" { + network_id = meraki_network.test.id + name = "A cool stack" + serials = meraki_network_device_claim.test.serials +} + +` + +// End of section. //template:end testPrerequisites + +// Section below is generated&owned by "gen/generator.go". //template:begin testAccConfigMinimal + +func testAccMerakiSwitchStackRoutingInterfaceConfig_minimum() string { + config := `resource "meraki_switch_stack_routing_interface" "test" {` + "\n" + config += ` network_id = meraki_network.test.id` + "\n" + config += ` switch_stack_id = meraki_switch_stack.test.id` + "\n" + config += ` default_gateway = "192.168.1.1"` + "\n" + config += ` interface_ip = "192.168.1.2"` + "\n" + config += ` name = "L3 interface"` + "\n" + config += ` subnet = "192.168.1.0/24"` + "\n" + config += ` vlan_id = 100` + "\n" + config += `}` + "\n" + return config +} + +// End of section. //template:end testAccConfigMinimal + +// Section below is generated&owned by "gen/generator.go". //template:begin testAccConfigAll + +func testAccMerakiSwitchStackRoutingInterfaceConfig_all() string { + config := `resource "meraki_switch_stack_routing_interface" "test" {` + "\n" + config += ` network_id = meraki_network.test.id` + "\n" + config += ` switch_stack_id = meraki_switch_stack.test.id` + "\n" + config += ` default_gateway = "192.168.1.1"` + "\n" + config += ` interface_ip = "192.168.1.2"` + "\n" + config += ` multicast_routing = "disabled"` + "\n" + config += ` name = "L3 interface"` + "\n" + config += ` subnet = "192.168.1.0/24"` + "\n" + config += ` vlan_id = 100` + "\n" + config += ` ipv6_address = "1:2:3:4::1"` + "\n" + config += ` ipv6_assignment_mode = "static"` + "\n" + config += ` ipv6_gateway = "1:2:3:4::2"` + "\n" + config += ` ipv6_prefix = "1:2:3:4::/64"` + "\n" + config += `}` + "\n" + return config +} + +// End of section. //template:end testAccConfigAll