From d1d2a22f341fcba65cde5898318c4851cb1c7f35 Mon Sep 17 00:00:00 2001 From: Vaerh Date: Mon, 5 Aug 2024 12:32:20 +0300 Subject: [PATCH 1/5] Add Validation64k helper --- routeros/provider_schema_helpers.go | 2 ++ routeros/resource_bgp_connection.go | 4 +-- routeros/resource_interface_bridge.go | 2 +- routeros/resource_interface_bridge_v0.go | 2 +- ...resource_interface_ethernet_switch_rule.go | 4 +-- routeros/resource_interface_wireguard_peer.go | 2 +- routeros/resource_ip_dns_record.go | 2 +- routeros/resource_radius.go | 10 +++--- routeros/resource_radius_v0.go | 4 +-- ...esource_routing_ospf_interface_template.go | 2 +- routeros/resource_user_manager_router.go | 3 +- routeros/resource_wifi_interworking.go | 14 ++++---- routeros/resource_wifi_security.go | 32 +++++++++---------- 13 files changed, 42 insertions(+), 41 deletions(-) diff --git a/routeros/provider_schema_helpers.go b/routeros/provider_schema_helpers.go index 0330a017..07c3772d 100644 --- a/routeros/provider_schema_helpers.go +++ b/routeros/provider_schema_helpers.go @@ -495,6 +495,8 @@ func PropMtuRw() *schema.Schema { // Properties validation. var ( + Validation64k = validation.IntBetween(0, 65535) + ValidationTime = validation.StringMatch(regexp.MustCompile(`^(\d+([smhdw]|ms)?)+$`), "value should be an integer or a time interval: 0..4294967295 (seconds) or 500ms, 2d, 1w") diff --git a/routeros/resource_bgp_connection.go b/routeros/resource_bgp_connection.go index c198f525..7519eff8 100644 --- a/routeros/resource_bgp_connection.go +++ b/routeros/resource_bgp_connection.go @@ -281,7 +281,7 @@ func ResourceRoutingBGPConnection() *schema.Resource { Optional: true, Default: 179, Description: "Local connection port.", - ValidateFunc: validation.IntBetween(0, 65535), + ValidateFunc: Validation64k, }, "role": { Type: schema.TypeString, @@ -468,7 +468,7 @@ func ResourceRoutingBGPConnection() *schema.Resource { Optional: true, Description: "Local connection port.", Default: 179, - ValidateFunc: validation.IntBetween(0, 65535), + ValidateFunc: Validation64k, }, "ttl": { Type: schema.TypeInt, diff --git a/routeros/resource_interface_bridge.go b/routeros/resource_interface_bridge.go index 2009cdd6..b08fb8f6 100644 --- a/routeros/resource_interface_bridge.go +++ b/routeros/resource_interface_bridge.go @@ -268,7 +268,7 @@ func ResourceInterfaceBridge() *schema.Resource { Type: schema.TypeInt, Optional: true, Description: "MSTP configuration revision number. This property only has effect when protocol-mode is set to mstp.", - ValidateFunc: validation.IntBetween(0, 65535), + ValidateFunc: Validation64k, }, "startup_query_count": { Type: schema.TypeInt, diff --git a/routeros/resource_interface_bridge_v0.go b/routeros/resource_interface_bridge_v0.go index b29b48c1..50ebef6c 100644 --- a/routeros/resource_interface_bridge_v0.go +++ b/routeros/resource_interface_bridge_v0.go @@ -258,7 +258,7 @@ func ResourceInterfaceBridgeV0() *schema.Resource { Type: schema.TypeInt, Optional: true, Description: "MSTP configuration revision number. This property only has effect when protocol-mode is set to mstp.", - ValidateFunc: validation.IntBetween(0, 65535), + ValidateFunc: Validation64k, }, "startup_query_count": { Type: schema.TypeInt, diff --git a/routeros/resource_interface_ethernet_switch_rule.go b/routeros/resource_interface_ethernet_switch_rule.go index 39b0a018..30fc42bc 100644 --- a/routeros/resource_interface_ethernet_switch_rule.go +++ b/routeros/resource_interface_ethernet_switch_rule.go @@ -45,7 +45,7 @@ func ResourceInterfaceEthernetSwitchRule() *schema.Resource { Type: schema.TypeInt, Optional: true, Description: "Matching destination protocol port number or range.", - ValidateFunc: validation.IntBetween(0, 65535), + ValidateFunc: Validation64k, }, "dscp": { Type: schema.TypeInt, @@ -142,7 +142,7 @@ func ResourceInterfaceEthernetSwitchRule() *schema.Resource { Type: schema.TypeInt, Optional: true, Description: "Matching source protocol port number or range.", - ValidateFunc: validation.IntBetween(0, 65535), + ValidateFunc: Validation64k, }, "switch": { Type: schema.TypeString, diff --git a/routeros/resource_interface_wireguard_peer.go b/routeros/resource_interface_wireguard_peer.go index c3bc587b..a9067a8a 100644 --- a/routeros/resource_interface_wireguard_peer.go +++ b/routeros/resource_interface_wireguard_peer.go @@ -55,7 +55,7 @@ func ResourceInterfaceWireguardPeer() *schema.Resource { Optional: true, Description: "The local port upon which this WireGuard tunnel will listen for incoming traffic from peers, " + "and the port from which it will source outgoing packets.", - ValidateFunc: validation.IntBetween(0, 65535), + ValidateFunc: Validation64k, DiffSuppressFunc: AlwaysPresentNotUserProvided, }, "current_endpoint_address": { diff --git a/routeros/resource_ip_dns_record.go b/routeros/resource_ip_dns_record.go index 56ccd187..e73c0d1a 100644 --- a/routeros/resource_ip_dns_record.go +++ b/routeros/resource_ip_dns_record.go @@ -116,7 +116,7 @@ func ResourceDnsRecord() *schema.Resource { Optional: true, Computed: true, Description: "The TCP or UDP port on which the service is to be found.", - ValidateFunc: validation.IntBetween(0, 65535), + ValidateFunc: Validation64k, RequiredWith: []string{"srv_target"}, }, "srv_priority": { diff --git a/routeros/resource_radius.go b/routeros/resource_radius.go index ea80ca1c..b678f5c1 100644 --- a/routeros/resource_radius.go +++ b/routeros/resource_radius.go @@ -22,7 +22,7 @@ func ResourceRadius() *schema.Resource { Optional: true, Default: 1813, Description: "RADIUS server port used for accounting.", - ValidateFunc: validation.IntBetween(0, 65535), + ValidateFunc: Validation64k, }, "address": { Type: schema.TypeString, @@ -35,7 +35,7 @@ func ResourceRadius() *schema.Resource { Optional: true, Default: 1812, Description: "RADIUS server port used for authentication.", - ValidateFunc: validation.IntBetween(0, 65535), + ValidateFunc: Validation64k, }, "called_id": { Type: schema.TypeString, @@ -116,11 +116,11 @@ func ResourceRadius() *schema.Resource { StateContext: schema.ImportStatePassthroughContext, }, - Schema: resSchema, + Schema: resSchema, SchemaVersion: 1, StateUpgraders: []schema.StateUpgrader{ { - Type: ResourceRadiusV0().CoreConfigSchema().ImpliedType(), + Type: ResourceRadiusV0().CoreConfigSchema().ImpliedType(), Upgrade: stateMigrationScalarToList("service"), Version: 0, }, @@ -145,7 +145,7 @@ func ResourceRadiusIncoming() *schema.Resource { Optional: true, Default: 3799, Description: "The port number to listen for the requests on.", - ValidateFunc: validation.IntBetween(0, 65535), + ValidateFunc: Validation64k, }, "vrf": { Type: schema.TypeString, diff --git a/routeros/resource_radius_v0.go b/routeros/resource_radius_v0.go index d028fe88..b00a4b80 100644 --- a/routeros/resource_radius_v0.go +++ b/routeros/resource_radius_v0.go @@ -22,7 +22,7 @@ func ResourceRadiusV0() *schema.Resource { Optional: true, Default: 1813, Description: "RADIUS server port used for accounting.", - ValidateFunc: validation.IntBetween(0, 65535), + ValidateFunc: Validation64k, }, "address": { Type: schema.TypeString, @@ -35,7 +35,7 @@ func ResourceRadiusV0() *schema.Resource { Optional: true, Default: 1812, Description: "RADIUS server port used for authentication.", - ValidateFunc: validation.IntBetween(0, 65535), + ValidateFunc: Validation64k, }, "called_id": { Type: schema.TypeString, diff --git a/routeros/resource_routing_ospf_interface_template.go b/routeros/resource_routing_ospf_interface_template.go index 3831feda..233ec1d7 100644 --- a/routeros/resource_routing_ospf_interface_template.go +++ b/routeros/resource_routing_ospf_interface_template.go @@ -75,7 +75,7 @@ func ResourceRoutingOspfInterfaceTemplate() *schema.Resource { Optional: true, Default: 1, Description: "Interface cost expressed as link state metric.", - ValidateFunc: validation.IntBetween(0, 65535), + ValidateFunc: Validation64k, }, "dead_interval": { Type: schema.TypeString, diff --git a/routeros/resource_user_manager_router.go b/routeros/resource_user_manager_router.go index a5bbfdbf..b97ebaaf 100644 --- a/routeros/resource_user_manager_router.go +++ b/routeros/resource_user_manager_router.go @@ -2,7 +2,6 @@ package routeros import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) /* @@ -33,7 +32,7 @@ func ResourceUserManagerRouter() *schema.Resource { Optional: true, Default: 3799, Description: "Port number of CoA (Change of Authorization) communication.", - ValidateFunc: validation.IntBetween(0, 65535), + ValidateFunc: Validation64k, }, KeyDisabled: PropDisabledRw, KeyName: PropName("Unique name of the RADIUS client."), diff --git a/routeros/resource_wifi_interworking.go b/routeros/resource_wifi_interworking.go index 605bcfa9..fa9c3f60 100644 --- a/routeros/resource_wifi_interworking.go +++ b/routeros/resource_wifi_interworking.go @@ -161,9 +161,9 @@ func ResourceWifiInterworking() *schema.Resource { Description: "An option to enable Unauthenticated Emergency Service Accessibility.", }, "venue": { - Type: schema.TypeString, - Optional: true, - Description: "Information about the venue in which the Access Point is located.", + Type: schema.TypeString, + Optional: true, + Description: "Information about the venue in which the Access Point is located.", }, "venue_names": { Type: schema.TypeList, @@ -188,10 +188,10 @@ func ResourceWifiInterworking() *schema.Resource { ValidateFunc: validation.IntBetween(0, 255), }, "wan_measurement_duration": { - Type: schema.TypeInt, - Optional: true, - Description: "The duration during which `wan_downlink_load` and `wan_uplink_load` are measured.", - ValidateFunc: validation.IntBetween(0, 65535), + Type: schema.TypeInt, + Optional: true, + Description: "The duration during which `wan_downlink_load` and `wan_uplink_load` are measured.", + ValidateFunc: Validation64k, }, "wan_status": { Type: schema.TypeString, diff --git a/routeros/resource_wifi_security.go b/routeros/resource_wifi_security.go index 57200964..389ed57f 100644 --- a/routeros/resource_wifi_security.go +++ b/routeros/resource_wifi_security.go @@ -51,9 +51,9 @@ func ResourceWifiSecurity() *schema.Resource { MetaId: PropId(Id), "authentication_types": { - Type: schema.TypeSet, - Optional: true, - Elem: &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ Type: schema.TypeString, ValidateFunc: validation.StringInSlice([]string{"wpa-psk", "wpa2-psk", "wpa-eap", "wpa2-eap", "wpa3-psk", "owe", "wpa3-eap", "wpa3-eap-192"}, false), }, @@ -71,9 +71,9 @@ func ResourceWifiSecurity() *schema.Resource { Description: "An option to determine how a connection is handled if the MAC address of the client device is the same as that of another active connection to another AP.", }, "dh_groups": { - Type: schema.TypeSet, - Optional: true, - Elem: &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ Type: schema.TypeInt, ValidateFunc: validation.IntInSlice([]int{19, 20, 21}), }, @@ -126,9 +126,9 @@ func ResourceWifiSecurity() *schema.Resource { Description: "Username to use when the chosen EAP method requires one. ", }, "encryption": { - Type: schema.TypeSet, - Optional: true, - Elem: &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ Type: schema.TypeString, ValidateFunc: validation.StringInSlice([]string{"ccmp", "ccmp-256", "gcmp", "gcmp-256", "tkip"}, false), }, @@ -143,12 +143,12 @@ func ResourceWifiSecurity() *schema.Resource { Type: schema.TypeInt, Optional: true, Description: "The fast BSS transition mobility domain ID.", - ValidateFunc: validation.IntBetween(0, 65535), + ValidateFunc: Validation64k, }, "ft_nas_identifier": { - Type: schema.TypeString, - Optional: true, - Description: "Fast BSS transition PMK-R0 key holder identifier.", + Type: schema.TypeString, + Optional: true, + Description: "Fast BSS transition PMK-R0 key holder identifier.", ValidateFunc: validation.StringMatch(regexp.MustCompile(`^[0-9a-zA-Z]{2,96}$`), "Must be a string of 2 - 96 hex characters."), }, @@ -215,9 +215,9 @@ func ResourceWifiSecurity() *schema.Resource { Description: "A parameter to mitigate DoS attacks by specifying a threshold of in-progress SAE authentications.", }, "sae_max_failure_rate": { - Type: schema.TypeString, - Optional: true, - Description: "Rate of failed SAE (WPA3) associations per minute, at which the AP will stop processing new association requests.", + Type: schema.TypeString, + Optional: true, + Description: "Rate of failed SAE (WPA3) associations per minute, at which the AP will stop processing new association requests.", }, "sae_pwe": { Type: schema.TypeString, From 8748be3ada67930c2df07fb676764df7fc97a858 Mon Sep 17 00:00:00 2001 From: Vaerh Date: Mon, 5 Aug 2024 12:38:11 +0300 Subject: [PATCH 2/5] feat: Add `routeros_tool_netwatch` resource Closes #487 --- .../routeros_tool_netwatch/import.sh | 3 + .../routeros_tool_netwatch/resource.tf | 6 + routeros/provider.go | 1 + routeros/resource_tool_netwatch.go | 236 ++++++++++++++++++ routeros/resource_tool_netwatch_test.go | 59 +++++ 5 files changed, 305 insertions(+) create mode 100644 examples/resources/routeros_tool_netwatch/import.sh create mode 100644 examples/resources/routeros_tool_netwatch/resource.tf create mode 100644 routeros/resource_tool_netwatch.go create mode 100644 routeros/resource_tool_netwatch_test.go diff --git a/examples/resources/routeros_tool_netwatch/import.sh b/examples/resources/routeros_tool_netwatch/import.sh new file mode 100644 index 00000000..af8e6c76 --- /dev/null +++ b/examples/resources/routeros_tool_netwatch/import.sh @@ -0,0 +1,3 @@ +#The ID can be found via API or the terminal +#The command for the terminal is -> :put [/tool/netwatch get [print show-ids]] +terraform import routeros_tool_netwatch.test *3 \ No newline at end of file diff --git a/examples/resources/routeros_tool_netwatch/resource.tf b/examples/resources/routeros_tool_netwatch/resource.tf new file mode 100644 index 00000000..9f18a74d --- /dev/null +++ b/examples/resources/routeros_tool_netwatch/resource.tf @@ -0,0 +1,6 @@ +resource "routeros_tool_netwatch" "test" { + name = "watch-google-pdns" + host = "8.8.8.8" + interval = "30s" + up_script = ":log info \"Ping to 8.8.8.8 successful\"" +} diff --git a/routeros/provider.go b/routeros/provider.go index 9793f1bd..1f5f9798 100644 --- a/routeros/provider.go +++ b/routeros/provider.go @@ -252,6 +252,7 @@ func Provider() *schema.Provider { "routeros_tool_bandwidth_server": ResourceToolBandwidthServer(), "routeros_tool_mac_server": ResourceToolMacServer(), "routeros_tool_mac_server_winbox": ResourceToolMacServerWinBox(), + "routeros_tool_netwatch": ResourceToolNetwatch(), // User Manager "routeros_user_manager_advanced": ResourceUserManagerAdvanced(), diff --git a/routeros/resource_tool_netwatch.go b/routeros/resource_tool_netwatch.go new file mode 100644 index 00000000..46d31380 --- /dev/null +++ b/routeros/resource_tool_netwatch.go @@ -0,0 +1,236 @@ +package routeros + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +/* + { + ".id": "*1", + "disabled": "true", + "down-script": "", + "host": "192.168.180.1", + "http-codes": "", + "name": "111", + "status": "unknown", + "test-script": "", + "type": "simple", + "up-script": "" + } +*/ + +// https://help.mikrotik.com/docs/display/ROS/Netwatch +func ResourceToolNetwatch() *schema.Resource { + resSchema := map[string]*schema.Schema{ + MetaResourcePath: PropResourcePath("/tool/netwatch"), + MetaId: PropId(Id), + + KeyComment: PropCommentRw, + KeyDisabled: PropDisabledRw, + "down_script": { + Type: schema.TypeString, + Optional: true, + Description: "Script to execute on the event of probe state change `OK` --> `fail`.", + }, + "host": { + Type: schema.TypeString, + Required: true, + Description: `The IP address of the server to be probed. Formats: + - ipv4 + - ipv4@vrf + - ipv6 + - ipv6@vrf + - ipv6-linklocal%interface + `, + }, + "interval": { + Type: schema.TypeString, + Optional: true, + Description: "The time interval between probe tests.", + DiffSuppressFunc: TimeEquall, + }, + KeyName: PropName("Task name."), + "src_address": { + Type: schema.TypeString, + Optional: true, + Description: "Source IP address which the Netwatch will try to use in order to reach the host. If address " + + "is not present, then the host will be considered as `down`.", + }, + "start_delay": { + Type: schema.TypeString, + Optional: true, + Description: "Time to wait before starting probe (on add, enable, or system start).", + DiffSuppressFunc: TimeEquall, + }, + "startup_delay": { + Type: schema.TypeString, + Optional: true, + Description: "Time to wait until starting Netwatch probe after system startup.", + DiffSuppressFunc: TimeEquall, + }, + "test_script": { + Type: schema.TypeString, + Optional: true, + Description: "Script to execute at the end of every probe test.", + }, + "timeout": { + Type: schema.TypeString, + Optional: true, + Description: "Max time limit to wait for a response.", + DiffSuppressFunc: TimeEquall, + }, + "type": { + Type: schema.TypeString, + Optional: true, + Description: `Type of the probe: + - icmp - (ping-style) series of ICMP request-response with statistics + - tcp-conn - test TCP connection (3-way handshake) to a server specified by IP and port + - http-get - do an HTTP Get request and test for a range of correct replies + - simple - simplified ICMP probe, with fewer options than **ICMP** type, used for backward compatibility with the older Netwatch version + `, + ValidateFunc: validation.StringInSlice([]string{"icmp", "tcp-conn", "http-get", "simple"}, false), + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "up_script": { + Type: schema.TypeString, + Optional: true, + Description: "Script to execute on the event of probe state change `fail` --> `OK`.", + }, + + // ICMP probe options + "accept_icmp_time_exceeded": { + Type: schema.TypeBool, + Optional: true, + Description: "If the ICMP `time exceeded` message should be considered a valid response.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "packet_count": { + Type: schema.TypeInt, + Optional: true, + Description: "Total count of ICMP packets to send out within a single test.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "packet_interval": { + Type: schema.TypeString, + Optional: true, + Description: "The time between ICMP-request packet send.", + DiffSuppressFunc: TimeEquall, + }, + "packet_size": { + Type: schema.TypeInt, + Optional: true, + Description: "The total size of the IP ICMP packet.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "thr_loss_count": { + Type: schema.TypeInt, + Optional: true, + Description: "Fail threshold for loss-count.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "thr_loss_percent": { + Type: schema.TypeFloat, + Optional: true, + Description: "Fail threshold for loss-percent.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "thr_rtt_avg": { + Type: schema.TypeString, + Optional: true, + Description: "Fail threshold for rtt-avg.", + DiffSuppressFunc: TimeEquall, + }, + "thr_rtt_jitter": { + Type: schema.TypeString, + Optional: true, + Description: "Fail threshold for rtt-jitter.", + DiffSuppressFunc: TimeEquall, + }, + "thr_rtt_max": { + Type: schema.TypeString, + Optional: true, + Description: "Fail threshold for rtt-max (a value above thr-max is a probe fail).", + DiffSuppressFunc: TimeEquall, + }, + "thr_rtt_stdev": { + Type: schema.TypeString, + Optional: true, + Description: "Fail threshold for rtt-stdev.", + DiffSuppressFunc: TimeEquall, + }, + "ttl": { + Type: schema.TypeInt, + Optional: true, + Description: "Manually set time to live value for ICMP packet.", + }, + + // TCP-CONNECT/HTTP-GET probe options + "port": { + Type: schema.TypeInt, + Optional: true, + Description: "TCP port (for both tcp-conn and http-get probes)", + ValidateFunc: Validation64k, + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + + // TCP-CONNECT pass-fail criteria + "thr_tcp_conn_time": { + Type: schema.TypeString, + Optional: true, + Description: "Fail threshold for tcp-connect-time, the configuration uses microseconds, if the time " + + "unit is not specified (s/m/h), log and status pages display the same value in milliseconds.", + DiffSuppressFunc: TimeEquall, + }, + + // HTTP-GET probe pass/fail criteria + "thr_http_time": { + Type: schema.TypeString, + Optional: true, + Description: "Fail threshold for http-resp-time.", + DiffSuppressFunc: TimeEquall, + }, + "http_code_min": { + Type: schema.TypeInt, + Optional: true, + Description: "OK/fail criteria for HTTP response code.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "http_code_max": { + Type: schema.TypeInt, + Optional: true, + Description: "Response in the range [http-code-min , http-code-max] is a probe pass/OK; outside - a " + + "probe fail. See [mozilla-http-status](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) or " + + "[rfc7231](https://datatracker.ietf.org/doc/html/rfc7231#section-6).", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + + // DNS probe options + "record_type": { + Type: schema.TypeString, + Optional: true, + Description: "Record type that will be used for DNS probe.", + ValidateFunc: validation.StringInSlice([]string{"A", "AAAA", "MX", "NS"}, false), + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "dns_server": { + Type: schema.TypeString, + Optional: true, + Description: "The DNS server that the probe should send its requests to, if not specified it will use the value from `/ip dns`.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + } + + return &schema.Resource{ + CreateContext: DefaultCreate(resSchema), + ReadContext: DefaultRead(resSchema), + UpdateContext: DefaultUpdate(resSchema), + DeleteContext: DefaultDelete(resSchema), + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: resSchema, + } +} diff --git a/routeros/resource_tool_netwatch_test.go b/routeros/resource_tool_netwatch_test.go new file mode 100644 index 00000000..aa9a4971 --- /dev/null +++ b/routeros/resource_tool_netwatch_test.go @@ -0,0 +1,59 @@ +package routeros + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +const testToolNetwatch = "routeros_tool_netwatch.test" + +func TestAccToolNetwatchTest_basic(t *testing.T) { + t.Parallel() + for _, name := range testNames { + t.Run(name, func(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testSetTransportEnv(t, name) + }, + ProviderFactories: testAccProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccToolNetwatchConfig(), + Check: resource.ComposeTestCheckFunc( + testResourcePrimaryInstanceId(testToolNetwatch), + resource.TestCheckResourceAttr(testToolNetwatch, "name", "watch-google-pdns"), + resource.TestCheckResourceAttr(testToolNetwatch, "host", "8.8.8.8"), + resource.TestCheckResourceAttr(testToolNetwatch, "interval", "30s"), + resource.TestCheckResourceAttr(testToolNetwatch, "up_script", ":log info \"Ping to 8.8.8.8 successful\""), + ), + }, + { + Config: testAccToolNetwatchConfig(), + Check: resource.ComposeTestCheckFunc( + testResourcePrimaryInstanceId(testToolNetwatch), + resource.TestCheckResourceAttr(testToolNetwatch, "name", "watch-google-pdns"), + resource.TestCheckResourceAttr(testToolNetwatch, "host", "8.8.8.8"), + resource.TestCheckResourceAttr(testToolNetwatch, "interval", "30s"), + resource.TestCheckResourceAttr(testToolNetwatch, "up_script", ":log info \"Ping to 8.8.8.8 successful\""), + ), + }, + }, + }) + + }) + } +} + +func testAccToolNetwatchConfig() string { + return fmt.Sprintf(`%v +resource "routeros_tool_netwatch" "test" { + name = "watch-google-pdns" + host = "8.8.8.8" + interval = "30s" + up_script = ":log info \"Ping to 8.8.8.8 successful\"" +} +`, providerConfig) +} From 279e2ee607e573cb9d92619e6cee2b98caa77e7d Mon Sep 17 00:00:00 2001 From: Vaerh Date: Mon, 5 Aug 2024 13:41:00 +0300 Subject: [PATCH 3/5] chore: Add attributes generation from CSV table Added the ability to create a draft attribute description from a CSV table exported from WIKI MT. --- tools/boilerplate/main.go | 138 +++++++++++++++++++++++++++++++++++++- 1 file changed, 135 insertions(+), 3 deletions(-) diff --git a/tools/boilerplate/main.go b/tools/boilerplate/main.go index 8119588c..0fd0575d 100644 --- a/tools/boilerplate/main.go +++ b/tools/boilerplate/main.go @@ -2,14 +2,16 @@ package main import ( + "bufio" "flag" "fmt" - "html/template" "log" "os" "path/filepath" "regexp" + "strconv" "strings" + "text/template" "unicode" "github.com/fatih/color" @@ -19,6 +21,7 @@ var ( reNewItemName = regexp.MustCompile(`^routeros_[a-z_]+$`) // isDS = flag.Bool("ds", false, "This is a datasource") isSystem = flag.Bool("system", false, "This is a system resource") + csvTable = flag.Bool("table", false, "Extracting attributes from the WIKI table") ) func Fatalf(format string, a ...any) { @@ -63,6 +66,14 @@ func main() { Fatalf("Usage: go run tools/bolerplate/main.go routeros_new_resource") } + if *csvTable { + if _, err := os.Stat(flag.Args()[0]); err != nil { + Fatalf("CSV file %v not found", flag.Args()[0]) + } + extractAttributes(flag.Args()[0]) + os.Exit(0) + } + resName := flag.Args()[0] if !reNewItemName.MatchString(resName) { Fatalf("The resource name must be in the format: 'routeros_[a-z_]+', got '%v'", resName) @@ -80,6 +91,8 @@ func main() { goName := Capitalize(resName) + os.MkdirAll("routeros", os.ModePerm) + // if !*isDS { fName := fmt.Sprintf("%v_%v", Resource.HCL(), strings.TrimPrefix(resName, "routeros_")) f, err := os.OpenFile(filepath.Join("routeros", fName+".go"), os.O_WRONLY|os.O_TRUNC|os.O_CREATE, os.ModePerm) @@ -152,11 +165,16 @@ func main() { } f.Close() - f, err = os.OpenFile(filepath.Join("routeros", "provider.go"), os.O_WRONLY|os.O_APPEND, os.ModePerm) + var flags int = os.O_WRONLY | os.O_APPEND + if _, err := os.Stat(filepath.Join("routeros", "provider.go")); err != nil { + flags |= os.O_CREATE + } + + f, err = os.OpenFile(filepath.Join("routeros", "provider.go"), flags, os.ModePerm) if err != nil { panic(err) } - fmt.Fprintf(f, `"%v": %v(),\n`, resName, Resource.String()+goName) + fmt.Fprintf(f, "\"%v\": %v(),\n", resName, Resource.String()+goName) f.Close() // } } @@ -275,3 +293,117 @@ func Capitalize(s string) (res string) { return } + +var attribute = ` "{{.Attribute}}": { + Type: schema.Type{{.Type}}, + Optional: true, + Description: "{{.Description}}", + {{- if .Slice }} + ValidateFunc: validation.StringInSlice([]string{ "{{.Slice}}" }, false),{{ end }} + {{- if .DiffSuppress }} + DiffSuppressFunc: AlwaysPresentNotUserProvided,{{ end }} + }, +` + +var ( + reCSV = regexp.MustCompile(`(?m)"(.*?)"(?:,|$)`) + reAttrName = regexp.MustCompile(`[a-z-]+`) + reAttrDefault = regexp.MustCompile(`(?m)Default:?\s*(""|\w+)`) + reAttrEnum = regexp.MustCompile(`(?m)\(\s*([\w-| ]+);`) + enumReplacer = strings.NewReplacer(" ", "", `"`, "`", "'", "`", "|", `", "`) +) + +func extractAttributes(filename string) { + tmpl, err := template.New("attr").Parse(attribute) + if err != nil { + panic(err) + } + tmpl.Option() + + file, err := os.Open(filename) + if err != nil { + Fatalf("[extractAttributes] %v", err) + } + defer file.Close() + + w := os.Stdout + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + row := scanner.Text() + rec := reCSV.FindAllStringSubmatch(row, -1) + if len(rec) != 2 { + fmt.Fprintln(w, row) + continue + } + + r1, r2 := rec[0][1], rec[1][1] + + if len(r1) > 0 && r1[0] == '"' { + r1 = r1[1:] + } + if len(r1) > 0 && r1[len(r1)-1] == '"' { + r1 = r1[:len(r1)-1] + } + + if len(r2) > 0 && r2[0] == '"' { + r2 = r2[1:] + } + if len(r2) > 0 && r2[len(r2)-1] == '"' { + r2 = r2[:len(r2)-1] + } + + // [ ["Property", Property] ["Description" Description] ] + if r1 == "Property" && r2 == "Description" { + continue + } + + var diffSuppress bool + attrType := "String" + if res := reAttrDefault.FindStringSubmatch(r1); len(res) > 1 { + switch res[1] { + // src-address (Default:"") + case `""`: + // use-network-apn (yes | no; Default: yes) + case "yes", "no": + attrType = "Bool" + diffSuppress = true + // startup-delay (Default: 5m) + default: + diffSuppress = true + if _, err := strconv.Atoi(res[1]); err == nil { + attrType = "Int" + } + } + } + + var validate string + for _, match := range reAttrEnum.FindAllStringSubmatch(r1, -1) { + validate = enumReplacer.Replace(match[1]) + } + + ww := os.Stdout + + tmpl.Execute(ww, struct { + Attribute string + Type string + Description string + Slice string + DiffSuppress bool + }{ + Attribute: strings.ReplaceAll(reAttrName.FindString(r1), "-", "_"), + Type: attrType, + Description: strings.ReplaceAll(r2, `"`, "`"), + Slice: validate, + DiffSuppress: diffSuppress, + }) + + if r1 == "type" { + os.Exit(0) + } + + if err != nil { + Fatalf("%v", err) + } + } +} From a4c084c39008ab5fa23341fe04fe0b25af150bda Mon Sep 17 00:00:00 2001 From: Vaerh Date: Mon, 5 Aug 2024 13:41:37 +0300 Subject: [PATCH 4/5] feat: Add `routeros_routing_rule` resource Closes #524 --- .../resources/routeros_routing_rule/import.sh | 3 + .../routeros_routing_rule/resource.tf | 5 ++ routeros/provider.go | 1 + routeros/resource_routing_rule.go | 87 +++++++++++++++++++ routeros/resource_routing_rule_test.go | 56 ++++++++++++ 5 files changed, 152 insertions(+) create mode 100644 examples/resources/routeros_routing_rule/import.sh create mode 100644 examples/resources/routeros_routing_rule/resource.tf create mode 100644 routeros/resource_routing_rule.go create mode 100644 routeros/resource_routing_rule_test.go diff --git a/examples/resources/routeros_routing_rule/import.sh b/examples/resources/routeros_routing_rule/import.sh new file mode 100644 index 00000000..3458ca16 --- /dev/null +++ b/examples/resources/routeros_routing_rule/import.sh @@ -0,0 +1,3 @@ +#The ID can be found via API or the terminal +#The command for the terminal is -> :put [/routing/rule get [print show-ids]] +terraform import routeros_routing_rule.test *3 \ No newline at end of file diff --git a/examples/resources/routeros_routing_rule/resource.tf b/examples/resources/routeros_routing_rule/resource.tf new file mode 100644 index 00000000..fb4da2f0 --- /dev/null +++ b/examples/resources/routeros_routing_rule/resource.tf @@ -0,0 +1,5 @@ +resource "routeros_routing_rule" "test" { + dst_address = "192.168.1.0/24" + action = "lookup-only-in-table" + interface = "ether1" +} \ No newline at end of file diff --git a/routeros/provider.go b/routeros/provider.go index 1f5f9798..ba7a9297 100644 --- a/routeros/provider.go +++ b/routeros/provider.go @@ -222,6 +222,7 @@ func Provider() *schema.Provider { "routeros_routing_bgp_template": ResourceRoutingBGPTemplate(), "routeros_routing_filter_rule": ResourceRoutingFilterRule(), "routeros_routing_table": ResourceRoutingTable(), + "routeros_routing_rule": ResourceRoutingRule(), // OSPF "routeros_routing_ospf_instance": ResourceRoutingOspfInstance(), diff --git a/routeros/resource_routing_rule.go b/routeros/resource_routing_rule.go new file mode 100644 index 00000000..87e2bac2 --- /dev/null +++ b/routeros/resource_routing_rule.go @@ -0,0 +1,87 @@ +package routeros + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +/* + { + ".id": "*1", + ".nextid": "*FFFFFFFF", + "action": "lookup", + "disabled": "false", + "dst-address": "2.2.2.0/24", + "inactive": "false", + "interface": "bridge1", + "routing-mark": "main", + "src-address": "1.1.1.1/32", + "table": "main" + } +*/ + +// https://help.mikrotik.com/docs/display/ROS/Policy+Routing +func ResourceRoutingRule() *schema.Resource { + resSchema := map[string]*schema.Schema{ + MetaResourcePath: PropResourcePath("/routing/rule"), + MetaId: PropId(Id), + + "action": { + Type: schema.TypeString, + Optional: true, + Description: "An action to take on the matching packet:drop - silently drop the packet.lookup - perform a " + + "lookup in routing tables.lookup-only-in-table - perform lookup only in the specified routing table " + + "(see table parameter).unreachable - generate ICMP unreachable message and send it back to the source.", + ValidateFunc: validation.StringInSlice([]string{"drop", "lookup", "lookup-only-in-table", "unreachable"}, false), + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + KeyComment: PropCommentRw, + "dst_address": { + Type: schema.TypeString, + Optional: true, + Description: "The destination address of the packet to match.", + }, + KeyDisabled: PropDisabledRw, + KeyInactive: PropInactiveRo, + "interface": { + Type: schema.TypeString, + Optional: true, + Description: "Incoming interface to match.", + }, + "min_prefix": { + Type: schema.TypeInt, + Optional: true, + Description: "Equivalent to Linux IP rule `suppress_prefixlength`. For example to suppress the default route " + + "in the routing decision set the value to 0.", + }, + "routing_mark": { + Type: schema.TypeString, + Optional: true, + Description: "Match specific routing mark.", + }, + "src_address": { + Type: schema.TypeString, + Optional: true, + Description: "The source address of the packet to match.", + }, + "table": { + Type: schema.TypeString, + Optional: true, + Description: "Name of the routing table to use for lookup.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + } + + return &schema.Resource{ + CreateContext: DefaultCreate(resSchema), + ReadContext: DefaultRead(resSchema), + UpdateContext: DefaultUpdate(resSchema), + DeleteContext: DefaultDelete(resSchema), + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: resSchema, + } +} diff --git a/routeros/resource_routing_rule_test.go b/routeros/resource_routing_rule_test.go new file mode 100644 index 00000000..78252737 --- /dev/null +++ b/routeros/resource_routing_rule_test.go @@ -0,0 +1,56 @@ +package routeros + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +const testRoutingRule = "routeros_routing_rule.test" + +func TestAccRoutingRuleTest_basic(t *testing.T) { + t.Parallel() + for _, name := range testNames { + t.Run(name, func(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testSetTransportEnv(t, name) + }, + ProviderFactories: testAccProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccRoutingRuleConfig(), + Check: resource.ComposeTestCheckFunc( + testResourcePrimaryInstanceId(testRoutingRule), + resource.TestCheckResourceAttr(testRoutingRule, "dst_address", "192.168.1.0/24"), + resource.TestCheckResourceAttr(testRoutingRule, "action", "lookup-only-in-table"), + resource.TestCheckResourceAttr(testRoutingRule, "interface", "ether1"), + ), + }, + { + Config: testAccRoutingRuleConfig(), + Check: resource.ComposeTestCheckFunc( + testResourcePrimaryInstanceId(testRoutingRule), + resource.TestCheckResourceAttr(testRoutingRule, "dst_address", "192.168.1.0/24"), + resource.TestCheckResourceAttr(testRoutingRule, "action", "lookup-only-in-table"), + resource.TestCheckResourceAttr(testRoutingRule, "interface", "ether1"), + ), + }, + }, + }) + + }) + } +} + +func testAccRoutingRuleConfig() string { + return fmt.Sprintf(`%v +resource "routeros_routing_rule" "test" { + dst_address = "192.168.1.0/24" + action = "lookup-only-in-table" + interface = "ether1" +} +`, providerConfig) +} From fcc12d62056ed0151c987d30b5361615dfd1b78c Mon Sep 17 00:00:00 2001 From: Vaerh Date: Mon, 5 Aug 2024 13:46:40 +0300 Subject: [PATCH 5/5] docs: Example for `routeros_interface_lte` --- examples/resources/routeros_interface_lte/resource.tf | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/examples/resources/routeros_interface_lte/resource.tf b/examples/resources/routeros_interface_lte/resource.tf index cba6d4ee..8ec240dc 100644 --- a/examples/resources/routeros_interface_lte/resource.tf +++ b/examples/resources/routeros_interface_lte/resource.tf @@ -1,3 +1,12 @@ resource "routeros_interface_lte" "test" { + allow_roaming = false + apn_profiles = "default" + band = [] + default_name = "lte1" + disabled = false + mtu = "1500" + name = "lte1" + network_mode = ["3g", "lte"] + sms_protocol = null } \ No newline at end of file