From 0e3c29f62f587db7b340fee8299c6ec0a3622898 Mon Sep 17 00:00:00 2001 From: Michael Dokolin Date: Sun, 9 Jun 2024 08:12:11 +0200 Subject: [PATCH 01/10] fix: Fix `topics` property type in `routeros_system_logging` to ignore values order --- routeros/resource_system_logging.go | 2 +- routeros/resource_system_logging_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/routeros/resource_system_logging.go b/routeros/resource_system_logging.go index 7a6bf652..19d27a60 100644 --- a/routeros/resource_system_logging.go +++ b/routeros/resource_system_logging.go @@ -59,7 +59,7 @@ func ResourceSystemLogging() *schema.Resource { Computed: true, }, "topics": { - Type: schema.TypeList, + Type: schema.TypeSet, Elem: &schema.Schema{Type: schema.TypeString, ValidateFunc: validation.StringInSlice(validTopics, false)}, Optional: true, Description: `log all messages that falls into specified topic or list of topics. diff --git a/routeros/resource_system_logging_test.go b/routeros/resource_system_logging_test.go index fcabf3c4..dac83f66 100644 --- a/routeros/resource_system_logging_test.go +++ b/routeros/resource_system_logging_test.go @@ -26,8 +26,8 @@ func TestAccSystemLoggingTest_basic(t *testing.T) { resource.TestCheckResourceAttr(testSystemSimpleLoggingTask, "disabled", "false"), resource.TestCheckResourceAttr(testSystemSimpleLoggingTask, "invalid", "false"), resource.TestCheckResourceAttr(testSystemSimpleLoggingTask, "prefix", "simple_prefix"), - resource.TestCheckResourceAttr(testSystemSimpleLoggingTask, "topics.0", "snmp"), - resource.TestCheckResourceAttr(testSystemSimpleLoggingTask, "topics.1", "gsm"), + resource.TestCheckResourceAttr(testSystemSimpleLoggingTask, "topics.0", "gsm"), + resource.TestCheckResourceAttr(testSystemSimpleLoggingTask, "topics.1", "snmp"), ), }, }, From 858ecab2b52c8933051c2ae992d272969b90c6be Mon Sep 17 00:00:00 2001 From: Michael Dokolin Date: Mon, 10 Jun 2024 07:55:12 +0200 Subject: [PATCH 02/10] feat: Add state migrator helper to convert scalar values to lists --- routeros/provider_resource_state_migration.go | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/routeros/provider_resource_state_migration.go b/routeros/provider_resource_state_migration.go index aecbf914..89fdaabd 100644 --- a/routeros/provider_resource_state_migration.go +++ b/routeros/provider_resource_state_migration.go @@ -3,6 +3,8 @@ package routeros import ( "context" "fmt" + "reflect" + "strings" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -33,3 +35,28 @@ func stateMigrationNameToId(resourcePath string) schema.StateUpgradeFunc { return rawState, nil } } + +func stateMigrationScalarToList(keys ...string) schema.StateUpgradeFunc { + return func(ctx context.Context, rawState map[string]interface{}, m interface{}) (map[string]interface{}, error) { + for _, key := range keys { + if rawState[key] == nil { + continue + } + + value := reflect.ValueOf(rawState[key]) + if value.IsZero() { + rawState[key] = []interface{}{} + } + + if reflect.ValueOf(value).Kind() == reflect.String { + rawState[key] = strings.Split(rawState[key].(string), ",") + } + + slice := reflect.MakeSlice(reflect.SliceOf(value.Type()), 0, 1) + reflect.Append(slice, value) + rawState[key] = slice.Interface() + } + + return rawState, nil + } +} From a1beccacd612960abb27b4c66c4941e0e3f0cf4c Mon Sep 17 00:00:00 2001 From: Michael Dokolin Date: Wed, 12 Jun 2024 07:58:31 +0200 Subject: [PATCH 03/10] feat: Update the `service` property in `routeros_radius` to support multiple values --- docs/resources/radius.md | 2 +- .../resources/routeros_radius/resource.tf | 2 +- routeros/resource_radius.go | 17 ++- routeros/resource_radius_v0.go | 109 ++++++++++++++++++ 4 files changed, 126 insertions(+), 4 deletions(-) create mode 100644 routeros/resource_radius_v0.go diff --git a/docs/resources/radius.md b/docs/resources/radius.md index 42802840..26721485 100644 --- a/docs/resources/radius.md +++ b/docs/resources/radius.md @@ -5,7 +5,7 @@ ```terraform resource "routeros_radius" "user_manager" { address = "127.0.0.1" - service = "ppp,login" + service = ["ppp", "login"] } ``` diff --git a/examples/resources/routeros_radius/resource.tf b/examples/resources/routeros_radius/resource.tf index 4f279c27..1d37843a 100644 --- a/examples/resources/routeros_radius/resource.tf +++ b/examples/resources/routeros_radius/resource.tf @@ -1,4 +1,4 @@ resource "routeros_radius" "user_manager" { address = "127.0.0.1" - service = "ppp,login" + service = ["ppp", "login"] } diff --git a/routeros/resource_radius.go b/routeros/resource_radius.go index dc15111d..77fd0f82 100644 --- a/routeros/resource_radius.go +++ b/routeros/resource_radius.go @@ -74,9 +74,14 @@ func ResourceRadius() *schema.Resource { Description: "The shared secret to access the RADIUS server.", }, "service": { - Type: schema.TypeString, + Type: schema.TypeSet, Optional: true, - Description: "A comma-separated list of router services that will use the RADIUS server. Possible values: " + + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice([]string{"hotspot", "login", "ppp", "wireless", + "dhcp", "ipsec", "dot1x"}, false), + }, + Description: "A set of router services that will use the RADIUS server. Possible values: " + "`hotspot`, `login`, `ppp`, `wireless`, `dhcp`, `ipsec`, `dot1x`.", }, "src_address": { @@ -105,6 +110,14 @@ func ResourceRadius() *schema.Resource { }, Schema: resSchema, + SchemaVersion: 1, + StateUpgraders: []schema.StateUpgrader{ + { + Type: ResourceRadiusV0().CoreConfigSchema().ImpliedType(), + Upgrade: stateMigrationScalarToList("service"), + Version: 0, + }, + }, } } diff --git a/routeros/resource_radius_v0.go b/routeros/resource_radius_v0.go new file mode 100644 index 00000000..d028fe88 --- /dev/null +++ b/routeros/resource_radius_v0.go @@ -0,0 +1,109 @@ +package routeros + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +// https://help.mikrotik.com/docs/display/ROS/RADIUS#RADIUS-RADIUSClient +func ResourceRadiusV0() *schema.Resource { + resSchema := map[string]*schema.Schema{ + MetaResourcePath: PropResourcePath("/radius"), + MetaId: PropId(Id), + + "accounting_backup": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "An option whether the configuration is for the backup RADIUS server.", + }, + "accounting_port": { + Type: schema.TypeInt, + Optional: true, + Default: 1813, + Description: "RADIUS server port used for accounting.", + ValidateFunc: validation.IntBetween(0, 65535), + }, + "address": { + Type: schema.TypeString, + Required: true, + Description: "IPv4 or IPv6 address of RADIUS server.", + ValidateFunc: validation.IsIPAddress, + }, + "authentication_port": { + Type: schema.TypeInt, + Optional: true, + Default: 1812, + Description: "RADIUS server port used for authentication.", + ValidateFunc: validation.IntBetween(0, 65535), + }, + "called_id": { + Type: schema.TypeString, + Optional: true, + Description: "RADIUS calling station identifier.", + }, + "certificate": { + Type: schema.TypeString, + Optional: true, + Default: "none", + Description: "Certificate to use for communication with RADIUS Server with RadSec enabled.", + }, + KeyComment: PropCommentRw, + KeyDisabled: PropDisabledRw, + "domain": { + Type: schema.TypeString, + Optional: true, + Description: "Microsoft Windows domain of client passed to RADIUS servers that require domain validation.", + }, + "protocol": { + Type: schema.TypeString, + Optional: true, + Default: "udp", + Description: "An option specifies the protocol to use when communicating with the RADIUS Server.", + ValidateFunc: validation.StringInSlice([]string{"radsec", "udp"}, false), + }, + "realm": { + Type: schema.TypeString, + Optional: true, + Description: "Explicitly stated realm (user domain), so the users do not have to provide proper ISP domain name in the user name.", + }, + "secret": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + Description: "The shared secret to access the RADIUS server.", + }, + "service": { + Type: schema.TypeString, + Optional: true, + Description: "A comma-separated list of router services that will use the RADIUS server. Possible values: " + + "`hotspot`, `login`, `ppp`, `wireless`, `dhcp`, `ipsec`, `dot1x`.", + }, + "src_address": { + Type: schema.TypeString, + Optional: true, + Description: "Source IPv4/IPv6 address of the packets sent to the RADIUS server.", + ValidateFunc: validation.IsIPAddress, + }, + "timeout": { + Type: schema.TypeString, + Optional: true, + Default: "300ms", + Description: "A timeout, after which the request should be resent.", + DiffSuppressFunc: TimeEquall, + }, + } + + return &schema.Resource{ + CreateContext: DefaultCreate(resSchema), + ReadContext: DefaultRead(resSchema), + UpdateContext: DefaultUpdate(resSchema), + DeleteContext: DefaultDelete(resSchema), + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: resSchema, + } +} From ea25f0088d2da7dfaa001fab0b06c0bc67bbc847 Mon Sep 17 00:00:00 2001 From: Michael Dokolin Date: Thu, 13 Jun 2024 18:48:12 +0200 Subject: [PATCH 04/10] feat: Update the `servers` property in `routeros_dns` to support multiple values --- docs/resources/ip_dns.md | 5 +- .../resources/routeros_ip_dns/resource.tf | 5 +- routeros/resource_ip_dns.go | 11 +- routeros/resource_ip_dns_test.go | 7 +- routeros/resource_ip_dns_v0.go | 194 ++++++++++++++++++ 5 files changed, 218 insertions(+), 4 deletions(-) create mode 100644 routeros/resource_ip_dns_v0.go diff --git a/docs/resources/ip_dns.md b/docs/resources/ip_dns.md index 731885d9..0c228bfb 100644 --- a/docs/resources/ip_dns.md +++ b/docs/resources/ip_dns.md @@ -5,7 +5,10 @@ A MikroTik router with DNS feature enabled can be set as a DNS server for any DN ```terraform resource "routeros_dns" "dns-server" { allow_remote_requests = true - servers = "2606:4700:4700::1111,1.1.1.1,2606:4700:4700::1001,1.0.0.1" + servers = [ + "2606:4700:4700::1111,1.1.1.1", + "2606:4700:4700::1001,1.0.0.1", + ] } ``` diff --git a/examples/resources/routeros_ip_dns/resource.tf b/examples/resources/routeros_ip_dns/resource.tf index 07092e26..000ef16f 100644 --- a/examples/resources/routeros_ip_dns/resource.tf +++ b/examples/resources/routeros_ip_dns/resource.tf @@ -1,4 +1,7 @@ resource "routeros_dns" "dns-server" { allow_remote_requests = true - servers = "2606:4700:4700::1111,1.1.1.1,2606:4700:4700::1001,1.0.0.1" + servers = [ + "2606:4700:4700::1111,1.1.1.1", + "2606:4700:4700::1001,1.0.0.1", + ] } \ No newline at end of file diff --git a/routeros/resource_ip_dns.go b/routeros/resource_ip_dns.go index 4776fe17..186878c5 100644 --- a/routeros/resource_ip_dns.go +++ b/routeros/resource_ip_dns.go @@ -130,8 +130,9 @@ func ResourceDns() *schema.Resource { DiffSuppressFunc: TimeEquall, }, "servers": { - Type: schema.TypeString, + Type: schema.TypeList, Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, Description: "List of DNS server IPv4/IPv6 addresses.", }, KeyVrf: PropVrfRw, @@ -190,5 +191,13 @@ func ResourceDns() *schema.Resource { }, Schema: resSchema, + SchemaVersion: 1, + StateUpgraders: []schema.StateUpgrader{ + { + Type: ResourceDnsV0().CoreConfigSchema().ImpliedType(), + Upgrade: stateMigrationScalarToList("servers"), + Version: 0, + }, + }, } } diff --git a/routeros/resource_ip_dns_test.go b/routeros/resource_ip_dns_test.go index ed0d501a..5c4df97b 100644 --- a/routeros/resource_ip_dns_test.go +++ b/routeros/resource_ip_dns_test.go @@ -46,7 +46,12 @@ resource "routeros_dns" "test" { max_udp_packet_size = 8192 query_server_timeout = "500ms" query_total_timeout = "15" - servers = "2606:4700:4700::1112,1.1.1.2,2606:4700:4700::1002,1.0.0.2" + servers = [ + "2606:4700:4700::1112", + "1.1.1.2", + "2606:4700:4700::1002", + "1.0.0.2", + ] use_doh_server = "https://cloudflare-dns.com/dns-query" verify_doh_cert = true }` diff --git a/routeros/resource_ip_dns_v0.go b/routeros/resource_ip_dns_v0.go new file mode 100644 index 00000000..9ced028d --- /dev/null +++ b/routeros/resource_ip_dns_v0.go @@ -0,0 +1,194 @@ +package routeros + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +/* +{ + "allow-remote-requests": "true", + "cache-max-ttl": "1w", + "cache-size": "2048", + "cache-used": "99", RO + "dynamic-servers": "", RO + "max-concurrent-queries": "100", + "max-concurrent-tcp-sessions": "20", + "max-udp-packet-size": "4096", + "query-server-timeout": "2s", + "query-total-timeout": "10s", + "servers": "192.168.1.1", + "use-doh-server": "", + "verify-doh-cert": "false" +} +*/ + +// ResourceDns https://wiki.mikrotik.com/wiki/Manual:IP/DNS +func ResourceDnsV0() *schema.Resource { + resSchema := map[string]*schema.Schema{ + MetaResourcePath: PropResourcePath("/ip/dns"), + MetaId: PropId(Name), + + "address_list_extra_time": { + Type: schema.TypeString, + Optional: true, + Description: "", + ValidateFunc: ValidationTime, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + return AlwaysPresentNotUserProvided(k, old, new, d) || TimeEquall(k, old, new, d) + }, + }, + "allow_remote_requests": { + Type: schema.TypeBool, + Optional: true, + Description: "Specifies whether to allow network requests.", + }, + "cache_max_ttl": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "Maximum time-to-live for cache records. In other words, cache records will expire " + + "unconditionally after cache-max-ttl time. Shorter TTL received from DNS servers are respected. " + + "*Default: 1w*", + ValidateFunc: ValidationTime, + DiffSuppressFunc: TimeEquall, + }, + "cache_size": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: "Specifies the size of DNS cache in KiB (64..4294967295). *Default: 2048*", + }, + "cache_used": { + Type: schema.TypeInt, + Computed: true, + Description: "Shows the currently used cache size in KiB.", + }, + "doh_max_concurrent_queries": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: "Specifies how many DoH concurrent queries are allowed.", + }, + "doh_max_server_connections": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: "Specifies how many concurrent connections to the DoH server are allowed.", + }, + "doh_timeout": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "Specifies how long to wait for query response from the DoH server.", + DiffSuppressFunc: TimeEquall, + }, + "dynamic_servers": { + Type: schema.TypeString, + Computed: true, + Description: "List of dynamically added DNS server from different services, for example, DHCP.", + }, + "max_concurrent_queries": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: "Specifies how much concurrent queries are allowed. *Default: 100*", + }, + "max_concurrent_tcp_sessions": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: "Specifies how much concurrent TCP sessions are allowed. *Default: 20*", + }, + "max_udp_packet_size": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: "Maximum size of allowed UDP packet. *Default: 4096*", + ValidateFunc: validation.IntBetween(50, 65507), + }, + "query_server_timeout": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "Specifies how long to wait for query response from one server. " + + "Time can be specified in milliseconds. *Default: 2s*", + ValidateFunc: ValidationTime, + DiffSuppressFunc: TimeEquall, + }, + "query_total_timeout": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "Specifies how long to wait for query response in total. Note that this setting must be " + + "configured taking into account query_server_timeout and number of used DNS server. " + + "Time can be specified in milliseconds. *Default: 10s*", + ValidateFunc: ValidationTime, + DiffSuppressFunc: TimeEquall, + }, + "servers": { + Type: schema.TypeString, + Optional: true, + Description: "List of DNS server IPv4/IPv6 addresses.", + }, + KeyVrf: PropVrfRw, + "use_doh_server": { + Type: schema.TypeString, + Optional: true, + Description: `DNS over HTTPS (DoH) server URL. + > Mikrotik strongly suggest not use third-party download links for certificate fetching. + Use the Certificate Authority's own website. + + > RouterOS prioritize DoH over DNS server if both are configured on the device.`, + }, + "verify_doh_cert": { + Type: schema.TypeBool, + Optional: true, + Description: "DoH certificate verification. [See docs](https://wiki.mikrotik.com/wiki/Manual:IP/DNS#DNS_over_HTTPS).", + }, + } + + return &schema.Resource{ + Description: "A MikroTik router with DNS feature enabled can be set as a DNS server for any DNS-compliant client.", + + CreateContext: DefaultSystemCreate(resSchema), + ReadContext: DefaultSystemRead(resSchema), + UpdateContext: DefaultSystemUpdate(resSchema), + // This behavior when deleting a system resource is the exception rather than the rule. + // With existing serialization logic, the best way to avoid undefined DNS service state + // is to clear the main fields. + DeleteContext: func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + // Values in the Mikrotik notation! + resetFileds := map[string]string{ + "allow-remote-requests": "no", + "servers": "", + "use-doh-server": "", + "verify-doh-cert": "no", + } + + var resUrl string + if m.(Client).GetTransport() == TransportREST { + // https://router/rest/ip/dns/set + resUrl = "/set" + } + + // Used POST request! + err := m.(Client).SendRequest(crudPost, &URL{Path: resSchema[MetaResourcePath].Default.(string) + resUrl}, + resetFileds, nil) + if err != nil { + return diag.FromErr(err) + } + + d.SetId("") + return nil + }, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: resSchema, + } +} From bc1cf68b5f3126f0f19bd3f4b4288cc269f25f67 Mon Sep 17 00:00:00 2001 From: Michael Dokolin Date: Fri, 14 Jun 2024 06:25:32 +0200 Subject: [PATCH 05/10] feat: Update properties in `routeros_ip_dhcp_server_network` to support multiple values --- docs/resources/ip_dhcp_server_network.md | 2 +- .../resource.tf | 2 +- routeros/resource_ip_dhcp_server_network.go | 49 +++++--- .../resource_ip_dhcp_server_network_v0.go | 106 ++++++++++++++++++ 4 files changed, 144 insertions(+), 15 deletions(-) create mode 100644 routeros/resource_ip_dhcp_server_network_v0.go diff --git a/docs/resources/ip_dhcp_server_network.md b/docs/resources/ip_dhcp_server_network.md index 58e4fb32..1a332400 100644 --- a/docs/resources/ip_dhcp_server_network.md +++ b/docs/resources/ip_dhcp_server_network.md @@ -6,7 +6,7 @@ resource "routeros_ip_dhcp_server_network" "dhcp_server_network" { address = "10.0.0.0/24" gateway = "10.0.0.1" - dns_server = "1.1.1.1" + dns_server = ["1.1.1.1"] } ``` diff --git a/examples/resources/routeros_ip_dhcp_server_network/resource.tf b/examples/resources/routeros_ip_dhcp_server_network/resource.tf index 72ebfb7b..377b217e 100644 --- a/examples/resources/routeros_ip_dhcp_server_network/resource.tf +++ b/examples/resources/routeros_ip_dhcp_server_network/resource.tf @@ -1,5 +1,5 @@ resource "routeros_ip_dhcp_server_network" "dhcp_server_network" { address = "10.0.0.0/24" gateway = "10.0.0.1" - dns_server = "1.1.1.1" + dns_server = ["1.1.1.1"] } \ No newline at end of file diff --git a/routeros/resource_ip_dhcp_server_network.go b/routeros/resource_ip_dhcp_server_network.go index 780dc959..82cef372 100644 --- a/routeros/resource_ip_dhcp_server_network.go +++ b/routeros/resource_ip_dhcp_server_network.go @@ -22,15 +22,21 @@ func ResourceDhcpServerNetwork() *schema.Resource { Description: "Boot filename.", }, "caps_manager": { - Type: schema.TypeString, + Type: schema.TypeList, Optional: true, - Description: "A comma-separated list of IP addresses for one or more CAPsMAN system managers. " + + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: "A list of IP addresses for one or more CAPsMAN system managers. " + "DHCP Option 138 (capwap) will be used.", }, KeyComment: PropCommentRw, "dhcp_option": { - Type: schema.TypeString, - Optional: true, + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, Description: "Add additional DHCP options from the option list.", }, "dhcp_option_set": { @@ -45,9 +51,12 @@ func ResourceDhcpServerNetwork() *schema.Resource { "DHCP clients if no DNS Server in DNS-server is set.", }, "dns_server": { - Type: schema.TypeString, + Type: schema.TypeList, Optional: true, - Description: "the DHCP client will use these as the default DNS servers. Two comma-separated DNS servers " + + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: "The DHCP client will use these as the default DNS servers. Two DNS servers " + "can be specified to be used by the DHCP client as primary and secondary DNS servers.", }, "domain": { @@ -78,18 +87,24 @@ func ResourceDhcpServerNetwork() *schema.Resource { ValidateFunc: validation.IsIPv4Address, }, "ntp_server": { - Type: schema.TypeString, + Type: schema.TypeList, Optional: true, - Description: "The DHCP client will use these as the default NTP servers. Two comma-separated NTP servers " + + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.IsIPv4Address, + }, + Description: "The DHCP client will use these as the default NTP servers. Two NTP servers " + "can be specified to be used by the DHCP client as primary and secondary NTP servers", - ValidateFunc: validation.IsIPv4Address, }, "wins_server": { - Type: schema.TypeString, + Type: schema.TypeList, Optional: true, - Description: "The Windows DHCP client will use these as the default WINS servers. Two comma-separated " + - "WINS servers can be specified to be used by the DHCP client as primary and secondary WINS servers", - ValidateFunc: validation.IsIPv4Address, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.IsIPv4Address, + }, + Description: "The Windows DHCP client will use these as the default WINS servers. Two WINS servers " + + "can be specified to be used by the DHCP client as primary and secondary WINS servers", }, } return &schema.Resource{ @@ -102,5 +117,13 @@ func ResourceDhcpServerNetwork() *schema.Resource { }, Schema: resSchema, + SchemaVersion: 1, + StateUpgraders: []schema.StateUpgrader{ + { + Type: ResourceDhcpServerNetworkV0().CoreConfigSchema().ImpliedType(), + Upgrade: stateMigrationScalarToList("caps_manager", "dhcp_option", "dns_server", "ntp_server", "wins_server"), + Version: 0, + }, + }, } } diff --git a/routeros/resource_ip_dhcp_server_network_v0.go b/routeros/resource_ip_dhcp_server_network_v0.go new file mode 100644 index 00000000..40dbe4d2 --- /dev/null +++ b/routeros/resource_ip_dhcp_server_network_v0.go @@ -0,0 +1,106 @@ +package routeros + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +// ResourceDhcpServerNetwork https://wiki.mikrotik.com/wiki/Manual:IP/DHCP_Server#Networks +func ResourceDhcpServerNetworkV0() *schema.Resource { + resSchema := map[string]*schema.Schema{ + MetaResourcePath: PropResourcePath("/ip/dhcp-server/network"), + MetaId: PropId(Id), + + "address": { + Type: schema.TypeString, + Required: true, + Description: "The network DHCP server(s) will lease addresses from.", + }, + "boot_file_name": { + Type: schema.TypeString, + Optional: true, + Description: "Boot filename.", + }, + "caps_manager": { + Type: schema.TypeString, + Optional: true, + Description: "A comma-separated list of IP addresses for one or more CAPsMAN system managers. " + + "DHCP Option 138 (capwap) will be used.", + }, + KeyComment: PropCommentRw, + "dhcp_option": { + Type: schema.TypeString, + Optional: true, + Description: "Add additional DHCP options from the option list.", + }, + "dhcp_option_set": { + Type: schema.TypeString, + Optional: true, + Description: "Add an additional set of DHCP options.", + }, + "dns_none": { + Type: schema.TypeBool, + Optional: true, + Description: "If set, then DHCP Server will not pass dynamic DNS servers configured on the router to the " + + "DHCP clients if no DNS Server in DNS-server is set.", + }, + "dns_server": { + Type: schema.TypeString, + Optional: true, + Description: "the DHCP client will use these as the default DNS servers. Two comma-separated DNS servers " + + "can be specified to be used by the DHCP client as primary and secondary DNS servers.", + }, + "domain": { + Type: schema.TypeString, + Optional: true, + Description: "The DHCP client will use this as the 'DNS domain' setting for the network adapter.", + }, + KeyDynamic: PropDynamicRo, + "gateway": { + Type: schema.TypeString, + Optional: true, + Default: "0.0.0.0", + Description: "The default gateway to be used by DHCP Client.", + ValidateFunc: validation.IsIPv4Address, + }, + "netmask": { + Type: schema.TypeInt, + Optional: true, + Default: 0, + Description: "The actual network mask is to be used by the DHCP client. If set to '0' - netmask from " + + "network address will be used.", + ValidateFunc: validation.IntBetween(0, 32), + }, + "next_server": { + Type: schema.TypeString, + Optional: true, + Description: "The IP address of the next server to use in bootstrap.", + ValidateFunc: validation.IsIPv4Address, + }, + "ntp_server": { + Type: schema.TypeString, + Optional: true, + Description: "The DHCP client will use these as the default NTP servers. Two comma-separated NTP servers " + + "can be specified to be used by the DHCP client as primary and secondary NTP servers", + ValidateFunc: validation.IsIPv4Address, + }, + "wins_server": { + Type: schema.TypeString, + Optional: true, + Description: "The Windows DHCP client will use these as the default WINS servers. Two comma-separated " + + "WINS servers can be specified to be used by the DHCP client as primary and secondary WINS servers", + ValidateFunc: validation.IsIPv4Address, + }, + } + return &schema.Resource{ + CreateContext: DefaultCreate(resSchema), + ReadContext: DefaultRead(resSchema), + UpdateContext: DefaultUpdate(resSchema), + DeleteContext: DefaultDelete(resSchema), + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: resSchema, + } +} From 66ab1d2cec80d8c677190315d258b47ca9fe50fa Mon Sep 17 00:00:00 2001 From: Michael Dokolin Date: Sat, 15 Jun 2024 08:15:12 +0200 Subject: [PATCH 06/10] feat: Update properties in `routeros_interface_dot1x_client` and `routeros_interface_dot1x_server` to support multiple values --- docs/resources/interface_dot1x_client.md | 2 +- docs/resources/interface_dot1x_server.md | 2 +- .../resource.tf | 2 +- .../resource.tf | 2 +- routeros/resource_interface_dot1x.go | 35 +++- routeros/resource_interface_dot1x_v0.go | 159 ++++++++++++++++++ 6 files changed, 192 insertions(+), 10 deletions(-) create mode 100644 routeros/resource_interface_dot1x_v0.go diff --git a/docs/resources/interface_dot1x_client.md b/docs/resources/interface_dot1x_client.md index 8bec586d..b1f58d5c 100644 --- a/docs/resources/interface_dot1x_client.md +++ b/docs/resources/interface_dot1x_client.md @@ -4,7 +4,7 @@ ## Example Usage ```terraform resource "routeros_interface_dot1x_client" "ether2" { - eap_methods = "eap-peap,eap-mschapv2" + eap_methods = ["eap-peap", "eap-mschapv2"] identity = "router" interface = "ether2" } diff --git a/docs/resources/interface_dot1x_server.md b/docs/resources/interface_dot1x_server.md index 7ec648cd..6487ca7b 100644 --- a/docs/resources/interface_dot1x_server.md +++ b/docs/resources/interface_dot1x_server.md @@ -4,7 +4,7 @@ ## Example Usage ```terraform resource "routeros_interface_dot1x_server" "ether2" { - auth_types = "mac-auth" + auth_types = ["mac-auth"] interface = "ether2" } ``` diff --git a/examples/resources/routeros_interface_dot1x_client/resource.tf b/examples/resources/routeros_interface_dot1x_client/resource.tf index bad3d219..511439ac 100644 --- a/examples/resources/routeros_interface_dot1x_client/resource.tf +++ b/examples/resources/routeros_interface_dot1x_client/resource.tf @@ -1,5 +1,5 @@ resource "routeros_interface_dot1x_client" "ether2" { - eap_methods = "eap-peap,eap-mschapv2" + eap_methods = ["eap-peap", "eap-mschapv2"] identity = "router" interface = "ether2" } diff --git a/examples/resources/routeros_interface_dot1x_server/resource.tf b/examples/resources/routeros_interface_dot1x_server/resource.tf index 38de4b6d..489789c9 100644 --- a/examples/resources/routeros_interface_dot1x_server/resource.tf +++ b/examples/resources/routeros_interface_dot1x_server/resource.tf @@ -1,4 +1,4 @@ resource "routeros_interface_dot1x_server" "ether2" { - auth_types = "mac-auth" + auth_types = ["mac-auth"] interface = "ether2" } diff --git a/routeros/resource_interface_dot1x.go b/routeros/resource_interface_dot1x.go index b0f89c21..a24ad155 100644 --- a/routeros/resource_interface_dot1x.go +++ b/routeros/resource_interface_dot1x.go @@ -25,9 +25,13 @@ func ResourceInterfaceDot1xClient() *schema.Resource { KeyComment: PropCommentRw, KeyDisabled: PropDisabledRw, "eap_methods": { - Type: schema.TypeString, - Required: true, - Description: "A list of EAP methods used for authentication: `eap-tls`, `eap-ttls`, `eap-peap`, `eap-mschapv2`.", + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice([]string{"eap-tls", "eap-ttls", "eap-peap", "eap-mschapv2"}, false), + }, + Description: "A set of EAP methods used for authentication: `eap-tls`, `eap-ttls`, `eap-peap`, `eap-mschapv2`.", }, "identity": { Type: schema.TypeString, @@ -58,6 +62,14 @@ func ResourceInterfaceDot1xClient() *schema.Resource { }, Schema: resSchema, + SchemaVersion: 1, + StateUpgraders: []schema.StateUpgrader{ + { + Type: ResourceInterfaceDot1xClientV0().CoreConfigSchema().ImpliedType(), + Upgrade: stateMigrationScalarToList("eap_methods"), + Version: 0, + }, + }, } } @@ -81,9 +93,12 @@ func ResourceInterfaceDot1xServer() *schema.Resource { DiffSuppressFunc: TimeEquall, }, "auth_types": { - Type: schema.TypeString, - Optional: true, - Default: "dot1x", + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice([]string{"dot1x", "mac-auth"}, false), + }, Description: "Used authentication type on a server interface. Comma-separated list of `dot1x` and `mac-auth`.", }, KeyComment: PropCommentRw, @@ -155,5 +170,13 @@ func ResourceInterfaceDot1xServer() *schema.Resource { }, Schema: resSchema, + SchemaVersion: 1, + StateUpgraders: []schema.StateUpgrader{ + { + Type: ResourceInterfaceDot1xServerV0().CoreConfigSchema().ImpliedType(), + Upgrade: stateMigrationScalarToList("auth_types"), + Version: 0, + }, + }, } } diff --git a/routeros/resource_interface_dot1x_v0.go b/routeros/resource_interface_dot1x_v0.go new file mode 100644 index 00000000..c1a1b465 --- /dev/null +++ b/routeros/resource_interface_dot1x_v0.go @@ -0,0 +1,159 @@ +package routeros + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +// https://help.mikrotik.com/docs/display/ROS/Dot1X#Dot1X-Client +func ResourceInterfaceDot1xClientV0() *schema.Resource { + resSchema := map[string]*schema.Schema{ + MetaResourcePath: PropResourcePath("/interface/dot1x/client"), + MetaId: PropId(Id), + + "anon_identity": { + Type: schema.TypeString, + Optional: true, + Description: "Identity for outer layer EAP authentication. Used only with `eap-ttls` and `eap-peap` methods. If not set, the value from the identity parameter will be used for outer layer EAP authentication.", + }, + "certificate": { + Type: schema.TypeString, + Optional: true, + Default: "none", + Description: "Name of a certificate. Required when the `eap-tls` method is used.", + }, + KeyComment: PropCommentRw, + KeyDisabled: PropDisabledRw, + "eap_methods": { + Type: schema.TypeString, + Required: true, + Description: "A list of EAP methods used for authentication: `eap-tls`, `eap-ttls`, `eap-peap`, `eap-mschapv2`.", + }, + "identity": { + Type: schema.TypeString, + Required: true, + Description: "The supplicant identity that is used for EAP authentication.", + }, + KeyInterface: PropInterfaceRw, + "password": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + Description: "Cleartext password for the supplicant.", + }, + "status": { + Type: schema.TypeString, + Computed: true, + }, + } + + return &schema.Resource{ + CreateContext: DefaultCreate(resSchema), + ReadContext: DefaultRead(resSchema), + UpdateContext: DefaultUpdate(resSchema), + DeleteContext: DefaultDelete(resSchema), + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: resSchema, + } +} + +// https://help.mikrotik.com/docs/display/ROS/Dot1X#Dot1X-Server +func ResourceInterfaceDot1xServerV0() *schema.Resource { + resSchema := map[string]*schema.Schema{ + MetaResourcePath: PropResourcePath("/interface/dot1x/server"), + MetaId: PropId(Id), + + "accounting": { + Type: schema.TypeBool, + Optional: true, + Default: true, + Description: "Whether to send RADIUS accounting requests to the authentication server.", + }, + "auth_timeout": { + Type: schema.TypeString, + Optional: true, + Default: "1m", + Description: "Total time available for EAP authentication.", + DiffSuppressFunc: TimeEquall, + }, + "auth_types": { + Type: schema.TypeString, + Optional: true, + Default: "dot1x", + Description: "Used authentication type on a server interface. Comma-separated list of `dot1x` and `mac-auth`.", + }, + KeyComment: PropCommentRw, + KeyDisabled: PropDisabledRw, + "guest_vlan_id": { + Type: schema.TypeInt, + Optional: true, + Description: "Assigned VLAN when end devices do not support dot1x authentication and no mac-auth fallback is configured.", + ValidateFunc: validation.IntBetween(1, 4094), + }, + KeyInterface: PropInterfaceRw, + "interim_update": { + Type: schema.TypeString, + Optional: true, + Default: "0s", + Description: "Interval between scheduled RADIUS Interim-Update messages.", + DiffSuppressFunc: TimeEquall, + }, + "mac_auth_mode": { + Type: schema.TypeString, + Optional: true, + Default: "mac-as-username", + Description: "An option that allows to control User-Name and User-Password RADIUS attributes when using MAC authentication.", + ValidateFunc: validation.StringInSlice([]string{"mac-as-username", "mac-as-username-and-password"}, false), + }, + "radius_mac_format": { + Type: schema.TypeString, + Optional: true, + Default: "XX:XX:XX:XX:XX:XX", + Description: "An option that controls how the MAC address of the client is encoded in the User-Name and User-Password attributes when using MAC authentication.", + ValidateFunc: validation.StringInSlice([]string{"XX-XX-XX-XX-XX-XX", "XX:XX:XX:XX:XX:XX", "XXXXXXXXXXXX", + "xx-xx-xx-xx-xx-xx", "xx:xx:xx:xx:xx:xx", "xxxxxxxxxxxx"}, false), + }, + "reauth_timeout": { + Type: schema.TypeString, + Optional: true, + Description: "An option that enables server port re-authentication.", + DiffSuppressFunc: TimeEquall, + }, + "reject_vlan_id": { + Type: schema.TypeInt, + Optional: true, + Description: "Assigned VLAN when authentication failed, and a RADIUS server responded with an Access-Reject message. ", + ValidateFunc: validation.IntBetween(1, 4094), + }, + "retrans_timeout": { + Type: schema.TypeString, + Optional: true, + Default: "30s", + Description: "The time interval between message re-transmissions if no response is received from the supplicant.", + DiffSuppressFunc: TimeEquall, + }, + "server_fail_vlan_id": { + Type: schema.TypeInt, + Optional: true, + Description: "Assigned VLAN when RADIUS server is not responding and request timed out.", + ValidateFunc: validation.IntBetween(1, 4094), + }, + } + + return &schema.Resource{ + CreateContext: DefaultCreate(resSchema), + ReadContext: DefaultRead(resSchema), + UpdateContext: DefaultUpdate(resSchema), + DeleteContext: DefaultDelete(resSchema), + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: resSchema, + } +} From 3ec3d343d9ed0bf501624fae89aae5840da8fa8e Mon Sep 17 00:00:00 2001 From: Michael Dokolin Date: Mon, 17 Jun 2024 08:11:44 +0200 Subject: [PATCH 07/10] feat: Update the `vlan_ids` property in `routeros_interface_bridge_vlan` to support multiple values --- docs/resources/interface_bridge_vlan.md | 2 +- .../resource.tf | 2 +- routeros/resource_interface_bridge_vlan.go | 13 ++- .../resource_interface_bridge_vlan_test.go | 2 +- routeros/resource_interface_bridge_vlan_v0.go | 100 ++++++++++++++++++ 5 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 routeros/resource_interface_bridge_vlan_v0.go diff --git a/docs/resources/interface_bridge_vlan.md b/docs/resources/interface_bridge_vlan.md index 7e7afabd..20043bb9 100644 --- a/docs/resources/interface_bridge_vlan.md +++ b/docs/resources/interface_bridge_vlan.md @@ -4,7 +4,7 @@ ## Example Usage ```terraform resource "routeros_interface_bridge_vlan" "bridge_vlan" { - vlan_ids = "50" + vlan_ids = ["50"] bridge = "bridge" tagged = [ "bridge", diff --git a/examples/resources/routeros_interface_bridge_vlan/resource.tf b/examples/resources/routeros_interface_bridge_vlan/resource.tf index e8d04a76..6304b228 100644 --- a/examples/resources/routeros_interface_bridge_vlan/resource.tf +++ b/examples/resources/routeros_interface_bridge_vlan/resource.tf @@ -1,5 +1,5 @@ resource "routeros_interface_bridge_vlan" "bridge_vlan" { - vlan_ids = "50" + vlan_ids = ["50"] bridge = "bridge" tagged = [ "bridge", diff --git a/routeros/resource_interface_bridge_vlan.go b/routeros/resource_interface_bridge_vlan.go index c5661d2b..d080bf21 100644 --- a/routeros/resource_interface_bridge_vlan.go +++ b/routeros/resource_interface_bridge_vlan.go @@ -79,8 +79,11 @@ func ResourceInterfaceBridgeVlan() *schema.Resource { "separated values. E.g. untagged=ether3,ether4", }, "vlan_ids": { - Type: schema.TypeString, + Type: schema.TypeSet, Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, Description: "The list of VLAN IDs for certain port configuration. This setting accepts VLAN ID range " + "as well as comma separated values. E.g. vlan-ids=100-115,120,122,128-130.", }, @@ -96,5 +99,13 @@ func ResourceInterfaceBridgeVlan() *schema.Resource { }, Schema: resSchema, + SchemaVersion: 1, + StateUpgraders: []schema.StateUpgrader{ + { + Type: ResourceInterfaceBridgeVlanV0().CoreConfigSchema().ImpliedType(), + Upgrade: stateMigrationScalarToList("vlan_ids"), + Version: 0, + }, + }, } } diff --git a/routeros/resource_interface_bridge_vlan_test.go b/routeros/resource_interface_bridge_vlan_test.go index 09ca8590..59930816 100644 --- a/routeros/resource_interface_bridge_vlan_test.go +++ b/routeros/resource_interface_bridge_vlan_test.go @@ -41,7 +41,7 @@ resource "routeros_interface_bridge_vlan" "test_vlan" { bridge = "bridge" untagged = ["ether1"] tagged = ["bridge"] - vlan_ids = 200 + vlan_ids = [200] } ` diff --git a/routeros/resource_interface_bridge_vlan_v0.go b/routeros/resource_interface_bridge_vlan_v0.go new file mode 100644 index 00000000..2ddce565 --- /dev/null +++ b/routeros/resource_interface_bridge_vlan_v0.go @@ -0,0 +1,100 @@ +package routeros + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +/* +[ + { + ".id": "*1", + "bridge": "bridge", + "comment": "Management", + "current-tagged": "bridge,ether2,ether3", + "current-untagged": "", + "disabled": "false", + "dynamic": "false", + "tagged": "ether2,ether4,ether5,bridge,ether3", + "untagged": "", + "vlan-ids": "2" + }, + {...} +] +*/ + +// ResourceInterfaceBridgeVlan https://wiki.mikrotik.com/wiki/Manual:Interface/Bridge#Bridge_VLAN_Filtering +func ResourceInterfaceBridgeVlanV0() *schema.Resource { + resSchema := map[string]*schema.Schema{ + MetaResourcePath: PropResourcePath("/interface/bridge/vlan"), + MetaId: PropId(Id), + MetaSkipFields: PropSkipFields("debug_info"), + + "bridge": { + Type: schema.TypeString, + Required: true, + Description: "The bridge interface which the respective VLAN entry is intended for.", + }, + KeyComment: PropCommentRw, + "current_tagged": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "current_untagged": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + KeyDisabled: PropDisabledRw, + KeyDynamic: PropDynamicRo, + "mvrp_forbidden": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: "Ports that ignore all MRP messages and remains Not Registered (MT), as well as disables applicant from declaring specific VLAN ID (available since RouterOS 7.15).", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "tagged": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: "Interface list with a VLAN tag adding action in egress. This setting accepts comma " + + "separated values. E.g. tagged=ether1,ether2.", + }, + "untagged": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: "Interface list with a VLAN tag removing action in egress. This setting accepts comma " + + "separated values. E.g. untagged=ether3,ether4", + }, + "vlan_ids": { + Type: schema.TypeString, + Required: true, + Description: "The list of VLAN IDs for certain port configuration. This setting accepts VLAN ID range " + + "as well as comma separated values. E.g. vlan-ids=100-115,120,122,128-130.", + }, + } + + return &schema.Resource{ + CreateContext: DefaultCreate(resSchema), + ReadContext: DefaultRead(resSchema), + UpdateContext: DefaultUpdate(resSchema), + DeleteContext: DefaultDelete(resSchema), + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: resSchema, + } +} From eb8ff28b6c125f20c45ebb783ed248a28f72b935 Mon Sep 17 00:00:00 2001 From: Michael Dokolin Date: Wed, 19 Jun 2024 08:52:13 +0200 Subject: [PATCH 08/10] fix: Fix `tagged` and `untagged` properties in `routeros_interface_bridge_vlan` to ignore values order --- routeros/resource_interface_bridge_vlan.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/routeros/resource_interface_bridge_vlan.go b/routeros/resource_interface_bridge_vlan.go index d080bf21..b7658df7 100644 --- a/routeros/resource_interface_bridge_vlan.go +++ b/routeros/resource_interface_bridge_vlan.go @@ -61,7 +61,7 @@ func ResourceInterfaceBridgeVlan() *schema.Resource { DiffSuppressFunc: AlwaysPresentNotUserProvided, }, "tagged": { - Type: schema.TypeList, + Type: schema.TypeSet, Optional: true, Elem: &schema.Schema{ Type: schema.TypeString, @@ -70,7 +70,7 @@ func ResourceInterfaceBridgeVlan() *schema.Resource { "separated values. E.g. tagged=ether1,ether2.", }, "untagged": { - Type: schema.TypeList, + Type: schema.TypeSet, Optional: true, Elem: &schema.Schema{ Type: schema.TypeString, From 14a6e4e45a91be4f4811f97a7696c25460e1313b Mon Sep 17 00:00:00 2001 From: Michael Dokolin Date: Fri, 21 Jun 2024 07:16:48 +0200 Subject: [PATCH 09/10] feat: Update the frequency properties in `routeros_capsman_channel` to support multiple values --- docs/resources/capsman_channel.md | 4 +- docs/resources/capsman_configuration.md | 4 +- docs/resources/capsman_interface.md | 2 +- .../routeros_capsman_channel/resource.tf | 4 +- .../routeros_capsman_interface/resource.tf | 2 +- routeros/resource_capsman_channel.go | 22 ++-- routeros/resource_capsman_channel_test.go | 4 +- routeros/resource_capsman_channel_v1.go | 114 ++++++++++++++++++ 8 files changed, 139 insertions(+), 17 deletions(-) create mode 100644 routeros/resource_capsman_channel_v1.go diff --git a/docs/resources/capsman_channel.md b/docs/resources/capsman_channel.md index 9849126b..dc331f55 100644 --- a/docs/resources/capsman_channel.md +++ b/docs/resources/capsman_channel.md @@ -9,10 +9,10 @@ resource "routeros_capsman_channel" "test_channel" { band = "2ghz-b/g/n" control_channel_width = "10mhz" extension_channel = "eCee" - frequency = 2412 + frequency = [2412] reselect_interval = "1h" save_selected = true - secondary_frequency = "disabled" + secondary_frequency = ["disabled"] skip_dfs_channels = true tx_power = 20 } diff --git a/docs/resources/capsman_configuration.md b/docs/resources/capsman_configuration.md index a4b58e5a..65e36331 100644 --- a/docs/resources/capsman_configuration.md +++ b/docs/resources/capsman_configuration.md @@ -49,10 +49,10 @@ resource "routeros_capsman_configuration" "test_configuration_2" { band = "2ghz-b/g/n" control_channel_width = "10mhz" extension_channel = "eCee" - frequency = 2412 + frequency = [2412] reselect_interval = "1h" save_selected = "true" - secondary_frequency = "disabled" + secondary_frequency = ["disabled"] skip_dfs_channels = "true" tx_power = 20 } diff --git a/docs/resources/capsman_interface.md b/docs/resources/capsman_interface.md index a4205244..1840fce7 100644 --- a/docs/resources/capsman_interface.md +++ b/docs/resources/capsman_interface.md @@ -6,7 +6,7 @@ resource "routeros_capsman_channel" "channel1" { name = "1" band = "2ghz-g/n" - frequency = 2412 + frequency = [2412] } resource "routeros_capsman_interface" "cap1" { diff --git a/examples/resources/routeros_capsman_channel/resource.tf b/examples/resources/routeros_capsman_channel/resource.tf index 1e017f0e..f4a23a1a 100644 --- a/examples/resources/routeros_capsman_channel/resource.tf +++ b/examples/resources/routeros_capsman_channel/resource.tf @@ -4,10 +4,10 @@ resource "routeros_capsman_channel" "test_channel" { band = "2ghz-b/g/n" control_channel_width = "10mhz" extension_channel = "eCee" - frequency = 2412 + frequency = [2412] reselect_interval = "1h" save_selected = true - secondary_frequency = "disabled" + secondary_frequency = ["disabled"] skip_dfs_channels = true tx_power = 20 } diff --git a/examples/resources/routeros_capsman_interface/resource.tf b/examples/resources/routeros_capsman_interface/resource.tf index 654eeefd..3a127585 100644 --- a/examples/resources/routeros_capsman_interface/resource.tf +++ b/examples/resources/routeros_capsman_interface/resource.tf @@ -1,7 +1,7 @@ resource "routeros_capsman_channel" "channel1" { name = "1" band = "2ghz-g/n" - frequency = 2412 + frequency = [2412] } resource "routeros_capsman_interface" "cap1" { diff --git a/routeros/resource_capsman_channel.go b/routeros/resource_capsman_channel.go index a3e9e5c0..55f7e7d4 100644 --- a/routeros/resource_capsman_channel.go +++ b/routeros/resource_capsman_channel.go @@ -23,7 +23,6 @@ import ( // https://help.mikrotik.com/docs/display/ROS/CAPsMAN func ResourceCapsManChannel() *schema.Resource { - resSchema := map[string]*schema.Schema{ MetaResourcePath: PropResourcePath("/caps-man/channel"), MetaId: PropId(Id), @@ -52,9 +51,11 @@ func ResourceCapsManChannel() *schema.Resource { "xx", "xxxx", "xxxxxxxx", "disabled"}, false), }, "frequency": { - Type: schema.TypeInt, + Type: schema.TypeList, Optional: true, - Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeInt, + }, Description: "Channel frequency value in MHz on which AP will operate. If left blank, CAPsMAN will " + "automatically determine the best frequency that is least occupied.", }, @@ -74,8 +75,11 @@ func ResourceCapsManChannel() *schema.Resource { "saves the last picked frequency.", }, "secondary_frequency": { - Type: schema.TypeString, + Type: schema.TypeList, Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, Description: "Specifies the second frequency that will be used for 80+80MHz configuration. " + "Set it to Disabled in order to disable 80+80MHz capability.", }, @@ -109,15 +113,19 @@ func ResourceCapsManChannel() *schema.Resource { StateContext: schema.ImportStatePassthroughContext, }, - SchemaVersion: 1, + Schema: resSchema, + SchemaVersion: 2, StateUpgraders: []schema.StateUpgrader{ { Type: ResourceCapsManChannelV0().CoreConfigSchema().ImpliedType(), Upgrade: stateMigrationNameToId(resSchema[MetaResourcePath].Default.(string)), Version: 0, }, + { + Type: ResourceCapsManChannelV1().CoreConfigSchema().ImpliedType(), + Upgrade: stateMigrationScalarToList("frequency", "secondary_frequency"), + Version: 1, + }, }, - - Schema: resSchema, } } diff --git a/routeros/resource_capsman_channel_test.go b/routeros/resource_capsman_channel_test.go index 07aced00..40a13550 100644 --- a/routeros/resource_capsman_channel_test.go +++ b/routeros/resource_capsman_channel_test.go @@ -47,10 +47,10 @@ resource "routeros_capsman_channel" "test_channel" { band = "2ghz-b/g/n" control_channel_width = "10mhz" extension_channel = "eCee" - frequency = 2412 + frequency = [2412] reselect_interval = "1h" save_selected = true - secondary_frequency = "disabled" + secondary_frequency = ["disabled"] skip_dfs_channels = true tx_power = 20 } diff --git a/routeros/resource_capsman_channel_v1.go b/routeros/resource_capsman_channel_v1.go new file mode 100644 index 00000000..6a9cd4ef --- /dev/null +++ b/routeros/resource_capsman_channel_v1.go @@ -0,0 +1,114 @@ +package routeros + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +/* + { + ".id": "*1", + "band": "2ghz-b", + "control-channel-width": "5mhz", + "extension-channel": "disabled", + "frequency": "2112", + "name": "channel1", + "reselect-interval": "10m", + "save-selected": "true", + "secondary-frequency": "disabled", + "skip-dfs-channels": "true", + "tx-power": "-20" + } +*/ + +// https://help.mikrotik.com/docs/display/ROS/CAPsMAN +func ResourceCapsManChannelV1() *schema.Resource { + + resSchema := map[string]*schema.Schema{ + MetaResourcePath: PropResourcePath("/caps-man/channel"), + MetaId: PropId(Id), + + "band": { + Type: schema.TypeString, + Optional: true, + Description: "Define operational radio frequency band and mode taken from hardware capability of wireless card.", + ValidateFunc: validation.StringInSlice([]string{"2ghz-b", "2ghz-b/g", "2ghz-b/g/n", "2ghz-g/n", "2ghz-onlyg", "2ghz-onlyn", + "5ghz-a", "5ghz-a/n", "5ghz-a/n/ac", "5ghz-n/ac", "5ghz-onlyac", "5ghz-onlyn"}, false), + }, + KeyComment: PropCommentRw, + "control_channel_width": { + Type: schema.TypeString, + Optional: true, + Description: "Control channel width.", + ValidateFunc: validation.StringInSlice([]string{"5mhz", "10mhz", "20mhz", "40mhz-turbo"}, false), + }, + "extension_channel": { + Type: schema.TypeString, + Optional: true, + Description: "Extension channel configuration. (E.g. Ce = extension channel is above Control channel, " + + "eC = extension channel is below Control channel)", + ValidateFunc: validation.StringInSlice([]string{"Ce", "Ceee", "Ceeeeeee", "eC", "eCee", "eCeeeeee", + "eeCe", "eeCeeeee", "eeeC", "eeeCeeee", "eeeeCeee", "eeeeeCee", "eeeeeeCe", "eeeeeeeC", + "xx", "xxxx", "xxxxxxxx", "disabled"}, false), + }, + "frequency": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: "Channel frequency value in MHz on which AP will operate. If left blank, CAPsMAN will " + + "automatically determine the best frequency that is least occupied.", + }, + KeyName: PropNameForceNewRw, + "reselect_interval": { + Type: schema.TypeString, + Optional: true, + Description: "The interval after which the least occupied frequency is chosen, can be defined as a random " + + "interval, ex. as '30m..60m'. Works only if channel.frequency is left blank.", + // We may need to write a custom DiffSuppressFunc. + // DiffSuppressFunc: TimeEquall, not for time ranges + }, + "save_selected": { + Type: schema.TypeBool, + Optional: true, + Description: "If channel frequency is chosen automatically and channel.reselect-interval is used, then " + + "saves the last picked frequency.", + }, + "secondary_frequency": { + Type: schema.TypeString, + Optional: true, + Description: "Specifies the second frequency that will be used for 80+80MHz configuration. " + + "Set it to Disabled in order to disable 80+80MHz capability.", + }, + "skip_dfs_channels": { + Type: schema.TypeBool, + Optional: true, + Description: "If channel.frequency is left blank, the selection will skip DFS channels.", + }, + "tx_power": { + Type: schema.TypeInt, + Optional: true, + Description: "TX Power for CAP interface (for the whole interface not for individual chains) in dBm. " + + "It is not possible to set higher than allowed by country regulations or interface. By " + + "default max allowed by country or interface is used.", + ValidateFunc: validation.IntBetween(-30, 40), + }, + "width": { + Type: schema.TypeString, + Optional: true, + Description: "Channel Width in MHz.", + }, + } + + return &schema.Resource{ + CreateContext: DefaultCreate(resSchema), + ReadContext: DefaultRead(resSchema), + UpdateContext: DefaultUpdate(resSchema), + DeleteContext: DefaultDelete(resSchema), + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: resSchema, + } +} From 27c858806bdf9f79bf96a4cd5e85746abc2b2e99 Mon Sep 17 00:00:00 2001 From: Michael Dokolin Date: Sat, 29 Jun 2024 07:25:13 +0200 Subject: [PATCH 10/10] fix: Add state migrator to `routeros_snmp_community` to fix backward compatibility --- routeros/resource_snmp_community.go | 8 ++ routeros/resource_snmp_community_v0.go | 110 +++++++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 routeros/resource_snmp_community_v0.go diff --git a/routeros/resource_snmp_community.go b/routeros/resource_snmp_community.go index aaddac9a..1379332e 100644 --- a/routeros/resource_snmp_community.go +++ b/routeros/resource_snmp_community.go @@ -114,5 +114,13 @@ func ResourceSNMPCommunity() *schema.Resource { }, Schema: resSchema, + SchemaVersion: 1, + StateUpgraders: []schema.StateUpgrader{ + { + Type: ResourceSNMPCommunityV0().CoreConfigSchema().ImpliedType(), + Upgrade: stateMigrationScalarToList("addresses"), + Version: 0, + }, + }, } } diff --git a/routeros/resource_snmp_community_v0.go b/routeros/resource_snmp_community_v0.go new file mode 100644 index 00000000..1ca0794a --- /dev/null +++ b/routeros/resource_snmp_community_v0.go @@ -0,0 +1,110 @@ +package routeros + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +/* + { + ".id": "*2", + "addresses": "::/0", + "authentication-password": "", + "authentication-protocol": "MD5", + "comment": "Comment", + "default": "false", + "disabled": "true", + "encryption-password": "", + "encryption-protocol": "DES", + "name": "private", + "read-access": "true", + "security": "none", + "write-access": "false" + } +*/ + +// https://help.mikrotik.com/docs/display/ROS/SNMP#SNMP-CommunityProperties +func ResourceSNMPCommunityV0() *schema.Resource { + resSchema := map[string]*schema.Schema{ + MetaResourcePath: PropResourcePath("/snmp/community"), + MetaId: PropId(Id), + + "addresses": { + Type: schema.TypeString, + Optional: true, + Description: "Set of IP (v4 or v6) addresses or CIDR networks from which connections to SNMP server are allowed.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "authentication_password": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + Description: "Password used to authenticate the connection to the server (SNMPv3).", + }, + "authentication_protocol": { + Type: schema.TypeString, + Optional: true, + Default: "MD5", + Description: "The protocol used for authentication (SNMPv3).", + ValidateFunc: validation.StringInSlice([]string{"MD5", "SHA1"}, false), + }, + KeyComment: PropCommentRw, + "default": { + Type: schema.TypeBool, + Computed: true, + Description: "It's a default community.", + }, + KeyDisabled: PropDisabledRw, + "encryption_password": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + Description: "The password used for encryption (SNMPv3).", + }, + "encryption_protocol": { + Type: schema.TypeString, + Optional: true, + Default: "DES", + Description: "encryption protocol to be used to encrypt the communication (SNMPv3). AES (see rfc3826) " + + "available since v6.16.", + ValidateFunc: validation.StringInSlice([]string{"DES", "AES"}, false), + }, + + "name": { + Type: schema.TypeString, + Optional: true, + Description: "Community Name.", + }, + "read_access": { + Type: schema.TypeBool, + Optional: true, + Default: true, + Description: "Whether read access is enabled for this community.", + }, + "security": { + Type: schema.TypeString, + Optional: true, + Default: "none", + Description: "Security features.", + ValidateFunc: validation.StringInSlice([]string{"authorized", "none", "private"}, false), + }, + "write_access": { + Type: schema.TypeBool, + Optional: true, + Description: "Whether write access is enabled for this community.", + }, + } + + return &schema.Resource{ + CreateContext: DefaultCreate(resSchema), + ReadContext: DefaultRead(resSchema), + UpdateContext: DefaultUpdate(resSchema), + DeleteContext: DefaultDelete(resSchema), + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: resSchema, + } +}