From 6477fcdc61d5769a685fb32e551b41609f6f6aa6 Mon Sep 17 00:00:00 2001 From: Vaerh Date: Wed, 3 May 2023 21:45:16 +0300 Subject: [PATCH] feat: Add OpenVPN Server support --- .../routeros_interface_ovpn_server/import.sh | 3 + .../resource.tf | 5 + .../routeros_openvpn_server/import.sh | 1 + .../routeros_openvpn_server/resource.tf | 52 ++++ .../resources/routeros_ppp_profile/import.sh | 3 + .../routeros_ppp_profile/resource.tf | 6 + .../resources/routeros_ppp_secret/import.sh | 3 + .../resources/routeros_ppp_secret/resource.tf | 5 + routeros/provider.go | 8 + routeros/resource_interface_ovpn_server.go | 67 ++++ routeros/resource_ovpn_server.go | 187 ++++++++++++ routeros/resource_ovpn_server_test.go | 94 ++++++ routeros/resource_ppp_profile.go | 289 ++++++++++++++++++ routeros/resource_ppp_profile_test.go | 62 ++++ routeros/resource_ppp_secret.go | 143 +++++++++ routeros/resource_ppp_secret_test.go | 60 ++++ 16 files changed, 988 insertions(+) create mode 100644 examples/resources/routeros_interface_ovpn_server/import.sh create mode 100644 examples/resources/routeros_interface_ovpn_server/resource.tf create mode 100644 examples/resources/routeros_openvpn_server/import.sh create mode 100644 examples/resources/routeros_openvpn_server/resource.tf create mode 100644 examples/resources/routeros_ppp_profile/import.sh create mode 100644 examples/resources/routeros_ppp_profile/resource.tf create mode 100644 examples/resources/routeros_ppp_secret/import.sh create mode 100644 examples/resources/routeros_ppp_secret/resource.tf create mode 100644 routeros/resource_interface_ovpn_server.go create mode 100644 routeros/resource_ovpn_server.go create mode 100644 routeros/resource_ovpn_server_test.go create mode 100644 routeros/resource_ppp_profile.go create mode 100644 routeros/resource_ppp_profile_test.go create mode 100644 routeros/resource_ppp_secret.go create mode 100644 routeros/resource_ppp_secret_test.go diff --git a/examples/resources/routeros_interface_ovpn_server/import.sh b/examples/resources/routeros_interface_ovpn_server/import.sh new file mode 100644 index 00000000..2ce8fce0 --- /dev/null +++ b/examples/resources/routeros_interface_ovpn_server/import.sh @@ -0,0 +1,3 @@ +#The ID can be found via API or the terminal +#The command for the terminal is -> :put [/interface/ovpn-server get [print show-ids]] +terraform import routeros_interface_ovpn_server.user1 *29 \ No newline at end of file diff --git a/examples/resources/routeros_interface_ovpn_server/resource.tf b/examples/resources/routeros_interface_ovpn_server/resource.tf new file mode 100644 index 00000000..59994b1b --- /dev/null +++ b/examples/resources/routeros_interface_ovpn_server/resource.tf @@ -0,0 +1,5 @@ +resource "routeros_interface_ovpn_server" "user1" { + name = "ovpn-in1" + user = "user1" + depends_on = [routeros_ovpn_server.server] +} diff --git a/examples/resources/routeros_openvpn_server/import.sh b/examples/resources/routeros_openvpn_server/import.sh new file mode 100644 index 00000000..60abcdf2 --- /dev/null +++ b/examples/resources/routeros_openvpn_server/import.sh @@ -0,0 +1 @@ +terraform import routeros_openvpn_server.server . \ No newline at end of file diff --git a/examples/resources/routeros_openvpn_server/resource.tf b/examples/resources/routeros_openvpn_server/resource.tf new file mode 100644 index 00000000..149fd3ab --- /dev/null +++ b/examples/resources/routeros_openvpn_server/resource.tf @@ -0,0 +1,52 @@ +resource "routeros_ip_pool" "ovpn-pool" { + name = "ovpn-pool" + ranges = ["192.168.77.2-192.168.77.254"] +} + +resource "routeros_system_certificate" "ovpn_ca" { + name = "OpenVPN-Root-CA" + common_name = "OpenVPN Root CA" + key_size = "prime256v1" + key_usage = ["key-cert-sign", "crl-sign"] + trusted = true + sign { + } +} + +resource "routeros_system_certificate" "ovpn_server_crt" { + name = "OpenVPN-Server-Certificate" + common_name = "Mikrotik OpenVPN" + key_size = "prime256v1" + key_usage = ["digital-signature", "key-encipherment", "tls-server"] + sign { + ca = routeros_system_certificate.ovpn_ca.name + } +} + +resource "routeros_ppp_profile" "test" { + name = "ovpn" + local_address = "192.168.77.1" + remote_address = "ovpn-pool" + use_upnp = "no" +} + +resource "routeros_ppp_secret" "test" { + name = "user-test" + password = "123" + profile = routeros_ppp_profile.test.name +} + +resource "routeros_ovpn_server" "server" { + enabled = true + certificate = routeros_system_certificate.ovpn_server_crt.name + auth = "sha256,sha512" + tls_version = "only-1.2" + default_profile = routeros_ppp_profile.test.name +} + +# The resource should be created only after the OpenVPN server is enabled! +resource "routeros_interface_ovpn_server" "user1" { + name = "ovpn-in1" + user = "user1" + depends_on = [routeros_ovpn_server.server] +} diff --git a/examples/resources/routeros_ppp_profile/import.sh b/examples/resources/routeros_ppp_profile/import.sh new file mode 100644 index 00000000..1855dea8 --- /dev/null +++ b/examples/resources/routeros_ppp_profile/import.sh @@ -0,0 +1,3 @@ +#The ID can be found via API or the terminal +#The command for the terminal is -> :put [/ppp/profile get [print show-ids]] +terraform import routeros_ppp_profile.test *6 \ No newline at end of file diff --git a/examples/resources/routeros_ppp_profile/resource.tf b/examples/resources/routeros_ppp_profile/resource.tf new file mode 100644 index 00000000..bfc8300a --- /dev/null +++ b/examples/resources/routeros_ppp_profile/resource.tf @@ -0,0 +1,6 @@ +resource "routeros_ppp_profile" "test" { + name = "ovpn" + local_address = "192.168.77.1" + remote_address = "ovpn-pool" + use_upnp = "no" +} \ No newline at end of file diff --git a/examples/resources/routeros_ppp_secret/import.sh b/examples/resources/routeros_ppp_secret/import.sh new file mode 100644 index 00000000..f540e3ca --- /dev/null +++ b/examples/resources/routeros_ppp_secret/import.sh @@ -0,0 +1,3 @@ +#The ID can be found via API or the terminal +#The command for the terminal is -> :put [/ppp/secret get [print show-ids]] +terraform import routeros_ppp_secret.test *6 \ No newline at end of file diff --git a/examples/resources/routeros_ppp_secret/resource.tf b/examples/resources/routeros_ppp_secret/resource.tf new file mode 100644 index 00000000..2414d6fc --- /dev/null +++ b/examples/resources/routeros_ppp_secret/resource.tf @@ -0,0 +1,5 @@ +resource "routeros_ppp_secret" "test" { + name = "user-test" + password = "123" + profile = "default" +} diff --git a/routeros/provider.go b/routeros/provider.go index e90d2d5c..e5b6d282 100644 --- a/routeros/provider.go +++ b/routeros/provider.go @@ -105,6 +105,7 @@ func Provider() *schema.Provider { "routeros_interface_wireguard_peer": ResourceInterfaceWireguardPeer(), "routeros_interface_list": ResourceInterfaceList(), "routeros_interface_list_member": ResourceInterfaceListMember(), + "routeros_interface_ovpn_server": ResourceInterfaceOpenVPNServer(), // Aliases for interface objects to retain compatibility between original and fork "routeros_bridge": ResourceInterfaceBridge(), @@ -143,6 +144,13 @@ func Provider() *schema.Provider { // Routing tables "routeros_routing_table": ResourceRoutingTable(), + + // VPN + "routeros_ovpn_server": ResourceOpenVPNServer(), + + // PPP + "routeros_ppp_profile": ResourcePPPProfile(), + "routeros_ppp_secret": ResourcePPPSecret(), }, DataSourcesMap: map[string]*schema.Resource{ "routeros_interfaces": DatasourceInterfaces(), diff --git a/routeros/resource_interface_ovpn_server.go b/routeros/resource_interface_ovpn_server.go new file mode 100644 index 00000000..ffe29ff4 --- /dev/null +++ b/routeros/resource_interface_ovpn_server.go @@ -0,0 +1,67 @@ +package routeros + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +/* + { + ".id": "*1D", + "comment": "comment", + "client-address": "172.18.0.2", + "disabled": "false", + "encoding": "BF-128-CBC/SHA256", + "mtu": "1500", + "name": "ovpn-in1", + "running": "true", + "uptime": "1m25s", + "user": "user1" + } +*/ + +// https://help.mikrotik.com/docs/display/ROS/??? +func ResourceInterfaceOpenVPNServer() *schema.Resource { + resSchema := map[string]*schema.Schema{ + MetaResourcePath: PropResourcePath("/interface/ovpn-server"), + MetaId: PropId(Id), + + "client_address": { + Type: schema.TypeString, + Computed: true, + Description: "The address of the remote side.", + }, + KeyComment: PropCommentRw, + KeyDisabled: PropDisabledRw, + "encoding": { + Type: schema.TypeString, + Computed: true, + Description: "Encryption characteristics.", + }, + KeyMtu: PropL2MtuRo, + KeyName: PropName("Interface name (Example: ovpn-in1)."), + KeyRunning: PropRunningRo, + "uptime": { + Type: schema.TypeString, + Computed: true, + Description: "Connection uptime.", + }, + "user": { + Type: schema.TypeString, + Optional: true, + Description: "User name used for authentication.", + }, + } + + 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_ovpn_server.go b/routeros/resource_ovpn_server.go new file mode 100644 index 00000000..d9db0422 --- /dev/null +++ b/routeros/resource_ovpn_server.go @@ -0,0 +1,187 @@ +package routeros + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +/* + { + "auth": "sha1,md5,sha256,sha512", + "certificate": "root-cert", + "cipher": "blowfish128,aes128-cbc", + "default-profile": "default", + "enable-tun-ipv6": "false", + "enabled": "true", + "ipv6-prefix-len": "64", + "keepalive-timeout": "60", + "mac-address": "FE:01:63:24:35:19", + "max-mtu": "1500", + "mode": "ip", + "netmask": "24", + "port": "1194", + "protocol": "tcp", + "redirect-gateway": "disabled", + "reneg-sec": "3600", + "require-client-certificate": "true", + "tls-version": "only-1.2", + "tun-server-ipv6": "::" + } +*/ + +// https://help.mikrotik.com/docs/display/ROS/OpenVPN +func ResourceOpenVPNServer() *schema.Resource { + resSchema := map[string]*schema.Schema{ + MetaResourcePath: PropResourcePath("/interface/ovpn-server/server"), + MetaId: PropId(Id), + + "auth": { + Type: schema.TypeString, + Optional: true, + Default: "sha1,md5,sha256,sha512", + Description: "Authentication methods that the server will accept.", + ValidateDiagFunc: ValidationMultiValInSlice([]string{"md5", "sha1", "null", "sha256", "sha512"}, false, false), + }, + "certificate": { + Type: schema.TypeString, + Required: true, + Description: "Name of the certificate that the OVPN server will use.", + }, + "cipher": { + Type: schema.TypeString, + Optional: true, + Default: "blowfish128,aes128-cbc", + Description: "Allowed ciphers.", + ValidateDiagFunc: ValidationMultiValInSlice([]string{ + "null", "aes128-cbc", "aes128-gcm", "aes192-cbc", "aes192-gcm", "aes256-cbc", "aes256-gcm", "blowfish128", + }, false, false), + }, + "default_profile": { + Type: schema.TypeString, + Optional: true, + Default: "default", + Description: "Default profile to use.", + }, + "enable_tun_ipv6": { + Type: schema.TypeBool, + Optional: true, + Description: "Specifies if IPv6 IP tunneling mode should be possible with this OVPN server.", + }, + "enabled": { + Type: schema.TypeBool, + Optional: true, + Description: "Defines whether the OVPN server is enabled or not.", + }, + "ipv6_prefix_len": { + Type: schema.TypeInt, + Optional: true, + Default: 64, + Description: "Length of IPv6 prefix for IPv6 address which will be used when generating OVPN interface " + + "on the server side.", + ValidateFunc: validation.IntBetween(1, 128), + }, + "keepalive_timeout": { + Type: schema.TypeString, + Optional: true, + Default: "60", + Description: "Defines the time period (in seconds) after which the router is starting to send " + + "keepalive packets every second. If no traffic and no keepalive responses have come for " + + "that period of time (i.e. 2 * keepalive-timeout), not responding client is proclaimed " + + "disconnected", + DiffSuppressFunc: TimeEquall, + }, + "mac_address": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "Automatically generated MAC address of the server.", + }, + "max_mtu": { + Type: schema.TypeInt, + Optional: true, + Default: 1500, + Description: "Maximum Transmission Unit. Max packet size that the OVPN interface will be able to send " + + "without packet fragmentation.", + ValidateFunc: validation.IntBetween(64, 65535), + }, + "mode": { + Type: schema.TypeString, + Optional: true, + Default: "ip", + Description: "Layer3 or layer2 tunnel mode (alternatively tun, tap)", + ValidateFunc: validation.StringInSlice([]string{"ip", "ethernet"}, false), + }, + "netmask": { + Type: schema.TypeInt, + Optional: true, + Default: 24, + Description: "Subnet mask to be applied to the client.", + ValidateFunc: validation.IntBetween(0, 32), + }, + "port": { + Type: schema.TypeInt, + Optional: true, + Default: 1194, + Description: "Port to run the server on.", + ValidateFunc: validation.IntBetween(1, 65535), + }, + "protocol": { + Type: schema.TypeString, + Optional: true, + Default: "tcp", + Description: "indicates the protocol to use when connecting with the remote endpoint.", + ValidateFunc: validation.StringInSlice([]string{"tcp", "udp"}, false), + }, + "redirect_gateway": { + Type: schema.TypeString, + Optional: true, + Default: "", + Description: "Specifies what kind of routes the OVPN client must add to the routing table. def1 – Use " + + "this flag to override the default gateway by using 0.0.0.0/1 and 128.0.0.0/1 rather " + + "than 0.0.0.0/0. This has the benefit of overriding but not wiping out the original " + + "default gateway. disabled - Do not send redirect-gateway flags to the OVPN client. ipv6 " + + "- Redirect IPv6 routing into the tunnel on the client side. This works similarly to the " + + "def1 flag, that is, more specific IPv6 routes are added (2000::/4 and 3000::/4), " + + "covering the whole IPv6 unicast space.", + ValidateDiagFunc: ValidationMultiValInSlice([]string{"def1", "disabled", "ipv6"}, false, false), + }, + "reneg_sec": { + Type: schema.TypeInt, + Optional: true, + Default: 3600, + Description: "Renegotiate data channel key after n seconds (default=3600).", + }, + "require_client_certificate": { + Type: schema.TypeBool, + Optional: true, + Description: "If set to yes, then the server checks whether the client's certificate belongs to the " + + "same certificate chain.", + }, + "tls_version": { + Type: schema.TypeString, + Optional: true, + Description: "Specifies which TLS versions to allow.", + ValidateFunc: validation.StringInSlice([]string{"any", "only-1.2"}, false), + }, + "tun_server_ipv6": { + Type: schema.TypeString, + Optional: true, + Default: "::", + Description: "IPv6 prefix address which will be used when generating the OVPN interface on the server " + + "side.", + }, + } + + return &schema.Resource{ + CreateContext: DefaultSystemCreate(resSchema), + ReadContext: DefaultSystemRead(resSchema), + UpdateContext: DefaultSystemUpdate(resSchema), + DeleteContext: DefaultSystemDelete(resSchema), + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: resSchema, + } +} diff --git a/routeros/resource_ovpn_server_test.go b/routeros/resource_ovpn_server_test.go new file mode 100644 index 00000000..a5bc7dd7 --- /dev/null +++ b/routeros/resource_ovpn_server_test.go @@ -0,0 +1,94 @@ +package routeros + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +const testOpenVPNServer = "routeros_ovpn_server.server" +const testInterfaceOpenVPNServer = "routeros_interface_ovpn_server.user1" + +func TestAccOpenVPNServerTest_basic(t *testing.T) { + 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: testAccOpenVPNServerConfig(), + Check: resource.ComposeTestCheckFunc( + testAccCheckOpenVPNServerExists(testOpenVPNServer), + testAccCheckOpenVPNServerExists(testInterfaceOpenVPNServer), + resource.TestCheckResourceAttr(testOpenVPNServer, "id", "interface.ovpn-server.server"), + resource.TestCheckResourceAttr(testInterfaceOpenVPNServer, "name", "ovpn-in1"), + resource.TestCheckResourceAttr(testInterfaceOpenVPNServer, "user", "user1"), + ), + }, + }, + }) + + }) + } +} + +func testAccCheckOpenVPNServerExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("not found: %s", name) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("no id is set") + } + + return nil + } +} + +// Complex test for OpenVPN server resources. +func testAccOpenVPNServerConfig() string { + return providerConfig + ` + resource "routeros_system_certificate" "ovpn_ca" { + name = "OpenVPN-Root-CA" + common_name = "OpenVPN Root CA" + key_size = "prime256v1" + key_usage = ["key-cert-sign", "crl-sign"] + trusted = true + sign { + } + } + + resource "routeros_system_certificate" "ovpn_server_crt" { + name = "OpenVPN-Server-Certificate" + common_name = "Mikrotik OpenVPN" + key_size = "prime256v1" + key_usage = ["digital-signature", "key-encipherment", "tls-server"] + sign { + ca = routeros_system_certificate.ovpn_ca.name + } + } + + resource "routeros_ovpn_server" "server" { + enabled = false + certificate = routeros_system_certificate.ovpn_server_crt.name + auth = "sha256,sha512" + redirect_gateway = "def1,ipv6" + tls_version = "only-1.2" + } + + # The resource should be created only after the OpenVPN server is enabled! + resource "routeros_interface_ovpn_server" "user1" { + name = "ovpn-in1" + user = "user1" + depends_on = [routeros_ovpn_server.server] + } +` +} diff --git a/routeros/resource_ppp_profile.go b/routeros/resource_ppp_profile.go new file mode 100644 index 00000000..a7407dab --- /dev/null +++ b/routeros/resource_ppp_profile.go @@ -0,0 +1,289 @@ +package routeros + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +/* + { + ".id": "*1", + "address-list": "", + "bridge-learning": "default", + "change-tcp-mss": "default", + "default": "false", + "insert-queue-before": "first", + "local-address": "192.168.77.1", + "name": "ovpn", + "on-down": "", + "on-up": "", + "only-one": "default", + "parent-queue": "none", + "queue-type": "multi-queue-ethernet-default", + "remote-address": "*2", + "use-compression": "default", + "use-encryption": "default", + "use-ipv6": "yes", + "use-mpls": "default", + "use-upnp": "default" + } +*/ + +// https://help.mikrotik.com/docs/display/ROS/PPP+AAA#PPPAAA-UserProfiles +func ResourcePPPProfile() *schema.Resource { + resSchema := map[string]*schema.Schema{ + MetaResourcePath: PropResourcePath("/ppp/profile"), + MetaId: PropId(Id), + + "address_list": { + Type: schema.TypeString, + Optional: true, + Description: "Address list name to which ppp assigned (on server) or received (on client) address will " + + "be added.", + }, + "bridge": { + Type: schema.TypeString, + Optional: true, + Description: "Name of the bridge interface to which ppp interface will be added as a slave port. Both " + + "tunnel endpoints (server and client) must be in bridge in order to make this work, see " + + "more details on the BCP bridging manual.", + }, + "bridge_horizon": { + Type: schema.TypeInt, + Optional: true, + Description: "Used split-horizon value for the dynamically created bridge port. Can be used to " + + "prevent bridging loops and isolate traffic. Set the same value for a group of ports, to " + + "prevent them from sending data to ports with the same horizon value.", + ValidateFunc: validation.IntAtLeast(1), + }, + "bridge_learning": { + Type: schema.TypeString, + Optional: true, + Default: "default", + Description: "Changes MAC learning behavior on the dynamically created bridge port: yes - enables MAC " + + "learning no - disables MAC learning default - derive this value from the interface " + + "default profile; same as yes if this is the interface default profile.", + ValidateFunc: validation.StringInSlice([]string{"default", "no", "yes"}, false), + }, + "bridge_path_cost": { + Type: schema.TypeInt, + Optional: true, + Description: "Used path cost for the dynamically created bridge port, used by STP/RSTP to determine " + + "the best path, used by MSTP to determine the best path between regions. This property " + + "has no effect when a bridge protocol-mode is set to none.", + ValidateFunc: validation.IntAtLeast(0), + }, + "bridge_port_priority": { + Type: schema.TypeInt, + Optional: true, + Description: "Used priority for the dynamically created bridge port, used by STP/RSTP to determine " + + "the root port, used by MSTP to determine root port between regions. This property has " + + "no effect when a bridge protocol-mode is set to none.", + ValidateFunc: validation.IntBetween(0, 240), + }, + "change_tcp_mss": { + Type: schema.TypeString, + Optional: true, + Default: "default", + Description: "Modifies connection MSS settings (applies only for IPv4): yes - adjust connection MSS " + + "value no - do not adjust connection MSS value default - derive this value from the " + + "interface default profile; same as no if this is the interface default profile.", + ValidateFunc: validation.StringInSlice([]string{"yes", "no", "default"}, false), + }, + KeyComment: PropCommentRw, + "default": { + Type: schema.TypeString, + Computed: true, + Description: "Default profile sign.", + }, + "dhcpv6_pd_pool": { + Type: schema.TypeString, + Optional: true, + Description: "Name of the IPv6 pool which will be used by dynamically created DHCPv6-PD server when " + + "client connects. [Read more >>](https://wiki.mikrotik.com/wiki/Manual:IPv6_PD_over_PPP)", + }, + "dns_server": { + Type: schema.TypeSet, + Optional: true, + Description: "IP address of the DNS server that is supplied to ppp clients.", + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.IsIPv4Address, + }, + MaxItems: 2, + }, + "idle_timeout": { + Type: schema.TypeString, + Optional: true, + Description: "Specifies the amount of time after which the link will be terminated if there are no " + + "activity present. Timeout is not set by default.", + DiffSuppressFunc: TimeEquall, + }, + "incoming_filter": { + Type: schema.TypeString, + Optional: true, + Description: "Firewall chain name for incoming packets. Specified chain gets control for each packet " + + "coming from the client. The ppp chain should be manually added and rules with " + + "action=jump jump-target=ppp should be added to other relevant chains in order for this " + + "feature to work. For more information look at the examples section.", + }, + "insert_queue_before": { + Type: schema.TypeString, + Optional: true, + Description: "Specify where to place dynamic simple queue entries for static DCHP leases with rate-limit " + + "parameter set.", + }, + "interface_list": { + Type: schema.TypeString, + Optional: true, + Description: "Interface list name.", + }, + "local_address": { + Type: schema.TypeString, + Optional: true, + Description: "Tunnel address or name of the pool from which address is assigned to ppp interface locally.", + }, + KeyName: PropName("PPP profile name."), + "on_up": { + Type: schema.TypeString, + Optional: true, + Description: "Execute script on user login-event. These are available variables that are accessible " + + "for the event script: *user *local-address *remote-address *caller-id *called-id *interface.", + }, + "on_down": { + Type: schema.TypeString, + Optional: true, + Description: "Execute script on user logging off. See on-up for more details.", + }, + "only_one": { + Type: schema.TypeString, + Optional: true, + Default: "default", + Description: "Defines whether a user is allowed to have more than one ppp session at a time yes - a " + + "user is not allowed to have more than one ppp session at a time no - the user is allowed " + + "to have more than one ppp session at a time default - derive this value from the " + + "interface default profile; same as no if this is the interface default profile.", + ValidateFunc: validation.StringInSlice([]string{"yes", "no", "default"}, false), + }, + "outgoing_filter": { + Type: schema.TypeString, + Optional: true, + Description: "Firewall chain name for outgoing packets. The specified chain gets control for each " + + "packet going to the client. The PPP chain should be manually added and rules with " + + "action=jump jump-target=ppp should be added to other relevant chains in order for this " + + "feature to work. For more information look at the Examples section.", + }, + "parent_queue": { + Type: schema.TypeString, + Optional: true, + Description: "Name of parent simple queue.", + }, + "queue_type": { + Type: schema.TypeString, + Optional: true, + Description: "Queue types.", + }, + "rate_limit": { + Type: schema.TypeString, + Optional: true, + Description: "Rate limitation in form of rx-rate[/tx-rate] [rx-burst-rate[/tx-burst-rate] " + + "[rx-burst-threshold[/tx-burst-threshold] [rx-burst-time[/tx-burst-time] [priority] " + + "[rx-rate-min[/tx-rate-min]]]] from the point of view of the router (so 'rx' is client " + + "upload, and 'tx' is client download). All rates are measured in bits per second, " + + "unless followed by optional 'k' suffix (kilobits per second) or 'M' suffix (megabits " + + "per second). If tx-rate is not specified, rx-rate serves as tx-rate too. The same " + + "applies for tx-burst-rate, tx-burst-threshold and tx-burst-time. If both " + + "rx-burst-threshold and tx-burst-threshold are not specified (but burst-rate is " + + "specified), rx-rate and tx-rate are used as burst thresholds. If both rx-burst-time " + + "and tx-burst-time are not specified, 1s is used as default. Priority takes values 1..8, " + + "where 1 implies the highest priority, but 8 - the lowest. If rx-rate-min and " + + "tx-rate-min are not specified rx-rate and tx-rate values are used. The rx-rate-min and " + + "tx-rate-min values can not exceed rx-rate and tx-rate values.", + }, + "remote_address": { + Type: schema.TypeString, + Optional: true, + Description: "Tunnel address or name of the pool from which address is assigned to remote ppp interface.", + }, + "remote_ipv6_prefix_pool": { + Type: schema.TypeString, + Optional: true, + Description: "Assign prefix from IPv6 pool to the client and install corresponding IPv6 route.", + }, + "session_timeout": { + Type: schema.TypeString, + Optional: true, + Description: "Maximum time the connection can stay up. By default no time limit is set.", + DiffSuppressFunc: TimeEquall, + }, + "use_compression": { + Type: schema.TypeString, + Optional: true, + Default: "default", + Description: "Specifies whether to use data compression or not. yes - enable data compression no - " + + "disable data compression default - derive this value from the interface default profile; " + + "same as no if this is the interface default profile This setting does not affect OVPN " + + "tunnels.", + ValidateFunc: validation.StringInSlice([]string{"yes", "no", "default"}, false), + }, + "use_encryption": { + Type: schema.TypeString, + Optional: true, + Default: "default", + Description: "Specifies whether to use data encryption or not. yes - enable data encryption no - " + + "disable data encryption default - derive this value from the interface default profile; " + + "same as no if this is the interface default profile require - explicitly requires " + + "encryption This setting does not work on OVPN and SSTP tunnels.", + ValidateFunc: validation.StringInSlice([]string{"yes", "no", "default", "require"}, false), + }, + "use_ipv6": { + Type: schema.TypeString, + Optional: true, + Default: "default", + Description: "Specifies whether to allow IPv6. By default is enabled if IPv6 package is installed. yes " + + "- enable IPv6 support no - disable IPv6 support default - derive this value from the " + + "interface default profile; same as no if this is the interface default profile require - " + + "explicitly requires IPv6 support.", + ValidateFunc: validation.StringInSlice([]string{"yes", "no", "default", "require"}, false), + }, + "use_mpls": { + Type: schema.TypeString, + Optional: true, + Default: "default", + Description: "Specifies whether to allow MPLS over PPP. yes - enable MPLS support no - disable MPLS " + + "support default - derive this value from the interface default profile; same as no if " + + "this is the interface default profile require - explicitly requires MPLS support", + ValidateFunc: validation.StringInSlice([]string{"yes", "no", "default", "require"}, false), + }, + "use_upnp": { + Type: schema.TypeString, + Optional: true, + Default: "default", + Description: "Specifies whether to allow UPnP.", + ValidateFunc: validation.StringInSlice([]string{"yes", "no", "default"}, false), + }, + "wins_server": { + Type: schema.TypeSet, + Optional: true, + Description: "IP address of the WINS server to supply to Windows clients.", + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.IsIPv4Address, + }, + MaxItems: 2, + }, + } + + 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_ppp_profile_test.go b/routeros/resource_ppp_profile_test.go new file mode 100644 index 00000000..9c06df3f --- /dev/null +++ b/routeros/resource_ppp_profile_test.go @@ -0,0 +1,62 @@ +package routeros + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +const testPPPProfile = "routeros_ppp_profile.test" + +func TestAccPPPProfileTest_basic(t *testing.T) { + 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, + CheckDestroy: testCheckResourceDestroy("/ppp/profile", "routeros_ppp_profile"), + Steps: []resource.TestStep{ + { + Config: testAccPPPProfileConfig(), + Check: resource.ComposeTestCheckFunc( + testAccCheckPPPProfileExists(testPPPProfile), + resource.TestCheckResourceAttr(testPPPProfile, "name", "profile1"), + ), + }, + }, + }) + + }) + } +} + +func testAccCheckPPPProfileExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("not found: %s", name) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("no id is set") + } + + return nil + } +} + +func testAccPPPProfileConfig() string { + return providerConfig + ` + resource "routeros_ppp_profile" "test" { + name = "profile1" + rate_limit = "10M/200k" + use_upnp = "no" + dns_server = ["8.8.8.8", "1.1.1.1"] + } +` +} diff --git a/routeros/resource_ppp_secret.go b/routeros/resource_ppp_secret.go new file mode 100644 index 00000000..e6f37318 --- /dev/null +++ b/routeros/resource_ppp_secret.go @@ -0,0 +1,143 @@ +package routeros + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +/* + { + ".id": "*1", + "caller-id": "", + "disabled": "false", + >>>"ipv6-routes": "", + "last-caller-id": "172.18.0.2", + "last-disconnect-reason": "hung-up", + "last-logged-out": "may/01/2023 20:52:13", + "limit-bytes-in": "0", + "limit-bytes-out": "0", + "local-address": "172.18.0.2", + "name": "user1", + "password": "1", + "profile": "ovpn", + "routes": "", + "service": "ovpn" + } +*/ + +// https://help.mikrotik.com/docs/display/ROS/PPP+AAA#PPPAAA-UserDatabase +func ResourcePPPSecret() *schema.Resource { + resSchema := map[string]*schema.Schema{ + MetaResourcePath: PropResourcePath("/ppp/secret"), + MetaId: PropId(Id), + + "caller_id": { + Type: schema.TypeString, + Optional: true, + Description: "For PPTP and L2TP it is the IP address a client must connect from. For PPPoE it is the " + + "MAC address (written in CAPITAL letters) a client must connect from. For ISDN it is the " + + "caller's number (that may or may not be provided by the operator) the client may " + + "dial-in from.", + }, + KeyComment: PropCommentRw, + KeyDisabled: PropDisabledRw, + "ipv6_routes": { + Type: schema.TypeSet, + Optional: true, + Description: "IPv6 routes.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "last_caller_id": { + Type: schema.TypeString, + Computed: true, + }, + "last_disconnect_reason": { + Type: schema.TypeString, + Computed: true, + }, + "last_logged_out": { + Type: schema.TypeString, + Computed: true, + }, + "limit_bytes_in": { + Type: schema.TypeInt, + Optional: true, + Default: 0, + Description: "Maximal amount of bytes for a session that client can upload.", + }, + "limit_bytes_out": { + Type: schema.TypeInt, + Optional: true, + Default: 0, + Description: "Maximal amount of bytes for a session that client can download.", + }, + "local_address": { + Type: schema.TypeString, + Optional: true, + Description: "IP address that will be set locally on ppp interface.", + ValidateFunc: validation.IsIPv4Address, + }, + KeyName: PropName("Name used for authentication."), + "password": { + Type: schema.TypeString, + Optional: true, + Description: "Password used for authentication.", + Sensitive: true, + }, + "profile": { + Type: schema.TypeString, + Optional: true, + Default: "default", + Description: "Which user profile to use.", + }, + "remote_address": { + Type: schema.TypeString, + Optional: true, + Description: "IP address that will be assigned to remote ppp interface.", + ValidateFunc: validation.IsIPv4Address, + }, + "remote_ipv6_prefix": { + Type: schema.TypeString, + Optional: true, + Description: "IPv6 prefix assigned to ppp client. Prefix is added to ND prefix list enabling stateless " + + "address auto-configuration on ppp interface.Available starting from v5.0.", + }, + "routes": { + Type: schema.TypeSet, + Optional: true, + Description: "Routes that appear on the server when the client is connected. The route format is: " + + "dst-address gateway metric (for example, 10.1.0.0/ 24 10.0.0.1 1). Other syntax is not " + + "acceptable since it can be represented in incorrect way. Several routes may be " + + "specified separated with commas. This parameter will be ignored for OpenVPN.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + // The ROS 7.8 version does not contain the isdn option. + "service": { + Type: schema.TypeString, + Optional: true, + Default: "any", + Description: "Specifies the services that particular user will be able to use.", + ValidateFunc: validation.StringInSlice( + []string{"any", "async", "isdn", "l2tp", "pppoe", "pptp", "ovpn", "sstp"}, + false, + ), + }, + } + + 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_ppp_secret_test.go b/routeros/resource_ppp_secret_test.go new file mode 100644 index 00000000..119015d0 --- /dev/null +++ b/routeros/resource_ppp_secret_test.go @@ -0,0 +1,60 @@ +package routeros + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +const testPPPSecret = "routeros_ppp_secret.test" + +func TestAccPPPSecretTest_basic(t *testing.T) { + 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, + CheckDestroy: testCheckResourceDestroy("/ppp/secret", "routeros_ppp_secret"), + Steps: []resource.TestStep{ + { + Config: testAccPPPSecretConfig(), + Check: resource.ComposeTestCheckFunc( + testAccCheckPPPSecretExists(testPPPSecret), + resource.TestCheckResourceAttr(testPPPSecret, "name", "user-test"), + ), + }, + }, + }) + + }) + } +} + +func testAccCheckPPPSecretExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("not found: %s", name) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("no id is set") + } + + return nil + } +} + +func testAccPPPSecretConfig() string { + return providerConfig + ` + resource "routeros_ppp_secret" "test" { + name = "user-test" + password = "12345678" + } +` +}